@feathersjs/schema 5.0.0-pre.20 → 5.0.0-pre.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,191 @@
1
+ import { HookContext, NextFunction } from '@feathersjs/feathers'
2
+ import { compose } from '@feathersjs/hooks'
3
+ import { Resolver, ResolverStatus } from '../resolver'
4
+
5
+ const getContext = <H extends HookContext>(context: H) => {
6
+ return {
7
+ ...context,
8
+ params: {
9
+ ...context.params,
10
+ query: {}
11
+ }
12
+ }
13
+ }
14
+
15
+ const getData = <H extends HookContext>(context: H) => {
16
+ const isPaginated = context.method === 'find' && context.result.data
17
+ const data = isPaginated ? context.result.data : context.result
18
+
19
+ return { isPaginated, data }
20
+ }
21
+
22
+ const runResolvers = async <T, H extends HookContext>(
23
+ resolvers: Resolver<T, H>[],
24
+ data: any,
25
+ ctx: H,
26
+ status?: Partial<ResolverStatus<T, H>>
27
+ ) => {
28
+ let current: any = data
29
+
30
+ for (const resolver of resolvers) {
31
+ if (resolver && typeof resolver.resolve === 'function') {
32
+ current = await resolver.resolve(current, ctx, status)
33
+ }
34
+ }
35
+
36
+ return current as T
37
+ }
38
+
39
+ export type ResolverSetting<H extends HookContext> = Resolver<any, H> | Resolver<any, H>[]
40
+
41
+ export type DataResolvers<H extends HookContext> = {
42
+ create: Resolver<any, H>
43
+ patch: Resolver<any, H>
44
+ update: Resolver<any, H>
45
+ }
46
+
47
+ export type ResolveAllSettings<H extends HookContext> = {
48
+ data?: DataResolvers<H>
49
+ query?: Resolver<any, H>
50
+ result?: Resolver<any, H>
51
+ dispatch?: Resolver<any, H>
52
+ }
53
+
54
+ export const DISPATCH = Symbol('@feathersjs/schema/dispatch')
55
+
56
+ export const getDispatch = (value: any) =>
57
+ typeof value === 'object' && value !== null && value[DISPATCH] !== undefined ? value[DISPATCH] : value
58
+
59
+ export const resolveQuery =
60
+ <T, H extends HookContext>(...resolvers: Resolver<T, H>[]) =>
61
+ async (context: H, next?: NextFunction) => {
62
+ const ctx = getContext(context)
63
+ const data = context?.params?.query || {}
64
+ const query = await runResolvers(resolvers, data, ctx)
65
+
66
+ context.params = {
67
+ ...context.params,
68
+ query
69
+ }
70
+
71
+ if (typeof next === 'function') {
72
+ return next()
73
+ }
74
+ }
75
+
76
+ export const resolveData =
77
+ <H extends HookContext>(settings: DataResolvers<H> | Resolver<any, H>) =>
78
+ async (context: H, next?: NextFunction) => {
79
+ if (context.method === 'create' || context.method === 'patch' || context.method === 'update') {
80
+ const resolvers = settings instanceof Resolver ? [settings] : [settings[context.method]]
81
+ const ctx = getContext(context)
82
+ const data = context.data
83
+
84
+ const status = {
85
+ originalContext: context
86
+ }
87
+
88
+ if (Array.isArray(data)) {
89
+ context.data = await Promise.all(data.map((current) => runResolvers(resolvers, current, ctx, status)))
90
+ } else {
91
+ context.data = await runResolvers(resolvers, data, ctx, status)
92
+ }
93
+ }
94
+
95
+ if (typeof next === 'function') {
96
+ return next()
97
+ }
98
+ }
99
+
100
+ export const resolveResult =
101
+ <T, H extends HookContext>(...resolvers: Resolver<T, H>[]) =>
102
+ async (context: H, next?: NextFunction) => {
103
+ if (typeof next === 'function') {
104
+ const { $resolve: properties, ...query } = context.params?.query || {}
105
+ const resolve = {
106
+ originalContext: context,
107
+ ...context.params.resolve,
108
+ properties
109
+ }
110
+
111
+ context.params = {
112
+ ...context.params,
113
+ resolve,
114
+ query
115
+ }
116
+
117
+ await next()
118
+ }
119
+
120
+ const ctx = getContext(context)
121
+ const status = context.params.resolve
122
+ const { isPaginated, data } = getData(context)
123
+
124
+ const result = Array.isArray(data)
125
+ ? await Promise.all(data.map(async (current) => runResolvers(resolvers, current, ctx, status)))
126
+ : await runResolvers(resolvers, data, ctx, status)
127
+
128
+ if (isPaginated) {
129
+ context.result.data = result
130
+ } else {
131
+ context.result = result
132
+ }
133
+ }
134
+
135
+ export const resolveDispatch =
136
+ <T, H extends HookContext>(...resolvers: Resolver<T, H>[]) =>
137
+ async (context: H, next?: NextFunction) => {
138
+ if (typeof next === 'function') {
139
+ await next()
140
+ }
141
+
142
+ const ctx = getContext(context)
143
+ const status = context.params.resolve
144
+ const { isPaginated, data } = getData(context)
145
+ const resolveAndGetDispatch = async (current: any) => {
146
+ const resolved = await runResolvers(resolvers, current, ctx, status)
147
+
148
+ return Object.keys(resolved).reduce((res, key) => {
149
+ res[key] = getDispatch(current[key])
150
+
151
+ return res
152
+ }, {} as any)
153
+ }
154
+
155
+ const result = await (Array.isArray(data)
156
+ ? Promise.all(data.map(resolveAndGetDispatch))
157
+ : resolveAndGetDispatch(data))
158
+ const dispatch = isPaginated
159
+ ? {
160
+ ...context.result,
161
+ data: result
162
+ }
163
+ : result
164
+
165
+ context.dispatch = dispatch
166
+ Object.defineProperty(context.result, DISPATCH, {
167
+ value: dispatch,
168
+ enumerable: false,
169
+ configurable: false
170
+ })
171
+ }
172
+
173
+ export const resolveAll = <H extends HookContext>(map: ResolveAllSettings<H>) => {
174
+ const middleware = []
175
+
176
+ middleware.push(resolveDispatch(map.dispatch))
177
+
178
+ if (map.result) {
179
+ middleware.push(resolveResult(map.result))
180
+ }
181
+
182
+ if (map.query) {
183
+ middleware.push(resolveQuery(map.query))
184
+ }
185
+
186
+ if (map.data) {
187
+ middleware.push(resolveData(map.data))
188
+ }
189
+
190
+ return compose(middleware)
191
+ }
@@ -0,0 +1,44 @@
1
+ import { HookContext, NextFunction } from '@feathersjs/feathers'
2
+ import { BadRequest } from '../../../errors/lib'
3
+ import { Schema } from '../schema'
4
+
5
+ export const validateQuery =
6
+ <H extends HookContext>(schema: Schema<any>) =>
7
+ async (context: H, next?: NextFunction) => {
8
+ const data = context?.params?.query || {}
9
+
10
+ try {
11
+ const query = await schema.validate(data)
12
+
13
+ context.params = {
14
+ ...context.params,
15
+ query
16
+ }
17
+
18
+ if (typeof next === 'function') {
19
+ return next()
20
+ }
21
+ } catch (error: any) {
22
+ throw error.ajv ? new BadRequest(error.message, error.errors) : error
23
+ }
24
+ }
25
+
26
+ export const validateData =
27
+ <H extends HookContext>(schema: Schema<any>) =>
28
+ async (context: H, next?: NextFunction) => {
29
+ const data = context.data
30
+
31
+ try {
32
+ if (Array.isArray(data)) {
33
+ context.data = await Promise.all(data.map((current) => schema.validate(current)))
34
+ } else {
35
+ context.data = await schema.validate(data)
36
+ }
37
+ } catch (error: any) {
38
+ throw error.ajv ? new BadRequest(error.message, error.errors) : error
39
+ }
40
+
41
+ if (typeof next === 'function') {
42
+ return next()
43
+ }
44
+ }
package/src/index.ts CHANGED
@@ -1,14 +1,16 @@
1
- import { ResolverStatus } from './resolver';
1
+ import { ResolverStatus } from './resolver'
2
2
 
