Skip to content

Commit

Permalink
feat(schema): Add resolver for safe external data dispatching (#2641)
Browse files Browse the repository at this point in the history
  • Loading branch information
daffl authored May 22, 2022
1 parent 26d9e05 commit 72b980e
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 22 deletions.
49 changes: 46 additions & 3 deletions packages/schema/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ const getContext = <H extends HookContext> (context: H) => {
}
}

const getData = <H extends HookContext> (context: H) => {
const isPaginated = context.method === 'find' && context.result.data;
const data = isPaginated ? context.result.data : context.result;

return { isPaginated, data };
}

const runResolvers = async <T, H extends HookContext> (
resolvers: Resolver<T, H>[],
data: any,
Expand All @@ -28,6 +35,8 @@ const runResolvers = async <T, H extends HookContext> (
return current as T;
}

export const DISPATCH = Symbol('@feathersjs/schema/dispatch');

export const resolveQuery = <T, H extends HookContext> (...resolvers: Resolver<T, H>[]) =>
async (context: H, next?: NextFunction) => {
const ctx = getContext(context);
Expand Down Expand Up @@ -86,9 +95,7 @@ export const resolveResult = <T, H extends HookContext> (...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))) :
Expand All @@ -101,6 +108,42 @@ export const resolveResult = <T, H extends HookContext> (...resolvers: Resolver<
}
};

export const resolveDispatch = <T, H extends HookContext> (...resolvers: Resolver<T, H>[]) =>
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 = <H extends HookContext> (schema: Schema<any>) =>
async (context: H, next?: NextFunction) => {
const data = context?.params?.query || {};
Expand Down
25 changes: 19 additions & 6 deletions packages/schema/test/fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -48,10 +48,14 @@ export const userDataResolver = resolve<User, HookContext<Application>>({
export const userResultResolver = resolve<UserResult, HookContext<Application>>({
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<UserResult, HookContext<Application>>({
schema: userResultSchema,
properties: {
password: () => undefined
}
});

Expand All @@ -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);

Expand Down Expand Up @@ -105,6 +110,12 @@ export const messageResultResolver = resolve<MessageResult, HookContext<Applicat
}
});

export const messageDispatchResolver = resolve<MessageResult, HookContext<Application>>({
properties: {
secret: () => undefined
}
})

export const messageQuerySchema = schema({
$id: 'MessageQuery',
type: 'object',
Expand Down Expand Up @@ -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)
Expand All @@ -168,6 +180,7 @@ app.service('paginatedMessages').hooks([
]);

app.service('users').hooks([
resolveDispatch(userDispatchResolver),
resolveResult(userResultResolver, secondUserResultResolver)
]);

Expand Down
48 changes: 35 additions & 13 deletions packages/schema/test/hooks.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { createContext } from '@feathersjs/feathers';
import assert from 'assert';
import { app, MessageResult, UserResult } from './fixture';

Expand All @@ -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,
Expand All @@ -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
}

Expand All @@ -55,7 +56,7 @@ describe('@feathersjs/schema/hooks', () => {

assert.deepStrictEqual(messages, [{
id: 0,
user: externalUser,
user,
...payload
}]);

Expand All @@ -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
}

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

0 comments on commit 72b980e

Please # to comment.