Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

fix(schema): Allow query schemas with no properties, error on unsupported types #2904

Merged
merged 2 commits into from
Dec 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions packages/schema/src/json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ export const queryProperty = <T extends JSONSchema>(def: T) => {
} as const
}

export const SUPPORTED_TYPES = ['string', 'number', 'integer', 'boolean', 'null']
daffl marked this conversation as resolved.
Show resolved Hide resolved

/**
* Creates Feathers a query syntax compatible JSON schema for multiple properties.
*
Expand All @@ -132,6 +134,15 @@ export const queryProperties = <T extends { [key: string]: JSONSchema }>(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)

Expand All @@ -145,8 +156,11 @@ export const queryProperties = <T extends { [key: string]: JSONSchema }>(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 = <T extends { [key: string]: any }>(definition: T) =>
({
export const querySyntax = <T extends { [key: string]: JSONSchema }>(definition: T) => {
const keys = Object.keys(definition)
const props = queryProperties(definition)

return {
$limit: {
type: 'number',
minimum: 0
Expand All @@ -157,7 +171,7 @@ export const querySyntax = <T extends { [key: string]: any }>(definition: T) =>
},
$sort: {
type: 'object',
properties: Object.keys(definition).reduce((res, key) => {
properties: keys.reduce((res, key) => {
const result = res as any

result[key] = {
Expand All @@ -170,10 +184,20 @@ export const querySyntax = <T extends { [key: string]: any }>(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
}
42 changes: 42 additions & 0 deletions packages/schema/test/json-schema.test.ts
Original file line number Diff line number Diff line change
@@ -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)
})
})
22 changes: 19 additions & 3 deletions packages/typebox/src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -44,7 +44,14 @@ export function StringEnum<T extends string[]>(allowedValues: [...T]) {

const arrayOfKeys = <T extends TObject>(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',
maxItems: keys.length,
items: {
type: 'string',
...(keys.length > 0 ? { enum: keys } : {})
}
})
}

/**
Expand Down Expand Up @@ -102,8 +109,17 @@ type QueryProperty<T extends TSchema> = ReturnType<typeof queryProperty<T>>
export const queryProperties = <T extends TObject>(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<T['properties'][K]> })
Expand Down
84 changes: 64 additions & 20 deletions packages/typebox/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof querySchema>
type Query = Static<typeof querySchema>

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 () => {
Expand Down