From 499ecc0b4dffab6c0b82496b4e52d3a0821b9279 Mon Sep 17 00:00:00 2001 From: daffl Date: Sat, 3 Dec 2022 09:20:11 -0800 Subject: [PATCH 1/2] fix(schema): Allow query schemas with no properties and properly error on unsupported property types --- packages/schema/src/json-schema.ts | 36 ++++++++-- packages/schema/test/json-schema.test.ts | 42 ++++++++++++ packages/typebox/src/index.ts | 21 +++++- packages/typebox/test/index.test.ts | 84 ++++++++++++++++++------ 4 files changed, 154 insertions(+), 29 deletions(-) create mode 100644 packages/schema/test/json-schema.test.ts diff --git a/packages/schema/src/json-schema.ts b/packages/schema/src/json-schema.ts index 08798fcbd2..c60d280ebc 100644 --- a/packages/schema/src/json-schema.ts +++ b/packages/schema/src/json-schema.ts @@ -122,6 +122,8 @@ export const queryProperty = (def: T) => { } as const } +export const SUPPORTED_TYPES = ['string', 'number', 'integer', 'boolean', 'null'] + /** * Creates Feathers a query syntax compatible JSON schema for multiple properties. * @@ -132,6 +134,15 @@ export const queryProperties = (definit Object.keys(definitions).reduce((res, key) => { const result = res as any const definition = definitions[key] + const { type, $ref } = definition as any + + if ($ref || !SUPPORTED_TYPES.includes(type)) { + throw new Error( + `Can not create query syntax schema for property '${key}'. Only types ${SUPPORTED_TYPES.join( + ', ' + )} are allowed.` + ) + } result[key] = queryProperty(definition) @@ -145,8 +156,11 @@ export const queryProperties = (definit * @param definition The property definitions to create the query syntax schema for * @returns A JSON schema for the complete query syntax */ -export const querySyntax = (definition: T) => - ({ +export const querySyntax = (definition: T) => { + const keys = Object.keys(definition) + const props = queryProperties(definition) + + return { $limit: { type: 'number', minimum: 0 @@ -157,7 +171,7 @@ export const querySyntax = (definition: T) => }, $sort: { type: 'object', - properties: Object.keys(definition).reduce((res, key) => { + properties: keys.reduce((res, key) => { const result = res as any result[key] = { @@ -170,10 +184,20 @@ export const querySyntax = (definition: T) => }, $select: { type: 'array', + maxItems: keys.length, items: { type: 'string', - enum: Object.keys(definition) as any as (keyof T)[] + ...(keys.length > 0 ? { enum: keys as any as (keyof T)[] } : {}) } }, - ...queryProperties(definition) - } as const) + $or: { + type: 'array', + items: { + type: 'object', + additionalProperties: false, + properties: props + } + }, + ...props + } as const +} diff --git a/packages/schema/test/json-schema.test.ts b/packages/schema/test/json-schema.test.ts new file mode 100644 index 0000000000..34f789db99 --- /dev/null +++ b/packages/schema/test/json-schema.test.ts @@ -0,0 +1,42 @@ +import Ajv from 'ajv' +import assert from 'assert' +import { queryProperties, querySyntax } from '../src/json-schema' + +describe('@feathersjs/schema/json-schema', () => { + it('queryProperties errors for unsupported query types', () => { + assert.throws( + () => + queryProperties({ + something: { + type: 'object' + } + }), + { + message: + "Can not create query syntax schema for property 'something'. Only types string, number, integer, boolean, null are allowed." + } + ) + + assert.throws( + () => + queryProperties({ + otherThing: { + type: 'array' + } + }), + { + message: + "Can not create query syntax schema for property 'otherThing'. Only types string, number, integer, boolean, null are allowed." + } + ) + }) + + it('querySyntax works with no properties', async () => { + const schema = { + type: 'object', + properties: querySyntax({}) + } + + new Ajv().compile(schema) + }) +}) diff --git a/packages/typebox/src/index.ts b/packages/typebox/src/index.ts index 0b7bf7ce0c..9c0889f509 100644 --- a/packages/typebox/src/index.ts +++ b/packages/typebox/src/index.ts @@ -1,5 +1,5 @@ import { Type, TObject, TInteger, TOptional, TSchema, TIntersect, ObjectOptions } from '@sinclair/typebox' -import { jsonSchema, Validator, DataValidatorMap, Ajv } from '@feathersjs/schema' +import { jsonSchema, Validator, DataValidatorMap, Ajv, SUPPORTED_TYPES } from '@feathersjs/schema' export * from '@sinclair/typebox' export * from './default-schemas' @@ -44,7 +44,13 @@ export function StringEnum(allowedValues: [...T]) { const arrayOfKeys = (type: T) => { const keys = Object.keys(type.properties) - return Type.Unsafe<(keyof T['properties'])[]>({ type: 'array', items: { type: 'string', enum: keys } }) + return Type.Unsafe<(keyof T['properties'])[]>({ + type: 'array', + items: { + type: 'string', + ...(keys.length > 0 ? { enum: keys } : {}) + } + }) } /** @@ -102,8 +108,17 @@ type QueryProperty = ReturnType> export const queryProperties = (definition: T) => { const properties = Object.keys(definition.properties).reduce((res, key) => { const result = res as any + const value = definition.properties[key] - result[key] = queryProperty(definition.properties[key]) + if (value.$ref || !SUPPORTED_TYPES.includes(value.type)) { + throw new Error( + `Can not create query syntax schema for property '${key}'. Only types ${SUPPORTED_TYPES.join( + ', ' + )} are allowed.` + ) + } + + result[key] = queryProperty(value) return result }, {} as { [K in keyof T['properties']]: QueryProperty }) diff --git a/packages/typebox/test/index.test.ts b/packages/typebox/test/index.test.ts index ee77df156a..33aaabd640 100644 --- a/packages/typebox/test/index.test.ts +++ b/packages/typebox/test/index.test.ts @@ -1,33 +1,77 @@ import assert from 'assert' import { Ajv } from '@feathersjs/schema' -import { querySyntax, Type, Static, defaultAppConfiguration, getDataValidator, getValidator } from '../src' +import { + querySyntax, + Type, + Static, + defaultAppConfiguration, + getDataValidator, + getValidator, + queryProperties +} from '../src' describe('@feathersjs/schema/typebox', () => { - it('querySyntax', async () => { - const schema = Type.Object({ - name: Type.String(), - age: Type.Number() - }) - const querySchema = querySyntax(schema) + describe('querySyntax', () => { + it('basics', async () => { + const schema = Type.Object({ + name: Type.String(), + age: Type.Number() + }) + const querySchema = querySyntax(schema) - type Query = Static + type Query = Static - const query: Query = { - name: 'Dave', - age: { $gt: 42, $in: [50, 51] }, - $select: ['age', 'name'], - $sort: { - age: 1 + const query: Query = { + name: 'Dave', + age: { $gt: 42, $in: [50, 51] }, + $select: ['age', 'name'], + $sort: { + age: 1 + } } - } - const validator = new Ajv().compile(querySchema) - let validated = (await validator(query)) as any as Query + const validator = new Ajv().compile(querySchema) + let validated = (await validator(query)) as any as Query - assert.ok(validated) + assert.ok(validated) + + validated = (await validator({ ...query, something: 'wrong' })) as any as Query + assert.ok(!validated) + }) - validated = (await validator({ ...query, something: 'wrong' })) as any as Query - assert.ok(!validated) + it('queryProperties errors for unsupported query types', () => { + assert.throws( + () => + queryProperties( + Type.Object({ + something: Type.Object({}) + }) + ), + { + message: + "Can not create query syntax schema for property 'something'. Only types string, number, integer, boolean, null are allowed." + } + ) + + assert.throws( + () => + queryProperties( + Type.Object({ + otherThing: Type.Array(Type.String()) + }) + ), + { + message: + "Can not create query syntax schema for property 'otherThing'. Only types string, number, integer, boolean, null are allowed." + } + ) + }) + + it('querySyntax works with no properties', async () => { + const schema = querySyntax(Type.Object({})) + + new Ajv().compile(schema) + }) }) it('defaultAppConfiguration', async () => { From 45c59a42f82438a00d3d40d39243aa909cb9af22 Mon Sep 17 00:00:00 2001 From: daffl Date: Sat, 3 Dec 2022 09:23:45 -0800 Subject: [PATCH 2/2] Add maxItems --- packages/typebox/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/typebox/src/index.ts b/packages/typebox/src/index.ts index 9c0889f509..da9c1815bf 100644 --- a/packages/typebox/src/index.ts +++ b/packages/typebox/src/index.ts @@ -46,6 +46,7 @@ const arrayOfKeys = (type: T) => { const keys = Object.keys(type.properties) return Type.Unsafe<(keyof T['properties'])[]>({ type: 'array', + maxItems: keys.length, items: { type: 'string', ...(keys.length > 0 ? { enum: keys } : {})