From 72b980e05631136d30c8f1468dee45ec6a8d2503 Mon Sep 17 00:00:00 2001 From: David Luecke Date: Sun, 22 May 2022 09:00:18 -0700 Subject: [PATCH] feat(schema): Add resolver for safe external data dispatching (#2641) --- packages/schema/src/hooks.ts | 49 ++++++++++++++++++++++++++++-- packages/schema/test/fixture.ts | 25 +++++++++++---- packages/schema/test/hooks.test.ts | 48 +++++++++++++++++++++-------- 3 files changed, 100 insertions(+), 22 deletions(-) diff --git a/packages/schema/src/hooks.ts b/packages/schema/src/hooks.ts index 4f890d5be6..1c2205c2c6 100644 --- a/packages/schema/src/hooks.ts +++ b/packages/schema/src/hooks.ts @@ -13,6 +13,13 @@ const getContext = (context: H) => { } } +const getData = (context: H) => { + const isPaginated = context.method === 'find' && context.result.data; + const data = isPaginated ? context.result.data : context.result; + + return { isPaginated, data }; +} + const runResolvers = async ( resolvers: Resolver[], data: any, @@ -28,6 +35,8 @@ const runResolvers = async ( return current as T; } +export const DISPATCH = Symbol('@feathersjs/schema/dispatch'); + export const resolveQuery = (...resolvers: Resolver[]) => async (context: H, next?: NextFunction) => { const ctx = getContext(context); @@ -86,9 +95,7 @@ export const resolveResult = (...resolvers: Resolver< const ctx = getContext(context); const status = context.params.resolve; - - const isPaginated = context.method === 'find' && context.result.data; - const data = isPaginated ? context.result.data : context.result; + const { isPaginated, data } = getData(context); const result = Array.isArray(data) ? await Promise.all(data.map(async current => runResolvers(resolvers, current, ctx, status))) : @@ -101,6 +108,42 @@ export const resolveResult = (...resolvers: Resolver< } }; +export const resolveDispatch = (...resolvers: Resolver[]) => + async (context: H, next?: NextFunction) => { + if (typeof next === 'function') { + await next(); + } + + const ctx = getContext(context); + const status = context.params.resolve; + const { isPaginated, data } = getData(context); + const resolveDispatch = async (current: any) => { + const resolved = await runResolvers(resolvers, current, ctx, status) + + return Object.keys(resolved).reduce((res, key) => { + const value = current[key]; + const hasDispatch = typeof value === 'object' && value !== null && value[DISPATCH] !== undefined; + + res[key] = hasDispatch ? value[DISPATCH] : value; + + return res + }, {} as any) + } + + const result = await (Array.isArray(data) ? Promise.all(data.map(resolveDispatch)) : resolveDispatch(data)); + const dispatch = isPaginated ? { + ...context.result, + data: result + } : result; + + context.dispatch = dispatch; + Object.defineProperty(context.result, DISPATCH, { + value: dispatch, + enumerable: false, + configurable: false + }); + }; + export const validateQuery = (schema: Schema) => async (context: H, next?: NextFunction) => { const data = context?.params?.query || {}; diff --git a/packages/schema/test/fixture.ts b/packages/schema/test/fixture.ts index 8780536f31..1bc2677b4d 100644 --- a/packages/schema/test/fixture.ts +++ b/packages/schema/test/fixture.ts @@ -6,7 +6,7 @@ import { GeneralError } from '@feathersjs/errors'; import { schema, resolve, Infer, resolveResult, resolveQuery, - resolveData, validateData, validateQuery, querySyntax, Combine + resolveData, validateData, validateQuery, querySyntax, Combine, resolveDispatch } from '../src'; import { AdapterParams } from '../../memory/node_modules/@feathersjs/adapter-commons/lib'; @@ -48,10 +48,14 @@ export const userDataResolver = resolve>({ export const userResultResolver = resolve>({ schema: userResultSchema, properties: { - name: async (_value, user) => user.email.split('@')[0], - password: async (value, _user, context) => { - return context.params.provider ? undefined : value; - } + name: async (_value, user) => user.email.split('@')[0] + } +}); + +export const userDispatchResolver = resolve>({ + schema: userResultSchema, + properties: { + password: () => undefined } }); @@ -69,7 +73,8 @@ export const messageSchema = schema({ required: ['text', 'userId'], properties: { text: { type: 'string' }, - userId: { type: 'number' } + userId: { type: 'number' }, + secret: { type: 'boolean' } } } as const); @@ -105,6 +110,12 @@ export const messageResultResolver = resolve>({ + properties: { + secret: () => undefined + } +}) + export const messageQuerySchema = schema({ $id: 'MessageQuery', type: 'object', @@ -156,6 +167,7 @@ app.use('messages', memory()) app.use('paginatedMessages', memory({paginate: { default: 10 }})); app.service('messages').hooks([ + resolveDispatch(messageDispatchResolver), validateQuery(messageQuerySchema), resolveQuery(messageQueryResolver), resolveResult(messageResultResolver) @@ -168,6 +180,7 @@ app.service('paginatedMessages').hooks([ ]); app.service('users').hooks([ + resolveDispatch(userDispatchResolver), resolveResult(userResultResolver, secondUserResultResolver) ]); diff --git a/packages/schema/test/hooks.test.ts b/packages/schema/test/hooks.test.ts index e74458f3ab..fbf369b994 100644 --- a/packages/schema/test/hooks.test.ts +++ b/packages/schema/test/hooks.test.ts @@ -1,3 +1,4 @@ +import { createContext } from '@feathersjs/feathers'; import assert from 'assert'; import { app, MessageResult, UserResult } from './fixture'; @@ -15,7 +16,8 @@ describe('@feathersjs/schema/hooks', () => { }]))[0]; message = await app.service('messages').create({ text, - userId: user.id + userId: user.id, + secret: true }); messageOnPaginatedService = await app.service('paginatedMessages').create({ text, @@ -34,10 +36,9 @@ describe('@feathersjs/schema/hooks', () => { }); it('resolves results and handles resolver errors (#2534)', async () => { - // eslint-disable-next-line - const { password, ...externalUser } = user; const payload = { userId: user.id, + secret: true, text } @@ -55,7 +56,7 @@ describe('@feathersjs/schema/hooks', () => { assert.deepStrictEqual(messages, [{ id: 0, - user: externalUser, + user, ...payload }]); @@ -79,10 +80,9 @@ describe('@feathersjs/schema/hooks', () => { }); it('resolves get result with the object on result', async () => { - // eslint-disable-next-line - const { password, ...externalUser } = user; const payload = { userId: user.id, + secret: true, text } @@ -100,14 +100,12 @@ describe('@feathersjs/schema/hooks', () => { assert.deepStrictEqual(result, { id: 0, - user: externalUser, + user, ...payload }); }); it('resolves find results with paginated result object', async () => { - // eslint-disable-next-line - const { password, ...externalUser } = user; const payload = { userId: user.id, text @@ -129,11 +127,35 @@ describe('@feathersjs/schema/hooks', () => { } }); - assert.deepStrictEqual(messages, { limit: 1, skip: 0, total: 1, data: [{ + assert.deepStrictEqual(messages, { + limit: 1, + skip: 0, + total: 1, + data: [{ + id: 0, + user, + ...payload + }] + }); + }); + + it('resolves safe dispatch data recursively', async () => { + const service = app.service('messages'); + const context = await service.get(0, {}, createContext(service as any, 'get')) + + assert.ok(context.result.secret) + assert.strictEqual(context.result.user.password, 'hashed') + + assert.deepStrictEqual(context.dispatch, { + text: 'Hi there', + userId: 0, id: 0, - user: externalUser, - ...payload - }]}); + user: { + email: 'hello@feathersjs.com', + id: 0, + name: 'hello (hello@feathersjs.com)' + } + }) }); it('validates and converts the query', async () => {