@shipfox/api-triggers 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +224 -0
- package/LICENSE +21 -0
- package/README.md +300 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +42 -0
- package/dist/config.js.map +1 -0
- package/dist/core/compute-next-fire-at.d.ts +9 -0
- package/dist/core/compute-next-fire-at.d.ts.map +1 -0
- package/dist/core/compute-next-fire-at.js +28 -0
- package/dist/core/compute-next-fire-at.js.map +1 -0
- package/dist/core/config.d.ts +26 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/config.js +68 -0
- package/dist/core/config.js.map +1 -0
- package/dist/core/dispatch-integration-event.d.ts +14 -0
- package/dist/core/dispatch-integration-event.d.ts.map +1 -0
- package/dist/core/dispatch-integration-event.js +136 -0
- package/dist/core/dispatch-integration-event.js.map +1 -0
- package/dist/core/drain-cron-schedules.d.ts +30 -0
- package/dist/core/drain-cron-schedules.d.ts.map +1 -0
- package/dist/core/drain-cron-schedules.js +76 -0
- package/dist/core/drain-cron-schedules.js.map +1 -0
- package/dist/core/entities/cron-schedule.d.ts +11 -0
- package/dist/core/entities/cron-schedule.d.ts.map +1 -0
- package/dist/core/entities/cron-schedule.js +3 -0
- package/dist/core/entities/cron-schedule.js.map +1 -0
- package/dist/core/entities/decision.d.ts +23 -0
- package/dist/core/entities/decision.d.ts.map +1 -0
- package/dist/core/entities/decision.js +7 -0
- package/dist/core/entities/decision.js.map +1 -0
- package/dist/core/entities/index.d.ts +3 -0
- package/dist/core/entities/index.d.ts.map +1 -0
- package/dist/core/entities/index.js +3 -0
- package/dist/core/entities/index.js.map +1 -0
- package/dist/core/entities/job-listener-subscription.d.ts +14 -0
- package/dist/core/entities/job-listener-subscription.d.ts.map +1 -0
- package/dist/core/entities/job-listener-subscription.js +3 -0
- package/dist/core/entities/job-listener-subscription.js.map +1 -0
- package/dist/core/entities/received-event.d.ts +28 -0
- package/dist/core/entities/received-event.d.ts.map +1 -0
- package/dist/core/entities/received-event.js +14 -0
- package/dist/core/entities/received-event.js.map +1 -0
- package/dist/core/entities/subscription.d.ts +13 -0
- package/dist/core/entities/subscription.d.ts.map +1 -0
- package/dist/core/entities/subscription.js +3 -0
- package/dist/core/entities/subscription.js.map +1 -0
- package/dist/core/errors.d.ts +25 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +41 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/fire-cron.d.ts +28 -0
- package/dist/core/fire-cron.d.ts.map +1 -0
- package/dist/core/fire-cron.js +85 -0
- package/dist/core/fire-cron.js.map +1 -0
- package/dist/core/fire-manual.d.ts +9 -0
- package/dist/core/fire-manual.d.ts.map +1 -0
- package/dist/core/fire-manual.js +87 -0
- package/dist/core/fire-manual.js.map +1 -0
- package/dist/core/index.d.ts +11 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +10 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/record-trigger-history.d.ts +35 -0
- package/dist/core/record-trigger-history.d.ts.map +1 -0
- package/dist/core/record-trigger-history.js +75 -0
- package/dist/core/record-trigger-history.js.map +1 -0
- package/dist/core/route-event-to-job-listeners.d.ts +31 -0
- package/dist/core/route-event-to-job-listeners.d.ts.map +1 -0
- package/dist/core/route-event-to-job-listeners.js +140 -0
- package/dist/core/route-event-to-job-listeners.js.map +1 -0
- package/dist/db/cron-schedules.d.ts +36 -0
- package/dist/db/cron-schedules.d.ts.map +1 -0
- package/dist/db/cron-schedules.js +104 -0
- package/dist/db/cron-schedules.js.map +1 -0
- package/dist/db/db.d.ts +2515 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +28 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/event-history.d.ts +50 -0
- package/dist/db/event-history.d.ts.map +1 -0
- package/dist/db/event-history.js +207 -0
- package/dist/db/event-history.js.map +1 -0
- package/dist/db/event-queries.d.ts +38 -0
- package/dist/db/event-queries.d.ts.map +1 -0
- package/dist/db/event-queries.js +78 -0
- package/dist/db/event-queries.js.map +1 -0
- package/dist/db/index.d.ts +11 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +14 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/job-listener-subscriptions.d.ts +24 -0
- package/dist/db/job-listener-subscriptions.d.ts.map +1 -0
- package/dist/db/job-listener-subscriptions.js +69 -0
- package/dist/db/job-listener-subscriptions.js.map +1 -0
- package/dist/db/received-events.d.ts +7 -0
- package/dist/db/received-events.d.ts.map +1 -0
- package/dist/db/received-events.js +11 -0
- package/dist/db/received-events.js.map +1 -0
- package/dist/db/schema/common.d.ts +2 -0
- package/dist/db/schema/common.d.ts.map +1 -0
- package/dist/db/schema/common.js +4 -0
- package/dist/db/schema/common.js.map +1 -0
- package/dist/db/schema/cron-schedules.d.ts +148 -0
- package/dist/db/schema/cron-schedules.d.ts.map +1 -0
- package/dist/db/schema/cron-schedules.js +39 -0
- package/dist/db/schema/cron-schedules.js.map +1 -0
- package/dist/db/schema/decisions.d.ts +284 -0
- package/dist/db/schema/decisions.d.ts.map +1 -0
- package/dist/db/schema/decisions.js +70 -0
- package/dist/db/schema/decisions.js.map +1 -0
- package/dist/db/schema/job-listener-subscriptions.d.ts +185 -0
- package/dist/db/schema/job-listener-subscriptions.d.ts.map +1 -0
- package/dist/db/schema/job-listener-subscriptions.js +41 -0
- package/dist/db/schema/job-listener-subscriptions.js.map +1 -0
- package/dist/db/schema/outbox.d.ts +195 -0
- package/dist/db/schema/outbox.d.ts.map +1 -0
- package/dist/db/schema/outbox.js +5 -0
- package/dist/db/schema/outbox.js.map +1 -0
- package/dist/db/schema/received-events.d.ts +544 -0
- package/dist/db/schema/received-events.d.ts.map +1 -0
- package/dist/db/schema/received-events.js +86 -0
- package/dist/db/schema/received-events.js.map +1 -0
- package/dist/db/schema/subscriptions.d.ts +182 -0
- package/dist/db/schema/subscriptions.d.ts.map +1 -0
- package/dist/db/schema/subscriptions.js +39 -0
- package/dist/db/schema/subscriptions.js.map +1 -0
- package/dist/db/subscriptions.d.ts +26 -0
- package/dist/db/subscriptions.d.ts.map +1 -0
- package/dist/db/subscriptions.js +103 -0
- package/dist/db/subscriptions.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +68 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/index.d.ts +3 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +4 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/instance.d.ts +15 -0
- package/dist/metrics/instance.d.ts.map +1 -0
- package/dist/metrics/instance.js +32 -0
- package/dist/metrics/instance.js.map +1 -0
- package/dist/metrics/service.d.ts +2 -0
- package/dist/metrics/service.d.ts.map +1 -0
- package/dist/metrics/service.js +15 -0
- package/dist/metrics/service.js.map +1 -0
- package/dist/presentation/dto/trigger-events.d.ts +7 -0
- package/dist/presentation/dto/trigger-events.d.ts.map +1 -0
- package/dist/presentation/dto/trigger-events.js +47 -0
- package/dist/presentation/dto/trigger-events.js.map +1 -0
- package/dist/presentation/index.d.ts +3 -0
- package/dist/presentation/index.d.ts.map +1 -0
- package/dist/presentation/index.js +4 -0
- package/dist/presentation/index.js.map +1 -0
- package/dist/presentation/routes/fire-manual.d.ts +2 -0
- package/dist/presentation/routes/fire-manual.d.ts.map +1 -0
- package/dist/presentation/routes/fire-manual.js +71 -0
- package/dist/presentation/routes/fire-manual.js.map +1 -0
- package/dist/presentation/routes/get-trigger-event.d.ts +2 -0
- package/dist/presentation/routes/get-trigger-event.d.ts.map +1 -0
- package/dist/presentation/routes/get-trigger-event.js +37 -0
- package/dist/presentation/routes/get-trigger-event.js.map +1 -0
- package/dist/presentation/routes/index.d.ts +3 -0
- package/dist/presentation/routes/index.d.ts.map +1 -0
- package/dist/presentation/routes/index.js +27 -0
- package/dist/presentation/routes/index.js.map +1 -0
- package/dist/presentation/routes/list-trigger-event-facets.d.ts +2 -0
- package/dist/presentation/routes/list-trigger-event-facets.d.ts.map +1 -0
- package/dist/presentation/routes/list-trigger-event-facets.js +29 -0
- package/dist/presentation/routes/list-trigger-event-facets.js.map +1 -0
- package/dist/presentation/routes/list-trigger-events.d.ts +2 -0
- package/dist/presentation/routes/list-trigger-events.d.ts.map +1 -0
- package/dist/presentation/routes/list-trigger-events.js +56 -0
- package/dist/presentation/routes/list-trigger-events.js.map +1 -0
- package/dist/presentation/subscribers/index.d.ts +6 -0
- package/dist/presentation/subscribers/index.d.ts.map +1 -0
- package/dist/presentation/subscribers/index.js +7 -0
- package/dist/presentation/subscribers/index.js.map +1 -0
- package/dist/presentation/subscribers/on-definition-deleted.d.ts +3 -0
- package/dist/presentation/subscribers/on-definition-deleted.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-definition-deleted.js +8 -0
- package/dist/presentation/subscribers/on-definition-deleted.js.map +1 -0
- package/dist/presentation/subscribers/on-definition-resolved.d.ts +3 -0
- package/dist/presentation/subscribers/on-definition-resolved.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-definition-resolved.js +11 -0
- package/dist/presentation/subscribers/on-definition-resolved.js.map +1 -0
- package/dist/presentation/subscribers/on-integration-event-received.d.ts +4 -0
- package/dist/presentation/subscribers/on-integration-event-received.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-integration-event-received.js +17 -0
- package/dist/presentation/subscribers/on-integration-event-received.js.map +1 -0
- package/dist/presentation/subscribers/on-job-activated.d.ts +3 -0
- package/dist/presentation/subscribers/on-job-activated.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-job-activated.js +13 -0
- package/dist/presentation/subscribers/on-job-activated.js.map +1 -0
- package/dist/presentation/subscribers/on-job-terminated.d.ts +3 -0
- package/dist/presentation/subscribers/on-job-terminated.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-job-terminated.js +6 -0
- package/dist/presentation/subscribers/on-job-terminated.js.map +1 -0
- package/dist/temporal/activities/drain-cron-batch.d.ts +4 -0
- package/dist/temporal/activities/drain-cron-batch.d.ts.map +1 -0
- package/dist/temporal/activities/drain-cron-batch.js +20 -0
- package/dist/temporal/activities/drain-cron-batch.js.map +1 -0
- package/dist/temporal/activities/index.d.ts +10 -0
- package/dist/temporal/activities/index.d.ts.map +1 -0
- package/dist/temporal/activities/index.js +15 -0
- package/dist/temporal/activities/index.js.map +1 -0
- package/dist/temporal/activities/prune-trigger-events.d.ts +4 -0
- package/dist/temporal/activities/prune-trigger-events.d.ts.map +1 -0
- package/dist/temporal/activities/prune-trigger-events.js +11 -0
- package/dist/temporal/activities/prune-trigger-events.js.map +1 -0
- package/dist/temporal/constants.d.ts +7 -0
- package/dist/temporal/constants.d.ts.map +1 -0
- package/dist/temporal/constants.js +7 -0
- package/dist/temporal/constants.js.map +1 -0
- package/dist/temporal/workflows/cron-tick-cron.d.ts +9 -0
- package/dist/temporal/workflows/cron-tick-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/cron-tick-cron.js +54 -0
- package/dist/temporal/workflows/cron-tick-cron.js.map +1 -0
- package/dist/temporal/workflows/index.d.ts +3 -0
- package/dist/temporal/workflows/index.d.ts.map +1 -0
- package/dist/temporal/workflows/index.js +4 -0
- package/dist/temporal/workflows/index.js.map +1 -0
- package/dist/temporal/workflows/prune-trigger-events-cron.d.ts +2 -0
- package/dist/temporal/workflows/prune-trigger-events-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/prune-trigger-events-cron.js +18 -0
- package/dist/temporal/workflows/prune-trigger-events-cron.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/drizzle/0000_initial.sql +109 -0
- package/drizzle/meta/0000_snapshot.json +905 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +14 -0
- package/package.json +83 -0
- package/src/config.test.ts +33 -0
- package/src/config.ts +53 -0
- package/src/core/compute-next-fire-at.test.ts +69 -0
- package/src/core/compute-next-fire-at.ts +37 -0
- package/src/core/config.test.ts +213 -0
- package/src/core/config.ts +86 -0
- package/src/core/dispatch-integration-event.history-failure.test.ts +66 -0
- package/src/core/dispatch-integration-event.test.ts +1006 -0
- package/src/core/dispatch-integration-event.ts +150 -0
- package/src/core/drain-cron-schedules.test.ts +265 -0
- package/src/core/drain-cron-schedules.ts +90 -0
- package/src/core/entities/cron-schedule.ts +10 -0
- package/src/core/entities/decision.ts +24 -0
- package/src/core/entities/index.ts +5 -0
- package/src/core/entities/job-listener-subscription.ts +14 -0
- package/src/core/entities/received-event.ts +36 -0
- package/src/core/entities/subscription.ts +12 -0
- package/src/core/errors.ts +63 -0
- package/src/core/fire-cron.test.ts +146 -0
- package/src/core/fire-cron.ts +98 -0
- package/src/core/fire-manual.test.ts +157 -0
- package/src/core/fire-manual.ts +92 -0
- package/src/core/index.ts +34 -0
- package/src/core/record-trigger-history.test.ts +159 -0
- package/src/core/record-trigger-history.ts +145 -0
- package/src/core/route-event-to-job-listeners.test.ts +369 -0
- package/src/core/route-event-to-job-listeners.ts +184 -0
- package/src/db/cron-schedules.test.ts +358 -0
- package/src/db/cron-schedules.ts +163 -0
- package/src/db/db.ts +31 -0
- package/src/db/event-history.test.ts +421 -0
- package/src/db/event-history.ts +279 -0
- package/src/db/event-queries.ts +140 -0
- package/src/db/index.ts +51 -0
- package/src/db/job-listener-subscriptions.test.ts +270 -0
- package/src/db/job-listener-subscriptions.ts +133 -0
- package/src/db/received-events.ts +16 -0
- package/src/db/schema/common.ts +3 -0
- package/src/db/schema/cron-schedules.ts +37 -0
- package/src/db/schema/decisions.test.ts +279 -0
- package/src/db/schema/decisions.ts +77 -0
- package/src/db/schema/job-listener-subscriptions.ts +65 -0
- package/src/db/schema/outbox.ts +4 -0
- package/src/db/schema/received-events.test.ts +135 -0
- package/src/db/schema/received-events.ts +93 -0
- package/src/db/schema/subscriptions.ts +46 -0
- package/src/db/subscriptions.test.ts +340 -0
- package/src/db/subscriptions.ts +179 -0
- package/src/index.ts +108 -0
- package/src/metrics/index.ts +8 -0
- package/src/metrics/instance.ts +33 -0
- package/src/metrics/service.ts +18 -0
- package/src/presentation/dto/trigger-events.test.ts +144 -0
- package/src/presentation/dto/trigger-events.ts +60 -0
- package/src/presentation/index.ts +6 -0
- package/src/presentation/routes/fire-manual.test.ts +118 -0
- package/src/presentation/routes/fire-manual.ts +70 -0
- package/src/presentation/routes/get-trigger-event.test.ts +184 -0
- package/src/presentation/routes/get-trigger-event.ts +37 -0
- package/src/presentation/routes/index.ts +21 -0
- package/src/presentation/routes/list-trigger-event-facets.test.ts +116 -0
- package/src/presentation/routes/list-trigger-event-facets.ts +29 -0
- package/src/presentation/routes/list-trigger-events.test.ts +379 -0
- package/src/presentation/routes/list-trigger-events.ts +68 -0
- package/src/presentation/subscribers/index.ts +5 -0
- package/src/presentation/subscribers/on-definition-deleted.test.ts +42 -0
- package/src/presentation/subscribers/on-definition-deleted.ts +6 -0
- package/src/presentation/subscribers/on-definition-resolved.test.ts +67 -0
- package/src/presentation/subscribers/on-definition-resolved.ts +11 -0
- package/src/presentation/subscribers/on-integration-event-received.test.ts +53 -0
- package/src/presentation/subscribers/on-integration-event-received.ts +21 -0
- package/src/presentation/subscribers/on-job-activated.ts +14 -0
- package/src/presentation/subscribers/on-job-terminated.ts +6 -0
- package/src/temporal/activities/drain-cron-batch.ts +20 -0
- package/src/temporal/activities/index.ts +15 -0
- package/src/temporal/activities/prune-trigger-events.test.ts +78 -0
- package/src/temporal/activities/prune-trigger-events.ts +9 -0
- package/src/temporal/constants.ts +7 -0
- package/src/temporal/workflows/cron-tick-cron.ts +56 -0
- package/src/temporal/workflows/index.ts +2 -0
- package/src/temporal/workflows/prune-trigger-events-cron.ts +17 -0
- package/test/env.ts +8 -0
- package/test/factories/cron-schedule.ts +34 -0
- package/test/factories/decision.ts +49 -0
- package/test/factories/job-listener-subscription.ts +42 -0
- package/test/factories/received-event.ts +49 -0
- package/test/factories/subscription.ts +38 -0
- package/test/globalSetup.ts +20 -0
- package/test/index.ts +5 -0
- package/test/setup.ts +17 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {AUTH_USER} from '@shipfox/api-auth-context';
|
|
2
|
+
import type {RouteGroup} from '@shipfox/node-fastify';
|
|
3
|
+
import {fireManualTriggerRoute} from './fire-manual.js';
|
|
4
|
+
import {getTriggerEventRoute} from './get-trigger-event.js';
|
|
5
|
+
import {listTriggerEventFacetsRoute} from './list-trigger-event-facets.js';
|
|
6
|
+
import {listTriggerEventsRoute} from './list-trigger-events.js';
|
|
7
|
+
|
|
8
|
+
export const triggerRoutes: RouteGroup[] = [
|
|
9
|
+
{
|
|
10
|
+
prefix: '/workflow-definitions',
|
|
11
|
+
auth: AUTH_USER,
|
|
12
|
+
routes: [fireManualTriggerRoute],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
// The static /facets path is matched ahead of the /:id detail route by Fastify's
|
|
16
|
+
// radix router, so registration order does not affect resolution.
|
|
17
|
+
prefix: '/trigger-events',
|
|
18
|
+
auth: AUTH_USER,
|
|
19
|
+
routes: [listTriggerEventFacetsRoute, listTriggerEventsRoute, getTriggerEventRoute],
|
|
20
|
+
},
|
|
21
|
+
];
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import {buildUserContext, setUserContext} from '@shipfox/api-auth-context';
|
|
2
|
+
import type {TriggerEventFacetItemDto} from '@shipfox/api-triggers-dto';
|
|
3
|
+
import type {FastifyInstance} from 'fastify';
|
|
4
|
+
import Fastify from 'fastify';
|
|
5
|
+
import {serializerCompiler, validatorCompiler} from 'fastify-type-provider-zod';
|
|
6
|
+
import {receivedEventFactory} from '#test/index.js';
|
|
7
|
+
import {listTriggerEventFacetsRoute} from './list-trigger-event-facets.js';
|
|
8
|
+
|
|
9
|
+
const facets = (res: {
|
|
10
|
+
json: () => {sources: TriggerEventFacetItemDto[]; events: TriggerEventFacetItemDto[]};
|
|
11
|
+
}) => res.json();
|
|
12
|
+
|
|
13
|
+
describe('GET /trigger-events/facets', () => {
|
|
14
|
+
let app: FastifyInstance;
|
|
15
|
+
let workspaceId: string;
|
|
16
|
+
let memberships: Array<{workspaceId: string; role: 'admin'}>;
|
|
17
|
+
|
|
18
|
+
beforeAll(async () => {
|
|
19
|
+
app = Fastify();
|
|
20
|
+
app.setValidatorCompiler(validatorCompiler);
|
|
21
|
+
app.setSerializerCompiler(serializerCompiler);
|
|
22
|
+
app.addHook('onRequest', (request, _reply, done) => {
|
|
23
|
+
setUserContext(
|
|
24
|
+
request,
|
|
25
|
+
buildUserContext({userId: crypto.randomUUID(), email: 'user@example.com', memberships}),
|
|
26
|
+
);
|
|
27
|
+
done();
|
|
28
|
+
});
|
|
29
|
+
app.get('/trigger-events/facets', listTriggerEventFacetsRoute);
|
|
30
|
+
await app.ready();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
workspaceId = crypto.randomUUID();
|
|
35
|
+
memberships = [{workspaceId, role: 'admin'}];
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('returns distinct sources and events with counts, ordered by count desc', async () => {
|
|
39
|
+
await receivedEventFactory.create({workspaceId, source: 'github', event: 'push'});
|
|
40
|
+
await receivedEventFactory.create({workspaceId, source: 'github', event: 'pull_request'});
|
|
41
|
+
await receivedEventFactory.create({workspaceId, source: 'gitea', event: 'push'});
|
|
42
|
+
|
|
43
|
+
const res = await app.inject({
|
|
44
|
+
method: 'GET',
|
|
45
|
+
url: `/trigger-events/facets?workspace_id=${workspaceId}`,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(res.statusCode).toBe(200);
|
|
49
|
+
expect(facets(res).sources).toEqual([
|
|
50
|
+
{value: 'github', count: 2},
|
|
51
|
+
{value: 'gitea', count: 1},
|
|
52
|
+
]);
|
|
53
|
+
expect(facets(res).events).toEqual([
|
|
54
|
+
{value: 'push', count: 2},
|
|
55
|
+
{value: 'pull_request', count: 1},
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('caps each facet at the top 50 values by count', async () => {
|
|
60
|
+
await Promise.all(
|
|
61
|
+
Array.from({length: 51}, (_, index) =>
|
|
62
|
+
receivedEventFactory.create({workspaceId, source: `source-${index}`}),
|
|
63
|
+
),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const res = await app.inject({
|
|
67
|
+
method: 'GET',
|
|
68
|
+
url: `/trigger-events/facets?workspace_id=${workspaceId}`,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(facets(res).sources).toHaveLength(50);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('scopes facets to the workspace', async () => {
|
|
75
|
+
await receivedEventFactory.create({workspaceId, source: 'github'});
|
|
76
|
+
await receivedEventFactory.create({source: 'gitlab'});
|
|
77
|
+
|
|
78
|
+
const res = await app.inject({
|
|
79
|
+
method: 'GET',
|
|
80
|
+
url: `/trigger-events/facets?workspace_id=${workspaceId}`,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
expect(facets(res).sources.map((facet) => facet.value)).toEqual(['github']);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('returns empty facets for a workspace with no events', async () => {
|
|
87
|
+
const res = await app.inject({
|
|
88
|
+
method: 'GET',
|
|
89
|
+
url: `/trigger-events/facets?workspace_id=${workspaceId}`,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(res.statusCode).toBe(200);
|
|
93
|
+
expect(res.json()).toEqual({sources: [], events: []});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('denies a workspace the caller does not belong to', async () => {
|
|
97
|
+
const otherWorkspaceId = crypto.randomUUID();
|
|
98
|
+
|
|
99
|
+
const res = await app.inject({
|
|
100
|
+
method: 'GET',
|
|
101
|
+
url: `/trigger-events/facets?workspace_id=${otherWorkspaceId}`,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(res.statusCode).toBe(403);
|
|
105
|
+
expect(res.json().code).toBe('forbidden');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('rejects a non-uuid workspace_id', async () => {
|
|
109
|
+
const res = await app.inject({
|
|
110
|
+
method: 'GET',
|
|
111
|
+
url: '/trigger-events/facets?workspace_id=nope',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
expect(res.statusCode).toBe(400);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {requireUserContext} from '@shipfox/api-auth-context';
|
|
2
|
+
import {
|
|
3
|
+
triggerEventFacetsQuerySchema,
|
|
4
|
+
triggerEventFacetsResponseSchema,
|
|
5
|
+
} from '@shipfox/api-triggers-dto';
|
|
6
|
+
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
7
|
+
import {listTriggerEventFacets} from '#db/index.js';
|
|
8
|
+
|
|
9
|
+
export const listTriggerEventFacetsRoute = defineRoute({
|
|
10
|
+
method: 'GET',
|
|
11
|
+
path: '/facets',
|
|
12
|
+
description: 'Distinct source and event filter values (with counts) for a workspace.',
|
|
13
|
+
schema: {
|
|
14
|
+
querystring: triggerEventFacetsQuerySchema,
|
|
15
|
+
response: {
|
|
16
|
+
200: triggerEventFacetsResponseSchema,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
handler: async (request) => {
|
|
20
|
+
const {workspace_id: workspaceId} = request.query;
|
|
21
|
+
|
|
22
|
+
const userContext = requireUserContext(request);
|
|
23
|
+
if (!userContext.canAccess(workspaceId)) {
|
|
24
|
+
throw new ClientError('Not a member of this workspace', 'forbidden', {status: 403});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return await listTriggerEventFacets({workspaceId});
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import {buildUserContext, setUserContext} from '@shipfox/api-auth-context';
|
|
2
|
+
import {encodeTimestampIdCursor} from '@shipfox/node-drizzle';
|
|
3
|
+
import type {FastifyInstance} from 'fastify';
|
|
4
|
+
import Fastify from 'fastify';
|
|
5
|
+
import {serializerCompiler, validatorCompiler} from 'fastify-type-provider-zod';
|
|
6
|
+
import {receivedEventFactory} from '#test/index.js';
|
|
7
|
+
import {listTriggerEventsRoute} from './list-trigger-events.js';
|
|
8
|
+
|
|
9
|
+
const eventIds = (res: {json: () => {trigger_events: Array<{id: string}>}}) =>
|
|
10
|
+
res.json().trigger_events.map((event) => event.id);
|
|
11
|
+
|
|
12
|
+
describe('GET /trigger-events', () => {
|
|
13
|
+
let app: FastifyInstance;
|
|
14
|
+
let workspaceId: string;
|
|
15
|
+
let memberships: Array<{workspaceId: string; role: 'admin'}>;
|
|
16
|
+
|
|
17
|
+
beforeAll(async () => {
|
|
18
|
+
app = Fastify();
|
|
19
|
+
app.setValidatorCompiler(validatorCompiler);
|
|
20
|
+
app.setSerializerCompiler(serializerCompiler);
|
|
21
|
+
app.addHook('onRequest', (request, _reply, done) => {
|
|
22
|
+
setUserContext(
|
|
23
|
+
request,
|
|
24
|
+
buildUserContext({userId: crypto.randomUUID(), email: 'user@example.com', memberships}),
|
|
25
|
+
);
|
|
26
|
+
done();
|
|
27
|
+
});
|
|
28
|
+
app.get('/trigger-events', listTriggerEventsRoute);
|
|
29
|
+
await app.ready();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
workspaceId = crypto.randomUUID();
|
|
34
|
+
memberships = [{workspaceId, role: 'admin'}];
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('returns a workspace events newest first, isolated and payload-free', async () => {
|
|
38
|
+
const older = await receivedEventFactory.create({
|
|
39
|
+
workspaceId,
|
|
40
|
+
receivedAt: new Date('2026-05-07T00:00:00.000Z'),
|
|
41
|
+
});
|
|
42
|
+
const newer = await receivedEventFactory.create({
|
|
43
|
+
workspaceId,
|
|
44
|
+
receivedAt: new Date('2026-05-07T02:00:00.000Z'),
|
|
45
|
+
});
|
|
46
|
+
await receivedEventFactory.create({receivedAt: new Date('2026-05-07T03:00:00.000Z')});
|
|
47
|
+
|
|
48
|
+
const res = await app.inject({
|
|
49
|
+
method: 'GET',
|
|
50
|
+
url: `/trigger-events?workspace_id=${workspaceId}`,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(res.statusCode).toBe(200);
|
|
54
|
+
expect(eventIds(res)).toEqual([newer.id, older.id]);
|
|
55
|
+
expect(res.json().next_cursor).toBeNull();
|
|
56
|
+
expect(res.json().trigger_events[0]).not.toHaveProperty('payload');
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('filters by source', async () => {
|
|
60
|
+
const gitea = await receivedEventFactory.create({workspaceId, source: 'gitea'});
|
|
61
|
+
await receivedEventFactory.create({workspaceId, source: 'github'});
|
|
62
|
+
|
|
63
|
+
const res = await app.inject({
|
|
64
|
+
method: 'GET',
|
|
65
|
+
url: `/trigger-events?workspace_id=${workspaceId}&source=gitea`,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
expect(eventIds(res)).toEqual([gitea.id]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('filters by repeated source keys (IN-list)', async () => {
|
|
72
|
+
const gitea = await receivedEventFactory.create({workspaceId, source: 'gitea'});
|
|
73
|
+
const github = await receivedEventFactory.create({workspaceId, source: 'github'});
|
|
74
|
+
await receivedEventFactory.create({workspaceId, source: 'manual'});
|
|
75
|
+
|
|
76
|
+
const res = await app.inject({
|
|
77
|
+
method: 'GET',
|
|
78
|
+
url: `/trigger-events?workspace_id=${workspaceId}&source=gitea&source=github`,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
expect(eventIds(res).sort()).toEqual([gitea.id, github.id].sort());
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('filters by comma-separated sources', async () => {
|
|
85
|
+
const gitea = await receivedEventFactory.create({workspaceId, source: 'gitea'});
|
|
86
|
+
const github = await receivedEventFactory.create({workspaceId, source: 'github'});
|
|
87
|
+
await receivedEventFactory.create({workspaceId, source: 'manual'});
|
|
88
|
+
|
|
89
|
+
const res = await app.inject({
|
|
90
|
+
method: 'GET',
|
|
91
|
+
url: `/trigger-events?workspace_id=${workspaceId}&source=gitea,github`,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(eventIds(res).sort()).toEqual([gitea.id, github.id].sort());
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('filters by event', async () => {
|
|
98
|
+
const pullRequest = await receivedEventFactory.create({workspaceId, event: 'pull_request'});
|
|
99
|
+
await receivedEventFactory.create({workspaceId, event: 'push'});
|
|
100
|
+
|
|
101
|
+
const res = await app.inject({
|
|
102
|
+
method: 'GET',
|
|
103
|
+
url: `/trigger-events?workspace_id=${workspaceId}&event=pull_request`,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(eventIds(res)).toEqual([pullRequest.id]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test('filters by repeated event keys (IN-list)', async () => {
|
|
110
|
+
const pullRequest = await receivedEventFactory.create({workspaceId, event: 'pull_request'});
|
|
111
|
+
const push = await receivedEventFactory.create({workspaceId, event: 'push'});
|
|
112
|
+
await receivedEventFactory.create({workspaceId, event: 'release'});
|
|
113
|
+
|
|
114
|
+
const res = await app.inject({
|
|
115
|
+
method: 'GET',
|
|
116
|
+
url: `/trigger-events?workspace_id=${workspaceId}&event=pull_request&event=push`,
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(eventIds(res).sort()).toEqual([pullRequest.id, push.id].sort());
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('filters by comma-separated events', async () => {
|
|
123
|
+
const pullRequest = await receivedEventFactory.create({workspaceId, event: 'pull_request'});
|
|
124
|
+
const push = await receivedEventFactory.create({workspaceId, event: 'push'});
|
|
125
|
+
await receivedEventFactory.create({workspaceId, event: 'release'});
|
|
126
|
+
|
|
127
|
+
const res = await app.inject({
|
|
128
|
+
method: 'GET',
|
|
129
|
+
url: `/trigger-events?workspace_id=${workspaceId}&event=pull_request,push`,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(eventIds(res).sort()).toEqual([pullRequest.id, push.id].sort());
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test('filters by repeated outcome keys (IN-list)', async () => {
|
|
136
|
+
const {routed, discarded, failed} = await seedOutcomes(workspaceId);
|
|
137
|
+
|
|
138
|
+
const res = await app.inject({
|
|
139
|
+
method: 'GET',
|
|
140
|
+
url: `/trigger-events?workspace_id=${workspaceId}&outcome=routed&outcome=failed`,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(eventIds(res).sort()).toEqual([routed.id, failed.id].sort());
|
|
144
|
+
expect(eventIds(res)).not.toContain(discarded.id);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('filters by comma-separated outcomes', async () => {
|
|
148
|
+
const {routed, discarded, failed} = await seedOutcomes(workspaceId);
|
|
149
|
+
|
|
150
|
+
const res = await app.inject({
|
|
151
|
+
method: 'GET',
|
|
152
|
+
url: `/trigger-events?workspace_id=${workspaceId}&outcome=routed,failed`,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(eventIds(res).sort()).toEqual([routed.id, failed.id].sort());
|
|
156
|
+
expect(eventIds(res)).not.toContain(discarded.id);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('filters by a single outcome', async () => {
|
|
160
|
+
const {discarded} = await seedOutcomes(workspaceId);
|
|
161
|
+
|
|
162
|
+
const res = await app.inject({
|
|
163
|
+
method: 'GET',
|
|
164
|
+
url: `/trigger-events?workspace_id=${workspaceId}&outcome=discarded`,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(eventIds(res)).toEqual([discarded.id]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test('filters and serializes the errored outcome', async () => {
|
|
171
|
+
const errored = await receivedEventFactory.create({workspaceId, outcome: 'errored'});
|
|
172
|
+
|
|
173
|
+
const res = await app.inject({
|
|
174
|
+
method: 'GET',
|
|
175
|
+
url: `/trigger-events?workspace_id=${workspaceId}&outcome=errored`,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
expect(res.statusCode).toBe(200);
|
|
179
|
+
expect(eventIds(res)).toEqual([errored.id]);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('treats a blank outcome as no filter', async () => {
|
|
183
|
+
const {routed, discarded, failed} = await seedOutcomes(workspaceId);
|
|
184
|
+
|
|
185
|
+
const res = await app.inject({
|
|
186
|
+
method: 'GET',
|
|
187
|
+
url: `/trigger-events?workspace_id=${workspaceId}&outcome=`,
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
expect(res.statusCode).toBe(200);
|
|
191
|
+
expect(eventIds(res).sort()).toEqual([routed.id, discarded.id, failed.id].sort());
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('drops a trailing comma in the outcome filter', async () => {
|
|
195
|
+
const {routed, discarded, failed} = await seedOutcomes(workspaceId);
|
|
196
|
+
|
|
197
|
+
const res = await app.inject({
|
|
198
|
+
method: 'GET',
|
|
199
|
+
url: `/trigger-events?workspace_id=${workspaceId}&outcome=routed,`,
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(eventIds(res)).toEqual([routed.id]);
|
|
203
|
+
expect(eventIds(res)).not.toContain(discarded.id);
|
|
204
|
+
expect(eventIds(res)).not.toContain(failed.id);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('filters by received_at window', async () => {
|
|
208
|
+
await receivedEventFactory.create({
|
|
209
|
+
workspaceId,
|
|
210
|
+
receivedAt: new Date('2026-05-01T00:00:00.000Z'),
|
|
211
|
+
});
|
|
212
|
+
const inWindow = await receivedEventFactory.create({
|
|
213
|
+
workspaceId,
|
|
214
|
+
receivedAt: new Date('2026-05-10T00:00:00.000Z'),
|
|
215
|
+
});
|
|
216
|
+
await receivedEventFactory.create({
|
|
217
|
+
workspaceId,
|
|
218
|
+
receivedAt: new Date('2026-05-20T00:00:00.000Z'),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const res = await app.inject({
|
|
222
|
+
method: 'GET',
|
|
223
|
+
url: `/trigger-events?workspace_id=${workspaceId}&from=2026-05-05T00:00:00.000Z&to=2026-05-15T00:00:00.000Z`,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(eventIds(res)).toEqual([inWindow.id]);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('includes events on the from and to window boundaries', async () => {
|
|
230
|
+
const onFrom = await receivedEventFactory.create({
|
|
231
|
+
workspaceId,
|
|
232
|
+
receivedAt: new Date('2026-05-05T00:00:00.000Z'),
|
|
233
|
+
});
|
|
234
|
+
const onTo = await receivedEventFactory.create({
|
|
235
|
+
workspaceId,
|
|
236
|
+
receivedAt: new Date('2026-05-15T00:00:00.000Z'),
|
|
237
|
+
});
|
|
238
|
+
await receivedEventFactory.create({
|
|
239
|
+
workspaceId,
|
|
240
|
+
receivedAt: new Date('2026-05-04T23:59:59.999Z'),
|
|
241
|
+
});
|
|
242
|
+
await receivedEventFactory.create({
|
|
243
|
+
workspaceId,
|
|
244
|
+
receivedAt: new Date('2026-05-15T00:00:00.001Z'),
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const res = await app.inject({
|
|
248
|
+
method: 'GET',
|
|
249
|
+
url: `/trigger-events?workspace_id=${workspaceId}&from=2026-05-05T00:00:00.000Z&to=2026-05-15T00:00:00.000Z`,
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
expect(eventIds(res).sort()).toEqual([onFrom.id, onTo.id].sort());
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test('paginates with a received_at + id cursor', async () => {
|
|
256
|
+
const first = await receivedEventFactory.create({
|
|
257
|
+
workspaceId,
|
|
258
|
+
receivedAt: new Date('2026-05-07T00:00:00.000Z'),
|
|
259
|
+
});
|
|
260
|
+
const second = await receivedEventFactory.create({
|
|
261
|
+
workspaceId,
|
|
262
|
+
receivedAt: new Date('2026-05-07T01:00:00.000Z'),
|
|
263
|
+
});
|
|
264
|
+
const third = await receivedEventFactory.create({
|
|
265
|
+
workspaceId,
|
|
266
|
+
receivedAt: new Date('2026-05-07T02:00:00.000Z'),
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const page1 = await app.inject({
|
|
270
|
+
method: 'GET',
|
|
271
|
+
url: `/trigger-events?workspace_id=${workspaceId}&limit=2`,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(eventIds(page1)).toEqual([third.id, second.id]);
|
|
275
|
+
expect(page1.json().next_cursor).toBe(
|
|
276
|
+
encodeTimestampIdCursor({createdAt: second.receivedAt, id: second.id}),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const page2 = await app.inject({
|
|
280
|
+
method: 'GET',
|
|
281
|
+
url: `/trigger-events?workspace_id=${workspaceId}&limit=2&cursor=${page1.json().next_cursor}`,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
expect(eventIds(page2)).toEqual([first.id]);
|
|
285
|
+
expect(page2.json().next_cursor).toBeNull();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('breaks received_at ties by id without duplicating or skipping rows', async () => {
|
|
289
|
+
const tie = new Date('2026-05-07T00:00:00.000Z');
|
|
290
|
+
const a = await receivedEventFactory.create({workspaceId, receivedAt: tie});
|
|
291
|
+
const b = await receivedEventFactory.create({workspaceId, receivedAt: tie});
|
|
292
|
+
const earlier = await receivedEventFactory.create({
|
|
293
|
+
workspaceId,
|
|
294
|
+
receivedAt: new Date('2026-05-06T00:00:00.000Z'),
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
const page1 = await app.inject({
|
|
298
|
+
method: 'GET',
|
|
299
|
+
url: `/trigger-events?workspace_id=${workspaceId}&limit=2`,
|
|
300
|
+
});
|
|
301
|
+
const page2 = await app.inject({
|
|
302
|
+
method: 'GET',
|
|
303
|
+
url: `/trigger-events?workspace_id=${workspaceId}&limit=2&cursor=${page1.json().next_cursor}`,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
expect(eventIds(page1)).toEqual([a.id, b.id].sort().reverse());
|
|
307
|
+
expect(eventIds(page2)).toEqual([earlier.id]);
|
|
308
|
+
expect(new Set([...eventIds(page1), ...eventIds(page2)]).size).toBe(3);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test('returns an empty page for a workspace with no events', async () => {
|
|
312
|
+
const res = await app.inject({
|
|
313
|
+
method: 'GET',
|
|
314
|
+
url: `/trigger-events?workspace_id=${workspaceId}`,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
expect(res.statusCode).toBe(200);
|
|
318
|
+
expect(res.json()).toEqual({trigger_events: [], next_cursor: null});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('denies a workspace the caller does not belong to', async () => {
|
|
322
|
+
const otherWorkspaceId = crypto.randomUUID();
|
|
323
|
+
|
|
324
|
+
const res = await app.inject({
|
|
325
|
+
method: 'GET',
|
|
326
|
+
url: `/trigger-events?workspace_id=${otherWorkspaceId}`,
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
expect(res.statusCode).toBe(403);
|
|
330
|
+
expect(res.json().code).toBe('forbidden');
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('rejects an invalid cursor', async () => {
|
|
334
|
+
const res = await app.inject({
|
|
335
|
+
method: 'GET',
|
|
336
|
+
url: `/trigger-events?workspace_id=${workspaceId}&cursor=not-a-cursor`,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
expect(res.statusCode).toBe(400);
|
|
340
|
+
expect(res.json().code).toBe('invalid-cursor');
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test('requires a workspace_id', async () => {
|
|
344
|
+
const res = await app.inject({method: 'GET', url: '/trigger-events'});
|
|
345
|
+
|
|
346
|
+
expect(res.statusCode).toBe(400);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('rejects a non-uuid workspace_id', async () => {
|
|
350
|
+
const res = await app.inject({method: 'GET', url: '/trigger-events?workspace_id=nope'});
|
|
351
|
+
|
|
352
|
+
expect(res.statusCode).toBe(400);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test('rejects an unknown outcome value', async () => {
|
|
356
|
+
const res = await app.inject({
|
|
357
|
+
method: 'GET',
|
|
358
|
+
url: `/trigger-events?workspace_id=${workspaceId}&outcome=bogus`,
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
expect(res.statusCode).toBe(400);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test('rejects a reversed date window', async () => {
|
|
365
|
+
const res = await app.inject({
|
|
366
|
+
method: 'GET',
|
|
367
|
+
url: `/trigger-events?workspace_id=${workspaceId}&from=2026-05-10T00:00:00.000Z&to=2026-05-01T00:00:00.000Z`,
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
expect(res.statusCode).toBe(400);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
async function seedOutcomes(workspaceId: string) {
|
|
375
|
+
const routed = await receivedEventFactory.create({workspaceId, outcome: 'routed'});
|
|
376
|
+
const discarded = await receivedEventFactory.create({workspaceId, outcome: 'discarded'});
|
|
377
|
+
const failed = await receivedEventFactory.create({workspaceId, outcome: 'failed'});
|
|
378
|
+
return {routed, discarded, failed};
|
|
379
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import {requireUserContext} from '@shipfox/api-auth-context';
|
|
2
|
+
import {
|
|
3
|
+
triggerEventListQuerySchema,
|
|
4
|
+
triggerEventListResponseSchema,
|
|
5
|
+
} from '@shipfox/api-triggers-dto';
|
|
6
|
+
import {decodeTimestampIdCursor, encodeTimestampIdCursor} from '@shipfox/node-drizzle';
|
|
7
|
+
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
8
|
+
import {listTriggerEvents} from '#db/index.js';
|
|
9
|
+
import {toTriggerEventListItemDto} from '#presentation/dto/trigger-events.js';
|
|
10
|
+
|
|
11
|
+
export const listTriggerEventsRoute = defineRoute({
|
|
12
|
+
method: 'GET',
|
|
13
|
+
path: '/',
|
|
14
|
+
description: 'List trigger events for a workspace, newest first.',
|
|
15
|
+
schema: {
|
|
16
|
+
querystring: triggerEventListQuerySchema,
|
|
17
|
+
response: {
|
|
18
|
+
200: triggerEventListResponseSchema,
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
handler: async (request) => {
|
|
22
|
+
const {
|
|
23
|
+
workspace_id: workspaceId,
|
|
24
|
+
source,
|
|
25
|
+
event,
|
|
26
|
+
outcome: outcomes,
|
|
27
|
+
from,
|
|
28
|
+
to,
|
|
29
|
+
limit,
|
|
30
|
+
cursor,
|
|
31
|
+
} = request.query;
|
|
32
|
+
|
|
33
|
+
const userContext = requireUserContext(request);
|
|
34
|
+
if (!userContext.canAccess(workspaceId)) {
|
|
35
|
+
throw new ClientError('Not a member of this workspace', 'forbidden', {status: 403});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const decodedCursor = decodeTimestampIdCursor(cursor);
|
|
39
|
+
if (cursor && !decodedCursor) {
|
|
40
|
+
throw new ClientError('Invalid cursor', 'invalid-cursor', {status: 400});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const result = await listTriggerEvents({
|
|
44
|
+
workspaceId,
|
|
45
|
+
limit,
|
|
46
|
+
cursor: decodedCursor
|
|
47
|
+
? {receivedAt: decodedCursor.createdAt, id: decodedCursor.id}
|
|
48
|
+
: undefined,
|
|
49
|
+
filters: {
|
|
50
|
+
source,
|
|
51
|
+
event,
|
|
52
|
+
outcomes,
|
|
53
|
+
from: from ? new Date(from) : undefined,
|
|
54
|
+
to: to ? new Date(to) : undefined,
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
trigger_events: result.events.map(toTriggerEventListItemDto),
|
|
60
|
+
next_cursor: result.nextCursor
|
|
61
|
+
? encodeTimestampIdCursor({
|
|
62
|
+
createdAt: result.nextCursor.receivedAt,
|
|
63
|
+
id: result.nextCursor.id,
|
|
64
|
+
})
|
|
65
|
+
: null,
|
|
66
|
+
};
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export {onDefinitionDeleted} from './on-definition-deleted.js';
|
|
2
|
+
export {onDefinitionResolved} from './on-definition-resolved.js';
|
|
3
|
+
export {onIntegrationEventReceived} from './on-integration-event-received.js';
|
|
4
|
+
export {onJobActivated} from './on-job-activated.js';
|
|
5
|
+
export {onJobTerminated} from './on-job-terminated.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type {DefinitionDeletedEvent} from '@shipfox/api-definitions-dto';
|
|
2
|
+
import {eq} from 'drizzle-orm';
|
|
3
|
+
import {db} from '#db/db.js';
|
|
4
|
+
import {triggerSubscriptions} from '#db/schema/subscriptions.js';
|
|
5
|
+
import {triggerSubscriptionFactory} from '#test/index.js';
|
|
6
|
+
import {onDefinitionDeleted} from './on-definition-deleted.js';
|
|
7
|
+
|
|
8
|
+
function buildPayload(definitionId: string): DefinitionDeletedEvent {
|
|
9
|
+
return {definitionId, projectId: crypto.randomUUID(), workspaceId: crypto.randomUUID()};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function listSubscriptions(workflowDefinitionId: string) {
|
|
13
|
+
return db()
|
|
14
|
+
.select()
|
|
15
|
+
.from(triggerSubscriptions)
|
|
16
|
+
.where(eq(triggerSubscriptions.workflowDefinitionId, workflowDefinitionId));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('onDefinitionDeleted', () => {
|
|
20
|
+
it('deletes every subscription for the definition', async () => {
|
|
21
|
+
const definitionId = crypto.randomUUID();
|
|
22
|
+
await triggerSubscriptionFactory.create({workflowDefinitionId: definitionId, name: 'a'});
|
|
23
|
+
await triggerSubscriptionFactory.create({workflowDefinitionId: definitionId, name: 'b'});
|
|
24
|
+
|
|
25
|
+
await onDefinitionDeleted(buildPayload(definitionId));
|
|
26
|
+
|
|
27
|
+
const rows = await listSubscriptions(definitionId);
|
|
28
|
+
expect(rows).toHaveLength(0);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('leaves subscriptions for other definitions untouched', async () => {
|
|
32
|
+
const deletedId = crypto.randomUUID();
|
|
33
|
+
const keptId = crypto.randomUUID();
|
|
34
|
+
await triggerSubscriptionFactory.create({workflowDefinitionId: deletedId, name: 'a'});
|
|
35
|
+
const kept = await triggerSubscriptionFactory.create({workflowDefinitionId: keptId, name: 'a'});
|
|
36
|
+
|
|
37
|
+
await onDefinitionDeleted(buildPayload(deletedId));
|
|
38
|
+
|
|
39
|
+
const rows = await listSubscriptions(keptId);
|
|
40
|
+
expect(rows.map((r) => r.id)).toEqual([kept.id]);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type {DefinitionDeletedEvent} from '@shipfox/api-definitions-dto';
|
|
2
|
+
import {deleteSubscriptionsForDefinition} from '#db/subscriptions.js';
|
|
3
|
+
|
|
4
|
+
export async function onDefinitionDeleted(payload: DefinitionDeletedEvent): Promise<void> {
|
|
5
|
+
await deleteSubscriptionsForDefinition({workflowDefinitionId: payload.definitionId});
|
|
6
|
+
}
|