diff --git a/packages/schema/src/resolver.ts b/packages/schema/src/resolver.ts index 4c191ca79d..fb08bed913 100644 --- a/packages/schema/src/resolver.ts +++ b/packages/schema/src/resolver.ts @@ -26,7 +26,7 @@ export const IS_VIRTUAL = Symbol.for('@feathersjs/schema/virtual') * @returns The property resolver function */ export const virtual = (virtualResolver: VirtualResolver) => { - const propertyResolver: PropertyResolver = async (_value, obj, context, status) => + const propertyResolver: PropertyResolver = (_value, obj, context, status) => virtualResolver(obj, context, status) propertyResolver[IS_VIRTUAL] = true @@ -90,12 +90,12 @@ export class Resolver { * @param status The current resolver status * @returns The resolver property */ - async resolveProperty( + resolveProperty( name: K, data: D, context: C, status: Partial> = {} - ): Promise { + ): PromiseOrLiteral { const resolver = this.options.properties[name] const value = (data as any)[name] const { path = [], stack = [] } = status || {} @@ -178,6 +178,88 @@ export class Resolver { return schema && validate === 'after' ? await schema.validate(result) : result } + + async _resolve(_data: D, context: C, status?: Partial>): Promise { + const { properties: resolvers, schema, validate } = this.options + const payload = await this.convert(_data, context, status) + + if (!Array.isArray(status?.properties) && this.propertyNames.length === 0) { + return payload as T + } + + const data = schema && validate === 'before' ? await schema.validate(payload) : payload + const propertyList = ( + Array.isArray(status?.properties) + ? status?.properties + : // By default get all data and resolver keys but remove duplicates + [...new Set(Object.keys(data).concat(this.propertyNames as string[]))] + ) as (keyof T)[] + + const result: any = {} + const errors: any = {} + let hasErrors = false + + const resolveProperties = () => { + return new Promise((resolve) => { + for (let index = 0; index < propertyList.length; index++) { + const name = propertyList[index] + const value = (data as any)[name] + + const handleResolve = (resolvedValue: any) => { + if (resolvedValue !== undefined) { + result[name] = resolvedValue + } + if (index === propertyList.length - 1) { + resolve(null) + } + } + + const handleReject = (error: any) => { + // TODO add error stacks + const convertedError = + typeof error.toJSON === 'function' ? error.toJSON() : { message: error.message || error } + + errors[name] = convertedError + hasErrors = true + + if (index === propertyList.length - 1) { + resolve(null) + } + } + + if (!resolvers[name]) { + handleResolve(value) + continue + } + + try { + const resolved = this.resolveProperty(name, data, context, status) + // @ts-ignore + if (typeof resolved.then === 'function') { + resolved + // @ts-ignore + .then(handleResolve) + .catch(handleReject) + } else { + handleResolve(resolved) + } + } catch (error: any) { + handleReject(error) + } + } + }) + } + + await resolveProperties() + + if (hasErrors) { + const propertyName = status?.properties ? ` ${status.properties.join('.')}` : '' + + throw new BadRequest('Error resolving data' + (propertyName ? ` ${propertyName}` : ''), errors) + } + + return schema && validate === 'after' ? await schema.validate(result) : result + } } /** diff --git a/packages/schema/test/resolver.test.ts b/packages/schema/test/resolver.test.ts index 748c3e0650..304d9a2347 100644 --- a/packages/schema/test/resolver.test.ts +++ b/packages/schema/test/resolver.test.ts @@ -217,4 +217,50 @@ describe('@feathersjs/schema/resolver', () => { assert.deepStrictEqual(resolved, { message: 'Hello' }) }) + + it('optimizes promises', async () => { + const count = 10000 + const asyncResolvers = 1 + const syncResolvers = 10 + const runs = 10 + const properties: { [key: string]: any } = {} + + for (let i = 0; i < asyncResolvers; i++) { + properties[`async${i}`] = async (_value: any, data: any) => { + return data.id + } + } + + for (let i = 0; i < syncResolvers; i++) { + properties[`sync${i}`] = (_value: any, data: any) => { + return data.id + } + } + + const data = Array.from({ length: count }, (_, i) => { + return { id: i } + }) + + // Treats all resolvers as either a promise or a function. + for (let index = 0; index < runs; index++) { + const resolver = resolve({ properties }) + const funcs = data.map((data) => { + return resolver._resolve(data, {}) + }) + console.time('sync' + index) + await Promise.all(funcs) + console.timeEnd('sync' + index) + } + + // Treats all resolvers as promises. + for (let index = 0; index < runs; index++) { + const resolver = resolve({ properties }) + const funcs = data.map((data) => { + return resolver.resolve(data, {}) + }) + console.time('async' + index) + await Promise.all(funcs) + console.timeEnd('async' + index) + } + }) })