3
- export * from './schema';
4
- export * from './resolver';
5
- export * from './hooks';
6
- export * from './query';
3
+ export * from './schema'
4
+ export * from './resolver'
5
+ export * from './hooks'
6
+ export * from './query'
7
7
 
8
- export type Infer<S extends { _type: any }> = S['_type'];
8
+ export type Infer<S extends { _type: any }> = S['_type']
9
+
10
+ export type Combine<S extends { _type: any }, U> = Pick<Infer<S>, Exclude<keyof Infer<S>, keyof U>> & U
9
11
 
10
12
  declare module '@feathersjs/feathers/lib/declarations' {
11
13
  interface Params {
12
- resolve?: ResolverStatus<any, HookContext>;
14
+ resolve?: ResolverStatus<any, HookContext>
13
15
  }
14
16
  }
package/src/query.ts CHANGED
@@ -1,23 +1,24 @@
1
- import { JSONSchema } from 'json-schema-to-ts';
1
+ import { _ } from '@feathersjs/commons'
2
+ import { JSONSchema } from 'json-schema-to-ts'
2
3
 
3
4
  export type PropertyQuery<D extends JSONSchema> = {
4
5
  anyOf: [
5
6
  D,
6
7
  {
7
- type: 'object',
8
- additionalProperties: false,
8
+ type: 'object'
9
+ additionalProperties: false
9
10
  properties: {
10
- $gt: D,
11
- $gte: D,
12
- $lt: D,
13
- $lte: D,
14
- $ne: D,
11
+ $gt: D
12
+ $gte: D
13
+ $lt: D
14
+ $lte: D
15
+ $ne: D
15
16
  $in: {
16
- type: 'array',
17
+ type: 'array'
17
18
  items: D
18
- },
19
+ }
19
20
  $nin: {
20
- type: 'array',
21
+ type: 'array'
21
22
  items: D
22
23
  }
23
24
  }
@@ -25,64 +26,72 @@ export type PropertyQuery<D extends JSONSchema> = {
25
26
  ]
26
27
  }
27
28
 
28
- export const queryProperty = <T extends JSONSchema> (definition: T) => ({
29
- anyOf: [
30
- definition,
31
- {
32
- type: 'object',
33
- additionalProperties: false,
34
- properties: {
35
- $gt: definition,
36
- $gte: definition,
37
- $lt: definition,
38
- $lte: definition,
39
- $ne: definition,
40
- $in: {
41
- type: 'array',
42
- items: definition
43
- },
44
- $nin: {
45
- type: 'array',
46
- items: definition
29
+ export const queryProperty = <T extends JSONSchema>(def: T) => {
30
+ const definition = _.omit(def, 'default')
31
+ return {
32
+ anyOf: [
33
+ definition,
34
+ {
35
+ type: 'object',
36
+ additionalProperties: false,
37
+ properties: {
38
+ $gt: definition,
39
+ $gte: definition,
40
+ $lt: definition,
41
+ $lte: definition,
42
+ $ne: definition,
43
+ $in: {
44
+ type: 'array',
45
+ items: definition
46
+ },
47
+ $nin: {
48
+ type: 'array',
49
+ items: definition
50
+ }
47
51
  }
48
52
  }
49
- }
50
- ]
51
- } as const);
53
+ ]
54
+ } as const
55
+ }
52
56
 
53
- export const queryProperties = <T extends { [key: string]: JSONSchema }> (definition: T) =>
57
+ export const queryProperties = <T extends { [key: string]: JSONSchema }>(definition: T) =>
54
58
  Object.keys(definition).reduce((res, key) => {
55
- (res as any)[key] = queryProperty(definition[key])
59
+ const result = res as any
60
+
61
+ result[key] = queryProperty(definition[key])
56
62
 
57
- return res
63
+ return result
58
64
  }, {} as { [K in keyof T]: PropertyQuery<T[K]> })
59
65
 
60
- export const querySyntax = <T extends { [key: string]: JSONSchema }> (definition: T) => ({
61
- $limit: {
62
- type: 'number',
63
- minimum: 0
64
- },
65
- $skip: {
66
- type: 'number',
67
- minimum: 0
68
- },
69
- $sort: {
70
- type: 'object',
71
- properties: Object.keys(definition).reduce((res, key) => {
72
- (res as any)[key] = {
73
- type: 'number',
74
- enum: [1, -1]
75
- }
66
+ export const querySyntax = <T extends { [key: string]: JSONSchema }>(definition: T) =>
67
+ ({
68
+ $limit: {
69
+ type: 'number',
70
+ minimum: 0
71
+ },
72
+ $skip: {
73
+ type: 'number',
74
+ minimum: 0
75
+ },
76
+ $sort: {
77
+ type: 'object',
78
+ properties: Object.keys(definition).reduce((res, key) => {
79
+ const result = res as any
76
80
 
77
- return res
78
- }, {} as { [K in keyof T]: { readonly type: 'number', readonly enum: [1, -1] } })
79
- },
80
- $select: {
81
- type: 'array',
82
- items: {
83
- type: 'string',
84
- enum: Object.keys(definition) as any as (keyof T)[]
85
- }
86
- },
87
- ...queryProperties(definition)
88
- } as const)
81
+ result[key] = {
82
+ type: 'number',
83
+ enum: [1, -1]
84
+ }
85
+
86
+ return result
87
+ }, {} as { [K in keyof T]: { readonly type: 'number'; readonly enum: [1, -1] } })
88
+ },
89
+ $select: {
90
+ type: 'array',
91
+ items: {
92
+ type: 'string',
93
+ enum: Object.keys(definition) as any as (keyof T)[]
94
+ }
95
+ },
96
+ ...queryProperties(definition)
97
+ } as const)
package/src/resolver.ts CHANGED
@@ -1,49 +1,55 @@
1
- import { BadRequest } from '@feathersjs/errors';
2
- import { Schema } from './schema';
1
+ import { BadRequest } from '@feathersjs/errors'
2
+ import { Schema } from './schema'
3
3
 
4
4
  export type PropertyResolver<T, V, C> = (
5
- value: V|undefined,
5
+ value: V | undefined,
6
6
  obj: T,
7
7
  context: C,
8
8
  status: ResolverStatus<T, C>
9
- ) => Promise<V|undefined>;
9
+ ) => Promise<V | undefined>
10
10
 
11
11
  export type PropertyResolverMap<T, C> = {
12
12
  [key in keyof T]?: PropertyResolver<T, T[key], C>
13
13
  }
14
14
 
15
+ export type ResolverConverter<T, C> = (
16
+ obj: any,
17
+ context: C,
18
+ status: ResolverStatus<T, C>
19
+ ) => Promise<T | undefined>
20
+
15
21
  export interface ResolverConfig<T, C> {
16
- schema?: Schema<T>,
17
- validate?: 'before'|'after'|false,
22
+ schema?: Schema<T>
23
+ validate?: 'before' | 'after' | false
18
24
  properties: PropertyResolverMap<T, C>
25
+ converter?: ResolverConverter<T, C>
19
26
  }
20
27
 
21
28
  export interface ResolverStatus<T, C> {
22
- path: string[];
23
- originalContext?: C;
24
- properties?: string[];
25
- stack: PropertyResolver<T, any, C>[];
29
+ path: string[]
30
+ originalContext?: C
31
+ properties?: string[]
32
+ stack: PropertyResolver<T, any, C>[]
26
33
  }
27
34
 
28
35
  export class Resolver<T, C> {
29
- readonly _type!: T;
36
+ readonly _type!: T
30
37
 
31
- constructor (public options: ResolverConfig<T, C>) {
32
- }
38
+ constructor(public options: ResolverConfig<T, C>) {}
33
39
 
34
- async resolveProperty<D, K extends keyof T> (
40
+ async resolveProperty<D, K extends keyof T>(
35
41
  name: K,
36
42
  data: D,
37
43
  context: C,
38
44
  status: Partial<ResolverStatus<T, C>> = {}
39
45
  ): Promise<T[K]> {
40
- const resolver = this.options.properties[name];
41
- const value = (data as any)[name];
42
- const { path = [], stack = [] } = status || {};
46
+ const resolver = this.options.properties[name]
47
+ const value = (data as any)[name]
48
+ const { path = [], stack = [] } = status || {}
43
49
 
44
50
  // This prevents circular dependencies
45
51
  if (stack.includes(resolver)) {
46
- return undefined;
52
+ return undefined
47
53
  }
48
54
 
49
55
  const resolverStatus = {
@@ -52,59 +58,70 @@ export class Resolver<T, C> {
52
58
  stack: [...stack, resolver]
53
59
  }
54
60
 
55
- return resolver(value, data as any, context, resolverStatus);
61
+ return resolver(value, data as any, context, resolverStatus)
56
62
  }
57
63
 
58
- async resolve<D> (_data: D, context: C, status?: Partial<ResolverStatus<T, C>>): Promise<T> {
59
- const { properties: resolvers, schema, validate } = this.options;
60
- const data = schema && validate === 'before' ? await schema.validate(_data) : _data;
61
- const propertyList = (Array.isArray(status?.properties)
62
- ? status?.properties
63
- // By default get all data and resolver keys but remove duplicates
64
- : [...new Set(Object.keys(data).concat(Object.keys(resolvers)))]
65
- ) as (keyof T)[];
64
+ async convert<D>(data: D, context: C, status?: Partial<ResolverStatus<T, C>>) {
65
+ if (this.options.converter) {
66
+ const { path = [], stack = [] } = status || {}
66
67
 
67
- const result: any = {};
68
- const errors: any = {};
69
- let hasErrors = false;
68
+ return this.options.converter(data, context, { ...status, path, stack })
69
+ }
70
70
 
71
- // Not the most elegant but better performance
72
- await Promise.all(propertyList.map(async name => {
73
- const value = (data as any)[name];
71
+ return data
72
+ }
74
73
 
75
- if (resolvers[name]) {
76
- try {
77
- const resolved = await this.resolveProperty(name, data, context, status);
74
+ async resolve<D>(_data: D, context: C, status?: Partial<ResolverStatus<T, C>>): Promise<T> {
75
+ const { properties: resolvers, schema, validate } = this.options
76
+ const payload = await this.convert(_data, context, status)
77
+ const data = schema && validate === 'before' ? await schema.validate(payload) : payload
78
+ const propertyList = (
79
+ Array.isArray(status?.properties)
80
+ ? status?.properties
81
+ : // By default get all data and resolver keys but remove duplicates
82
+ [...new Set(Object.keys(data).concat(Object.keys(resolvers)))]
83
+ ) as (keyof T)[]
84
+
85
+ const result: any = {}
86
+ const errors: any = {}
87
+ let hasErrors = false
78
88
 
79
- if (resolved !== undefined) {
80
- result[name] = resolved;
89
+ // Not the most elegant but better performance
90
+ await Promise.all(
91
+ propertyList.map(async (name) => {
92
+ const value = (data as any)[name]
93
+
94
+ if (resolvers[name]) {
95
+ try {
96
+ const resolved = await this.resolveProperty(name, data, context, status)
97
+
98
+ if (resolved !== undefined) {
99
+ result[name] = resolved
100
+ }
101
+ } catch (error: any) {
102
+ // TODO add error stacks
103
+ const convertedError =
104
+ typeof error.toJSON === 'function' ? error.toJSON() : { message: error.message || error }
105
+
106
+ errors[name] = convertedError
107
+ hasErrors = true
81
108
  }
82
- } catch (error: any) {
83
- // TODO add error stacks
84
- const convertedError = typeof error.toJSON === 'function'
85
- ? error.toJSON()
86
- : { message: error.message || error };
87
-
88
- errors[name] = convertedError;
89
- hasErrors = true;
109
+ } else if (value !== undefined) {
110
+ result[name] = value
90
111
  }
91
- } else if (value !== undefined) {
92
- result[name] = value;
93
- }
94
- }));
112
+ })
113
+ )
95
114
 
96
115
  if (hasErrors) {
97
- const propertyName = status?.properties ? ` ${status.properties.join('.')}` : '';
116
+ const propertyName = status?.properties ? ` ${status.properties.join('.')}` : ''
98
117
 
99
- throw new BadRequest('Error resolving data' + propertyName, errors);
118
+ throw new BadRequest('Error resolving data' + (propertyName ? ` ${propertyName}` : ''), errors)
100
119
  }
101
120
 
102
- return schema && validate === 'after'
103
- ? await schema.validate(result)
104
- : result;
121
+ return schema && validate === 'after' ? await schema.validate(result) : result
105
122
  }
106
123
  }
107
124
 
108
- export function resolve <T, C> (options: ResolverConfig<T, C>) {
109
- return new Resolver<T, C>(options);
125
+ export function resolve<T, C>(options: ResolverConfig<T, C>) {
126
+ return new Resolver<T, C>(options)
110
127
  }