Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,9 @@ export default defineConfig({
.map((x) => ({
text: x.title,
link: x.path,
}))
})),
},
{ text: 'Utility Types', link: '/utility-types' },

],
nav: [
{
Expand Down Expand Up @@ -161,7 +160,8 @@ export default defineConfig({
resolve(__dirname, `../../src/${category}/index.ts`),
]
return acc
}, {} as Record<string, string[]>
},
{} as Record<string, string[]>,
),
},
},
Expand Down
6 changes: 3 additions & 3 deletions docs/.vitepress/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ export default {
app.component('PredicatesTable', PredicatesTable)
app.component('TransformersTable', TransformersTable)
app.component('GuardsTable', GuardsTable)
app.component("HooksTable", HooksTable)
app.component("UtilsTable", UtilsTable)
app.component('HooksTable', HooksTable)
app.component('UtilsTable', UtilsTable)

app.component("Chip", Chip)
app.component('Chip', Chip)
},
} satisfies Theme
178 changes: 168 additions & 10 deletions src/hooks/create-related/create-related.hook.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import assert from 'node:assert'
import { expect } from 'vitest'
import { feathers } from '@feathersjs/feathers'
import { MemoryService } from '@feathersjs/memory'
import { createRelated } from './create-related.hook.js'
Expand Down Expand Up @@ -28,6 +28,127 @@
}
}

type User = {
id: number
name: string
}

type Todo = {
id: number
title: string
userId: number
}

const mockAppStronglyTyped = (_options?: MockAppOptions) => {
const options = Object.assign({}, defaultOptions, _options)
const app = feathers<{
users: MemoryService<User>
todos: MemoryService<Todo>
}>()

app.use('users', new MemoryService<User>({ startId: 1, multi: true }))
app.use(
'todos',
new MemoryService<Todo>({ startId: 1, multi: options.multi }),
)

const usersService = app.service('users')
const todosService = app.service('todos')

return {
app,
todosService,
usersService,
}
}

describe('hook - createRelated (type tests)', function () {
it('errors on wrong service name', function () {
const { app } = mockAppStronglyTyped()

app.service('users').hooks({
after: {
create: [
createRelated({
// @ts-expect-error - 'nonexistent' is not a valid service name
service: 'nonexistent',
data: (item) => [{ title: 'test', userId: item.id }],
}),
],
},
})
})

it('item in data function is properly typed', function () {
const { app } = mockAppStronglyTyped()

app.service('users').hooks({
after: {
create: [
createRelated({
service: 'todos',
data: (item) => {
const name: string = item.name
// @ts-expect-error - 'nonExistentProp' does not exist on User
const bad = item.nonExistentProp

Check warning on line 93 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'bad' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 93 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'bad' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 93 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'bad' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 93 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'bad' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 93 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'bad' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 93 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'bad' is assigned a value but never used. Allowed unused vars must match /^_/u
return [{ title: name, userId: item.id }]
},
}),
],
},
})
})

it('data can return a single object', function () {
const { app } = mockAppStronglyTyped()

app.service('users').hooks({
after: {
create: [
createRelated({
service: 'todos',
data: (item) => ({ title: 'test', userId: item.id }),
}),
],
},
})
})

it('data can return an array', function () {
const { app } = mockAppStronglyTyped()

app.service('users').hooks({
after: {
create: [
createRelated({
service: 'todos',
data: (item) => [
{ title: 'test1', userId: item.id },
{ title: 'test2', userId: item.id },
],
}),
],
},
})
})

it('data return type must match target service', function () {
const { app } = mockAppStronglyTyped()

app.service('users').hooks({
after: {
create: [
createRelated({
service: 'todos',
// @ts-expect-error - wrong data shape for todos service
data: (item) => [{ wrongField: 'test' }],

Check warning on line 144 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'item' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 144 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'item' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 144 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'item' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 144 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'item' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 144 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'item' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 144 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'item' is defined but never used. Allowed unused args must match /^_/u
}),
],
},
})
})
})

