Skip to content

Commit

Permalink
feat(schema): Virtual property resolvers (#2900)
Browse files Browse the repository at this point in the history
  • Loading branch information
daffl committed Dec 3, 2022
1 parent 834091c commit 7d03b57
Show file tree
Hide file tree
Showing 11 changed files with 14,818 additions and 85 deletions.
65 changes: 45 additions & 20 deletions docs/api/schema/resolvers.md
Expand Up @@ -98,6 +98,31 @@ Property resolver functions should only return a value and not have side effects

</BlockQuote>

## Virtual property resolvers

Virtual resolvers are property resolvers that do not use the `value` but instead always return a value of their own. The parameters are (`(data, context, status)`). The above example can be written like this:

```ts
import { resolve, virtual } from '@feathersjs/schema'

const userResolver = resolve<User, MyContext>({
isDrinkingAge: virtual(async (user, context) => {
const drinkingAge = await context.getDrinkingAge(user.country)

return user.age >= drinkingAge
}),
fullName: virtual(async (user, context) => {
return `${user.firstName} ${user.lastName}`
})
})
```

<BlockQuote type="warning" label="Important">

Virtual resolvers should always be used when combined with a [database adapter](../databases/adapters.md) in order to make valid [$select queries](../databases/querying.md#select). Otherwise queries could try to select fields that do not exist in the database which will throw an error.

</BlockQuote>

## Options

A resolver takes the following options as the second parameter:
Expand Down Expand Up @@ -194,8 +219,14 @@ app.service('users').hooks({

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.

<BlockQuote type="warning" label="Important">

Use as an `around` hook is recommended since this will ensure that database adapters will be able to handle [$select queries](../databases/querying.md#select) properly when using [virtual properties](#virtual-property-resolvers).

</BlockQuote>

```ts
import { schemaHooks, resolve } from '@feathersjs/schema'
import { schemaHooks, resolve, virtual } from '@feathersjs/schema'
import { Type } from '@feathersjs/typebox'
import type { Static } from '@feathersjs/typebox'
import type { HookContext } from '../declarations'
Expand Down Expand Up @@ -225,16 +256,14 @@ const messageSchema = Type.Object(
type Message = Static<typeof messageSchema>

export const messageResolver = resolve<Message, HookContext>({
properties: {
user: async (_value, message, context) => {
// Populate the user associated via `userId`
return context.app.service('users').get(message.userId)
}
}
user: virtual(async (message, context) => {
// Populate the user associated via `userId`
return context.app.service('users').get(message.userId)
})
})

app.service('messages').hooks({
after: {
around: {
all: [schemaHooks.resolveResult(messageResolver)]
}
})
Expand Down Expand Up @@ -262,10 +291,8 @@ const userSchema = Type.Object(
type User = Static<typeof userSchema>

export const userExternalResolver = resolve<User, HookContext>({
properties: {
// Always hide the password for external responses
password: async () => undefined
}
// Always hide the password for external responses
password: async () => undefined
})

// Dispatch should be resolved on every method
Expand Down Expand Up @@ -310,15 +337,13 @@ export const userQuerySchema = querySyntax(userQueryProperties)
export type UserQuery = Static<typeof userQuerySchema>

export const userQueryResolver = resolve<UserQuery, HookContext>({
properties: {
// If there is an authenticated user, they can only see their own data
id: async (value, query, context) => {
if (context.params.user) {
return context.params.user.id
}

return value
// If there is an authenticated user, they can only see their own data
id: async (value, query, context) => {
if (context.params.user) {
return context.params.user.id
}

return value
}
})

Expand Down
14 changes: 11 additions & 3 deletions docs/guides/basics/hooks.md
Expand Up @@ -139,8 +139,16 @@ export const message = (app: Application) => {
around: {
all: [
logRuntime,
authenticate('jwt')
]
authenticate('jwt'),
schemaHooks.resolveExternal(messageExternalResolver),
schemaHooks.resolveResult(messageResolver)
],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},
before: {
all: [
Expand All @@ -151,7 +159,7 @@ export const message = (app: Application) => {
]
},
after: {
all: [schemaHooks.resolveResult(messageResolver), schemaHooks.resolveExternal(messageExternalResolver)]
all: []
},
error: {
all: []
Expand Down
16 changes: 8 additions & 8 deletions docs/guides/basics/schemas.md
Expand Up @@ -214,8 +214,8 @@ Update the `src/services/messages/messages.schema.js` file like this:

<DatabaseBlock global-id="sql">

```ts{7,14-16,22-25,38-44,50,60-64}
import { resolve } from '@feathersjs/schema'
```ts{1, 7,14-16,22-25,38-44,50,60-64}
import { resolve, virtual } from '@feathersjs/schema'
import { Type, getDataValidator, getValidator, querySyntax } from '@feathersjs/typebox'
import type { Static } from '@feathersjs/typebox'
Expand All @@ -236,10 +236,10 @@ export const messageSchema = Type.Object(
)
export type Message = Static<typeof messageSchema>
export const messageResolver = resolve<Message, HookContext>({
user: async (_value, message, context) => {
user: virtual(async (message, context) => {
// Associate the user that sent the message
return context.app.service('users').get(message.userId)
}
})
})
export const messageExternalResolver = resolve<Message, HookContext>({})
Expand Down Expand Up @@ -289,8 +289,8 @@ export const messageQueryResolver = resolve<MessageQuery, HookContext>({

<DatabaseBlock global-id="mongodb">

```ts{7,14-16,22-25,38-44,50,60-64}
import { resolve } from '@feathersjs/schema'
```ts{1,7,14-16,22-25,38-44,50,60-64}
import { resolve, virtual } from '@feathersjs/schema'
import { Type, getDataValidator, getValidator, querySyntax } from '@feathersjs/typebox'
import type { Static } from '@feathersjs/typebox'
Expand All @@ -311,10 +311,10 @@ export const messageSchema = Type.Object(
)
export type Message = Static<typeof messageSchema>
export const messageResolver = resolve<Message, HookContext>({
user: async (_value, message, context) => {
user: virtual(async (message, context) => {
// Associate the user that sent the message
return context.app.service('users').get(message.userId)
}
})
})
export const messageExternalResolver = resolve<Message, HookContext>({})
Expand Down

0 comments on commit 7d03b57

Please sign in to comment.