Skip to content

Commit

Permalink
fix(schema): Use the same options for resolveData hook (#2833)
Browse files Browse the repository at this point in the history
  • Loading branch information
daffl authored Oct 26, 2022
1 parent 0776e26 commit ed3b050
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 44 deletions.
8 changes: 4 additions & 4 deletions docs/api/schema/resolvers.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ In a Feathers application, resolvers are used through [hooks](../hooks.md) to co

### resolveData

Data resolvers use the `schemaHooks.resolveData` hook and convert the `data` from a `create`, `update` or `patch` [service method](../services.md) or a [custom method](../services.md#custom-methods). This can be used to validate against the schema and e.g. hash a password before storing it in the database or to remove properties the user is not allowed to write. `schemaHooks.resolveData` can be used as an `around` and `before` hook.
Data resolvers use the `schemaHooks.resolveData(...resolvers)` hook and convert the `data` from a `create`, `update` or `patch` [service method](../services.md) or a [custom method](../services.md#custom-methods). This can be used to validate against the schema and e.g. hash a password before storing it in the database or to remove properties the user is not allowed to write. It is possible to pass multiple resolvers which will run in the order they are passed, using the previous data. `schemaHooks.resolveData` can be used as an `around` and `before` hook.

```ts
import type { HookContext } from '../declarations'
Expand Down Expand Up @@ -173,7 +173,7 @@ app.service('users').hooks({

### resolveResult

Result resolvers use the `schemaHooks.resolveResult` hook and resolve the data that is returned by a service call ([context.result](../hooks.md#context-result) in a hook). This can be used to populate associations or add other computed properties etc. `schemaHooks.resolveResult` can be used as an `around` and `after` hook.
Result resolvers use the `schemaHooks.resolveResult(...resolvers)` hook and resolve the data that is returned by a service call ([context.result](../hooks.md#context-result) in a hook). This can be used to populate associations or add other computed properties etc. It is possible to pass multiple resolvers which will run in the order they are passed, using the previous data. `schemaHooks.resolveResult` can be used as an `around` and `after` hook.

```ts
import { schemaHooks, resolve } from '@feathersjs/schema'
Expand Down Expand Up @@ -223,7 +223,7 @@ app.service('messages').hooks({

### resolveExternal

External (or dispatch) resolver use the `schemaHooks.resolveDispatch` hook to return a safe version of the data that will be sent to external clients. Returning `undefined` for a property resolver will exclude the property which can be used to hide sensitive data like the user password. This includes nested associations and real-time events. `schemaHooks.resolveExternal` can be used as an `around` or `after` hook.
External (or dispatch) resolver use the `schemaHooks.resolveDispatch(...resolvers)` hook to return a safe version of the data that will be sent to external clients. It is possible to pass multiple resolvers which will run in the order they are passed, using the previous data. Returning `undefined` for a property resolver will exclude the property which can be used to hide sensitive data like the user password. This includes nested associations and real-time events. `schemaHooks.resolveExternal` can be used as an `around` or `after` hook.

```ts
import { schemaHooks, resolve } from '@feathersjs/schema'
Expand Down Expand Up @@ -267,7 +267,7 @@ In order to get the safe data from resolved associations **all services** involv

### resolveQuery

Query resolvers use the `schemaHooks.resolveQuery` hook to modify `params.query`. This is often used to set default values or limit the query so a user can only request data they are allowed to see. `schemaHooks.resolveQuery` can be used as an `around` or `before` hook.
Query resolvers use the `schemaHooks.resolveQuery(...resolvers)` hook to modify `params.query`. This is often used to set default values or limit the query so a user can only request data they are allowed to see. It is possible to pass multiple resolvers which will run in the order they are passed, using the previous data. `schemaHooks.resolveQuery` can be used as an `around` or `before` hook.

```ts
import { schemaHooks, resolve } from '@feathersjs/schema'
Expand Down
51 changes: 29 additions & 22 deletions packages/schema/src/hooks/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,6 @@ const runResolvers = async <T, H extends HookContext>(

export type ResolverSetting<H extends HookContext> = Resolver<any, H> | Resolver<any, H>[]

export type DataResolvers<H extends HookContext> = {
create: Resolver<any, H>
patch: Resolver<any, H>
update: Resolver<any, H>
}

export type ResolveAllSettings<H extends HookContext> = {
data?: DataResolvers<H>
query?: Resolver<any, H>
result?: Resolver<any, H>
dispatch?: Resolver<any, H>
}

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

export const getDispatch = (value: any, fallback = value) =>
typeof value === 'object' && value !== null && value[DISPATCH] !== undefined ? value[DISPATCH] : fallback

export const resolveQuery =
<T, H extends HookContext>(...resolvers: Resolver<T, H>[]) =>
async (context: H, next?: NextFunction) => {
Expand All @@ -74,10 +56,9 @@ export const resolveQuery =
}

export const resolveData =
<H extends HookContext>(settings: DataResolvers<H> | Resolver<any, H>) =>
<T, H extends HookContext>(...resolvers: Resolver<T, H>[]) =>
async (context: H, next?: NextFunction) => {
if (context.method === 'create' || context.method === 'patch' || context.method === 'update') {
const resolvers = settings instanceof Resolver ? [settings] : [settings[context.method]]
if (context.data !== undefined) {
const ctx = getContext(context)
const data = context.data

Expand Down Expand Up @@ -132,6 +113,11 @@ export const resolveResult =
}
}

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

export const getDispatch = (value: any, fallback = value) =>
typeof value === 'object' && value !== null && value[DISPATCH] !== undefined ? value[DISPATCH] : fallback

export const resolveDispatch =
<T, H extends HookContext>(...resolvers: Resolver<T, H>[]) =>
async (context: H, next?: NextFunction) => {
Expand Down Expand Up @@ -178,6 +164,19 @@ export const resolveDispatch =

export const resolveExternal = resolveDispatch

export type ResolveAllSettings<H extends HookContext> = {
data?: {
create: Resolver<any, H>
patch: Resolver<any, H>
update: Resolver<any, H>
}
query?: Resolver<any, H>
result?: Resolver<any, H>
dispatch?: Resolver<any, H>
}

const dataMethods = ['create', 'update', 'patch'] as const

export const resolveAll = <H extends HookContext>(map: ResolveAllSettings<H>) => {
const middleware = []

Expand All @@ -192,7 +191,15 @@ export const resolveAll = <H extends HookContext>(map: ResolveAllSettings<H>) =>
}

if (map.data) {
middleware.push(resolveData(map.data))
dataMethods.forEach((name) => {
if (map.data[name]) {
const resolver = resolveData(map.data[name])

middleware.push(async (context: H, next: NextFunction) =>
context.method === name ? resolver(context, next) : next()
)
}
})
}

return compose(middleware)
Expand Down
54 changes: 36 additions & 18 deletions packages/schema/test/fixture.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { feathers, HookContext, Application as FeathersApplication } from '@feathersjs/feathers'
import {
feathers,
HookContext,
Application as FeathersApplication,
defaultServiceMethods
} from '@feathersjs/feathers'
import { memory, MemoryService } from '@feathersjs/memory'
import { GeneralError } from '@feathersjs/errors'

Expand Down Expand Up @@ -158,14 +163,27 @@ export const messageQueryResolver = resolve<MessageQuery, HookContext<Applicatio
}
})

class MessageService extends MemoryService<Message, MessageData, ServiceParams> {
async customMethod(data: any) {
return data
}
}

const customMethodDataResolver = resolve<any, HookContext<Application>>({
properties: {
userId: async () => 0,
additionalData: async () => 'additional data'
}
})

interface ServiceParams extends AdapterParams {
user?: User
error?: boolean
}

type ServiceTypes = {
users: MemoryService<User, UserData, ServiceParams>
messages: MemoryService<Message, MessageData, ServiceParams>
messages: MessageService
paginatedMessages: MemoryService<Message, MessageData, ServiceParams>
}
type Application = FeathersApplication<ServiceTypes>
Expand All @@ -178,16 +196,23 @@ app.use(
multi: ['create']
})
)
app.use('messages', memory())
app.use('messages', new MessageService(), {
methods: [...defaultServiceMethods, 'customMethod']
})
app.use('paginatedMessages', memory({ paginate: { default: 10 } }))

app.service('messages').hooks([
resolveAll({
result: messageResolver,
query: messageQueryResolver
}),
validateQuery(messageQueryValidator)
])
app.service('messages').hooks({
around: {
all: [
resolveAll({
result: messageResolver,
query: messageQueryResolver
}),
validateQuery(messageQueryValidator)
],
customMethod: [resolveData(customMethodDataResolver)]
}
})

app
.service('paginatedMessages')
Expand All @@ -202,14 +227,7 @@ app
.hooks([resolveDispatch(userExternalResolver), resolveResult(userResolver, secondUserResolver)])

app.service('users').hooks({
create: [
validateData(userDataValidator),
resolveData({
create: userDataResolver,
patch: userDataResolver,
update: userDataResolver
})
]
create: [validateData(userDataValidator), resolveData(userDataResolver)]
})

export { app }
16 changes: 16 additions & 0 deletions packages/schema/test/hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,22 @@ describe('@feathersjs/schema/hooks', () => {
})
})

it('resolves data for custom methods', async () => {
const result = await app.service('messages').customMethod({ message: 'Hello' })

assert.deepStrictEqual(result, {
message: 'Hello',
userId: 0,
additionalData: 'additional data',
user: {
email: 'hello@feathersjs.com',
password: 'hashed',
id: 0,
name: 'hello (hello@feathersjs.com)'
}
})
})

it('validates and converts the query', async () => {
const otherUser = await app.service('users').create({
email: 'helloagain@feathersjs.com',
Expand Down

0 comments on commit ed3b050

Please sign in to comment.