Skip to content

Commit

Permalink
ref: set all default keys in FormBase to be required
Browse files Browse the repository at this point in the history
reverse the schema to make form base keys optional when extending
  • Loading branch information
karrui committed Jul 16, 2021
1 parent 835171f commit bdeed1a
Show file tree
Hide file tree
Showing 6 changed files with 118 additions and 91 deletions.
74 changes: 27 additions & 47 deletions shared/types/form/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@ 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,
EMAIL_PUBLIC_FORM_FIELDS,
STORAGE_FORM_SETTINGS_FIELDS,
STORAGE_PUBLIC_FORM_FIELDS,
} from '../../constants/form'
import { DateString } from '../generic'

export type FormId = Opaque<string, 'FormId'>

export enum FormColorTheme {
Blue = 'blue',
Expand Down Expand Up @@ -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 {
Expand All @@ -107,39 +107,19 @@ export interface StorageFormBase extends FormBase {
publicKey: string
}

export type Form<T extends FormBase = FormBase> = 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<StorageFormBase>,
{
form_fields: FormFieldDto[]
_id: string
}
>
export type StorageFormDto = Merge<StorageFormBase, FormDtoBase>

export type EmailFormDto = Merge<
Form<EmailFormBase>,
{
form_fields: FormFieldDto[]
_id: string
}
>
export type EmailFormDto = Merge<EmailFormBase, FormDtoBase>

export type FormDto = StorageFormDto | EmailFormDto

Expand Down
23 changes: 13 additions & 10 deletions src/app/models/form.server.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -471,6 +461,19 @@ const compileFormModel = (db: Mongoose): IFormModel => {

const FormDocumentSchema = FormSchema as unknown as Schema<IFormDocument>

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
Expand Down
11 changes: 7 additions & 4 deletions src/app/modules/form/admin-form/admin-form.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -1074,7 +1075,7 @@ export const handleTransferFormOwnership = [
*/
export const createForm: ControllerHandler<
unknown,
FormDto | ErrorDto,
DeserializeTransform<FormDto> | ErrorDto,
{ form: CreateFormBodyDto }
> = async (req, res) => {
const { form: formParams } = req.body
Expand All @@ -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<FormDto>)
})
.mapErr((error) => {
logger.error({
message: 'Error occurred when creating form',
Expand Down
27 changes: 15 additions & 12 deletions src/app/modules/form/admin-form/admin-form.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,24 +361,27 @@ export const transferFormOwnership = (
export const createForm = (
formParams: Merge<IForm, { admin: string }>,
): 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<IFormDocument>,
(error) => {
logger.error({
message: 'Database error encountered when creating form',
meta: {
action: 'createForm',
formParams,
},
error,
})

return transformMongoError(error)
})
return transformMongoError(error)
},
)
}

/**
Expand Down
60 changes: 42 additions & 18 deletions src/types/form.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<FormBase, FormDefaultableKey>,
{
// Loosen types here to allow for IPopulatedForm extension
admin: any
permission?: Permission[]
form_fields?: FormFieldSchema[]
form_logics?: FormLogicSchema[]

webhook?: Partial<FormBase['webhook']>
startPage?: Partial<FormBase['startPage']>
endPage?: Partial<FormBase['endPage']>

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.
Expand All @@ -100,6 +124,9 @@ export interface IFormSchema extends IForm, Document, PublicView<PublicForm> {
form_fields?: Types.DocumentArray<FormFieldSchema> | FormFieldSchema[]
form_logics?: Types.DocumentArray<FormLogicSchema> | 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
Expand Down Expand Up @@ -201,12 +228,9 @@ export interface IFormDocument extends IFormSchema {
// Hence, using Exclude here over NonNullable.
submissionLimit: Exclude<IFormSchema['submissionLimit'], undefined>
isListed: NonNullable<IFormSchema['isListed']>
startPage: SetRequired<NonNullable<IFormSchema['startPage']>, 'colorTheme'>
endPage: SetRequired<
NonNullable<IFormSchema['endPage']>,
'title' | 'buttonText'
>
webhook: SetRequired<NonNullable<IFormSchema['webhook']>, 'url'>
startPage: Required<NonNullable<IFormSchema['startPage']>>
endPage: Required<NonNullable<IFormSchema['endPage']>>
webhook: Required<NonNullable<IFormSchema['webhook']>>
}

export interface IPopulatedForm extends Omit<IFormDocument, 'toJSON'> {
Expand Down
14 changes: 14 additions & 0 deletions src/types/utils.ts
Original file line number Diff line number Diff line change
@@ -1 +1,15 @@
import { DateString } from '../../shared/types/generic'

export type ExtractTypeFromArray<T> = 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<T> = {
[K in keyof T]: T[K] extends DateString ? Date : T[K]
}

0 comments on commit bdeed1a

Please sign in to comment.