diff --git a/packages/core/src/__mocks__/hook.ts b/packages/core/src/__mocks__/hook.ts new file mode 100644 index 000000000..75589eba6 --- /dev/null +++ b/packages/core/src/__mocks__/hook.ts @@ -0,0 +1,65 @@ +import { type Hook, HookEvent } from '@logto/schemas'; + +export const mockNanoIdForHook = 'random_string'; + +export const mockCreatedAtForHook = 1_650_969_000_000; + +export const mockTenantIdForHook = 'fake_tenant'; + +export const mockHook: Hook = { + tenantId: mockTenantIdForHook, + id: mockNanoIdForHook, + name: 'foo', + event: null, + events: [HookEvent.PostRegister], + config: { + url: 'https://example.com', + }, + signingKey: mockNanoIdForHook, + enabled: true, + createdAt: mockCreatedAtForHook, +}; + +const mockHookData1: Hook = { + tenantId: mockTenantIdForHook, + id: 'hook_id_1', + name: 'foo', + event: null, + events: [HookEvent.PostRegister], + config: { + url: 'https://example1.com', + }, + signingKey: mockNanoIdForHook, + enabled: true, + createdAt: mockCreatedAtForHook, +}; + +const mockHookData2: Hook = { + tenantId: mockTenantIdForHook, + id: 'hook_id_2', + name: 'bar', + event: null, + events: [HookEvent.PostResetPassword], + config: { + url: 'https://example2.com', + }, + signingKey: mockNanoIdForHook, + enabled: true, + createdAt: mockCreatedAtForHook, +}; + +const mockHookData3: Hook = { + tenantId: mockTenantIdForHook, + id: 'hook_id_3', + name: 'baz', + event: null, + events: [HookEvent.PostSignIn], + config: { + url: 'https://example3.com', + }, + signingKey: mockNanoIdForHook, + enabled: true, + createdAt: mockCreatedAtForHook, +}; + +export const mockHookList: Hook[] = [mockHookData1, mockHookData2, mockHookData3]; diff --git a/packages/core/src/libraries/hook.ts b/packages/core/src/libraries/hook.ts index 27d5d5248..cacbf4683 100644 --- a/packages/core/src/libraries/hook.ts +++ b/packages/core/src/libraries/hook.ts @@ -53,7 +53,10 @@ export const createHookLibrary = (queries: Queries) => { const hookEvent = eventToHook[event]; const found = await findAllHooks(); - const rows = found.filter(({ event }) => event === hookEvent); + const rows = found.filter( + ({ event, events, enabled }) => + enabled && (events.length > 0 ? events.includes(hookEvent) : event === hookEvent) // For backward compatibility + ); if (rows.length === 0) { return; @@ -90,7 +93,7 @@ export const createHookLibrary = (queries: Queries) => { .post(url, { headers: { 'user-agent': 'Logto (https://logto.io)', ...headers }, json, - retry: { limit: retries }, + retry: { limit: retries ?? 3 }, timeout: { request: 10_000 }, }) .then(async (response) => { diff --git a/packages/core/src/routes/hook.test.ts b/packages/core/src/routes/hook.test.ts new file mode 100644 index 000000000..416734126 --- /dev/null +++ b/packages/core/src/routes/hook.test.ts @@ -0,0 +1,236 @@ +import { + HookEvent, + type Hook, + type HookEvents, + type HookConfig, + type CreateHook, +} from '@logto/schemas'; +import { pickDefault } from '@logto/shared/esm'; + +import { + mockCreatedAtForHook, + mockHook, + mockHookList, + mockNanoIdForHook, + mockTenantIdForHook, +} from '#src/__mocks__/hook.js'; +import { MockTenant } from '#src/test-utils/tenant.js'; +import { createRequester } from '#src/utils/test-utils.js'; + +const { jest } = import.meta; + +const hooks = { + findAllHooks: async (): Promise => mockHookList, + insertHook: async (data: CreateHook): Promise => ({ + ...mockHook, + ...data, + }), + findHookById: async (id: string): Promise => { + const hook = mockHookList.find((hook) => hook.id === id); + if (!hook) { + throw new Error('Not found'); + } + return hook; + }, + updateHookById: async (id: string, data: Partial): Promise => { + const targetHook = mockHookList.find((hook) => hook.id === id) ?? mockHook; + return { + ...targetHook, + ...data, + }; + }, + deleteHookById: jest.fn(), +}; + +const tenantContext = new MockTenant(undefined, { hooks }); + +const hookRoutes = await pickDefault(import('./hook.js')); + +describe('hook routes', () => { + const hookRequest = createRequester({ authedRoutes: hookRoutes, tenantContext }); + + it('GET /hooks', async () => { + const response = await hookRequest.get('/hooks'); + expect(response.status).toEqual(200); + expect(response.body).toEqual(mockHookList); + expect(response.header).not.toHaveProperty('total-number'); + }); + + it('GET /hooks/:id', async () => { + const hookIdInMockList = mockHookList[0]?.id ?? ''; + const response = await hookRequest.get(`/hooks/${hookIdInMockList}`); + expect(response.status).toEqual(200); + expect(response.body.id).toBe(hookIdInMockList); + }); + + it('POST /hooks', async () => { + const name = 'fooName'; + const events: HookEvents = [HookEvent.PostRegister]; + const config: HookConfig = { + url: 'https://example.com', + }; + + const response = await hookRequest.post('/hooks').send({ name, events, config }); + expect(response.status).toEqual(201); + expect(response.body.id).toBeTruthy(); + expect(response.body.signingKey).toBeTruthy(); + + expect(response.body).toMatchObject({ + tenantId: mockTenantIdForHook, + name, + events, + config, + enabled: true, + createdAt: mockCreatedAtForHook, + }); + }); + + it('POST /hooks should be able to create a hook with multi events', async () => { + const name = 'anyName'; + const events: HookEvents = [HookEvent.PostSignIn, HookEvent.PostRegister]; + const config: HookConfig = { + url: 'https://example.com', + }; + + const response = await hookRequest.post('/hooks').send({ name, events, config }); + expect(response.status).toEqual(201); + expect(response.body.id).toBeTruthy(); + expect(response.body.signingKey).toBeTruthy(); + + expect(response.body).toMatchObject({ + tenantId: mockTenantIdForHook, + name, + events, + config, + enabled: true, + createdAt: mockCreatedAtForHook, + }); + }); + + it('POST /hooks should fail when no events are provided', async () => { + const payload: Partial = { + name: 'hook_name', + config: { + url: 'https://example.com', + }, + }; + await expect(hookRequest.post('/hooks').send(payload)).resolves.toHaveProperty('status', 400); + }); + + it('POST /hooks should success when create a hook with the old payload format', async () => { + const payload: Partial = { + event: HookEvent.PostRegister, + config: { + url: 'https://example.com', + retries: 2, + }, + }; + const response = await hookRequest.post('/hooks').send(payload); + expect(response.status).toEqual(201); + const generatedId = response.body.id as string; + + expect(response.body).toMatchObject({ + tenantId: mockTenantIdForHook, + id: generatedId, + event: HookEvent.PostRegister, + config: { + url: 'https://example.com', + retries: 2, + }, + }); + }); + + it('PATCH /hooks/:id', async () => { + const targetMockHook = mockHookList[0] ?? mockHook; + const name = 'newName'; + const events: HookEvents = [HookEvent.PostSignIn]; + const config: HookConfig = { + url: 'https://new.com', + }; + + const response = await hookRequest + .patch(`/hooks/${targetMockHook.id}`) + .send({ name, events, config, enabled: false }); + + expect(response.status).toEqual(200); + expect(response.body).toMatchObject({ + name, + events, + config, + enabled: false, + }); + }); + + it('PATCH /hooks/:id should success when update a hook with multi events', async () => { + const targetMockHook = mockHookList[0] ?? mockHook; + const events = [HookEvent.PostSignIn, HookEvent.PostResetPassword]; + const response = await hookRequest.patch(`/hooks/${targetMockHook.id}`).send({ events }); + + expect(response.status).toEqual(200); + expect(response.body).toMatchObject({ + events, + }); + }); + + it('PATCH /hooks/:id should success when update a hook with the old payload format', async () => { + const targetMockHook = mockHookList[0] ?? mockHook; + const event = HookEvent.PostSignIn; + const response = await hookRequest.patch(`/hooks/${targetMockHook.id}`).send({ + event, + config: { + url: 'https://example2.com', + retries: 1, + }, + }); + + expect(response.status).toEqual(200); + expect(response.body).toMatchObject({ + event, + config: { + url: 'https://example2.com', + retries: 1, + }, + }); + }); + + it('PATCH /hooks/:id with empty events list should fail', async () => { + const invalidEvents: HookEvent[] = []; + const response = await hookRequest + .patch(`/hooks/${mockNanoIdForHook}`) + .send({ events: invalidEvents }); + expect(response.status).toEqual(400); + }); + + it('PATCH /hooks/:id should not update signing key', async () => { + const targetMockHook = mockHookList[0] ?? mockHook; + const newSigningKey = `New-${targetMockHook.signingKey}`; + const response = await hookRequest + .patch(`/hooks/${targetMockHook.id}`) + .send({ signingKey: newSigningKey }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(targetMockHook); + expect(response.body.config.signingKey).not.toEqual(newSigningKey); + }); + + it('PATCH /hooks/:id/signing-key should update the singing key of a hook', async () => { + const targetMockHook = mockHookList[0] ?? mockHook; + const originalSigningKey = targetMockHook.signingKey; + const response = await hookRequest.patch(`/hooks/${targetMockHook.id}/signing-key`).send(); + expect(response.status).toEqual(200); + + const newSigningKey = response.body.signingKey as string; + expect(originalSigningKey).not.toEqual(newSigningKey); + expect(response.body).toEqual({ + ...targetMockHook, + signingKey: newSigningKey, + }); + }); + + it('DELETE /hooks/:id', async () => { + await expect(hookRequest.delete(`/hooks/${mockNanoIdForHook}`)).resolves.toHaveProperty( + 'status', + 204 + ); + }); +}); diff --git a/packages/core/src/routes/hook.ts b/packages/core/src/routes/hook.ts index 8dda6b59c..13d02d58b 100644 --- a/packages/core/src/routes/hook.ts +++ b/packages/core/src/routes/hook.ts @@ -1,11 +1,18 @@ -import { Hooks } from '@logto/schemas'; +import { Hooks, hookEventsGuard } from '@logto/schemas'; import { generateStandardId } from '@logto/shared'; +import { conditional, deduplicate } from '@silverhand/essentials'; import { z } from 'zod'; +import RequestError from '#src/errors/RequestError/index.js'; import koaGuard from '#src/middleware/koa-guard.js'; +import assertThat from '#src/utils/assert-that.js'; import type { AuthedRouter, RouterInitArgs } from './types.js'; +const nonemptyUniqueHookEventsGuard = hookEventsGuard + .nonempty() + .transform((events) => deduplicate(events)); + export default function hookRoutes( ...[router, { queries }]: RouterInitArgs ) { @@ -21,23 +28,10 @@ export default function hookRoutes( } ); - router.post( - '/hooks', - koaGuard({ body: Hooks.createGuard.omit({ id: true }), response: Hooks.guard, status: 200 }), - async (ctx, next) => { - ctx.body = await insertHook({ - id: generateStandardId(), - ...ctx.guard.body, - }); - - return next(); - } - ); - router.get( '/hooks/:id', koaGuard({ - params: z.object({ id: z.string().min(1) }), + params: z.object({ id: z.string() }), response: Hooks.guard, status: [200, 404], }), @@ -52,11 +46,44 @@ export default function hookRoutes( } ); + router.post( + '/hooks', + koaGuard({ + body: Hooks.createGuard.omit({ id: true, signingKey: true }).extend({ + events: nonemptyUniqueHookEventsGuard.optional(), + }), + response: Hooks.guard, + status: [201, 400], + }), + async (ctx, next) => { + const { event, events, enabled, ...rest } = ctx.guard.body; + assertThat(events ?? event, new RequestError({ code: 'hook.missing_events', status: 400 })); + + ctx.body = await insertHook({ + ...rest, + id: generateStandardId(), + signingKey: generateStandardId(), + events: events ?? [], + enabled: enabled ?? true, + ...conditional(event && { event }), + }); + + ctx.status = 201; + + return next(); + } + ); + router.patch( '/hooks/:id', koaGuard({ - params: z.object({ id: z.string().min(1) }), - body: Hooks.createGuard.omit({ id: true }).partial(), + params: z.object({ id: z.string() }), + body: Hooks.createGuard + .omit({ id: true, signingKey: true }) + .extend({ + events: nonemptyUniqueHookEventsGuard, + }) + .partial(), response: Hooks.guard, status: [200, 404], }), @@ -66,7 +93,27 @@ export default function hookRoutes( body, } = ctx.guard; - ctx.body = await updateHookById(id, body); + ctx.body = await updateHookById(id, body, 'replace'); + + return next(); + } + ); + + router.patch( + '/hooks/:id/signing-key', + koaGuard({ + params: z.object({ id: z.string() }), + response: Hooks.guard, + status: [200, 404], + }), + async (ctx, next) => { + const { + params: { id }, + } = ctx.guard; + + ctx.body = await updateHookById(id, { + signingKey: generateStandardId(), + }); return next(); } @@ -74,7 +121,7 @@ export default function hookRoutes( router.delete( '/hooks/:id', - koaGuard({ params: z.object({ id: z.string().min(1) }), status: [204, 404] }), + koaGuard({ params: z.object({ id: z.string() }), status: [204, 404] }), async (ctx, next) => { const { id } = ctx.guard.params; await deleteHookById(id); diff --git a/packages/core/src/utils/zod.ts b/packages/core/src/utils/zod.ts index f557e5569..8abae7aa5 100644 --- a/packages/core/src/utils/zod.ts +++ b/packages/core/src/utils/zod.ts @@ -252,12 +252,18 @@ export const zodTypeToSwagger = ( }; } - // TO-DO: Improve swagger output for zod schema with refinement (validate through JS functions) - if (config instanceof ZodEffects && config._def.effect.type === 'refinement') { - return { - type: 'object', - description: 'Validator function', - }; + if (config instanceof ZodEffects) { + if (config._def.effect.type === 'transform') { + return zodTypeToSwagger(config._def.schema); + } + + // TO-DO: Improve swagger output for zod schema with refinement (validate through JS functions) + if (config._def.effect.type === 'refinement') { + return { + type: 'object', + description: 'Validator function', + }; + } } throw new RequestError('swagger.invalid_zod_type', config); diff --git a/packages/integration-tests/src/tests/api/hooks.test.ts b/packages/integration-tests/src/tests/api/hooks.test.ts index 5e0981fbd..3ebca0e88 100644 --- a/packages/integration-tests/src/tests/api/hooks.test.ts +++ b/packages/integration-tests/src/tests/api/hooks.test.ts @@ -1,19 +1,24 @@ -import type { Hook, LogKey } from '@logto/schemas'; +import type { Hook, HookConfig, LogKey } from '@logto/schemas'; import { HookEvent, SignInIdentifier, LogResult, InteractionEvent } from '@logto/schemas'; import { authedAdminApi, deleteUser, getLogs, putInteraction } from '#src/api/index.js'; +import { createResponseWithCode } from '#src/helpers/admin-tenant.js'; import { initClient, processSession } from '#src/helpers/client.js'; import { createMockServer } from '#src/helpers/index.js'; import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js'; import { generateNewUser, generateNewUserProfile } from '#src/helpers/user.js'; import { waitFor } from '#src/utils.js'; -const createPayload = (event: HookEvent, url = 'not_work_url'): Partial => ({ - event, +type CreateHookPayload = Pick & { + config: HookConfig; +}; + +const createPayload = (event: HookEvent, url = 'not_work_url'): CreateHookPayload => ({ + name: 'hook_name', + events: [event], config: { url, headers: { foo: 'bar' }, - retries: 3, }, }); @@ -37,8 +42,33 @@ describe('hooks', () => { const payload = createPayload(HookEvent.PostRegister); const created = await authedAdminApi.post('hooks', { json: payload }).json(); - expect(payload.event).toEqual(created.event); - expect(payload.config).toEqual(created.config); + expect(created).toMatchObject(payload); + + expect(await authedAdminApi.get('hooks').json()).toContainEqual(created); + expect(await authedAdminApi.get(`hooks/${created.id}`).json()).toEqual(created); + expect( + await authedAdminApi + .patch(`hooks/${created.id}`, { json: { events: [HookEvent.PostSignIn] } }) + .json() + ).toMatchObject({ ...created, events: [HookEvent.PostSignIn] }); + expect(await authedAdminApi.delete(`hooks/${created.id}`)).toHaveProperty('statusCode', 204); + await expect(authedAdminApi.get(`hooks/${created.id}`)).rejects.toHaveProperty( + 'response.statusCode', + 404 + ); + }); + + it('should be able to create, query, update, and delete a hook by the original API', async () => { + const payload = { + event: HookEvent.PostRegister, + config: { + url: 'not_work_url', + retries: 2, + }, + }; + const created = await authedAdminApi.post('hooks', { json: payload }).json(); + + expect(created).toMatchObject(payload); expect(await authedAdminApi.get('hooks').json()).toContainEqual(created); expect(await authedAdminApi.get(`hooks/${created.id}`).json()).toEqual(created); @@ -46,7 +76,10 @@ describe('hooks', () => { await authedAdminApi .patch(`hooks/${created.id}`, { json: { event: HookEvent.PostSignIn } }) .json() - ).toEqual({ ...created, event: HookEvent.PostSignIn }); + ).toMatchObject({ + ...created, + event: HookEvent.PostSignIn, + }); expect(await authedAdminApi.delete(`hooks/${created.id}`)).toHaveProperty('statusCode', 204); await expect(authedAdminApi.get(`hooks/${created.id}`)).rejects.toHaveProperty( 'response.statusCode', @@ -54,6 +87,47 @@ describe('hooks', () => { ); }); + it('should throw error when creating a hook with an empty hook name', async () => { + const payload = { + name: '', + events: [HookEvent.PostRegister], + config: { + url: 'not_work_url', + }, + }; + await expect(authedAdminApi.post('hooks', { json: payload })).rejects.toMatchObject( + createResponseWithCode(400) + ); + }); + + it('should throw error when no event is provided when creating a hook', async () => { + const payload = { + name: 'hook_name', + config: { + url: 'not_work_url', + }, + }; + await expect(authedAdminApi.post('hooks', { json: payload })).rejects.toMatchObject( + createResponseWithCode(400) + ); + }); + + it('should throw error if update a hook with a invalid hook id', async () => { + const payload = { + name: 'new_hook_name', + }; + + await expect(authedAdminApi.patch('hooks/invalid_id', { json: payload })).rejects.toMatchObject( + createResponseWithCode(404) + ); + }); + + it('should throw error if regenerate a hook signing key with a invalid hook id', async () => { + await expect(authedAdminApi.patch('hooks/invalid_id/signing-key')).rejects.toMatchObject( + createResponseWithCode(404) + ); + }); + it('should trigger sign-in hook and record error when interaction finished', async () => { const createdHook = await authedAdminApi .post('hooks', { json: createPayload(HookEvent.PostSignIn) }) @@ -93,11 +167,20 @@ describe('hooks', () => { }); it('should trigger multiple register hooks and record properly when interaction finished', async () => { - const [hook1, hook2] = await Promise.all([ + const [hook1, hook2, hook3] = await Promise.all([ authedAdminApi.post('hooks', { json: createPayload(HookEvent.PostRegister) }).json(), authedAdminApi .post('hooks', { json: createPayload(HookEvent.PostRegister, 'http://localhost:9999') }) .json(), + // Using the old API to create a hook + authedAdminApi + .post('hooks', { + json: { + event: HookEvent.PostRegister, + config: { url: 'http://localhost:9999', retries: 2 }, + }, + }) + .json(), ]); const logKey: LogKey = 'TriggerHook.PostRegister'; @@ -128,11 +211,17 @@ describe('hooks', () => { ({ payload: { hookId, result } }) => hookId === hook2.id && result === LogResult.Success ) ).toBeTruthy(); + expect( + logs.some( + ({ payload: { hookId, result } }) => hookId === hook3.id && result === LogResult.Success + ) + ).toBeTruthy(); // Clean up await Promise.all([ authedAdminApi.delete(`hooks/${hook1.id}`), authedAdminApi.delete(`hooks/${hook2.id}`), + authedAdminApi.delete(`hooks/${hook3.id}`), ]); await deleteUser(id); }); diff --git a/packages/phrases/src/locales/de/errors/hook.ts b/packages/phrases/src/locales/de/errors/hook.ts new file mode 100644 index 000000000..f284c9e2f --- /dev/null +++ b/packages/phrases/src/locales/de/errors/hook.ts @@ -0,0 +1,5 @@ +const hook = { + missing_events: 'Sie müssen mindestens ein Ereignis angeben.', +}; + +export default hook; diff --git a/packages/phrases/src/locales/de/errors/index.ts b/packages/phrases/src/locales/de/errors/index.ts index 1d06bcccf..67928cc0b 100644 --- a/packages/phrases/src/locales/de/errors/index.ts +++ b/packages/phrases/src/locales/de/errors/index.ts @@ -2,6 +2,7 @@ import auth from './auth.js'; import connector from './connector.js'; import entity from './entity.js'; import guard from './guard.js'; +import hook from './hook.js'; import localization from './localization.js'; import log from './log.js'; import oidc from './oidc.js'; @@ -36,6 +37,7 @@ const errors = { scope, storage, resource, + hook, }; export default errors; diff --git a/packages/phrases/src/locales/en/errors/hook.ts b/packages/phrases/src/locales/en/errors/hook.ts new file mode 100644 index 000000000..99c52cf6f --- /dev/null +++ b/packages/phrases/src/locales/en/errors/hook.ts @@ -0,0 +1,5 @@ +const hook = { + missing_events: 'You need to provide at least one event.', +}; + +export default hook; diff --git a/packages/phrases/src/locales/en/errors/index.ts b/packages/phrases/src/locales/en/errors/index.ts index 1d06bcccf..67928cc0b 100644 --- a/packages/phrases/src/locales/en/errors/index.ts +++ b/packages/phrases/src/locales/en/errors/index.ts @@ -2,6 +2,7 @@ import auth from './auth.js'; import connector from './connector.js'; import entity from './entity.js'; import guard from './guard.js'; +import hook from './hook.js'; import localization from './localization.js'; import log from './log.js'; import oidc from './oidc.js'; @@ -36,6 +37,7 @@ const errors = { scope, storage, resource, + hook, }; export default errors; diff --git a/packages/phrases/src/locales/es/errors/hook.ts b/packages/phrases/src/locales/es/errors/hook.ts new file mode 100644 index 000000000..e64b4309b --- /dev/null +++ b/packages/phrases/src/locales/es/errors/hook.ts @@ -0,0 +1,5 @@ +const hook = { + missing_events: 'Necesita proporcionar al menos un evento.', +}; + +export default hook; diff --git a/packages/phrases/src/locales/es/errors/index.ts b/packages/phrases/src/locales/es/errors/index.ts index 1d06bcccf..67928cc0b 100644 --- a/packages/phrases/src/locales/es/errors/index.ts +++ b/packages/phrases/src/locales/es/errors/index.ts @@ -2,6 +2,7 @@ import auth from './auth.js'; import connector from './connector.js'; import entity from './entity.js'; import guard from './guard.js'; +import hook from './hook.js'; import localization from './localization.js'; import log from './log.js'; import oidc from './oidc.js'; @@ -36,6 +37,7 @@ const errors = { scope, storage, resource, + hook, }; export default errors; diff --git a/packages/phrases/src/locales/fr/errors/hook.ts b/packages/phrases/src/locales/fr/errors/hook.ts new file mode 100644 index 000000000..7619a3b99 --- /dev/null +++ b/packages/phrases/src/locales/fr/errors/hook.ts @@ -0,0 +1,5 @@ +const hook = { + missing_events: 'Vous devez fournir au moins un événement.', +}; + +export default hook; diff --git a/packages/phrases/src/locales/fr/errors/index.ts b/packages/phrases/src/locales/fr/errors/index.ts index 1d06bcccf..67928cc0b 100644 --- a/packages/phrases/src/locales/fr/errors/index.ts +++ b/packages/phrases/src/locales/fr/errors/index.ts @@ -2,6 +2,7 @@ import auth from './auth.js'; import connector from './connector.js'; import entity from './entity.js'; import guard from './guard.js'; +import hook from './hook.js'; import localization from './localization.js'; import log from './log.js'; import oidc from './oidc.js'; @@ -36,6 +37,7 @@ const errors = { scope, storage, resource, + hook, }; export default errors; diff --git a/packages/phrases/src/locales/it/errors/hook.ts b/packages/phrases/src/locales/it/errors/hook.ts new file mode 100644 index 000000000..c89b732d9 --- /dev/null +++ b/packages/phrases/src/locales/it/errors/hook.ts @@ -0,0 +1,5 @@ +const hook = { + missing_events: 'È necessario fornire almeno un evento.', +}; + +export default hook; diff --git a/packages/phrases/src/locales/it/errors/index.ts b/packages/phrases/src/locales/it/errors/index.ts index 1d06bcccf..67928cc0b 100644 --- a/packages/phrases/src/locales/it/errors/index.ts +++ b/packages/phrases/src/locales/it/errors/index.ts @@ -2,6 +2,7 @@ import auth from './auth.js'; import connector from './connector.js'; import entity from './entity.js'; import guard from './guard.js'; +import hook from './hook.js'; import localization from './localization.js'; import log from './log.js'; import oidc from './oidc.js'; @@ -36,6 +37,7 @@ const errors = { scope, storage, resource, + hook, }; export default errors; diff --git a/packages/phrases/src/locales/ja/errors/hook.ts b/packages/phrases/src/locales/ja/errors/hook.ts new file mode 100644 index 000000000..f789008f7 --- /dev/null +++ b/packages/phrases/src/locales/ja/errors/hook.ts @@ -0,0 +1,5 @@ +const hook = { + missing_events: '少なくとも1つのイベントを提供する必要があります。', +}; + +export default hook; diff --git a/packages/phrases/src/locales/ja/errors/index.ts b/packages/phrases/src/locales/ja/errors/index.ts index 1d06bcccf..67928cc0b 100644 --- a/packages/phrases/src/locales/ja/errors/index.ts +++ b/packages/phrases/src/locales/ja/errors/index.ts @@ -2,6 +2,7 @@ import auth from './auth.js'; import connector from './connector.js'; import entity from './entity.js'; import guard from './guard.js'; +import hook from './hook.js'; import localization from './localization.js'; import log from './log.js'; import oidc from './oidc.js'; @@ -36,6 +37,7 @@ const errors = { scope, storage, resource, + hook, }; export default errors; diff --git a/packages/phrases/src/locales/ko/errors/hook.ts b/packages/phrases/src/locales/ko/errors/hook.ts new file mode 100644 index 000000000..ae5e73011 --- /dev/null +++ b/packages/phrases/src/locales/ko/errors/hook.ts @@ -0,0 +1,5 @@ +const hook = { + missing_events: '최소한 하나의 이벤트를 제공해야 합니다.', +}; + +export default hook; diff --git a/packages/phrases/src/locales/ko/errors/index.ts b/packages/phrases/src/locales/ko/errors/index.ts index 1d06bcccf..67928cc0b 100644 --- a/packages/phrases/src/locales/ko/errors/index.ts +++ b/packages/phrases/src/locales/ko/errors/index.ts @@ -2,6 +2,7 @@ import auth from './auth.js'; import connector from './connector.js'; import entity from './entity.js'; import guard from './guard.js'; +import hook from './hook.js'; import localization from './localization.js'; import log from './log.js'; import oidc from './oidc.js'; @@ -36,6 +37,7 @@ const errors = { scope, storage, resource, + hook, }; export default errors; diff --git a/packages/phrases/src/locales/pl-pl/errors/hook.ts b/packages/phrases/src/locales/pl-pl/errors/hook.ts new file mode 100644 index 000000000..5b9e1efc8 --- /dev/null +++ b/packages/phrases/src/locales/pl-pl/errors/hook.ts @@ -0,0 +1,5 @@ +const hook = { + missing_events: 'Musisz podać przynajmniej jedno zdarzenie.', +}; + +export default hook; diff --git a/packages/phrases/src/locales/pl-pl/errors/index.ts b/packages/phrases/src/locales/pl-pl/errors/index.ts index 1d06bcccf..67928cc0b 100644 --- a/packages/phrases/src/locales/pl-pl/errors/index.ts +++ b/packages/phrases/src/locales/pl-pl/errors/index.ts @@ -2,6 +2,7 @@ import auth from './auth.js'; import connector from './connector.js'; import entity from './entity.js'; import guard from './guard.js'; +import hook from './hook.js'; import localization from './localization.js'; import log from './log.js'; import oidc from './oidc.js'; @@ -36,6 +37,7 @@ const errors = { scope, storage, resource, + hook, }; export default errors; diff --git a/packages/phrases/src/locales/pt-br/errors/hook.ts b/packages/phrases/src/locales/pt-br/errors/hook.ts new file mode 100644 index 000000000..09fabce04 --- /dev/null +++ b/packages/phrases/src/locales/pt-br/errors/hook.ts @@ -0,0 +1,5 @@ +const hook = { + missing_events: 'Você precisa fornecer pelo menos um evento.', +}; + +export default hook; diff --git a/packages/phrases/src/locales/pt-br/errors/index.ts b/packages/phrases/src/locales/pt-br/errors/index.ts index 1d06bcccf..67928cc0b 100644 --- a/packages/phrases/src/locales/pt-br/errors/index.ts +++ b/packages/phrases/src/locales/pt-br/errors/index.ts @@ -2,6 +2,7 @@ import auth from './auth.js'; import connector from './connector.js'; import entity from './entity.js'; import guard from './guard.js'; +import hook from './hook.js'; import localization from './localization.js'; import log from './log.js'; import oidc from './oidc.js'; @@ -36,6 +37,7 @@ const errors = { scope, storage, resource, + hook, }; export default errors; diff --git a/packages/phrases/src/locales/pt-pt/errors/hook.ts b/packages/phrases/src/locales/pt-pt/errors/hook.ts new file mode 100644 index 000000000..09fabce04 --- /dev/null +++ b/packages/phrases/src/locales/pt-pt/errors/hook.ts @@ -0,0 +1,5 @@ +const hook = { + missing_events: 'Você precisa fornecer pelo menos um evento.', +}; + +export default hook; diff --git a/packages/phrases/src/locales/pt-pt/errors/index.ts b/packages/phrases/src/locales/pt-pt/errors/index.ts index 1d06bcccf..67928cc0b 100644 --- a/packages/phrases/src/locales/pt-pt/errors/index.ts +++ b/packages/phrases/src/locales/pt-pt/errors/index.ts @@ -2,6 +2,7 @@ import auth from './auth.js'; import connector from './connector.js'; import entity from './entity.js'; import guard from './guard.js'; +import hook from './hook.js'; import localization from './localization.js'; import log from './log.js'; import oidc from './oidc.js'; @@ -36,6 +37,7 @@ const errors = { scope, storage, resource, + hook, }; export default errors; diff --git a/packages/phrases/src/locales/ru/errors/hook.ts b/packages/phrases/src/locales/ru/errors/hook.ts new file mode 100644 index 000000000..802fb1a76 --- /dev/null +++ b/packages/phrases/src/locales/ru/errors/hook.ts @@ -0,0 +1,5 @@ +const hook = { + missing_events: 'Вы должны предоставить как минимум одно событие.', +}; + +export default hook; diff --git a/packages/phrases/src/locales/ru/errors/index.ts b/packages/phrases/src/locales/ru/errors/index.ts index 1d06bcccf..67928cc0b 100644 --- a/packages/phrases/src/locales/ru/errors/index.ts +++ b/packages/phrases/src/locales/ru/errors/index.ts @@ -2,6 +2,7 @@ import auth from './auth.js'; import connector from './connector.js'; import entity from './entity.js'; import guard from './guard.js'; +import hook from './hook.js'; import localization from './localization.js'; import log from './log.js'; import oidc from './oidc.js'; @@ -36,6 +37,7 @@ const errors = { scope, storage, resource, + hook, }; export default errors; diff --git a/packages/phrases/src/locales/tr-tr/errors/hook.ts b/packages/phrases/src/locales/tr-tr/errors/hook.ts new file mode 100644 index 000000000..6b1512c0e --- /dev/null +++ b/packages/phrases/src/locales/tr-tr/errors/hook.ts @@ -0,0 +1,5 @@ +const hook = { + missing_events: 'En az bir etkinlik sağlamanız gerekiyor.', +}; + +export default hook; diff --git a/packages/phrases/src/locales/tr-tr/errors/index.ts b/packages/phrases/src/locales/tr-tr/errors/index.ts index 1d06bcccf..67928cc0b 100644 --- a/packages/phrases/src/locales/tr-tr/errors/index.ts +++ b/packages/phrases/src/locales/tr-tr/errors/index.ts @@ -2,6 +2,7 @@ import auth from './auth.js'; import connector from './connector.js'; import entity from './entity.js'; import guard from './guard.js'; +import hook from './hook.js'; import localization from './localization.js'; import log from './log.js'; import oidc from './oidc.js'; @@ -36,6 +37,7 @@ const errors = { scope, storage, resource, + hook, }; export default errors; diff --git a/packages/phrases/src/locales/zh-cn/errors/hook.ts b/packages/phrases/src/locales/zh-cn/errors/hook.ts new file mode 100644 index 000000000..f1e0f9df1 --- /dev/null +++ b/packages/phrases/src/locales/zh-cn/errors/hook.ts @@ -0,0 +1,5 @@ +const hook = { + missing_events: '你需要提供至少一个事件。', +}; + +export default hook; diff --git a/packages/phrases/src/locales/zh-cn/errors/index.ts b/packages/phrases/src/locales/zh-cn/errors/index.ts index 1d06bcccf..67928cc0b 100644 --- a/packages/phrases/src/locales/zh-cn/errors/index.ts +++ b/packages/phrases/src/locales/zh-cn/errors/index.ts @@ -2,6 +2,7 @@ import auth from './auth.js'; import connector from './connector.js'; import entity from './entity.js'; import guard from './guard.js'; +import hook from './hook.js'; import localization from './localization.js'; import log from './log.js'; import oidc from './oidc.js'; @@ -36,6 +37,7 @@ const errors = { scope, storage, resource, + hook, }; export default errors; diff --git a/packages/phrases/src/locales/zh-hk/errors/hook.ts b/packages/phrases/src/locales/zh-hk/errors/hook.ts new file mode 100644 index 000000000..1f1232e53 --- /dev/null +++ b/packages/phrases/src/locales/zh-hk/errors/hook.ts @@ -0,0 +1,5 @@ +const hook = { + missing_events: '你需要至少提供一個事件。', +}; + +export default hook; diff --git a/packages/phrases/src/locales/zh-hk/errors/index.ts b/packages/phrases/src/locales/zh-hk/errors/index.ts index 1d06bcccf..67928cc0b 100644 --- a/packages/phrases/src/locales/zh-hk/errors/index.ts +++ b/packages/phrases/src/locales/zh-hk/errors/index.ts @@ -2,6 +2,7 @@ import auth from './auth.js'; import connector from './connector.js'; import entity from './entity.js'; import guard from './guard.js'; +import hook from './hook.js'; import localization from './localization.js'; import log from './log.js'; import oidc from './oidc.js'; @@ -36,6 +37,7 @@ const errors = { scope, storage, resource, + hook, }; export default errors; diff --git a/packages/phrases/src/locales/zh-tw/errors/hook.ts b/packages/phrases/src/locales/zh-tw/errors/hook.ts new file mode 100644 index 000000000..1f1232e53 --- /dev/null +++ b/packages/phrases/src/locales/zh-tw/errors/hook.ts @@ -0,0 +1,5 @@ +const hook = { + missing_events: '你需要至少提供一個事件。', +}; + +export default hook; diff --git a/packages/phrases/src/locales/zh-tw/errors/index.ts b/packages/phrases/src/locales/zh-tw/errors/index.ts index 1d06bcccf..67928cc0b 100644 --- a/packages/phrases/src/locales/zh-tw/errors/index.ts +++ b/packages/phrases/src/locales/zh-tw/errors/index.ts @@ -2,6 +2,7 @@ import auth from './auth.js'; import connector from './connector.js'; import entity from './entity.js'; import guard from './guard.js'; +import hook from './hook.js'; import localization from './localization.js'; import log from './log.js'; import oidc from './oidc.js'; @@ -36,6 +37,7 @@ const errors = { scope, storage, resource, + hook, }; export default errors; diff --git a/packages/schemas/src/foundations/jsonb-types.ts b/packages/schemas/src/foundations/jsonb-types.ts index 041fab325..b8d1e8300 100644 --- a/packages/schemas/src/foundations/jsonb-types.ts +++ b/packages/schemas/src/foundations/jsonb-types.ts @@ -217,7 +217,7 @@ export const hookConfigGuard = z.object({ * Now the retry times is fixed to 3. * Keep for backward compatibility. */ - retries: z.number().gte(0).lte(3), + retries: z.number().gte(0).lte(3).optional(), }); export type HookConfig = z.infer; diff --git a/packages/schemas/src/types/hook.ts b/packages/schemas/src/types/hook.ts index 5ba79d284..2cec796a1 100644 --- a/packages/schemas/src/types/hook.ts +++ b/packages/schemas/src/types/hook.ts @@ -1,5 +1,5 @@ -import type { Application, User } from '../db-entries/index.js'; -import type { HookEvent } from '../foundations/index.js'; +import { type Application, type User } from '../db-entries/index.js'; +import { type HookEvent } from '../foundations/index.js'; import type { userInfoSelectFields } from './user.js';