diff --git a/shared/types/form/form.ts b/shared/types/form/form.ts index d1a729ead4..f5422b7559 100644 --- a/shared/types/form/form.ts +++ b/shared/types/form/form.ts @@ -3,7 +3,7 @@ import { FormField, FormFieldDto } from '../field' import { FormLogic } from './form_logic' import { FormLogo } from './form_logo' -import { Merge, PartialDeep, SetRequired } from 'type-fest' +import { Merge, Opaque, PartialDeep } from 'type-fest' import { ADMIN_FORM_META_FIELDS, EMAIL_FORM_SETTINGS_FIELDS, @@ -11,6 +11,9 @@ import { STORAGE_FORM_SETTINGS_FIELDS, STORAGE_PUBLIC_FORM_FIELDS, } from '../../constants/form' +import { DateString } from '../generic' + +export type FormId = Opaque export enum FormColorTheme { Blue = 'blue', @@ -69,32 +72,29 @@ export interface FormBase { title: string admin: UserDto['_id'] - form_fields?: FormField[] - form_logics?: FormLogic[] - permissionList?: FormPermission[] + form_fields: FormField[] + form_logics: FormLogic[] + permissionList: FormPermission[] - startPage?: FormStartPage - endPage?: FormEndPage + startPage: FormStartPage + endPage: FormEndPage - hasCaptcha?: boolean - authType?: FormAuthType + hasCaptcha: boolean + authType: FormAuthType - status?: FormStatus + status: FormStatus - inactiveMessage?: string - submissionLimit?: number | null - isListed?: boolean + inactiveMessage: string + submissionLimit: number | null + isListed: boolean esrvcId?: string msgSrvcName?: string - webhook?: FormWebhook + webhook: FormWebhook responseMode: FormResponseMode - - created?: Date - lastModified?: Date } export interface EmailFormBase extends FormBase { @@ -107,39 +107,19 @@ export interface StorageFormBase extends FormBase { publicKey: string } -export type Form = SetRequired< - T, - | 'form_fields' - | 'form_logics' - | 'permissionList' - | 'startPage' - | 'endPage' - | 'hasCaptcha' - | 'authType' - | 'status' - | 'inactiveMessage' - | 'submissionLimit' - | 'isListed' - | 'webhook' - | 'created' - | 'lastModified' -> +/** + * Additional props to be added/replaced when tranformed into DTO. + */ +type FormDtoBase = { + _id: FormId + form_fields: FormFieldDto[] + created: DateString + lastModified: DateString +} -export type StorageFormDto = Merge< - Form, - { - form_fields: FormFieldDto[] - _id: string - } -> +export type StorageFormDto = Merge -export type EmailFormDto = Merge< - Form, - { - form_fields: FormFieldDto[] - _id: string - } -> +export type EmailFormDto = Merge export type FormDto = StorageFormDto | EmailFormDto diff --git a/src/app/models/form.server.model.ts b/src/app/models/form.server.model.ts index 54b45fac52..7d664e09f3 100644 --- a/src/app/models/form.server.model.ts +++ b/src/app/models/form.server.model.ts @@ -420,16 +420,6 @@ const compileFormModel = (db: Mongoose): IFormModel => { FormLogicPath.discriminator(LogicType.PreventSubmit, PreventSubmitLogicSchema) // Methods - FormSchema.methods.getDashboardView = function (admin: IPopulatedUser) { - return { - _id: this._id, - title: this.title, - status: this.status, - lastModified: this.lastModified, - responseMode: this.responseMode, - admin, - } - } // Method to return myInfo attributes FormSchema.methods.getUniqueMyInfoAttrs = function () { @@ -471,6 +461,19 @@ const compileFormModel = (db: Mongoose): IFormModel => { const FormDocumentSchema = FormSchema as unknown as Schema + FormDocumentSchema.methods.getDashboardView = function ( + admin: IPopulatedUser, + ) { + return { + _id: this._id, + title: this.title, + status: this.status, + lastModified: this.lastModified, + responseMode: this.responseMode, + admin, + } + } + FormDocumentSchema.methods.getSettings = function (): FormSettings { const formSettings = this.responseMode === ResponseMode.Encrypt diff --git a/src/app/modules/form/admin-form/admin-form.controller.ts b/src/app/modules/form/admin-form/admin-form.controller.ts index c74cf143a6..b02bd5330f 100644 --- a/src/app/modules/form/admin-form/admin-form.controller.ts +++ b/src/app/modules/form/admin-form/admin-form.controller.ts @@ -44,6 +44,7 @@ import { SettingsUpdateDto, StartPageUpdateDto, } from '../../../../types/api' +import { DeserializeTransform } from '../../../../types/utils' import { createLoggerWithLabel } from '../../../config/logger' import MailService from '../../../services/mail/mail.service' import { createReqMeta } from '../../../utils/request' @@ -1074,7 +1075,7 @@ export const handleTransferFormOwnership = [ */ export const createForm: ControllerHandler< unknown, - FormDto | ErrorDto, + DeserializeTransform | ErrorDto, { form: CreateFormBodyDto } > = async (req, res) => { const { form: formParams } = req.body @@ -1087,9 +1088,11 @@ export const createForm: ControllerHandler< .andThen((user) => AdminFormService.createForm({ ...formParams, admin: user._id }), ) - .map((createdForm) => - res.status(StatusCodes.OK).json(createdForm as FormDto), - ) + .map((createdForm) => { + return res + .status(StatusCodes.OK) + .json(createdForm as DeserializeTransform) + }) .mapErr((error) => { logger.error({ message: 'Error occurred when creating form', diff --git a/src/app/modules/form/admin-form/admin-form.service.ts b/src/app/modules/form/admin-form/admin-form.service.ts index 4af112344d..4b0fb80737 100644 --- a/src/app/modules/form/admin-form/admin-form.service.ts +++ b/src/app/modules/form/admin-form/admin-form.service.ts @@ -361,24 +361,27 @@ export const transferFormOwnership = ( export const createForm = ( formParams: Merge, ): ResultAsync< - IFormSchema, + IFormDocument, | DatabaseError | DatabaseValidationError | DatabaseConflictError | DatabasePayloadSizeError > => { - return ResultAsync.fromPromise(FormModel.create(formParams), (error) => { - logger.error({ - message: 'Database error encountered when creating form', - meta: { - action: 'createForm', - formParams, - }, - error, - }) + return ResultAsync.fromPromise( + FormModel.create(formParams) as Promise, + (error) => { + logger.error({ + message: 'Database error encountered when creating form', + meta: { + action: 'createForm', + formParams, + }, + error, + }) - return transformMongoError(error) - }) + return transformMongoError(error) + }, + ) } /** diff --git a/src/types/form.ts b/src/types/form.ts index 8c857de058..bb69323cc7 100644 --- a/src/types/form.ts +++ b/src/types/form.ts @@ -1,5 +1,5 @@ import { Document, LeanDocument, Model, ToObjectOptions, Types } from 'mongoose' -import { Merge, SetRequired } from 'type-fest' +import { Merge, SetOptional } from 'type-fest' import { AdminDashboardFormMetaDto, @@ -68,18 +68,42 @@ export type FormOtpData = { export type Permission = FormPermission -export interface IForm extends FormBase { - // Loosen types here to allow for IPopulatedForm extension - admin: any - permission?: Permission[] - form_fields?: FormFieldSchema[] - form_logics?: FormLogicSchema[] +/** + * Keys with defaults in schema. + */ +type FormDefaultableKey = + | 'form_fields' + | 'form_logics' + | 'permissionList' + | 'startPage' + | 'endPage' + | 'hasCaptcha' + | 'authType' + | 'status' + | 'inactiveMessage' + | 'submissionLimit' + | 'isListed' + | 'webhook' - publicKey?: string - // string type is allowed due to a setter on the form schema that transforms - // strings to string array. - emails?: string[] | string -} +export type IForm = Merge< + SetOptional, + { + // Loosen types here to allow for IPopulatedForm extension + admin: any + permission?: Permission[] + form_fields?: FormFieldSchema[] + form_logics?: FormLogicSchema[] + + webhook?: Partial + startPage?: Partial + endPage?: Partial + + publicKey?: string + // string type is allowed due to a setter on the form schema that transforms + // strings to string array. + emails?: string[] | string + } +> /** * Typing for duplicate form with specific keys. @@ -100,6 +124,9 @@ export interface IFormSchema extends IForm, Document, PublicView { form_fields?: Types.DocumentArray | FormFieldSchema[] form_logics?: Types.DocumentArray | FormLogicSchema[] + created?: Date + lastModified?: Date + /** * Replaces the field corresponding to given id to given new field * @param fieldId the id of the field to update @@ -201,12 +228,9 @@ export interface IFormDocument extends IFormSchema { // Hence, using Exclude here over NonNullable. submissionLimit: Exclude isListed: NonNullable - startPage: SetRequired, 'colorTheme'> - endPage: SetRequired< - NonNullable, - 'title' | 'buttonText' - > - webhook: SetRequired, 'url'> + startPage: Required> + endPage: Required> + webhook: Required> } export interface IPopulatedForm extends Omit { diff --git a/src/types/utils.ts b/src/types/utils.ts index 7ab885ce0b..c14d4ac8a7 100644 --- a/src/types/utils.ts +++ b/src/types/utils.ts @@ -1 +1,15 @@ +import { DateString } from '../../shared/types/generic' + export type ExtractTypeFromArray = T extends readonly (infer E)[] ? E : T + +/** + * Helper type to transform DTO types back to their serialized types. + * Currently only used to cast DateStrings back to Date types. + * + * This is useful to transform shared DTO types back to their backend types + * for typing express controller return types, relying on implicit + * JSON.parse(JSON.stringify()) conversions between client and server. + */ +export type DeserializeTransform = { + [K in keyof T]: T[K] extends DateString ? Date : T[K] +}