Skip to content

Commit b66c734

Browse files
authored
fix(schema): Allow query schemas with no properties, error on unsupported types (#2904)
1 parent 4ff1ed0 commit b66c734

File tree

4 files changed

+155
-29
lines changed

4 files changed

+155
-29
lines changed

packages/schema/src/json-schema.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,8 @@ export const queryProperty = <T extends JSONSchema>(def: T) => {
122122
} as const
123123
}
124124

125+
export const SUPPORTED_TYPES = ['string', 'number', 'integer', 'boolean', 'null']
126+
125127
/**
126128
* Creates Feathers a query syntax compatible JSON schema for multiple properties.
127129
*
@@ -132,6 +134,15 @@ export const queryProperties = <T extends { [key: string]: JSONSchema }>(definit
132134
Object.keys(definitions).reduce((res, key) => {
133135
const result = res as any
134136
const definition = definitions[key]
137+
const { type, $ref } = definition as any
138+
139+
if ($ref || !SUPPORTED_TYPES.includes(type)) {
140+
throw new Error(
141+
`Can not create query syntax schema for property '${key}'. Only types ${SUPPORTED_TYPES.join(
142+
', '
143+
)} are allowed.`
144+
)
145+
}
135146

136147
result[key] = queryProperty(definition)
137148

@@ -145,8 +156,11 @@ export const queryProperties = <T extends { [key: string]: JSONSchema }>(definit
145156
* @param definition The property definitions to create the query syntax schema for
146157
* @returns A JSON schema for the complete query syntax
147158
*/
148-
export const querySyntax = <T extends { [key: string]: any }>(definition: T) =>
149-
({
159+
export const querySyntax = <T extends { [key: string]: JSONSchema }>(definition: T) => {
160+
const keys = Object.keys(definition)
161+
const props = queryProperties(definition)
162+
163+
return {
150164
$limit: {
151165
type: 'number',
152166
minimum: 0
@@ -157,7 +171,7 @@ export const querySyntax = <T extends { [key: string]: any }>(definition: T) =>
157171
},
158172
$sort: {
159173
type: 'object',
160-
properties: Object.keys(definition).reduce((res, key) => {
174+
properties: keys.reduce((res, key) => {
161175
const result = res as any
162176

163177
result[key] = {
@@ -170,10 +184,20 @@ export const querySyntax = <T extends { [key: string]: any }>(definition: T) =>
170184
},
171185
$select: {
172186
type: 'array',
187+
maxItems: keys.length,
173188
items: {
174189
type: 'string',
175-
enum: Object.keys(definition) as any as (keyof T)[]
190+
...(keys.length > 0 ? { enum: keys as any as (keyof T)[] } : {})
176191
}
177192
},
178-
...queryProperties(definition)
179-
} as const)
193+
$or: {
194+
type: 'array',
195+
items: {
196+
type: 'object',
197+
additionalProperties: false,
198+
properties: props
199+
}
200+
},
201+
...props
202+
} as const
203+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import Ajv from 'ajv'
2+
import assert from 'assert'
3+
import { queryProperties, querySyntax } from '../src/json-schema'
4+
5+
describe('@feathersjs/schema/json-schema', () => {
6+
it('queryProperties errors for unsupported query types', () => {
7+
assert.throws(
8+
() =>
9+
queryProperties({
10+
something: {
11+
type: 'object'
12+
}
13+
}),
14+
{
15+
message:
16+
"Can not create query syntax schema for property 'something'. Only types string, number, integer, boolean, null are allowed."
17+
}
18+
)
19+
20+
assert.throws(
21+
() =>
22+
queryProperties({
23+
otherThing: {
24+
type: 'array'
25+
}
26+
}),
27+
{
28+
message:
29+
"Can not create query syntax schema for property 'otherThing'. Only types string, number, integer, boolean, null are allowed."
30+
}
31+
)
32+
})
33+
34+
it('querySyntax works with no properties', async () => {
35+
const schema = {
36+
type: 'object',
37+
properties: querySyntax({})
38+
}
39+
40+
new Ajv().compile(schema)
41+
})
42+
})

packages/typebox/src/index.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Type, TObject, TInteger, TOptional, TSchema, TIntersect, ObjectOptions } from '@sinclair/typebox'
2-
import { jsonSchema, Validator, DataValidatorMap, Ajv } from '@feathersjs/schema'
2+
import { jsonSchema, Validator, DataValidatorMap, Ajv, SUPPORTED_TYPES } from '@feathersjs/schema'
33

44
export * from '@sinclair/typebox'
55
export * from './default-schemas'
@@ -44,7 +44,14 @@ export function StringEnum<T extends string[]>(allowedValues: [...T]) {
4444

4545
const arrayOfKeys = <T extends TObject>(type: T) => {
4646
const keys = Object.keys(type.properties)
47-
return Type.Unsafe<(keyof T['properties'])[]>({ type: 'array', items: { type: 'string', enum: keys } })
47+
return Type.Unsafe<(keyof T['properties'])[]>({
48+
type: 'array',
49+
maxItems: keys.length,
50+
items: {
51+
type: 'string',
52+
...(keys.length > 0 ? { enum: keys } : {})
53+
}
54+
})
4855
}
4956

5057
/**
@@ -102,8 +109,17 @@ type QueryProperty<T extends TSchema> = ReturnType<typeof queryProperty<T>>
102109
export const queryProperties = <T extends TObject>(definition: T) => {
103110
const properties = Object.keys(definition.properties).reduce((res, key) => {
104111
const result = res as any
112+
const value = definition.properties[key]
105113

106-
result[key] = queryProperty(definition.properties[key])
114+
if (value.$ref || !SUPPORTED_TYPES.includes(value.type)) {
115+
throw new Error(
116+
`Can not create query syntax schema for property '${key}'. Only types ${SUPPORTED_TYPES.join(
117+
', '
118+
)} are allowed.`
119+
)
120+
}
121+
122+
result[key] = queryProperty(value)
107123

108124
return result
109125
}, {} as { [K in keyof T['properties']]: QueryProperty<T['properties'][K]> })

packages/typebox/test/index.test.ts

Lines changed: 64 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,77 @@
11
import assert from 'assert'
22
import { Ajv } from '@feathersjs/schema'
3-
import { querySyntax, Type, Static, defaultAppConfiguration, getDataValidator, getValidator } from '../src'
3+
import {
4+
querySyntax,
5+
Type,
6+
Static,
7+
defaultAppConfiguration,
8+
getDataValidator,
9+
getValidator,
10+
queryProperties
11+
} from '../src'
412

513
describe('@feathersjs/schema/typebox', () => {
6-
it('querySyntax', async () => {
7-
const schema = Type.Object({
8-
name: Type.String(),
9-
age: Type.Number()
10-
})
11-
const querySchema = querySyntax(schema)
14+
describe('querySyntax', () => {
15+
it('basics', async () => {
16+
const schema = Type.Object({
17+
name: Type.String(),
18+
age: Type.Number()
19+
})
20+
const querySchema = querySyntax(schema)
1221

13-
type Query = Static<typeof querySchema>
22+
type Query = Static<typeof querySchema>
1423

15-
const query: Query = {
16-
name: 'Dave',
17-
age: { $gt: 42, $in: [50, 51] },
18-
$select: ['age', 'name'],
19-
$sort: {
20-
age: 1
24+
const query: Query = {
25+
name: 'Dave',
26+
age: { $gt: 42, $in: [50, 51] },
27+
$select: ['age', 'name'],
28+
$sort: {
29+
age: 1
30+
}
2131
}
22-
}
2332

24-
const validator = new Ajv().compile(querySchema)
25-
let validated = (await validator(query)) as any as Query
33+
const validator = new Ajv().compile(querySchema)
34+
let validated = (await validator(query)) as any as Query
2635

27-
assert.ok(validated)
36+
assert.ok(validated)
37+
38+
validated = (await validator({ ...query, something: 'wrong' })) as any as Query
39+
assert.ok(!validated)
40+
})
2841

29-
validated = (await validator({ ...query, something: 'wrong' })) as any as Query
30-
assert.ok(!validated)
42+
it('queryProperties errors for unsupported query types', () => {
43+
assert.throws(
44+
() =>
45+
queryProperties(
46+
Type.Object({
47+
something: Type.Object({})
48+
})
49+
),
50+
{
51+
message:
52+
"Can not create query syntax schema for property 'something'. Only types string, number, integer, boolean, null are allowed."
53+
}
54+
)
55+
56+
assert.throws(
57+
() =>
58+
queryProperties(
59+
Type.Object({
60+
otherThing: Type.Array(Type.String())
61+
})
62+
),
63+
{
64+
message:
65+
"Can not create query syntax schema for property 'otherThing'. Only types string, number, integer, boolean, null are allowed."
66+
}
67+
)
68+
})
69+
70+
it('querySyntax works with no properties', async () => {
71+
const schema = querySyntax(Type.Object({}))
72+
73+
new Ajv().compile(schema)
74+
})
3175
})
3276

3377
it('defaultAppConfiguration', async () => {

0 commit comments

Comments
 (0)