@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/index.js
CHANGED
|
@@ -262,6 +262,9 @@ function validateFeatureSpec(spec, module) {
|
|
|
262
262
|
if (!spec.name.trim() || spec.states.length === 0 || spec.states.some((state) => !state.trim()) || new Set(spec.states).size !== spec.states.length) {
|
|
263
263
|
error("invalid-feature-states", "Feature name and states must be non-empty; states must be unique.");
|
|
264
264
|
}
|
|
265
|
+
if (spec.tenantField !== undefined && !/^[A-Za-z_][A-Za-z0-9_]*$/.test(spec.tenantField)) {
|
|
266
|
+
error("feature-tenant-boundary-invalid", `Feature ${spec.name} declares an unsafe tenant field.`);
|
|
267
|
+
}
|
|
265
268
|
const names = new Set;
|
|
266
269
|
for (const transition of spec.transitions) {
|
|
267
270
|
if (!transition.name.trim() || names.has(transition.name)) {
|
|
@@ -274,8 +277,41 @@ function validateFeatureSpec(spec, module) {
|
|
|
274
277
|
if (transition.permission !== undefined && !transition.permission.trim()) {
|
|
275
278
|
error("feature-governance-drift", `Transition ${transition.name} declares an empty permission.`);
|
|
276
279
|
}
|
|
280
|
+
if (transition.persistence) {
|
|
281
|
+
const persistence = transition.persistence;
|
|
282
|
+
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")) {
|
|
283
|
+
error("feature-persistence-invalid", `Database transition ${transition.name} requires distinct safe tenant/version/state fields, optimistic locking, and a receipt, without reconciliation.`);
|
|
284
|
+
}
|
|
285
|
+
if (persistence.boundary === "database" && spec.tenantField !== persistence.tenantField) {
|
|
286
|
+
error("feature-tenant-boundary-drift", `Database transition ${transition.name} tenant field must match Feature ${spec.name} tenantField.`);
|
|
287
|
+
}
|
|
288
|
+
if (persistence.boundary === "external" && (persistence.optimisticLock !== "none" || persistence.versionField !== undefined || persistence.tenantField !== undefined || persistence.stateField !== undefined || persistence.receipt !== "required" || persistence.reconciliation !== "required")) {
|
|
289
|
+
error("feature-persistence-invalid", `External transition ${transition.name} requires a receipt and reconciliation, without database optimistic locking.`);
|
|
290
|
+
}
|
|
291
|
+
if (persistence.boundary === "external" && !transition.job) {
|
|
292
|
+
error("feature-job-unresolved", `External transition ${transition.name} must bind a reconciliation Job.`);
|
|
293
|
+
}
|
|
294
|
+
if (!transition.command) {
|
|
295
|
+
error("feature-command-unresolved", `Persistent transition ${transition.name} must reference a command.`);
|
|
296
|
+
}
|
|
297
|
+
if (!transition.permission?.trim()) {
|
|
298
|
+
error("feature-governance-drift", `Persistent transition ${transition.name} must declare a permission.`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
277
301
|
if (!module)
|
|
278
302
|
continue;
|
|
303
|
+
if (transition.persistence?.boundary === "database") {
|
|
304
|
+
for (const query of module.queries) {
|
|
305
|
+
if (query.tenantScope !== "required") {
|
|
306
|
+
error("feature-query-tenant-boundary", `Query ${query.className} in Feature ${spec.name} must declare tenantScope: "required".`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
for (const job of module.jobs ?? []) {
|
|
310
|
+
if (job.tenantScope !== "required") {
|
|
311
|
+
error("feature-job-tenant-boundary", `Job ${job.className} in Feature ${spec.name} must declare tenantScope: "required".`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
279
315
|
const commands = module.commands.filter((command) => command.className === transition.command || command.name === transition.command);
|
|
280
316
|
const command = commands[0];
|
|
281
317
|
if (transition.command && commands.length !== 1) {
|
|
@@ -290,23 +326,59 @@ function validateFeatureSpec(spec, module) {
|
|
|
290
326
|
error("feature-governance-drift", `Transition ${transition.name} ${key} differs from command ${command.className}.`);
|
|
291
327
|
}
|
|
292
328
|
}
|
|
329
|
+
if (transition.persistence) {
|
|
330
|
+
if (transition.persistence.boundary === "database" && command.transaction !== "required") {
|
|
331
|
+
error("feature-persistence-drift", `Database transition ${transition.name} requires command ${command.className} to declare transaction: "required".`);
|
|
332
|
+
}
|
|
333
|
+
if (command.tenantScope !== "required") {
|
|
334
|
+
error("feature-command-tenant-boundary", `Command ${command.className} in Feature ${spec.name} must declare tenantScope: "required".`);
|
|
335
|
+
}
|
|
336
|
+
if (transition.persistence.boundary === "external" && command.transaction !== "none") {
|
|
337
|
+
error("feature-persistence-drift", `External transition ${transition.name} requires command ${command.className} to declare transaction: "none".`);
|
|
338
|
+
}
|
|
339
|
+
if (command.idempotency !== "required" || !command.audit) {
|
|
340
|
+
error("feature-persistence-drift", `Persistent transition ${transition.name} requires command ${command.className} to declare idempotency and audit.`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
if (transition.job) {
|
|
345
|
+
const jobs = (module.jobs ?? []).filter((job) => job.className === transition.job || job.name === transition.job);
|
|
346
|
+
if (jobs.length !== 1) {
|
|
347
|
+
error("feature-job-unresolved", `Transition ${transition.name} must reference exactly one Job in module ${module.name}.`);
|
|
348
|
+
} else {
|
|
349
|
+
const job = jobs[0];
|
|
350
|
+
if (job.tenantScope !== "required") {
|
|
351
|
+
error("feature-job-tenant-boundary", `Job ${job.className} in Feature ${spec.name} must declare tenantScope: "required".`);
|
|
352
|
+
}
|
|
353
|
+
if (job.idempotency !== "required") {
|
|
354
|
+
error("feature-job-idempotency", `Job ${job.className} in Feature ${spec.name} must declare idempotency: "required".`);
|
|
355
|
+
}
|
|
356
|
+
if (job.mode === undefined) {
|
|
357
|
+
error("feature-job-mode", `Job ${job.className} in Feature ${spec.name} must declare mode: "task" or "workflow".`);
|
|
358
|
+
}
|
|
359
|
+
if (job.input === undefined || job.schemaKinds?.input !== "declared") {
|
|
360
|
+
error("feature-job-contract", `Job ${job.className} in Feature ${spec.name} must declare a non-opaque input schema.`);
|
|
361
|
+
}
|
|
362
|
+
const provider = module.providers.find((candidate) => candidate.token === job.className || candidate.useClass === job.className);
|
|
363
|
+
if (job.tenantScope === "required" && provider && !provider.deps.some((token) => isJobContextToken(token))) {
|
|
364
|
+
error("feature-job-context", `Job ${job.className} in Feature ${spec.name} must inject JOB_CONTEXT for trusted tenant execution.`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
293
367
|
}
|
|
294
368
|
if (transition.route) {
|
|
295
369
|
const routes = module.controllers.flatMap((controller) => controller.routes.filter((route) => `${route.method} ${joinRoutePaths(controller.path, route.path)}` === transition.route));
|
|
296
370
|
if (routes.length !== 1) {
|
|
297
371
|
error("feature-route-unresolved", `Transition ${transition.name} must reference exactly one route '${transition.route}' in module ${module.name}.`);
|
|
298
|
-
} else if (command && routes[0]?.command !== command.className) {
|
|
372
|
+
} else if (command && transition.routeBinding !== "controller" && routes[0]?.command !== command.className) {
|
|
299
373
|
error("feature-route-drift", `Route ${transition.route} is not bound to ${command.className}.`);
|
|
374
|
+
} else if (transition.routeBinding === "controller" && routes[0]?.command !== undefined) {
|
|
375
|
+
error("feature-route-drift", `Controller-bound route ${transition.route} must not also declare a framework command binding.`);
|
|
300
376
|
}
|
|
301
377
|
}
|
|
302
378
|
}
|
|
303
379
|
return diagnostics;
|
|
304
380
|
}
|
|
305
381
|
function generateFeatureSource(spec) {
|
|
306
|
-
const errors = validateFeatureSpec(spec);
|
|
307
|
-
if (errors.length)
|
|
308
|
-
throw new Error(errors.map((error) => error.message).join(`
|
|
309
|
-
`));
|
|
310
382
|
if (spec.transitions.some((transition) => !transition.permission)) {
|
|
311
383
|
throw new Error("Spec-to-Code requires an explicit permission for every transition.");
|
|
312
384
|
}
|
|
@@ -314,13 +386,34 @@ function generateFeatureSource(spec) {
|
|
|
314
386
|
...transition,
|
|
315
387
|
command: `Transition${index + 1}Command`
|
|
316
388
|
}));
|
|
389
|
+
const errors = validateFeatureSpec({ ...spec, transitions });
|
|
390
|
+
if (errors.length)
|
|
391
|
+
throw new Error(errors.map((error) => error.message).join(`
|
|
392
|
+
`));
|
|
317
393
|
if (transitions.some((transition) => transition.route)) {
|
|
318
394
|
throw new Error("Generate the command slice first; existing HTTP routes require explicit schema and handler implementations.");
|
|
319
395
|
}
|
|
320
|
-
const sourceSpec = {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
396
|
+
const sourceSpec = {
|
|
397
|
+
name: spec.name,
|
|
398
|
+
states: spec.states,
|
|
399
|
+
tenantField: spec.tenantField ?? "tenant_id",
|
|
400
|
+
transitions: Object.fromEntries(transitions.map((transition) => {
|
|
401
|
+
const { name, ...options } = transition;
|
|
402
|
+
const persistence = transition.persistence ?? {
|
|
403
|
+
boundary: "database",
|
|
404
|
+
optimisticLock: "required",
|
|
405
|
+
versionField: "version",
|
|
406
|
+
tenantField: "tenant_id",
|
|
407
|
+
stateField: "state",
|
|
408
|
+
receipt: "required",
|
|
409
|
+
reconciliation: "none"
|
|
410
|
+
};
|
|
411
|
+
return [name, {
|
|
412
|
+
...options,
|
|
413
|
+
persistence
|
|
414
|
+
}];
|
|
415
|
+
}))
|
|
416
|
+
};
|
|
324
417
|
return [
|
|
325
418
|
'import { Command, defineFeatureSlice, defineFeatureSpec } from "@supacloud/app";',
|
|
326
419
|
"",
|
|
@@ -331,9 +424,10 @@ function generateFeatureSource(spec) {
|
|
|
331
424
|
`@Command(${JSON.stringify({
|
|
332
425
|
name: `${spec.name}.${transition.name}`,
|
|
333
426
|
permission: transition.permission,
|
|
334
|
-
transaction: transition.transaction ?? "none",
|
|
335
|
-
idempotency: transition.idempotency ?? "
|
|
336
|
-
audit: transition.audit
|
|
427
|
+
transaction: transition.transaction ?? (transition.persistence?.boundary === "external" ? "none" : "required"),
|
|
428
|
+
idempotency: transition.idempotency ?? "required",
|
|
429
|
+
audit: transition.audit ?? `${spec.name}.${transition.name}`,
|
|
430
|
+
tenantScope: "required"
|
|
337
431
|
})})`,
|
|
338
432
|
`export class ${transition.command} {`,
|
|
339
433
|
` execute(state: FeatureState): never {`,
|
|
@@ -801,13 +895,40 @@ function validateGraph(graph, options = false) {
|
|
|
801
895
|
permission: `${module.name}.${command.name}`
|
|
802
896
|
});
|
|
803
897
|
}
|
|
898
|
+
if (command.rpc && command.branches) {
|
|
899
|
+
error("command-rpc-branches-conflict", `Command ${command.name} cannot declare both rpc and branches.`, module.file, module.line);
|
|
900
|
+
}
|
|
901
|
+
const branchAdapters = command.branches ?? [];
|
|
902
|
+
for (const branch of branchAdapters) {
|
|
903
|
+
if (typeof options !== "object" || !Object.hasOwn(options.commandCapabilities?.rpc ?? {}, branch.rpc)) {
|
|
904
|
+
error("command-rpc-unavailable", `Command ${command.name} requires configured RPC adapter '${branch.rpc}'.`, module.file, module.line);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
804
907
|
if (command.rpc && (typeof options !== "object" || !Object.hasOwn(options.commandCapabilities?.rpc ?? {}, command.rpc))) {
|
|
805
908
|
error("command-rpc-unavailable", `Command ${command.name} requires configured RPC adapter '${command.rpc}'.`, module.file, module.line);
|
|
806
909
|
}
|
|
807
910
|
if (typeof options === "object" && options.commandCapabilities) {
|
|
808
911
|
const hostCaps = options.commandCapabilities;
|
|
809
912
|
const rpcCaps = command.rpc && Object.hasOwn(hostCaps.rpc ?? {}, command.rpc) ? hostCaps.rpc?.[command.rpc] : undefined;
|
|
810
|
-
|
|
913
|
+
const branchCaps = branchAdapters.map((branch) => ({
|
|
914
|
+
branch,
|
|
915
|
+
capabilities: Object.hasOwn(hostCaps.rpc ?? {}, branch.rpc) ? hostCaps.rpc?.[branch.rpc] : undefined
|
|
916
|
+
}));
|
|
917
|
+
for (const { branch, capabilities } of branchCaps) {
|
|
918
|
+
if (capabilities?.boundary === "external" && (branch.transaction === "required" || capabilities.transaction === true)) {
|
|
919
|
+
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.");
|
|
920
|
+
}
|
|
921
|
+
if (branch.audit && capabilities?.audit !== true) {
|
|
922
|
+
error("command-audit-unsupported", `Command ${command.name} branch '${branch.when}' declares audit, but runtime audit persistence is unavailable.`, module.file, module.line);
|
|
923
|
+
}
|
|
924
|
+
if (branch.idempotency === "required" && capabilities?.idempotency !== true) {
|
|
925
|
+
error("command-idempotency-unsupported", `Command ${command.name} branch '${branch.when}' declares idempotency, but receipt persistence is unavailable.`, module.file, module.line);
|
|
926
|
+
}
|
|
927
|
+
if (branch.transaction === "required" && capabilities?.transaction !== true) {
|
|
928
|
+
error("command-transaction-unsupported", `Command ${command.name} branch '${branch.when}' declares transaction, but transaction support is unavailable.`, module.file, module.line);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
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)) {
|
|
811
932
|
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.");
|
|
812
933
|
}
|
|
813
934
|
if (rpcCaps?.boundary === "external" && (command.transaction === "required" || rpcCaps.transaction === true)) {
|
|
@@ -1283,6 +1404,18 @@ var init_validate = __esm(() => {
|
|
|
1283
1404
|
"invalid-feature-transition": { code: "SC6103", docsUrl: "https://supacloud.dev/errors/SC6103" },
|
|
1284
1405
|
"feature-command-unresolved": { code: "SC6104", docsUrl: "https://supacloud.dev/errors/SC6104" },
|
|
1285
1406
|
"feature-governance-drift": { code: "SC6105", docsUrl: "https://supacloud.dev/errors/SC6105" },
|
|
1407
|
+
"feature-persistence-invalid": { code: "SC6110", docsUrl: "https://supacloud.dev/errors/SC6110" },
|
|
1408
|
+
"feature-persistence-drift": { code: "SC6111", docsUrl: "https://supacloud.dev/errors/SC6111" },
|
|
1409
|
+
"feature-tenant-boundary-invalid": { code: "SC6112", docsUrl: "https://supacloud.dev/errors/SC6112" },
|
|
1410
|
+
"feature-tenant-boundary-drift": { code: "SC6113", docsUrl: "https://supacloud.dev/errors/SC6113" },
|
|
1411
|
+
"feature-query-tenant-boundary": { code: "SC6114", docsUrl: "https://supacloud.dev/errors/SC6114" },
|
|
1412
|
+
"feature-job-tenant-boundary": { code: "SC6115", docsUrl: "https://supacloud.dev/errors/SC6115" },
|
|
1413
|
+
"feature-command-tenant-boundary": { code: "SC6116", docsUrl: "https://supacloud.dev/errors/SC6116" },
|
|
1414
|
+
"feature-job-unresolved": { code: "SC6117", docsUrl: "https://supacloud.dev/errors/SC6117" },
|
|
1415
|
+
"feature-job-idempotency": { code: "SC6118", docsUrl: "https://supacloud.dev/errors/SC6118" },
|
|
1416
|
+
"feature-job-mode": { code: "SC6119", docsUrl: "https://supacloud.dev/errors/SC6119" },
|
|
1417
|
+
"feature-job-contract": { code: "SC6120", docsUrl: "https://supacloud.dev/errors/SC6120" },
|
|
1418
|
+
"feature-job-context": { code: "SC6121", docsUrl: "https://supacloud.dev/errors/SC6121" },
|
|
1286
1419
|
"feature-route-unresolved": { code: "SC6106", docsUrl: "https://supacloud.dev/errors/SC6106" },
|
|
1287
1420
|
"feature-route-drift": { code: "SC6107", docsUrl: "https://supacloud.dev/errors/SC6107" },
|
|
1288
1421
|
"invalid-feature-spec": { code: "SC6108", docsUrl: "https://supacloud.dev/errors/SC6108" }
|
|
@@ -1293,6 +1426,8 @@ var init_validate = __esm(() => {
|
|
|
1293
1426
|
function buildContractManifest(graph, artifacts) {
|
|
1294
1427
|
const commands = [];
|
|
1295
1428
|
const queries = [];
|
|
1429
|
+
const jobs = [];
|
|
1430
|
+
const features = [];
|
|
1296
1431
|
const routes = [];
|
|
1297
1432
|
const permissions = new Set;
|
|
1298
1433
|
const rpc = [];
|
|
@@ -1304,21 +1439,95 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1304
1439
|
permissions.add(command.permission);
|
|
1305
1440
|
if (command.rpc)
|
|
1306
1441
|
rpc.push({ command: command.name, adapter: command.rpc });
|
|
1442
|
+
for (const branch of command.branches ?? []) {
|
|
1443
|
+
rpc.push({ command: `${command.name} [${branch.when}]`, adapter: branch.rpc });
|
|
1444
|
+
}
|
|
1307
1445
|
if (command.audit)
|
|
1308
1446
|
events.push({ name: command.audit, source: "command.audit", command: command.name });
|
|
1447
|
+
for (const branch of command.branches ?? []) {
|
|
1448
|
+
if (branch.audit)
|
|
1449
|
+
events.push({ name: branch.audit, source: "command.branch.audit", command: command.name });
|
|
1450
|
+
}
|
|
1309
1451
|
commands.push({
|
|
1310
1452
|
name: command.name,
|
|
1311
1453
|
className: command.className,
|
|
1312
1454
|
module: module.name,
|
|
1313
1455
|
...command.permission === undefined ? {} : { permission: command.permission },
|
|
1314
1456
|
...command.rpc === undefined ? {} : { rpc: command.rpc },
|
|
1457
|
+
...command.branches === undefined ? {} : {
|
|
1458
|
+
branches: command.branches.map((branch) => ({
|
|
1459
|
+
when: branch.when,
|
|
1460
|
+
adapter: branch.rpc,
|
|
1461
|
+
transaction: branch.transaction,
|
|
1462
|
+
idempotency: branch.idempotency,
|
|
1463
|
+
...branch.audit === undefined ? {} : { auditEvent: branch.audit },
|
|
1464
|
+
...branch.boundary === undefined ? {} : { boundary: branch.boundary }
|
|
1465
|
+
}))
|
|
1466
|
+
},
|
|
1315
1467
|
transaction: command.transaction,
|
|
1316
1468
|
idempotency: command.idempotency,
|
|
1469
|
+
...command.tenantScope === undefined ? {} : { tenantScope: command.tenantScope },
|
|
1317
1470
|
...command.audit === undefined ? {} : { auditEvent: command.audit }
|
|
1318
1471
|
});
|
|
1319
1472
|
}
|
|
1320
1473
|
for (const query of module.queries) {
|
|
1321
|
-
queries.push({
|
|
1474
|
+
queries.push({
|
|
1475
|
+
name: query.name,
|
|
1476
|
+
className: query.className,
|
|
1477
|
+
module: module.name,
|
|
1478
|
+
...query.tenantScope === undefined ? {} : { tenantScope: query.tenantScope }
|
|
1479
|
+
});
|
|
1480
|
+
}
|
|
1481
|
+
for (const job of module.jobs ?? []) {
|
|
1482
|
+
jobs.push({
|
|
1483
|
+
name: job.name,
|
|
1484
|
+
className: job.className,
|
|
1485
|
+
module: module.name,
|
|
1486
|
+
...job.input === undefined ? {} : { input: job.input },
|
|
1487
|
+
...job.schemaKinds?.input === undefined ? {} : { inputKind: job.schemaKinds.input },
|
|
1488
|
+
...job.output === undefined ? {} : { output: job.output },
|
|
1489
|
+
...job.schemaKinds?.output === undefined ? {} : { outputKind: job.schemaKinds.output },
|
|
1490
|
+
...job.mode === undefined ? {} : { mode: job.mode },
|
|
1491
|
+
...job.idempotency === undefined ? {} : { idempotency: job.idempotency },
|
|
1492
|
+
...job.tenantScope === undefined ? {} : { tenantScope: job.tenantScope }
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
if (module.featureSpec) {
|
|
1496
|
+
features.push({
|
|
1497
|
+
name: module.featureSpec.name,
|
|
1498
|
+
states: [...module.featureSpec.states],
|
|
1499
|
+
...module.featureSpec.tenantField === undefined ? {} : { tenantField: module.featureSpec.tenantField },
|
|
1500
|
+
transitions: module.featureSpec.transitions.map((transition) => {
|
|
1501
|
+
const job = transition.job === undefined ? undefined : (module.jobs ?? []).find((candidate) => candidate.className === transition.job || candidate.name === transition.job);
|
|
1502
|
+
return {
|
|
1503
|
+
name: transition.name,
|
|
1504
|
+
from: transition.from,
|
|
1505
|
+
to: transition.to,
|
|
1506
|
+
...transition.permission === undefined ? {} : { permission: transition.permission },
|
|
1507
|
+
...transition.command === undefined ? {} : { command: transition.command },
|
|
1508
|
+
...transition.route === undefined ? {} : { route: transition.route },
|
|
1509
|
+
...transition.routeBinding === undefined ? {} : { routeBinding: transition.routeBinding },
|
|
1510
|
+
...transition.transaction === undefined ? {} : { transaction: transition.transaction },
|
|
1511
|
+
...transition.idempotency === undefined ? {} : { idempotency: transition.idempotency },
|
|
1512
|
+
...transition.audit === undefined ? {} : { audit: transition.audit },
|
|
1513
|
+
...transition.job === undefined ? {} : { job: transition.job },
|
|
1514
|
+
...job === undefined ? {} : {
|
|
1515
|
+
jobDetails: {
|
|
1516
|
+
name: job.name,
|
|
1517
|
+
className: job.className,
|
|
1518
|
+
...job.input === undefined ? {} : { input: job.input },
|
|
1519
|
+
...job.schemaKinds?.input === undefined ? {} : { inputKind: job.schemaKinds.input },
|
|
1520
|
+
...job.output === undefined ? {} : { output: job.output },
|
|
1521
|
+
...job.schemaKinds?.output === undefined ? {} : { outputKind: job.schemaKinds.output },
|
|
1522
|
+
...job.mode === undefined ? {} : { mode: job.mode },
|
|
1523
|
+
...job.idempotency === undefined ? {} : { idempotency: job.idempotency },
|
|
1524
|
+
...job.tenantScope === undefined ? {} : { tenantScope: job.tenantScope }
|
|
1525
|
+
}
|
|
1526
|
+
},
|
|
1527
|
+
...transition.persistence === undefined ? {} : { persistence: transition.persistence }
|
|
1528
|
+
};
|
|
1529
|
+
})
|
|
1530
|
+
});
|
|
1322
1531
|
}
|
|
1323
1532
|
for (const controller of module.controllers) {
|
|
1324
1533
|
for (const route of controller.routes) {
|
|
@@ -1349,6 +1558,8 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1349
1558
|
}
|
|
1350
1559
|
commands.sort((left, right) => left.name.localeCompare(right.name));
|
|
1351
1560
|
queries.sort((left, right) => left.name.localeCompare(right.name));
|
|
1561
|
+
jobs.sort((left, right) => left.name.localeCompare(right.name));
|
|
1562
|
+
features.sort((left, right) => left.name.localeCompare(right.name));
|
|
1352
1563
|
routes.sort((left, right) => `${left.method} ${left.path}`.localeCompare(`${right.method} ${right.path}`));
|
|
1353
1564
|
rpc.sort((left, right) => left.command.localeCompare(right.command));
|
|
1354
1565
|
events.sort((left, right) => left.name.localeCompare(right.name));
|
|
@@ -1356,6 +1567,8 @@ function buildContractManifest(graph, artifacts) {
|
|
|
1356
1567
|
version: 1,
|
|
1357
1568
|
commands,
|
|
1358
1569
|
queries,
|
|
1570
|
+
jobs,
|
|
1571
|
+
features,
|
|
1359
1572
|
routes,
|
|
1360
1573
|
permissions: [...permissions].sort(),
|
|
1361
1574
|
rpc,
|
|
@@ -1503,6 +1716,7 @@ function renderApplication(graph, options) {
|
|
|
1503
1716
|
name: command.name,
|
|
1504
1717
|
permission: command.permission ?? null,
|
|
1505
1718
|
rpc: command.rpc ?? null,
|
|
1719
|
+
branches: command.branches ?? [],
|
|
1506
1720
|
transaction: command.transaction ?? null,
|
|
1507
1721
|
audit: command.audit ?? null,
|
|
1508
1722
|
idempotency: command.idempotency ?? null
|
|
@@ -1929,7 +2143,9 @@ ${indent(item, 2)}`).join(",")}
|
|
|
1929
2143
|
`transaction: ${JSON.stringify(command.transaction)}`,
|
|
1930
2144
|
...command.audit ? [`audit: ${JSON.stringify(command.audit)}`] : [],
|
|
1931
2145
|
`idempotency: ${JSON.stringify(command.idempotency)}`,
|
|
2146
|
+
...command.tenantScope ? [`tenantScope: ${JSON.stringify(command.tenantScope)}`] : [],
|
|
1932
2147
|
...command.rpc ? [`rpc: ${JSON.stringify(command.rpc)}`] : [],
|
|
2148
|
+
...command.branches && command.branches.length > 0 ? [`branches: ${JSON.stringify(command.branches)}`] : [],
|
|
1933
2149
|
...command.standalone ? ["standalone: true"] : [],
|
|
1934
2150
|
...command.aspects && command.aspects.length > 0 ? [`aspects: ${this.renderAspects(command.aspects)}`, `aspectPipeline: ${this.renderAspectPipeline(command.aspects)}`] : []
|
|
1935
2151
|
];
|
|
@@ -1962,6 +2178,8 @@ ${indent(item, 2)}`).join(",")}
|
|
|
1962
2178
|
fields.push(`maxAttempts: ${job.maxAttempts}`);
|
|
1963
2179
|
if (job.idempotency !== undefined)
|
|
1964
2180
|
fields.push(`idempotency: ${JSON.stringify(job.idempotency)}`);
|
|
2181
|
+
if (job.tenantScope !== undefined)
|
|
2182
|
+
fields.push(`tenantScope: ${JSON.stringify(job.tenantScope)}`);
|
|
1965
2183
|
if (job.aspects && job.aspects.length > 0) {
|
|
1966
2184
|
fields.push(`aspects: ${this.renderAspects(job.aspects)}`);
|
|
1967
2185
|
fields.push(`aspectPipeline: ${this.renderAspectPipeline(job.aspects)}`);
|
|
@@ -3665,17 +3883,28 @@ var HEADER = "// GENERATED BY @supacloud/compiler — do not edit", INTERFACES =
|
|
|
3665
3883
|
|
|
3666
3884
|
export interface CompiledCommand {
|
|
3667
3885
|
rpc?: string;
|
|
3886
|
+
branches?: readonly CompiledCommandBranch[];
|
|
3668
3887
|
className: string;
|
|
3669
3888
|
name: string;
|
|
3670
3889
|
permission: string;
|
|
3671
3890
|
transaction: "required" | "none";
|
|
3672
3891
|
audit?: string;
|
|
3673
3892
|
idempotency: "required" | "none";
|
|
3893
|
+
tenantScope?: "required" | "none";
|
|
3674
3894
|
standalone?: boolean;
|
|
3675
3895
|
aspects?: CompiledAspect[];
|
|
3676
3896
|
aspectPipeline?: CompiledAspectPipeline;
|
|
3677
3897
|
}
|
|
3678
3898
|
|
|
3899
|
+
export interface CompiledCommandBranch {
|
|
3900
|
+
when: string;
|
|
3901
|
+
rpc: string;
|
|
3902
|
+
transaction?: "required" | "none" | string;
|
|
3903
|
+
audit?: string;
|
|
3904
|
+
idempotency?: "required" | "none" | string;
|
|
3905
|
+
boundary?: "database" | "external";
|
|
3906
|
+
}
|
|
3907
|
+
|
|
3679
3908
|
export interface CompiledJob {
|
|
3680
3909
|
className: string;
|
|
3681
3910
|
name: string;
|
|
@@ -3687,6 +3916,7 @@ export interface CompiledJob {
|
|
|
3687
3916
|
timeoutSec?: number;
|
|
3688
3917
|
maxAttempts?: number;
|
|
3689
3918
|
idempotency?: "required" | "none";
|
|
3919
|
+
tenantScope?: "required" | "none";
|
|
3690
3920
|
aspects?: CompiledAspect[];
|
|
3691
3921
|
aspectPipeline?: CompiledAspectPipeline;
|
|
3692
3922
|
}
|
|
@@ -15088,6 +15318,7 @@ function createContextPack(graph, subject) {
|
|
|
15088
15318
|
} : undefined;
|
|
15089
15319
|
const files = [...new Set(modules.flatMap((module) => [
|
|
15090
15320
|
module.file,
|
|
15321
|
+
...module.featureSpec?.file ? [module.featureSpec.file] : [],
|
|
15091
15322
|
...module.providers.map((provider) => provider.file),
|
|
15092
15323
|
...module.controllers.map((controller) => controller.file),
|
|
15093
15324
|
...graph.moduleHandlerFiles?.[module.name] ?? [],
|
|
@@ -15942,7 +16173,7 @@ async function readDeliverySelection(manifestPath, target) {
|
|
|
15942
16173
|
return { root, object, planned };
|
|
15943
16174
|
}
|
|
15944
16175
|
async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
|
|
15945
|
-
if (object.files.length > 1024 || object.files.some((file) => file.bytes >
|
|
16176
|
+
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) {
|
|
15946
16177
|
throw new Error("Artifact inventory exceeds limits.");
|
|
15947
16178
|
}
|
|
15948
16179
|
const objectRoot = join8(root, "objects", object.objectId);
|
|
@@ -15981,10 +16212,12 @@ async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
|
|
|
15981
16212
|
}
|
|
15982
16213
|
return retained;
|
|
15983
16214
|
}
|
|
15984
|
-
var JSON_LIMIT = 1048576;
|
|
16215
|
+
var JSON_LIMIT = 1048576, DELIVERY_ARTIFACT_MAX_FILE_BYTES, DELIVERY_ARTIFACT_MAX_TOTAL_BYTES;
|
|
15985
16216
|
var init_delivery_artifact = __esm(() => {
|
|
15986
16217
|
init_delivery_build_schema();
|
|
15987
16218
|
init_delivery_files();
|
|
16219
|
+
DELIVERY_ARTIFACT_MAX_FILE_BYTES = 500 * 1024 * 1024;
|
|
16220
|
+
DELIVERY_ARTIFACT_MAX_TOTAL_BYTES = 500 * 1024 * 1024;
|
|
15988
16221
|
});
|
|
15989
16222
|
|
|
15990
16223
|
// node_modules/@supacloud/delivery/src/application-release.ts
|
|
@@ -16843,6 +17076,7 @@ var init_delivery_migration_plan = () => {};
|
|
|
16843
17076
|
|
|
16844
17077
|
// node_modules/@supacloud/delivery/src/index.ts
|
|
16845
17078
|
var init_src = __esm(() => {
|
|
17079
|
+
init_delivery_artifact();
|
|
16846
17080
|
init_delivery_artifact();
|
|
16847
17081
|
init_worker_execution();
|
|
16848
17082
|
init_worker_execution();
|
|
@@ -18765,6 +18999,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
|
|
|
18765
18999
|
if (meta && booleanProp(meta, "standalone")) {
|
|
18766
19000
|
const permission = stringLiteralProp(meta, "permission");
|
|
18767
19001
|
const rpc = checkedRpc(meta, ctx);
|
|
19002
|
+
const branches = checkedBranches(meta, ctx);
|
|
18768
19003
|
const audit = stringLiteralProp(meta, "audit");
|
|
18769
19004
|
const standaloneName = stringLiteralProp(meta, "name") ?? classInfo.decl.name?.text ?? name;
|
|
18770
19005
|
const standaloneUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${standaloneName}`, sourcePath(ctx.rootDir, classInfo.file), lineOf(classInfo.decl));
|
|
@@ -18774,6 +19009,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
|
|
|
18774
19009
|
name: standaloneName,
|
|
18775
19010
|
...permission === undefined ? {} : { permission },
|
|
18776
19011
|
...rpc === undefined ? {} : { rpc },
|
|
19012
|
+
...branches === undefined ? {} : { branches },
|
|
18777
19013
|
transaction: checkedCommandMode(meta, "transaction", ctx, classInfo.decl.name?.text ?? name) ?? "none",
|
|
18778
19014
|
...audit === undefined ? {} : { audit },
|
|
18779
19015
|
idempotency: checkedCommandMode(meta, "idempotency", ctx, classInfo.decl.name?.text ?? name) ?? "none",
|
|
@@ -19105,18 +19341,22 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19105
19341
|
const aspects = parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${cls.name?.text ?? "<anonymous>"}`);
|
|
19106
19342
|
const permission = stringLiteralProp(meta, "permission");
|
|
19107
19343
|
const rpc = checkedRpc(meta, ctx);
|
|
19344
|
+
const branches = checkedBranches(meta, ctx);
|
|
19108
19345
|
const audit = stringLiteralProp(meta, "audit");
|
|
19109
19346
|
const commandName = stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>";
|
|
19110
19347
|
const uses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19111
19348
|
rejectUndeclaredResourceUses(uses, moduleResourceNames, ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19349
|
+
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19112
19350
|
commands.push({
|
|
19113
19351
|
className: cls.name?.text ?? "<anonymous>",
|
|
19114
19352
|
name: commandName,
|
|
19115
19353
|
...permission === undefined ? {} : { permission },
|
|
19116
19354
|
...rpc === undefined ? {} : { rpc },
|
|
19355
|
+
...branches === undefined ? {} : { branches },
|
|
19117
19356
|
transaction: checkedCommandMode(meta, "transaction", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
|
|
19118
19357
|
...audit === undefined ? {} : { audit },
|
|
19119
19358
|
idempotency: checkedCommandMode(meta, "idempotency", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
|
|
19359
|
+
...tenantScope === undefined ? {} : { tenantScope },
|
|
19120
19360
|
...booleanProp(meta, "standalone") ? { standalone: true } : {},
|
|
19121
19361
|
...uses.length > 0 ? { uses } : {},
|
|
19122
19362
|
...aspects.length > 0 ? { aspects } : {}
|
|
@@ -19147,12 +19387,14 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19147
19387
|
const contract = parseJobOptions(meta, className, ctx);
|
|
19148
19388
|
const jobUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19149
19389
|
rejectUndeclaredResourceUses(jobUses, moduleResourceNames, ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19390
|
+
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19150
19391
|
jobs.push({
|
|
19151
19392
|
className,
|
|
19152
19393
|
name: stringLiteralProp(meta, "name") ?? className,
|
|
19153
19394
|
serviceKey: camelName(provider?.token ?? className),
|
|
19154
19395
|
scope,
|
|
19155
19396
|
...contract,
|
|
19397
|
+
...tenantScope === undefined ? {} : { tenantScope },
|
|
19156
19398
|
...jobUses.length > 0 ? { uses: jobUses } : {},
|
|
19157
19399
|
...aspects.length > 0 ? { aspects } : {}
|
|
19158
19400
|
});
|
|
@@ -19162,9 +19404,11 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19162
19404
|
if (queryDec) {
|
|
19163
19405
|
const meta = decoratorObjectArg(queryDec);
|
|
19164
19406
|
if (meta) {
|
|
19407
|
+
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19165
19408
|
queries.push({
|
|
19166
19409
|
className: cls.name?.text ?? "<anonymous>",
|
|
19167
|
-
name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>"
|
|
19410
|
+
name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
|
|
19411
|
+
...tenantScope === undefined ? {} : { tenantScope }
|
|
19168
19412
|
});
|
|
19169
19413
|
}
|
|
19170
19414
|
}
|
|
@@ -19219,6 +19463,7 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19219
19463
|
if (!ts7.isObjectLiteralExpression(input))
|
|
19220
19464
|
return invalid();
|
|
19221
19465
|
const name = stringLiteralProp(input, "name");
|
|
19466
|
+
const tenantField = stringLiteralProp(input, "tenantField");
|
|
19222
19467
|
const statesExpr = getProp(input, "states");
|
|
19223
19468
|
const transitionObject = getProp(input, "transitions");
|
|
19224
19469
|
if (!name || !statesExpr || !ts7.isArrayLiteralExpression(statesExpr) || statesExpr.elements.some((state) => !ts7.isStringLiteral(state)) || !transitionObject || !ts7.isObjectLiteralExpression(transitionObject) || input.properties.some((property) => !ts7.isPropertyAssignment(property))) {
|
|
@@ -19237,15 +19482,23 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19237
19482
|
const options = property.initializer;
|
|
19238
19483
|
const from = stringLiteralProp(options, "from");
|
|
19239
19484
|
const to = stringLiteralProp(options, "to");
|
|
19240
|
-
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))) {
|
|
19485
|
+
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))) {
|
|
19241
19486
|
return invalid();
|
|
19242
19487
|
}
|
|
19243
19488
|
const permission = stringLiteralProp(options, "permission");
|
|
19244
19489
|
const command = stringLiteralProp(options, "command");
|
|
19245
19490
|
const route = stringLiteralProp(options, "route");
|
|
19491
|
+
const routeBinding = stringLiteralProp(options, "routeBinding");
|
|
19492
|
+
if (routeBinding !== undefined && routeBinding !== "command" && routeBinding !== "controller")
|
|
19493
|
+
return invalid();
|
|
19246
19494
|
const transaction = commandModeProp(options, "transaction");
|
|
19247
19495
|
const idempotency = commandModeProp(options, "idempotency");
|
|
19248
19496
|
const audit = stringLiteralProp(options, "audit");
|
|
19497
|
+
const job = stringLiteralProp(options, "job");
|
|
19498
|
+
const persistenceExpr = getProp(options, "persistence");
|
|
19499
|
+
const persistence = persistenceExpr && ts7.isObjectLiteralExpression(persistenceExpr) ? parseFeaturePersistence(persistenceExpr) : undefined;
|
|
19500
|
+
if (persistenceExpr && !persistence)
|
|
19501
|
+
return invalid();
|
|
19249
19502
|
transitions.push({
|
|
19250
19503
|
name: propertyName(property.name),
|
|
19251
19504
|
from,
|
|
@@ -19253,12 +19506,44 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19253
19506
|
...permission === undefined ? {} : { permission },
|
|
19254
19507
|
...command === undefined ? {} : { command },
|
|
19255
19508
|
...route === undefined ? {} : { route },
|
|
19509
|
+
...routeBinding === undefined ? {} : { routeBinding },
|
|
19256
19510
|
...transaction === undefined ? {} : { transaction },
|
|
19257
19511
|
...idempotency === undefined ? {} : { idempotency },
|
|
19258
|
-
...audit === undefined ? {} : { audit }
|
|
19512
|
+
...audit === undefined ? {} : { audit },
|
|
19513
|
+
...job === undefined ? {} : { job },
|
|
19514
|
+
...persistence === undefined ? {} : { persistence }
|
|
19259
19515
|
});
|
|
19260
19516
|
}
|
|
19261
|
-
return {
|
|
19517
|
+
return {
|
|
19518
|
+
name,
|
|
19519
|
+
states,
|
|
19520
|
+
...tenantField === undefined ? {} : { tenantField },
|
|
19521
|
+
transitions,
|
|
19522
|
+
file: sourcePath(ctx.rootDir, input.getSourceFile().fileName),
|
|
19523
|
+
line: lineOf(input)
|
|
19524
|
+
};
|
|
19525
|
+
}
|
|
19526
|
+
function parseFeaturePersistence(input) {
|
|
19527
|
+
if (input.properties.some((property) => !ts7.isPropertyAssignment(property)))
|
|
19528
|
+
return;
|
|
19529
|
+
const boundary = stringLiteralProp(input, "boundary");
|
|
19530
|
+
const optimisticLock = commandModeProp(input, "optimisticLock");
|
|
19531
|
+
const versionField = stringLiteralProp(input, "versionField");
|
|
19532
|
+
const tenantField = stringLiteralProp(input, "tenantField");
|
|
19533
|
+
const stateField = stringLiteralProp(input, "stateField");
|
|
19534
|
+
const receipt = commandModeProp(input, "receipt");
|
|
19535
|
+
const reconciliation = commandModeProp(input, "reconciliation");
|
|
19536
|
+
if (boundary !== "database" && boundary !== "external" || optimisticLock !== "required" && optimisticLock !== "none" || receipt !== "required" && receipt !== "none" || reconciliation !== "required" && reconciliation !== "none")
|
|
19537
|
+
return;
|
|
19538
|
+
return {
|
|
19539
|
+
boundary,
|
|
19540
|
+
optimisticLock,
|
|
19541
|
+
...versionField === undefined ? {} : { versionField },
|
|
19542
|
+
...tenantField === undefined ? {} : { tenantField },
|
|
19543
|
+
...stateField === undefined ? {} : { stateField },
|
|
19544
|
+
receipt,
|
|
19545
|
+
reconciliation
|
|
19546
|
+
};
|
|
19262
19547
|
}
|
|
19263
19548
|
function resolveStaticObjectLiteral(input, ctx, seen = new Set) {
|
|
19264
19549
|
if (!input || seen.has(input))
|
|
@@ -20685,6 +20970,75 @@ function checkedRpc(meta, ctx) {
|
|
|
20685
20970
|
});
|
|
20686
20971
|
return;
|
|
20687
20972
|
}
|
|
20973
|
+
function checkedBranches(meta, ctx) {
|
|
20974
|
+
const expression = getProp(meta, "branches");
|
|
20975
|
+
if (!expression)
|
|
20976
|
+
return;
|
|
20977
|
+
if (!ts7.isArrayLiteralExpression(expression)) {
|
|
20978
|
+
ctx.diagnostics.push({
|
|
20979
|
+
severity: "error",
|
|
20980
|
+
code: "invalid-command-branches",
|
|
20981
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
20982
|
+
line: lineOf(expression),
|
|
20983
|
+
message: "Command branches must be an explicit object array.",
|
|
20984
|
+
suggestion: "Declare each branch as { when, rpc, transaction, audit, idempotency, boundary }."
|
|
20985
|
+
});
|
|
20986
|
+
return;
|
|
20987
|
+
}
|
|
20988
|
+
const branches = [];
|
|
20989
|
+
for (const item of expression.elements) {
|
|
20990
|
+
if (!ts7.isObjectLiteralExpression(item)) {
|
|
20991
|
+
ctx.diagnostics.push({
|
|
20992
|
+
severity: "error",
|
|
20993
|
+
code: "invalid-command-branches",
|
|
20994
|
+
file: sourcePath(ctx.rootDir, item.getSourceFile().fileName),
|
|
20995
|
+
line: lineOf(item),
|
|
20996
|
+
message: "Command branches entries must be object literals."
|
|
20997
|
+
});
|
|
20998
|
+
continue;
|
|
20999
|
+
}
|
|
21000
|
+
const when = stringLiteralProp(item, "when");
|
|
21001
|
+
const rpc = stringLiteralProp(item, "rpc");
|
|
21002
|
+
const transaction = checkedCommandMode(item, "transaction", ctx, "command branch") ?? "none";
|
|
21003
|
+
const audit = stringLiteralProp(item, "audit");
|
|
21004
|
+
const idempotency = checkedCommandMode(item, "idempotency", ctx, "command branch") ?? "none";
|
|
21005
|
+
const boundary = stringLiteralProp(item, "boundary");
|
|
21006
|
+
const validBoundary = boundary === undefined || boundary === "database" || boundary === "external";
|
|
21007
|
+
const validWhen = when !== undefined && /^(?:[A-Za-z_$][\w$]*)(?:\.[A-Za-z_$][\w$]*)*\s*===\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')$/.test(when);
|
|
21008
|
+
if (!when || !validWhen || !rpc?.trim() || !validBoundary) {
|
|
21009
|
+
ctx.diagnostics.push({
|
|
21010
|
+
severity: "error",
|
|
21011
|
+
code: "invalid-command-branches",
|
|
21012
|
+
file: sourcePath(ctx.rootDir, item.getSourceFile().fileName),
|
|
21013
|
+
line: lineOf(item),
|
|
21014
|
+
message: "Command branches require a restricted equality selector, a non-empty rpc, and a valid boundary.",
|
|
21015
|
+
suggestion: `Use when: 'resource === "value"' (or body.resource/params.resource/query.resource), plus a named RPC adapter.`
|
|
21016
|
+
});
|
|
21017
|
+
continue;
|
|
21018
|
+
}
|
|
21019
|
+
branches.push({
|
|
21020
|
+
when,
|
|
21021
|
+
rpc,
|
|
21022
|
+
transaction,
|
|
21023
|
+
...audit === undefined ? {} : { audit },
|
|
21024
|
+
idempotency,
|
|
21025
|
+
...boundary === undefined ? {} : { boundary }
|
|
21026
|
+
});
|
|
21027
|
+
}
|
|
21028
|
+
if (branches.length === 0)
|
|
21029
|
+
return [];
|
|
21030
|
+
if (new Set(branches.map((branch) => branch.when)).size !== branches.length) {
|
|
21031
|
+
ctx.diagnostics.push({
|
|
21032
|
+
severity: "error",
|
|
21033
|
+
code: "duplicate-command-branch",
|
|
21034
|
+
file: sourcePath(ctx.rootDir, expression.getSourceFile().fileName),
|
|
21035
|
+
line: lineOf(expression),
|
|
21036
|
+
message: "Command branches must use distinct selectors.",
|
|
21037
|
+
suggestion: "Make each branch selector mutually distinct."
|
|
21038
|
+
});
|
|
21039
|
+
}
|
|
21040
|
+
return branches;
|
|
21041
|
+
}
|
|
20688
21042
|
function classDeps(cls, ctx) {
|
|
20689
21043
|
const injectable = parseInjectableOptions(cls, ctx);
|
|
20690
21044
|
const ctor = cls.members.find(ts7.isConstructorDeclaration);
|
|
@@ -24096,10 +24450,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
24096
24450
|
import { resolve as resolve17 } from "node:path";
|
|
24097
24451
|
|
|
24098
24452
|
// src/migration-app-version.ts
|
|
24099
|
-
var MIGRATION_APP_VERSION = "0.
|
|
24453
|
+
var MIGRATION_APP_VERSION = "0.29.0";
|
|
24100
24454
|
|
|
24101
24455
|
// src/migration-elysia-version.ts
|
|
24102
|
-
var MIGRATION_ELYSIA_VERSION = "0.
|
|
24456
|
+
var MIGRATION_ELYSIA_VERSION = "0.34.0";
|
|
24103
24457
|
|
|
24104
24458
|
// src/migration-policy.ts
|
|
24105
24459
|
var cachedCompilerVersion;
|
|
@@ -25068,6 +25422,163 @@ function isRecord2(value) {
|
|
|
25068
25422
|
function arrayLength(value) {
|
|
25069
25423
|
return Array.isArray(value) ? value.length : undefined;
|
|
25070
25424
|
}
|
|
25425
|
+
function safeField(value) {
|
|
25426
|
+
return typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
|
25427
|
+
}
|
|
25428
|
+
function validFeaturePersistence(value) {
|
|
25429
|
+
if (!isRecord2(value))
|
|
25430
|
+
return false;
|
|
25431
|
+
const boundary = value["boundary"];
|
|
25432
|
+
const receipt = value["receipt"];
|
|
25433
|
+
const reconciliation = value["reconciliation"];
|
|
25434
|
+
if (boundary === "database") {
|
|
25435
|
+
const fields = [value["tenantField"], value["versionField"], value["stateField"]];
|
|
25436
|
+
return value["optimisticLock"] === "required" && fields.every(safeField) && new Set(fields).size === 3 && receipt === "required" && reconciliation === "none";
|
|
25437
|
+
}
|
|
25438
|
+
if (boundary === "external") {
|
|
25439
|
+
return value["optimisticLock"] === "none" && value["versionField"] === undefined && value["tenantField"] === undefined && value["stateField"] === undefined && receipt === "required" && reconciliation === "required";
|
|
25440
|
+
}
|
|
25441
|
+
return false;
|
|
25442
|
+
}
|
|
25443
|
+
function validCommandInventory(value) {
|
|
25444
|
+
const names = new Set;
|
|
25445
|
+
const classes = new Set;
|
|
25446
|
+
return Array.isArray(value) && value.every((command) => {
|
|
25447
|
+
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")
|
|
25448
|
+
return false;
|
|
25449
|
+
if (names.has(command["name"]) || classes.has(command["className"]))
|
|
25450
|
+
return false;
|
|
25451
|
+
if (command["rpc"] !== undefined && (typeof command["rpc"] !== "string" || command["rpc"].trim() === ""))
|
|
25452
|
+
return false;
|
|
25453
|
+
if (command["auditEvent"] !== undefined && (typeof command["auditEvent"] !== "string" || command["auditEvent"].trim() === ""))
|
|
25454
|
+
return false;
|
|
25455
|
+
names.add(command["name"]);
|
|
25456
|
+
classes.add(command["className"]);
|
|
25457
|
+
return true;
|
|
25458
|
+
});
|
|
25459
|
+
}
|
|
25460
|
+
function validFeatureInventory(value, jobs, commands, routes, rpc) {
|
|
25461
|
+
const featureNames = new Set;
|
|
25462
|
+
return Array.isArray(value) && value.every((feature) => {
|
|
25463
|
+
if (!isRecord2(feature) || typeof feature["name"] !== "string" || feature["name"].trim() === "")
|
|
25464
|
+
return false;
|
|
25465
|
+
if (featureNames.has(feature["name"]))
|
|
25466
|
+
return false;
|
|
25467
|
+
featureNames.add(feature["name"]);
|
|
25468
|
+
if (feature["tenantField"] !== undefined && !safeField(feature["tenantField"]))
|
|
25469
|
+
return false;
|
|
25470
|
+
const states = feature["states"];
|
|
25471
|
+
const transitions = feature["transitions"];
|
|
25472
|
+
if (!Array.isArray(states) || states.length === 0 || states.some((state) => typeof state !== "string" || state.trim() === "") || new Set(states).size !== states.length || !Array.isArray(transitions))
|
|
25473
|
+
return false;
|
|
25474
|
+
const transitionNames = new Set;
|
|
25475
|
+
return transitions.every((transition) => {
|
|
25476
|
+
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"]))
|
|
25477
|
+
return false;
|
|
25478
|
+
if (transitionNames.has(transition["name"]))
|
|
25479
|
+
return false;
|
|
25480
|
+
transitionNames.add(transition["name"]);
|
|
25481
|
+
const persistence = transition["persistence"];
|
|
25482
|
+
if (persistence !== undefined) {
|
|
25483
|
+
if (!isRecord2(persistence) || !validFeaturePersistence(persistence))
|
|
25484
|
+
return false;
|
|
25485
|
+
if (typeof transition["command"] !== "string" || transition["command"].trim() === "" || typeof transition["permission"] !== "string" || transition["permission"].trim() === "")
|
|
25486
|
+
return false;
|
|
25487
|
+
if (persistence["boundary"] === "database" && feature["tenantField"] !== persistence["tenantField"])
|
|
25488
|
+
return false;
|
|
25489
|
+
}
|
|
25490
|
+
const commandName = transition["command"];
|
|
25491
|
+
if (commandName !== undefined) {
|
|
25492
|
+
const command = commands.find((candidate) => isRecord2(candidate) && (candidate["name"] === commandName || candidate["className"] === commandName));
|
|
25493
|
+
if (!isRecord2(command))
|
|
25494
|
+
return false;
|
|
25495
|
+
if (transition["permission"] !== undefined && command["permission"] !== transition["permission"])
|
|
25496
|
+
return false;
|
|
25497
|
+
if (transition["transaction"] !== undefined && command["transaction"] !== transition["transaction"])
|
|
25498
|
+
return false;
|
|
25499
|
+
if (transition["idempotency"] !== undefined && command["idempotency"] !== transition["idempotency"])
|
|
25500
|
+
return false;
|
|
25501
|
+
if (transition["audit"] !== undefined && command["auditEvent"] !== transition["audit"])
|
|
25502
|
+
return false;
|
|
25503
|
+
if (persistence !== undefined) {
|
|
25504
|
+
const adapter = command["rpc"];
|
|
25505
|
+
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() === "")
|
|
25506
|
+
return false;
|
|
25507
|
+
const expectedTransaction = persistence["boundary"] === "database" ? "required" : "none";
|
|
25508
|
+
if (command["transaction"] !== expectedTransaction)
|
|
25509
|
+
return false;
|
|
25510
|
+
}
|
|
25511
|
+
}
|
|
25512
|
+
const routeName = transition["route"];
|
|
25513
|
+
const routeBinding = transition["routeBinding"];
|
|
25514
|
+
if (routeBinding !== undefined && routeBinding !== "command" && routeBinding !== "controller")
|
|
25515
|
+
return false;
|
|
25516
|
+
if (routeName !== undefined) {
|
|
25517
|
+
const route = routes.find((candidate) => isRecord2(candidate) && `${candidate["method"]} ${candidate["path"]}` === routeName);
|
|
25518
|
+
if (!isRecord2(route))
|
|
25519
|
+
return false;
|
|
25520
|
+
if (routeBinding === "controller") {
|
|
25521
|
+
if (route["command"] !== undefined)
|
|
25522
|
+
return false;
|
|
25523
|
+
} else if (route["command"] !== commandName) {
|
|
25524
|
+
return false;
|
|
25525
|
+
}
|
|
25526
|
+
}
|
|
25527
|
+
const job = transition["job"];
|
|
25528
|
+
if (job === undefined)
|
|
25529
|
+
return true;
|
|
25530
|
+
const details = transition["jobDetails"];
|
|
25531
|
+
const inventory = jobs.find((candidate) => isRecord2(candidate) && (candidate["name"] === job || candidate["className"] === job));
|
|
25532
|
+
const contractFields = ["input", "inputKind", "output", "outputKind", "mode", "idempotency", "tenantScope"];
|
|
25533
|
+
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]);
|
|
25534
|
+
});
|
|
25535
|
+
});
|
|
25536
|
+
}
|
|
25537
|
+
function validJobInventory(value) {
|
|
25538
|
+
const names = new Set;
|
|
25539
|
+
const classes = new Set;
|
|
25540
|
+
return Array.isArray(value) && value.every((job) => {
|
|
25541
|
+
if (!isRecord2(job) || typeof job["name"] !== "string" || job["name"].trim() === "" || typeof job["className"] !== "string" || job["className"].trim() === "" || typeof job["module"] !== "string" || job["module"].trim() === "")
|
|
25542
|
+
return false;
|
|
25543
|
+
if (names.has(job["name"]) || classes.has(job["className"]))
|
|
25544
|
+
return false;
|
|
25545
|
+
names.add(job["name"]);
|
|
25546
|
+
classes.add(job["className"]);
|
|
25547
|
+
if (job["input"] !== undefined && (typeof job["input"] !== "string" || job["input"].trim() === ""))
|
|
25548
|
+
return false;
|
|
25549
|
+
if (job["inputKind"] !== undefined && job["inputKind"] !== "opaque" && job["inputKind"] !== "declared")
|
|
25550
|
+
return false;
|
|
25551
|
+
if (job["output"] !== undefined && (typeof job["output"] !== "string" || job["output"].trim() === ""))
|
|
25552
|
+
return false;
|
|
25553
|
+
if (job["outputKind"] !== undefined && job["outputKind"] !== "opaque" && job["outputKind"] !== "declared")
|
|
25554
|
+
return false;
|
|
25555
|
+
if (job["mode"] !== undefined && job["mode"] !== "task" && job["mode"] !== "workflow")
|
|
25556
|
+
return false;
|
|
25557
|
+
if (job["idempotency"] !== undefined && job["idempotency"] !== "required" && job["idempotency"] !== "none")
|
|
25558
|
+
return false;
|
|
25559
|
+
return job["tenantScope"] === undefined || job["tenantScope"] === "required" || job["tenantScope"] === "none";
|
|
25560
|
+
});
|
|
25561
|
+
}
|
|
25562
|
+
function validRpcInventory(value, commandInventory) {
|
|
25563
|
+
const commandNames = new Set;
|
|
25564
|
+
const adapters = new Set;
|
|
25565
|
+
return Array.isArray(value) && value.every((rpc) => {
|
|
25566
|
+
if (!isRecord2(rpc) || typeof rpc["command"] !== "string" || rpc["command"].trim() === "" || typeof rpc["adapter"] !== "string" || rpc["adapter"].trim() === "")
|
|
25567
|
+
return false;
|
|
25568
|
+
if (commandNames.has(rpc["command"]) || adapters.has(rpc["adapter"]))
|
|
25569
|
+
return false;
|
|
25570
|
+
const command = commandInventory.find((candidate) => isRecord2(candidate) && candidate["name"] === rpc["command"] && candidate["rpc"] === rpc["adapter"]);
|
|
25571
|
+
if (!isRecord2(command))
|
|
25572
|
+
return false;
|
|
25573
|
+
commandNames.add(rpc["command"]);
|
|
25574
|
+
adapters.add(rpc["adapter"]);
|
|
25575
|
+
return true;
|
|
25576
|
+
}) && commandInventory.every((command) => {
|
|
25577
|
+
if (!isRecord2(command) || command["rpc"] === undefined)
|
|
25578
|
+
return true;
|
|
25579
|
+
return value.some((rpc) => isRecord2(rpc) && rpc["command"] === command["name"] && rpc["adapter"] === command["rpc"]);
|
|
25580
|
+
});
|
|
25581
|
+
}
|
|
25071
25582
|
async function sha2566(path) {
|
|
25072
25583
|
return createHash11("sha256").update(await readFile11(path)).digest("hex");
|
|
25073
25584
|
}
|
|
@@ -25137,19 +25648,24 @@ async function assessMigration(options) {
|
|
|
25137
25648
|
};
|
|
25138
25649
|
try {
|
|
25139
25650
|
const parsed = JSON.parse(await readFile11(contractsManifestPath, "utf8"));
|
|
25140
|
-
if (!isRecord2(parsed) || parsed["version"] !== 1)
|
|
25651
|
+
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"])) {
|
|
25141
25652
|
throw new Error("invalid manifest");
|
|
25653
|
+
}
|
|
25142
25654
|
const commands = arrayLength(parsed["commands"]);
|
|
25655
|
+
const jobs = arrayLength(parsed["jobs"]);
|
|
25143
25656
|
const routes = arrayLength(parsed["routes"]);
|
|
25144
25657
|
const permissions = arrayLength(parsed["permissions"]);
|
|
25658
|
+
const features = arrayLength(parsed["features"]);
|
|
25145
25659
|
contractsManifest = {
|
|
25146
25660
|
path: contractsManifestPath,
|
|
25147
25661
|
present: true,
|
|
25148
25662
|
sha256: await sha2566(contractsManifestPath),
|
|
25149
25663
|
version: 1,
|
|
25150
25664
|
...commands === undefined ? {} : { commands },
|
|
25665
|
+
...jobs === undefined ? {} : { jobs },
|
|
25151
25666
|
...routes === undefined ? {} : { routes },
|
|
25152
|
-
...permissions === undefined ? {} : { permissions }
|
|
25667
|
+
...permissions === undefined ? {} : { permissions },
|
|
25668
|
+
...features === undefined ? {} : { features }
|
|
25153
25669
|
};
|
|
25154
25670
|
} catch {
|
|
25155
25671
|
findings.push({
|