@supacloud/compiler 0.47.2 → 0.48.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/DELIVERY.md +1 -1
- package/dist/cli.js +540 -24
- package/dist/contract-manifest.d.ts +53 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +540 -24
- package/dist/migration-app-version.d.ts +1 -1
- package/dist/migration-assess.d.ts +2 -0
- package/dist/migration-elysia-version.d.ts +1 -1
- package/dist/types.d.ts +26 -0
- package/package.json +4 -4
package/dist/cli.js
CHANGED
|
@@ -263,6 +263,9 @@ function validateFeatureSpec(spec, module) {
|
|
|
263
263
|
if (!spec.name.trim() || spec.states.length === 0 || spec.states.some((state) => !state.trim()) || new Set(spec.states).size !== spec.states.length) {
|
|
264
264
|
error("invalid-feature-states", "Feature name and states must be non-empty; states must be unique.");
|
|
265
265
|
}
|
|
266
|
+
if (spec.tenantField !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.tenantField)) {
|
|
267
|
+
error("feature-tenant-boundary-invalid", `Feature ${spec.name} declares an unsafe tenant field.`);
|
|
268
|
+
}
|
|
266
269
|
const names = new Set;
|
|
267
270
|
for (const transition of spec.transitions) {
|
|
268
271
|
if (!transition.name.trim() || names.has(transition.name)) {
|
|
@@ -275,8 +278,41 @@ function validateFeatureSpec(spec, module) {
|
|
|
275
278
|
if (transition.permission !== undefined && !transition.permission.trim()) {
|
|
276
279
|
error("feature-governance-drift", `Transition ${transition.name} declares an empty permission.`);
|
|
277
280
|
}
|
|
281
|
+
if (transition.persistence) {
|
|
282
|
+
const persistence = transition.persistence;
|
|
283
|
+
if (persistence.boundary === "database" && (persistence.optimisticLock !== "required" || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(persistence.versionField ?? "") || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(persistence.tenantField ?? "") || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(persistence.stateField ?? "") || new Set([persistence.tenantField, persistence.versionField, persistence.stateField]).size !== 3 || persistence.receipt !== "required" || persistence.reconciliation !== "none")) {
|
|
284
|
+
error("feature-persistence-invalid", `Database transition ${transition.name} requires distinct safe tenant/version/state fields, optimistic locking, and a receipt, without reconciliation.`);
|
|
285
|
+
}
|
|
286
|
+
if (persistence.boundary === "database" && spec.tenantField !== persistence.tenantField) {
|
|
287
|
+
error("feature-tenant-boundary-drift", `Database transition ${transition.name} tenant field must match Feature ${spec.name} tenantField.`);
|
|
288
|
+
}
|
|
289
|
+
if (persistence.boundary === "external" && (persistence.optimisticLock !== "none" || persistence.versionField !== undefined || persistence.tenantField !== undefined || persistence.stateField !== undefined || persistence.receipt !== "required" || persistence.reconciliation !== "required")) {
|
|
290
|
+
error("feature-persistence-invalid", `External transition ${transition.name} requires a receipt and reconciliation, without database optimistic locking.`);
|
|
291
|
+
}
|
|
292
|
+
if (persistence.boundary === "external" && !transition.job) {
|
|
293
|
+
error("feature-job-unresolved", `External transition ${transition.name} must bind a reconciliation Job.`);
|
|
294
|
+
}
|
|
295
|
+
if (!transition.command) {
|
|
296
|
+
error("feature-command-unresolved", `Persistent transition ${transition.name} must reference a command.`);
|
|
297
|
+
}
|
|
298
|
+
if (!transition.permission?.trim()) {
|
|
299
|
+
error("feature-governance-drift", `Persistent transition ${transition.name} must declare a permission.`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
278
302
|
if (!module)
|
|
279
303
|
continue;
|
|
304
|
+
if (transition.persistence?.boundary === "database") {
|
|
305
|
+
for (const query of module.queries) {
|
|
306
|
+
if (query.tenantScope !== "required") {
|
|
307
|
+
error("feature-query-tenant-boundary", `Query ${query.className} in Feature ${spec.name} must declare tenantScope: "required".`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const job of module.jobs ?? []) {
|
|
311
|
+
if (job.tenantScope !== "required") {
|
|
312
|
+
error("feature-job-tenant-boundary", `Job ${job.className} in Feature ${spec.name} must declare tenantScope: "required".`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
280
316
|
const commands = module.commands.filter((command) => command.className === transition.command || command.name === transition.command);
|
|
281
317
|
const command = commands[0];
|
|
282
318
|
if (transition.command && commands.length !== 1) {
|
|
@@ -291,23 +327,59 @@ function validateFeatureSpec(spec, module) {
|
|
|
291
327
|
error("feature-governance-drift", `Transition ${transition.name} ${key} differs from command ${command.className}.`);
|
|
292
328
|
}
|
|
293
329
|
}
|
|
330
|
+
if (transition.persistence) {
|
|
331
|
+
if (transition.persistence.boundary === "database" && command.transaction !== "required") {
|
|
332
|
+
error("feature-persistence-drift", `Database transition ${transition.name} requires command ${command.className} to declare transaction: "required".`);
|
|
333
|
+
}
|
|
334
|
+
if (command.tenantScope !== "required") {
|
|
335
|
+
error("feature-command-tenant-boundary", `Command ${command.className} in Feature ${spec.name} must declare tenantScope: "required".`);
|
|
336
|
+
}
|
|
337
|
+
if (transition.persistence.boundary === "external" && command.transaction !== "none") {
|
|
338
|
+
error("feature-persistence-drift", `External transition ${transition.name} requires command ${command.className} to declare transaction: "none".`);
|
|
339
|
+
}
|
|
340
|
+
if (command.idempotency !== "required" || !command.audit) {
|
|
341
|
+
error("feature-persistence-drift", `Persistent transition ${transition.name} requires command ${command.className} to declare idempotency and audit.`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (transition.job) {
|
|
346
|
+
const jobs = (module.jobs ?? []).filter((job) => job.className === transition.job || job.name === transition.job);
|
|
347
|
+
if (jobs.length !== 1) {
|
|
348
|
+
error("feature-job-unresolved", `Transition ${transition.name} must reference exactly one Job in module ${module.name}.`);
|
|
349
|
+
} else {
|
|
350
|
+
const job = jobs[0];
|
|
351
|
+
if (job.tenantScope !== "required") {
|
|
352
|
+
error("feature-job-tenant-boundary", `Job ${job.className} in Feature ${spec.name} must declare tenantScope: "required".`);
|
|
353
|
+
}
|
|
354
|
+
if (job.idempotency !== "required") {
|
|
355
|
+
error("feature-job-idempotency", `Job ${job.className} in Feature ${spec.name} must declare idempotency: "required".`);
|
|
356
|
+
}
|
|
357
|
+
if (job.mode === undefined) {
|
|
358
|
+
error("feature-job-mode", `Job ${job.className} in Feature ${spec.name} must declare mode: "task" or "workflow".`);
|
|
359
|
+
}
|
|
360
|
+
if (job.input === undefined || job.schemaKinds?.input !== "declared") {
|
|
361
|
+
error("feature-job-contract", `Job ${job.className} in Feature ${spec.name} must declare a non-opaque input schema.`);
|
|
362
|
+
}
|
|
363
|
+
const provider = module.providers.find((candidate) => candidate.token === job.className || candidate.useClass === job.className);
|
|
364
|
+
if (job.tenantScope === "required" && provider && !provider.deps.some((token) => isJobContextToken(token))) {
|
|
365
|
+
error("feature-job-context", `Job ${job.className} in Feature ${spec.name} must inject JOB_CONTEXT for trusted tenant execution.`);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
294
368
|
}
|
|
295
369
|
if (transition.route) {
|
|
296
370
|
const routes = module.controllers.flatMap((controller) => controller.routes.filter((route) => `${route.method} ${joinRoutePaths(controller.path, route.path)}` === transition.route));
|
|
297
371
|
if (routes.length !== 1) {
|
|
298
372
|
error("feature-route-unresolved", `Transition ${transition.name} must reference exactly one route '${transition.route}' in module ${module.name}.`);
|
|
299
|
-
} else if (command && routes[0]?.command !== command.className) {
|
|
373
|
+
} else if (command && transition.routeBinding !== "controller" && routes[0]?.command !== command.className) {
|
|
300
374
|
error("feature-route-drift", `Route ${transition.route} is not bound to ${command.className}.`);
|
|
375
|
+
} else if (transition.routeBinding === "controller" && routes[0]?.command !== undefined) {
|
|
376
|
+
error("feature-route-drift", `Controller-bound route ${transition.route} must not also declare a framework command binding.`);
|
|
301
377
|
}
|
|
302
378
|
}
|
|
303
379
|
}
|
|
304
380
|
return diagnostics;
|
|
305
381
|
}
|
|
306
382
|
function generateFeatureSource(spec) {
|
|
307
|
-
const errors = validateFeatureSpec(spec);
|
|
308
|
-
if (errors.length)
|
|
309
|
-
throw new Error(errors.map((error) => error.message).join(`
|
|
310
|
-
`));
|
|
311
383
|
if (spec.transitions.some((transition) => !transition.permission)) {
|
|
312
384
|
throw new Error("Spec-to-Code requires an explicit permission for every transition.");
|
|
313
385
|
}
|
|
@@ -315,13 +387,34 @@ function generateFeatureSource(spec) {
|
|
|
315
387
|
...transition,
|
|
316
388
|
command: `Transition${index + 1}Command`
|
|
317
389
|
}));
|
|
390
|
+
const errors = validateFeatureSpec({ ...spec, transitions });
|
|
391
|
+
if (errors.length)
|
|
392
|
+
throw new Error(errors.map((error) => error.message).join(`
|
|
393
|
+
`));
|
|
318
394
|
if (transitions.some((transition) => transition.route)) {
|
|
319
395
|
throw new Error("Generate the command slice first; existing HTTP routes require explicit schema and handler implementations.");
|
|
320
396
|
}
|
|
321
|
-
const sourceSpec = {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
397
|
+
const sourceSpec = {
|
|
398
|
+
name: spec.name,
|
|
399
|
+
states: spec.states,
|
|
400
|
+
tenantField: spec.tenantField ?? "tenant_id",
|
|
401
|
+
transitions: Object.fromEntries(transitions.map((transition) => {
|
|
402
|
+
const { name, ...options } = transition;
|
|
403
|
+
const persistence = transition.persistence ?? {
|
|
404
|
+
boundary: "database",
|
|
405
|
+
optimisticLock: "required",
|
|
406
|
+
versionField: "version",
|
|
407
|
+
tenantField: "tenant_id",
|
|
408
|
+
stateField: "state",
|
|
409
|
+
receipt: "required",
|
|
410
|
+
reconciliation: "none"
|
|
411
|
+
};
|
|
412
|
+
return [name, {
|
|
413
|
+
...options,
|
|
414
|
+
persistence
|
|
415
|
+
}];
|
|
416
|
+
}))
|
|
417
|
+
};
|
|
325
418
|
return [
|
|
326
419
|
'import { Command, defineFeatureSlice, defineFeatureSpec } from "@supacloud/app";',
|
|
327
420
|
"",
|
|
@@ -332,9 +425,10 @@ function generateFeatureSource(spec) {
|
|
|
332
425
|
`@Command(${JSON.stringify({
|
|
333
426
|
name: `${spec.name}.${transition.name}`,
|
|
334
427
|
permission: transition.permission,
|
|
335
|
-
transaction: transition.transaction ?? "none",
|
|
336
|
-
idempotency: transition.idempotency ?? "
|
|
337
|
-
audit: transition.audit
|
|
428
|
+
transaction: transition.transaction ?? (transition.persistence?.boundary === "external" ? "none" : "required"),
|
|
429
|
+
idempotency: transition.idempotency ?? "required",
|
|
430
|
+
audit: transition.audit ?? `${spec.name}.${transition.name}`,
|
|
431
|
+
tenantScope: "required"
|
|
338
432
|
})})`,
|
|
339
433
|
`export class ${transition.command} {`,
|
|
340
434
|
` execute(state: FeatureState): never {`,
|
|
@@ -802,13 +896,40 @@ function validateGraph(graph, options = false) {
|
|
|
802
896
|
permission: `${module.name}.${command.name}`
|
|
803
897
|
});
|
|
804
898
|
}
|
|
899
|
+
if (command.rpc && command.branches) {
|
|
900
|
+
error("command-rpc-branches-conflict", `Command ${command.name} cannot declare both rpc and branches.`, module.file, module.line);
|
|
901
|
+
}
|
|
902
|
+
const branchAdapters = command.branches ?? [];
|
|
903
|
+
for (const branch of branchAdapters) {
|
|
904
|
+
if (typeof options !== "object" || !Object.hasOwn(options.commandCapabilities?.rpc ?? {}, branch.rpc)) {
|
|
905
|
+
error("command-rpc-unavailable", `Command ${command.name} requires configured RPC adapter '${branch.rpc}'.`, module.file, module.line);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
805
908
|
if (command.rpc && (typeof options !== "object" || !Object.hasOwn(options.commandCapabilities?.rpc ?? {}, command.rpc))) {
|
|
806
909
|
error("command-rpc-unavailable", `Command ${command.name} requires configured RPC adapter '${command.rpc}'.`, module.file, module.line);
|
|
807
910
|
}
|
|
808
911
|
if (typeof options === "object" && options.commandCapabilities) {
|
|
809
912
|
const hostCaps = options.commandCapabilities;
|
|
810
913
|
const rpcCaps = command.rpc && Object.hasOwn(hostCaps.rpc ?? {}, command.rpc) ? hostCaps.rpc?.[command.rpc] : undefined;
|
|
811
|
-
|
|
914
|
+
const branchCaps = branchAdapters.map((branch) => ({
|
|
915
|
+
branch,
|
|
916
|
+
capabilities: Object.hasOwn(hostCaps.rpc ?? {}, branch.rpc) ? hostCaps.rpc?.[branch.rpc] : undefined
|
|
917
|
+
}));
|
|
918
|
+
for (const { branch, capabilities } of branchCaps) {
|
|
919
|
+
if (capabilities?.boundary === "external" && (branch.transaction === "required" || capabilities.transaction === true)) {
|
|
920
|
+
error("command-external-transaction", `Command ${command.name} branch '${branch.when}' calls an external service; use transaction: 'none'.`, module.file, module.line, "Use transaction: 'none' with durable intent and reconciliation.");
|
|
921
|
+
}
|
|
922
|
+
if (branch.audit && capabilities?.audit !== true) {
|
|
923
|
+
error("command-audit-unsupported", `Command ${command.name} branch '${branch.when}' declares audit, but runtime audit persistence is unavailable.`, module.file, module.line);
|
|
924
|
+
}
|
|
925
|
+
if (branch.idempotency === "required" && capabilities?.idempotency !== true) {
|
|
926
|
+
error("command-idempotency-unsupported", `Command ${command.name} branch '${branch.when}' declares idempotency, but receipt persistence is unavailable.`, module.file, module.line);
|
|
927
|
+
}
|
|
928
|
+
if (branch.transaction === "required" && capabilities?.transaction !== true) {
|
|
929
|
+
error("command-transaction-unsupported", `Command ${command.name} branch '${branch.when}' declares transaction, but transaction support is unavailable.`, module.file, module.line);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (hostCaps.requirePersistentAdapters && (command.rpc !== undefined && (!rpcCaps?.boundary || rpcCaps.audit !== true || rpcCaps.idempotency !== true) || branchCaps.length > 0 && branchCaps.some(({ branch, capabilities }) => !capabilities?.boundary || capabilities.audit !== true || capabilities.idempotency !== true || capabilities.boundary === "database" && (capabilities.transaction !== true || branch.transaction !== "required")) || hostCaps.permission !== true || !command.permission || branchCaps.length === 0 && (!command.audit || command.idempotency !== "required") || branchCaps.length > 0 && branchCaps.some(({ branch }) => !branch.audit || branch.idempotency !== "required") || branchCaps.length > 0 && branchCaps.some(({ capabilities }) => capabilities?.boundary === "external" && capabilities.reconcile !== true) || command.rpc !== undefined && rpcCaps?.boundary === "database" && (rpcCaps.transaction !== true || command.transaction !== "required") || command.rpc === undefined && branchCaps.length === 0 && (hostCaps.audit !== true || hostCaps.idempotency !== true || hostCaps.transaction !== true || command.transaction !== "required") || command.rpc !== undefined && rpcCaps?.boundary === "external" && rpcCaps.reconcile !== true)) {
|
|
812
933
|
error("command-persistence-required", `Command ${command.name} requires an explicit persistent adapter, permission, audit and idempotency policy.`, module.file, module.line, "Register a named database/external adapter, enable permission checks and declare permission, audit and required idempotency on the command.");
|
|
813
934
|
}
|
|
814
935
|
if (rpcCaps?.boundary === "external" && (command.transaction === "required" || rpcCaps.transaction === true)) {
|
|
@@ -1284,6 +1405,18 @@ var init_validate = __esm(() => {
|
|
|
1284
1405
|
"invalid-feature-transition": { code: "SC6103", docsUrl: "https://supacloud.dev/errors/SC6103" },
|
|
1285
1406
|
"feature-command-unresolved": { code: "SC6104", docsUrl: "https://supacloud.dev/errors/SC6104" },
|
|
1286
1407
|
"feature-governance-drift": { code: "SC6105", docsUrl: "https://supacloud.dev/errors/SC6105" },
|
|
1408
|
+
"feature-persistence-invalid": { code: "SC6110", docsUrl: "https://supacloud.dev/errors/SC6110" },
|
|
1409
|
+
"feature-persistence-drift": { code: "SC6111", docsUrl: "https://supacloud.dev/errors/SC6111" },
|
|
1410
|
+
"feature-tenant-boundary-invalid": { code: "SC6112", docsUrl: "https://supacloud.dev/errors/SC6112" },
|
|
1411
|
+
"feature-tenant-boundary-drift": { code: "SC6113", docsUrl: "https://supacloud.dev/errors/SC6113" },
|
|
1412
|
+
"feature-query-tenant-boundary": { code: "SC6114", docsUrl: "https://supacloud.dev/errors/SC6114" },
|
|
1413
|
+
"feature-job-tenant-boundary": { code: "SC6115", docsUrl: "https://supacloud.dev/errors/SC6115" },
|
|
1414
|
+
"feature-command-tenant-boundary": { code: "SC6116", docsUrl: "https://supacloud.dev/errors/SC6116" },
|
|
1415
|
+
"feature-job-unresolved": { code: "SC6117", docsUrl: "https://supacloud.dev/errors/SC6117" },
|
|
1416
|
+
"feature-job-idempotency": { code: "SC6118", docsUrl: "https://supacloud.dev/errors/SC6118" },
|
|
1417
|
+
"feature-job-mode": { code: "SC6119", docsUrl: "https://supacloud.dev/errors/SC6119" },
|
|
1418
|
+
"feature-job-contract": { code: "SC6120", docsUrl: "https://supacloud.dev/errors/SC6120" },
|
|
1419
|
+
"feature-job-context": { code: "SC6121", docsUrl: "https://supacloud.dev/errors/SC6121" },
|
|
1287
1420
|
"feature-route-unresolved": { code: "SC6106", docsUrl: "https://supacloud.dev/errors/SC6106" },
|
|
1288
1421
|
"feature-route-drift": { code: "SC6107", docsUrl: "https://supacloud.dev/errors/SC6107" },
|
|
1289
1422
|
"invalid-feature-spec": { code: "SC6108", docsUrl: "https://supacloud.dev/errors/SC6108" }
|
|
@@ -1294,6 +1427,8 @@ var init_validate = __esm(() => {
|
|
|
1294
1427
|
function buildContractManifest(graph, artifacts) {
|
|
1295
1428
|
const commands = [];
|
|
1296
1429
|
const queries = [];
|
|
1430
|
+
const jobs = [];
|
|
1431
|
+
const features = [];
|
|
1297
1432
|
const routes = [];
|
|
1298
1433
|
const permissions = new Set;
|
|
1299
1434
|
const rpc = [];
|
|
@@ -1305,21 +1440,95 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1305
1440
|
permissions.add(command.permission);
|
|
1306
1441
|
if (command.rpc)
|
|
1307
1442
|
rpc.push({ command: command.name, adapter: command.rpc });
|
|
1443
|
+
for (const branch of command.branches ?? []) {
|
|
1444
|
+
rpc.push({ command: `${command.name} [${branch.when}]`, adapter: branch.rpc });
|
|
1445
|
+
}
|
|
1308
1446
|
if (command.audit)
|
|
1309
1447
|
events.push({ name: command.audit, source: "command.audit", command: command.name });
|
|
1448
|
+
for (const branch of command.branches ?? []) {
|
|
1449
|
+
if (branch.audit)
|
|
1450
|
+
events.push({ name: branch.audit, source: "command.branch.audit", command: command.name });
|
|
1451
|
+
}
|
|
1310
1452
|
commands.push({
|
|
1311
1453
|
name: command.name,
|
|
1312
1454
|
className: command.className,
|
|
1313
1455
|
module: module.name,
|
|
1314
1456
|
...command.permission === undefined ? {} : { permission: command.permission },
|
|
1315
1457
|
...command.rpc === undefined ? {} : { rpc: command.rpc },
|
|
1458
|
+
...command.branches === undefined ? {} : {
|
|
1459
|
+
branches: command.branches.map((branch) => ({
|
|
1460
|
+
when: branch.when,
|
|
1461
|
+
adapter: branch.rpc,
|
|
1462
|
+
transaction: branch.transaction,
|
|
1463
|
+
idempotency: branch.idempotency,
|
|
1464
|
+
...branch.audit === undefined ? {} : { auditEvent: branch.audit },
|
|
1465
|
+
...branch.boundary === undefined ? {} : { boundary: branch.boundary }
|
|
1466
|
+
}))
|
|
1467
|
+
},
|
|
1316
1468
|
transaction: command.transaction,
|
|
1317
1469
|
idempotency: command.idempotency,
|
|
1470
|
+
...command.tenantScope === undefined ? {} : { tenantScope: command.tenantScope },
|
|
1318
1471
|
...command.audit === undefined ? {} : { auditEvent: command.audit }
|
|
1319
1472
|
});
|
|
1320
1473
|
}
|
|
1321
1474
|
for (const query of module.queries) {
|
|
1322
|
-
queries.push({
|
|
1475
|
+
queries.push({
|
|
1476
|
+
name: query.name,
|
|
1477
|
+
className: query.className,
|
|
1478
|
+
module: module.name,
|
|
1479
|
+
...query.tenantScope === undefined ? {} : { tenantScope: query.tenantScope }
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
for (const job of module.jobs ?? []) {
|
|
1483
|
+
jobs.push({
|
|
1484
|
+
name: job.name,
|
|
1485
|
+
className: job.className,
|
|
1486
|
+
module: module.name,
|
|
1487
|
+
...job.input === undefined ? {} : { input: job.input },
|
|
1488
|
+
...job.schemaKinds?.input === undefined ? {} : { inputKind: job.schemaKinds.input },
|
|
1489
|
+
...job.output === undefined ? {} : { output: job.output },
|
|
1490
|
+
...job.schemaKinds?.output === undefined ? {} : { outputKind: job.schemaKinds.output },
|
|
1491
|
+
...job.mode === undefined ? {} : { mode: job.mode },
|
|
1492
|
+
...job.idempotency === undefined ? {} : { idempotency: job.idempotency },
|
|
1493
|
+
...job.tenantScope === undefined ? {} : { tenantScope: job.tenantScope }
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
if (module.featureSpec) {
|
|
1497
|
+
features.push({
|
|
1498
|
+
name: module.featureSpec.name,
|
|
1499
|
+
states: [...module.featureSpec.states],
|
|
1500
|
+
...module.featureSpec.tenantField === undefined ? {} : { tenantField: module.featureSpec.tenantField },
|
|
1501
|
+
transitions: module.featureSpec.transitions.map((transition) => {
|
|
1502
|
+
const job = transition.job === undefined ? undefined : (module.jobs ?? []).find((candidate) => candidate.className === transition.job || candidate.name === transition.job);
|
|
1503
|
+
return {
|
|
1504
|
+
name: transition.name,
|
|
1505
|
+
from: transition.from,
|
|
1506
|
+
to: transition.to,
|
|
1507
|
+
...transition.permission === undefined ? {} : { permission: transition.permission },
|
|
1508
|
+
...transition.command === undefined ? {} : { command: transition.command },
|
|
1509
|
+
...transition.route === undefined ? {} : { route: transition.route },
|
|
1510
|
+
...transition.routeBinding === undefined ? {} : { routeBinding: transition.routeBinding },
|
|
1511
|
+
...transition.transaction === undefined ? {} : { transaction: transition.transaction },
|
|
1512
|
+
...transition.idempotency === undefined ? {} : { idempotency: transition.idempotency },
|
|
1513
|
+
...transition.audit === undefined ? {} : { audit: transition.audit },
|
|
1514
|
+
...transition.job === undefined ? {} : { job: transition.job },
|
|
1515
|
+
...job === undefined ? {} : {
|
|
1516
|
+
jobDetails: {
|
|
1517
|
+
name: job.name,
|
|
1518
|
+
className: job.className,
|
|
1519
|
+
...job.input === undefined ? {} : { input: job.input },
|
|
1520
|
+
...job.schemaKinds?.input === undefined ? {} : { inputKind: job.schemaKinds.input },
|
|
1521
|
+
...job.output === undefined ? {} : { output: job.output },
|
|
1522
|
+
...job.schemaKinds?.output === undefined ? {} : { outputKind: job.schemaKinds.output },
|
|
1523
|
+
...job.mode === undefined ? {} : { mode: job.mode },
|
|
1524
|
+
...job.idempotency === undefined ? {} : { idempotency: job.idempotency },
|
|
1525
|
+
...job.tenantScope === undefined ? {} : { tenantScope: job.tenantScope }
|
|
1526
|
+
}
|
|
1527
|
+
},
|
|
1528
|
+
...transition.persistence === undefined ? {} : { persistence: transition.persistence }
|
|
1529
|
+
};
|
|
1530
|
+
})
|
|
1531
|
+
});
|
|
1323
1532
|
}
|
|
1324
1533
|
for (const controller of module.controllers) {
|
|
1325
1534
|
for (const route of controller.routes) {
|
|
@@ -1350,6 +1559,8 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1350
1559
|
}
|
|
1351
1560
|
commands.sort((left, right) => left.name.localeCompare(right.name));
|
|
1352
1561
|
queries.sort((left, right) => left.name.localeCompare(right.name));
|
|
1562
|
+
jobs.sort((left, right) => left.name.localeCompare(right.name));
|
|
1563
|
+
features.sort((left, right) => left.name.localeCompare(right.name));
|
|
1353
1564
|
routes.sort((left, right) => `${left.method} ${left.path}`.localeCompare(`${right.method} ${right.path}`));
|
|
1354
1565
|
rpc.sort((left, right) => left.command.localeCompare(right.command));
|
|
1355
1566
|
events.sort((left, right) => left.name.localeCompare(right.name));
|
|
@@ -1357,6 +1568,8 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1357
1568
|
version: 1,
|
|
1358
1569
|
commands,
|
|
1359
1570
|
queries,
|
|
1571
|
+
jobs,
|
|
1572
|
+
features,
|
|
1360
1573
|
routes,
|
|
1361
1574
|
permissions: [...permissions].sort(),
|
|
1362
1575
|
rpc,
|
|
@@ -1504,6 +1717,7 @@ function renderApplication(graph, options) {
|
|
|
1504
1717
|
name: command.name,
|
|
1505
1718
|
permission: command.permission ?? null,
|
|
1506
1719
|
rpc: command.rpc ?? null,
|
|
1720
|
+
branches: command.branches ?? [],
|
|
1507
1721
|
transaction: command.transaction ?? null,
|
|
1508
1722
|
audit: command.audit ?? null,
|
|
1509
1723
|
idempotency: command.idempotency ?? null
|
|
@@ -1930,7 +2144,9 @@ ${indent(item, 2)}`).join(",")}
|
|
|
1930
2144
|
`transaction: ${JSON.stringify(command.transaction)}`,
|
|
1931
2145
|
...command.audit ? [`audit: ${JSON.stringify(command.audit)}`] : [],
|
|
1932
2146
|
`idempotency: ${JSON.stringify(command.idempotency)}`,
|
|
2147
|
+
...command.tenantScope ? [`tenantScope: ${JSON.stringify(command.tenantScope)}`] : [],
|
|
1933
2148
|
...command.rpc ? [`rpc: ${JSON.stringify(command.rpc)}`] : [],
|
|
2149
|
+
...command.branches && command.branches.length > 0 ? [`branches: ${JSON.stringify(command.branches)}`] : [],
|
|
1934
2150
|
...command.standalone ? ["standalone: true"] : [],
|
|
1935
2151
|
...command.aspects && command.aspects.length > 0 ? [`aspects: ${this.renderAspects(command.aspects)}`, `aspectPipeline: ${this.renderAspectPipeline(command.aspects)}`] : []
|
|
1936
2152
|
];
|
|
@@ -1963,6 +2179,8 @@ ${indent(item, 2)}`).join(",")}
|
|
|
1963
2179
|
fields.push(`maxAttempts: ${job.maxAttempts}`);
|
|
1964
2180
|
if (job.idempotency !== undefined)
|
|
1965
2181
|
fields.push(`idempotency: ${JSON.stringify(job.idempotency)}`);
|
|
2182
|
+
if (job.tenantScope !== undefined)
|
|
2183
|
+
fields.push(`tenantScope: ${JSON.stringify(job.tenantScope)}`);
|
|
1966
2184
|
if (job.aspects && job.aspects.length > 0) {
|
|
1967
2185
|
fields.push(`aspects: ${this.renderAspects(job.aspects)}`);
|
|
1968
2186
|
fields.push(`aspectPipeline: ${this.renderAspectPipeline(job.aspects)}`);
|
|
@@ -3666,17 +3884,28 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
|
|
|
3666
3884
|
|
|
3667
3885
|
export interface CompiledCommand {
|
|
3668
3886
|
rpc?: string;
|
|
3887
|
+
branches?: readonly CompiledCommandBranch[];
|
|
3669
3888
|
className: string;
|
|
3670
3889
|
name: string;
|
|
3671
3890
|
permission: string;
|
|
3672
3891
|
transaction: "required" | "none";
|
|
3673
3892
|
audit?: string;
|
|
3674
3893
|
idempotency: "required" | "none";
|
|
3894
|
+
tenantScope?: "required" | "none";
|
|
3675
3895
|
standalone?: boolean;
|
|
3676
3896
|
aspects?: CompiledAspect[];
|
|
3677
3897
|
aspectPipeline?: CompiledAspectPipeline;
|
|
3678
3898
|
}
|
|
3679
3899
|
|
|
3900
|
+
export interface CompiledCommandBranch {
|
|
3901
|
+
when: string;
|
|
3902
|
+
rpc: string;
|
|
3903
|
+
transaction?: "required" | "none" | string;
|
|
3904
|
+
audit?: string;
|
|
3905
|
+
idempotency?: "required" | "none" | string;
|
|
3906
|
+
boundary?: "database" | "external";
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3680
3909
|
export interface CompiledJob {
|
|
3681
3910
|
className: string;
|
|
3682
3911
|
name: string;
|
|
@@ -3688,6 +3917,7 @@ export interface CompiledJob {
|
|
|
3688
3917
|
timeoutSec?: number;
|
|
3689
3918
|
maxAttempts?: number;
|
|
3690
3919
|
idempotency?: "required" | "none";
|
|
3920
|
+
tenantScope?: "required" | "none";
|
|
3691
3921
|
aspects?: CompiledAspect[];
|
|
3692
3922
|
aspectPipeline?: CompiledAspectPipeline;
|
|
3693
3923
|
}
|
|
@@ -4609,6 +4839,7 @@ function createContextPack(graph, subject) {
|
|
|
4609
4839
|
} : undefined;
|
|
4610
4840
|
const files = [...new Set(modules.flatMap((module) => [
|
|
4611
4841
|
module.file,
|
|
4842
|
+
...module.featureSpec?.file ? [module.featureSpec.file] : [],
|
|
4612
4843
|
...module.providers.map((provider) => provider.file),
|
|
4613
4844
|
...module.controllers.map((controller) => controller.file),
|
|
4614
4845
|
...graph.moduleHandlerFiles?.[module.name] ?? [],
|
|
@@ -16218,7 +16449,7 @@ async function readDeliverySelection(manifestPath, target) {
|
|
|
16218
16449
|
return { root, object, planned };
|
|
16219
16450
|
}
|
|
16220
16451
|
async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
|
|
16221
|
-
if (object.files.length > 1024 || object.files.some((file) => file.bytes >
|
|
16452
|
+
if (object.files.length > 1024 || object.files.some((file) => file.bytes > DELIVERY_ARTIFACT_MAX_FILE_BYTES) || object.files.reduce((bytes, file) => bytes + file.bytes, 0) > DELIVERY_ARTIFACT_MAX_TOTAL_BYTES) {
|
|
16222
16453
|
throw new Error("Artifact inventory exceeds limits.");
|
|
16223
16454
|
}
|
|
16224
16455
|
const objectRoot = join7(root, "objects", object.objectId);
|
|
@@ -16257,10 +16488,12 @@ async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
|
|
|
16257
16488
|
}
|
|
16258
16489
|
return retained;
|
|
16259
16490
|
}
|
|
16260
|
-
var JSON_LIMIT = 1048576;
|
|
16491
|
+
var JSON_LIMIT = 1048576, DELIVERY_ARTIFACT_MAX_FILE_BYTES, DELIVERY_ARTIFACT_MAX_TOTAL_BYTES;
|
|
16261
16492
|
var init_delivery_artifact = __esm(() => {
|
|
16262
16493
|
init_delivery_build_schema();
|
|
16263
16494
|
init_delivery_files();
|
|
16495
|
+
DELIVERY_ARTIFACT_MAX_FILE_BYTES = 500 * 1024 * 1024;
|
|
16496
|
+
DELIVERY_ARTIFACT_MAX_TOTAL_BYTES = 500 * 1024 * 1024;
|
|
16264
16497
|
});
|
|
16265
16498
|
|
|
16266
16499
|
// src/delivery-artifact.ts
|
|
@@ -17425,6 +17658,7 @@ var init_delivery_migration_plan = () => {};
|
|
|
17425
17658
|
|
|
17426
17659
|
// node_modules/@supacloud/delivery/src/index.ts
|
|
17427
17660
|
var init_src = __esm(() => {
|
|
17661
|
+
init_delivery_artifact();
|
|
17428
17662
|
init_delivery_artifact();
|
|
17429
17663
|
init_worker_execution();
|
|
17430
17664
|
init_worker_execution();
|
|
@@ -18806,6 +19040,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
|
|
|
18806
19040
|
if (meta && booleanProp(meta, "standalone")) {
|
|
18807
19041
|
const permission = stringLiteralProp(meta, "permission");
|
|
18808
19042
|
const rpc = checkedRpc(meta, ctx);
|
|
19043
|
+
const branches = checkedBranches(meta, ctx);
|
|
18809
19044
|
const audit = stringLiteralProp(meta, "audit");
|
|
18810
19045
|
const standaloneName = stringLiteralProp(meta, "name") ?? classInfo.decl.name?.text ?? name;
|
|
18811
19046
|
const standaloneUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${standaloneName}`, sourcePath(ctx.rootDir, classInfo.file), lineOf(classInfo.decl));
|
|
@@ -18815,6 +19050,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
|
|
|
18815
19050
|
name: standaloneName,
|
|
18816
19051
|
...permission === undefined ? {} : { permission },
|
|
18817
19052
|
...rpc === undefined ? {} : { rpc },
|
|
19053
|
+
...branches === undefined ? {} : { branches },
|
|
18818
19054
|
transaction: checkedCommandMode(meta, "transaction", ctx, classInfo.decl.name?.text ?? name) ?? "none",
|
|
18819
19055
|
...audit === undefined ? {} : { audit },
|
|
18820
19056
|
idempotency: checkedCommandMode(meta, "idempotency", ctx, classInfo.decl.name?.text ?? name) ?? "none",
|
|
@@ -19146,18 +19382,22 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19146
19382
|
const aspects = parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${cls.name?.text ?? "<anonymous>"}`);
|
|
19147
19383
|
const permission = stringLiteralProp(meta, "permission");
|
|
19148
19384
|
const rpc = checkedRpc(meta, ctx);
|
|
19385
|
+
const branches = checkedBranches(meta, ctx);
|
|
19149
19386
|
const audit = stringLiteralProp(meta, "audit");
|
|
19150
19387
|
const commandName = stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>";
|
|
19151
19388
|
const uses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19152
19389
|
rejectUndeclaredResourceUses(uses, moduleResourceNames, ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19390
|
+
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19153
19391
|
commands.push({
|
|
19154
19392
|
className: cls.name?.text ?? "<anonymous>",
|
|
19155
19393
|
name: commandName,
|
|
19156
19394
|
...permission === undefined ? {} : { permission },
|
|
19157
19395
|
...rpc === undefined ? {} : { rpc },
|
|
19396
|
+
...branches === undefined ? {} : { branches },
|
|
19158
19397
|
transaction: checkedCommandMode(meta, "transaction", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
|
|
19159
19398
|
...audit === undefined ? {} : { audit },
|
|
19160
19399
|
idempotency: checkedCommandMode(meta, "idempotency", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
|
|
19400
|
+
...tenantScope === undefined ? {} : { tenantScope },
|
|
19161
19401
|
...booleanProp(meta, "standalone") ? { standalone: true } : {},
|
|
19162
19402
|
...uses.length > 0 ? { uses } : {},
|
|
19163
19403
|
...aspects.length > 0 ? { aspects } : {}
|
|
@@ -19188,12 +19428,14 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19188
19428
|
const contract = parseJobOptions(meta, className, ctx);
|
|
19189
19429
|
const jobUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19190
19430
|
rejectUndeclaredResourceUses(jobUses, moduleResourceNames, ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19431
|
+
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19191
19432
|
jobs.push({
|
|
19192
19433
|
className,
|
|
19193
19434
|
name: stringLiteralProp(meta, "name") ?? className,
|
|
19194
19435
|
serviceKey: camelName(provider?.token ?? className),
|
|
19195
19436
|
scope,
|
|
19196
19437
|
...contract,
|
|
19438
|
+
...tenantScope === undefined ? {} : { tenantScope },
|
|
19197
19439
|
...jobUses.length > 0 ? { uses: jobUses } : {},
|
|
19198
19440
|
...aspects.length > 0 ? { aspects } : {}
|
|
19199
19441
|
});
|
|
@@ -19203,9 +19445,11 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19203
19445
|
if (queryDec) {
|
|
19204
19446
|
const meta = decoratorObjectArg(queryDec);
|
|
19205
19447
|
if (meta) {
|
|
19448
|
+
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19206
19449
|
queries.push({
|
|
19207
19450
|
className: cls.name?.text ?? "<anonymous>",
|
|
19208
|
-
name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>"
|
|
19451
|
+
name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
|
|
19452
|
+
...tenantScope === undefined ? {} : { tenantScope }
|
|
19209
19453
|
});
|
|
19210
19454
|
}
|
|
19211
19455
|
}
|
|
@@ -19260,6 +19504,7 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19260
19504
|
if (!ts7.isObjectLiteralExpression(input))
|
|
19261
19505
|
return invalid();
|
|
19262
19506
|
const name = stringLiteralProp(input, "name");
|
|
19507
|
+
const tenantField = stringLiteralProp(input, "tenantField");
|
|
19263
19508
|
const statesExpr = getProp(input, "states");
|
|
19264
19509
|
const transitionObject = getProp(input, "transitions");
|
|
19265
19510
|
if (!name || !statesExpr || !ts7.isArrayLiteralExpression(statesExpr) || statesExpr.elements.some((state) => !ts7.isStringLiteral(state)) || !transitionObject || !ts7.isObjectLiteralExpression(transitionObject) || input.properties.some((property) => !ts7.isPropertyAssignment(property))) {
|
|
@@ -19278,15 +19523,23 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19278
19523
|
const options = property.initializer;
|
|
19279
19524
|
const from = stringLiteralProp(options, "from");
|
|
19280
19525
|
const to = stringLiteralProp(options, "to");
|
|
19281
|
-
if (!from || !to || options.properties.some((prop) => !ts7.isPropertyAssignment(prop)) || ["permission", "command", "route", "audit"].some((key) => getProp(options, key) && !stringLiteralProp(options, key)) || ["transaction", "idempotency"].some((key) => getProp(options, key) && !commandModeProp(options, key))) {
|
|
19526
|
+
if (!from || !to || options.properties.some((prop) => !ts7.isPropertyAssignment(prop)) || ["permission", "command", "route", "routeBinding", "audit", "job"].some((key) => getProp(options, key) && !stringLiteralProp(options, key)) || ["transaction", "idempotency"].some((key) => getProp(options, key) && !commandModeProp(options, key))) {
|
|
19282
19527
|
return invalid();
|
|
19283
19528
|
}
|
|
19284
19529
|
const permission = stringLiteralProp(options, "permission");
|
|
19285
19530
|
const command = stringLiteralProp(options, "command");
|
|
19286
19531
|
const route = stringLiteralProp(options, "route");
|
|
19532
|
+
const routeBinding = stringLiteralProp(options, "routeBinding");
|
|
19533
|
+
if (routeBinding !== undefined && routeBinding !== "command" && routeBinding !== "controller")
|
|
19534
|
+
return invalid();
|
|
19287
19535
|
const transaction = commandModeProp(options, "transaction");
|
|
19288
19536
|
const idempotency = commandModeProp(options, "idempotency");
|
|
19289
19537
|
const audit = stringLiteralProp(options, "audit");
|
|
19538
|
+
const job = stringLiteralProp(options, "job");
|
|
19539
|
+
const persistenceExpr = getProp(options, "persistence");
|
|
19540
|
+
const persistence = persistenceExpr && ts7.isObjectLiteralExpression(persistenceExpr) ? parseFeaturePersistence(persistenceExpr) : undefined;
|
|
19541
|
+
if (persistenceExpr && !persistence)
|
|
19542
|
+
return invalid();
|
|
19290
19543
|
transitions.push({
|
|
19291
19544
|
name: propertyName(property.name),
|
|
19292
19545
|
from,
|
|
@@ -19294,12 +19547,44 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19294
19547
|
...permission === undefined ? {} : { permission },
|
|
19295
19548
|
...command === undefined ? {} : { command },
|
|
19296
19549
|
...route === undefined ? {} : { route },
|
|
19550
|
+
...routeBinding === undefined ? {} : { routeBinding },
|
|
19297
19551
|
...transaction === undefined ? {} : { transaction },
|
|
19298
19552
|
...idempotency === undefined ? {} : { idempotency },
|
|
19299
|
-
...audit === undefined ? {} : { audit }
|
|
19553
|
+
...audit === undefined ? {} : { audit },
|
|
19554
|
+
...job === undefined ? {} : { job },
|
|
19555
|
+
...persistence === undefined ? {} : { persistence }
|
|
19300
19556
|
});
|
|
19301
19557
|
}
|
|
19302
|
-
return {
|
|
19558
|
+
return {
|
|
19559
|
+
name,
|
|
19560
|
+
states,
|
|
19561
|
+
...tenantField === undefined ? {} : { tenantField },
|
|
19562
|
+
transitions,
|
|
19563
|
+
file: sourcePath(ctx.rootDir, input.getSourceFile().fileName),
|
|
19564
|
+
line: lineOf(input)
|
|
19565
|
+
};
|
|
19566
|
+
}
|
|
19567
|
+
function parseFeaturePersistence(input) {
|
|
19568
|
+
if (input.properties.some((property) => !ts7.isPropertyAssignment(property)))
|
|
19569
|
+
return;
|
|
19570
|
+
const boundary = stringLiteralProp(input, "boundary");
|
|
19571
|
+
const optimisticLock = commandModeProp(input, "optimisticLock");
|
|
19572
|
+
const versionField = stringLiteralProp(input, "versionField");
|
|
19573
|
+
const tenantField = stringLiteralProp(input, "tenantField");
|
|
19574
|
+
const stateField = stringLiteralProp(input, "stateField");
|
|
19575
|
+
const receipt = commandModeProp(input, "receipt");
|
|
19576
|
+
const reconciliation = commandModeProp(input, "reconciliation");
|
|
19577
|
+
if (boundary !== "database" && boundary !== "external" || optimisticLock !== "required" && optimisticLock !== "none" || receipt !== "required" && receipt !== "none" || reconciliation !== "required" && reconciliation !== "none")
|
|
19578
|
+
return;
|
|
19579
|
+
return {
|
|
19580
|
+
boundary,
|
|
19581
|
+
optimisticLock,
|
|
19582
|
+
...versionField === undefined ? {} : { versionField },
|
|
19583
|
+
...tenantField === undefined ? {} : { tenantField },
|
|
19584
|
+
...stateField === undefined ? {} : { stateField },
|
|
19585
|
+
receipt,
|
|
19586
|
+
reconciliation
|
|
19587
|
+
};
|
|
19303
19588
|
}
|
|
19304
19589
|
function resolveStaticObjectLiteral(input, ctx, seen = new Set) {
|
|
19305
19590
|
if (!input || seen.has(input))
|
|
@@ -20726,6 +21011,75 @@ function checkedRpc(meta, ctx) {
|
|
|
20726
21011
|
});
|
|
20727
21012
|
return;
|
|
20728
21013
|
}
|
|
21014
|
+
function checkedBranches(meta, ctx) {
|
|
21015
|
+
const expression = getProp(meta, "branches");
|
|
21016
|
+
if (!expression)
|
|
21017
|
+
return;
|
|
21018
|
+
if (!ts7.isArrayLiteralExpression(expression)) {
|
|
21019
|
+
ctx.diagnostics.push({
|
|
21020
|
+
severity: "error",
|
|
21021
|
+
code: "invalid-command-branches",
|
|
21022
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
21023
|
+
line: lineOf(expression),
|
|
21024
|
+
message: "Command branches must be an explicit object array.",
|
|
21025
|
+
suggestion: "Declare each branch as { when, rpc, transaction, audit, idempotency, boundary }."
|
|
21026
|
+
});
|
|
21027
|
+
return;
|
|
21028
|
+
}
|
|
21029
|
+
const branches = [];
|
|
21030
|
+
for (const item of expression.elements) {
|
|
21031
|
+
if (!ts7.isObjectLiteralExpression(item)) {
|
|
21032
|
+
ctx.diagnostics.push({
|
|
21033
|
+
severity: "error",
|
|
21034
|
+
code: "invalid-command-branches",
|
|
21035
|
+
file: sourcePath(ctx.rootDir, item.getSourceFile().fileName),
|
|
21036
|
+
line: lineOf(item),
|
|
21037
|
+
message: "Command branches entries must be object literals."
|
|
21038
|
+
});
|
|
21039
|
+
continue;
|
|
21040
|
+
}
|
|
21041
|
+
const when = stringLiteralProp(item, "when");
|
|
21042
|
+
const rpc = stringLiteralProp(item, "rpc");
|
|
21043
|
+
const transaction = checkedCommandMode(item, "transaction", ctx, "command branch") ?? "none";
|
|
21044
|
+
const audit = stringLiteralProp(item, "audit");
|
|
21045
|
+
const idempotency = checkedCommandMode(item, "idempotency", ctx, "command branch") ?? "none";
|
|
21046
|
+
const boundary = stringLiteralProp(item, "boundary");
|
|
21047
|
+
const validBoundary = boundary === undefined || boundary === "database" || boundary === "external";
|
|
21048
|
+
const validWhen = when !== undefined && /^(?:[A-Za-z_$][\w$]*)(?:\.[A-Za-z_$][\w$]*)*\s*===\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')$/.test(when);
|
|
21049
|
+
if (!when || !validWhen || !rpc?.trim() || !validBoundary) {
|
|
21050
|
+
ctx.diagnostics.push({
|
|
21051
|
+
severity: "error",
|
|
21052
|
+
code: "invalid-command-branches",
|
|
21053
|
+
file: sourcePath(ctx.rootDir, item.getSourceFile().fileName),
|
|
21054
|
+
line: lineOf(item),
|
|
21055
|
+
message: "Command branches require a restricted equality selector, a non-empty rpc, and a valid boundary.",
|
|
21056
|
+
suggestion: `Use when: 'resource === "value"' (or body.resource/params.resource/query.resource), plus a named RPC adapter.`
|
|
21057
|
+
});
|
|
21058
|
+
continue;
|
|
21059
|
+
}
|
|
21060
|
+
branches.push({
|
|
21061
|
+
when,
|
|
21062
|
+
rpc,
|
|
21063
|
+
transaction,
|
|
21064
|
+
...audit === undefined ? {} : { audit },
|
|
21065
|
+
idempotency,
|
|
21066
|
+
...boundary === undefined ? {} : { boundary }
|
|
21067
|
+
});
|
|
21068
|
+
}
|
|
21069
|
+
if (branches.length === 0)
|
|
21070
|
+
return [];
|
|
21071
|
+
if (new Set(branches.map((branch) => branch.when)).size !== branches.length) {
|
|
21072
|
+
ctx.diagnostics.push({
|
|
21073
|
+
severity: "error",
|
|
21074
|
+
code: "duplicate-command-branch",
|
|
21075
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
21076
|
+
line: lineOf(expression),
|
|
21077
|
+
message: "Command branches must use distinct selectors.",
|
|
21078
|
+
suggestion: "Make each branch selector mutually distinct."
|
|
21079
|
+
});
|
|
21080
|
+
}
|
|
21081
|
+
return branches;
|
|
21082
|
+
}
|
|
20729
21083
|
function classDeps(cls, ctx) {
|
|
20730
21084
|
const injectable = parseInjectableOptions(cls, ctx);
|
|
20731
21085
|
const ctor = cls.members.find(ts7.isConstructorDeclaration);
|
|
@@ -24414,10 +24768,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
24414
24768
|
import { resolve as resolve18 } from "node:path";
|
|
24415
24769
|
|
|
24416
24770
|
// src/migration-app-version.ts
|
|
24417
|
-
var MIGRATION_APP_VERSION = "0.
|
|
24771
|
+
var MIGRATION_APP_VERSION = "0.29.0";
|
|
24418
24772
|
|
|
24419
24773
|
// src/migration-elysia-version.ts
|
|
24420
|
-
var MIGRATION_ELYSIA_VERSION = "0.
|
|
24774
|
+
var MIGRATION_ELYSIA_VERSION = "0.34.0";
|
|
24421
24775
|
|
|
24422
24776
|
// src/migration-policy.ts
|
|
24423
24777
|
var cachedCompilerVersion;
|
|
@@ -25387,6 +25741,163 @@ function isRecord2(value) {
|
|
|
25387
25741
|
function arrayLength(value) {
|
|
25388
25742
|
return Array.isArray(value) ? value.length : undefined;
|
|
25389
25743
|
}
|
|
25744
|
+
function safeField(value) {
|
|
25745
|
+
return typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
|
25746
|
+
}
|
|
25747
|
+
function validFeaturePersistence(value) {
|
|
25748
|
+
if (!isRecord2(value))
|
|
25749
|
+
return false;
|
|
25750
|
+
const boundary = value["boundary"];
|
|
25751
|
+
const receipt = value["receipt"];
|
|
25752
|
+
const reconciliation = value["reconciliation"];
|
|
25753
|
+
if (boundary === "database") {
|
|
25754
|
+
const fields = [value["tenantField"], value["versionField"], value["stateField"]];
|
|
25755
|
+
return value["optimisticLock"] === "required" && fields.every(safeField) && new Set(fields).size === 3 && receipt === "required" && reconciliation === "none";
|
|
25756
|
+
}
|
|
25757
|
+
if (boundary === "external") {
|
|
25758
|
+
return value["optimisticLock"] === "none" && value["versionField"] === undefined && value["tenantField"] === undefined && value["stateField"] === undefined && receipt === "required" && reconciliation === "required";
|
|
25759
|
+
}
|
|
25760
|
+
return false;
|
|
25761
|
+
}
|
|
25762
|
+
function validCommandInventory(value) {
|
|
25763
|
+
const names = new Set;
|
|
25764
|
+
const classes = new Set;
|
|
25765
|
+
return Array.isArray(value) && value.every((command) => {
|
|
25766
|
+
if (!isRecord2(command) || typeof command["name"] !== "string" || command["name"].trim() === "" || typeof command["className"] !== "string" || command["className"].trim() === "" || typeof command["module"] !== "string" || command["module"].trim() === "" || typeof command["permission"] !== "string" || command["permission"].trim() === "" || command["transaction"] !== "required" && command["transaction"] !== "none" || command["idempotency"] !== "required" && command["idempotency"] !== "none" || command["tenantScope"] !== undefined && command["tenantScope"] !== "required" && command["tenantScope"] !== "none")
|
|
25767
|
+
return false;
|
|
25768
|
+
if (names.has(command["name"]) || classes.has(command["className"]))
|
|
25769
|
+
return false;
|
|
25770
|
+
if (command["rpc"] !== undefined && (typeof command["rpc"] !== "string" || command["rpc"].trim() === ""))
|
|
25771
|
+
return false;
|
|
25772
|
+
if (command["auditEvent"] !== undefined && (typeof command["auditEvent"] !== "string" || command["auditEvent"].trim() === ""))
|
|
25773
|
+
return false;
|
|
25774
|
+
names.add(command["name"]);
|
|
25775
|
+
classes.add(command["className"]);
|
|
25776
|
+
return true;
|
|
25777
|
+
});
|
|
25778
|
+
}
|
|
25779
|
+
function validFeatureInventory(value, jobs, commands, routes, rpc) {
|
|
25780
|
+
const featureNames = new Set;
|
|
25781
|
+
return Array.isArray(value) && value.every((feature) => {
|
|
25782
|
+
if (!isRecord2(feature) || typeof feature["name"] !== "string" || feature["name"].trim() === "")
|
|
25783
|
+
return false;
|
|
25784
|
+
if (featureNames.has(feature["name"]))
|
|
25785
|
+
return false;
|
|
25786
|
+
featureNames.add(feature["name"]);
|
|
25787
|
+
if (feature["tenantField"] !== undefined && !safeField(feature["tenantField"]))
|
|
25788
|
+
return false;
|
|
25789
|
+
const states = feature["states"];
|
|
25790
|
+
const transitions = feature["transitions"];
|
|
25791
|
+
if (!Array.isArray(states) || states.length === 0 || states.some((state) => typeof state !== "string" || state.trim() === "") || new Set(states).size !== states.length || !Array.isArray(transitions))
|
|
25792
|
+
return false;
|
|
25793
|
+
const transitionNames = new Set;
|
|
25794
|
+
return transitions.every((transition) => {
|
|
25795
|
+
if (!isRecord2(transition) || typeof transition["name"] !== "string" || transition["name"].trim() === "" || typeof transition["from"] !== "string" || !states.includes(transition["from"]) || typeof transition["to"] !== "string" || !states.includes(transition["to"]))
|
|
25796
|
+
return false;
|
|
25797
|
+
if (transitionNames.has(transition["name"]))
|
|
25798
|
+
return false;
|
|
25799
|
+
transitionNames.add(transition["name"]);
|
|
25800
|
+
const persistence = transition["persistence"];
|
|
25801
|
+
if (persistence !== undefined) {
|
|
25802
|
+
if (!isRecord2(persistence) || !validFeaturePersistence(persistence))
|
|
25803
|
+
return false;
|
|
25804
|
+
if (typeof transition["command"] !== "string" || transition["command"].trim() === "" || typeof transition["permission"] !== "string" || transition["permission"].trim() === "")
|
|
25805
|
+
return false;
|
|
25806
|
+
if (persistence["boundary"] === "database" && feature["tenantField"] !== persistence["tenantField"])
|
|
25807
|
+
return false;
|
|
25808
|
+
}
|
|
25809
|
+
const commandName = transition["command"];
|
|
25810
|
+
if (commandName !== undefined) {
|
|
25811
|
+
const command = commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === commandName || candidate["className"] === commandName));
|
|
25812
|
+
if (!isRecord2(command))
|
|
25813
|
+
return false;
|
|
25814
|
+
if (transition["permission"] !== undefined && command["permission"] !== transition["permission"])
|
|
25815
|
+
return false;
|
|
25816
|
+
if (transition["transaction"] !== undefined && command["transaction"] !== transition["transaction"])
|
|
25817
|
+
return false;
|
|
25818
|
+
if (transition["idempotency"] !== undefined && command["idempotency"] !== transition["idempotency"])
|
|
25819
|
+
return false;
|
|
25820
|
+
if (transition["audit"] !== undefined && command["auditEvent"] !== transition["audit"])
|
|
25821
|
+
return false;
|
|
25822
|
+
if (persistence !== undefined) {
|
|
25823
|
+
const adapter = command["rpc"];
|
|
25824
|
+
if (typeof adapter !== "string" || adapter.trim() === "" || !rpc.some((candidate) => isRecord2(candidate) && candidate["command"] === command["name"] && candidate["adapter"] === adapter) || command["idempotency"] !== "required" || command["tenantScope"] !== "required" || typeof command["auditEvent"] !== "string" || command["auditEvent"].trim() === "")
|
|
25825
|
+
return false;
|
|
25826
|
+
const expectedTransaction = persistence["boundary"] === "database" ? "required" : "none";
|
|
25827
|
+
if (command["transaction"] !== expectedTransaction)
|
|
25828
|
+
return false;
|
|
25829
|
+
}
|
|
25830
|
+
}
|
|
25831
|
+
const routeName = transition["route"];
|
|
25832
|
+
const routeBinding = transition["routeBinding"];
|
|
25833
|
+
if (routeBinding !== undefined && routeBinding !== "command" && routeBinding !== "controller")
|
|
25834
|
+
return false;
|
|
25835
|
+
if (routeName !== undefined) {
|
|
25836
|
+
const route = routes.find((candidate) => isRecord2(candidate) && `${candidate["method"]} ${candidate["path"]}` === routeName);
|
|
25837
|
+
if (!isRecord2(route))
|
|
25838
|
+
return false;
|
|
25839
|
+
if (routeBinding === "controller") {
|
|
25840
|
+
if (route["command"] !== undefined)
|
|
25841
|
+
return false;
|
|
25842
|
+
} else if (route["command"] !== commandName) {
|
|
25843
|
+
return false;
|
|
25844
|
+
}
|
|
25845
|
+
}
|
|
25846
|
+
const job = transition["job"];
|
|
25847
|
+
if (job === undefined)
|
|
25848
|
+
return true;
|
|
25849
|
+
const details = transition["jobDetails"];
|
|
25850
|
+
const inventory = jobs.find((candidate) => isRecord2(candidate) && (candidate["name"] === job || candidate["className"] === job));
|
|
25851
|
+
const contractFields = ["input", "inputKind", "output", "outputKind", "mode", "idempotency", "tenantScope"];
|
|
25852
|
+
return typeof job === "string" && job.trim() !== "" && isRecord2(details) && isRecord2(inventory) && (details["name"] === job || details["className"] === job) && typeof details["className"] === "string" && details["className"].trim() !== "" && details["inputKind"] === "declared" && (details["mode"] === "task" || details["mode"] === "workflow") && details["idempotency"] === "required" && details["tenantScope"] === "required" && contractFields.every((field) => details[field] === inventory[field]);
|
|
25853
|
+
});
|
|
25854
|
+
});
|
|
25855
|
+
}
|
|
25856
|
+
function validJobInventory(value) {
|
|
25857
|
+
const names = new Set;
|
|
25858
|
+
const classes = new Set;
|
|
25859
|
+
return Array.isArray(value) && value.every((job) => {
|
|
25860
|
+
if (!isRecord2(job) || typeof job["name"] !== "string" || job["name"].trim() === "" || typeof job["className"] !== "string" || job["className"].trim() === "" || typeof job["module"] !== "string" || job["module"].trim() === "")
|
|
25861
|
+
return false;
|
|
25862
|
+
if (names.has(job["name"]) || classes.has(job["className"]))
|
|
25863
|
+
return false;
|
|
25864
|
+
names.add(job["name"]);
|
|
25865
|
+
classes.add(job["className"]);
|
|
25866
|
+
if (job["input"] !== undefined && (typeof job["input"] !== "string" || job["input"].trim() === ""))
|
|
25867
|
+
return false;
|
|
25868
|
+
if (job["inputKind"] !== undefined && job["inputKind"] !== "opaque" && job["inputKind"] !== "declared")
|
|
25869
|
+
return false;
|
|
25870
|
+
if (job["output"] !== undefined && (typeof job["output"] !== "string" || job["output"].trim() === ""))
|
|
25871
|
+
return false;
|
|
25872
|
+
if (job["outputKind"] !== undefined && job["outputKind"] !== "opaque" && job["outputKind"] !== "declared")
|
|
25873
|
+
return false;
|
|
25874
|
+
if (job["mode"] !== undefined && job["mode"] !== "task" && job["mode"] !== "workflow")
|
|
25875
|
+
return false;
|
|
25876
|
+
if (job["idempotency"] !== undefined && job["idempotency"] !== "required" && job["idempotency"] !== "none")
|
|
25877
|
+
return false;
|
|
25878
|
+
return job["tenantScope"] === undefined || job["tenantScope"] === "required" || job["tenantScope"] === "none";
|
|
25879
|
+
});
|
|
25880
|
+
}
|
|
25881
|
+
function validRpcInventory(value, commandInventory) {
|
|
25882
|
+
const commandNames = new Set;
|
|
25883
|
+
const adapters = new Set;
|
|
25884
|
+
return Array.isArray(value) && value.every((rpc) => {
|
|
25885
|
+
if (!isRecord2(rpc) || typeof rpc["command"] !== "string" || rpc["command"].trim() === "" || typeof rpc["adapter"] !== "string" || rpc["adapter"].trim() === "")
|
|
25886
|
+
return false;
|
|
25887
|
+
if (commandNames.has(rpc["command"]) || adapters.has(rpc["adapter"]))
|
|
25888
|
+
return false;
|
|
25889
|
+
const command = commandInventory.find((candidate) => isRecord2(candidate) && candidate["name"] === rpc["command"] && candidate["rpc"] === rpc["adapter"]);
|
|
25890
|
+
if (!isRecord2(command))
|
|
25891
|
+
return false;
|
|
25892
|
+
commandNames.add(rpc["command"]);
|
|
25893
|
+
adapters.add(rpc["adapter"]);
|
|
25894
|
+
return true;
|
|
25895
|
+
}) && commandInventory.every((command) => {
|
|
25896
|
+
if (!isRecord2(command) || command["rpc"] === undefined)
|
|
25897
|
+
return true;
|
|
25898
|
+
return value.some((rpc) => isRecord2(rpc) && rpc["command"] === command["name"] && rpc["adapter"] === command["rpc"]);
|
|
25899
|
+
});
|
|
25900
|
+
}
|
|
25390
25901
|
async function sha2566(path) {
|
|
25391
25902
|
return createHash11("sha256").update(await readFile11(path)).digest("hex");
|
|
25392
25903
|
}
|
|
@@ -25456,19 +25967,24 @@ async function assessMigration(options) {
|
|
|
25456
25967
|
};
|
|
25457
25968
|
try {
|
|
25458
25969
|
const parsed = JSON.parse(await readFile11(contractsManifestPath, "utf8"));
|
|
25459
|
-
if (!isRecord2(parsed) || parsed["version"] !== 1)
|
|
25970
|
+
if (!isRecord2(parsed) || parsed["version"] !== 1 || !validJobInventory(parsed["jobs"]) || !validCommandInventory(parsed["commands"]) || !Array.isArray(parsed["routes"]) || !validRpcInventory(parsed["rpc"], parsed["commands"]) || !validFeatureInventory(parsed["features"], parsed["jobs"], parsed["commands"], parsed["routes"], parsed["rpc"])) {
|
|
25460
25971
|
throw new Error("invalid manifest");
|
|
25972
|
+
}
|
|
25461
25973
|
const commands = arrayLength(parsed["commands"]);
|
|
25974
|
+
const jobs = arrayLength(parsed["jobs"]);
|
|
25462
25975
|
const routes = arrayLength(parsed["routes"]);
|
|
25463
25976
|
const permissions = arrayLength(parsed["permissions"]);
|
|
25977
|
+
const features = arrayLength(parsed["features"]);
|
|
25464
25978
|
contractsManifest = {
|
|
25465
25979
|
path: contractsManifestPath,
|
|
25466
25980
|
present: true,
|
|
25467
25981
|
sha256: await sha2566(contractsManifestPath),
|
|
25468
25982
|
version: 1,
|
|
25469
25983
|
...commands === undefined ? {} : { commands },
|
|
25984
|
+
...jobs === undefined ? {} : { jobs },
|
|
25470
25985
|
...routes === undefined ? {} : { routes },
|
|
25471
|
-
...permissions === undefined ? {} : { permissions }
|
|
25986
|
+
...permissions === undefined ? {} : { permissions },
|
|
25987
|
+
...features === undefined ? {} : { features }
|
|
25472
25988
|
};
|
|
25473
25989
|
} catch {
|
|
25474
25990
|
findings.push({
|