From abf3113476de4d7471ef83cbdf3f3c1df1a56174 Mon Sep 17 00:00:00 2001 From: Will Franklin Date: Fri, 19 Jan 2024 17:43:12 +0000 Subject: [PATCH] First pass at generating API docs --- package-lock.json | 69 ++++++++++++++++++- package.json | 2 + src/api/app.ts | 36 +--------- src/api/controllers/ApiKeyController.ts | 9 ++- src/api/controllers/CalloutController.ts | 22 ++++-- .../CalloutResponseCommentController.ts | 9 ++- .../controllers/CalloutResponseController.ts | 6 +- src/api/controllers/ContactController.ts | 30 +++++--- src/api/controllers/ContentController.ts | 3 +- src/api/controllers/EmailController.ts | 15 ++-- src/api/controllers/NoticeController.ts | 9 ++- src/api/controllers/PaymentController.ts | 7 +- src/api/controllers/SegmentController.ts | 7 +- src/api/controllers/SignupController.ts | 2 + src/api/controllers/StatsController.ts | 2 + src/api/controllers/UploadController.ts | 2 + src/api/controllers/index.ts | 35 ++++++++++ src/api/dto/AddressDto.ts | 6 +- src/api/dto/ApiKeyDto.ts | 7 ++ src/api/dto/BaseDto.ts | 42 +++++++---- src/api/dto/CalloutDto.ts | 25 +++++++ src/api/dto/CalloutResponseCommentDto.ts | 8 +++ src/api/dto/CalloutResponseDto.ts | 17 +++++ src/api/dto/ContactDto.ts | 21 +++--- src/api/dto/ContactProfileDto.ts | 9 +-- src/api/dto/ContactRoleDto.ts | 7 +- src/api/dto/EmailDto.ts | 4 +- src/api/dto/NoticeDto.ts | 16 ++++- src/api/dto/PaymentDto.ts | 9 +++ src/api/transformers/AddressTransformer.ts | 6 +- .../transformers/ContactRoleTransformer.ts | 8 +-- src/api/utils/rules.ts | 6 +- src/tools/generate-docs.ts | 54 +++++++++++++++ src/typings/class-transformer.d.ts | 5 ++ 34 files changed, 395 insertions(+), 120 deletions(-) create mode 100644 src/api/controllers/index.ts create mode 100644 src/tools/generate-docs.ts create mode 100644 src/typings/class-transformer.d.ts diff --git a/package-lock.json b/package-lock.json index f1ff950c0..1015c7b2b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "chance": "^1.1.11", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "class-validator-jsonschema": "^4.0.0", "clean-deep": "^3.4.0", "connect-busboy": "^1.0.0", "connect-pg-simple": "^9.0.1", @@ -56,6 +57,7 @@ "query-string": "^7.1.3", "reflect-metadata": "^0.1.13", "routing-controllers": "^0.10.4", + "routing-controllers-openapi": "^4.0.0", "slugify": "^1.6.6", "stripe": "^9.16.0", "tar-stream": "^3.1.6", @@ -3508,6 +3510,22 @@ "validator": "^13.7.0" } }, + "node_modules/class-validator-jsonschema": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/class-validator-jsonschema/-/class-validator-jsonschema-4.0.0.tgz", + "integrity": "sha512-CMhDl5jG7/C3C9BzOQePBZftyt5oS3S//L/d4DNAlQWseWdrX0Fbw8WfokbppEcCgQPuV3eqCO1pY8khhIuTew==", + "dependencies": { + "lodash.groupby": "^4.6.0", + "lodash.merge": "^4.6.2", + "openapi3-ts": "^2.0.2", + "reflect-metadata": "^0.1.13", + "tslib": "^2.4.1" + }, + "peerDependencies": { + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.14.0" + } + }, "node_modules/clean-deep": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/clean-deep/-/clean-deep-3.4.0.tgz", @@ -8564,12 +8582,22 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.capitalize": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", + "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==" + }, "node_modules/lodash.clonedeep": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", "dev": true }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -8611,11 +8639,21 @@ "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" + }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==" + }, "node_modules/lodash.transform": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.transform/-/lodash.transform-4.6.0.tgz", @@ -10222,6 +10260,14 @@ "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==", "optional": true }, + "node_modules/openapi3-ts": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.2.tgz", + "integrity": "sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==", + "dependencies": { + "yaml": "^1.10.2" + } + }, "node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -11770,6 +11816,28 @@ "class-validator": "^0.14.0" } }, + "node_modules/routing-controllers-openapi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/routing-controllers-openapi/-/routing-controllers-openapi-4.0.0.tgz", + "integrity": "sha512-rObiqX3nLN63ARMj/uf1YKr3WYR8mmny0YR68U70EU5IH3MLe+2uA3r//EkPBj+kKrnOtL2NcfQwhlLiJsQlMg==", + "dependencies": { + "lodash.capitalize": "^4.2.1", + "lodash.merge": "^4.6.2", + "lodash.startcase": "^4.4.0", + "openapi3-ts": "^2.0.2", + "path-to-regexp": "^6.2.1", + "reflect-metadata": "^0.1.13", + "tslib": "^2.4.1" + }, + "peerDependencies": { + "routing-controllers": "^0.10.0" + } + }, + "node_modules/routing-controllers-openapi/node_modules/path-to-regexp": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==" + }, "node_modules/routing-controllers/node_modules/cookie": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", @@ -14353,7 +14421,6 @@ "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", - "dev": true, "engines": { "node": ">= 6" } diff --git a/package.json b/package.json index b2e70ada3..616a4afff 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "chance": "^1.1.11", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "class-validator-jsonschema": "^4.0.0", "clean-deep": "^3.4.0", "connect-busboy": "^1.0.0", "connect-pg-simple": "^9.0.1", @@ -81,6 +82,7 @@ "query-string": "^7.1.3", "reflect-metadata": "^0.1.13", "routing-controllers": "^0.10.4", + "routing-controllers-openapi": "^4.0.0", "slugify": "^1.6.6", "stripe": "^9.16.0", "tar-stream": "^3.1.6", diff --git a/src/api/app.ts b/src/api/app.ts index 7e2875044..6ac629516 100644 --- a/src/api/app.ts +++ b/src/api/app.ts @@ -12,22 +12,7 @@ import { useExpressServer } from "routing-controllers"; -import { ApiKeyController } from "./controllers/ApiKeyController"; -import { AuthController } from "./controllers/AuthController"; -import { CalloutController } from "./controllers/CalloutController"; -import { CalloutResponseController } from "./controllers/CalloutResponseController"; -import { CalloutResponseCommentController } from "./controllers/CalloutResponseCommentController"; -import { ContentController } from "./controllers/ContentController"; -import { EmailController } from "./controllers/EmailController"; -import { ContactController } from "./controllers/ContactController"; -import { NoticeController } from "./controllers/NoticeController"; -import { PaymentController } from "./controllers/PaymentController"; -import { SegmentController } from "./controllers/SegmentController"; -import { SignupController } from "./controllers/SignupController"; -import { StatsController } from "./controllers/StatsController"; -import { ResetPasswordController } from "./controllers/ResetPasswordController"; -import { ResetDeviceController } from "./controllers/ResetDeviceController"; -import { UploadController } from "./controllers/UploadController"; +import controllers from "@api/controllers"; import { ValidateResponseInterceptor } from "./interceptors/ValidateResponseInterceptor"; @@ -72,24 +57,7 @@ initApp() useExpressServer(app, { routePrefix: "/1.0", - controllers: [ - ApiKeyController, - AuthController, - CalloutController, - CalloutResponseController, - CalloutResponseCommentController, - ContentController, - EmailController, - ContactController, - NoticeController, - PaymentController, - SegmentController, - SignupController, - StatsController, - ResetPasswordController, - ResetDeviceController, - UploadController - ], + controllers, interceptors: [ValidateResponseInterceptor], middlewares: [AuthMiddleware], currentUserChecker, diff --git a/src/api/controllers/ApiKeyController.ts b/src/api/controllers/ApiKeyController.ts index 3026fcf9c..2e8cb09bd 100644 --- a/src/api/controllers/ApiKeyController.ts +++ b/src/api/controllers/ApiKeyController.ts @@ -12,6 +12,7 @@ import { Delete, Param } from "routing-controllers"; +import { OpenAPI, ResponseSchema } from "routing-controllers-openapi"; import { getRepository } from "@core/database"; import { generateApiKey } from "@core/utils/auth"; @@ -24,26 +25,29 @@ import { CurrentAuth } from "@api/decorators/CurrentAuth"; import { CreateApiKeyDto, GetApiKeyDto, + GetApiKeysListDto, ListApiKeysDto, NewApiKeyDto } from "@api/dto/ApiKeyDto"; -import { PaginatedDto } from "@api/dto/PaginatedDto"; import ApiKeyTransformer from "@api/transformers/ApiKeyTransformer"; import { AuthInfo } from "@type/auth-info"; +@OpenAPI({ tags: ["API Keys"] }) @JsonController("/api-key") @Authorized("admin") export class ApiKeyController { @Get("/") + @ResponseSchema(GetApiKeysListDto) async getApiKeys( @CurrentAuth({ required: true }) auth: AuthInfo, @QueryParams() query: ListApiKeysDto - ): Promise> { + ): Promise { return await ApiKeyTransformer.fetch(auth, query); } @Get("/:id") + @ResponseSchema(GetApiKeyDto, { statusCode: 200 }) async getApiKey( @CurrentAuth({ required: true }) auth: AuthInfo, @Param("id") id: string @@ -52,6 +56,7 @@ export class ApiKeyController { } @Post("/") + @ResponseSchema(NewApiKeyDto) async createApiKey( @CurrentAuth({ required: true }) auth: AuthInfo, @CurrentUser({ required: true }) creator: Contact, diff --git a/src/api/controllers/CalloutController.ts b/src/api/controllers/CalloutController.ts index 510ec16a3..055077690 100644 --- a/src/api/controllers/CalloutController.ts +++ b/src/api/controllers/CalloutController.ts @@ -16,6 +16,7 @@ import { QueryParams, Res } from "routing-controllers"; +import { ResponseSchema } from "routing-controllers-openapi"; import slugify from "slugify"; import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity"; @@ -30,17 +31,18 @@ import { GetExportQuery } from "@api/dto/BaseDto"; import { CreateCalloutDto, GetCalloutDto, + GetCalloutListDto, GetCalloutOptsDto, ListCalloutsDto } from "@api/dto/CalloutDto"; import { CreateCalloutResponseDto, - GetCalloutResponseDto, + GetCalloutResponseListDto, GetCalloutResponseMapDto, + GetCalloutResponseMapListDto, ListCalloutResponsesDto } from "@api/dto/CalloutResponseDto"; import { CreateCalloutTagDto, GetCalloutTagDto } from "@api/dto/CalloutTagDto"; -import { PaginatedDto } from "@api/dto/PaginatedDto"; import { CurrentAuth } from "@api/decorators/CurrentAuth"; import PartialBody from "@api/decorators/PartialBody"; @@ -63,15 +65,17 @@ import { AuthInfo } from "@type/auth-info"; @JsonController("/callout") export class CalloutController { @Get("/") + @ResponseSchema(GetCalloutListDto) async getCallouts( @CurrentAuth() auth: AuthInfo | undefined, @QueryParams() query: ListCalloutsDto - ): Promise> { + ): Promise { return CalloutTransformer.fetch(auth, query); } @Authorized("admin") @Post("/") + @ResponseSchema(GetCalloutDto) async createCallout(@Body() data: CreateCalloutDto): Promise { const callout = await CalloutsService.createCallout( { @@ -84,6 +88,7 @@ export class CalloutController { } @Get("/:slug") + @ResponseSchema(GetCalloutDto, { statusCode: 200 }) async getCallout( @CurrentAuth() auth: AuthInfo | undefined, @Param("slug") slug: string, @@ -97,6 +102,7 @@ export class CalloutController { @Authorized("admin") @Patch("/:slug") + @ResponseSchema(GetCalloutDto, { statusCode: 200 }) async updateCallout( @CurrentAuth({ required: true }) auth: AuthInfo, @Param("slug") slug: string, @@ -143,11 +149,12 @@ export class CalloutController { } @Get("/:slug/responses") + @ResponseSchema(GetCalloutResponseListDto) async getCalloutResponses( @CurrentAuth({ required: true }) auth: AuthInfo, @Param("slug") slug: string, @QueryParams() query: ListCalloutResponsesDto - ): Promise> { + ): Promise { return await CalloutResponseTransformer.fetchForCallout(auth, slug, query); } @@ -168,11 +175,12 @@ export class CalloutController { } @Get("/:slug/responses/map") + @ResponseSchema(GetCalloutResponseMapListDto) async getCalloutResponsesMap( @CurrentAuth() auth: AuthInfo | undefined, @Param("slug") slug: string, @QueryParams() query: ListCalloutResponsesDto - ): Promise> { + ): Promise { return await CalloutResponseMapTransformer.fetchForCallout( auth, slug, @@ -211,6 +219,7 @@ export class CalloutController { @Authorized("admin") @Get("/:slug/tags") + @ResponseSchema(GetCalloutTagDto, { isArray: true }) async getCalloutTags( @CurrentAuth({ required: true }) auth: AuthInfo, @Param("slug") slug: string @@ -227,6 +236,7 @@ export class CalloutController { @Authorized("admin") @Post("/:slug/tags") + @ResponseSchema(GetCalloutTagDto) async createCalloutTag( @Param("slug") slug: string, @Body() data: CreateCalloutTagDto @@ -243,6 +253,7 @@ export class CalloutController { @Authorized("admin") @Get("/:slug/tags/:tag") + @ResponseSchema(GetCalloutTagDto, { statusCode: 200 }) async getCalloutTag( @CurrentAuth({ required: true }) auth: AuthInfo, @Param("tag") tagId: string @@ -252,6 +263,7 @@ export class CalloutController { @Authorized("admin") @Patch("/:slug/tags/:tag") + @ResponseSchema(GetCalloutTagDto, { statusCode: 200 }) async updateCalloutTag( @CurrentAuth({ required: true }) auth: AuthInfo, @Param("slug") slug: string, diff --git a/src/api/controllers/CalloutResponseCommentController.ts b/src/api/controllers/CalloutResponseCommentController.ts index ad71ca5a8..bfd1e156d 100644 --- a/src/api/controllers/CalloutResponseCommentController.ts +++ b/src/api/controllers/CalloutResponseCommentController.ts @@ -12,6 +12,7 @@ import { Post, QueryParams } from "routing-controllers"; +import { ResponseSchema } from "routing-controllers-openapi"; import { getRepository } from "@core/database"; @@ -20,9 +21,9 @@ import PartialBody from "@api/decorators/PartialBody"; import { CreateCalloutResponseCommentDto, GetCalloutResponseCommentDto, + GetCalloutResponseCommentListDto, ListCalloutResponseCommentsDto } from "@api/dto/CalloutResponseCommentDto"; -import { PaginatedDto } from "@api/dto/PaginatedDto"; import { UUIDParams } from "@api/params/UUIDParams"; import CalloutResponseCommentTransformer from "@api/transformers/CalloutResponseCommentTransformer"; @@ -36,6 +37,7 @@ import { AuthInfo } from "@type/auth-info"; @Authorized("admin") export class CalloutResponseCommentController { @Post("/") + @ResponseSchema(GetCalloutResponseCommentDto) async createCalloutReponseComment( @Body() data: CreateCalloutResponseCommentDto, @CurrentUser({ required: true }) contact: Contact @@ -51,14 +53,16 @@ export class CalloutResponseCommentController { } @Get("/") + @ResponseSchema(GetCalloutResponseCommentListDto) async getCalloutResponseComments( @CurrentAuth({ required: true }) auth: AuthInfo, @QueryParams() query: ListCalloutResponseCommentsDto - ): Promise> { + ): Promise { return await CalloutResponseCommentTransformer.fetch(auth, query); } @Get("/:id") + @ResponseSchema(GetCalloutResponseCommentDto, { statusCode: 200 }) async getCalloutResponseComment( @CurrentAuth({ required: true }) auth: AuthInfo, @Params() { id }: UUIDParams @@ -67,6 +71,7 @@ export class CalloutResponseCommentController { } @Patch("/:id") + @ResponseSchema(GetCalloutResponseCommentDto, { statusCode: 200 }) async updateCalloutResponseComment( @CurrentAuth({ required: true }) auth: AuthInfo, @Params() { id }: UUIDParams, diff --git a/src/api/controllers/CalloutResponseController.ts b/src/api/controllers/CalloutResponseController.ts index f1b65fbd4..02a9d5321 100644 --- a/src/api/controllers/CalloutResponseController.ts +++ b/src/api/controllers/CalloutResponseController.ts @@ -7,6 +7,7 @@ import { Patch, QueryParams } from "routing-controllers"; +import { ResponseSchema } from "routing-controllers-openapi"; import { CurrentAuth } from "@api/decorators/CurrentAuth"; import PartialBody from "@api/decorators/PartialBody"; @@ -17,10 +18,10 @@ import { BatchUpdateCalloutResponseResultDto, CreateCalloutResponseDto, GetCalloutResponseDto, + GetCalloutResponseListDto, GetCalloutResponseOptsDto, ListCalloutResponsesDto } from "@api/dto/CalloutResponseDto"; -import { PaginatedDto } from "@api/dto/PaginatedDto"; import CalloutResponseTransformer from "@api/transformers/CalloutResponseTransformer"; import { AuthInfo } from "@type/auth-info"; @@ -28,10 +29,11 @@ import { AuthInfo } from "@type/auth-info"; @JsonController("/callout-responses") export class CalloutResponseController { @Get("/") + @ResponseSchema(GetCalloutResponseListDto) async getCalloutResponses( @CurrentAuth() auth: AuthInfo | undefined, @QueryParams() query: ListCalloutResponsesDto - ): Promise> { + ): Promise { return CalloutResponseTransformer.fetch(auth, query); } diff --git a/src/api/controllers/ContactController.ts b/src/api/controllers/ContactController.ts index 2c94f3535..a1d29fc85 100644 --- a/src/api/controllers/ContactController.ts +++ b/src/api/controllers/ContactController.ts @@ -18,6 +18,7 @@ import { QueryParams, Res } from "routing-controllers"; +import { ResponseSchema } from "routing-controllers-openapi"; import ContactsService from "@core/services/ContactsService"; import OptionsService from "@core/services/OptionsService"; @@ -34,6 +35,7 @@ import { GetExportQuery } from "@api/dto/BaseDto"; import { CreateContactDto, GetContactDto, + GetContactListDto, GetContactOptsDto, GetContributionInfoDto, ListContactsDto, @@ -44,18 +46,14 @@ import { DeleteContactMfaDto, GetContactMfaDto } from "@api/dto/ContactMfaDto"; -import { - GetContactRoleDto, - UpdateContactRoleDto -} from "@api/dto/ContactRoleDto"; +import { ContactRoleDto, UpdateContactRoleDto } from "@api/dto/ContactRoleDto"; import { StartContributionDto, ForceUpdateContributionDto, UpdateContributionDto } from "@api/dto/ContributionDto"; import { CompleteJoinFlowDto, StartJoinFlowDto } from "@api/dto/JoinFlowDto"; -import { PaginatedDto } from "@api/dto/PaginatedDto"; -import { GetPaymentDto, ListPaymentsDto } from "@api/dto/PaymentDto"; +import { GetPaymentListDto, ListPaymentsDto } from "@api/dto/PaymentDto"; import { GetPaymentFlowDto } from "@api/dto/PaymentFlowDto"; import { CurrentAuth } from "@api/decorators/CurrentAuth"; @@ -122,6 +120,7 @@ function TargetUser() { export class ContactController { @Authorized("admin") @Post("/") + @ResponseSchema(GetContactDto) async createContact( @CurrentAuth({ required: true }) auth: AuthInfo, @Body() data: CreateContactDto @@ -172,10 +171,11 @@ export class ContactController { @Authorized("admin") @Get("/") + @ResponseSchema(GetContactListDto) async getContacts( @CurrentAuth({ required: true }) auth: AuthInfo, @QueryParams() query: ListContactsDto - ): Promise> { + ): Promise { return await ContactTransformer.fetch(auth, query); } @@ -192,6 +192,7 @@ export class ContactController { } @Get("/:id") + @ResponseSchema(GetContactDto, { statusCode: 200 }) async getContact( @CurrentAuth({ required: true }) auth: AuthInfo, @TargetUser() target: Contact, @@ -201,6 +202,7 @@ export class ContactController { } @Patch("/:id") + @ResponseSchema(GetContactDto, { statusCode: 200 }) async updateContact( @CurrentAuth({ required: true }) auth: AuthInfo, @TargetUser() target: Contact, @@ -234,6 +236,7 @@ export class ContactController { } @Get("/:id/contribution") + @ResponseSchema(GetContributionInfoDto) async getContribution( @TargetUser() target: Contact ): Promise { @@ -242,6 +245,7 @@ export class ContactController { } @Patch("/:id/contribution") + @ResponseSchema(GetContributionInfoDto) async updateContribution( @TargetUser() target: Contact, @Body() data: UpdateContributionDto @@ -256,6 +260,7 @@ export class ContactController { } @Post("/:id/contribution") + @ResponseSchema(GetPaymentFlowDto) async startContribution( @TargetUser() target: Contact, @Body() data: StartContributionDto @@ -268,6 +273,7 @@ export class ContactController { * @param target The target contact */ @Get("/:id/mfa") + @ResponseSchema(GetContactMfaDto, { statusCode: 200 }) async getContactMfa( @TargetUser() target: Contact ): Promise { @@ -321,6 +327,7 @@ export class ContactController { } @Post("/:id/contribution/complete") + @ResponseSchema(GetContributionInfoDto) async completeStartContribution( @TargetUser() target: Contact, @Body() data: CompleteJoinFlowDto @@ -339,6 +346,7 @@ export class ContactController { */ @Authorized("admin") @Patch("/:id/contribution/force") + @ResponseSchema(GetContributionInfoDto) async forceUpdateContribution( @TargetUser() target: Contact, @Body() data: ForceUpdateContributionDto @@ -348,11 +356,12 @@ export class ContactController { } @Get("/:id/payment") + @ResponseSchema(GetPaymentListDto) async getPayments( @CurrentAuth({ required: true }) auth: AuthInfo, @TargetUser() target: Contact, @QueryParams() query: ListPaymentsDto - ): Promise> { + ): Promise { return PaymentTransformer.fetch(auth, { ...query, rules: mergeRules([ @@ -363,6 +372,7 @@ export class ContactController { } @Put("/:id/payment-method") + @ResponseSchema(GetPaymentFlowDto) async updatePaymentMethod( @TargetUser() target: Contact, @Body() data: StartJoinFlowDto @@ -386,6 +396,7 @@ export class ContactController { } @Post("/:id/payment-method/complete") + @ResponseSchema(GetContributionInfoDto) async completeUpdatePaymentMethod( @TargetUser() target: Contact, @Body() data: CompleteJoinFlowDto @@ -451,12 +462,13 @@ export class ContactController { @Authorized("admin") @Put("/:id/role/:roleType") + @ResponseSchema(ContactRoleDto) async updateRole( @CurrentAuth({ required: true }) auth: AuthInfo, @TargetUser() target: Contact, @Params() { roleType }: ContactRoleParams, @Body() data: UpdateContactRoleDto - ): Promise { + ): Promise { if (data.dateExpires && data.dateAdded >= data.dateExpires) { throw new BadRequestError(); } diff --git a/src/api/controllers/ContentController.ts b/src/api/controllers/ContentController.ts index c3d199478..c55cd8ca6 100644 --- a/src/api/controllers/ContentController.ts +++ b/src/api/controllers/ContentController.ts @@ -1,6 +1,5 @@ import { Authorized, - Body, Get, JsonController, Params, @@ -23,7 +22,7 @@ import ContentTransformer from "@api/transformers/ContentTransformer"; @JsonController("/content") export class ContentController { - @Get("/:id(?:*)") + @Get("/:id") // TODO: fix :id async get(@Params() { id }: ContentParams): Promise { return await ContentTransformer.fetchOne(id); } diff --git a/src/api/controllers/EmailController.ts b/src/api/controllers/EmailController.ts index aa5cc4e45..11c4c7306 100644 --- a/src/api/controllers/EmailController.ts +++ b/src/api/controllers/EmailController.ts @@ -8,6 +8,7 @@ import { Param, Put } from "routing-controllers"; +import { ResponseSchema } from "routing-controllers-openapi"; import EmailService from "@core/services/EmailService"; @@ -15,7 +16,7 @@ import { getRepository } from "@core/database"; import Email from "@models/Email"; -import { GetEmailDto, UpdateEmailDto } from "@api/dto/EmailDto"; +import { EmailDto } from "@api/dto/EmailDto"; import ExternalEmailTemplate from "@api/errors/ExternalEmailTemplate"; async function findEmail(id: string): Promise { @@ -33,8 +34,8 @@ async function findEmail(id: string): Promise { } // TODO: move to transformer -function emailToData(email: Email): GetEmailDto { - return plainToInstance(GetEmailDto, { +function emailToData(email: Email): EmailDto { + return plainToInstance(EmailDto, { subject: email.subject, body: email.body }); @@ -44,16 +45,18 @@ function emailToData(email: Email): GetEmailDto { @JsonController("/email") export class EmailController { @Get("/:id") - async getEmail(@Param("id") id: string): Promise { + @ResponseSchema(EmailDto, { statusCode: 200 }) + async getEmail(@Param("id") id: string): Promise { const email = await findEmail(id); return email ? emailToData(email) : undefined; } @Put("/:id") + @ResponseSchema(EmailDto, { statusCode: 200 }) async updateEmail( @Param("id") id: string, - @Body() data: UpdateEmailDto - ): Promise { + @Body() data: EmailDto + ): Promise { const email = await findEmail(id); if (email) { await getRepository(Email).update(email.id, data); diff --git a/src/api/controllers/NoticeController.ts b/src/api/controllers/NoticeController.ts index 9075184c6..6ae602442 100644 --- a/src/api/controllers/NoticeController.ts +++ b/src/api/controllers/NoticeController.ts @@ -11,6 +11,7 @@ import { Post, QueryParams } from "routing-controllers"; +import { ResponseSchema } from "routing-controllers-openapi"; import { getRepository } from "@core/database"; @@ -19,9 +20,9 @@ import PartialBody from "@api/decorators/PartialBody"; import { CreateNoticeDto, GetNoticeDto, + GetNoticeListDto, ListNoticesDto } from "@api/dto/NoticeDto"; -import { PaginatedDto } from "@api/dto/PaginatedDto"; import { UUIDParams } from "@api/params/UUIDParams"; import NoticeTransformer from "@api/transformers/NoticeTransformer"; @@ -33,14 +34,16 @@ import { AuthInfo } from "@type/auth-info"; @Authorized() export class NoticeController { @Get("/") + @ResponseSchema(GetNoticeListDto) async getNotices( @CurrentAuth({ required: true }) auth: AuthInfo, @QueryParams() query: ListNoticesDto - ): Promise> { + ): Promise { return await NoticeTransformer.fetch(auth, query); } @Get("/:id") + @ResponseSchema(GetNoticeDto, { statusCode: 200 }) async getNotice( @CurrentAuth({ required: true }) auth: AuthInfo, @Params() { id }: UUIDParams @@ -50,6 +53,7 @@ export class NoticeController { @Post("/") @Authorized("admin") + @ResponseSchema(GetNoticeDto) async createNotice(@Body() data: CreateNoticeDto): Promise { const notice = await getRepository(Notice).save(data); return NoticeTransformer.convert(notice); @@ -57,6 +61,7 @@ export class NoticeController { @Patch("/:id") @Authorized("admin") + @ResponseSchema(GetNoticeDto, { statusCode: 200 }) async updateNotice( @CurrentAuth({ required: true }) auth: AuthInfo, @Params() { id }: UUIDParams, diff --git a/src/api/controllers/PaymentController.ts b/src/api/controllers/PaymentController.ts index 2ed709d0d..d80a4ea1c 100644 --- a/src/api/controllers/PaymentController.ts +++ b/src/api/controllers/PaymentController.ts @@ -5,11 +5,12 @@ import { Param, QueryParams } from "routing-controllers"; +import { ResponseSchema } from "routing-controllers-openapi"; import { CurrentAuth } from "@api/decorators/CurrentAuth"; -import { PaginatedDto } from "@api/dto/PaginatedDto"; import { GetPaymentDto, + GetPaymentListDto, GetPaymentOptsDto, ListPaymentsDto } from "@api/dto/PaymentDto"; @@ -21,14 +22,16 @@ import { AuthInfo } from "@type/auth-info"; @Authorized() export class PaymentController { @Get("/") + @ResponseSchema(GetPaymentListDto) async getPayments( @CurrentAuth({ required: true }) auth: AuthInfo, @QueryParams() query: ListPaymentsDto - ): Promise> { + ): Promise { return await PaymentTransformer.fetch(auth, query); } @Get("/:id") + @ResponseSchema(GetPaymentDto, { statusCode: 200 }) async getPayment( @CurrentAuth({ required: true }) auth: AuthInfo, @Param("id") id: string, diff --git a/src/api/controllers/SegmentController.ts b/src/api/controllers/SegmentController.ts index 7434ba406..812db6308 100644 --- a/src/api/controllers/SegmentController.ts +++ b/src/api/controllers/SegmentController.ts @@ -11,12 +11,13 @@ import { Post, QueryParams } from "routing-controllers"; +import { ResponseSchema } from "routing-controllers-openapi"; import { getRepository } from "@core/database"; import { CurrentAuth } from "@api/decorators/CurrentAuth"; import PartialBody from "@api/decorators/PartialBody"; -import { GetContactDto, ListContactsDto } from "@api/dto/ContactDto"; +import { GetContactListDto, ListContactsDto } from "@api/dto/ContactDto"; import { GetSegmentDto, ListSegmentsDto, @@ -24,7 +25,6 @@ import { GetSegmentWith, GetSegmentOptsDto } from "@api/dto/SegmentDto"; -import { PaginatedDto } from "@api/dto/PaginatedDto"; import { UUIDParams } from "@api/params/UUIDParams"; import ContactTransformer from "@api/transformers/ContactTransformer"; import SegmentTransformer from "@api/transformers/SegmentTransformer"; @@ -102,11 +102,12 @@ export class SegmentController { } @Get("/:id/contacts") + @ResponseSchema(GetContactListDto, { statusCode: 200 }) async getSegmentContacts( @CurrentAuth({ required: true }) auth: AuthInfo, @Params() { id }: UUIDParams, @QueryParams() query: ListContactsDto - ): Promise | undefined> { + ): Promise { const segment = await getRepository(Segment).findOneBy({ id }); if (segment) { return await ContactTransformer.fetch(auth, { diff --git a/src/api/controllers/SignupController.ts b/src/api/controllers/SignupController.ts index 15e603c35..4a4864ac5 100644 --- a/src/api/controllers/SignupController.ts +++ b/src/api/controllers/SignupController.ts @@ -8,6 +8,7 @@ import { Post, Req } from "routing-controllers"; +import { ResponseSchema } from "routing-controllers-openapi"; import { getRepository } from "@core/database"; import { generatePassword } from "@core/utils/auth"; @@ -29,6 +30,7 @@ import Password from "@models/Password"; export class SignupController { @OnUndefined(204) @Post("/") + @ResponseSchema(GetPaymentFlowDto, { statusCode: 200 }) async startSignup( @Body() data: StartSignupFlowDto ): Promise { diff --git a/src/api/controllers/StatsController.ts b/src/api/controllers/StatsController.ts index 67b00ed9c..8331a1437 100644 --- a/src/api/controllers/StatsController.ts +++ b/src/api/controllers/StatsController.ts @@ -6,6 +6,7 @@ import { JsonController, QueryParams } from "routing-controllers"; +import { ResponseSchema } from "routing-controllers-openapi"; import { createQueryBuilder } from "@core/database"; @@ -18,6 +19,7 @@ import Payment from "@models/Payment"; export class StatsController { @Authorized("admin") @Get("/") + @ResponseSchema(GetStatsDto) async getStats(@QueryParams() query: GetStatsOptsDto): Promise { const newContacts = await createQueryBuilder(Contact, "m") .where("m.joined BETWEEN :from AND :to", query) diff --git a/src/api/controllers/UploadController.ts b/src/api/controllers/UploadController.ts index ad41d1537..4dc718ffb 100644 --- a/src/api/controllers/UploadController.ts +++ b/src/api/controllers/UploadController.ts @@ -12,6 +12,7 @@ import { Post, Req } from "routing-controllers"; +import { ResponseSchema } from "routing-controllers-openapi"; import { MoreThan } from "typeorm"; import { getRepository } from "@core/database"; @@ -35,6 +36,7 @@ async function canUploadOrFail(ipAddress: string, date: Date, max: number) { @JsonController("/upload") export class UploadController { @Post("/") + @ResponseSchema(GetUploadFlowDto) async create( @CurrentUser() contact: Contact | undefined, @Req() req: Request diff --git a/src/api/controllers/index.ts b/src/api/controllers/index.ts new file mode 100644 index 000000000..f5bec2095 --- /dev/null +++ b/src/api/controllers/index.ts @@ -0,0 +1,35 @@ +import { ApiKeyController } from "./ApiKeyController"; +import { AuthController } from "./AuthController"; +import { CalloutController } from "./CalloutController"; +import { CalloutResponseCommentController } from "./CalloutResponseCommentController"; +import { CalloutResponseController } from "./CalloutResponseController"; +import { ContactController } from "./ContactController"; +import { ContentController } from "./ContentController"; +import { EmailController } from "./EmailController"; +import { NoticeController } from "./NoticeController"; +import { PaymentController } from "./PaymentController"; +import { ResetDeviceController } from "./ResetDeviceController"; +import { ResetPasswordController } from "./ResetPasswordController"; +import { SegmentController } from "./SegmentController"; +import { SignupController } from "./SignupController"; +import { StatsController } from "./StatsController"; +import { UploadController } from "./UploadController"; + +export default [ + ApiKeyController, + AuthController, + CalloutController, + CalloutResponseCommentController, + CalloutResponseController, + ContactController, + ContentController, + EmailController, + NoticeController, + PaymentController, + ResetDeviceController, + ResetPasswordController, + SegmentController, + SignupController, + StatsController, + UploadController +]; diff --git a/src/api/dto/AddressDto.ts b/src/api/dto/AddressDto.ts index 0d4ba83ed..036fabf58 100644 --- a/src/api/dto/AddressDto.ts +++ b/src/api/dto/AddressDto.ts @@ -1,7 +1,9 @@ import Address from "@models/Address"; import { IsDefined, IsOptional, IsString } from "class-validator"; -export class UpdateAddressDto implements Address { +// Use @IsDefined() to make sure the field is present even if used within +// PartialBody +export class AddressDto implements Address { @IsDefined() @IsString() line1!: string; @@ -18,5 +20,3 @@ export class UpdateAddressDto implements Address { @IsString() postcode!: string; } - -export class GetAddressDto extends UpdateAddressDto {} diff --git a/src/api/dto/ApiKeyDto.ts b/src/api/dto/ApiKeyDto.ts index ebd34c39e..54a238355 100644 --- a/src/api/dto/ApiKeyDto.ts +++ b/src/api/dto/ApiKeyDto.ts @@ -9,6 +9,7 @@ import { Type } from "class-transformer"; import { GetPaginatedQuery } from "@api/dto/BaseDto"; import { GetContactDto } from "@api/dto/ContactDto"; +import { PaginatedDto } from "@api/dto/PaginatedDto"; export class CreateApiKeyDto { @IsString() @@ -40,3 +41,9 @@ export class ListApiKeysDto extends GetPaginatedQuery { @IsIn(["createdAt", "expires"]) sort?: string; } + +export class GetApiKeysListDto extends PaginatedDto { + @ValidateNested({ each: true }) + @Type(() => GetApiKeyDto) + items!: GetApiKeyDto[]; +} diff --git a/src/api/dto/BaseDto.ts b/src/api/dto/BaseDto.ts index d6b8853a6..b9ece80f7 100644 --- a/src/api/dto/BaseDto.ts +++ b/src/api/dto/BaseDto.ts @@ -22,6 +22,7 @@ import { Min, Max } from "class-validator"; +import { JSONSchema } from "class-validator-jsonschema"; import { IsType } from "@api/validators/IsType"; @@ -34,23 +35,15 @@ export class GetPaginatedRule implements Rule { @IsArray() @IsType(["string", "boolean", "number"], { each: true }) + @JSONSchema(() => ({ + type: "array", + items: { + oneOf: [{ type: "string" }, { type: "boolean" }, { type: "number" }] + } + })) value!: RuleValue[]; } -export type GetPaginatedRuleGroupRule = - | GetPaginatedRuleGroup - | GetPaginatedRule; - -function transformRules({ - value -}: TransformFnParams): GetPaginatedRuleGroupRule { - return value.map((v: GetPaginatedRuleGroupRule) => - plainToClass( - isRuleGroup(v) ? GetPaginatedRuleGroup : GetPaginatedRule, - v - ) - ); -} export class GetPaginatedRuleGroup implements RuleGroup { @IsIn(["AND", "OR"]) condition!: "AND" | "OR"; @@ -58,9 +51,28 @@ export class GetPaginatedRuleGroup implements RuleGroup { @IsArray() @ValidateNested({ each: true }) @Transform(transformRules) - rules!: GetPaginatedRuleGroupRule[]; + @JSONSchema(() => ({ + type: "array", + items: { + oneOf: [ + { $ref: "#/components/schemas/GetPaginatedRule" }, + { $ref: "#/components/schemas/GetPaginatedRuleGroup" } + ] + } + })) + rules!: GetPaginatedRuleOrGroup[]; } +export type GetPaginatedRuleOrGroup = GetPaginatedRuleGroup | GetPaginatedRule; + +function transformRules({ value }: TransformFnParams): GetPaginatedRuleOrGroup { + return value.map((v: GetPaginatedRuleOrGroup) => + plainToClass( + isRuleGroup(v) ? GetPaginatedRuleGroup : GetPaginatedRule, + v + ) + ); +} export class GetExportQuery { @IsOptional() @ValidateNested() diff --git a/src/api/dto/CalloutDto.ts b/src/api/dto/CalloutDto.ts index 687c9bc0c..8d28e3071 100644 --- a/src/api/dto/CalloutDto.ts +++ b/src/api/dto/CalloutDto.ts @@ -15,9 +15,11 @@ import { Min, ValidateNested } from "class-validator"; +import { JSONSchema } from "class-validator-jsonschema"; import { GetExportQuery, GetPaginatedQuery } from "@api/dto/BaseDto"; import { LinkDto } from "@api/dto/LinkDto"; +import { PaginatedDto } from "@api/dto/PaginatedDto"; import IsSlug from "@api/validators/IsSlug"; import IsUrl from "@api/validators/IsUrl"; import IsMapBounds from "@api/validators/IsMapBounds"; @@ -46,6 +48,12 @@ export class GetCalloutOptsDto extends GetExportQuery { showHiddenForAll: boolean = false; } +export class GetCalloutListDto extends PaginatedDto { + @ValidateNested({ each: true }) + @Type(() => GetCalloutDto) + items!: GetCalloutDto[]; +} + export class ListCalloutsDto extends GetPaginatedQuery { @IsOptional() @IsEnum(GetCalloutWith, { each: true }) @@ -64,9 +72,26 @@ class SetCalloutMapSchemaDto implements CalloutMapSchema { style!: string; @IsLngLat() + @JSONSchema(() => ({ + type: "array", + items: { type: "number", minimum: -180, maximum: 180 }, + minItems: 2, + maxItems: 2 + })) center!: [number, number]; @IsMapBounds() + @JSONSchema(() => ({ + type: "array", + items: { + type: "array", + items: { type: "number", minimum: -180, maximum: 180 }, + minItems: 2, + maxItems: 2 + }, + minItems: 2, + maxItems: 2 + })) bounds!: [[number, number], [number, number]]; @IsNumber() diff --git a/src/api/dto/CalloutResponseCommentDto.ts b/src/api/dto/CalloutResponseCommentDto.ts index dfca1626c..944d58fc1 100644 --- a/src/api/dto/CalloutResponseCommentDto.ts +++ b/src/api/dto/CalloutResponseCommentDto.ts @@ -1,7 +1,9 @@ +import { Type } from "class-transformer"; import { IsDate, IsIn, IsString, ValidateNested } from "class-validator"; import { GetPaginatedQuery } from "@api/dto/BaseDto"; import { GetContactDto } from "@api/dto/ContactDto"; +import { PaginatedDto } from "@api/dto/PaginatedDto"; export class CreateCalloutResponseCommentDto { @IsString() @@ -25,6 +27,12 @@ export class GetCalloutResponseCommentDto extends CreateCalloutResponseCommentDt updatedAt!: Date; } +export class GetCalloutResponseCommentListDto extends PaginatedDto { + @ValidateNested({ each: true }) + @Type(() => GetCalloutResponseCommentDto) + items!: GetCalloutResponseCommentDto[]; +} + export class ListCalloutResponseCommentsDto extends GetPaginatedQuery { @IsIn(["createdAt", "updatedAt"]) sort?: string; diff --git a/src/api/dto/CalloutResponseDto.ts b/src/api/dto/CalloutResponseDto.ts index 61958b096..c9320fb7b 100644 --- a/src/api/dto/CalloutResponseDto.ts +++ b/src/api/dto/CalloutResponseDto.ts @@ -30,6 +30,7 @@ import { GetContactDto } from "@api/dto/ContactDto"; import { GetCalloutDto } from "@api/dto/CalloutDto"; import { GetCalloutResponseCommentDto } from "@api/dto/CalloutResponseCommentDto"; import { GetCalloutTagDto } from "@api/dto/CalloutTagDto"; +import { PaginatedDto } from "@api/dto/PaginatedDto"; import Callout, { CalloutResponseViewSchema } from "@models/Callout"; @@ -101,21 +102,31 @@ export class GetCalloutResponseDto { @IsOptional() @ValidateNested() + @Type(() => GetContactDto) contact?: GetContactDto | null; @IsOptional() @ValidateNested({ each: true }) + @Type(() => GetCalloutTagDto) tags?: GetCalloutTagDto[]; @IsOptional() @ValidateNested() + @Type(() => GetContactDto) assignee?: GetContactDto | null; @IsOptional() @ValidateNested() + @Type(() => GetCalloutResponseCommentDto) latestComment?: GetCalloutResponseCommentDto | null; } +export class GetCalloutResponseListDto extends PaginatedDto { + @ValidateNested({ each: true }) + @Type(() => GetCalloutResponseDto) + items!: GetCalloutResponseDto[]; +} + export class CreateCalloutResponseDto { // TODO: validate @IsObject() @@ -201,6 +212,12 @@ export class GetCalloutResponseMapDto { address?: CalloutResponseAnswerAddress; } +export class GetCalloutResponseMapListDto extends PaginatedDto { + @ValidateNested({ each: true }) + @Type(() => GetCalloutResponseMapDto) + items!: GetCalloutResponseMapDto[]; +} + export interface GetCalloutResponseMapOptsDto extends BaseGetCalloutResponseOptsDto { callout: Callout & { responseViewSchema: CalloutResponseViewSchema }; diff --git a/src/api/dto/ContactDto.ts b/src/api/dto/ContactDto.ts index 201e85f70..f622e5a29 100644 --- a/src/api/dto/ContactDto.ts +++ b/src/api/dto/ContactDto.ts @@ -27,11 +27,9 @@ import { GetContactProfileDto, UpdateContactProfileDto } from "@api/dto/ContactProfileDto"; -import { - CreateContactRoleDto, - GetContactRoleDto -} from "@api/dto/ContactRoleDto"; +import { ContactRoleDto } from "@api/dto/ContactRoleDto"; import { ForceUpdateContributionDto } from "@api/dto/ContributionDto"; +import { PaginatedDto } from "@api/dto/PaginatedDto"; import IsPassword from "@api/validators/IsPassword"; @@ -156,8 +154,15 @@ export class GetContactDto extends BaseContactDto { profile?: GetContactProfileDto; @IsOptional() - @ValidateNested() - roles?: GetContactRoleDto[]; + @ValidateNested({ each: true }) + @Type(() => ContactRoleDto) + roles?: ContactRoleDto[]; +} + +export class GetContactListDto extends PaginatedDto { + @ValidateNested({ each: true }) + @Type(() => GetContactDto) + items!: GetContactDto[]; } export class UpdateContactDto extends BaseContactDto { @@ -179,8 +184,8 @@ export class CreateContactDto extends UpdateContactDto { @IsOptional() @ValidateNested({ each: true }) - @Type(() => CreateContactRoleDto) - roles?: CreateContactRoleDto[]; + @Type(() => ContactRoleDto) + roles?: ContactRoleDto[]; } export interface ExportContactDto { diff --git a/src/api/dto/ContactProfileDto.ts b/src/api/dto/ContactProfileDto.ts index eee5f90aa..afe6c8785 100644 --- a/src/api/dto/ContactProfileDto.ts +++ b/src/api/dto/ContactProfileDto.ts @@ -9,7 +9,7 @@ import { ValidateNested } from "class-validator"; -import { GetAddressDto, UpdateAddressDto } from "@api/dto/AddressDto"; +import { AddressDto } from "@api/dto/AddressDto"; export class GetContactProfileDto { @IsString() @@ -26,7 +26,8 @@ export class GetContactProfileDto { @IsOptional() @ValidateNested() - deliveryAddress!: GetAddressDto | null; + @Type(() => AddressDto) + deliveryAddress!: AddressDto | null; @IsEnum(NewsletterStatus) newsletterStatus!: NewsletterStatus; @@ -69,8 +70,8 @@ export class UpdateContactProfileDto implements Partial { @IsOptional() @ValidateNested() - @Type(() => UpdateAddressDto) - deliveryAddress?: UpdateAddressDto; + @Type(() => AddressDto) + deliveryAddress?: AddressDto; @IsOptional() @IsEnum(NewsletterStatus) diff --git a/src/api/dto/ContactRoleDto.ts b/src/api/dto/ContactRoleDto.ts index 4975fd4d8..01a94d40a 100644 --- a/src/api/dto/ContactRoleDto.ts +++ b/src/api/dto/ContactRoleDto.ts @@ -13,12 +13,7 @@ export class UpdateContactRoleDto { dateExpires!: Date | null; } -export class CreateContactRoleDto - extends UpdateContactRoleDto - implements GetContactRoleDto -{ +export class ContactRoleDto extends UpdateContactRoleDto { @IsIn(RoleTypes) role!: RoleType; } - -export class GetContactRoleDto extends CreateContactRoleDto {} diff --git a/src/api/dto/EmailDto.ts b/src/api/dto/EmailDto.ts index ddefa81d9..953c5fac0 100644 --- a/src/api/dto/EmailDto.ts +++ b/src/api/dto/EmailDto.ts @@ -1,11 +1,9 @@ import { IsString } from "class-validator"; -export class UpdateEmailDto { +export class EmailDto { @IsString() subject!: string; @IsString() body!: string; } - -export class GetEmailDto extends UpdateEmailDto {} diff --git a/src/api/dto/NoticeDto.ts b/src/api/dto/NoticeDto.ts index 22055dd13..462e5e347 100644 --- a/src/api/dto/NoticeDto.ts +++ b/src/api/dto/NoticeDto.ts @@ -1,8 +1,16 @@ import { ItemStatus } from "@beabee/beabee-common"; import { Type } from "class-transformer"; -import { IsDate, IsEnum, IsIn, IsOptional, IsString } from "class-validator"; +import { + IsDate, + IsEnum, + IsIn, + IsOptional, + IsString, + ValidateNested +} from "class-validator"; import { GetPaginatedQuery } from "@api/dto/BaseDto"; +import { PaginatedDto } from "@api/dto/PaginatedDto"; const sortFields = ["createdAt", "updatedAt", "name", "expires"] as const; @@ -50,3 +58,9 @@ export class GetNoticeDto extends CreateNoticeDto { @IsEnum(ItemStatus) status!: ItemStatus; } + +export class GetNoticeListDto extends PaginatedDto { + @ValidateNested({ each: true }) + @Type(() => GetNoticeDto) + items!: GetNoticeDto[]; +} diff --git a/src/api/dto/PaymentDto.ts b/src/api/dto/PaymentDto.ts index ae30fbead..b681f4669 100644 --- a/src/api/dto/PaymentDto.ts +++ b/src/api/dto/PaymentDto.ts @@ -1,4 +1,5 @@ import { PaymentStatus } from "@beabee/beabee-common"; +import { Type } from "class-transformer"; import { IsArray, IsDate, @@ -11,6 +12,7 @@ import { import { GetPaginatedQuery } from "@api/dto/BaseDto"; import { GetContactDto } from "@api/dto/ContactDto"; +import { PaginatedDto } from "@api/dto/PaginatedDto"; export class GetPaymentDto { @IsNumber() @@ -24,9 +26,16 @@ export class GetPaymentDto { @IsOptional() @ValidateNested() + @Type(() => GetContactDto) contact?: GetContactDto | null; } +export class GetPaymentListDto extends PaginatedDto { + @ValidateNested({ each: true }) + @Type(() => GetPaymentDto) + items!: GetPaymentDto[]; +} + export enum GetPaymentWith { Contact = "contact" } diff --git a/src/api/transformers/AddressTransformer.ts b/src/api/transformers/AddressTransformer.ts index 76829b1d3..7995deaac 100644 --- a/src/api/transformers/AddressTransformer.ts +++ b/src/api/transformers/AddressTransformer.ts @@ -1,12 +1,12 @@ -import { GetAddressDto } from "@api/dto/AddressDto"; +import { AddressDto } from "@api/dto/AddressDto"; import Address from "@models/Address"; import { TransformPlainToInstance } from "class-transformer"; // TODO: make Address into a proper model class AddressTransformer { - @TransformPlainToInstance(GetAddressDto) - convert(address: Address): GetAddressDto { + @TransformPlainToInstance(AddressDto) + convert(address: Address): AddressDto { return { line1: address.line1, line2: address.line2 || "", diff --git a/src/api/transformers/ContactRoleTransformer.ts b/src/api/transformers/ContactRoleTransformer.ts index 46d69111c..c969a6513 100644 --- a/src/api/transformers/ContactRoleTransformer.ts +++ b/src/api/transformers/ContactRoleTransformer.ts @@ -1,19 +1,19 @@ import { BaseTransformer } from "./BaseTransformer"; import { TransformPlainToInstance } from "class-transformer"; -import { GetContactRoleDto } from "@api/dto/ContactRoleDto"; +import { ContactRoleDto } from "@api/dto/ContactRoleDto"; import ContactRole from "@models/ContactRole"; class ContactRoleTransformer extends BaseTransformer< ContactRole, - GetContactRoleDto + ContactRoleDto > { protected model = ContactRole; protected filters = {}; - @TransformPlainToInstance(GetContactRoleDto) - convert(role: ContactRole): GetContactRoleDto { + @TransformPlainToInstance(ContactRoleDto) + convert(role: ContactRole): ContactRoleDto { return { role: role.type, dateAdded: role.dateAdded, diff --git a/src/api/utils/rules.ts b/src/api/utils/rules.ts index 7e7cc6222..e15c9d2de 100644 --- a/src/api/utils/rules.ts +++ b/src/api/utils/rules.ts @@ -28,7 +28,7 @@ import { QueryDeepPartialEntity } from "typeorm/query-builder/QueryPartialEntity import { createQueryBuilder } from "@core/database"; import type { - GetPaginatedRuleGroupRule, + GetPaginatedRuleOrGroup, GetPaginatedRuleGroup } from "@api/dto/BaseDto"; @@ -344,10 +344,10 @@ export async function batchUpdate< } export function mergeRules( - rules: (GetPaginatedRuleGroupRule | undefined | false)[] + rules: (GetPaginatedRuleOrGroup | undefined | false)[] ): GetPaginatedRuleGroup { return { condition: "AND", - rules: rules.filter((rule): rule is GetPaginatedRuleGroupRule => !!rule) + rules: rules.filter((rule): rule is GetPaginatedRuleOrGroup => !!rule) }; } diff --git a/src/tools/generate-docs.ts b/src/tools/generate-docs.ts new file mode 100644 index 000000000..637cd5c44 --- /dev/null +++ b/src/tools/generate-docs.ts @@ -0,0 +1,54 @@ +import "module-alias/register"; + +import { defaultMetadataStorage } from "class-transformer/cjs/storage"; +import { validationMetadatasToSchemas } from "class-validator-jsonschema"; +import { getMetadataArgsStorage } from "routing-controllers"; +import { routingControllersToSpec } from "routing-controllers-openapi"; + +import "@api/controllers"; + +import "@api/dto/AddressDto"; +import "@api/dto/ApiKeyDto"; +import "@api/dto/BaseDto"; +import "@api/dto/CalloutDto"; +import "@api/dto/CalloutResponseCommentDto"; +import "@api/dto/CalloutResponseDto"; +import "@api/dto/CalloutTagDto"; +import "@api/dto/ContactDto"; +import "@api/dto/ContactMfaDto"; +import "@api/dto/ContactProfileDto"; +import "@api/dto/ContactRoleDto"; +import "@api/dto/ContentDto"; +import "@api/dto/ContributionDto"; +import "@api/dto/EmailDto"; +import "@api/dto/JoinFlowDto"; +import "@api/dto/LinkDto"; +import "@api/dto/LoginDto"; +import "@api/dto/NoticeDto"; +// import "@api/dto/PaginatedDto"; +import "@api/dto/PaymentDto"; +import "@api/dto/PaymentFlowDto"; +import "@api/dto/ResetDeviceDto"; +import "@api/dto/ResetPasswordDto"; +import "@api/dto/SegmentDto"; +import "@api/dto/SignupFlowDto"; +import "@api/dto/StatsDto"; +import "@api/dto/UploadFlowDto"; +import { ValidationTypes } from "class-validator"; + +const schemas = validationMetadatasToSchemas({ + additionalConverters: { + [ValidationTypes.IS_DEFINED]: () => ({}) + }, + refPointerPrefix: "#/components/schemas/", + classTransformerMetadataStorage: defaultMetadataStorage +}); + +const storage = getMetadataArgsStorage(); +const spec = routingControllersToSpec( + storage, + { routePrefix: "/api/1.0" }, + { components: { schemas } } +); + +console.log(JSON.stringify(spec)); diff --git a/src/typings/class-transformer.d.ts b/src/typings/class-transformer.d.ts new file mode 100644 index 000000000..8452fcad5 --- /dev/null +++ b/src/typings/class-transformer.d.ts @@ -0,0 +1,5 @@ +declare module "class-transformer/cjs/storage" { + import type { MetadataStorage } from "class-transformer/types/MetadataStorage"; + + export const defaultMetadataStorage: MetadataStorage; +}