@classytic/arc 2.23.0 → 2.25.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/dist/{BaseController-BQ8KNvWC.mjs → BaseController--M8EMzt0.mjs} +90 -21
- package/dist/QueryCache-DOMT_iq7.d.mts +72 -0
- package/dist/actionPermissions-CAphcmUG.mjs +24 -0
- package/dist/audit/index.d.mts +32 -3
- package/dist/audit/index.mjs +7 -4
- package/dist/auth/index.d.mts +2 -2
- package/dist/auth/index.mjs +263 -259
- package/dist/{buildHandler-D_2ULWxZ.mjs → buildHandler-Dtwd8fdz.mjs} +1 -1
- package/dist/cache/index.d.mts +41 -3
- package/dist/cache/index.mjs +33 -4
- package/dist/cli/commands/describe.d.mts +1 -1
- package/dist/cli/commands/docs.mjs +1 -1
- package/dist/cli/commands/generate.mjs +1 -1
- package/dist/cli/commands/init.mjs +1 -1
- package/dist/cli/index.mjs +2 -2
- package/dist/context/index.mjs +2 -64
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.mjs +4 -4
- package/dist/{core-DQ6rDTyH.mjs → core-EUNu0u07.mjs} +296 -11
- package/dist/{createActionRouter-BuOv-m4Y.mjs → createActionRouter-B6GvAuAX.mjs} +3 -2
- package/dist/{createAggregationRouter-CgJv_BU_.mjs → createAggregationRouter-wzwFa_dW.mjs} +3 -3
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{elevation-COpYKt2h.mjs → elevation-CIz1GjGm.mjs} +4 -3
- package/dist/{eventPlugin-A6EbthXR.d.mts → eventPlugin-Bg3cP2_u.d.mts} +38 -3
- package/dist/{eventPlugin-DJG2D7sy.mjs → eventPlugin-DrQ_FesK.mjs} +22 -4
- package/dist/events/index.d.mts +57 -314
- package/dist/events/index.mjs +163 -91
- package/dist/events/transports/redis-stream-entry.d.mts +1 -1
- package/dist/events/transports/redis-stream-entry.mjs +24 -2
- package/dist/factory/index.d.mts +4 -5
- package/dist/factory/index.mjs +28 -8
- package/dist/{fastify-augmentation-g_f9omSc.d.mts → fastify-augmentation-dPNBVeIj.d.mts} +25 -2
- package/dist/{generate-usRSdQmP.mjs → generate-D0wZupBg.mjs} +1 -1
- package/dist/headers-BvlQSDMB.mjs +45 -0
- package/dist/health-CB_m4S32.mjs +377 -0
- package/dist/hooks/index.d.mts +1 -1
- package/dist/idempotency/index.mjs +27 -3
- package/dist/{index-wMoWBLkn.d.mts → index-BMnbzvtO.d.mts} +3 -3
- package/dist/{integrations/jobs.d.mts → index-CG4yqMDB.d.mts} +82 -2
- package/dist/{index-BBsvQt7U.d.mts → index-DPRH0Cme.d.mts} +36 -106
- package/dist/{module-B4u10iOJ.d.mts → index-KUoxjIq1.d.mts} +263 -23
- package/dist/{index-CC5ruDGh.d.mts → index-f4PTrk3B.d.mts} +37 -5
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +5 -5
- package/dist/{init-CjO4p_q1.mjs → init-tlRSt667.mjs} +206 -208
- package/dist/integrations/event-gateway.d.mts +1 -1
- package/dist/integrations/event-gateway.mjs +1 -1
- package/dist/integrations/index.d.mts +3 -3
- package/dist/integrations/jobs/index.d.mts +2 -0
- package/dist/integrations/{jobs.mjs → jobs/index.mjs} +110 -67
- package/dist/integrations/mcp/index.d.mts +4 -4
- package/dist/integrations/mcp/index.mjs +2 -2
- package/dist/integrations/mcp/testing.d.mts +1 -1
- package/dist/integrations/mcp/testing.mjs +1 -1
- package/dist/integrations/webhooks.d.mts +37 -30
- package/dist/integrations/webhooks.mjs +51 -62
- package/dist/integrations/websocket-redis.d.mts +1 -1
- package/dist/integrations/websocket.d.mts +1 -1
- package/dist/integrations/websocket.mjs +41 -14
- package/dist/{interface-C1_WLTMK.d.mts → interface-BGZCzOGL.d.mts} +24 -31
- package/dist/{mcpPlugin-BtzIL_kM.mjs → mcpPlugin-Cb7olw0j.mjs} +11 -9
- package/dist/{memory-CLXA2VeJ.mjs → memory-CmnzXE5Z.mjs} +29 -0
- package/dist/middleware/index.d.mts +17 -1
- package/dist/middleware/index.mjs +1 -1
- package/dist/migrations/index.d.mts +94 -44
- package/dist/migrations/index.mjs +196 -44
- package/dist/{multipartBody-CHEQSZ6G.mjs → multipartBody-D0eBvkeq.mjs} +17 -0
- package/dist/{openapi-jSDx-VDv.mjs → openapi-DrLtog_B.mjs} +3 -3
- package/dist/permissions/index.d.mts +2 -2
- package/dist/permissions/index.mjs +2 -2
- package/dist/{permissions-tbxFHxjZ.mjs → permissions-BrtiBKGs.mjs} +229 -229
- package/dist/pipeline/index.d.mts +1 -1
- package/dist/{actionPermissions-lszoJle1.mjs → pipeline-BN5G05rd.mjs} +3 -26
- package/dist/plugins/index.d.mts +6 -77
- package/dist/plugins/index.mjs +11 -95
- package/dist/plugins/response-cache.mjs +1 -0
- package/dist/plugins/tracing-entry.mjs +1 -1
- package/dist/{index-Ba10fdKp.d.mts → preloadResources-0oe-JSSS.d.mts} +50 -50
- package/dist/{createApp-CjOeWJ9_.mjs → preloadResources-DfyngIN9.mjs} +233 -70
- package/dist/presets/filesUpload.d.mts +3 -3
- package/dist/presets/filesUpload.mjs +3 -3
- package/dist/presets/index.d.mts +2 -1
- package/dist/presets/index.mjs +1 -1
- package/dist/presets/multiTenant.d.mts +1 -1
- package/dist/presets/multiTenant.mjs +1 -1
- package/dist/presets/search.d.mts +2 -2
- package/dist/presets/search.mjs +1 -1
- package/dist/{presets-BOe4Ugkl.mjs → presets-DwFDVKP0.mjs} +2 -2
- package/dist/{queryCachePlugin-DzqLN904.mjs → queryCachePlugin-DSGuSVLW.mjs} +39 -8
- package/dist/{queryCachePlugin-CfYWq-Ak.d.mts → queryCachePlugin-br4otiqF.d.mts} +2 -2
- package/dist/{registerRoute-CWO6mdYe.mjs → rateLimit-DrED-C4v.mjs} +2 -109
- package/dist/{realtime-I8OrfnMR.mjs → realtime-Cuw_deA-.mjs} +2 -2
- package/dist/{redis-stream-iVR2l7aS.d.mts → redis-stream-uIaauNpZ.d.mts} +22 -0
- package/dist/registerRoute-v0Wb6Plt.mjs +108 -0
- package/dist/registry/index.d.mts +2 -2
- package/dist/renewingLease-3cuQH7h8.mjs +44 -0
- package/dist/{requestContext-BZKOr13H.mjs → requestContext-MrlJwECf.mjs} +2 -2
- package/dist/schedules-CjOd2MG9.mjs +113 -0
- package/dist/schemas/index.d.mts +67 -67
- package/dist/schemas/index.mjs +2 -3
- package/dist/scim/index.mjs +1 -2
- package/dist/scope/index.d.mts +2 -2
- package/dist/scope/index.mjs +4 -4
- package/dist/{scope-LONQ3mfZ.mjs → scope-DfaPntpo.mjs} +4 -5
- package/dist/{sse-0u4P4-OI.mjs → sse-BQ1w0dfl.mjs} +1 -1
- package/dist/testing/index.d.mts +4 -3
- package/dist/testing/index.mjs +2 -2
- package/dist/transactionContext-PNdoyLLm.mjs +64 -0
- package/dist/types/index.d.mts +4 -4
- package/dist/{types-bHblZ9YN.mjs → types-CSyCxl2V.mjs} +38 -7
- package/dist/{types-DH0GupI7.d.mts → types-DjC-KUCz.d.mts} +118 -20
- package/dist/{types-Csxga7d3.d.mts → types-Dmw-6Qeo.d.mts} +96 -4
- package/dist/{types-5OuqeTDX.d.mts → types-QFGc3Cjv.d.mts} +1 -1
- package/dist/usage/index.d.mts +3 -52
- package/dist/usage/index.mjs +0 -0
- package/dist/userHelpers-Cg_y3fBz.mjs +29 -0
- package/dist/utils/index.d.mts +58 -85
- package/dist/utils/index.mjs +4 -2
- package/dist/{utils-CZyeRXTh.mjs → utils-taUVD4p3.mjs} +47 -337
- package/dist/{validate-C2mGgSXg.mjs → validate-Bk44F2yT.mjs} +1 -1
- package/dist/{versioning-D6PvCwpq.d.mts → versioning-DXgY5BUm.d.mts} +11 -29
- package/dist/{versioning-Dk-71iIz.mjs → versioning-gKWyryT3.mjs} +8 -1
- package/dist/{websocket-uuWqM6UY.d.mts → websocket-_Pd0CgR0.d.mts} +15 -1
- package/package.json +526 -513
- package/skills/arc/references/api-reference.md +3 -1
- package/skills/arc/references/integrations.md +20 -11
- package/dist/QueryCache-Z1B38koh.d.mts +0 -40
- package/dist/betterAuthOpenApi-CV1lxpRX.mjs +0 -2
- package/dist/health-ScyjIVo5.mjs +0 -202
- package/dist/requestId-5BhTVzBv.mjs +0 -113
- package/dist/types-XTApa8b6.mjs +0 -24
|
@@ -1,4 +1,59 @@
|
|
|
1
|
+
import { t as startRenewingLease } from "../renewingLease-3cuQH7h8.mjs";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
1
3
|
//#region src/migrations/index.ts
|
|
4
|
+
/**
|
|
5
|
+
* Schema Versioning and Migrations System
|
|
6
|
+
*
|
|
7
|
+
* Manages database schema changes over time with version tracking.
|
|
8
|
+
* Supports forward migrations, rollbacks, and schema compatibility layers.
|
|
9
|
+
*
|
|
10
|
+
* DB-agnostic: the `db` parameter is typed as `unknown` — the user passes
|
|
11
|
+
* whatever connection object their adapter uses (Mongoose db, Prisma client,
|
|
12
|
+
* Knex instance, etc.) and their `up`/`down` functions cast it internally.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* import { defineMigration, MigrationRunner } from '@classytic/arc/migrations';
|
|
16
|
+
*
|
|
17
|
+
* const productV2 = defineMigration({
|
|
18
|
+
* version: 2,
|
|
19
|
+
* resource: 'product',
|
|
20
|
+
* up: async (db) => {
|
|
21
|
+
* const mongo = db as import('mongoose').mongo.Db;
|
|
22
|
+
* await mongo.collection('products').updateMany(
|
|
23
|
+
* {},
|
|
24
|
+
* { $rename: { 'oldField': 'newField' } }
|
|
25
|
+
* );
|
|
26
|
+
* },
|
|
27
|
+
* down: async (db) => {
|
|
28
|
+
* const mongo = db as import('mongoose').mongo.Db;
|
|
29
|
+
* await mongo.collection('products').updateMany(
|
|
30
|
+
* {},
|
|
31
|
+
* { $rename: { 'newField': 'oldField' } }
|
|
32
|
+
* );
|
|
33
|
+
* },
|
|
34
|
+
* });
|
|
35
|
+
*
|
|
36
|
+
* const runner = new MigrationRunner(mongoose.connection.db, {
|
|
37
|
+
* store: new MongoMigrationStore(mongoose.connection.db),
|
|
38
|
+
* lock: createMongoLockAdapter({ connection }), // multi-replica safety
|
|
39
|
+
* });
|
|
40
|
+
* await runner.up(migrations);
|
|
41
|
+
*
|
|
42
|
+
* ## Deployment discipline
|
|
43
|
+
*
|
|
44
|
+
* Migrations should be executed by ONE owner — a deploy job, a CI step, or
|
|
45
|
+
* a leader — not by every replica on startup. The optional `lock` (any
|
|
46
|
+
* ecosystem `LockAdapter`) makes concurrent runners fail fast instead of
|
|
47
|
+
* double-applying, and the pending set is recomputed INSIDE the lock so a
|
|
48
|
+
* runner that waited on another deploy sees its writes. Two windows remain
|
|
49
|
+
* host concerns and are deliberately documented instead of hidden:
|
|
50
|
+
* - a crash between `up()` and `record()` re-executes that migration on
|
|
51
|
+
* the next run — write migrations to be idempotent (`$rename`,
|
|
52
|
+
* `IF NOT EXISTS`, upserts);
|
|
53
|
+
* - `up()` and `record()` are not one transaction — stores backed by a
|
|
54
|
+
* transactional DB can override `record()` to join the migration's
|
|
55
|
+
* transaction.
|
|
56
|
+
*/
|
|
2
57
|
/** Default logger that writes to stdout/stderr */
|
|
3
58
|
const defaultLogger = {
|
|
4
59
|
info: (msg) => process.stdout.write(`${msg}\n`),
|
|
@@ -14,13 +69,14 @@ var MongoMigrationStore = class {
|
|
|
14
69
|
async getApplied() {
|
|
15
70
|
return await this.db.collection(this.collectionName).find({}).sort({ appliedAt: 1 }).toArray();
|
|
16
71
|
}
|
|
17
|
-
async record(migration, executionTime) {
|
|
72
|
+
async record(migration, executionTime, meta) {
|
|
18
73
|
await this.db.collection(this.collectionName).insertOne({
|
|
19
74
|
version: migration.version,
|
|
20
75
|
resource: migration.resource,
|
|
21
76
|
description: migration.description,
|
|
22
77
|
appliedAt: /* @__PURE__ */ new Date(),
|
|
23
|
-
executionTime
|
|
78
|
+
executionTime,
|
|
79
|
+
...meta?.checksum ? { checksum: meta.checksum } : {}
|
|
24
80
|
});
|
|
25
81
|
}
|
|
26
82
|
async remove(migration) {
|
|
@@ -30,6 +86,16 @@ var MongoMigrationStore = class {
|
|
|
30
86
|
});
|
|
31
87
|
}
|
|
32
88
|
};
|
|
89
|
+
const MIGRATION_LOCK_NAME = "arc:migrations";
|
|
90
|
+
const DEFAULT_LOCK_LEASE_MS = 600 * 1e3;
|
|
91
|
+
/**
|
|
92
|
+
* Source checksum for drift detection — SHA-256 over the migration's
|
|
93
|
+
* `up`/`down` source. Stable for a given build; a transpile-target change
|
|
94
|
+
* can legitimately shift it (hence `checksumMismatch: 'warn'` default).
|
|
95
|
+
*/
|
|
96
|
+
function computeMigrationChecksum(migration) {
|
|
97
|
+
return createHash("sha256").update(migration.up.toString()).update("\n").update(migration.down.toString()).digest("hex");
|
|
98
|
+
}
|
|
33
99
|
/**
|
|
34
100
|
* Define a migration
|
|
35
101
|
*/
|
|
@@ -62,62 +128,148 @@ var MigrationRunner = class {
|
|
|
62
128
|
db;
|
|
63
129
|
store;
|
|
64
130
|
log;
|
|
131
|
+
lock;
|
|
132
|
+
lockLeaseMs;
|
|
133
|
+
holderId;
|
|
134
|
+
checksumMismatch;
|
|
65
135
|
constructor(db, opts) {
|
|
66
136
|
this.db = db;
|
|
67
137
|
this.store = opts.store;
|
|
68
138
|
this.log = opts.logger ?? defaultLogger;
|
|
139
|
+
this.lock = opts.lock;
|
|
140
|
+
this.lockLeaseMs = opts.lockLeaseMs ?? DEFAULT_LOCK_LEASE_MS;
|
|
141
|
+
this.holderId = opts.holderId ?? `migration-runner-${randomUUID()}`;
|
|
142
|
+
this.checksumMismatch = opts.checksumMismatch ?? "warn";
|
|
69
143
|
}
|
|
70
144
|
/**
|
|
71
|
-
*
|
|
145
|
+
* Acquire the migration lease (when a lock is configured) and run `fn`.
|
|
146
|
+
* Fail-fast on contention: a concurrent deploy should error loudly, not
|
|
147
|
+
* queue — the OTHER runner is applying the same set.
|
|
148
|
+
*
|
|
149
|
+
* The lease renews while `fn` runs (`startRenewingLease` — serialized,
|
|
150
|
+
* awaited teardown), so a migration slower than `lockLeaseMs` can't let
|
|
151
|
+
* a second deployment acquire the lock mid-run. Ownership loss is
|
|
152
|
+
* ENFORCED, not just logged: `fn` receives a `leaseLost` probe to refuse
|
|
153
|
+
* starting further migrations, and even a successful `fn` fails the run
|
|
154
|
+
* when the lease was lost — a concurrent runner may have interleaved, so
|
|
155
|
+
* the applied state must be verified, not trusted. (A stale holder's
|
|
156
|
+
* in-flight write can only be truly fenced by a token from the lock
|
|
157
|
+
* contract — planned repo-core evolution.)
|
|
72
158
|
*/
|
|
73
|
-
async
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
159
|
+
async withLock(fn) {
|
|
160
|
+
const lock = this.lock;
|
|
161
|
+
if (!lock) return fn(() => false);
|
|
162
|
+
if (!await lock.tryAcquire(MIGRATION_LOCK_NAME, this.holderId, this.lockLeaseMs)) throw new Error(`MigrationRunner: another runner holds the migration lock — refusing to run concurrently. If a previous runner crashed, the lease expires on its own (leaseMs: ${this.lockLeaseMs}).`);
|
|
163
|
+
const lease = startRenewingLease({
|
|
164
|
+
lock,
|
|
165
|
+
name: MIGRATION_LOCK_NAME,
|
|
166
|
+
holderId: this.holderId,
|
|
167
|
+
leaseMs: this.lockLeaseMs,
|
|
168
|
+
onLost: () => this.log.error("MigrationRunner: lease renewal lost — another holder took the migration lock; no further migrations will start and this run will fail."),
|
|
169
|
+
onError: (error) => this.log.error(`MigrationRunner: lease renewal errored (lease may lapse mid-run): ${error.message}`)
|
|
170
|
+
});
|
|
171
|
+
try {
|
|
172
|
+
const result = await fn(() => lease.lost);
|
|
173
|
+
if (lease.lost) throw new Error("MigrationRunner: migration lock ownership was lost mid-run — a concurrent runner may have executed migrations in parallel. Verify the applied state (getAppliedMigrations) before re-running.");
|
|
174
|
+
return result;
|
|
175
|
+
} finally {
|
|
176
|
+
await lease.stop();
|
|
177
|
+
try {
|
|
178
|
+
await lock.release(MIGRATION_LOCK_NAME, this.holderId);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
this.log.error(`MigrationRunner: lock release failed: ${error.message}`);
|
|
181
|
+
}
|
|
80
182
|
}
|
|
81
|
-
this.log.info(`Running ${pending.length} migration(s)...`);
|
|
82
|
-
for (const migration of pending) await this.runMigration(migration, "up");
|
|
83
|
-
this.log.info("All migrations completed successfully");
|
|
84
183
|
}
|
|
85
184
|
/**
|
|
86
|
-
*
|
|
185
|
+
* Drift detection: compare each APPLIED record's stored checksum against
|
|
186
|
+
* the current source of the same migration. A mismatch means the file was
|
|
187
|
+
* edited after being applied — its `down()` may no longer invert what
|
|
188
|
+
* actually ran.
|
|
87
189
|
*/
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
190
|
+
verifyChecksums(applied, migrations) {
|
|
191
|
+
for (const record of applied) {
|
|
192
|
+
if (!record.checksum) continue;
|
|
193
|
+
const current = migrations.find((m) => m.resource === record.resource && m.version === record.version);
|
|
194
|
+
if (!current) continue;
|
|
195
|
+
const checksum = computeMigrationChecksum(current);
|
|
196
|
+
if (checksum !== record.checksum) {
|
|
197
|
+
const msg = `Migration ${record.resource}:${record.version} source changed after it was applied (checksum ${record.checksum.slice(0, 12)}… → ${checksum.slice(0, 12)}…). Ship a NEW migration instead of editing an applied one.`;
|
|
198
|
+
if (this.checksumMismatch === "error") throw new Error(`MigrationRunner: ${msg}`);
|
|
199
|
+
this.log.error(`WARNING: ${msg}`);
|
|
200
|
+
}
|
|
98
201
|
}
|
|
99
|
-
const migration = migrations.find((m) => m.resource === last.resource && m.version === last.version);
|
|
100
|
-
if (!migration) throw new Error(`Migration ${last.resource}:${last.version} not found in migration files`);
|
|
101
|
-
this.log.info(`Rolling back ${migration.resource} v${migration.version}...`);
|
|
102
|
-
await this.runMigration(migration, "down", true);
|
|
103
|
-
this.log.info("Rollback completed");
|
|
104
202
|
}
|
|
105
203
|
/**
|
|
106
|
-
*
|
|
204
|
+
* Run all pending migrations.
|
|
205
|
+
*
|
|
206
|
+
* With a `lock` configured, the applied set is (re)read INSIDE the lease —
|
|
207
|
+
* a runner that raced another deploy computes pending against the winner's
|
|
208
|
+
* completed writes, not a stale pre-lock snapshot.
|
|
107
209
|
*/
|
|
108
|
-
async
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
this.
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
210
|
+
async up(migrations) {
|
|
211
|
+
await this.withLock(async (leaseLost) => {
|
|
212
|
+
const applied = await this.store.getApplied();
|
|
213
|
+
this.verifyChecksums(applied, migrations);
|
|
214
|
+
const appliedVersions = new Set(applied.map((m) => `${m.resource}:${m.version}`));
|
|
215
|
+
const pending = migrations.filter((m) => !appliedVersions.has(`${m.resource}:${m.version}`)).sort((a, b) => a.version - b.version);
|
|
216
|
+
if (pending.length === 0) {
|
|
217
|
+
this.log.info("No pending migrations");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this.log.info(`Running ${pending.length} migration(s)...`);
|
|
221
|
+
for (const migration of pending) {
|
|
222
|
+
if (leaseLost()) throw new Error("MigrationRunner: refusing to start the next migration — lock ownership lost.");
|
|
223
|
+
await this.runMigration(migration, "up");
|
|
224
|
+
}
|
|
225
|
+
this.log.info("All migrations completed successfully");
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Rollback last migration
|
|
230
|
+
*/
|
|
231
|
+
async down(migrations) {
|
|
232
|
+
await this.withLock(async () => {
|
|
233
|
+
const applied = await this.store.getApplied();
|
|
234
|
+
const last = applied[applied.length - 1];
|
|
235
|
+
if (!last) {
|
|
236
|
+
this.log.info("No migrations to rollback");
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const migration = migrations.find((m) => m.resource === last.resource && m.version === last.version);
|
|
240
|
+
if (!migration) throw new Error(`Migration ${last.resource}:${last.version} not found in migration files`);
|
|
241
|
+
this.log.info(`Rolling back ${migration.resource} v${migration.version}...`);
|
|
118
242
|
await this.runMigration(migration, "down", true);
|
|
119
|
-
|
|
120
|
-
|
|
243
|
+
this.log.info("Rollback completed");
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Rollback to a specific version.
|
|
248
|
+
*
|
|
249
|
+
* Versions are RESOURCE-scoped, so a bare numeric threshold is only
|
|
250
|
+
* meaningful within one resource. When the applied set spans multiple
|
|
251
|
+
* resources, `options.resource` is required — rolling every resource back
|
|
252
|
+
* across a shared number silently reverts unrelated schemas.
|
|
253
|
+
*/
|
|
254
|
+
async downTo(migrations, targetVersion, options) {
|
|
255
|
+
await this.withLock(async (leaseLost) => {
|
|
256
|
+
const applied = await this.store.getApplied();
|
|
257
|
+
const resources = new Set(applied.map((m) => m.resource));
|
|
258
|
+
if (!options?.resource && resources.size > 1) throw new Error(`MigrationRunner.downTo: applied migrations span ${resources.size} resources (${[...resources].sort().join(", ")}) — versions are resource-scoped, so pass { resource: '<name>' } to select which one to roll back.`);
|
|
259
|
+
const toRollback = (options?.resource ? applied.filter((m) => m.resource === options.resource) : applied).filter((m) => m.version > targetVersion).reverse();
|
|
260
|
+
if (toRollback.length === 0) {
|
|
261
|
+
this.log.info(`Already at or below version ${targetVersion}`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
this.log.info(`Rolling back ${toRollback.length} migration(s)...`);
|
|
265
|
+
for (const record of toRollback) {
|
|
266
|
+
if (leaseLost()) throw new Error("MigrationRunner: refusing to start the next rollback — lock ownership lost.");
|
|
267
|
+
const migration = migrations.find((m) => m.resource === record.resource && m.version === record.version);
|
|
268
|
+
if (!migration) throw new Error(`Migration ${record.resource}:${record.version} not found`);
|
|
269
|
+
await this.runMigration(migration, "down", true);
|
|
270
|
+
}
|
|
271
|
+
this.log.info("Rollback completed");
|
|
272
|
+
});
|
|
121
273
|
}
|
|
122
274
|
/**
|
|
123
275
|
* Get all applied migrations
|
|
@@ -154,7 +306,7 @@ var MigrationRunner = class {
|
|
|
154
306
|
if (migration.validate) {
|
|
155
307
|
if (!await migration.validate(this.db)) throw new Error("Migration validation failed");
|
|
156
308
|
}
|
|
157
|
-
await this.store.record(migration, Date.now() - start);
|
|
309
|
+
await this.store.record(migration, Date.now() - start, { checksum: computeMigrationChecksum(migration) });
|
|
158
310
|
} else {
|
|
159
311
|
await migration.down(this.db);
|
|
160
312
|
if (isRollback) await this.store.remove(migration);
|
|
@@ -232,4 +384,4 @@ var MigrationRegistry = class {
|
|
|
232
384
|
}
|
|
233
385
|
};
|
|
234
386
|
//#endregion
|
|
235
|
-
export { MigrationRegistry, MigrationRunner, MongoMigrationStore, defineMigration, withSchemaVersion };
|
|
387
|
+
export { MigrationRegistry, MigrationRunner, MongoMigrationStore, computeMigrationChecksum, defineMigration, withSchemaVersion };
|
|
@@ -2,6 +2,7 @@ import sjson from "secure-json-parse";
|
|
|
2
2
|
//#region src/middleware/multipartBody.ts
|
|
3
3
|
const DEFAULT_MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
4
4
|
const DEFAULT_MAX_FILES = 5;
|
|
5
|
+
const DEFAULT_MAX_TOTAL_BYTES = 25 * 1024 * 1024;
|
|
5
6
|
const DEFAULT_FILES_KEY = "_files";
|
|
6
7
|
/**
|
|
7
8
|
* Build a matcher for MIME allow-lists that supports exact (e.g. `image/png`),
|
|
@@ -45,10 +46,19 @@ function buildMimeMatcher(allowed) {
|
|
|
45
46
|
* For non-multipart requests (regular JSON), this is a no-op — the request
|
|
46
47
|
* passes through unchanged. This makes it safe to add to create/update
|
|
47
48
|
* middlewares without breaking JSON clients.
|
|
49
|
+
*
|
|
50
|
+
* **BUFFERED — for small uploads only.** Every accepted file is held fully
|
|
51
|
+
* in process memory for the request's lifetime; memory cost is
|
|
52
|
+
* `concurrency × maxTotalBytes`. Suitable for avatars, documents, and
|
|
53
|
+
* form attachments. For large files use PRESIGNED direct-to-object-storage
|
|
54
|
+
* uploads (see the files-upload preset / production guide) and pair upload
|
|
55
|
+
* routes with strict rate limits — buffering video through the app tier is
|
|
56
|
+
* the wrong architecture regardless of limits.
|
|
48
57
|
*/
|
|
49
58
|
function multipartBody(options = {}) {
|
|
50
59
|
const maxFileSize = options.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
51
60
|
const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
|
|
61
|
+
const maxTotalBytes = options.maxTotalBytes ?? DEFAULT_MAX_TOTAL_BYTES;
|
|
52
62
|
const mimeMatcher = buildMimeMatcher(options.allowedMimeTypes);
|
|
53
63
|
const filesKey = options.filesKey ?? DEFAULT_FILES_KEY;
|
|
54
64
|
const requiredFields = options.requiredFields && options.requiredFields.length > 0 ? options.requiredFields : void 0;
|
|
@@ -61,6 +71,7 @@ function multipartBody(options = {}) {
|
|
|
61
71
|
const body = {};
|
|
62
72
|
const files = {};
|
|
63
73
|
let fileCount = 0;
|
|
74
|
+
let totalBytes = 0;
|
|
64
75
|
try {
|
|
65
76
|
const parts = request.parts();
|
|
66
77
|
for await (const part of parts) if (part.type === "file") {
|
|
@@ -76,6 +87,12 @@ function multipartBody(options = {}) {
|
|
|
76
87
|
message: `File '${part.filename}' exceeds maximum size of ${Math.round(maxFileSize / 1024 / 1024)}MB`,
|
|
77
88
|
status: 413
|
|
78
89
|
});
|
|
90
|
+
totalBytes += buffer.length;
|
|
91
|
+
if (totalBytes > maxTotalBytes) return reply.code(413).send({
|
|
92
|
+
code: "arc.payload_too_large",
|
|
93
|
+
message: `Combined upload size exceeds ${Math.round(maxTotalBytes / 1024 / 1024)}MB per request`,
|
|
94
|
+
status: 413
|
|
95
|
+
});
|
|
79
96
|
files[part.fieldname] = {
|
|
80
97
|
filename: part.filename,
|
|
81
98
|
mimetype: part.mimetype,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { n as getUserRoles } from "./userHelpers-Cg_y3fBz.mjs";
|
|
2
2
|
import { n as convertRouteSchema } from "./schemaConverter-DJvodiHj.mjs";
|
|
3
|
-
import { t as resolveActionPermission } from "./actionPermissions-lszoJle1.mjs";
|
|
4
3
|
import { t as pluralize } from "./pluralize-BhrgwZb9.mjs";
|
|
5
|
-
import { t as
|
|
4
|
+
import { t as resolveActionPermission } from "./actionPermissions-CAphcmUG.mjs";
|
|
5
|
+
import { t as buildActionBodySchema } from "./createActionRouter-B6GvAuAX.mjs";
|
|
6
6
|
import fp from "fastify-plugin";
|
|
7
7
|
//#region src/docs/openapi/canonical-schemas.ts
|
|
8
8
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { i as UserBase, n as PermissionContext, o as getUserRoles, r as PermissionResult, s as normalizeRoles, t as PermissionCheck } from "../types-Dmw-6Qeo.mjs";
|
|
2
2
|
import { a as applyFieldWritePermissions, i as applyFieldReadPermissions, n as FieldPermissionMap, o as fields, r as FieldPermissionType, s as resolveEffectiveRoles, t as FieldPermission } from "../fields-D3lhgzhD.mjs";
|
|
3
|
-
import { A as allOf, B as applyPermissionResult, C as requireGrant, D as PermissionEventBus, E as DynamicPermissionMatrixConfig, F as requireAuth, G as requireDPoP, H as RequireAgentScopeOptions, I as requireOwnership, K as requireMandate, L as requireRoles, M as anyOf, N as denyAll, O as createDynamicPermissionMatrix, P as not, R as roles, S as modeSatisfies, T as DynamicPermissionMatrix, U as RequireMandateOptions, V as normalizePermissionResult, W as requireAgentScope, _ as createRoleHierarchy, a as permissionMatrix, b as GrantResolution, c as publicReadAdminWrite, d as requireOrgMembership, f as requireOrgRole, g as RoleHierarchy, h as requireTeamMembership, i as ownerWithAdminBypass, j as allowPublic, k as createOrgPermissions, l as readOnly, m as requireServiceScope, n as authenticated, o as presets_d_exports, p as requireScopeContext, r as fullPublic, s as publicRead, t as adminOnly, u as requireOrgInScope, v as GRANT_MODES, w as ConnectEventsOptions, x as RequireGrantOptions, y as GrantMode, z as when } from "../index-
|
|
3
|
+
import { A as allOf, B as applyPermissionResult, C as requireGrant, D as PermissionEventBus, E as DynamicPermissionMatrixConfig, F as requireAuth, G as requireDPoP, H as RequireAgentScopeOptions, I as requireOwnership, K as requireMandate, L as requireRoles, M as anyOf, N as denyAll, O as createDynamicPermissionMatrix, P as not, R as roles, S as modeSatisfies, T as DynamicPermissionMatrix, U as RequireMandateOptions, V as normalizePermissionResult, W as requireAgentScope, _ as createRoleHierarchy, a as permissionMatrix, b as GrantResolution, c as publicReadAdminWrite, d as requireOrgMembership, f as requireOrgRole, g as RoleHierarchy, h as requireTeamMembership, i as ownerWithAdminBypass, j as allowPublic, k as createOrgPermissions, l as readOnly, m as requireServiceScope, n as authenticated, o as presets_d_exports, p as requireScopeContext, r as fullPublic, s as publicRead, t as adminOnly, u as requireOrgInScope, v as GRANT_MODES, w as ConnectEventsOptions, x as RequireGrantOptions, y as GrantMode, z as when } from "../index-BMnbzvtO.mjs";
|
|
4
4
|
export { type ConnectEventsOptions, type DynamicPermissionMatrix, type DynamicPermissionMatrixConfig, type FieldPermission, type FieldPermissionMap, type FieldPermissionType, GRANT_MODES, type GrantMode, type GrantResolution, type PermissionCheck, type PermissionContext, type PermissionEventBus, type PermissionResult, type RequireAgentScopeOptions, type RequireGrantOptions, type RequireMandateOptions, type RoleHierarchy, type UserBase, adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, applyPermissionResult, authenticated, createDynamicPermissionMatrix, createOrgPermissions, createRoleHierarchy, denyAll, fields, fullPublic, getUserRoles, modeSatisfies, normalizePermissionResult, normalizeRoles, not, ownerWithAdminBypass, permissionMatrix, presets_d_exports as permissions, publicRead, publicReadAdminWrite, readOnly, requireAgentScope, requireAuth, requireDPoP, requireGrant, requireMandate, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, resolveEffectiveRoles, roles, when };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
+
import { n as getUserRoles, r as normalizeRoles } from "../userHelpers-Cg_y3fBz.mjs";
|
|
1
2
|
import { a as applyFieldWritePermissions, i as applyFieldReadPermissions, o as fields, r as normalizePermissionResult, s as resolveEffectiveRoles, t as applyPermissionResult } from "../applyPermissionResult-Tw4iXtlf.mjs";
|
|
2
|
-
import { n as
|
|
3
|
-
import { A as roles, C as allowPublic, D as requireAuth, E as not, M as requireAgentScope, N as requireDPoP, O as requireOwnership, P as requireMandate, S as allOf, T as denyAll, _ as requireOrgMembership, a as permissionMatrix, b as requireServiceScope, c as publicReadAdminWrite, d as GRANT_MODES, f as modeSatisfies, g as requireOrgInScope, h as createOrgPermissions, i as ownerWithAdminBypass, j as when, k as requireRoles, l as readOnly, m as createDynamicPermissionMatrix, n as authenticated, o as presets_exports, p as requireGrant, r as fullPublic, s as publicRead, t as adminOnly, u as createRoleHierarchy, v as requireOrgRole, w as anyOf, x as requireTeamMembership, y as requireScopeContext } from "../permissions-tbxFHxjZ.mjs";
|
|
3
|
+
import { A as requireAuth, C as requireDPoP, D as anyOf, E as allowPublic, M as requireRoles, N as roles, O as denyAll, P as when, S as requireAgentScope, T as allOf, _ as requireOrgMembership, a as permissionMatrix, b as requireServiceScope, c as publicReadAdminWrite, d as GRANT_MODES, f as modeSatisfies, g as requireOrgInScope, h as createOrgPermissions, i as ownerWithAdminBypass, j as requireOwnership, k as not, l as readOnly, m as createDynamicPermissionMatrix, n as authenticated, o as presets_exports, p as requireGrant, r as fullPublic, s as publicRead, t as adminOnly, u as createRoleHierarchy, v as requireOrgRole, w as requireMandate, x as requireTeamMembership, y as requireScopeContext } from "../permissions-BrtiBKGs.mjs";
|
|
4
4
|
export { GRANT_MODES, adminOnly, allOf, allowPublic, anyOf, applyFieldReadPermissions, applyFieldWritePermissions, applyPermissionResult, authenticated, createDynamicPermissionMatrix, createOrgPermissions, createRoleHierarchy, denyAll, fields, fullPublic, getUserRoles, modeSatisfies, normalizePermissionResult, normalizeRoles, not, ownerWithAdminBypass, permissionMatrix, presets_exports as permissions, publicRead, publicReadAdminWrite, readOnly, requireAgentScope, requireAuth, requireDPoP, requireGrant, requireMandate, requireOrgInScope, requireOrgMembership, requireOrgRole, requireOwnership, requireRoles, requireScopeContext, requireServiceScope, requireTeamMembership, resolveEffectiveRoles, roles, when };
|