0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

refactor: update webhook API (#3819)

This commit is contained in:
Xiao Yijun 2023-05-19 16:48:05 +08:00 committed by GitHub
parent a265f1f48e
commit 9423b273b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 589 additions and 38 deletions

View file

@ -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];

View file

@ -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) => {

View file

@ -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<Hook[]> => mockHookList,
insertHook: async (data: CreateHook): Promise<Hook> => ({
...mockHook,
...data,
}),
findHookById: async (id: string): Promise<Hook> => {
const hook = mockHookList.find((hook) => hook.id === id);
if (!hook) {
throw new Error('Not found');
}
return hook;
},
updateHookById: async (id: string, data: Partial<CreateHook>): Promise<Hook> => {
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<Hook> = {
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<Hook> = {
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
);
});
});

View file

@ -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<T extends AuthedRouter>(
...[router, { queries }]: RouterInitArgs<T>
) {
@ -21,23 +28,10 @@ export default function hookRoutes<T extends AuthedRouter>(
}
);
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<T extends AuthedRouter>(
}
);
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<T extends AuthedRouter>(
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<T extends AuthedRouter>(
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);

View file

@ -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);

View file

@ -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<Hook> => ({
event,
type CreateHookPayload = Pick<Hook, 'name' | 'events'> & {
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<Hook>();
expect(payload.event).toEqual(created.event);
expect(payload.config).toEqual(created.config);
expect(created).toMatchObject(payload);
expect(await authedAdminApi.get('hooks').json<Hook[]>()).toContainEqual(created);
expect(await authedAdminApi.get(`hooks/${created.id}`).json<Hook>()).toEqual(created);
expect(
await authedAdminApi
.patch(`hooks/${created.id}`, { json: { events: [HookEvent.PostSignIn] } })
.json<Hook>()
).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<Hook>();
expect(created).toMatchObject(payload);
expect(await authedAdminApi.get('hooks').json<Hook[]>()).toContainEqual(created);
expect(await authedAdminApi.get(`hooks/${created.id}`).json<Hook>()).toEqual(created);
@ -46,7 +76,10 @@ describe('hooks', () => {
await authedAdminApi
.patch(`hooks/${created.id}`, { json: { event: HookEvent.PostSignIn } })
.json<Hook>()
).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<Hook>(),
authedAdminApi
.post('hooks', { json: createPayload(HookEvent.PostRegister, 'http://localhost:9999') })
.json<Hook>(),
// Using the old API to create a hook
authedAdminApi
.post('hooks', {
json: {
event: HookEvent.PostRegister,
config: { url: 'http://localhost:9999', retries: 2 },
},
})
.json<Hook>(),
]);
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);
});

View file

@ -0,0 +1,5 @@
const hook = {
missing_events: 'Sie müssen mindestens ein Ereignis angeben.',
};
export default hook;

View file

@ -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;

View file

@ -0,0 +1,5 @@
const hook = {
missing_events: 'You need to provide at least one event.',
};
export default hook;

View file

@ -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;

View file

@ -0,0 +1,5 @@
const hook = {
missing_events: 'Necesita proporcionar al menos un evento.',
};
export default hook;

View file

@ -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;

View file

@ -0,0 +1,5 @@
const hook = {
missing_events: 'Vous devez fournir au moins un événement.',
};
export default hook;

View file

@ -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;

View file

@ -0,0 +1,5 @@
const hook = {
missing_events: 'È necessario fornire almeno un evento.',
};
export default hook;

View file

@ -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;

View file

@ -0,0 +1,5 @@
const hook = {
missing_events: '少なくとも1つのイベントを提供する必要があります。',
};
export default hook;

View file

@ -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;

View file

@ -0,0 +1,5 @@
const hook = {
missing_events: '최소한 하나의 이벤트를 제공해야 합니다.',
};
export default hook;

View file

@ -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;

View file

@ -0,0 +1,5 @@
const hook = {
missing_events: 'Musisz podać przynajmniej jedno zdarzenie.',
};
export default hook;

View file

@ -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;

View file

@ -0,0 +1,5 @@
const hook = {
missing_events: 'Você precisa fornecer pelo menos um evento.',
};
export default hook;

View file

@ -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;

View file

@ -0,0 +1,5 @@
const hook = {
missing_events: 'Você precisa fornecer pelo menos um evento.',
};
export default hook;

View file

@ -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;

View file

@ -0,0 +1,5 @@
const hook = {
missing_events: 'Вы должны предоставить как минимум одно событие.',
};
export default hook;

View file

@ -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;

View file

@ -0,0 +1,5 @@
const hook = {
missing_events: 'En az bir etkinlik sağlamanız gerekiyor.',
};
export default hook;

View file

@ -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;

View file

@ -0,0 +1,5 @@
const hook = {
missing_events: '你需要提供至少一个事件。',
};
export default hook;

View file

@ -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;

View file

@ -0,0 +1,5 @@
const hook = {
missing_events: '你需要至少提供一個事件。',
};
export default hook;

View file

@ -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;

View file

@ -0,0 +1,5 @@
const hook = {
missing_events: '你需要至少提供一個事件。',
};
export default hook;

View file

@ -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;

View file

@ -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<typeof hookConfigGuard>;

View file

@ -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';