Skip to content

Commit 187868e

Browse files
authored
fix(schema): Allow regular functions in resolvers (#3487)
1 parent f2829b1 commit 187868e

2 files changed

Lines changed: 28 additions & 31 deletions

File tree

docs/api/schema/resolvers.md

Lines changed: 23 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
outline: deep
33
---
4+
45
# Resolvers
56

67
Resolvers dynamically resolve individual properties based on a context, in a Feathers application usually the [hook context](../hooks.md#hook-context).
@@ -38,23 +39,23 @@ type Message = {
3839
}
3940

4041
class MyContext {
41-
async getUser(id) {
42+
getUser(id) {
4243
return {
4344
id,
4445
name: 'David'
4546
}
4647
}
4748

48-
async getLikes(messageId) {
49+
getLikes(messageId) {
4950
return 10
5051
}
5152
}
5253

5354
const messageResolver = resolve<Message, MyContext>({
54-
likes: async (value, message, context) => {
55+
likes: (value, message, context) => {
5556
return context.getLikes(message.id)
5657
},
57-
user: async (value, message, context) => {
58+
user: (value, message, context) => {
5859
return context.getUser(message.userId)
5960
}
6061
})
@@ -71,7 +72,7 @@ const resolvedMessage = await messageResolver.resolve(
7172

7273
## Property resolvers
7374

74-
Property resolvers are a map of property names to resolver functions. A resolver function is an `async` function that resolves a property on a data object. If it returns `undefined` the property will not be included. It gets passed the following parameters:
75+
Property resolvers are a map of property names to resolver functions. A resolver function is an `async` or regular function that resolves a property on a data object. If it returns `undefined` the property will not be included. It gets passed the following parameters:
7576

7677
- `value` - The current value which can also be `undefined`
7778
- `data` - The initial data object
@@ -85,7 +86,7 @@ const userResolver = resolve<User, MyContext>({
8586

8687
return user.age >= drinkingAge
8788
},
88-
fullName: async (value, user, context) => {
89+
fullName: (value, user, context) => {
8990
return `${user.firstName} ${user.lastName}`
9091
}
9192
})
@@ -110,7 +111,7 @@ const userResolver = resolve<User, MyContext>({
110111

111112
return user.age >= drinkingAge
112113
}),
113-
fullName: virtual(async (user, context) => {
114+
fullName: virtual((user, context) => {
114115
return `${user.firstName} ${user.lastName}`
115116
})
116117
})
@@ -126,7 +127,7 @@ Virtual resolvers should always be used when combined with a [database adapter](
126127

127128
A resolver takes the following options as the second parameter:
128129

129-
- `converter` (optional): A `async (data, context) => {}` function that can return a completely new representation of the data. A `converter` runs before `properties` resolvers.
130+
- `converter` (optional): A `(data, context) => {}` or `async (data, context) => {}` function that can return a completely new representation of the data. A `converter` runs before `properties` resolvers.
130131

131132
```ts
132133
const userResolver = resolve<User, MyContext>(
@@ -185,22 +186,16 @@ type MessageData = Static<typeof messageDataSchema>
185186

186187
// Resolver that automatically set `userId` and `createdAt`
187188
const messageDataResolver = resolve<Message, HookContext>({
188-
userId: async (value, message, context) => {
189-
// Associate the currently authenticated user
190-
return context.params?.user.id
191-
},
192-
createdAt: async () => {
193-
// Return the current date
194-
return Date.now()
195-
}
189+
// Associate the currently authenticated user
190+
userId: (value, message, context) => context.params?.user.id,
191+
// Return the current date
192+
createdAt: () => Date.now()
196193
})
197194

198195
// Resolver that automatically sets `updatedAt`
199196
const messagePatchResolver = resolve<Message, HookContext>({
200-
updatedAt: async () => {
201-
// Return the current date
202-
return Date.now()
203-
}
197+
// Return the current date
198+
updatedAt: () => Date.now()
204199
})
205200

206201
app.service('users').hooks({
@@ -267,7 +262,8 @@ type Message = Static<typeof messageSchema>
267262
export const messageResolver = resolve<Message, HookContext>({
268263
user: virtual(async (message, context) => {
269264
// Populate the user associated via `userId`
270-
return context.app.service('users').get(message.userId)
265+
const user = await context.app.service('users').get(message.userId)
266+
return user
271267
})
272268
})
273269

@@ -301,7 +297,7 @@ type User = Static<typeof userSchema>
301297

302298
export const userExternalResolver = resolve<User, HookContext>({
303299
// Always hide the password for external responses
304-
password: async () => undefined
300+
password: () => undefined
305301
})
306302

307303
// Dispatch should be resolved on every method
@@ -324,11 +320,11 @@ In order to get the safe data from resolved associations **all services** involv
324320

325321
Query resolvers use the `hooks.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.
326322

327-
In this example for a `User` schema we are first checking if a user is available in our request. In the case a user is available we are returning the user's ID. Otherwise we return whatever value was provided for `id`.
323+
In this example for a `User` schema we are first checking if a user is available in our request. In the case a user is available we are returning the user's ID. Otherwise we return whatever value was provided for `id`.
328324

329325
`context.params.user` would only be set if the request contains a user. This is usually the case when an external request is made. In the case of an internal request we may not have a specific user we are dealing with, and we will just return `value`.
330326

331-
If we were to receive an internal request, such as `app.service('users').get(123)`, `context.params.user` would be `undefined` and we would just return the `value` which is `123`.
327+
If we were to receive an internal request, such as `app.service('users').get(123)`, `context.params.user` would be `undefined` and we would just return the `value` which is `123`.
332328

333329
```ts
334330
import { hooks as schemaHooks, resolve } from '@feathersjs/schema'
@@ -353,7 +349,7 @@ export type UserQuery = Static<typeof userQuerySchema>
353349

354350
export const userQueryResolver = resolve<UserQuery, HookContext>({
355351
// If there is an authenticated user, they can only see their own data
356-
id: async (value, query, context) => {
352+
id: (value, query, context) => {
357353
if (context.params.user) {
358354
return context.params.user.id
359355
}
@@ -372,7 +368,7 @@ app.service('users').hooks({
372368

373369
For a more complicated example. We will make a separate `queryResolver`, called `companyFilterQueryResolver`, that will act as a ownership filter. We will have a `Company` service that is owned by a `User`. We will assume our app has two registered users and two companies. Each user owning one company. For simplicity, `User1` owns `Company1`, and `User2` owns `Company2`
374370

375-
We want to make sure only the user that owns the company can make any requests related to it. Our schema contains a `ownerUser` field, this is the owner of the company. When a request is made to the company schema, we are effectivly filtering our search for companies to be only those whose `ownerUser` matches the requesting user's id.
371+
We want to make sure only the user that owns the company can make any requests related to it. Our schema contains a `ownerUser` field, this is the owner of the company. When a request is made to the company schema, we are effectivly filtering our search for companies to be only those whose `ownerUser` matches the requesting user's id.
376372

377373
So if a `GET /company` request is made by `User1`, our resolver will convert our query to `GET /company?name=Company1&ownerUser={User1.id}`. The result will only return an array of 1 company to `User1`
378374

@@ -404,12 +400,11 @@ export const companyQueryValidator = getValidator(companyQuerySchema, queryValid
404400
export const companyQueryResolver = resolve<CompanyQuery, HookContext>({})
405401

406402
export const companyFilterQueryResolver = resolve<Company, HookContext>({
407-
ownerUser: async (value, obj, context) => {
403+
ownerUser: (value, obj, context) => {
408404
if (context.params.user) {
409405
return context.params.user.id
410406
}
411407
return value
412408
}
413409
})
414410
```
415-

packages/schema/src/resolver.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import { BadRequest } from '@feathersjs/errors'
22
import { Schema } from './schema'
33

4+
type PromiseOrLiteral<V> = Promise<V> | V
5+
46
export type PropertyResolver<T, V, C> = ((
57
value: V | undefined,
68
obj: T,
79
context: C,
810
status: ResolverStatus<T, C>
9-
) => Promise<V | undefined>) & { [IS_VIRTUAL]?: boolean }
11+
) => PromiseOrLiteral<V | undefined>) & { [IS_VIRTUAL]?: boolean }
1012

1113
export type VirtualResolver<T, V, C> = (
1214
obj: T,
1315
context: C,
1416
status: ResolverStatus<T, C>
15-
) => Promise<V | undefined>
17+
) => PromiseOrLiteral<V | undefined>
1618

1719
export const IS_VIRTUAL = Symbol.for('@feathersjs/schema/virtual')
1820

@@ -40,7 +42,7 @@ export type ResolverConverter<T, C> = (
4042
obj: any,
4143
context: C,
4244
status: ResolverStatus<T, C>
43-
) => Promise<T | undefined>
45+
) => PromiseOrLiteral<T | undefined>
4446

4547
export interface ResolverOptions<T, C> {
4648
schema?: Schema<T>

0 commit comments

Comments
 (0)