@supacloud/compiler 0.47.3 → 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 +539 -24
- package/dist/contract-manifest.d.ts +53 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +539 -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
|
}
|
|
@@ -16219,7 +16449,7 @@ async function readDeliverySelection(manifestPath, target) {
|
|
|
16219
16449
|
return { root, object, planned };
|
|
16220
16450
|
}
|
|
16221
16451
|
async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
|
|
16222
|
-
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) {
|
|
16223
16453
|
throw new Error("Artifact inventory exceeds limits.");
|
|
16224
16454
|
}
|
|
16225
16455
|
const objectRoot = join7(root, "objects", object.objectId);
|
|
@@ -16258,10 +16488,12 @@ async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
|
|
|
16258
16488
|
}
|
|
16259
16489
|
return retained;
|
|
16260
16490
|
}
|
|
16261
|
-
var JSON_LIMIT = 1048576;
|
|
16491
|
+
var JSON_LIMIT = 1048576, DELIVERY_ARTIFACT_MAX_FILE_BYTES, DELIVERY_ARTIFACT_MAX_TOTAL_BYTES;
|
|
16262
16492
|
var init_delivery_artifact = __esm(() => {
|
|
16263
16493
|
init_delivery_build_schema();
|
|
16264
16494
|
init_delivery_files();
|
|
16495
|
+
DELIVERY_ARTIFACT_MAX_FILE_BYTES = 500 * 1024 * 1024;
|
|
16496
|
+
DELIVERY_ARTIFACT_MAX_TOTAL_BYTES = 500 * 1024 * 1024;
|
|
16265
16497
|
});
|
|
16266
16498
|
|
|
16267
16499
|
// src/delivery-artifact.ts
|
|
@@ -17426,6 +17658,7 @@ var init_delivery_migration_plan = () => {};
|
|
|
17426
17658
|
|
|
17427
17659
|
// node_modules/@supacloud/delivery/src/index.ts
|
|
17428
17660
|
var init_src = __esm(() => {
|
|
17661
|
+
init_delivery_artifact();
|
|
17429
17662
|
init_delivery_artifact();
|
|
17430
17663
|
init_worker_execution();
|
|
17431
17664
|
init_worker_execution();
|
|
@@ -18807,6 +19040,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
|
|
|
18807
19040
|
if (meta && booleanProp(meta, "standalone")) {
|
|
18808
19041
|
const permission = stringLiteralProp(meta, "permission");
|
|
18809
19042
|
const rpc = checkedRpc(meta, ctx);
|
|
19043
|
+
const branches = checkedBranches(meta, ctx);
|
|
18810
19044
|
const audit = stringLiteralProp(meta, "audit");
|
|
18811
19045
|
const standaloneName = stringLiteralProp(meta, "name") ?? classInfo.decl.name?.text ?? name;
|
|
18812
19046
|
const standaloneUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${standaloneName}`, sourcePath(ctx.rootDir, classInfo.file), lineOf(classInfo.decl));
|
|
@@ -18816,6 +19050,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
|
|
|
18816
19050
|
name: standaloneName,
|
|
18817
19051
|
...permission === undefined ? {} : { permission },
|
|
18818
19052
|
...rpc === undefined ? {} : { rpc },
|
|
19053
|
+
...branches === undefined ? {} : { branches },
|
|
18819
19054
|
transaction: checkedCommandMode(meta, "transaction", ctx, classInfo.decl.name?.text ?? name) ?? "none",
|
|
18820
19055
|
...audit === undefined ? {} : { audit },
|
|
18821
19056
|
idempotency: checkedCommandMode(meta, "idempotency", ctx, classInfo.decl.name?.text ?? name) ?? "none",
|
|
@@ -19147,18 +19382,22 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19147
19382
|
const aspects = parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${cls.name?.text ?? "<anonymous>"}`);
|
|
19148
19383
|
const permission = stringLiteralProp(meta, "permission");
|
|
19149
19384
|
const rpc = checkedRpc(meta, ctx);
|
|
19385
|
+
const branches = checkedBranches(meta, ctx);
|
|
19150
19386
|
const audit = stringLiteralProp(meta, "audit");
|
|
19151
19387
|
const commandName = stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>";
|
|
19152
19388
|
const uses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19153
19389
|
rejectUndeclaredResourceUses(uses, moduleResourceNames, ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19390
|
+
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19154
19391
|
commands.push({
|
|
19155
19392
|
className: cls.name?.text ?? "<anonymous>",
|
|
19156
19393
|
name: commandName,
|
|
19157
19394
|
...permission === undefined ? {} : { permission },
|
|
19158
19395
|
...rpc === undefined ? {} : { rpc },
|
|
19396
|
+
...branches === undefined ? {} : { branches },
|
|
19159
19397
|
transaction: checkedCommandMode(meta, "transaction", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
|
|
19160
19398
|
...audit === undefined ? {} : { audit },
|
|
19161
19399
|
idempotency: checkedCommandMode(meta, "idempotency", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
|
|
19400
|
+
...tenantScope === undefined ? {} : { tenantScope },
|
|
19162
19401
|
...booleanProp(meta, "standalone") ? { standalone: true } : {},
|
|
19163
19402
|
...uses.length > 0 ? { uses } : {},
|
|
19164
19403
|
...aspects.length > 0 ? { aspects } : {}
|
|
@@ -19189,12 +19428,14 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19189
19428
|
const contract = parseJobOptions(meta, className, ctx);
|
|
19190
19429
|
const jobUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19191
19430
|
rejectUndeclaredResourceUses(jobUses, moduleResourceNames, ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19431
|
+
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19192
19432
|
jobs.push({
|
|
19193
19433
|
className,
|
|
19194
19434
|
name: stringLiteralProp(meta, "name") ?? className,
|
|
19195
19435
|
serviceKey: camelName(provider?.token ?? className),
|
|
19196
19436
|
scope,
|
|
19197
19437
|
...contract,
|
|
19438
|
+
...tenantScope === undefined ? {} : { tenantScope },
|
|
19198
19439
|
...jobUses.length > 0 ? { uses: jobUses } : {},
|
|
19199
19440
|
...aspects.length > 0 ? { aspects } : {}
|
|
19200
19441
|
});
|
|
@@ -19204,9 +19445,11 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19204
19445
|
if (queryDec) {
|
|
19205
19446
|
const meta = decoratorObjectArg(queryDec);
|
|
19206
19447
|
if (meta) {
|
|
19448
|
+
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19207
19449
|
queries.push({
|
|
19208
19450
|
className: cls.name?.text ?? "<anonymous>",
|
|
19209
|
-
name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>"
|
|
19451
|
+
name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
|
|
19452
|
+
...tenantScope === undefined ? {} : { tenantScope }
|
|
19210
19453
|
});
|
|
19211
19454
|
}
|
|
19212
19455
|
}
|
|
@@ -19261,6 +19504,7 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19261
19504
|
if (!ts7.isObjectLiteralExpression(input))
|
|
19262
19505
|
return invalid();
|
|
19263
19506
|
const name = stringLiteralProp(input, "name");
|
|
19507
|
+
const tenantField = stringLiteralProp(input, "tenantField");
|
|
19264
19508
|
const statesExpr = getProp(input, "states");
|
|
19265
19509
|
const transitionObject = getProp(input, "transitions");
|
|
19266
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))) {
|
|
@@ -19279,15 +19523,23 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19279
19523
|
const options = property.initializer;
|
|
19280
19524
|
const from = stringLiteralProp(options, "from");
|
|
19281
19525
|
const to = stringLiteralProp(options, "to");
|
|
19282
|
-
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))) {
|
|
19283
19527
|
return invalid();
|
|
19284
19528
|
}
|
|
19285
19529
|
const permission = stringLiteralProp(options, "permission");
|
|
19286
19530
|
const command = stringLiteralProp(options, "command");
|
|
19287
19531
|
const route = stringLiteralProp(options, "route");
|
|
19532
|
+
const routeBinding = stringLiteralProp(options, "routeBinding");
|
|
19533
|
+
if (routeBinding !== undefined && routeBinding !== "command" && routeBinding !== "controller")
|
|
19534
|
+
return invalid();
|
|
19288
19535
|
const transaction = commandModeProp(options, "transaction");
|
|
19289
19536
|
const idempotency = commandModeProp(options, "idempotency");
|
|
19290
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();
|
|
19291
19543
|
transitions.push({
|
|
19292
19544
|
name: propertyName(property.name),
|
|
19293
19545
|
from,
|
|
@@ -19295,12 +19547,44 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19295
19547
|
...permission === undefined ? {} : { permission },
|
|
19296
19548
|
...command === undefined ? {} : { command },
|
|
19297
19549
|
...route === undefined ? {} : { route },
|
|
19550
|
+
...routeBinding === undefined ? {} : { routeBinding },
|
|
19298
19551
|
...transaction === undefined ? {} : { transaction },
|
|
19299
19552
|
...idempotency === undefined ? {} : { idempotency },
|
|
19300
|
-
...audit === undefined ? {} : { audit }
|
|
19553
|
+
...audit === undefined ? {} : { audit },
|
|
19554
|
+
...job === undefined ? {} : { job },
|
|
19555
|
+
...persistence === undefined ? {} : { persistence }
|
|
19301
19556
|
});
|
|
19302
19557
|
}
|
|
19303
|
-
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
|
+
};
|
|
19304
19588
|
}
|
|
19305
19589
|
function resolveStaticObjectLiteral(input, ctx, seen = new Set) {
|
|
19306
19590
|
if (!input || seen.has(input))
|
|
@@ -20727,6 +21011,75 @@ function checkedRpc(meta, ctx) {
|
|
|
20727
21011
|
});
|
|
20728
21012
|
return;
|
|
20729
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
|
+
}
|
|
20730
21083
|
function classDeps(cls, ctx) {
|
|
20731
21084
|
const injectable = parseInjectableOptions(cls, ctx);
|
|
20732
21085
|
const ctor = cls.members.find(ts7.isConstructorDeclaration);
|
|
@@ -24415,10 +24768,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
24415
24768
|
import { resolve as resolve18 } from "node:path";
|
|
24416
24769
|
|
|
24417
24770
|
// src/migration-app-version.ts
|
|
24418
|
-
var MIGRATION_APP_VERSION = "0.
|
|
24771
|
+
var MIGRATION_APP_VERSION = "0.29.0";
|
|
24419
24772
|
|
|
24420
24773
|
// src/migration-elysia-version.ts
|
|
24421
|
-
var MIGRATION_ELYSIA_VERSION = "0.
|
|
24774
|
+
var MIGRATION_ELYSIA_VERSION = "0.34.0";
|
|
24422
24775
|
|
|
24423
24776
|
// src/migration-policy.ts
|
|
24424
24777
|
var cachedCompilerVersion;
|
|
@@ -25388,6 +25741,163 @@ function isRecord2(value) {
|
|
|
25388
25741
|
function arrayLength(value) {
|
|
25389
25742
|
return Array.isArray(value) ? value.length : undefined;
|
|
25390
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
|
+
}
|
|
25391
25901
|
async function sha2566(path) {
|
|
25392
25902
|
return createHash11("sha256").update(await readFile11(path)).digest("hex");
|
|
25393
25903
|
}
|
|
@@ -25457,19 +25967,24 @@ async function assessMigration(options) {
|
|
|
25457
25967
|
};
|
|
25458
25968
|
try {
|
|
25459
25969
|
const parsed = JSON.parse(await readFile11(contractsManifestPath, "utf8"));
|
|
25460
|
-
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"])) {
|
|
25461
25971
|
throw new Error("invalid manifest");
|
|
25972
|
+
}
|
|
25462
25973
|
const commands = arrayLength(parsed["commands"]);
|
|
25974
|
+
const jobs = arrayLength(parsed["jobs"]);
|
|
25463
25975
|
const routes = arrayLength(parsed["routes"]);
|
|
25464
25976
|
const permissions = arrayLength(parsed["permissions"]);
|
|
25977
|
+
const features = arrayLength(parsed["features"]);
|
|
25465
25978
|
contractsManifest = {
|
|
25466
25979
|
path: contractsManifestPath,
|
|
25467
25980
|
present: true,
|
|
25468
25981
|
sha256: await sha2566(contractsManifestPath),
|
|
25469
25982
|
version: 1,
|
|
25470
25983
|
...commands === undefined ? {} : { commands },
|
|
25984
|
+
...jobs === undefined ? {} : { jobs },
|
|
25471
25985
|
...routes === undefined ? {} : { routes },
|
|
25472
|
-
...permissions === undefined ? {} : { permissions }
|
|
25986
|
+
...permissions === undefined ? {} : { permissions },
|
|
25987
|
+
...features === undefined ? {} : { features }
|
|
25473
25988
|
};
|
|
25474
25989
|
} catch {
|
|
25475
25990
|
findings.push({
|