describe('hook - createRelated', function () {
it('creates single item for single item', async function () {
const { app, todosService } = mockApp()
Expand All @@ -37,7 +158,7 @@
create: [
createRelated({
service: 'todos',
data: (item, context) => ({

Check warning on line 161 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 161 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 161 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 161 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 161 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 161 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'context' is defined but never used. Allowed unused args must match /^_/u
title: 'First issue',
userId: item.id,
}),
Expand All @@ -46,13 +167,13 @@
},
})

const user = await app.service('users').create({

Check warning on line 170 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'user' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 170 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'user' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 170 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'user' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 170 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'user' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 170 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'user' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 170 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'user' is assigned a value but never used. Allowed unused vars must match /^_/u
name: 'John Doe',
})

const todos = await todosService.find({ query: {} })

assert.deepStrictEqual(todos, [{ id: 1, title: 'First issue', userId: 1 }])
expect(todos).toStrictEqual([{ id: 1, title: 'First issue', userId: 1 }])
})

it('can use context in data function', async function () {
Expand All @@ -72,13 +193,13 @@
},
})

const user = await app.service('users').create({

Check warning on line 196 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'user' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 196 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'user' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 196 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'user' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 196 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'user' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 196 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'user' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 196 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'user' is assigned a value but never used. Allowed unused vars must match /^_/u
name: 'John Doe',
})

const todos = await todosService.find({ query: {} })

assert.deepStrictEqual(todos, [{ id: 1, title: 'users', userId: 1 }])
expect(todos).toStrictEqual([{ id: 1, title: 'users', userId: 1 }])
})

