@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/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
|
}
|
|
@@ -15943,7 +16173,7 @@ async function readDeliverySelection(manifestPath, target) {
|
|
|
15943
16173
|
return { root, object, planned };
|
|
15944
16174
|
}
|
|
15945
16175
|
async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
|
|
15946
|
-
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) {
|
|
15947
16177
|
throw new Error("Artifact inventory exceeds limits.");
|
|
15948
16178
|
}
|
|
15949
16179
|
const objectRoot = join8(root, "objects", object.objectId);
|
|
@@ -15982,10 +16212,12 @@ async function readVerifiedDeliveryFiles(root, object, target, selectedPaths) {
|
|
|
15982
16212
|
}
|
|
15983
16213
|
return retained;
|
|
15984
16214
|
}
|
|
15985
|
-
var JSON_LIMIT = 1048576;
|
|
16215
|
+
var JSON_LIMIT = 1048576, DELIVERY_ARTIFACT_MAX_FILE_BYTES, DELIVERY_ARTIFACT_MAX_TOTAL_BYTES;
|
|
15986
16216
|
var init_delivery_artifact = __esm(() => {
|
|
15987
16217
|
init_delivery_build_schema();
|
|
15988
16218
|
init_delivery_files();
|
|
16219
|
+
DELIVERY_ARTIFACT_MAX_FILE_BYTES = 500 * 1024 * 1024;
|
|
16220
|
+
DELIVERY_ARTIFACT_MAX_TOTAL_BYTES = 500 * 1024 * 1024;
|
|
15989
16221
|
});
|
|
15990
16222
|
|
|
15991
16223
|
// node_modules/@supacloud/delivery/src/application-release.ts
|
|
@@ -16844,6 +17076,7 @@ var init_delivery_migration_plan = () => {};
|
|
|
16844
17076
|
|
|
16845
17077
|
// node_modules/@supacloud/delivery/src/index.ts
|
|
16846
17078
|
var init_src = __esm(() => {
|
|
17079
|
+
init_delivery_artifact();
|
|
16847
17080
|
init_delivery_artifact();
|
|
16848
17081
|
init_worker_execution();
|
|
16849
17082
|
init_worker_execution();
|
|
@@ -18766,6 +18999,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
|
|
|
18766
18999
|
if (meta && booleanProp(meta, "standalone")) {
|
|
18767
19000
|
const permission = stringLiteralProp(meta, "permission");
|
|
18768
19001
|
const rpc = checkedRpc(meta, ctx);
|
|
19002
|
+
const branches = checkedBranches(meta, ctx);
|
|
18769
19003
|
const audit = stringLiteralProp(meta, "audit");
|
|
18770
19004
|
const standaloneName = stringLiteralProp(meta, "name") ?? classInfo.decl.name?.text ?? name;
|
|
18771
19005
|
const standaloneUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${standaloneName}`, sourcePath(ctx.rootDir, classInfo.file), lineOf(classInfo.decl));
|
|
@@ -18775,6 +19009,7 @@ async function analyzeProject(rootDir, include, cache, changedPaths, effectOptio
|
|
|
18775
19009
|
name: standaloneName,
|
|
18776
19010
|
...permission === undefined ? {} : { permission },
|
|
18777
19011
|
...rpc === undefined ? {} : { rpc },
|
|
19012
|
+
...branches === undefined ? {} : { branches },
|
|
18778
19013
|
transaction: checkedCommandMode(meta, "transaction", ctx, classInfo.decl.name?.text ?? name) ?? "none",
|
|
18779
19014
|
...audit === undefined ? {} : { audit },
|
|
18780
19015
|
idempotency: checkedCommandMode(meta, "idempotency", ctx, classInfo.decl.name?.text ?? name) ?? "none",
|
|
@@ -19106,18 +19341,22 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19106
19341
|
const aspects = parseAspectRefs(getProp(meta, "aspects"), ctx, `command ${cls.name?.text ?? "<anonymous>"}`);
|
|
19107
19342
|
const permission = stringLiteralProp(meta, "permission");
|
|
19108
19343
|
const rpc = checkedRpc(meta, ctx);
|
|
19344
|
+
const branches = checkedBranches(meta, ctx);
|
|
19109
19345
|
const audit = stringLiteralProp(meta, "audit");
|
|
19110
19346
|
const commandName = stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>";
|
|
19111
19347
|
const uses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19112
19348
|
rejectUndeclaredResourceUses(uses, moduleResourceNames, ctx, `command ${commandName}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19349
|
+
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19113
19350
|
commands.push({
|
|
19114
19351
|
className: cls.name?.text ?? "<anonymous>",
|
|
19115
19352
|
name: commandName,
|
|
19116
19353
|
...permission === undefined ? {} : { permission },
|
|
19117
19354
|
...rpc === undefined ? {} : { rpc },
|
|
19355
|
+
...branches === undefined ? {} : { branches },
|
|
19118
19356
|
transaction: checkedCommandMode(meta, "transaction", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
|
|
19119
19357
|
...audit === undefined ? {} : { audit },
|
|
19120
19358
|
idempotency: checkedCommandMode(meta, "idempotency", ctx, cls.name?.text ?? "<anonymous>") ?? "none",
|
|
19359
|
+
...tenantScope === undefined ? {} : { tenantScope },
|
|
19121
19360
|
...booleanProp(meta, "standalone") ? { standalone: true } : {},
|
|
19122
19361
|
...uses.length > 0 ? { uses } : {},
|
|
19123
19362
|
...aspects.length > 0 ? { aspects } : {}
|
|
@@ -19148,12 +19387,14 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19148
19387
|
const contract = parseJobOptions(meta, className, ctx);
|
|
19149
19388
|
const jobUses = parseInfraResourceUseRefs(infraResourceProp(meta, "uses", ctx), ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19150
19389
|
rejectUndeclaredResourceUses(jobUses, moduleResourceNames, ctx, `job ${stringLiteralProp(meta, "name") ?? className}`, sourcePath(ctx.rootDir, cls.getSourceFile().fileName), lineOf(cls));
|
|
19390
|
+
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19151
19391
|
jobs.push({
|
|
19152
19392
|
className,
|
|
19153
19393
|
name: stringLiteralProp(meta, "name") ?? className,
|
|
19154
19394
|
serviceKey: camelName(provider?.token ?? className),
|
|
19155
19395
|
scope,
|
|
19156
19396
|
...contract,
|
|
19397
|
+
...tenantScope === undefined ? {} : { tenantScope },
|
|
19157
19398
|
...jobUses.length > 0 ? { uses: jobUses } : {},
|
|
19158
19399
|
...aspects.length > 0 ? { aspects } : {}
|
|
19159
19400
|
});
|
|
@@ -19163,9 +19404,11 @@ function parseModule(candidate, nameByNode, ctx) {
|
|
|
19163
19404
|
if (queryDec) {
|
|
19164
19405
|
const meta = decoratorObjectArg(queryDec);
|
|
19165
19406
|
if (meta) {
|
|
19407
|
+
const tenantScope = commandModeProp(meta, "tenantScope");
|
|
19166
19408
|
queries.push({
|
|
19167
19409
|
className: cls.name?.text ?? "<anonymous>",
|
|
19168
|
-
name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>"
|
|
19410
|
+
name: stringLiteralProp(meta, "name") ?? cls.name?.text ?? "<anonymous>",
|
|
19411
|
+
...tenantScope === undefined ? {} : { tenantScope }
|
|
19169
19412
|
});
|
|
19170
19413
|
}
|
|
19171
19414
|
}
|
|
@@ -19220,6 +19463,7 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19220
19463
|
if (!ts7.isObjectLiteralExpression(input))
|
|
19221
19464
|
return invalid();
|
|
19222
19465
|
const name = stringLiteralProp(input, "name");
|
|
19466
|
+
const tenantField = stringLiteralProp(input, "tenantField");
|
|
19223
19467
|
const statesExpr = getProp(input, "states");
|
|
19224
19468
|
const transitionObject = getProp(input, "transitions");
|
|
19225
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))) {
|
|
@@ -19238,15 +19482,23 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19238
19482
|
const options = property.initializer;
|
|
19239
19483
|
const from = stringLiteralProp(options, "from");
|
|
19240
19484
|
const to = stringLiteralProp(options, "to");
|
|
19241
|
-
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))) {
|
|
19242
19486
|
return invalid();
|
|
19243
19487
|
}
|
|
19244
19488
|
const permission = stringLiteralProp(options, "permission");
|
|
19245
19489
|
const command = stringLiteralProp(options, "command");
|
|
19246
19490
|
const route = stringLiteralProp(options, "route");
|
|
19491
|
+
const routeBinding = stringLiteralProp(options, "routeBinding");
|
|
19492
|
+
if (routeBinding !== undefined && routeBinding !== "command" && routeBinding !== "controller")
|
|
19493
|
+
return invalid();
|
|
19247
19494
|
const transaction = commandModeProp(options, "transaction");
|
|
19248
19495
|
const idempotency = commandModeProp(options, "idempotency");
|
|
19249
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();
|
|
19250
19502
|
transitions.push({
|
|
19251
19503
|
name: propertyName(property.name),
|
|
19252
19504
|
from,
|
|
@@ -19254,12 +19506,44 @@ function parseFeatureSpec(input, ctx, seen = new Set) {
|
|
|
19254
19506
|
...permission === undefined ? {} : { permission },
|
|
19255
19507
|
...command === undefined ? {} : { command },
|
|
19256
19508
|
...route === undefined ? {} : { route },
|
|
19509
|
+
...routeBinding === undefined ? {} : { routeBinding },
|
|
19257
19510
|
...transaction === undefined ? {} : { transaction },
|
|
19258
19511
|
...idempotency === undefined ? {} : { idempotency },
|
|
19259
|
-
...audit === undefined ? {} : { audit }
|
|
19512
|
+
...audit === undefined ? {} : { audit },
|
|
19513
|
+
...job === undefined ? {} : { job },
|
|
19514
|
+
...persistence === undefined ? {} : { persistence }
|
|
19260
19515
|
});
|
|
19261
19516
|
}
|
|
19262
|
-
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
|
+
};
|
|
19263
19547
|
}
|
|
19264
19548
|
function resolveStaticObjectLiteral(input, ctx, seen = new Set) {
|
|
19265
19549
|
if (!input || seen.has(input))
|
|
@@ -20686,6 +20970,75 @@ function checkedRpc(meta, ctx) {
|
|
|
20686
20970
|
});
|
|
20687
20971
|
return;
|
|
20688
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
|
+
}
|
|
20689
21042
|
function classDeps(cls, ctx) {
|
|
20690
21043
|
const injectable = parseInjectableOptions(cls, ctx);
|
|
20691
21044
|
const ctor = cls.members.find(ts7.isConstructorDeclaration);
|
|
@@ -24097,10 +24450,10 @@ import { readFileSync as readFileSync4 } from "node:fs";
|
|
|
24097
24450
|
import { resolve as resolve17 } from "node:path";
|
|
24098
24451
|
|
|
24099
24452
|
// src/migration-app-version.ts
|
|
24100
|
-
var MIGRATION_APP_VERSION = "0.
|
|
24453
|
+
var MIGRATION_APP_VERSION = "0.29.0";
|
|
24101
24454
|
|
|
24102
24455
|
// src/migration-elysia-version.ts
|
|
24103
|
-
var MIGRATION_ELYSIA_VERSION = "0.
|
|
24456
|
+
var MIGRATION_ELYSIA_VERSION = "0.34.0";
|
|
24104
24457
|
|
|
24105
24458
|
// src/migration-policy.ts
|
|
24106
24459
|
var cachedCompilerVersion;
|
|
@@ -25069,6 +25422,163 @@ function isRecord2(value) {
|
|
|
25069
25422
|
function arrayLength(value) {
|
|
25070
25423
|
return Array.isArray(value) ? value.length : undefined;
|
|
25071
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
|
+
}
|
|
25072
25582
|
async function sha2566(path) {
|
|
25073
25583
|
return createHash11("sha256").update(await readFile11(path)).digest("hex");
|
|
25074
25584
|
}
|
|
@@ -25138,19 +25648,24 @@ async function assessMigration(options) {
|
|
|
25138
25648
|
};
|
|
25139
25649
|
try {
|
|
25140
25650
|
const parsed = JSON.parse(await readFile11(contractsManifestPath, "utf8"));
|
|
25141
|
-
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"])) {
|
|
25142
25652
|
throw new Error("invalid manifest");
|
|
25653
|
+
}
|
|
25143
25654
|
const commands = arrayLength(parsed["commands"]);
|
|
25655
|
+
const jobs = arrayLength(parsed["jobs"]);
|
|
25144
25656
|
const routes = arrayLength(parsed["routes"]);
|
|
25145
25657
|
const permissions = arrayLength(parsed["permissions"]);
|
|
25658
|
+
const features = arrayLength(parsed["features"]);
|
|
25146
25659
|
contractsManifest = {
|
|
25147
25660
|
path: contractsManifestPath,
|
|
25148
25661
|
present: true,
|
|
25149
25662
|
sha256: await sha2566(contractsManifestPath),
|
|
25150
25663
|
version: 1,
|
|
25151
25664
|
...commands === undefined ? {} : { commands },
|
|
25665
|
+
...jobs === undefined ? {} : { jobs },
|
|
25152
25666
|
...routes === undefined ? {} : { routes },
|
|
25153
|
-
...permissions === undefined ? {} : { permissions }
|
|
25667
|
+
...permissions === undefined ? {} : { permissions },
|
|
25668
|
+
...features === undefined ? {} : { features }
|
|
25154
25669
|
};
|
|
25155
25670
|
} catch {
|
|
25156
25671
|
findings.push({
|