feathers icon indicating copy to clipboard operation
feathers copied to clipboard

Automatically respect TypeBox default value

Open AshotN opened this issue 2 years ago • 10 comments

Steps to reproduce

This is my schema

// Main data model schema
export const clientSchema = Type.Object(
  {
    _id: Type.String(),
    apiKey: Type.String(),
    online: Type.Boolean({ default: false }),
    report: Type.Optional(Type.Any({ default: {} }))
  },
  { $id: 'Client', additionalProperties: false }
)

For the default values to be used I have to specifiy it like so

export const clientDataResolver = resolve<Client, HookContext>({
  properties: {
    online: async () => clientSchema.properties.online.default, // <-- Would be great if that could be avoided
    report: async () => clientSchema.properties.report.default
  }
})

Expected behavior

Use the default value specified in the TypeBox schema

Actual behavior

Have to explicitly use the default value in resolver

System configuration

Tell us about the applicable parts of your setup.

Module versions (especially the part that's not working):

"@feathersjs/feathers": "^5.0.0-pre.32",
"@feathersjs/typebox": "^5.0.0-pre.32",
"@feathersjs/schema": "^5.0.0-pre.32",
 "@feathersjs/mongodb": "^5.0.0-pre.32",

AshotN avatar Oct 29 '22 02:10 AshotN

Well this is an embarrassingly simple fix.

validators.ts export const dataValidator = addFormats(new Ajv({ useDefaults: true }), formats)

AshotN avatar Oct 29 '22 03:10 AshotN

Reopening since this came up before and it's not a big deal adding it to the generate validators file.

daffl avatar Oct 31 '22 02:10 daffl

Re-posting from Discord chat as I think it's relevant to this issue.

I have some confusion on how AJV is intended to handle creation vs update. If I have this given schema

  {
    _id: Type.String(),
    name: Type.String({ default: '[No Name]' }),
    online: Type.Boolean({ default: false }),
    report: Type.Any({ default: {} })
  },
  { $id: 'Clients', additionalProperties: false }
)
...

// Schema for creating new entries
export const clientsDataSchema = Type.Pick(clientsSchema, ['report', 'online'], {
  $id: 'ClientsData',
  additionalProperties: false
})

During creation I don't want online to be specified it should always default to false so I have

export const dataValidator = addFormats(new Ajv({ useDefaults: true }), formats)

In my validators.ts file

This works fine except if I use patch to update the online status and don't pass anything else. AJV automatically fills reports with the default empty array. Feathers then proceeds to wipe my report.

How would I allow for create with AJV defaults but not have those defaults effect a patch request?

AshotN avatar Nov 04 '22 00:11 AshotN

It would be helpful I think if there was a recommended way to handle default values.

I created this utility function with the help of @marshallswain

const withDefault = (defaultValue: any) => {
  return async (val: any, data: any, context: HookContext) => {
    if (context.method === 'create') return val || defaultValue
    return val
  }
}

export const buildDefaults = <_ = Static<TObject>>(schema: TObject) => {
  type T = Static<typeof schema>
  return (Object.keys(schema.properties) as [keyof T]).reduce((acc, propertyName) => {
    const { default: defaultVal } = schema.properties[propertyName]
    if (defaultVal !== undefined) {
      acc[propertyName] = withDefault(defaultVal)
    }
    return acc
  }, {} as { [key in keyof T]: any })
}

Usage

// Main data model schema
export const clientsSchema = Type.Object(
  {
    _id: Type.String(),
    name: Type.Optional(Type.String({ default: '[No Name]' })),
    online: Type.Optional(Type.Boolean({ default: false })),
    report: Type.Optional(Type.Any({ default: {} })),
    enabled_custom_reports: Type.Optional(Type.Array(Type.String(), { default: [] }))
  },
  { $id: 'Clients', additionalProperties: false }
)
...
export const clientsDataResolver = resolve<Clients, HookContext>({
  properties: {
    ...buildDefaults<Clients>(clientsSchema)
  }
})

AshotN avatar Nov 04 '22 04:11 AshotN

@daffl Is this still the recommended way even though the create and the patch schemas have been separated?

philipimperato avatar Dec 27 '22 22:12 philipimperato

Hello, Thanks for the code with buildDefaults @AshotN I have the following issue: as the validator runs before the buildDefaults (here in the clientsDataResolver) I get a validation error, if the field (where the default value should be used) is empty before. Any idea how to solve this or any other way to handle the default values in the schema. The problem with using the default value while creating and patching when using the ajv option still exists.

Thanks, Martin

martinfruehmorgen avatar Jan 06 '23 10:01 martinfruehmorgen

You can move the existing or add a new schemaHooks.resolveData hook to wherever you need it (e.g. before schemaHooks.validateData).

daffl avatar Jan 06 '23 17:01 daffl

Yes, thanks! Creating a 'service'DefaultResolver and adding it before the validation works perfect.

martinfruehmorgen avatar Jan 06 '23 17:01 martinfruehmorgen

i have some problem when prop type set of union [ Date,String ], that buildDefaults will invald

Hareis avatar Apr 22 '23 01:04 Hareis

Ya it won't handle complex types, you would have to modify it to your needs.

AshotN avatar Apr 24 '23 16:04 AshotN