diff --git a/packages/cli/src/app/templates/logger.tpl.ts b/packages/cli/src/app/templates/logger.tpl.ts index dd6781908d..709808126f 100644 --- a/packages/cli/src/app/templates/logger.tpl.ts +++ b/packages/cli/src/app/templates/logger.tpl.ts @@ -22,8 +22,14 @@ export const logger = createLogger({ export const logErrorHook = async (context: HookContext, next: NextFunction) => { try { await next() - } catch (error) { - logger.error(error) + } catch (error: any) { + logger.error(error.stack) + + // Log validation errors + if (error.errors) { + logger.error(error.errors) + } + throw error } } diff --git a/packages/cli/src/app/templates/schemas.tpl.ts b/packages/cli/src/app/templates/schemas.tpl.ts index e0020b297e..2182986a78 100644 --- a/packages/cli/src/app/templates/schemas.tpl.ts +++ b/packages/cli/src/app/templates/schemas.tpl.ts @@ -30,7 +30,7 @@ export const queryValidator = addFormats(new Ajv({ ` const configurationJsonTemplate = - ({}: AppGeneratorContext) => /* ts */ `import { defaultAppSettings, jsonSchema } from '@feathersjs/schema' + ({}: AppGeneratorContext) => /* ts */ `import { defaultAppSettings, getValidator } from '@feathersjs/schema' import type { FromSchema } from '@feathersjs/schema' import { dataValidator } from './validators' @@ -47,14 +47,13 @@ export const configurationSchema = { } } as const -export const configurationValidator = jsonSchema.getValidator(configurationSchema, dataValidator) +export const configurationValidator = getValidator(configurationSchema, dataValidator) export type ApplicationConfiguration = FromSchema ` const configurationTypeboxTemplate = - ({}: AppGeneratorContext) => /* ts */ `import { jsonSchema } from '@feathersjs/schema' -import { Type, defaultAppConfiguration } from '@feathersjs/typebox' + ({}: AppGeneratorContext) => /* ts */ `import { Type, getValidator, defaultAppConfiguration } from '@feathersjs/typebox' import type { Static } from '@feathersjs/typebox' import { dataValidator } from './validators' @@ -70,7 +69,7 @@ export const configurationSchema = Type.Intersect([ export type ApplicationConfiguration = Static -export const configurationValidator = jsonSchema.getValidator(configurationSchema, dataValidator) +export const configurationValidator = getValidator(configurationSchema, dataValidator) ` export const generate = (ctx: AppGeneratorContext) => diff --git a/packages/cli/src/authentication/templates/knex.tpl.ts b/packages/cli/src/authentication/templates/knex.tpl.ts index f7a2ac2e64..0ad4f8f31c 100644 --- a/packages/cli/src/authentication/templates/knex.tpl.ts +++ b/packages/cli/src/authentication/templates/knex.tpl.ts @@ -3,12 +3,12 @@ import { getDatabaseAdapter, renderSource } from '../../commons' import { AuthenticationGeneratorContext } from '../index' const migrationTemplate = ({ - kebabName, + kebabPath, authStrategies }: AuthenticationGeneratorContext) => /* ts */ `import type { Knex } from 'knex' export async function up(knex: Knex): Promise { - await knex.schema.alterTable('${kebabName}', function (table) { + await knex.schema.alterTable('${kebabPath}', function (table) { table.dropColumn('text')${authStrategies .map((name) => name === 'local' @@ -23,7 +23,7 @@ export async function up(knex: Knex): Promise { } export async function down(knex: Knex): Promise { - await knex.schema.alterTable('${kebabName}', function (table) { + await knex.schema.alterTable('${kebabPath}', function (table) { table.string('text')${authStrategies .map((name) => name === 'local' diff --git a/packages/cli/src/authentication/templates/schema.json.tpl.ts b/packages/cli/src/authentication/templates/schema.json.tpl.ts index 6e7ef992fd..dfb6b1fcad 100644 --- a/packages/cli/src/authentication/templates/schema.json.tpl.ts +++ b/packages/cli/src/authentication/templates/schema.json.tpl.ts @@ -8,20 +8,23 @@ const template = ({ authStrategies, type, relative -}: AuthenticationGeneratorContext) => /* ts */ `import { resolve, jsonSchema } from '@feathersjs/schema' +}: AuthenticationGeneratorContext) => /* ts */ `import { resolve, querySyntax, getValidator, getDataValidator } from '@feathersjs/schema' import type { FromSchema } from '@feathersjs/schema' ${authStrategies.includes('local') ? `import { passwordHash } from '@feathersjs/authentication-local'` : ''} import type { HookContext } from '${relative}/declarations' import { dataValidator, queryValidator } from '${relative}/schemas/validators' -// Schema for the basic data model (e.g. creating new entries) -export const ${camelName}DataSchema = { - $id: '${upperName}Data', +// Main data model schema +export const ${camelName}Schema = { + $id: '${upperName}', type: 'object', additionalProperties: false, - required: [ ${authStrategies.includes('local') ? "'email'" : ''} ], + required: [ '${type === 'mongodb' ? '_id' : 'id'}'${authStrategies.includes('local') ? ", 'email'" : ''} ], properties: { + ${type === 'mongodb' ? '_id' : 'id'}: { + type: '${type === 'mongodb' ? 'string' : 'number'}' + }, ${authStrategies .map((name) => name === 'local' @@ -32,30 +35,27 @@ export const ${camelName}DataSchema = { .join(',\n')} } } as const -export type ${upperName}Data = FromSchema -export const ${camelName}DataValidator = jsonSchema.getDataValidator(${camelName}DataSchema, dataValidator) -export const ${camelName}DataResolver = resolve<${upperName}Data, HookContext>({ - properties: { - ${authStrategies.includes('local') ? `password: passwordHash({ strategy: 'local' })` : ''} - } +export type ${upperName} = FromSchema +export const ${camelName}Resolver = resolve<${upperName}, HookContext>({ + properties: {} }) -// Schema for the data that is being returned -export const ${camelName}Schema = { - $id: '${upperName}', +// Schema for the basic data model (e.g. creating new entries) +export const ${camelName}DataSchema = { + $id: '${upperName}Data', type: 'object', additionalProperties: false, - required: [ ...${camelName}DataSchema.required, '${type === 'mongodb' ? '_id' : 'id'}' ], + required: [ ], properties: { - ...${camelName}DataSchema.properties, - ${type === 'mongodb' ? '_id' : 'id'}: { - type: '${type === 'mongodb' ? 'string' : 'number'}' - } + ...${camelName}Schema.properties } } as const -export type ${upperName} = FromSchema -export const ${camelName}Resolver = resolve<${upperName}, HookContext>({ - properties: {} +export type ${upperName}Data = FromSchema +export const ${camelName}DataValidator = getDataValidator(${camelName}DataSchema, dataValidator) +export const ${camelName}DataResolver = resolve<${upperName}Data, HookContext>({ + properties: { + ${authStrategies.includes('local') ? `password: passwordHash({ strategy: 'local' })` : ''} + } }) export const ${camelName}ExternalResolver = resolve<${upperName}, HookContext>({ @@ -71,11 +71,11 @@ export const ${camelName}QuerySchema = { type: 'object', additionalProperties: false, properties: { - ...jsonSchema.querySyntax(${camelName}Schema.properties) + ...querySyntax(${camelName}Schema.properties) } } as const export type ${upperName}Query = FromSchema -export const ${camelName}QueryValidator = jsonSchema.getValidator(${camelName}QuerySchema, queryValidator) +export const ${camelName}QueryValidator = getValidator(${camelName}QuerySchema, queryValidator) export const ${camelName}QueryResolver = resolve<${upperName}Query, HookContext>({ properties: { // If there is a user (e.g. with authentication), they are only allowed to see their own data diff --git a/packages/cli/src/authentication/templates/schema.typebox.tpl.ts b/packages/cli/src/authentication/templates/schema.typebox.tpl.ts index 66b3eb5599..36a3ccba90 100644 --- a/packages/cli/src/authentication/templates/schema.typebox.tpl.ts +++ b/packages/cli/src/authentication/templates/schema.typebox.tpl.ts @@ -8,40 +8,26 @@ export const template = ({ authStrategies, type, relative -}: AuthenticationGeneratorContext) => /* ts */ `import { jsonSchema, resolve } from '@feathersjs/schema' -import { Type, querySyntax } from '@feathersjs/typebox' +}: AuthenticationGeneratorContext) => /* ts */ `import { resolve } from '@feathersjs/schema' +import { Type, getDataValidator, getValidator, querySyntax } from '@feathersjs/typebox' import type { Static } from '@feathersjs/typebox' ${authStrategies.includes('local') ? `import { passwordHash } from '@feathersjs/authentication-local'` : ''} import type { HookContext } from '${relative}/declarations' import { dataValidator, queryValidator } from '${relative}/schemas/validators' -// Schema for the basic data model (e.g. creating new entries) -export const ${camelName}DataSchema = Type.Object({ +// Main data model schema +export const ${camelName}Schema = Type.Object({ + ${type === 'mongodb' ? '_id: Type.String()' : 'id: Type.Number()'}, ${authStrategies .map((name) => name === 'local' ? ` email: Type.String(), - password: Type.String()` + password: Type.Optional(Type.String())` : ` ${name}Id: Type.Optional(Type.String())` ) .join(',\n')} -}, { $id: '${upperName}Data', additionalProperties: false }) -export type ${upperName}Data = Static -export const ${camelName}DataValidator = jsonSchema.getDataValidator(${camelName}DataSchema, dataValidator) -export const ${camelName}DataResolver = resolve<${upperName}Data, HookContext>({ - properties: { - ${authStrategies.includes('local') ? `password: passwordHash({ strategy: 'local' })` : ''} - } -}) - -// Schema for the data that is being returned -export const ${camelName}Schema = Type.Intersect([ - ${camelName}DataSchema, - Type.Object({ - ${type === 'mongodb' ? '_id: Type.String()' : 'id: Type.Number()'} - }) -], { $id: '${upperName}' }) +},{ $id: '${upperName}', additionalProperties: false }) export type ${upperName} = Static export const ${camelName}Resolver = resolve<${upperName}, HookContext>({ properties: {} @@ -54,14 +40,28 @@ export const ${camelName}ExternalResolver = resolve<${upperName}, HookContext>({ } }) +// Schema for the basic data model (e.g. creating new entries) +export const ${camelName}DataSchema = Type.Pick(${camelName}Schema, [ + ${authStrategies.map((name) => (name === 'local' ? `'email', 'password'` : `'${name}Id'`)).join(', ')} +], + { $id: '${upperName}Data', additionalProperties: false } +) +export type ${upperName}Data = Static +export const ${camelName}DataValidator = getDataValidator(${camelName}DataSchema, dataValidator) +export const ${camelName}DataResolver = resolve<${upperName}, HookContext>({ + properties: { + ${authStrategies.includes('local') ? `password: passwordHash({ strategy: 'local' })` : ''} + } +}) + // Schema for allowed query properties -export const ${camelName}QuerySchema = Type.Intersect([ - querySyntax(${camelName}Schema), - // Add additional query properties here - Type.Object({}) +export const ${camelName}QueryProperties = Type.Pick(${camelName}Schema, ['${ + type === 'mongodb' ? '_id' : 'id' +}', ${authStrategies.map((name) => (name === 'local' ? `'email'` : `'${name}Id'`)).join(', ')} ]) +export const ${camelName}QuerySchema = querySyntax(${camelName}QueryProperties) export type ${upperName}Query = Static -export const ${camelName}QueryValidator = jsonSchema.getValidator(${camelName}QuerySchema, queryValidator) +export const ${camelName}QueryValidator = getValidator(${camelName}QuerySchema, queryValidator) export const ${camelName}QueryResolver = resolve<${upperName}Query, HookContext>({ properties: { // If there is a user (e.g. with authentication), they are only allowed to see their own data diff --git a/packages/cli/src/service/index.ts b/packages/cli/src/service/index.ts index d40aef02f9..3011cd155c 100644 --- a/packages/cli/src/service/index.ts +++ b/packages/cli/src/service/index.ts @@ -41,6 +41,10 @@ export interface ServiceGeneratorContext extends FeathersBaseContext { * The actual filename (the last element of the path) */ fileName: string + /** + * The kebab-cased name of the path. Will be used for e.g. database names + */ + kebabPath: string /** * Indicates how many file paths we should go up to import other things (e.g. `../../`) */ @@ -77,7 +81,7 @@ export const generate = (ctx: ServiceGeneratorArguments) => .then(checkPreconditions()) .then( prompt( - ({ name, path, type, schema, authentication, isEntityService }) => [ + ({ name, path, type, schema, authentication, isEntityService, feathers }) => [ { name: 'name', type: 'input', @@ -116,7 +120,7 @@ export const generate = (ctx: ServiceGeneratorArguments) => type: 'list', when: !type, message: 'What kind of service is it?', - default: getDatabaseAdapter(ctx.feathers?.database), + default: getDatabaseAdapter(feathers?.database), choices: [ { value: 'knex', @@ -137,7 +141,7 @@ export const generate = (ctx: ServiceGeneratorArguments) => type: 'list', when: schema === undefined, message: 'Which schema definition format do you want to use?', - default: ctx.feathers?.schema || 'json', + default: feathers?.schema, choices: [ { value: 'typebox', @@ -156,7 +160,7 @@ export const generate = (ctx: ServiceGeneratorArguments) => ] ) ) - .then(async (ctx) => { + .then(async (ctx): Promise => { const { name, path, type } = ctx const kebabName = _.kebabCase(name) const camelName = _.camelCase(name) @@ -166,6 +170,7 @@ export const generate = (ctx: ServiceGeneratorArguments) => const folder = path.split('/').filter((el) => el !== '') const relative = ['', ...folder].map(() => '..').join('/') const fileName = _.last(folder) + const kebabPath = _.kebabCase(path) return { name, @@ -177,6 +182,7 @@ export const generate = (ctx: ServiceGeneratorArguments) => className, kebabName, camelName, + kebabPath, relative, ...ctx } diff --git a/packages/cli/src/service/templates/schema.json.tpl.ts b/packages/cli/src/service/templates/schema.json.tpl.ts index c377ebb9ae..f713559151 100644 --- a/packages/cli/src/service/templates/schema.json.tpl.ts +++ b/packages/cli/src/service/templates/schema.json.tpl.ts @@ -7,48 +7,50 @@ const template = ({ upperName, relative, type -}: ServiceGeneratorContext) => /* ts */ `import { jsonSchema, resolve } from '@feathersjs/schema' +}: ServiceGeneratorContext) => /* ts */ `import { resolve, getDataValidator, getValidator, querySyntax } from '@feathersjs/schema' import type { FromSchema } from '@feathersjs/schema' import type { HookContext } from '${relative}/declarations' import { dataValidator, queryValidator } from '${relative}/schemas/validators' -// Schema for the basic data model (e.g. creating new entries) -export const ${camelName}DataSchema = { - $id: '${upperName}Data', +// Main data model schema +export const ${camelName}Schema = { + $id: '${upperName}', type: 'object', additionalProperties: false, - required: [ 'text' ], + required: [ '${type === 'mongodb' ? '_id' : 'id'}', 'text' ], properties: { + ${type === 'mongodb' ? '_id' : 'id'}: { + type: '${type === 'mongodb' ? 'string' : 'number'}' + }, text: { type: 'string' } } } as const -export type ${upperName}Data = FromSchema -export const ${camelName}DataValidator = jsonSchema.getDataValidator(${camelName}DataSchema, dataValidator) -export const ${camelName}DataResolver = resolve<${upperName}Data, HookContext>({ +export type ${upperName} = FromSchema +export const ${camelName}Resolver = resolve<${upperName}, HookContext>({ + properties: {} +}) +export const ${camelName}ExternalResolver = resolve<${upperName}, HookContext>({ properties: {} }) -// Schema for the data that is being returned -export const ${camelName}Schema = { - $id: '${upperName}', +// Schema for creating new data +export const ${camelName}DataSchema = { + $id: '${upperName}Data', type: 'object', additionalProperties: false, - required: [ ...${camelName}DataSchema.required, '${type === 'mongodb' ? '_id' : 'id'}' ], + required: [ 'text' ], properties: { - ...${camelName}DataSchema.properties, - ${type === 'mongodb' ? '_id' : 'id'}: { - type: '${type === 'mongodb' ? 'string' : 'number'}' + text: { + type: 'string' } } } as const -export type ${upperName} = FromSchema -export const ${camelName}Resolver = resolve<${upperName}, HookContext>({ - properties: {} -}) -export const ${camelName}ExternalResolver = resolve<${upperName}, HookContext>({ +export type ${upperName}Data = FromSchema +export const ${camelName}DataValidator = getDataValidator(${camelName}DataSchema, dataValidator) +export const ${camelName}DataResolver = resolve<${upperName}Data, HookContext>({ properties: {} }) @@ -58,11 +60,11 @@ export const ${camelName}QuerySchema = { type: 'object', additionalProperties: false, properties: { - ...jsonSchema.querySyntax(${camelName}Schema.properties) + ...querySyntax(${camelName}Schema.properties) } } as const export type ${upperName}Query = FromSchema -export const ${camelName}QueryValidator = jsonSchema.getValidator(${camelName}QuerySchema, queryValidator) +export const ${camelName}QueryValidator = getValidator(${camelName}QuerySchema, queryValidator) export const ${camelName}QueryResolver = resolve<${upperName}Query, HookContext>({ properties: {} }) diff --git a/packages/cli/src/service/templates/schema.typebox.tpl.ts b/packages/cli/src/service/templates/schema.typebox.tpl.ts index b2cd3f2a2a..761472a105 100644 --- a/packages/cli/src/service/templates/schema.typebox.tpl.ts +++ b/packages/cli/src/service/templates/schema.typebox.tpl.ts @@ -7,30 +7,18 @@ const template = ({ upperName, relative, type -}: ServiceGeneratorContext) => /* ts */ `import { jsonSchema, resolve } from '@feathersjs/schema' -import { Type, querySyntax } from '@feathersjs/typebox' +}: ServiceGeneratorContext) => /* ts */ `import { resolve } from '@feathersjs/schema' +import { Type, getDataValidator, getValidator, querySyntax } from '@feathersjs/typebox' import type { Static } from '@feathersjs/typebox' import type { HookContext } from '${relative}/declarations' import { dataValidator, queryValidator } from '${relative}/schemas/validators' -// Schema for the basic data model (e.g. creating new entries) -export const ${camelName}DataSchema = Type.Object({ - text: Type.String() -}, { $id: '${upperName}Data', additionalProperties: false }) -export type ${upperName}Data = Static -export const ${camelName}DataValidator = jsonSchema.getDataValidator(${camelName}DataSchema, dataValidator) -export const ${camelName}DataResolver = resolve<${upperName}Data, HookContext>({ - properties: {} -}) - -// Schema for the data that is being returned -export const ${camelName}Schema = Type.Intersect([ - ${camelName}DataSchema, - Type.Object({ - ${type === 'mongodb' ? '_id: Type.String()' : 'id: Type.Number()'} - }) -], { $id: '${upperName}', additionalProperties: false }) +// Main data model schema +export const ${camelName}Schema = Type.Object({ + ${type === 'mongodb' ? '_id: Type.String()' : 'id: Type.Number()'}, + text: Type.String() + }, { $id: '${upperName}', additionalProperties: false }) export type ${upperName} = Static export const ${camelName}Resolver = resolve<${upperName}, HookContext>({ properties: {} @@ -40,14 +28,23 @@ export const ${camelName}ExternalResolver = resolve<${upperName}, HookContext>({ properties: {} }) +// Schema for creating new entries +export const ${camelName}DataSchema = Type.Pick(${camelName}Schema, ['text'], { + $id: '${upperName}Data', additionalProperties: false +}) +export type ${upperName}Data = Static +export const ${camelName}DataValidator = getDataValidator(${camelName}DataSchema, dataValidator) +export const ${camelName}DataResolver = resolve<${upperName}, HookContext>({ + properties: {} +}) + // Schema for allowed query properties -export const ${camelName}QuerySchema = Type.Intersect([ - querySyntax(${camelName}Schema), - // Add additional query properties here - Type.Object({}) -]) +export const ${camelName}QueryProperties = Type.Pick(${camelName}Schema, [ + '${type === 'mongodb' ? '_id' : 'id'}', 'text' +], { additionalProperties: false }) +export const ${camelName}QuerySchema = querySyntax(${camelName}QueryProperties) export type ${upperName}Query = Static -export const ${camelName}QueryValidator = jsonSchema.getValidator(${camelName}QuerySchema, queryValidator) +export const ${camelName}QueryValidator = getValidator(${camelName}QuerySchema, queryValidator) export const ${camelName}QueryResolver = resolve<${upperName}Query, HookContext>({ properties: {} }) diff --git a/packages/cli/src/service/type/knex.tpl.ts b/packages/cli/src/service/type/knex.tpl.ts index b3378949b6..f149dce7b4 100644 --- a/packages/cli/src/service/type/knex.tpl.ts +++ b/packages/cli/src/service/type/knex.tpl.ts @@ -3,31 +3,30 @@ import { renderSource } from '../../commons' import { ServiceGeneratorContext } from '../index' const migrationTemplate = ({ - kebabName + kebabPath }: ServiceGeneratorContext) => /* ts */ `import type { Knex } from 'knex' export async function up(knex: Knex): Promise { - await knex.schema.createTable('${kebabName}', table => { + await knex.schema.createTable('${kebabPath}', table => { table.increments('id') table.string('text') }) } export async function down(knex: Knex): Promise { - await knex.schema.dropTable('${kebabName}') + await knex.schema.dropTable('${kebabPath}') } ` export const template = ({ className, upperName, - kebabName, feathers, schema, fileName, relative }: ServiceGeneratorContext) => /* ts */ `import { KnexService } from '@feathersjs/knex' -import type { KnexAdapterParams } from '@feathersjs/knex' +import type { KnexAdapterParams, KnexAdapterOptions } from '@feathersjs/knex' import type { Application } from '${relative}/declarations' ${ @@ -52,11 +51,11 @@ export interface ${upperName}Params extends KnexAdapterParams<${upperName}Query> export class ${className} extends KnexService<${upperName}, ${upperName}Data, ${upperName}Params> { } -export const getOptions = (app: Application) => { +export const getOptions = (app: Application): KnexAdapterOptions => { return { paginate: app.get('paginate'), Model: app.get('${feathers.database}Client'), - name: '${kebabName}' + name: '${fileName}' } } ` diff --git a/packages/cli/src/service/type/mongodb.tpl.ts b/packages/cli/src/service/type/mongodb.tpl.ts index e99238ddf9..1867616dc4 100644 --- a/packages/cli/src/service/type/mongodb.tpl.ts +++ b/packages/cli/src/service/type/mongodb.tpl.ts @@ -5,12 +5,12 @@ import { ServiceGeneratorContext } from '../index' export const template = ({ className, upperName, - kebabName, schema, fileName, + kebabPath, relative }: ServiceGeneratorContext) => /* ts */ `import { MongoDBService } from \'@feathersjs/mongodb\' -import type { MongoDBAdapterParams } from \'@feathersjs/mongodb\' +import type { MongoDBAdapterParams, MongoDBAdapterOptions } from \'@feathersjs/mongodb\' import type { Application } from '${relative}/declarations' ${ @@ -35,10 +35,10 @@ export interface ${upperName}Params extends MongoDBAdapterParams<${upperName}Que export class ${className} extends MongoDBService<${upperName}, ${upperName}Data, ${upperName}Params> { } -export const getOptions = (app: Application) => { +export const getOptions = (app: Application): MongoDBAdapterOptions => { return { paginate: app.get('paginate'), - Model: app.get('mongodbClient').then(db => db.collection('${kebabName}')) + Model: app.get('mongodbClient').then(db => db.collection('${kebabPath}')) } } ` diff --git a/packages/knex/src/adapter.ts b/packages/knex/src/adapter.ts index a3f349599b..bb99b25ffe 100644 --- a/packages/knex/src/adapter.ts +++ b/packages/knex/src/adapter.ts @@ -213,10 +213,10 @@ export class KnexAdapter< return data[0] } - async $create(data: Partial, params?: P): Promise - async $create(data: Partial[], params?: P): Promise - async $create(data: Partial | Partial[], _params?: P): Promise - async $create(_data: Partial | Partial[], params: P = {} as P): Promise { + async $create(data: D, params?: P): Promise + async $create(data: D[], params?: P): Promise + async $create(data: D | D[], _params?: P): Promise + async $create(_data: D | D[], params: P = {} as P): Promise { const data = _data as any if (Array.isArray(data)) { diff --git a/packages/mongodb/src/adapter.ts b/packages/mongodb/src/adapter.ts index bbcfd0139c..2a84d9a2a3 100644 --- a/packages/mongodb/src/adapter.ts +++ b/packages/mongodb/src/adapter.ts @@ -195,10 +195,10 @@ export class MongoDbAdapter< return runQuery(0).then((page) => page.data) } - async $create(data: Partial, params?: P): Promise - async $create(data: Partial[], params?: P): Promise - async $create(data: Partial | Partial[], _params?: P): Promise - async $create(data: Partial | Partial[], params: P = {} as P): Promise { + async $create(data: D, params?: P): Promise + async $create(data: D[], params?: P): Promise + async $create(data: D | D[], _params?: P): Promise + async $create(data: D | D[], params: P = {} as P): Promise { const writeOptions = params.mongodb const { Model } = this.getOptions(params) const model = await Promise.resolve(Model) diff --git a/packages/schema/package.json b/packages/schema/package.json index d7806e0c50..03c25c9a39 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -61,7 +61,6 @@ "@types/json-schema": "^7.0.11", "ajv": "^8.11.0", "ajv-formats": "^2.1.1", - "json-schema": "^0.4.0", "json-schema-to-ts": "^2.5.5" }, "devDependencies": { diff --git a/packages/schema/src/json-schema.ts b/packages/schema/src/json-schema.ts index 46c64f30c9..08798fcbd2 100644 --- a/packages/schema/src/json-schema.ts +++ b/packages/schema/src/json-schema.ts @@ -1,12 +1,11 @@ import { _ } from '@feathersjs/commons' import { JSONSchema } from 'json-schema-to-ts' -import { TObject } from '@sinclair/typebox' import { JSONSchemaDefinition, Ajv, Validator } from './schema' export type DataSchemaMap = { - create: JSONSchemaDefinition | TObject - update?: JSONSchemaDefinition | TObject - patch?: JSONSchemaDefinition | TObject + create: JSONSchemaDefinition + update?: JSONSchemaDefinition + patch?: JSONSchemaDefinition } export type DataValidatorMap = { @@ -22,10 +21,7 @@ export type DataValidatorMap = { * @param validator The AJV validation instance * @returns A compiled validation function */ -export const getValidator = ( - schema: JSONSchemaDefinition | TObject, - validator: Ajv -): Validator => +export const getValidator = (schema: JSONSchemaDefinition, validator: Ajv): Validator => validator.compile({ $async: true, ...(schema as any) @@ -42,7 +38,7 @@ export const getValidator = ( * @returns A map of validator functions */ export const getDataValidator = ( - def: JSONSchemaDefinition | TObject | DataSchemaMap, + def: JSONSchemaDefinition | DataSchemaMap, validator: Ajv ): DataValidatorMap => { const schema = ((def as any).create ? def : { create: def }) as DataSchemaMap @@ -129,14 +125,15 @@ export const queryProperty = (def: T) => { /** * Creates Feathers a query syntax compatible JSON schema for multiple properties. * - * @param definition A map of property definitions + * @param definitions A map of property definitions * @returns The JSON schema definition for the Feathers query syntax for multiple properties */ -export const queryProperties = (definition: T) => - Object.keys(definition).reduce((res, key) => { +export const queryProperties = (definitions: T) => + Object.keys(definitions).reduce((res, key) => { const result = res as any + const definition = definitions[key] - result[key] = queryProperty(definition[key]) + result[key] = queryProperty(definition) return result }, {} as { [K in keyof T]: PropertyQuery }) diff --git a/packages/typebox/package.json b/packages/typebox/package.json index 6eb6ca993f..da1795ad7d 100644 --- a/packages/typebox/package.json +++ b/packages/typebox/package.json @@ -54,10 +54,10 @@ "access": "public" }, "dependencies": { + "@feathersjs/schema": "^5.0.0-pre.30", "@sinclair/typebox": "^0.24.44" }, "devDependencies": { - "@feathersjs/schema": "^5.0.0-pre.30", "@types/mocha": "^10.0.0", "@types/node": "^18.8.2", "mocha": "^10.0.0", diff --git a/packages/typebox/src/index.ts b/packages/typebox/src/index.ts index 066c37c1c0..350cb29d4d 100644 --- a/packages/typebox/src/index.ts +++ b/packages/typebox/src/index.ts @@ -1,8 +1,38 @@ import { Type, TObject, TInteger, TOptional, TSchema, TIntersect } from '@sinclair/typebox' +import { jsonSchema, Validator, DataValidatorMap, Ajv } from '@feathersjs/schema' export * from '@sinclair/typebox' export * from './default-schemas' +export type TDataSchemaMap = { + create: TObject + update?: TObject + patch?: TObject +} + +/** + * Returns a compiled validation function for a TypeBox object and AJV validator instance. + * + * @param schema The JSON schema definition + * @param validator The AJV validation instance + * @returns A compiled validation function + */ +export const getValidator = (schema: TObject, validator: Ajv): Validator => + jsonSchema.getValidator(schema as any, validator) + +/** + * Returns compiled validation functions to validate data for the `create`, `update` and `patch` + * service methods. If not passed explicitly, the `update` validator will be the same as the `create` + * and `patch` will be the `create` validator with no required fields. + * + * @param def Either general TypeBox object definition or a mapping of `create`, `update` and `patch` + * to their respective type object + * @param validator The Ajv instance to use as the validator + * @returns A map of validator functions + */ +export const getDataValidator = (def: TObject | TDataSchemaMap, validator: Ajv): DataValidatorMap => + jsonSchema.getDataValidator(def as any, validator) + const arrayOfKeys = (type: T) => { const keys = Object.keys(type.properties) return Type.Unsafe<(keyof T['properties'])[]>({ type: 'array', items: { type: 'string', enum: keys } }) @@ -59,6 +89,13 @@ export const queryProperties = (type: T) => { } as TObject } +/** + * Creates a TypeBox schema for the complete Feathers query syntax including `$limit`, $skip` + * and `$sort` and `$select` for the allowed properties. + * + * @param type The properties to create the query syntax for + * @returns A TypeBox object representing the complete Feathers query syntax for the given properties + */ export const querySyntax = (type: T) => { return Type.Intersect([ Type.Object( diff --git a/packages/typebox/test/index.test.ts b/packages/typebox/test/index.test.ts index 88472f7620..01c6133d2b 100644 --- a/packages/typebox/test/index.test.ts +++ b/packages/typebox/test/index.test.ts @@ -1,6 +1,6 @@ import assert from 'assert' import { Ajv } from '@feathersjs/schema' -import { querySyntax, Type, Static, defaultAppConfiguration } from '../src' +import { querySyntax, Type, Static, defaultAppConfiguration, getDataValidator, getValidator } from '../src' describe('@feathersjs/schema/typebox', () => { it('querySyntax', async () => { @@ -47,4 +47,9 @@ describe('@feathersjs/schema/typebox', () => { assert.ok(validated) }) + + it('validators', () => { + assert.strictEqual(typeof getDataValidator(Type.Object({}), new Ajv()), 'object') + assert.strictEqual(typeof getValidator(Type.Object({}), new Ajv()), 'function') + }) })