@classytic/arc 2.9.1 → 2.10.8
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 +20 -91
- package/dist/{BaseController-Vu2yc56T.mjs → BaseController-DVNKvoX4.mjs} +154 -170
- package/dist/{ResourceRegistry-Dq3_zBQP.mjs → ResourceRegistry-CcN2LVrc.mjs} +1 -1
- package/dist/actionPermissions-TUVR3uiZ.mjs +22 -0
- package/dist/adapters/index.d.mts +3 -3
- package/dist/adapters/index.mjs +2 -2
- package/dist/{adapters-BBqAVvPK.mjs → adapters-BXY4i-hw.mjs} +210 -41
- package/dist/audit/index.d.mts +38 -3
- package/dist/audit/index.mjs +54 -22
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +3 -3
- package/dist/cache/index.d.mts +17 -15
- package/dist/cache/index.mjs +16 -15
- package/dist/{caching-CjybdRwx.mjs → caching-3h93rkJM.mjs} +8 -3
- package/dist/cli/commands/describe.mjs +1 -1
- package/dist/cli/commands/docs.mjs +2 -2
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/commands/introspect.mjs +1 -1
- package/dist/context/index.d.mts +58 -0
- package/dist/context/index.mjs +2 -0
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +3 -4
- package/dist/{defineResource-C__jkwvs.mjs → core-3MWJosCH.mjs} +174 -94
- package/dist/{createActionRouter-DH1YFL9m.mjs → createActionRouter-C8UUB3Px.mjs} +1 -1
- package/dist/{createApp-CBJUJKGP.mjs → createApp-BwnEAO2h.mjs} +53 -19
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-DxQ6ACbt.mjs → elevation-Dci0AYLT.mjs} +2 -2
- package/dist/errorHandler-2ii4RIYr.d.mts +114 -0
- package/dist/{errorHandler-CZDW4EXS.mjs → errorHandler-CSxe7KIM.mjs} +1 -1
- package/dist/{eventPlugin-Dl7MoVWH.mjs → eventPlugin-ByU4Cv0e.mjs} +1 -1
- package/dist/{eventPlugin-BxvaCIZF.d.mts → eventPlugin-D1ThQ1Pp.d.mts} +1 -1
- package/dist/events/index.d.mts +8 -5
- package/dist/events/index.mjs +87 -52
- 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/{types-DZi1aYhm.d.mts → fields-C8Y0XLAu.d.mts} +122 -2
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.d.mts +5 -2
- package/dist/idempotency/index.mjs +46 -37
- package/dist/{interface-YrWsmKqE.d.mts → index-BGbpGVyM.d.mts} +2107 -2756
- package/dist/{index-CtGKT0lf.d.mts → index-BziRPS4H.d.mts} +81 -7
- package/dist/{index-C-xjcA6F.d.mts → index-C_Noptz-.d.mts} +284 -409
- package/dist/{index-Cibkchnx.d.mts → index-EqQN6p0W.d.mts} +3 -3
- package/dist/index.d.mts +6 -219
- package/dist/index.mjs +10 -131
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +1 -1
- package/dist/integrations/mcp/index.d.mts +2 -2
- package/dist/integrations/mcp/index.mjs +1 -1
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/interface-yhyb_pLY.d.mts +77 -0
- package/dist/logger/index.d.mts +81 -0
- package/dist/{logger-CDjpjySd.mjs → logger/index.mjs} +1 -6
- package/dist/{memory-BFAYkf8H.mjs → memory-DqI-449b.mjs} +23 -8
- package/dist/middleware/index.d.mts +109 -0
- package/dist/middleware/index.mjs +70 -0
- package/dist/multipartBody-CUQGVlM_.mjs +123 -0
- package/dist/{openapi-CXuTG1M9.mjs → openapi-DpNpqBmo.mjs} +9 -7
- package/dist/org/index.d.mts +2 -2
- package/dist/permissions/index.d.mts +3 -4
- package/dist/permissions/index.mjs +5 -5
- package/dist/{permissions-oNZawnkR.mjs → permissions-wkqRwicB.mjs} +315 -397
- package/dist/pipe-CGJxqDGx.mjs +62 -0
- package/dist/pipeline/index.d.mts +62 -0
- package/dist/pipeline/index.mjs +53 -0
- package/dist/plugins/index.d.mts +23 -3
- package/dist/plugins/index.mjs +9 -11
- package/dist/plugins/response-cache.mjs +1 -1
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/presets/filesUpload.d.mts +3 -3
- package/dist/presets/filesUpload.mjs +255 -1
- package/dist/presets/index.d.mts +1 -1
- package/dist/presets/index.mjs +2 -2
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +43 -9
- package/dist/presets/search.d.mts +91 -4
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-hM4WhNWY.mjs → presets-CrwOvuXI.mjs} +1 -1
- package/dist/{queryCachePlugin-DbUVroUG.mjs → queryCachePlugin-ChLNZvFT.mjs} +9 -9
- package/dist/{queryCachePlugin-CnTZZTC5.d.mts → queryCachePlugin-Dumka73q.d.mts} +1 -1
- package/dist/{queryParser-Cs-6SHQK.mjs → queryParser-NR__Qiju.mjs} +69 -2
- package/dist/{redis-stream-Bz-4q96t.d.mts → redis-stream-bkO88VHx.d.mts} +1 -1
- package/dist/registry/index.d.mts +1 -1
- package/dist/registry/index.mjs +1 -1
- package/dist/{requestContext-DYtmNpm5.mjs → requestContext-C38GskNt.mjs} +1 -1
- package/dist/{resourceToTools-C3cWymnW.mjs → resourceToTools-BhF3JV5p.mjs} +8 -3
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +2 -2
- package/dist/{sse-CJpt7LGI.mjs → sse-D8UeDwis.mjs} +1 -1
- package/dist/{store-helpers-DFiZl5TL.mjs → store-helpers-DYYUQbQN.mjs} +4 -0
- package/dist/testing/index.d.mts +6 -5
- package/dist/testing/index.mjs +17 -10
- package/dist/types/index.d.mts +5 -5
- package/dist/types/index.mjs +1 -31
- package/dist/types-CDnTEpga.mjs +27 -0
- package/dist/{types-CoSzA-s-.d.mts → types-CVKBssX5.d.mts} +1 -1
- package/dist/{types-CunEX4UX.d.mts → types-CVdgPXBW.d.mts} +20 -7
- package/dist/utils/index.d.mts +277 -3
- package/dist/utils/index.mjs +4 -5
- package/dist/{utils-B7FuRr9w.mjs → utils-LMwVidKy.mjs} +303 -2
- package/dist/{versioning-Cm8qoFDg.mjs → versioning-B6mimogM.mjs} +3 -5
- package/dist/versioning-CeUXHfjw.d.mts +117 -0
- package/package.json +31 -18
- package/skills/arc/SKILL.md +8 -12
- package/skills/arc/references/production.md +0 -41
- package/dist/circuitBreaker-CvXkjfrW.d.mts +0 -206
- package/dist/circuitBreaker-l18oRgL5.mjs +0 -284
- package/dist/core-DNncu0xF.mjs +0 -34
- package/dist/dynamic/index.d.mts +0 -93
- package/dist/dynamic/index.mjs +0 -122
- package/dist/errorHandler-DixGcttC.d.mts +0 -218
- package/dist/fields-BC7zcmI9.d.mts +0 -121
- package/dist/filesUpload-q8oHt--L.mjs +0 -377
- package/dist/interface-DplgQO2e.d.mts +0 -54
- package/dist/policies/index.d.mts +0 -425
- package/dist/policies/index.mjs +0 -318
- package/dist/rpc/index.d.mts +0 -90
- package/dist/rpc/index.mjs +0 -248
- /package/dist/{EventTransport-CqZ8FyM_.d.mts → EventTransport-CfVEGaEl.d.mts} +0 -0
- /package/dist/{applyPermissionResult-bqGpo9ML.mjs → applyPermissionResult-QhV1Pa-g.mjs} +0 -0
- /package/dist/{constants-Cxde4rpC.mjs → constants-BhY1OHoH.mjs} +0 -0
- /package/dist/{elevation-B6S5csVA.d.mts → elevation-s5ykdNHr.d.mts} +0 -0
- /package/dist/{errors-CqWnSqM-.mjs → errors-BqdUDja_.mjs} +0 -0
- /package/dist/{fields-CU6FlaDV.mjs → fields-CTMWOUDt.mjs} +0 -0
- /package/dist/{keys-qcD-TVJl.mjs → keys-nWQGUTu1.mjs} +0 -0
- /package/dist/{types-ZUu_h0jp.mjs → types-D57iXYb8.mjs} +0 -0
- /package/dist/{types-BD85MlEK.d.mts → types-tgR4Pt8F.d.mts} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r as RequestScope } from "./types-
|
|
1
|
+
import { r as RequestScope } from "./types-tgR4Pt8F.mjs";
|
|
2
2
|
import { FastifyRequest } from "fastify";
|
|
3
3
|
|
|
4
4
|
//#region src/permissions/types.d.ts
|
|
@@ -175,4 +175,124 @@ interface PermissionCheckMeta {
|
|
|
175
175
|
_orgInScopeTarget?: string | ((ctx: PermissionContext) => string | undefined);
|
|
176
176
|
}
|
|
177
177
|
//#endregion
|
|
178
|
-
|
|
178
|
+
//#region src/permissions/fields.d.ts
|
|
179
|
+
/**
|
|
180
|
+
* Field-Level Permissions
|
|
181
|
+
*
|
|
182
|
+
* Control field visibility and writability per role.
|
|
183
|
+
* Integrated into the response path (read) and sanitization path (write).
|
|
184
|
+
*
|
|
185
|
+
* @example
|
|
186
|
+
* ```typescript
|
|
187
|
+
* import { fields, defineResource } from '@classytic/arc';
|
|
188
|
+
*
|
|
189
|
+
* const userResource = defineResource({
|
|
190
|
+
* name: 'user',
|
|
191
|
+
* adapter: userAdapter,
|
|
192
|
+
* fields: {
|
|
193
|
+
* salary: fields.visibleTo(['admin', 'hr']),
|
|
194
|
+
* internalNotes: fields.writableBy(['admin']),
|
|
195
|
+
* email: fields.redactFor(['viewer']),
|
|
196
|
+
* password: fields.hidden(),
|
|
197
|
+
* },
|
|
198
|
+
* });
|
|
199
|
+
* ```
|
|
200
|
+
*/
|
|
201
|
+
type FieldPermissionType = "hidden" | "visibleTo" | "writableBy" | "redactFor";
|
|
202
|
+
interface FieldPermission {
|
|
203
|
+
readonly _type: FieldPermissionType;
|
|
204
|
+
readonly roles?: readonly string[];
|
|
205
|
+
readonly redactValue?: unknown;
|
|
206
|
+
}
|
|
207
|
+
type FieldPermissionMap = Record<string, FieldPermission>;
|
|
208
|
+
declare const fields: {
|
|
209
|
+
/**
|
|
210
|
+
* Field is never included in responses. Not writable via API.
|
|
211
|
+
*
|
|
212
|
+
* @example
|
|
213
|
+
* ```typescript
|
|
214
|
+
* fields: { password: fields.hidden() }
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
hidden(): FieldPermission;
|
|
218
|
+
/**
|
|
219
|
+
* Field is only visible to users with specified roles.
|
|
220
|
+
* Other users don't see the field at all.
|
|
221
|
+
*
|
|
222
|
+
* @example
|
|
223
|
+
* ```typescript
|
|
224
|
+
* fields: { salary: fields.visibleTo(['admin', 'hr']) }
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
visibleTo(roles: readonly string[]): FieldPermission;
|
|
228
|
+
/**
|
|
229
|
+
* Field is only writable by users with specified roles.
|
|
230
|
+
* All users can still read the field. Users without the role
|
|
231
|
+
* have the field silently stripped from write operations.
|
|
232
|
+
*
|
|
233
|
+
* @example
|
|
234
|
+
* ```typescript
|
|
235
|
+
* fields: { role: fields.writableBy(['admin']) }
|
|
236
|
+
* ```
|
|
237
|
+
*/
|
|
238
|
+
writableBy(roles: readonly string[]): FieldPermission;
|
|
239
|
+
/**
|
|
240
|
+
* Field is redacted (replaced with a placeholder) for specified roles.
|
|
241
|
+
* Other users see the real value.
|
|
242
|
+
*
|
|
243
|
+
* @param roles - Roles that see the redacted value
|
|
244
|
+
* @param redactValue - Replacement value (default: '***')
|
|
245
|
+
*
|
|
246
|
+
* @example
|
|
247
|
+
* ```typescript
|
|
248
|
+
* fields: {
|
|
249
|
+
* email: fields.redactFor(['viewer']),
|
|
250
|
+
* ssn: fields.redactFor(['basic'], '***-**-****'),
|
|
251
|
+
* }
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
redactFor(roles: readonly string[], redactValue?: unknown): FieldPermission;
|
|
255
|
+
};
|
|
256
|
+
/**
|
|
257
|
+
* Apply field-level READ permissions to a response object.
|
|
258
|
+
* Strips hidden fields, enforces visibility, and applies redaction.
|
|
259
|
+
*
|
|
260
|
+
* @param data - The response object (mutated in place for performance)
|
|
261
|
+
* @param fieldPermissions - Field permission map from resource config
|
|
262
|
+
* @param userRoles - Current user's roles (empty array for unauthenticated)
|
|
263
|
+
* @returns The filtered object
|
|
264
|
+
*/
|
|
265
|
+
declare function applyFieldReadPermissions<T extends Record<string, unknown>>(data: T, fieldPermissions: FieldPermissionMap, userRoles: readonly string[]): T;
|
|
266
|
+
/**
|
|
267
|
+
* Result of applying write permissions — includes both the filtered body
|
|
268
|
+
* and the list of fields that were stripped so callers can decide whether
|
|
269
|
+
* to reject the request (secure default) or silently strip (legacy).
|
|
270
|
+
*/
|
|
271
|
+
interface FieldWritePermissionResult<T extends Record<string, unknown>> {
|
|
272
|
+
readonly body: T;
|
|
273
|
+
readonly deniedFields: readonly string[];
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Apply field-level WRITE permissions to request body.
|
|
277
|
+
*
|
|
278
|
+
* Returns both the filtered body and the list of denied fields. Callers are
|
|
279
|
+
* expected to reject the request when `deniedFields.length > 0` — silently
|
|
280
|
+
* stripping fields hides misconfigurations and real attacks. See
|
|
281
|
+
* `BodySanitizer` for the default policy.
|
|
282
|
+
*
|
|
283
|
+
* @param body - The request body (returns a new filtered copy)
|
|
284
|
+
* @param fieldPermissions - Field permission map from resource config
|
|
285
|
+
* @param userRoles - Current user's roles
|
|
286
|
+
*/
|
|
287
|
+
declare function applyFieldWritePermissions<T extends Record<string, unknown>>(body: T, fieldPermissions: FieldPermissionMap, userRoles: readonly string[]): FieldWritePermissionResult<T>;
|
|
288
|
+
/**
|
|
289
|
+
* Resolve effective roles by merging global user roles with org-level roles.
|
|
290
|
+
*
|
|
291
|
+
* Global roles come from `req.user.role` (normalized via getUserRoles()).
|
|
292
|
+
* Org roles come from `req.context.orgRoles` (set by BA adapter's org bridge).
|
|
293
|
+
*
|
|
294
|
+
* When no org context exists, returns global roles only — backward compatible.
|
|
295
|
+
*/
|
|
296
|
+
declare function resolveEffectiveRoles(userRoles: readonly string[], orgRoles: readonly string[]): string[];
|
|
297
|
+
//#endregion
|
|
298
|
+
export { applyFieldWritePermissions as a, PermissionCheck as c, UserBase as d, getUserRoles as f, applyFieldReadPermissions as i, PermissionContext as l, FieldPermissionMap as n, fields as o, normalizeRoles as p, FieldPermissionType as r, resolveEffectiveRoles as s, FieldPermission as t, PermissionResult as u };
|
package/dist/hooks/index.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $ as beforeCreate, G as HookOperation, H as DefineHookOptions, J as HookSystem, K as HookPhase, Q as afterUpdate, U as HookContext, W as HookHandler, X as afterCreate, Y as HookSystemOptions, Z as afterDelete, et as beforeDelete, nt as createHookSystem, q as HookRegistration, rt as defineHook, tt as beforeUpdate } from "../index-BGbpGVyM.mjs";
|
|
2
2
|
export { type DefineHookOptions, type HookContext, type HookHandler, type HookOperation, type HookPhase, type HookRegistration, HookSystem, type HookSystemOptions, afterCreate, afterDelete, afterUpdate, beforeCreate, beforeDelete, beforeUpdate, createHookSystem, defineHook };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { _n as RepositoryLike } from "../index-BGbpGVyM.mjs";
|
|
2
2
|
import { i as createIdempotencyResult, n as IdempotencyResult, r as IdempotencyStore, t as IdempotencyLock } from "../interface-B-pe8fhj.mjs";
|
|
3
3
|
import { i as RedisIdempotencyStoreOptions, n as RedisClient } from "../redis-MXLp1oOf.mjs";
|
|
4
4
|
import { FastifyPluginAsync } from "fastify";
|
|
@@ -82,6 +82,9 @@ declare module "fastify" {
|
|
|
82
82
|
declare const idempotencyPlugin: FastifyPluginAsync<IdempotencyPluginOptions>;
|
|
83
83
|
declare const _default: FastifyPluginAsync<IdempotencyPluginOptions>;
|
|
84
84
|
//#endregion
|
|
85
|
+
//#region src/idempotency/repository-idempotency-adapter.d.ts
|
|
86
|
+
declare function repositoryAsIdempotencyStore(repository: RepositoryLike, defaultTtlMs: number): IdempotencyStore;
|
|
87
|
+
//#endregion
|
|
85
88
|
//#region src/idempotency/stores/memory.d.ts
|
|
86
89
|
interface MemoryIdempotencyStoreOptions {
|
|
87
90
|
/** Default TTL in milliseconds (default: 86400000 = 24h) */
|
|
@@ -117,4 +120,4 @@ declare class MemoryIdempotencyStore implements IdempotencyStore {
|
|
|
117
120
|
private evictOldest;
|
|
118
121
|
}
|
|
119
122
|
//#endregion
|
|
120
|
-
export { type IdempotencyLock, type IdempotencyPluginOptions, type IdempotencyResult, type IdempotencyStore, MemoryIdempotencyStore, type MemoryIdempotencyStoreOptions, type RedisClient, type RedisIdempotencyStoreOptions, createIdempotencyResult, _default as idempotencyPlugin, idempotencyPlugin as idempotencyPluginFn };
|
|
123
|
+
export { type IdempotencyLock, type IdempotencyPluginOptions, type IdempotencyResult, type IdempotencyStore, MemoryIdempotencyStore, type MemoryIdempotencyStoreOptions, type RedisClient, type RedisIdempotencyStoreOptions, createIdempotencyResult, _default as idempotencyPlugin, idempotencyPlugin as idempotencyPluginFn, repositoryAsIdempotencyStore };
|
|
@@ -1,7 +1,28 @@
|
|
|
1
|
-
import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-
|
|
1
|
+
import { n as createSafeGetOne, t as createIsDuplicateKeyError } from "../store-helpers-DYYUQbQN.mjs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import fp from "fastify-plugin";
|
|
4
|
+
import { and, eq, exists, gt, lt, or, startsWith } from "@classytic/repo-core/filter";
|
|
5
|
+
import { update } from "@classytic/repo-core/update";
|
|
4
6
|
//#region src/idempotency/repository-idempotency-adapter.ts
|
|
7
|
+
/**
|
|
8
|
+
* RepositoryLike → IdempotencyStore adapter.
|
|
9
|
+
*
|
|
10
|
+
* Maps the idempotency store's verbs (get / set / tryLock / unlock / delete /
|
|
11
|
+
* deleteByPrefix / findByPrefix) onto arc's canonical repository primitives
|
|
12
|
+
* (`getOne` / `deleteMany` / `findOneAndUpdate`). `idempotencyPlugin` wraps
|
|
13
|
+
* a passed repository with this helper when you use the `{ repository }`
|
|
14
|
+
* option; the function is also re-exported from `@classytic/arc/idempotency`
|
|
15
|
+
* so consumers can build and decorate the store (metrics, tracing, key
|
|
16
|
+
* namespacing) before passing it via `store:`.
|
|
17
|
+
*
|
|
18
|
+
* Portability: filters compose via `@classytic/repo-core/filter` builders
|
|
19
|
+
* (`and` / `or` / `eq` / `gt` / `lt` / `exists` / `startsWith`) and updates
|
|
20
|
+
* via `@classytic/repo-core/update` (`update({ set, unset, setOnInsert })`).
|
|
21
|
+
* Both IRs compile to Mongo operators on mongokit, SQL predicates on
|
|
22
|
+
* sqlitekit / pgkit, and `WhereInput` / `update` on prismakit. The store
|
|
23
|
+
* therefore runs identically on every backend that implements the
|
|
24
|
+
* `StandardRepo.findOneAndUpdate` + `getOne` + `deleteMany` surface.
|
|
25
|
+
*/
|
|
5
26
|
function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
|
|
6
27
|
const missing = [];
|
|
7
28
|
if (typeof repository.getOne !== "function") missing.push("getOne");
|
|
@@ -9,13 +30,13 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
|
|
|
9
30
|
if (typeof repository.findOneAndUpdate !== "function") missing.push("findOneAndUpdate");
|
|
10
31
|
if (missing.length > 0) throw new Error(`idempotencyPlugin: repository is missing required methods: ${missing.join(", ")}. mongokit ≥3.8 satisfies these; other kits must implement them to back idempotency via a repository.`);
|
|
11
32
|
const r = repository;
|
|
33
|
+
const idField = repository.idField ?? "_id";
|
|
12
34
|
const isDuplicateKeyError = createIsDuplicateKeyError(repository);
|
|
13
35
|
const safeGetOne = createSafeGetOne(repository);
|
|
14
|
-
const escapeRegex = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
15
36
|
return {
|
|
16
37
|
name: "repository",
|
|
17
38
|
async get(key) {
|
|
18
|
-
const doc = await safeGetOne(
|
|
39
|
+
const doc = await safeGetOne(eq(idField, key));
|
|
19
40
|
if (!doc?.result) return void 0;
|
|
20
41
|
if (new Date(doc.expiresAt) < /* @__PURE__ */ new Date()) return void 0;
|
|
21
42
|
return {
|
|
@@ -28,8 +49,8 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
|
|
|
28
49
|
};
|
|
29
50
|
},
|
|
30
51
|
async set(key, result) {
|
|
31
|
-
await r.findOneAndUpdate(
|
|
32
|
-
|
|
52
|
+
await r.findOneAndUpdate(eq(idField, key), update({
|
|
53
|
+
set: {
|
|
33
54
|
result: {
|
|
34
55
|
statusCode: result.statusCode,
|
|
35
56
|
headers: result.headers,
|
|
@@ -38,8 +59,8 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
|
|
|
38
59
|
createdAt: result.createdAt,
|
|
39
60
|
expiresAt: result.expiresAt
|
|
40
61
|
},
|
|
41
|
-
|
|
42
|
-
}, {
|
|
62
|
+
unset: ["lock"]
|
|
63
|
+
}), {
|
|
43
64
|
upsert: true,
|
|
44
65
|
returnDocument: "after"
|
|
45
66
|
});
|
|
@@ -49,19 +70,16 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
|
|
|
49
70
|
const lockExpiresAt = new Date(now.getTime() + ttlMs);
|
|
50
71
|
const docExpiresAt = new Date(now.getTime() + defaultTtlMs);
|
|
51
72
|
try {
|
|
52
|
-
const doc = await r.findOneAndUpdate({
|
|
53
|
-
|
|
54
|
-
$or: [{ lock: { $exists: false } }, { "lock.expiresAt": { $lt: now } }]
|
|
55
|
-
}, {
|
|
56
|
-
$set: { lock: {
|
|
73
|
+
const doc = await r.findOneAndUpdate(and(eq(idField, key), or(exists("lock", false), lt("lock.expiresAt", now))), update({
|
|
74
|
+
set: { lock: {
|
|
57
75
|
requestId,
|
|
58
76
|
expiresAt: lockExpiresAt
|
|
59
77
|
} },
|
|
60
|
-
|
|
78
|
+
setOnInsert: {
|
|
61
79
|
createdAt: now,
|
|
62
80
|
expiresAt: docExpiresAt
|
|
63
81
|
}
|
|
64
|
-
}, {
|
|
82
|
+
}), {
|
|
65
83
|
upsert: true,
|
|
66
84
|
returnDocument: "after"
|
|
67
85
|
});
|
|
@@ -72,31 +90,24 @@ function repositoryAsIdempotencyStore(repository, defaultTtlMs) {
|
|
|
72
90
|
}
|
|
73
91
|
},
|
|
74
92
|
async unlock(key, requestId) {
|
|
75
|
-
await r.findOneAndUpdate({
|
|
76
|
-
_id: key,
|
|
77
|
-
"lock.requestId": requestId
|
|
78
|
-
}, { $unset: { lock: "" } });
|
|
93
|
+
await r.findOneAndUpdate(and(eq(idField, key), eq("lock.requestId", requestId)), update({ unset: ["lock"] }));
|
|
79
94
|
},
|
|
80
95
|
async isLocked(key) {
|
|
81
|
-
const doc = await safeGetOne(
|
|
96
|
+
const doc = await safeGetOne(eq(idField, key));
|
|
82
97
|
if (!doc?.lock) return false;
|
|
83
98
|
return new Date(doc.lock.expiresAt) > /* @__PURE__ */ new Date();
|
|
84
99
|
},
|
|
85
100
|
async delete(key) {
|
|
86
|
-
await r.deleteMany(
|
|
101
|
+
await r.deleteMany(eq(idField, key));
|
|
87
102
|
},
|
|
88
103
|
async deleteByPrefix(prefix) {
|
|
89
|
-
return (await r.deleteMany(
|
|
104
|
+
return (await r.deleteMany(startsWith(idField, prefix, "sensitive"))).deletedCount ?? 0;
|
|
90
105
|
},
|
|
91
106
|
async findByPrefix(prefix) {
|
|
92
|
-
const doc = await safeGetOne(
|
|
93
|
-
_id: { $regex: `^${escapeRegex(prefix)}` },
|
|
94
|
-
result: { $exists: true },
|
|
95
|
-
expiresAt: { $gt: /* @__PURE__ */ new Date() }
|
|
96
|
-
});
|
|
107
|
+
const doc = await safeGetOne(and(startsWith(idField, prefix, "sensitive"), exists("result", true), gt("expiresAt", /* @__PURE__ */ new Date())));
|
|
97
108
|
if (!doc?.result) return void 0;
|
|
98
109
|
return {
|
|
99
|
-
key: doc
|
|
110
|
+
key: String(doc[idField] ?? prefix),
|
|
100
111
|
statusCode: doc.result.statusCode,
|
|
101
112
|
headers: doc.result.headers,
|
|
102
113
|
body: doc.result.body,
|
|
@@ -361,6 +372,7 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
|
|
|
361
372
|
return;
|
|
362
373
|
}
|
|
363
374
|
request._idempotencyFullKey = fullKey;
|
|
375
|
+
reply.header(HEADER_IDEMPOTENCY_KEY, idempotencyKey);
|
|
364
376
|
};
|
|
365
377
|
fastify.decorate("idempotency", {
|
|
366
378
|
invalidate: async (key) => {
|
|
@@ -371,15 +383,12 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
|
|
|
371
383
|
},
|
|
372
384
|
middleware: idempotencyMiddleware
|
|
373
385
|
});
|
|
374
|
-
fastify.addHook("
|
|
386
|
+
fastify.addHook("preSerialization", async (request, reply, payload) => {
|
|
375
387
|
if (request.idempotencyReplayed) return payload;
|
|
376
388
|
const fullKey = request._idempotencyFullKey;
|
|
377
389
|
if (!fullKey) return payload;
|
|
378
390
|
const statusCode = reply.statusCode;
|
|
379
|
-
if (statusCode < 200 || statusCode >= 300)
|
|
380
|
-
await store.unlock(fullKey, request.id);
|
|
381
|
-
return payload;
|
|
382
|
-
}
|
|
391
|
+
if (statusCode < 200 || statusCode >= 300) return payload;
|
|
383
392
|
const headersToCache = {};
|
|
384
393
|
const excludeHeaders = new Set([
|
|
385
394
|
"content-length",
|
|
@@ -399,13 +408,13 @@ const idempotencyPlugin = async (fastify, opts = {}) => {
|
|
|
399
408
|
}
|
|
400
409
|
const result = createIdempotencyResult(statusCode, body, headersToCache, ttlMs);
|
|
401
410
|
await store.set(fullKey, result);
|
|
402
|
-
await store.unlock(fullKey, request.id);
|
|
403
|
-
reply.header(HEADER_IDEMPOTENCY_KEY, request.idempotencyKey);
|
|
404
411
|
return payload;
|
|
405
412
|
});
|
|
406
|
-
fastify.addHook("
|
|
413
|
+
fastify.addHook("onResponse", async (request) => {
|
|
414
|
+
if (request.idempotencyReplayed) return;
|
|
407
415
|
const fullKey = request._idempotencyFullKey;
|
|
408
|
-
if (fullKey)
|
|
416
|
+
if (!fullKey) return;
|
|
417
|
+
await store.unlock(fullKey, request.id);
|
|
409
418
|
});
|
|
410
419
|
fastify.addHook("onClose", async () => {
|
|
411
420
|
await store.close?.();
|
|
@@ -421,4 +430,4 @@ var idempotencyPlugin_default = fp(idempotencyPlugin, {
|
|
|
421
430
|
fastify: "5.x"
|
|
422
431
|
});
|
|
423
432
|
//#endregion
|
|
424
|
-
export { MemoryIdempotencyStore, createIdempotencyResult, idempotencyPlugin_default as idempotencyPlugin, idempotencyPlugin as idempotencyPluginFn };
|
|
433
|
+
export { MemoryIdempotencyStore, createIdempotencyResult, idempotencyPlugin_default as idempotencyPlugin, idempotencyPlugin as idempotencyPluginFn, repositoryAsIdempotencyStore };
|