From 6955f39a90223832b61c1e5ca2f7269335506dc6 Mon Sep 17 00:00:00 2001 From: Mateusz Baginski Date: Sat, 28 Sep 2024 16:14:23 +0200 Subject: [PATCH] feat(backend): add updating organizations endpoint --- .../controllers/organizations.controller.ts | 23 ++++++++-- apps/backend/src/modules/api/helpers/index.ts | 2 +- .../api/helpers/map-tag-to-sdk-errors.ts | 44 ++++++++++++++++++ .../reject-unsafe-create-sdk-errors.ts | 45 ------------------- .../database-record-already-exists.error.ts | 4 +- .../database-record-not-exists.error.ts | 4 +- .../organizations/organizations.firewall.ts | 5 +++ .../organizations/organizations.service.ts | 12 ++++- packages/commons/src/errors/tagged-error.ts | 2 + .../dashboard/organizations/dto/index.ts | 1 + .../dto/sdk-update-organization.dto.ts | 17 +++++++ .../organizations/organizations.sdk.ts | 16 ++++++- packages/sdk/src/shared/errors/index.ts | 1 + .../errors/sdk-record-already-exists.error.ts | 4 +- .../errors/sdk-record-not-found.error.ts | 5 +++ 15 files changed, 131 insertions(+), 54 deletions(-) create mode 100644 apps/backend/src/modules/api/helpers/map-tag-to-sdk-errors.ts delete mode 100644 apps/backend/src/modules/api/helpers/reject-unsafe-create-sdk-errors.ts create mode 100644 packages/sdk/src/modules/dashboard/organizations/dto/sdk-update-organization.dto.ts create mode 100644 packages/sdk/src/shared/errors/sdk-record-not-found.error.ts diff --git a/apps/backend/src/modules/api/controllers/organizations.controller.ts b/apps/backend/src/modules/api/controllers/organizations.controller.ts index 7f52c8d6..1bac6f16 100644 --- a/apps/backend/src/modules/api/controllers/organizations.controller.ts +++ b/apps/backend/src/modules/api/controllers/organizations.controller.ts @@ -5,12 +5,14 @@ import { type OrganizationsSdk, SdkCreateOrganizationInputV, SdKSearchOrganizationsInputV, + SdkUpdateOrganizationInputV, } from '@llm/sdk'; import { ConfigService } from '~/modules/config'; import { OrganizationsService } from '~/modules/organizations'; import { - rejectUnsafeCreateSdkErrors, + mapDbRecordAlreadyExistsToSdkError, + mapDbRecordNotFoundToSdkError, rejectUnsafeSdkErrors, sdkSchemaValidator, serializeSdkResponseTE, @@ -37,15 +39,30 @@ export class OrganizationsController extends AuthorizedController { ), ) .post( - '/create', + '/', sdkSchemaValidator('json', SdkCreateOrganizationInputV), async context => pipe( context.req.valid('json'), organizationsService.asUser(context.var.jwt).create, - rejectUnsafeCreateSdkErrors, + mapDbRecordAlreadyExistsToSdkError, rejectUnsafeSdkErrors, serializeSdkResponseTE>(context), ), + ) + .put( + '/:id', + sdkSchemaValidator('json', SdkUpdateOrganizationInputV), + async context => pipe( + { + id: Number(context.req.param().id), + ...context.req.valid('json'), + }, + organizationsService.asUser(context.var.jwt).update, + mapDbRecordAlreadyExistsToSdkError, + mapDbRecordNotFoundToSdkError, + rejectUnsafeSdkErrors, + serializeSdkResponseTE>(context), + ), ); } } diff --git a/apps/backend/src/modules/api/helpers/index.ts b/apps/backend/src/modules/api/helpers/index.ts index 8e6823c8..4ecdb332 100644 --- a/apps/backend/src/modules/api/helpers/index.ts +++ b/apps/backend/src/modules/api/helpers/index.ts @@ -1,4 +1,4 @@ -export * from './reject-unsafe-create-sdk-errors'; +export * from './map-tag-to-sdk-errors'; export * from './reject-unsafe-sdk-errors'; export * from './respond-with-tagged-error'; export * from './sdk-hono-schema-validator'; diff --git a/apps/backend/src/modules/api/helpers/map-tag-to-sdk-errors.ts b/apps/backend/src/modules/api/helpers/map-tag-to-sdk-errors.ts new file mode 100644 index 00000000..2c089787 --- /dev/null +++ b/apps/backend/src/modules/api/helpers/map-tag-to-sdk-errors.ts @@ -0,0 +1,44 @@ +import { taskEither as TE } from 'fp-ts'; +import { pipe } from 'fp-ts/function'; + +import type { TaggedError } from '@llm/commons'; + +import { isSdkTaggedError, SdkRecordAlreadyExistsError, SdkRecordNotFoundError } from '@llm/sdk'; +import { DatabaseRecordAlreadyExists, DatabaseRecordNotExists } from '~/modules/database'; +import { LoggerService } from '~/modules/logger'; + +export function mapTagToSdkError>( + catchTag: ET, + mapper: (err: TaggedError) => RET, +) { + const logger = LoggerService.of('mapTagToSdkError'); + + return >(task: TE.TaskEither) => pipe( + task, + TE.mapLeft((error) => { + if (isSdkTaggedError(error) || error.tag !== catchTag) { + return error; + } + + const { stack, ...context } = error; + + logger.error(`Mapped creator SDK error - ${error.tag}!`, context); + + if (stack) { + console.error(stack); + } + + return mapper(error); + }), + ) as TE.TaskEither> | RET, T>; +} + +export const mapDbRecordAlreadyExistsToSdkError = mapTagToSdkError( + DatabaseRecordAlreadyExists.tag, + () => new SdkRecordAlreadyExistsError({}), +); + +export const mapDbRecordNotFoundToSdkError = mapTagToSdkError( + DatabaseRecordNotExists.tag, + () => new SdkRecordNotFoundError({}), +); diff --git a/apps/backend/src/modules/api/helpers/reject-unsafe-create-sdk-errors.ts b/apps/backend/src/modules/api/helpers/reject-unsafe-create-sdk-errors.ts deleted file mode 100644 index 41d64e4a..00000000 --- a/apps/backend/src/modules/api/helpers/reject-unsafe-create-sdk-errors.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { taskEither as TE } from 'fp-ts'; -import { pipe } from 'fp-ts/function'; - -import type { TaggedError } from '@llm/commons'; - -import { - isSdkTaggedError, - SdkRecordAlreadyExistsError, -} from '@llm/sdk'; -import { LoggerService } from '~/modules/logger'; - -export function rejectUnsafeCreateSdkErrors>(task: TE.TaskEither) { - const logger = LoggerService.of('rejectUnsafeSdkErrors'); - - return pipe( - task, - TE.mapLeft((error) => { - if (isSdkTaggedError(error)) { - return error; - } - - const mappedError = (() => { - switch (error.tag) { - case 'DatabaseRecordAlreadyExists': - return new SdkRecordAlreadyExistsError({}); - - default: - return error; - } - })(); - - if (mappedError !== error) { - const { stack, ...context } = error; - - logger.error(`Mapped creator SDK error - ${error.tag}!`, context); - - if (stack) { - console.error(stack); - } - } - - return mappedError; - }), - ); -} diff --git a/apps/backend/src/modules/database/errors/database-record-already-exists.error.ts b/apps/backend/src/modules/database/errors/database-record-already-exists.error.ts index f84b21cd..0ea34f47 100644 --- a/apps/backend/src/modules/database/errors/database-record-already-exists.error.ts +++ b/apps/backend/src/modules/database/errors/database-record-already-exists.error.ts @@ -1,3 +1,5 @@ import { TaggedError } from '@llm/commons'; -export class DatabaseRecordAlreadyExists extends TaggedError.ofLiteral()('DatabaseRecordAlreadyExists') {} +export class DatabaseRecordAlreadyExists extends TaggedError.ofLiteral()('DatabaseRecordAlreadyExists') { + readonly httpCode = 403; +} diff --git a/apps/backend/src/modules/database/errors/database-record-not-exists.error.ts b/apps/backend/src/modules/database/errors/database-record-not-exists.error.ts index 8e15c22a..12282c19 100644 --- a/apps/backend/src/modules/database/errors/database-record-not-exists.error.ts +++ b/apps/backend/src/modules/database/errors/database-record-not-exists.error.ts @@ -1,3 +1,5 @@ import { TaggedError } from '@llm/commons'; -export class DatabaseRecordNotExists extends TaggedError.ofLiteral()('DatabaseRecordNotExists') {} +export class DatabaseRecordNotExists extends TaggedError.ofLiteral()('DatabaseRecordNotExists') { + readonly httpCode = 404; +} diff --git a/apps/backend/src/modules/organizations/organizations.firewall.ts b/apps/backend/src/modules/organizations/organizations.firewall.ts index 082f3667..92c330e9 100644 --- a/apps/backend/src/modules/organizations/organizations.firewall.ts +++ b/apps/backend/src/modules/organizations/organizations.firewall.ts @@ -14,6 +14,11 @@ export class OrganizationsFirewall extends AuthFirewallService { super(jwt); } + update = flow( + this.organizationsService.update, + this.tryTEIfUser.is.root, + ); + create = flow( this.organizationsService.create, this.tryTEIfUser.is.root, diff --git a/apps/backend/src/modules/organizations/organizations.service.ts b/apps/backend/src/modules/organizations/organizations.service.ts index 8674dfdc..f9209b94 100644 --- a/apps/backend/src/modules/organizations/organizations.service.ts +++ b/apps/backend/src/modules/organizations/organizations.service.ts @@ -1,11 +1,16 @@ import { pipe } from 'fp-ts/function'; import { inject, injectable } from 'tsyringe'; -import type { SdkCreateOrganizationInputT, SdkJwtTokenT } from '@llm/sdk'; +import type { + SdkCreateOrganizationInputT, + SdkJwtTokenT, + SdkUpdateOrganizationInputT, +} from '@llm/sdk'; import { tapTaskEitherTE } from '@llm/commons'; import type { WithAuthFirewall } from '../auth'; +import type { TableRowWithId } from '../database'; import { OrganizationsEsIndexRepo } from './elasticsearch'; import { OrganizationsEsSearchRepo } from './elasticsearch/organizations-es-search.repo'; @@ -27,5 +32,10 @@ export class OrganizationsService implements WithAuthFirewall this.esIndexRepo.findAndIndexDocumentById(id)), ); + update = ({ id, ...value }: SdkUpdateOrganizationInputT & TableRowWithId) => pipe( + this.repo.update({ id, value }), + tapTaskEitherTE(() => this.esIndexRepo.findAndIndexDocumentById(id)), + ); + asUser = (jwt: SdkJwtTokenT) => new OrganizationsFirewall(jwt, this); } diff --git a/packages/commons/src/errors/tagged-error.ts b/packages/commons/src/errors/tagged-error.ts index 1f8c3f43..5fc5d855 100644 --- a/packages/commons/src/errors/tagged-error.ts +++ b/packages/commons/src/errors/tagged-error.ts @@ -66,6 +66,8 @@ export abstract class TaggedError< static ofLiteral() { return (tag: S) => class TaggedLiteralError extends TaggedError { + static readonly tag = tag; + readonly tag = tag; }; } diff --git a/packages/sdk/src/modules/dashboard/organizations/dto/index.ts b/packages/sdk/src/modules/dashboard/organizations/dto/index.ts index 18914a50..c6b61f21 100644 --- a/packages/sdk/src/modules/dashboard/organizations/dto/index.ts +++ b/packages/sdk/src/modules/dashboard/organizations/dto/index.ts @@ -3,3 +3,4 @@ export * from './sdk-create-organization-user.dto'; export * from './sdk-organization.dto'; export * from './sdk-organization-user.dto'; export * from './sdk-search-organizations.dto'; +export * from './sdk-update-organization.dto'; diff --git a/packages/sdk/src/modules/dashboard/organizations/dto/sdk-update-organization.dto.ts b/packages/sdk/src/modules/dashboard/organizations/dto/sdk-update-organization.dto.ts new file mode 100644 index 00000000..5b2de637 --- /dev/null +++ b/packages/sdk/src/modules/dashboard/organizations/dto/sdk-update-organization.dto.ts @@ -0,0 +1,17 @@ +import type { z } from 'zod'; + +import { SdkTableRowWithIdV, ZodOmitArchivedFields, ZodOmitDateFields } from '~/shared'; + +import { SdkOrganizationV } from './sdk-organization.dto'; + +export const SdkUpdateOrganizationInputV = SdkOrganizationV.omit({ + ...ZodOmitDateFields, + ...ZodOmitArchivedFields, + id: true, +}); + +export type SdkUpdateOrganizationInputT = z.infer; + +export const SdkUpdateOrganizationOutputV = SdkTableRowWithIdV; + +export type SdkUpdateOrganizationOutputT = z.infer; diff --git a/packages/sdk/src/modules/dashboard/organizations/organizations.sdk.ts b/packages/sdk/src/modules/dashboard/organizations/organizations.sdk.ts index 87b94e98..08db3a61 100644 --- a/packages/sdk/src/modules/dashboard/organizations/organizations.sdk.ts +++ b/packages/sdk/src/modules/dashboard/organizations/organizations.sdk.ts @@ -3,7 +3,10 @@ import { getPayload, performApiRequest, postPayload, + putPayload, type SdkRecordAlreadyExistsError, + type SdkRecordNotFoundError, + type SdkTableRowWithIdT, } from '~/shared'; import type { @@ -11,6 +14,8 @@ import type { SdkCreateOrganizationOutputT, SdKSearchOrganizationsInputT, SdKSearchOrganizationsOutputT, + SdkUpdateOrganizationInputT, + SdkUpdateOrganizationOutputT, } from './dto'; export class OrganizationsSdk extends AbstractNestedSdkWithAuth { @@ -25,7 +30,16 @@ export class OrganizationsSdk extends AbstractNestedSdkWithAuth { create = (data: SdkCreateOrganizationInputT) => performApiRequest({ - url: this.endpoint('/create'), + url: this.endpoint('/'), options: postPayload(data), }); + + update = ({ id, ...data }: SdkUpdateOrganizationInputT & SdkTableRowWithIdT) => + performApiRequest< + SdkUpdateOrganizationOutputT, + SdkRecordAlreadyExistsError | SdkRecordNotFoundError + >({ + url: this.endpoint(`/${id}`), + options: putPayload(data), + }); }; diff --git a/packages/sdk/src/shared/errors/index.ts b/packages/sdk/src/shared/errors/index.ts index eafe6f44..847ff2c0 100644 --- a/packages/sdk/src/shared/errors/index.ts +++ b/packages/sdk/src/shared/errors/index.ts @@ -1,6 +1,7 @@ export * from './is-sdk-tagged-error'; export * from './sdk-payload-validation.error'; export * from './sdk-record-already-exists.error'; +export * from './sdk-record-not-found.error'; export * from './sdk-request.error'; export * from './sdk-server.error'; export * from './sdk-unauthorized.error'; diff --git a/packages/sdk/src/shared/errors/sdk-record-already-exists.error.ts b/packages/sdk/src/shared/errors/sdk-record-already-exists.error.ts index 7fbf437b..6ed09982 100644 --- a/packages/sdk/src/shared/errors/sdk-record-already-exists.error.ts +++ b/packages/sdk/src/shared/errors/sdk-record-already-exists.error.ts @@ -1,3 +1,5 @@ import { TaggedError } from '@llm/commons'; -export class SdkRecordAlreadyExistsError extends TaggedError.ofLiteral()('SdkRecordAlreadyExistsError') {} +export class SdkRecordAlreadyExistsError extends TaggedError.ofLiteral()('SdkRecordAlreadyExistsError') { + static readonly httpCode = 403; +} diff --git a/packages/sdk/src/shared/errors/sdk-record-not-found.error.ts b/packages/sdk/src/shared/errors/sdk-record-not-found.error.ts new file mode 100644 index 00000000..f4626054 --- /dev/null +++ b/packages/sdk/src/shared/errors/sdk-record-not-found.error.ts @@ -0,0 +1,5 @@ +import { TaggedError } from '@llm/commons'; + +export class SdkRecordNotFoundError extends TaggedError.ofLiteral()('SdkRecordNotFoundError') { + readonly httpCode = 404; +}