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
3 changes: 2 additions & 1 deletion .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
"Bash(gh release list *)",
"Bash(gh release view *)",
"Bash(npm run *)",
"Bash(pnpm typecheck *)"
"Bash(pnpm typecheck *)",
"Bash(pnpm vitest *)"
]
}
}
15 changes: 13 additions & 2 deletions src/hooks/cache/cache.hook.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { HookContext } from '@feathersjs/feathers'
import type { AroundHookFunction, HookContext } from '@feathersjs/feathers'
import { feathers } from '@feathersjs/feathers'
import type { CacheOptions } from './cache.hook.js'
import { cache } from './cache.hook.js'
import { LRUCache } from 'lru-cache'
import { TTLCache } from '@isaacs/ttlcache'
import { MemoryService } from '@feathersjs/memory'
import { expect } from 'vitest'
import { expect, expectTypeOf } from 'vitest'
import { copy } from 'fast-copy'

const setup = (options: CacheOptions, serviceOptions?: { id?: string }) => {
Expand Down Expand Up @@ -962,4 +962,15 @@ describe('cache hook with custom serialize', () => {
await usersService.find({ query: { name: 'Jane' } })
expect(before.find).toHaveBeenCalledTimes(1)
})

it('is type-compatible with AroundHookFunction', () => {
type Item = { id: number; name: string }
type Services = { items: MemoryService<Item> }
type App = ReturnType<typeof feathers<Services>>
type Ctx = HookContext<App, MemoryService<Item>>

expectTypeOf(
cache<Ctx>({ map: new Map(), transformParams: (p) => p }),
).toExtend<AroundHookFunction<App, MemoryService<Item>>>()
})
})
16 changes: 9 additions & 7 deletions src/hooks/cache/cache.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ export const cache = <H extends HookContext = HookContext>(
options: CacheOptions,
) => {
const cacheMap = new ContextCacheMap(options)
return async (context: H, next?: NextFunction) => {
return async (context: H, next?: NextFunction): Promise<void> => {
if (context.type === 'before') {
return await cacheBefore(context, cacheMap)
}
Expand All @@ -110,25 +110,27 @@ export const cache = <H extends HookContext = HookContext>(
}
}

const cacheBefore = async (context: HookContext, cacheMap: ContextCacheMap) => {
const cacheBefore = async (
context: HookContext,
cacheMap: ContextCacheMap,
): Promise<void> => {
if (context.method === 'get' || context.method === 'find') {
const value = await cacheMap.get(context)
if (value) {
context.result = value
}
}

return context
}

const cacheAfter = async (context: HookContext, cacheMap: ContextCacheMap) => {
const cacheAfter = async (
context: HookContext,
cacheMap: ContextCacheMap,
): Promise<void> => {
if (context.method === 'get' || context.method === 'find') {
await cacheMap.set(context)
} else {
await cacheMap.clear(context)
}

return context
}

class ContextCacheMap {
Expand Down
56 changes: 54 additions & 2 deletions src/hooks/check-multi/check-multi.hook.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { vi } from 'vitest'
import type { HookContext } from '@feathersjs/feathers'
import { expectTypeOf, vi } from 'vitest'
import { feathers } from '@feathersjs/feathers'
import { MemoryService } from '@feathersjs/memory'
import type { AroundHookFunction, HookContext } from '@feathersjs/feathers'
import { checkMulti } from './check-multi.hook.js'
import { MethodNotAllowed } from '@feathersjs/errors'

Expand Down Expand Up @@ -241,4 +243,54 @@ describe('checkMulti', function () {
})
})
})

describe('integration with service.hooks({ around })', () => {
type User = { id: number; name: string }
type Services = { users: MemoryService<User> }
type App = ReturnType<typeof feathers<Services>>
type Ctx = HookContext<App, MemoryService<User>>

const setup = (multi: boolean) => {
const app = feathers<Services>()
app.use('users', new MemoryService<User>({ multi }))

app.service('users').hooks({
around: {
create: [checkMulti<Ctx>()],
},
})

return app
}

it('blocks multi-create when multi is not allowed', async () => {
const service = setup(false).service('users')

await expect(
service.create([{ name: 'Alice' }, { name: 'Bob' }]),
).rejects.toThrow(/Can not create multiple entries/)
})

it('allows single-create when multi is not allowed', async () => {
const service = setup(false).service('users')

const created = await service.create({ name: 'Alice' })
assert.equal(created.name, 'Alice')
})

it('allows multi-create when multi is allowed', async () => {
const service = setup(true).service('users')

const created = await service.create([{ name: 'Alice' }, { name: 'Bob' }])
assert.lengthOf(created, 2)
})

it('is type-compatible with AroundHookFunction', () => {
const hook = checkMulti<Ctx>()

expectTypeOf(hook).toExtend<
AroundHookFunction<App, MemoryService<User>>
>()
})
})
})
7 changes: 5 additions & 2 deletions src/hooks/check-multi/check-multi.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ export type CheckMultiOptions = {
export function checkMulti<H extends HookContext = HookContext>(
options?: CheckMultiOptions,
) {
return (context: H, next?: NextFunction) => {
function hook(context: H): void
function hook(context: H, next: NextFunction): Promise<void>
function hook(context: H, next?: NextFunction): void | Promise<void> {
const { service, method } = context
if (
!service.allowsMulti ||
Expand All @@ -40,11 +42,12 @@ export function checkMulti<H extends HookContext = HookContext>(
service.allowsMulti(method)
) {
if (next) return next()
return context
return
}

throw options?.error
? options.error(context)
: new MethodNotAllowed(`Can not ${method} multiple entries`)
}
return hook
}
60 changes: 58 additions & 2 deletions src/hooks/check-required/check-required.hook.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { assert } from 'vitest'
import { assert, expectTypeOf } from 'vitest'
import { checkRequired } from './check-required.hook.js'
import type { HookContext } from '@feathersjs/feathers'
import { feathers } from '@feathersjs/feathers'
import { MemoryService } from '@feathersjs/memory'
import type { AroundHookFunction, HookContext } from '@feathersjs/feathers'

let hookBefore: HookContext

Expand Down Expand Up @@ -38,4 +40,58 @@ describe('checkRequired', () => {
it('ignores bad or missing no dot path', () => {
assert.throws(() => checkRequired('xx')(hookBefore))
})

describe('integration with service.hooks({ around })', () => {
type User = { id: number; email: string; password: string }
type Services = { users: MemoryService<User> }
type App = ReturnType<typeof feathers<Services>>
type Ctx = HookContext<App, MemoryService<User>>

const setup = () => {
const app = feathers<Services>()
app.use('users', new MemoryService<User>())

app.service('users').hooks({
around: {
create: [checkRequired<Ctx>(['email', 'password'])],
},
})

return app
}

it('blocks create when a required field is missing', async () => {
const service = setup().service('users')

await expect(
service.create({ email: 'a@b.com' } as User),
).rejects.toThrow(/Field password does not exist/)
})

it('blocks create when a required field is null', async () => {
const service = setup().service('users')

await expect(
service.create({ email: 'a@b.com', password: '' } as User),
).rejects.toThrow(/Field password is null/)
})

it('allows create when all required fields are present', async () => {
const service = setup().service('users')

const created = await service.create({
email: 'a@b.com',
password: 'secret',
} as User)
assert.equal(created.email, 'a@b.com')
})

it('is type-compatible with AroundHookFunction', () => {
const hook = checkRequired<Ctx>(['email'])

expectTypeOf(hook).toExtend<
AroundHookFunction<App, MemoryService<User>>
>()
})
})
})
7 changes: 6 additions & 1 deletion src/hooks/check-required/check-required.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export function checkRequired<H extends HookContext = HookContext>(
fieldNames: MaybeArray<string>,
) {
const fieldNamesArray = toArray(fieldNames)
return (context: H, next?: NextFunction) => {
function hook(context: H): void
function hook(context: H, next: NextFunction): Promise<void>
function hook(context: H, next?: NextFunction): void | Promise<void> {
checkContext(context, {
type: ['before', 'around'],
method: ['create', 'update', 'patch'],
Expand Down Expand Up @@ -55,5 +57,8 @@ export function checkRequired<H extends HookContext = HookContext>(
}

if (next) return next()

return
}
return hook
}
21 changes: 20 additions & 1 deletion src/hooks/create-related/create-related.hook.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { expect } from 'vitest'
import { expect, expectTypeOf } from 'vitest'
import { feathers } from '@feathersjs/feathers'
import { MemoryService } from '@feathersjs/memory'
import type { AroundHookFunction, HookContext } from '@feathersjs/feathers'
import { createRelated } from './create-related.hook.js'

type MockAppOptions = {
Expand Down Expand Up @@ -90,7 +91,7 @@
data: (item) => {
const name: string = item.name
// @ts-expect-error - 'nonExistentProp' does not exist on User
const bad = item.nonExistentProp

Check warning on line 94 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 94 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 94 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 94 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
return [{ title: name, userId: item.id }]
},
}),
Expand Down Expand Up @@ -141,7 +142,7 @@
createRelated({
service: 'todos',
// @ts-expect-error - wrong data shape for todos service
data: (item) => [{ wrongField: 'test' }],

Check warning on line 145 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 145 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 145 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 145 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
}),
],
},
Expand All @@ -158,7 +159,7 @@
create: [
createRelated({
service: 'todos',
data: (item, context) => ({

Check warning on line 162 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 162 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 162 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 162 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
title: 'First issue',
userId: item.id,
}),
Expand All @@ -167,7 +168,7 @@
},
})

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

Check warning on line 171 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 171 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 171 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 171 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
name: 'John Doe',
})

Expand All @@ -193,7 +194,7 @@
},
})

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

