@classytic/arc 2.8.4 → 2.9.1
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/README.md +116 -5
- package/dist/{BaseController-DAGGc5Xn.mjs → BaseController-Vu2yc56T.mjs} +188 -102
- package/dist/EventTransport-CqZ8FyM_.d.mts +293 -0
- package/dist/adapters/index.d.mts +2 -2
- package/dist/audit/index.d.mts +100 -11
- package/dist/audit/index.mjs +71 -18
- package/dist/auth/index.d.mts +15 -7
- package/dist/auth/index.mjs +13 -6
- package/dist/{betterAuthOpenApi-C5lDyRH2.mjs → betterAuthOpenApi--rdY15Ld.mjs} +1 -1
- package/dist/cache/index.d.mts +71 -1
- package/dist/cache/index.mjs +96 -3
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -5
- package/dist/{core-DKSwNSXf.mjs → core-DNncu0xF.mjs} +1 -1
- package/dist/{createActionRouter-Df1BuawX.mjs → createActionRouter-DH1YFL9m.mjs} +3 -3
- package/dist/{createApp-BOYjBgdI.mjs → createApp-CBJUJKGP.mjs} +6 -5
- package/dist/{defineResource-Bb_Bdhtw.mjs → defineResource-C__jkwvs.mjs} +22 -57
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +1 -1
- package/dist/dynamic/index.d.mts +2 -2
- package/dist/dynamic/index.mjs +3 -3
- package/dist/{elevation-BBGFjzIP.mjs → elevation-DxQ6ACbt.mjs} +20 -6
- package/dist/{errorHandler-mzqk4cGl.mjs → errorHandler-CZDW4EXS.mjs} +59 -7
- package/dist/{errorHandler-CdZDavNH.d.mts → errorHandler-DixGcttC.d.mts} +37 -2
- package/dist/{eventPlugin-CVxlE6De.d.mts → eventPlugin-BxvaCIZF.d.mts} +14 -2
- package/dist/{eventPlugin-D91S2YF4.mjs → eventPlugin-Dl7MoVWH.mjs} +83 -5
- package/dist/events/index.d.mts +147 -36
- package/dist/events/index.mjs +338 -101
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis.d.mts +1 -1
- package/dist/factory/index.d.mts +1 -1
- package/dist/factory/index.mjs +1 -1
- package/dist/{fields-DC4So2M2.d.mts → fields-BC7zcmI9.d.mts} +15 -3
- package/dist/{fields-ipsbIRPK.mjs → fields-CU6FlaDV.mjs} +18 -5
- package/dist/filesUpload-q8oHt--L.mjs +377 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +28 -4
- package/dist/idempotency/index.mjs +111 -2
- package/dist/idempotency/redis.d.mts +2 -2
- package/dist/idempotency/redis.mjs +134 -13
- package/dist/{index-CSkeivBx.d.mts → index-C-xjcA6F.d.mts} +2 -2
- package/dist/{index-CpTSDqmD.d.mts → index-Cibkchnx.d.mts} +5 -136
- package/dist/{index-BgmMdpm8.d.mts → index-CtGKT0lf.d.mts} +1 -1
- package/dist/index.d.mts +8 -8
- package/dist/index.mjs +8 -8
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/jobs.d.mts +25 -3
- package/dist/integrations/jobs.mjs +63 -4
- package/dist/integrations/mcp/index.d.mts +26 -8
- package/dist/integrations/mcp/index.mjs +96 -17
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +5 -0
- package/dist/integrations/webhooks.mjs +6 -0
- package/dist/{interface-BVuMfeVv.d.mts → interface-YrWsmKqE.d.mts} +324 -194
- package/dist/{openapi-CYCuekCn.mjs → openapi-CXuTG1M9.mjs} +3 -3
- package/dist/org/index.d.mts +2 -2
- package/dist/permissions/index.d.mts +3 -3
- package/dist/permissions/index.mjs +3 -3
- package/dist/{permissions-CH4cNwJi.mjs → permissions-oNZawnkR.mjs} +1 -1
- package/dist/plugins/index.d.mts +6 -6
- package/dist/plugins/index.mjs +4 -4
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.d.mts +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/policies/index.d.mts +26 -33
- package/dist/presets/filesUpload.d.mts +71 -0
- package/dist/presets/filesUpload.mjs +2 -0
- package/dist/presets/index.d.mts +4 -2
- package/dist/presets/index.mjs +4 -2
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +1 -1
- package/dist/presets/search.d.mts +91 -0
- package/dist/presets/search.mjs +150 -0
- package/dist/{presets-C2xgzW6x.mjs → presets-hM4WhNWY.mjs} +1 -1
- package/dist/{queryCachePlugin-D0iIVhW_.mjs → queryCachePlugin-DbUVroUG.mjs} +2 -2
- package/dist/redis-MXLp1oOf.d.mts +115 -0
- package/dist/{redis-stream-D54N5oXs.d.mts → redis-stream-Bz-4q96t.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/{resourceToTools-O_HwWXFa.mjs → resourceToTools-C3cWymnW.mjs} +65 -48
- package/dist/rpc/index.mjs +1 -1
- package/dist/{schemaConverter-OxfCshus.mjs → schemaConverter-BxFDdtXu.mjs} +25 -9
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +1 -1
- package/dist/storage-BwGQXUpd.d.mts +146 -0
- package/dist/store-helpers-DFiZl5TL.mjs +57 -0
- package/dist/testing/index.d.mts +7 -15
- package/dist/testing/index.mjs +23 -76
- package/dist/testing/storageContract.d.mts +26 -0
- package/dist/testing/storageContract.mjs +216 -0
- package/dist/types/index.d.mts +5 -5
- package/dist/types/storage.d.mts +2 -0
- package/dist/types/storage.mjs +1 -0
- package/dist/{types-CcG4avic.d.mts → types-CoSzA-s-.d.mts} +1 -1
- package/dist/{types-Bg2X42_m.d.mts → types-CunEX4UX.d.mts} +7 -5
- package/dist/{types-CVC4HOKi.d.mts → types-DZi1aYhm.d.mts} +1 -1
- package/dist/utils/index.d.mts +26 -8
- package/dist/utils/index.mjs +6 -6
- package/dist/{utils-yYT3HDXt.mjs → utils-B7FuRr9w.mjs} +1 -1
- package/package.json +23 -11
- package/skills/arc/SKILL.md +92 -14
- package/skills/arc/references/auth.md +94 -0
- package/skills/arc/references/events.md +229 -12
- package/skills/arc/references/mcp.md +4 -17
- package/skills/arc/references/multi-tenancy.md +43 -0
- package/skills/arc/references/production.md +34 -19
- package/dist/EventTransport-CinyO7zQ.d.mts +0 -135
- package/dist/audit/mongodb.d.mts +0 -2
- package/dist/audit/mongodb.mjs +0 -2
- package/dist/idempotency/mongodb.d.mts +0 -2
- package/dist/idempotency/mongodb.mjs +0 -123
- package/dist/mongodb-B5O6xaW1.mjs +0 -90
- package/dist/mongodb-B8U2xaLj.d.mts +0 -127
- package/dist/mongodb-X7LbEjTN.d.mts +0 -80
- package/dist/redis-z3sFr1UP.d.mts +0 -49
- /package/dist/{applyPermissionResult-D6GPMsvh.mjs → applyPermissionResult-bqGpo9ML.mjs} +0 -0
- /package/dist/{circuitBreaker-cmi5XDv5.mjs → circuitBreaker-l18oRgL5.mjs} +0 -0
- /package/dist/{elevation-s5ykdNHr.d.mts → elevation-B6S5csVA.d.mts} +0 -0
- /package/dist/{errors-Bmn3eZT6.d.mts → errors-BI8kEKsO.d.mts} +0 -0
- /package/dist/{errors-BF2bIOIS.mjs → errors-CqWnSqM-.mjs} +0 -0
- /package/dist/{memory-Cp7_cAko.mjs → memory-BFAYkf8H.mjs} +0 -0
- /package/dist/{pluralize-A0tWEl1K.mjs → pluralize-CWP6MB39.mjs} +0 -0
- /package/dist/{queryParser-CgCtsjti.mjs → queryParser-Cs-6SHQK.mjs} +0 -0
- /package/dist/{requestContext-DYvHl113.mjs → requestContext-DYtmNpm5.mjs} +0 -0
- /package/dist/{tracing-DxjKk7eW.d.mts → tracing-xqXzWeaf.d.mts} +0 -0
- /package/dist/{typeGuards-CcFZXgU7.mjs → typeGuards-Cj5Rgvlg.mjs} +0 -0
- /package/dist/{types-C72d3NDn.d.mts → types-BD85MlEK.d.mts} +0 -0
|
@@ -77,18 +77,24 @@ var RedisIdempotencyStore = class {
|
|
|
77
77
|
}
|
|
78
78
|
async findByPrefix(prefix) {
|
|
79
79
|
const keys = await this.scanByPrefix(this.resultKey(prefix));
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
80
|
+
if (keys.length === 0) return void 0;
|
|
81
|
+
const BATCH_SIZE = 10;
|
|
82
|
+
const now = /* @__PURE__ */ new Date();
|
|
83
|
+
for (let i = 0; i < keys.length; i += BATCH_SIZE) {
|
|
84
|
+
const batch = keys.slice(i, i + BATCH_SIZE);
|
|
85
|
+
const values = await Promise.all(batch.map((k) => this.client.get(k)));
|
|
86
|
+
for (const data of values) {
|
|
87
|
+
if (!data) continue;
|
|
88
|
+
try {
|
|
89
|
+
const result = JSON.parse(data);
|
|
90
|
+
if (new Date(result.expiresAt) < now) continue;
|
|
91
|
+
return {
|
|
92
|
+
...result,
|
|
93
|
+
createdAt: new Date(result.createdAt),
|
|
94
|
+
expiresAt: new Date(result.expiresAt)
|
|
95
|
+
};
|
|
96
|
+
} catch {}
|
|
97
|
+
}
|
|
92
98
|
}
|
|
93
99
|
}
|
|
94
100
|
/** Scan Redis keys matching a prefix pattern. Falls back to empty if SCAN unavailable. */
|
|
@@ -105,5 +111,120 @@ var RedisIdempotencyStore = class {
|
|
|
105
111
|
}
|
|
106
112
|
async close() {}
|
|
107
113
|
};
|
|
114
|
+
/**
|
|
115
|
+
* Wrap an ioredis instance as the arc idempotency `RedisClient`.
|
|
116
|
+
*
|
|
117
|
+
* Arc's idempotency store expects node-redis-v4 style option objects
|
|
118
|
+
* (`{ EX, NX }`). ioredis uses positional flags. This adapter lets users
|
|
119
|
+
* plug an ioredis instance in without writing the bridge themselves.
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* import Redis from 'ioredis';
|
|
124
|
+
* import { RedisIdempotencyStore, ioredisAsIdempotencyClient }
|
|
125
|
+
* from '@classytic/arc/idempotency/redis';
|
|
126
|
+
*
|
|
127
|
+
* const redis = new Redis(process.env.REDIS_URL);
|
|
128
|
+
* const store = new RedisIdempotencyStore({
|
|
129
|
+
* client: ioredisAsIdempotencyClient(redis),
|
|
130
|
+
* });
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
function ioredisAsIdempotencyClient(client) {
|
|
134
|
+
return {
|
|
135
|
+
async get(key) {
|
|
136
|
+
return client.get(key);
|
|
137
|
+
},
|
|
138
|
+
async set(key, value, options) {
|
|
139
|
+
const args = [key, value];
|
|
140
|
+
if (options?.EX != null) args.push("EX", options.EX);
|
|
141
|
+
if (options?.NX) args.push("NX");
|
|
142
|
+
return client.set(...args);
|
|
143
|
+
},
|
|
144
|
+
async del(key) {
|
|
145
|
+
if (Array.isArray(key)) return client.del(...key);
|
|
146
|
+
return client.del(key);
|
|
147
|
+
},
|
|
148
|
+
async exists(key) {
|
|
149
|
+
if (Array.isArray(key)) return client.exists(...key);
|
|
150
|
+
return client.exists(key);
|
|
151
|
+
},
|
|
152
|
+
async scan(cursor, ...args) {
|
|
153
|
+
const [next, keys] = await client.scan(cursor, ...args);
|
|
154
|
+
return [next, keys];
|
|
155
|
+
},
|
|
156
|
+
eval: client.eval ? (script, numKeys, ...args) => client.eval(script, numKeys, ...args) : void 0,
|
|
157
|
+
async quit() {
|
|
158
|
+
if (client.quit) return client.quit();
|
|
159
|
+
return "OK";
|
|
160
|
+
},
|
|
161
|
+
async disconnect() {
|
|
162
|
+
if (client.disconnect) client.disconnect();
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Wrap an `@upstash/redis` REST client as an idempotency `RedisClient`.
|
|
168
|
+
*
|
|
169
|
+
* Enables running arc's idempotency store on Cloudflare Workers, Vercel Edge
|
|
170
|
+
* and Deno Deploy — runtimes that don't support raw TCP (ioredis).
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```typescript
|
|
174
|
+
* import { Redis } from '@upstash/redis';
|
|
175
|
+
* import { RedisIdempotencyStore, upstashAsIdempotencyClient }
|
|
176
|
+
* from '@classytic/arc/idempotency/redis';
|
|
177
|
+
*
|
|
178
|
+
* const redis = Redis.fromEnv();
|
|
179
|
+
* const store = new RedisIdempotencyStore({
|
|
180
|
+
* client: upstashAsIdempotencyClient(redis),
|
|
181
|
+
* });
|
|
182
|
+
* ```
|
|
183
|
+
*/
|
|
184
|
+
function upstashAsIdempotencyClient(client) {
|
|
185
|
+
return {
|
|
186
|
+
async get(key) {
|
|
187
|
+
const raw = await client.get(key);
|
|
188
|
+
if (raw == null) return null;
|
|
189
|
+
return typeof raw === "string" ? raw : JSON.stringify(raw);
|
|
190
|
+
},
|
|
191
|
+
async set(key, value, options) {
|
|
192
|
+
const opts = {};
|
|
193
|
+
if (options?.EX != null) opts.ex = options.EX;
|
|
194
|
+
if (options?.NX) opts.nx = true;
|
|
195
|
+
const res = await client.set(key, value, opts);
|
|
196
|
+
return res == null ? null : String(res);
|
|
197
|
+
},
|
|
198
|
+
async del(key) {
|
|
199
|
+
if (Array.isArray(key)) return client.del(...key);
|
|
200
|
+
return client.del(key);
|
|
201
|
+
},
|
|
202
|
+
async exists(key) {
|
|
203
|
+
if (Array.isArray(key)) return client.exists(...key);
|
|
204
|
+
return client.exists(key);
|
|
205
|
+
},
|
|
206
|
+
async scan(cursor, ...args) {
|
|
207
|
+
const opts = {};
|
|
208
|
+
for (let i = 0; i < args.length; i += 2) {
|
|
209
|
+
const flag = String(args[i]).toLowerCase();
|
|
210
|
+
const val = args[i + 1];
|
|
211
|
+
if (flag === "match" && typeof val === "string") opts.match = val;
|
|
212
|
+
if (flag === "count") opts.count = Number(val);
|
|
213
|
+
}
|
|
214
|
+
const [next, keys] = await client.scan(cursor, opts);
|
|
215
|
+
return [next, keys];
|
|
216
|
+
},
|
|
217
|
+
eval: client.eval ? async (script, _numKeys, ...args) => {
|
|
218
|
+
const keyCount = _numKeys;
|
|
219
|
+
const keys = args.slice(0, keyCount).map(String);
|
|
220
|
+
const rest = args.slice(keyCount);
|
|
221
|
+
return client.eval(script, keys, rest);
|
|
222
|
+
} : void 0,
|
|
223
|
+
async quit() {
|
|
224
|
+
return "OK";
|
|
225
|
+
},
|
|
226
|
+
async disconnect() {}
|
|
227
|
+
};
|
|
228
|
+
}
|
|
108
229
|
//#endregion
|
|
109
|
-
export { RedisIdempotencyStore };
|
|
230
|
+
export { RedisIdempotencyStore, ioredisAsIdempotencyClient, upstashAsIdempotencyClient };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { r as RequestScope } from "./types-
|
|
2
|
-
import { n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "./types-
|
|
1
|
+
import { r as RequestScope } from "./types-BD85MlEK.mjs";
|
|
2
|
+
import { n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "./types-DZi1aYhm.mjs";
|
|
3
3
|
import { i as CacheStore, t as CacheLogger } from "./interface-DplgQO2e.mjs";
|
|
4
4
|
import { FastifyRequest } from "fastify";
|
|
5
5
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { r as RequestScope } from "./types-
|
|
2
|
-
import {
|
|
3
|
-
import { t as PermissionCheck } from "./types-
|
|
4
|
-
import {
|
|
1
|
+
import { r as RequestScope } from "./types-BD85MlEK.mjs";
|
|
2
|
+
import { Bt as IController, E as CrudRouterOptions, Ht as IRequestContext, Kt as ResourceDefinition, M as FastifyWithDecorators, Vt as IControllerResponse, at as RequestContext, lt as ResourceConfig, p as AnyRecord, w as CrudController } from "./interface-YrWsmKqE.mjs";
|
|
3
|
+
import { t as PermissionCheck } from "./types-DZi1aYhm.mjs";
|
|
4
|
+
import { FastifyReply, FastifyRequest, RouteHandlerMethod } from "fastify";
|
|
5
5
|
|
|
6
6
|
//#region src/constants.d.ts
|
|
7
7
|
/**
|
|
@@ -52,137 +52,6 @@ declare const MAX_FILTER_DEPTH: 10;
|
|
|
52
52
|
*/
|
|
53
53
|
declare const RESERVED_QUERY_PARAMS: Readonly<Set<string>>;
|
|
54
54
|
//#endregion
|
|
55
|
-
//#region src/core/createActionRouter.d.ts
|
|
56
|
-
/**
|
|
57
|
-
* Action handler function
|
|
58
|
-
* @param id - Resource ID
|
|
59
|
-
* @param data - Action-specific data from request body
|
|
60
|
-
* @param req - Full Fastify request object
|
|
61
|
-
* @returns Action result (will be wrapped in success response)
|
|
62
|
-
*/
|
|
63
|
-
type ActionHandler<TData = Record<string, unknown>, TResult = unknown> = (id: string, data: TData, req: RequestWithExtras) => Promise<TResult>;
|
|
64
|
-
/**
|
|
65
|
-
* Action router configuration
|
|
66
|
-
*/
|
|
67
|
-
interface ActionRouterConfig {
|
|
68
|
-
/**
|
|
69
|
-
* OpenAPI tag for grouping routes
|
|
70
|
-
*/
|
|
71
|
-
readonly tag?: string;
|
|
72
|
-
/**
|
|
73
|
-
* Action handlers map
|
|
74
|
-
* @example { approve: (id, data, req) => service.approve(id), ... }
|
|
75
|
-
*/
|
|
76
|
-
readonly actions: Record<string, ActionHandler>;
|
|
77
|
-
/**
|
|
78
|
-
* Per-action permission checks (PermissionCheck functions)
|
|
79
|
-
* @example { approve: requireRoles(['admin', 'manager']), cancel: requireRoles(['admin']) }
|
|
80
|
-
*/
|
|
81
|
-
readonly actionPermissions?: Record<string, PermissionCheck>;
|
|
82
|
-
/**
|
|
83
|
-
* Per-action schema for body validation.
|
|
84
|
-
*
|
|
85
|
-
* Accepted shapes per action:
|
|
86
|
-
*
|
|
87
|
-
* 1. **Full JSON Schema object** with `type: 'object'`, `properties`, `required` —
|
|
88
|
-
* used verbatim. Required fields ARE enforced by Fastify's AJV.
|
|
89
|
-
*
|
|
90
|
-
* 2. **Zod v4 schema** — auto-converted via `z.toJSONSchema()`. Required fields
|
|
91
|
-
* ARE enforced.
|
|
92
|
-
*
|
|
93
|
-
* 3. **Legacy field map** `{ fieldName: { type: 'string' } }` — each key becomes
|
|
94
|
-
* a property and is treated as REQUIRED unless the property schema has
|
|
95
|
-
* `nullable: true` or Arc's sentinel `required: false`. Kept for back-compat.
|
|
96
|
-
*
|
|
97
|
-
* All shapes are compiled into a single `oneOf` discriminator body schema
|
|
98
|
-
* so AJV can validate action-specific required fields at the HTTP layer.
|
|
99
|
-
*
|
|
100
|
-
* @example JSON Schema
|
|
101
|
-
* ```ts
|
|
102
|
-
* actionSchemas: {
|
|
103
|
-
* dispatch: {
|
|
104
|
-
* type: 'object',
|
|
105
|
-
* properties: { carrier: { type: 'string' } },
|
|
106
|
-
* required: ['carrier'],
|
|
107
|
-
* },
|
|
108
|
-
* }
|
|
109
|
-
* ```
|
|
110
|
-
*
|
|
111
|
-
* @example Zod v4
|
|
112
|
-
* ```ts
|
|
113
|
-
* actionSchemas: {
|
|
114
|
-
* dispatch: z.object({ carrier: z.string() }),
|
|
115
|
-
* }
|
|
116
|
-
* ```
|
|
117
|
-
*/
|
|
118
|
-
readonly actionSchemas?: Record<string, Record<string, unknown>>;
|
|
119
|
-
/**
|
|
120
|
-
* Global permission check applied to all actions (if action-specific not defined)
|
|
121
|
-
*/
|
|
122
|
-
readonly globalAuth?: PermissionCheck;
|
|
123
|
-
/**
|
|
124
|
-
* Optional idempotency service
|
|
125
|
-
* If provided, will handle idempotency-key header
|
|
126
|
-
*/
|
|
127
|
-
readonly idempotencyService?: IdempotencyService;
|
|
128
|
-
/**
|
|
129
|
-
* Custom error handler for action execution failures
|
|
130
|
-
* @param error - The error thrown by action handler
|
|
131
|
-
* @param action - The action that failed
|
|
132
|
-
* @param id - The resource ID
|
|
133
|
-
* @returns Status code and error response
|
|
134
|
-
*/
|
|
135
|
-
readonly onError?: (error: Error, action: string, id: string) => {
|
|
136
|
-
statusCode: number;
|
|
137
|
-
error: string;
|
|
138
|
-
code?: string;
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Idempotency service interface
|
|
143
|
-
* Apps can provide their own implementation
|
|
144
|
-
*/
|
|
145
|
-
interface IdempotencyService {
|
|
146
|
-
check(key: string, payload: unknown): Promise<{
|
|
147
|
-
isNew: boolean;
|
|
148
|
-
existingResult?: unknown;
|
|
149
|
-
}>;
|
|
150
|
-
complete(key: string | undefined, result: unknown): Promise<void>;
|
|
151
|
-
fail(key: string | undefined, error: Error): Promise<void>;
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* Create action-based state transition endpoint
|
|
155
|
-
*
|
|
156
|
-
* Registers: POST /:id/action
|
|
157
|
-
* Body: { action: string, ...actionData }
|
|
158
|
-
*
|
|
159
|
-
* @param fastify - Fastify instance
|
|
160
|
-
* @param config - Action router configuration
|
|
161
|
-
*/
|
|
162
|
-
declare function createActionRouter(fastify: FastifyInstance, config: ActionRouterConfig): void;
|
|
163
|
-
/**
|
|
164
|
-
* Build a discriminated body schema for the unified action endpoint.
|
|
165
|
-
*
|
|
166
|
-
* Produces a schema of the form:
|
|
167
|
-
* ```json
|
|
168
|
-
* {
|
|
169
|
-
* "type": "object",
|
|
170
|
-
* "required": ["action"],
|
|
171
|
-
* "oneOf": [
|
|
172
|
-
* { "properties": { "action": { "const": "dispatch" }, "carrier": {...} }, "required": ["action", "carrier"] },
|
|
173
|
-
* { "properties": { "action": { "const": "approve" } }, "required": ["action"] }
|
|
174
|
-
* ]
|
|
175
|
-
* }
|
|
176
|
-
* ```
|
|
177
|
-
*
|
|
178
|
-
* AJV validates this natively, so an action call missing required fields is
|
|
179
|
-
* rejected with HTTP 400 before the handler ever runs.
|
|
180
|
-
*
|
|
181
|
-
* Exported so OpenAPI generation and MCP tool generation can reuse the same
|
|
182
|
-
* schema shape (single source of truth).
|
|
183
|
-
*/
|
|
184
|
-
declare function buildActionBodySchema(actionEnum: readonly string[], actionSchemas?: Record<string, Record<string, unknown>>): Record<string, unknown>;
|
|
185
|
-
//#endregion
|
|
186
55
|
//#region src/core/createCrudRouter.d.ts
|
|
187
56
|
/**
|
|
188
57
|
* Create CRUD routes for a controller
|
|
@@ -309,4 +178,4 @@ declare function createCrudHandlers<TDoc>(controller: IController<TDoc>): {
|
|
|
309
178
|
delete: (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
|
310
179
|
};
|
|
311
180
|
//#endregion
|
|
312
|
-
export {
|
|
181
|
+
export { MAX_REGEX_LENGTH as C, RESERVED_QUERY_PARAMS as D, MutationOperation as E, SYSTEM_FIELDS as O, MAX_FILTER_DEPTH as S, MUTATION_OPERATIONS as T, DEFAULT_UPDATE_METHOD as _, getControllerScope as a, HookOperation as b, createCrudRouter as c, CrudOperation as d, DEFAULT_ID_FIELD as f, DEFAULT_TENANT_FIELD as g, DEFAULT_SORT as h, getControllerContext as i, createPermissionMiddleware as l, DEFAULT_MAX_LIMIT as m, createFastifyHandler as n, sendControllerResponse as o, DEFAULT_LIMIT as p, createRequestContext as r, defineResourceVariants as s, createCrudHandlers as t, CRUD_OPERATIONS as u, HOOK_OPERATIONS as v, MAX_SEARCH_LENGTH as w, HookPhase as x, HOOK_PHASES as y };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { J as OpenApiSchemas, X as ParsedQuery, Xt as CrudRepository, _t as RouteSchemaOptions, c as ValidationResult, n as AdapterSchemaContext, o as RepositoryLike, r as DataAdapter, s as SchemaMetadata, tt as QueryParserInterface } from "./interface-YrWsmKqE.mjs";
|
|
2
2
|
import { Model } from "mongoose";
|
|
3
3
|
|
|
4
4
|
//#region src/adapters/mongoose.d.ts
|
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { $ as
|
|
2
|
-
import { a as applyFieldWritePermissions, i as applyFieldReadPermissions, n as FieldPermissionMap, o as fields, t as FieldPermission } from "./fields-
|
|
3
|
-
import { i as UserBase, n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "./types-
|
|
4
|
-
import { l as createMongooseAdapter, o as createPrismaAdapter, s as MongooseAdapter, t as PrismaAdapter } from "./index-
|
|
5
|
-
import {
|
|
6
|
-
import { C as authenticated, D as publicRead, E as presets_d_exports, O as publicReadAdminWrite, S as adminOnly, T as ownerWithAdminBypass, _ as requireScopeContext, a as allOf, c as createDynamicPermissionMatrix, d as requireAuth, f as requireOrgInScope, g as requireRoles, h as requireOwnership, k as readOnly, l as createOrgPermissions, m as requireOrgRole, n as DynamicPermissionMatrix, o as allowPublic, p as requireOrgMembership, r as DynamicPermissionMatrixConfig, s as anyOf, u as denyAll, v as requireServiceScope, w as fullPublic, x as when, y as requireTeamMembership } from "./index-
|
|
7
|
-
import { a as NotFoundError, d as ValidationError, f as createDomainError, i as ForbiddenError, t as ArcError, u as UnauthorizedError } from "./errors-
|
|
1
|
+
import { $t as DeleteResult, A as FastifyRequestExtras, At as BaseControllerOptions, B as IntrospectionData, Bt as IController, D as CrudSchemas, Dt as envelope, E as CrudRouterOptions, Et as ValidationResult$1, F as HealthCheck, G as MiddlewareConfig, H as JWTPayload, Ht as IRequestContext, I as HealthOptions, Jt as BulkWriteOperation, K as MiddlewareHandler, Kt as ResourceDefinition, L as InferAdapterDoc, M as FastifyWithDecorators, N as FieldRule, O as EventDefinition, P as GracefulShutdownOptions, Q as PresetFunction, Qt as DeleteOptions, R as InferDocType, Rt as ControllerLike, S as ConfigError, St as TypedResourceConfig, T as CrudRouteKey, Tt as ValidateOptions, Ut as RouteHandler, V as IntrospectionPluginOptions, Vt as IControllerResponse, Xt as CrudRepository, Y as OwnershipCheck, Yt as BulkWriteResult, Zt as DeleteManyResult, _ as ArcRequest, _n as PipelineStep, _t as RouteSchemaOptions, a as RelationMetadata, an as PaginationParams, at as RequestContext, bt as TypedController, c as ValidationResult, cn as RepositorySession, dn as Guard, et as PresetResult, fn as Interceptor, ft as ResourceMetadata, g as ArcInternalMetadata, gn as PipelineContext, hn as PipelineConfig, ht as RouteHandlerMethod, i as FieldMetadata, in as PaginatedResult, it as RegistryStats, j as FastifyWithAuth, kt as BaseController, ln as UpdateManyResult, lt as ResourceConfig, m as ApiResponse, mn as OperationFilter, nn as KeysetPaginatedResult, nt as RateLimitConfig, o as RepositoryLike, on as PaginationResult, ot as RequestIdOptions, p as AnyRecord, pn as NextFunction, qt as defineResource, r as DataAdapter, rn as OffsetPaginatedResult, rt as RegistryEntry, s as SchemaMetadata, sn as QueryOptions, st as RequestWithExtras, un as WriteOptions, vn as Transform, vt as ServiceContext, w as CrudController, wt as UserOrganization, xt as TypedRepository, y as AuthPluginOptions, z as InferResourceDoc } from "./interface-YrWsmKqE.mjs";
|
|
2
|
+
import { a as applyFieldWritePermissions, i as applyFieldReadPermissions, n as FieldPermissionMap, o as fields, t as FieldPermission } from "./fields-BC7zcmI9.mjs";
|
|
3
|
+
import { i as UserBase, n as PermissionContext, r as PermissionResult, t as PermissionCheck } from "./types-DZi1aYhm.mjs";
|
|
4
|
+
import { l as createMongooseAdapter, o as createPrismaAdapter, s as MongooseAdapter, t as PrismaAdapter } from "./index-CtGKT0lf.mjs";
|
|
5
|
+
import { C as MAX_REGEX_LENGTH, D as RESERVED_QUERY_PARAMS, E as MutationOperation, O as SYSTEM_FIELDS, S as MAX_FILTER_DEPTH, T as MUTATION_OPERATIONS, _ as DEFAULT_UPDATE_METHOD, a as getControllerScope, b as HookOperation, d as CrudOperation, f as DEFAULT_ID_FIELD, g as DEFAULT_TENANT_FIELD, h as DEFAULT_SORT, m as DEFAULT_MAX_LIMIT, p as DEFAULT_LIMIT, s as defineResourceVariants, u as CRUD_OPERATIONS, v as HOOK_OPERATIONS, w as MAX_SEARCH_LENGTH, x as HookPhase, y as HOOK_PHASES } from "./index-Cibkchnx.mjs";
|
|
6
|
+
import { C as authenticated, D as publicRead, E as presets_d_exports, O as publicReadAdminWrite, S as adminOnly, T as ownerWithAdminBypass, _ as requireScopeContext, a as allOf, c as createDynamicPermissionMatrix, d as requireAuth, f as requireOrgInScope, g as requireRoles, h as requireOwnership, k as readOnly, l as createOrgPermissions, m as requireOrgRole, n as DynamicPermissionMatrix, o as allowPublic, p as requireOrgMembership, r as DynamicPermissionMatrixConfig, s as anyOf, u as denyAll, v as requireServiceScope, w as fullPublic, x as when, y as requireTeamMembership } from "./index-C-xjcA6F.mjs";
|
|
7
|
+
import { a as NotFoundError, d as ValidationError, f as createDomainError, i as ForbiddenError, t as ArcError, u as UnauthorizedError } from "./errors-BI8kEKsO.mjs";
|
|
8
8
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
9
9
|
import { RouteHandlerMethod as RouteHandlerMethod$1 } from "fastify";
|
|
10
10
|
|
|
@@ -253,4 +253,4 @@ declare function arcLog(module: string): ArcLogger;
|
|
|
253
253
|
//#region src/index.d.ts
|
|
254
254
|
declare const version: string;
|
|
255
255
|
//#endregion
|
|
256
|
-
export { type ValidationResult as AdapterValidationResult, type
|
|
256
|
+
export { type ValidationResult as AdapterValidationResult, type AnyRecord, type ApiResponse, ArcError, type ArcInternalMetadata, type ArcLogWriter, type ArcLogger, type ArcLoggerOptions, type ArcRequest, type AuthPluginOptions, BaseController, type BaseControllerOptions, type BulkWriteOperation, type BulkWriteResult, CRUD_OPERATIONS, type ConfigError, type ControllerLike, type CrudController, CrudOperation, type CrudRepository, type CrudRouteKey, type CrudRouterOptions, type CrudSchemas, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, type DataAdapter, type DeleteManyResult, type DeleteOptions, type DeleteResult, type DynamicPermissionMatrix, type DynamicPermissionMatrixConfig, type EventDefinition, type FastifyRequestExtras, type FastifyWithAuth, type FastifyWithDecorators, type FieldMetadata, type FieldPermission, type FieldPermissionMap, type FieldRule, ForbiddenError, type GracefulShutdownOptions, type Guard, HOOK_OPERATIONS, HOOK_PHASES, type HealthCheck, type HealthOptions, HookOperation, HookPhase, type IController, type IControllerResponse, type IRequestContext, type InferAdapterDoc, type InferDocType, type InferResourceDoc, type Interceptor, type IntrospectionData, type IntrospectionPluginOptions, type JWTPayload, type KeysetPaginatedResult, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, type MiddlewareConfig, MongooseAdapter, MutationOperation, type NamedMiddleware, NotFoundError, type OffsetPaginatedResult, type OwnershipCheck, type PaginatedResult, type PaginationParams, type PaginationResult, type PermissionCheck, type PermissionContext, type PermissionResult, type PipelineConfig, type PipelineContext, type PipelineStep, type PresetFunction, type PresetResult, PrismaAdapter, type QueryOptions, RESERVED_QUERY_PARAMS, type RateLimitConfig, type RegistryEntry, type RegistryStats, type RelationMetadata, type RepositoryLike, type RepositorySession, type RequestContext, type RequestIdOptions, type RequestStore, type RequestWithExtras, type ResourceConfig, ResourceDefinition, type ResourceMetadata, type RouteHandler, type RouteHandlerMethod, type RouteSchemaOptions, SYSTEM_FIELDS, type SchemaMetadata, type ServiceContext, type Transform, type TypedController, type TypedRepository, type TypedResourceConfig, UnauthorizedError, type UpdateManyResult, type UserBase, type UserOrganization, type ValidateOptions, ValidationError, type ValidationResult$1 as ValidationResult, type WriteOptions, adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, arcLog, assertValidConfig, authenticated, configureArcLogger, createDomainError, createDynamicPermissionMatrix, createMongooseAdapter, createOrgPermissions, createPrismaAdapter, defineResource, defineResourceVariants, denyAll, envelope, fields, formatValidationErrors, fullPublic, getControllerScope, guard, intercept, middleware, ownerWithAdminBypass, presets_d_exports as permissions, pipe, publicRead, publicReadAdminWrite, readOnly, requestContext, requireAuth, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, sortMiddlewares, transform, validateResourceConfig, version, when };
|
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { a as DEFAULT_SORT, c as HOOK_OPERATIONS, d as MAX_REGEX_LENGTH, f as MAX_SEARCH_LENGTH, h as SYSTEM_FIELDS, i as DEFAULT_MAX_LIMIT, l as HOOK_PHASES, m as RESERVED_QUERY_PARAMS, n as DEFAULT_ID_FIELD, o as DEFAULT_TENANT_FIELD, p as MUTATION_OPERATIONS, r as DEFAULT_LIMIT, s as DEFAULT_UPDATE_METHOD, t as CRUD_OPERATIONS, u as MAX_FILTER_DEPTH } from "./constants-Cxde4rpC.mjs";
|
|
2
2
|
import { a as createMongooseAdapter, i as MongooseAdapter, r as createPrismaAdapter, t as PrismaAdapter } from "./adapters-BBqAVvPK.mjs";
|
|
3
|
-
import { t as BaseController } from "./BaseController-
|
|
3
|
+
import { t as BaseController } from "./BaseController-Vu2yc56T.mjs";
|
|
4
4
|
import { envelope } from "./types/index.mjs";
|
|
5
|
-
import { n as applyFieldWritePermissions, r as fields, t as applyFieldReadPermissions } from "./fields-
|
|
6
|
-
import { t as
|
|
7
|
-
import {
|
|
8
|
-
import { a as validateResourceConfig, f as getControllerScope, i as formatValidationErrors, m as pipe, n as defineResource, r as assertValidConfig, t as ResourceDefinition } from "./defineResource-
|
|
9
|
-
import { C as publicRead, S as presets_exports, T as readOnly, _ as when, a as createOrgPermissions, b as fullPublic, c as requireOrgInScope, d as requireOwnership, f as requireRoles, h as requireTeamMembership, i as createDynamicPermissionMatrix, l as requireOrgMembership, m as requireServiceScope, n as allowPublic, o as denyAll, p as requireScopeContext, r as anyOf, s as requireAuth, t as allOf, u as requireOrgRole, v as adminOnly, w as publicReadAdminWrite, x as ownerWithAdminBypass, y as authenticated } from "./permissions-
|
|
10
|
-
import { t as defineResourceVariants } from "./core-
|
|
5
|
+
import { n as applyFieldWritePermissions, r as fields, t as applyFieldReadPermissions } from "./fields-CU6FlaDV.mjs";
|
|
6
|
+
import { d as createDomainError, i as NotFoundError, l as UnauthorizedError, r as ForbiddenError, t as ArcError, u as ValidationError } from "./errors-CqWnSqM-.mjs";
|
|
7
|
+
import { t as requestContext } from "./requestContext-DYtmNpm5.mjs";
|
|
8
|
+
import { a as validateResourceConfig, f as getControllerScope, i as formatValidationErrors, m as pipe, n as defineResource, r as assertValidConfig, t as ResourceDefinition } from "./defineResource-C__jkwvs.mjs";
|
|
9
|
+
import { C as publicRead, S as presets_exports, T as readOnly, _ as when, a as createOrgPermissions, b as fullPublic, c as requireOrgInScope, d as requireOwnership, f as requireRoles, h as requireTeamMembership, i as createDynamicPermissionMatrix, l as requireOrgMembership, m as requireServiceScope, n as allowPublic, o as denyAll, p as requireScopeContext, r as anyOf, s as requireAuth, t as allOf, u as requireOrgRole, v as adminOnly, w as publicReadAdminWrite, x as ownerWithAdminBypass, y as authenticated } from "./permissions-oNZawnkR.mjs";
|
|
10
|
+
import { t as defineResourceVariants } from "./core-DNncu0xF.mjs";
|
|
11
11
|
import { n as configureArcLogger, t as arcLog } from "./logger-CDjpjySd.mjs";
|
|
12
12
|
//#region src/middleware/middleware.ts
|
|
13
13
|
/**
|
|
@@ -128,6 +128,6 @@ function transform(name, handlerOrOptions) {
|
|
|
128
128
|
}
|
|
129
129
|
//#endregion
|
|
130
130
|
//#region src/index.ts
|
|
131
|
-
const version = "2.
|
|
131
|
+
const version = "2.9.1";
|
|
132
132
|
//#endregion
|
|
133
133
|
export { ArcError, BaseController, CRUD_OPERATIONS, DEFAULT_ID_FIELD, DEFAULT_LIMIT, DEFAULT_MAX_LIMIT, DEFAULT_SORT, DEFAULT_TENANT_FIELD, DEFAULT_UPDATE_METHOD, ForbiddenError, HOOK_OPERATIONS, HOOK_PHASES, MAX_FILTER_DEPTH, MAX_REGEX_LENGTH, MAX_SEARCH_LENGTH, MUTATION_OPERATIONS, MongooseAdapter, NotFoundError, PrismaAdapter, RESERVED_QUERY_PARAMS, ResourceDefinition, SYSTEM_FIELDS, UnauthorizedError, ValidationError, adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, arcLog, assertValidConfig, authenticated, configureArcLogger, createDomainError, createDynamicPermissionMatrix, createMongooseAdapter, createOrgPermissions, createPrismaAdapter, defineResource, defineResourceVariants, denyAll, envelope, fields, formatValidationErrors, fullPublic, getControllerScope, guard, intercept, middleware, ownerWithAdminBypass, presets_exports as permissions, pipe, publicRead, publicReadAdminWrite, readOnly, requestContext, requireAuth, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, sortMiddlewares, transform, validateResourceConfig, version, when };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as DomainEvent } from "../EventTransport-CqZ8FyM_.mjs";
|
|
2
2
|
import { WebSocketClient, WebSocketMessage } from "./websocket.mjs";
|
|
3
3
|
import { FastifyPluginAsync, FastifyRequest } from "fastify";
|
|
4
4
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { WebSocketClient, WebSocketMessage, WebSocketPluginOptions } from "./websocket.mjs";
|
|
2
2
|
import { EventGatewayOptions } from "./event-gateway.mjs";
|
|
3
3
|
import { JobDefinition, JobDispatchOptions, JobDispatcher, JobMeta, JobsPluginOptions, QueueStats } from "./jobs.mjs";
|
|
4
|
-
import { c as McpResourceConfig, f as ToolAnnotations, i as CrudOperation, l as PromptDefinition, m as ToolDefinition, n as CallToolResult, o as McpAuthResult, p as ToolContext, r as CreateMcpServerConfig, s as McpPluginOptions, t as BetterAuthHandler } from "../types-
|
|
4
|
+
import { c as McpResourceConfig, f as ToolAnnotations, i as CrudOperation, l as PromptDefinition, m as ToolDefinition, n as CallToolResult, o as McpAuthResult, p as ToolContext, r as CreateMcpServerConfig, s as McpPluginOptions, t as BetterAuthHandler } from "../types-CoSzA-s-.mjs";
|
|
5
5
|
import { StreamlinePluginOptions, WorkflowLike, WorkflowRunLike } from "./streamline.mjs";
|
|
6
6
|
import { WebhookDeliveryRecord, WebhookManager, WebhookPluginOptions, WebhookStore, WebhookSubscription } from "./webhooks.mjs";
|
|
7
7
|
export { type BetterAuthHandler, type CallToolResult, type CreateMcpServerConfig, type CrudOperation, type EventGatewayOptions, type JobDefinition, type JobDispatchOptions, type JobDispatcher, type JobMeta, type JobsPluginOptions, type McpAuthResult, type McpPluginOptions, type McpResourceConfig, type PromptDefinition, type QueueStats, type StreamlinePluginOptions, type ToolAnnotations, type ToolContext, type ToolDefinition, type WebSocketClient, type WebSocketMessage, type WebSocketPluginOptions, type WebhookDeliveryRecord, type WebhookManager, type WebhookPluginOptions, type WebhookStore, type WebhookSubscription, type WorkflowLike, type WorkflowRunLike };
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { FastifyPluginAsync } from "fastify";
|
|
2
2
|
|
|
3
3
|
//#region src/integrations/jobs.d.ts
|
|
4
|
+
/** Repeat schedule — cron pattern or fixed interval. Explicit timezone is required. */
|
|
5
|
+
interface JobRepeatOptions {
|
|
6
|
+
/** Cron pattern (e.g. '0 9 * * *' = every day 09:00). Mutually exclusive with `every`. */
|
|
7
|
+
pattern?: string;
|
|
8
|
+
/** Fixed interval in ms. Mutually exclusive with `pattern`. */
|
|
9
|
+
every?: number;
|
|
10
|
+
/** IANA timezone (e.g. 'UTC', 'America/New_York'). Required for `pattern` — prevents DST drift. */
|
|
11
|
+
tz?: string;
|
|
12
|
+
/** Stop repeating after this date. */
|
|
13
|
+
endDate?: Date | string | number;
|
|
14
|
+
/** Max total runs. */
|
|
15
|
+
limit?: number;
|
|
16
|
+
}
|
|
4
17
|
interface JobDefinition<TData = unknown, TResult = unknown> {
|
|
5
18
|
/** Unique job name */
|
|
6
19
|
name: string;
|
|
@@ -22,8 +35,10 @@ interface JobDefinition<TData = unknown, TResult = unknown> {
|
|
|
22
35
|
max: number;
|
|
23
36
|
duration: number;
|
|
24
37
|
};
|
|
25
|
-
/** Dead letter queue name (default: '{name}
|
|
38
|
+
/** Dead letter queue name (default: '{name}-dead') */
|
|
26
39
|
deadLetterQueue?: string;
|
|
40
|
+
/** Repeat schedule — cron or interval. Requires explicit timezone for cron. */
|
|
41
|
+
repeat?: JobRepeatOptions;
|
|
27
42
|
}
|
|
28
43
|
interface JobMeta {
|
|
29
44
|
jobId: string;
|
|
@@ -41,6 +56,8 @@ interface JobDispatchOptions {
|
|
|
41
56
|
removeOnComplete?: boolean | number;
|
|
42
57
|
/** Remove job after failure */
|
|
43
58
|
removeOnFail?: boolean | number;
|
|
59
|
+
/** One-shot repeat override at dispatch time. Usually prefer `JobDefinition.repeat`. */
|
|
60
|
+
repeat?: JobRepeatOptions;
|
|
44
61
|
}
|
|
45
62
|
interface JobsPluginOptions {
|
|
46
63
|
/** Redis connection options (passed to BullMQ) */
|
|
@@ -97,7 +114,12 @@ interface QueueStats {
|
|
|
97
114
|
* });
|
|
98
115
|
*/
|
|
99
116
|
declare function defineJob<TData = unknown, TResult = unknown>(definition: JobDefinition<TData, TResult>): JobDefinition<TData, TResult>;
|
|
100
|
-
/**
|
|
117
|
+
/**
|
|
118
|
+
* Pluggable BullMQ job queue integration for Arc.
|
|
119
|
+
*
|
|
120
|
+
* Wrapped with fastify-plugin so the `fastify.jobs` decorator is available
|
|
121
|
+
* in the outer scope (the documented `fastify.jobs.dispatch(...)` usage).
|
|
122
|
+
*/
|
|
101
123
|
declare const jobsPlugin: FastifyPluginAsync<JobsPluginOptions>;
|
|
102
124
|
//#endregion
|
|
103
|
-
export { JobDefinition, JobDispatchOptions, JobDispatcher, JobMeta, JobsPluginOptions, QueueStats, defineJob, jobsPlugin };
|
|
125
|
+
export { JobDefinition, JobDispatchOptions, JobDispatcher, JobMeta, JobRepeatOptions, JobsPluginOptions, QueueStats, defineJob, jobsPlugin };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fp from "fastify-plugin";
|
|
1
2
|
//#region src/integrations/jobs.ts
|
|
2
3
|
/**
|
|
3
4
|
* Define a background job with typed data and configuration.
|
|
@@ -26,16 +27,41 @@ const jobsPluginImpl = async (fastify, options) => {
|
|
|
26
27
|
} catch {
|
|
27
28
|
throw new Error("@classytic/arc/integrations/jobs requires \"bullmq\" package.\nInstall it: npm install bullmq");
|
|
28
29
|
}
|
|
30
|
+
if (connection && typeof connection === "object" && "options" in connection) {
|
|
31
|
+
if (connection.options?.maxRetriesPerRequest !== null) fastify.log.warn("[arc/jobs] BullMQ requires ioredis `maxRetriesPerRequest: null`. Pass `new Redis(url, { maxRetriesPerRequest: null, enableReadyCheck: false })` or workers will stall on transient Redis errors.");
|
|
32
|
+
}
|
|
29
33
|
const queues = /* @__PURE__ */ new Map();
|
|
30
34
|
const dlqQueues = /* @__PURE__ */ new Map();
|
|
31
35
|
const workers = /* @__PURE__ */ new Map();
|
|
36
|
+
for (const job of jobs) {
|
|
37
|
+
if (!job.repeat) continue;
|
|
38
|
+
const { pattern, every, tz } = job.repeat;
|
|
39
|
+
if (pattern && every) throw new Error(`[arc/jobs] Job '${job.name}' sets both repeat.pattern and repeat.every — use one.`);
|
|
40
|
+
if (!pattern && every == null) throw new Error(`[arc/jobs] Job '${job.name}' has repeat config but no pattern or every.`);
|
|
41
|
+
if (pattern && !tz) throw new Error(`[arc/jobs] Job '${job.name}' uses a cron pattern but no timezone. Set repeat.tz (e.g. 'UTC' or 'America/New_York') to avoid DST drift.`);
|
|
42
|
+
}
|
|
32
43
|
for (const job of jobs) {
|
|
33
44
|
const queueName = job.name;
|
|
34
45
|
const queue = new Queue(queueName, { connection });
|
|
35
46
|
queues.set(queueName, queue);
|
|
47
|
+
if (job.repeat) {
|
|
48
|
+
const repeatOpts = {
|
|
49
|
+
...job.repeat.pattern ? {
|
|
50
|
+
pattern: job.repeat.pattern,
|
|
51
|
+
tz: job.repeat.tz
|
|
52
|
+
} : { every: job.repeat.every },
|
|
53
|
+
...job.repeat.endDate ? { endDate: job.repeat.endDate } : {},
|
|
54
|
+
...job.repeat.limit != null ? { limit: job.repeat.limit } : {}
|
|
55
|
+
};
|
|
56
|
+
await queue.add(queueName, {}, {
|
|
57
|
+
repeat: repeatOpts,
|
|
58
|
+
removeOnComplete: defaults.removeOnComplete ?? 100,
|
|
59
|
+
removeOnFail: defaults.removeOnFail ?? 500
|
|
60
|
+
});
|
|
61
|
+
}
|
|
36
62
|
let dlqQueue = null;
|
|
37
63
|
if (job.deadLetterQueue != null) {
|
|
38
|
-
const dlqName = job.deadLetterQueue || `${queueName}
|
|
64
|
+
const dlqName = job.deadLetterQueue || `${queueName}-dead`;
|
|
39
65
|
dlqQueue = new Queue(dlqName, { connection });
|
|
40
66
|
dlqQueues.set(dlqName, dlqQueue);
|
|
41
67
|
}
|
|
@@ -110,12 +136,35 @@ const jobsPluginImpl = async (fastify, options) => {
|
|
|
110
136
|
}, `Failed to publish job.${queueName}.failed event`);
|
|
111
137
|
}
|
|
112
138
|
});
|
|
139
|
+
worker.on("stalled", async (jobId) => {
|
|
140
|
+
fastify.log.warn({
|
|
141
|
+
jobId,
|
|
142
|
+
queue: queueName
|
|
143
|
+
}, "Job stalled — worker may have crashed");
|
|
144
|
+
if (bridgeEvents && fastify.events?.publish) try {
|
|
145
|
+
await fastify.events.publish(`job.${queueName}.stalled`, { jobId });
|
|
146
|
+
} catch (err) {
|
|
147
|
+
fastify.log.warn({
|
|
148
|
+
err,
|
|
149
|
+
jobId
|
|
150
|
+
}, `Failed to publish job.${queueName}.stalled event`);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
113
153
|
workers.set(queueName, worker);
|
|
114
154
|
}
|
|
155
|
+
const JOB_PAYLOAD_WARN_BYTES = 100 * 1024;
|
|
115
156
|
const dispatcher = {
|
|
116
157
|
async dispatch(name, data, opts = {}) {
|
|
117
158
|
const queue = queues.get(name);
|
|
118
159
|
if (!queue) throw new Error(`Job queue '${name}' not registered. Available: ${Array.from(queues.keys()).join(", ")}`);
|
|
160
|
+
try {
|
|
161
|
+
const serializedBytes = Buffer.byteLength(JSON.stringify(data) ?? "", "utf8");
|
|
162
|
+
if (serializedBytes > JOB_PAYLOAD_WARN_BYTES) fastify.log.warn({
|
|
163
|
+
queue: name,
|
|
164
|
+
bytes: serializedBytes,
|
|
165
|
+
limit: JOB_PAYLOAD_WARN_BYTES
|
|
166
|
+
}, `[arc/jobs] Large job payload — prefer passing IDs and reloading in the handler`);
|
|
167
|
+
} catch {}
|
|
119
168
|
const jobDef = jobs.find((j) => j.name === name);
|
|
120
169
|
return { jobId: (await queue.add(name, data, {
|
|
121
170
|
delay: opts.delay,
|
|
@@ -127,7 +176,8 @@ const jobsPluginImpl = async (fastify, options) => {
|
|
|
127
176
|
backoff: jobDef?.backoff ?? defaults.backoff ?? {
|
|
128
177
|
type: "exponential",
|
|
129
178
|
delay: 1e3
|
|
130
|
-
}
|
|
179
|
+
},
|
|
180
|
+
repeat: jobDef?.repeat ?? opts.repeat
|
|
131
181
|
})).id };
|
|
132
182
|
},
|
|
133
183
|
getQueue(name) {
|
|
@@ -148,6 +198,7 @@ const jobsPluginImpl = async (fastify, options) => {
|
|
|
148
198
|
return stats;
|
|
149
199
|
},
|
|
150
200
|
async close() {
|
|
201
|
+
await Promise.all(Array.from(workers.values()).map((w) => w.pause().catch(() => {})));
|
|
151
202
|
const closePromises = [];
|
|
152
203
|
for (const worker of workers.values()) closePromises.push(worker.close());
|
|
153
204
|
for (const queue of queues.values()) closePromises.push(queue.close());
|
|
@@ -166,7 +217,15 @@ const jobsPluginImpl = async (fastify, options) => {
|
|
|
166
217
|
await dispatcher.close();
|
|
167
218
|
});
|
|
168
219
|
};
|
|
169
|
-
/**
|
|
170
|
-
|
|
220
|
+
/**
|
|
221
|
+
* Pluggable BullMQ job queue integration for Arc.
|
|
222
|
+
*
|
|
223
|
+
* Wrapped with fastify-plugin so the `fastify.jobs` decorator is available
|
|
224
|
+
* in the outer scope (the documented `fastify.jobs.dispatch(...)` usage).
|
|
225
|
+
*/
|
|
226
|
+
const jobsPlugin = fp(jobsPluginImpl, {
|
|
227
|
+
name: "arc-jobs",
|
|
228
|
+
fastify: "5.x"
|
|
229
|
+
});
|
|
171
230
|
//#endregion
|
|
172
231
|
export { defineJob, jobsPlugin };
|