it('creates multiple items for multiple items', async function () {
Expand All @@ -89,7 +210,7 @@
create: [
createRelated({
service: 'todos',
data: (item, context) => ({

Check warning on line 213 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 213 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 213 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 213 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 213 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 213 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'context' is defined but never used. Allowed unused args must match /^_/u
title: item.name,
userId: item.id,
}),
Expand All @@ -98,13 +219,13 @@
},
})

const users = await app

Check warning on line 222 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'users' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 222 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'users' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 222 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'users' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 222 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'users' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 222 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'users' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 222 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'users' is assigned a value but never used. Allowed unused vars must match /^_/u
.service('users')
.create([{ name: 'user1' }, { name: 'user2' }, { name: 'user3' }])

const todos = await todosService.find({ query: { $sort: { userId: 1 } } })

assert.deepStrictEqual(todos, [
expect(todos).toStrictEqual([
{ id: 1, title: 'user1', userId: 1 },
{ id: 2, title: 'user2', userId: 2 },
{ id: 3, title: 'user3', userId: 3 },
Expand All @@ -119,7 +240,7 @@
create: [
createRelated({
service: 'todos',
data: (item, context) => ({

Check warning on line 243 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 243 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 243 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 243 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 243 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 243 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'context' is defined but never used. Allowed unused args must match /^_/u
title: item.name,
userId: item.id,
}),
Expand All @@ -130,15 +251,15 @@
})

// @ts-expect-error - does not have options
assert.strictEqual(todosService.options.multi, false)
expect(todosService.options.multi).toBe(false)

const users = await app

Check warning on line 256 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'users' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 256 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'users' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 256 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'users' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 256 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'users' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 256 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'users' is assigned a value but never used. Allowed unused vars must match /^_/u

Check warning on line 256 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'users' is assigned a value but never used. Allowed unused vars must match /^_/u
.service('users')
.create([{ name: 'user1' }, { name: 'user2' }, { name: 'user3' }])

const todos = await todosService.find({ query: { $sort: { userId: 1 } } })

assert.deepStrictEqual(todos, [
expect(todos).toStrictEqual([
{ id: 1, title: 'user1', userId: 1 },
{ id: 2, title: 'user2', userId: 2 },
{ id: 3, title: 'user3', userId: 3 },
Expand All @@ -153,7 +274,7 @@
create: [
createRelated({
service: 'todos',
data: (item, context) => [

Check warning on line 277 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 277 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 277 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 277 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (20.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 277 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (24.x)

'context' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 277 in src/hooks/create-related/create-related.hook.test.ts

View workflow job for this annotation

GitHub Actions / test (22.x)

'context' is defined but never used. Allowed unused args must match /^_/u
{
title: 1,
userId: item.id,
Expand All @@ -174,12 +295,49 @@

const todos = await todosService.find({ query: {} })

assert.deepStrictEqual(todos, [
expect(todos).toStrictEqual([
{ id: 1, title: 1, userId: 1 },
{ id: 2, title: 2, userId: 1 },
])
})

it('creates multiple data for multiple records', async function () {
const { app, todosService } = mockApp()

app.service('users').hooks({
after: {
create: [
createRelated({
service: 'todos',
data: (item, context) => [
{
title: `${item.name}-a`,
userId: item.id,
},
{
title: `${item.name}-b`,
userId: item.id,
},
],
}),
],
},
})

const users = await app
.service('users')
.create([{ name: 'user1' }, { name: 'user2' }])

const todos = await todosService.find({ query: { $sort: { id: 1 } } })

expect(todos).toStrictEqual([
{ id: 1, title: 'user1-a', userId: 1 },
{ id: 2, title: 'user1-b', userId: 1 },
{ id: 3, title: 'user2-a', userId: 2 },
{ id: 4, title: 'user2-b', userId: 2 },
])
})

it('can pass an array', async function () {
const { app, todosService } = mockApp()

Expand Down Expand Up @@ -216,7 +374,7 @@

const todos = await todosService.find({ query: {} })

assert.deepStrictEqual(todos, [
expect(todos).toStrictEqual([
{ id: 1, title: 1, userId: 1 },
{ id: 2, title: 2, userId: 1 },
])
Expand All @@ -230,7 +388,7 @@
create: [
createRelated({
service: 'todos',
data: (item, context) => null as any,
data: (item, context) => undefined,
}),
],
},
Expand All @@ -242,6 +400,6 @@

const todos = await todosService.find({ query: {} })

assert.deepStrictEqual(todos, [])
expect(todos).toStrictEqual([])
})
})
39 changes: 29 additions & 10 deletions src/hooks/create-related/create-related.hook.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import type { HookContext, NextFunction } from '@feathersjs/feathers'
import { checkContext, getResultIsArray } from '../../utils/index.js'
import type { MaybeArray, Promisable } from '../../internal.utils.js'
import type { InferCreateDataSingle } from '../../utility-types/infer-service-methods.js'
import type { ResultSingleHookContext } from '../../utility-types/hook-context.js'

export interface CreateRelatedOptions<S = Record<string, any>> {
service: keyof S
export interface CreateRelatedOptions<
H extends HookContext = HookContext,
Services extends H['app']['services'] = H['app']['services'],
S extends keyof Services = keyof Services,
> {
service: S
/**
* Is relevant when the current context result is an array.
*
Expand All @@ -15,8 +21,16 @@ export interface CreateRelatedOptions<S = Record<string, any>> {
multi?: boolean
/**
* A function that returns the data to be created in the related service.
*
* Receives the current item from the context result and the full hook context as arguments.
* Can return a single data object, an array of data objects, or a promise that resolves to either.
*
* If the function returns undefined, no related record will be created for that item.
*/
data: (item: any, context: HookContext) => Promisable<Record<string, any>>
data: (
item: ResultSingleHookContext<H>,
context: H,
) => Promisable<MaybeArray<InferCreateDataSingle<Services[S]>> | undefined>
}

/**
Expand All @@ -37,10 +51,9 @@ export interface CreateRelatedOptions<S = Record<string, any>> {
*
* @see https://utils.feathersjs.com/hooks/create-related.html
*/
export function createRelated<
S = Record<string, any>,
H extends HookContext = HookContext,
>(options: MaybeArray<CreateRelatedOptions<S>>) {
export function createRelated<H extends HookContext = HookContext>(
options: MaybeArray<CreateRelatedOptions<H>>,
) {
return async (context: H, next?: NextFunction) => {
checkContext(context, ['after', 'around'], ['create'], 'createRelated')

Expand All @@ -58,7 +71,9 @@ export function createRelated<

const dataToCreate = (
await Promise.all(result.map(async (item) => data(item, context)))
).filter((x) => !!x)
)
.flat()
.filter((x) => !!x)

if (!dataToCreate || dataToCreate.length <= 0) {
return context
Expand All @@ -67,11 +82,15 @@ export function createRelated<
if (multi || dataToCreate.length === 1) {
await context.app
.service(service as string)
.create(dataToCreate.length === 1 ? dataToCreate[0] : dataToCreate)
.create(
dataToCreate.length === 1
? (dataToCreate[0] as any)
: (dataToCreate as any),
)
} else {
await Promise.all(
dataToCreate.map(async (item) =>
context.app.service(service as string).create(item),
context.app.service(service as string).create(item as any),
),
)
}
Expand Down
Loading