Check warning on line 197 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 197 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 197 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 197 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
name: 'John Doe',
})

Expand All @@ -210,7 +211,7 @@
create: [
createRelated({
service: 'todos',
data: (item, context) => ({

Check warning on line 214 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 214 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 214 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 214 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
title: item.name,
userId: item.id,
}),
Expand All @@ -219,7 +220,7 @@
},
})

const users = await app

Check warning on line 223 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 223 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 223 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 223 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
.service('users')
.create([{ name: 'user1' }, { name: 'user2' }, { name: 'user3' }])

Expand All @@ -240,7 +241,7 @@
create: [
createRelated({
service: 'todos',
data: (item, context) => ({

Check warning on line 244 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 244 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 244 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 244 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
title: item.name,
userId: item.id,
}),
Expand All @@ -253,7 +254,7 @@
// @ts-expect-error - does not have options
expect(todosService.options.multi).toBe(false)

const users = await app

Check warning on line 257 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 257 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 257 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 257 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
.service('users')
.create([{ name: 'user1' }, { name: 'user2' }, { name: 'user3' }])

Expand All @@ -274,7 +275,7 @@
create: [
createRelated({
service: 'todos',
data: (item, context) => [

Check warning on line 278 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 278 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 278 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 278 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
{
title: 1,
userId: item.id,
Expand Down Expand Up @@ -402,4 +403,22 @@

expect(todos).toStrictEqual([])
})

it('is type-compatible with AroundHookFunction', () => {
type User = { id: number; name: string }
type Todo = { id: number; userId: number; title: string }
type Services = {
users: MemoryService<User>
todos: MemoryService<Todo>
}
type App = ReturnType<typeof feathers<Services>>
type Ctx = HookContext<App, MemoryService<User>>

expectTypeOf(
createRelated<Ctx>({
service: 'todos',
data: (user) => ({ userId: user.id, title: 'welcome' }),
}),
).toExtend<AroundHookFunction<App, MemoryService<User>>>()
})
})
4 changes: 2 additions & 2 deletions src/hooks/create-related/create-related.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export interface CreateRelatedOptions<
export function createRelated<H extends HookContext = HookContext>(
options: MaybeArray<CreateRelatedOptions<H>>,
) {
return async (context: H, next?: NextFunction) => {
return async (context: H, next?: NextFunction): Promise<void> => {
checkContext(context, {
type: ['after', 'around'],
method: ['create'],
Expand All @@ -80,7 +80,7 @@ export function createRelated<H extends HookContext = HookContext>(
.filter((x) => !!x)

if (!dataToCreate || dataToCreate.length <= 0) {
return context
return
}

if (multi || dataToCreate.length === 1) {
Expand Down
20 changes: 20 additions & 0 deletions src/hooks/debug/debug.hook.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import { expectTypeOf } from 'vitest'
import type {
feathers,
AroundHookFunction,
HookContext,
} from '@feathersjs/feathers'
import type { MemoryService } from '@feathersjs/memory'
import { debug } from './debug.hook.js'

describe('services debug', () => {
Expand All @@ -22,4 +29,17 @@ describe('services debug', () => {
}
debug('my message', 'query', 'foo')(hook)
})

describe('integration with service.hooks({ around })', () => {
type Item = { id: number; name: string }
type Services = { items: MemoryService<Item> }
type App = ReturnType<typeof feathers<Services>>
type Ctx = HookContext<App, MemoryService<Item>>

it('is type-compatible with AroundHookFunction', () => {
expectTypeOf(debug<Ctx>('msg')).toExtend<
AroundHookFunction<App, MemoryService<Item>>
>()
})
})
})
2 changes: 1 addition & 1 deletion src/hooks/debug/debug.hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { HookContext, NextFunction } from '@feathersjs/feathers'
*/
export const debug =
<H extends HookContext = HookContext>(msg: string, ...fieldNames: string[]) =>
async (context: H, next?: NextFunction) => {
async (context: H, next?: NextFunction): Promise<void> => {
if (next) {
await next()
}
Expand Down
Loading
Loading