dopespec 0.0.1
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/README.md +195 -0
- package/bin/dopespec.mjs +26 -0
- package/dist/__tests__/codegen.test.d.ts +2 -0
- package/dist/__tests__/codegen.test.d.ts.map +1 -0
- package/dist/__tests__/codegen.test.js +713 -0
- package/dist/__tests__/codegen.test.js.map +1 -0
- package/dist/__tests__/e2e-proof.test.d.ts +2 -0
- package/dist/__tests__/e2e-proof.test.d.ts.map +1 -0
- package/dist/__tests__/e2e-proof.test.js +153 -0
- package/dist/__tests__/e2e-proof.test.js.map +1 -0
- package/dist/__tests__/schema.test.d.ts +2 -0
- package/dist/__tests__/schema.test.d.ts.map +1 -0
- package/dist/__tests__/schema.test.js +822 -0
- package/dist/__tests__/schema.test.js.map +1 -0
- package/dist/__tests__/type-errors.d.ts +2 -0
- package/dist/__tests__/type-errors.d.ts.map +1 -0
- package/dist/__tests__/type-errors.js +128 -0
- package/dist/__tests__/type-errors.js.map +1 -0
- package/dist/cli/generate.d.ts +2 -0
- package/dist/cli/generate.d.ts.map +1 -0
- package/dist/cli/generate.js +250 -0
- package/dist/cli/generate.js.map +1 -0
- package/dist/codegen/commands.d.ts +4 -0
- package/dist/codegen/commands.d.ts.map +1 -0
- package/dist/codegen/commands.js +25 -0
- package/dist/codegen/commands.js.map +1 -0
- package/dist/codegen/decisions-evaluate.d.ts +3 -0
- package/dist/codegen/decisions-evaluate.d.ts.map +1 -0
- package/dist/codegen/decisions-evaluate.js +50 -0
- package/dist/codegen/decisions-evaluate.js.map +1 -0
- package/dist/codegen/decisions-table.d.ts +3 -0
- package/dist/codegen/decisions-table.d.ts.map +1 -0
- package/dist/codegen/decisions-table.js +27 -0
- package/dist/codegen/decisions-table.js.map +1 -0
- package/dist/codegen/decisions-tests.d.ts +3 -0
- package/dist/codegen/decisions-tests.d.ts.map +1 -0
- package/dist/codegen/decisions-tests.js +43 -0
- package/dist/codegen/decisions-tests.js.map +1 -0
- package/dist/codegen/e2e-stubs.d.ts +4 -0
- package/dist/codegen/e2e-stubs.d.ts.map +1 -0
- package/dist/codegen/e2e-stubs.js +21 -0
- package/dist/codegen/e2e-stubs.js.map +1 -0
- package/dist/codegen/events.d.ts +8 -0
- package/dist/codegen/events.d.ts.map +1 -0
- package/dist/codegen/events.js +36 -0
- package/dist/codegen/events.js.map +1 -0
- package/dist/codegen/index.d.ts +18 -0
- package/dist/codegen/index.d.ts.map +1 -0
- package/dist/codegen/index.js +18 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/codegen/invariants.d.ts +4 -0
- package/dist/codegen/invariants.d.ts.map +1 -0
- package/dist/codegen/invariants.js +53 -0
- package/dist/codegen/invariants.js.map +1 -0
- package/dist/codegen/mermaid.d.ts +4 -0
- package/dist/codegen/mermaid.d.ts.map +1 -0
- package/dist/codegen/mermaid.js +21 -0
- package/dist/codegen/mermaid.js.map +1 -0
- package/dist/codegen/orchestrators.d.ts +7 -0
- package/dist/codegen/orchestrators.d.ts.map +1 -0
- package/dist/codegen/orchestrators.js +32 -0
- package/dist/codegen/orchestrators.js.map +1 -0
- package/dist/codegen/policy-index.d.ts +7 -0
- package/dist/codegen/policy-index.d.ts.map +1 -0
- package/dist/codegen/policy-index.js +40 -0
- package/dist/codegen/policy-index.js.map +1 -0
- package/dist/codegen/policy-mermaid.d.ts +7 -0
- package/dist/codegen/policy-mermaid.d.ts.map +1 -0
- package/dist/codegen/policy-mermaid.js +30 -0
- package/dist/codegen/policy-mermaid.js.map +1 -0
- package/dist/codegen/policy-tests.d.ts +8 -0
- package/dist/codegen/policy-tests.d.ts.map +1 -0
- package/dist/codegen/policy-tests.js +167 -0
- package/dist/codegen/policy-tests.js.map +1 -0
- package/dist/codegen/policy-validator.d.ts +8 -0
- package/dist/codegen/policy-validator.d.ts.map +1 -0
- package/dist/codegen/policy-validator.js +69 -0
- package/dist/codegen/policy-validator.js.map +1 -0
- package/dist/codegen/tests.d.ts +4 -0
- package/dist/codegen/tests.d.ts.map +1 -0
- package/dist/codegen/tests.js +125 -0
- package/dist/codegen/tests.js.map +1 -0
- package/dist/codegen/transitions.d.ts +4 -0
- package/dist/codegen/transitions.d.ts.map +1 -0
- package/dist/codegen/transitions.js +41 -0
- package/dist/codegen/transitions.js.map +1 -0
- package/dist/codegen/types.d.ts +4 -0
- package/dist/codegen/types.d.ts.map +1 -0
- package/dist/codegen/types.js +48 -0
- package/dist/codegen/types.js.map +1 -0
- package/dist/codegen/utils.d.ts +85 -0
- package/dist/codegen/utils.d.ts.map +1 -0
- package/dist/codegen/utils.js +357 -0
- package/dist/codegen/utils.js.map +1 -0
- package/dist/codegen/zod.d.ts +4 -0
- package/dist/codegen/zod.d.ts.map +1 -0
- package/dist/codegen/zod.js +32 -0
- package/dist/codegen/zod.js.map +1 -0
- package/dist/examples/generated/customer.types.d.ts +7 -0
- package/dist/examples/generated/customer.types.d.ts.map +1 -0
- package/dist/examples/generated/customer.types.js +2 -0
- package/dist/examples/generated/customer.types.js.map +1 -0
- package/dist/examples/generated/order.types.d.ts +9 -0
- package/dist/examples/generated/order.types.d.ts.map +1 -0
- package/dist/examples/generated/order.types.js +2 -0
- package/dist/examples/generated/order.types.js.map +1 -0
- package/dist/examples/generated/pet.types.d.ts +11 -0
- package/dist/examples/generated/pet.types.d.ts.map +1 -0
- package/dist/examples/generated/pet.types.js +2 -0
- package/dist/examples/generated/pet.types.js.map +1 -0
- package/dist/examples/pet-store.d.ts +137 -0
- package/dist/examples/pet-store.d.ts.map +1 -0
- package/dist/examples/pet-store.js +139 -0
- package/dist/examples/pet-store.js.map +1 -0
- package/dist/examples/src/customer.e2e.d.ts +2 -0
- package/dist/examples/src/customer.e2e.d.ts.map +1 -0
- package/dist/examples/src/customer.e2e.js +17 -0
- package/dist/examples/src/customer.e2e.js.map +1 -0
- package/dist/examples/src/customer.orchestrators.d.ts +5 -0
- package/dist/examples/src/customer.orchestrators.d.ts.map +1 -0
- package/dist/examples/src/customer.orchestrators.js +5 -0
- package/dist/examples/src/customer.orchestrators.js.map +1 -0
- package/dist/examples/src/order.e2e.d.ts +2 -0
- package/dist/examples/src/order.e2e.d.ts.map +1 -0
- package/dist/examples/src/order.e2e.js +22 -0
- package/dist/examples/src/order.e2e.js.map +1 -0
- package/dist/examples/src/order.orchestrators.d.ts +9 -0
- package/dist/examples/src/order.orchestrators.d.ts.map +1 -0
- package/dist/examples/src/order.orchestrators.js +10 -0
- package/dist/examples/src/order.orchestrators.js.map +1 -0
- package/dist/examples/src/pet.e2e.d.ts +2 -0
- package/dist/examples/src/pet.e2e.d.ts.map +1 -0
- package/dist/examples/src/pet.e2e.js +17 -0
- package/dist/examples/src/pet.e2e.js.map +1 -0
- package/dist/examples/src/pet.orchestrators.d.ts +8 -0
- package/dist/examples/src/pet.orchestrators.d.ts.map +1 -0
- package/dist/examples/src/pet.orchestrators.js +9 -0
- package/dist/examples/src/pet.orchestrators.js.map +1 -0
- package/dist/schema/actions.d.ts +37 -0
- package/dist/schema/actions.d.ts.map +1 -0
- package/dist/schema/actions.js +35 -0
- package/dist/schema/actions.js.map +1 -0
- package/dist/schema/constraints.d.ts +20 -0
- package/dist/schema/constraints.d.ts.map +1 -0
- package/dist/schema/constraints.js +15 -0
- package/dist/schema/constraints.js.map +1 -0
- package/dist/schema/decisions.d.ts +29 -0
- package/dist/schema/decisions.d.ts.map +1 -0
- package/dist/schema/decisions.js +56 -0
- package/dist/schema/decisions.js.map +1 -0
- package/dist/schema/index.d.ts +17 -0
- package/dist/schema/index.d.ts.map +1 -0
- package/dist/schema/index.js +8 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/model.d.ts +65 -0
- package/dist/schema/model.d.ts.map +1 -0
- package/dist/schema/model.js +30 -0
- package/dist/schema/model.js.map +1 -0
- package/dist/schema/policy.d.ts +25 -0
- package/dist/schema/policy.d.ts.map +1 -0
- package/dist/schema/policy.js +53 -0
- package/dist/schema/policy.js.map +1 -0
- package/dist/schema/props.d.ts +57 -0
- package/dist/schema/props.d.ts.map +1 -0
- package/dist/schema/props.js +53 -0
- package/dist/schema/props.js.map +1 -0
- package/dist/schema/relations.d.ts +9 -0
- package/dist/schema/relations.d.ts.map +1 -0
- package/dist/schema/relations.js +9 -0
- package/dist/schema/relations.js.map +1 -0
- package/dist/schema/transitions.d.ts +34 -0
- package/dist/schema/transitions.d.ts.map +1 -0
- package/dist/schema/transitions.js +25 -0
- package/dist/schema/transitions.js.map +1 -0
- package/dist/schema/types.d.ts +18 -0
- package/dist/schema/types.d.ts.map +1 -0
- package/dist/schema/types.js +11 -0
- package/dist/schema/types.js.map +1 -0
- package/package.json +55 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { buildModelDefaults, capitalize, defaultValueForProp, guardToSource, relationIdField, resolvePolicyGuardBody, } from "./utils.js";
|
|
2
|
+
/**
|
|
3
|
+
* Generate integration tests for all policies targeting a single model.
|
|
4
|
+
* Output: generated/${targetModel}.policy.test.ts
|
|
5
|
+
*/
|
|
6
|
+
export const generatePolicyTests = (targetModelName, policies, modelLookup) => {
|
|
7
|
+
if (policies.length === 0)
|
|
8
|
+
return "";
|
|
9
|
+
const lines = [];
|
|
10
|
+
const targetKey = targetModelName.toLowerCase();
|
|
11
|
+
emitTestImports(lines, targetKey, policies);
|
|
12
|
+
for (const policy of policies) {
|
|
13
|
+
emitPolicyTestBlock(lines, policy, modelLookup);
|
|
14
|
+
}
|
|
15
|
+
return lines.join("\n");
|
|
16
|
+
};
|
|
17
|
+
/** Build a source-code object literal with default values for a model's props. */
|
|
18
|
+
function buildCtxSource(model) {
|
|
19
|
+
if (!model?.props)
|
|
20
|
+
return "{}";
|
|
21
|
+
const entries = buildPropDefaults(model);
|
|
22
|
+
return `{ ${entries.join(", ")} }`;
|
|
23
|
+
}
|
|
24
|
+
function buildOverriddenCtx(model, overrideProp, overrideValue) {
|
|
25
|
+
const entries = [];
|
|
26
|
+
if (model.props) {
|
|
27
|
+
for (const [k, p] of Object.entries(model.props).sort(([a], [b]) => a.localeCompare(b))) {
|
|
28
|
+
entries.push(k === overrideProp
|
|
29
|
+
? `${k}: '${overrideValue}'`
|
|
30
|
+
: `${k}: ${defaultValueForProp(p)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (model.relations) {
|
|
34
|
+
for (const [k, rel] of Object.entries(model.relations).sort(([a], [b]) => a.localeCompare(b))) {
|
|
35
|
+
const field = relationIdField(k, rel.kind);
|
|
36
|
+
entries.push(rel.kind === "belongsTo" ? `${field}: ''` : `${field}: []`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return `{ ${entries.join(", ")} }`;
|
|
40
|
+
}
|
|
41
|
+
function buildPropDefaults(model) {
|
|
42
|
+
const entries = [];
|
|
43
|
+
if (model.props) {
|
|
44
|
+
for (const [key, prop] of Object.entries(model.props).sort(([a], [b]) => a.localeCompare(b))) {
|
|
45
|
+
entries.push(`${key}: ${defaultValueForProp(prop)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (model.relations) {
|
|
49
|
+
for (const [key, rel] of Object.entries(model.relations).sort(([a], [b]) => a.localeCompare(b))) {
|
|
50
|
+
const field = relationIdField(key, rel.kind);
|
|
51
|
+
entries.push(rel.kind === "belongsTo" ? `${field}: ''` : `${field}: []`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return entries;
|
|
55
|
+
}
|
|
56
|
+
function emitPolicyTestBlock(lines, policy, modelLookup) {
|
|
57
|
+
const policyName = capitalize(policy.name);
|
|
58
|
+
const validateFn = `validate${policyName}`;
|
|
59
|
+
const requiresModels = resolveRequiresModels(policy, modelLookup);
|
|
60
|
+
lines.push(`describe('${policy.name}', () => {`);
|
|
61
|
+
for (const [i, rule] of policy.rules.entries()) {
|
|
62
|
+
if (i > 0)
|
|
63
|
+
lines.push("");
|
|
64
|
+
emitRuleTest(lines, i, rule, policy, validateFn, requiresModels, modelLookup);
|
|
65
|
+
}
|
|
66
|
+
lines.push(`});`);
|
|
67
|
+
lines.push("");
|
|
68
|
+
}
|
|
69
|
+
function emitRuleTest(lines, index, rule, policy, validateFn, requiresModels, modelLookup) {
|
|
70
|
+
const rawBody = guardToSource(rule.when);
|
|
71
|
+
const guardBody = resolvePolicyGuardBody(rule.when, rawBody, requiresModels);
|
|
72
|
+
const ruleId = `${policy.name}:rule_${String(index)}`;
|
|
73
|
+
// Guard body in test description for readability; stable ID in assertion
|
|
74
|
+
const escapedBody = guardBody.replace(/'/g, "\\'");
|
|
75
|
+
lines.push(` it('${ruleId}: ${escapedBody} → ${rule.effect}', () => {`);
|
|
76
|
+
// Build context
|
|
77
|
+
lines.push(` const ctx = {`);
|
|
78
|
+
const onModel = modelLookup.get(policy.on.model.name);
|
|
79
|
+
lines.push(` ${policy.on.model.name.toLowerCase()}: ${buildCtxSource(onModel)},`);
|
|
80
|
+
for (const [key, rel] of Object.entries(policy.requires)) {
|
|
81
|
+
const model = requiresModels[key];
|
|
82
|
+
if (rel.kind === "hasMany") {
|
|
83
|
+
lines.push(` ${key}: [${buildCtxSource(model)}],`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
lines.push(` ${key}: ${findTriggerValues(rule.when, key, model)},`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
lines.push(` };`);
|
|
90
|
+
lines.push(` const result = ${validateFn}(ctx);`);
|
|
91
|
+
if (rule.effect === "prevent") {
|
|
92
|
+
lines.push(` expect(result.valid).toBe(false);`);
|
|
93
|
+
lines.push(` expect(result.violations).toContain('${ruleId}');`);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
lines.push(` expect(result.warnings).toContain('${ruleId}');`);
|
|
97
|
+
}
|
|
98
|
+
lines.push(` });`);
|
|
99
|
+
}
|
|
100
|
+
function emitTestImports(lines, targetKey, policies) {
|
|
101
|
+
lines.push(`import { describe, it, expect } from 'vitest';`);
|
|
102
|
+
const validatorImports = policies
|
|
103
|
+
.map((p) => `validate${capitalize(p.name)}`)
|
|
104
|
+
.join(", ");
|
|
105
|
+
lines.push(`import { ${validatorImports} } from './${targetKey}.policies.js';`);
|
|
106
|
+
const contextImports = policies
|
|
107
|
+
.map((p) => `${capitalize(p.name)}Context`)
|
|
108
|
+
.join(", ");
|
|
109
|
+
lines.push(`import type { ${contextImports} } from './${targetKey}.policies.js';`);
|
|
110
|
+
lines.push("");
|
|
111
|
+
}
|
|
112
|
+
function findTriggerEntry(guard, reqKey, model, defaults) {
|
|
113
|
+
if (!model.props)
|
|
114
|
+
return undefined;
|
|
115
|
+
for (const [propName, prop] of Object.entries(model.props)) {
|
|
116
|
+
const probeValues = probeValuesForProp(prop);
|
|
117
|
+
for (const value of probeValues) {
|
|
118
|
+
try {
|
|
119
|
+
const ctx = { [reqKey]: { ...defaults, [propName]: value } };
|
|
120
|
+
if (guard(ctx) === true)
|
|
121
|
+
return { propName, value: String(value) };
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
/* guard may access props not yet available */
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Find prop values that trigger a policy rule's guard for a specific requires key.
|
|
132
|
+
* Probes the guard with different enum/boolean values to find which one returns true.
|
|
133
|
+
*
|
|
134
|
+
* Limitation: probes one requires key at a time. Guards that check multiple keys
|
|
135
|
+
* (e.g. ctx.customer.status === 'x' && ctx.orders.length > 5) may not trigger
|
|
136
|
+
* during single-key probing — falls back to default values in that case.
|
|
137
|
+
*/
|
|
138
|
+
function findTriggerValues(guard, reqKey, model) {
|
|
139
|
+
if (!model?.props)
|
|
140
|
+
return "{}";
|
|
141
|
+
const defaults = buildModelDefaults(model);
|
|
142
|
+
const triggerEntry = findTriggerEntry(guard, reqKey, model, defaults);
|
|
143
|
+
if (!triggerEntry)
|
|
144
|
+
return buildCtxSource(model);
|
|
145
|
+
return buildOverriddenCtx(model, triggerEntry.propName, triggerEntry.value);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Probe values for a prop kind. Lifecycle/oneOf use their declared values,
|
|
149
|
+
* boolean tries true/false, other kinds are not probed.
|
|
150
|
+
*/
|
|
151
|
+
function probeValuesForProp(prop) {
|
|
152
|
+
if (prop.kind === "lifecycle" || prop.kind === "oneOf")
|
|
153
|
+
return prop.values;
|
|
154
|
+
if (prop.kind === "boolean")
|
|
155
|
+
return [true, false];
|
|
156
|
+
return [];
|
|
157
|
+
}
|
|
158
|
+
function resolveRequiresModels(policy, modelLookup) {
|
|
159
|
+
const result = {};
|
|
160
|
+
for (const [key, rel] of Object.entries(policy.requires)) {
|
|
161
|
+
const model = modelLookup.get(rel.target.name);
|
|
162
|
+
if (model)
|
|
163
|
+
result[key] = model;
|
|
164
|
+
}
|
|
165
|
+
return result;
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=policy-tests.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy-tests.js","sourceRoot":"","sources":["../../src/codegen/policy-tests.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ModelDef } from "../schema/model.js";
|
|
2
|
+
import type { PolicyDef } from "../schema/policy.js";
|
|
3
|
+
/**
|
|
4
|
+
* Generate policy validator functions for all policies targeting a single model.
|
|
5
|
+
* Output: generated/${targetModel}.policies.ts
|
|
6
|
+
*/
|
|
7
|
+
export declare const generatePolicyValidator: (policies: PolicyDef[], modelLookup: Map<string, ModelDef>) => string;
|
|
8
|
+
//# sourceMappingURL=policy-validator.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy-validator.d.ts","sourceRoot":"","sources":["../../src/codegen/policy-validator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAIrD;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GAClC,UAAU,SAAS,EAAE,EACrB,aAAa,GAAG,CAAC,MAAM,EAAE,QAAQ,CAAC,KACjC,MAcF,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { capitalize, guardToSource, resolvePolicyGuardBody } from "./utils.js";
|
|
2
|
+
/**
|
|
3
|
+
* Generate policy validator functions for all policies targeting a single model.
|
|
4
|
+
* Output: generated/${targetModel}.policies.ts
|
|
5
|
+
*/
|
|
6
|
+
export const generatePolicyValidator = (policies, modelLookup) => {
|
|
7
|
+
if (policies.length === 0)
|
|
8
|
+
return "";
|
|
9
|
+
const lines = [];
|
|
10
|
+
// Collect and emit imports
|
|
11
|
+
emitImports(lines, policies);
|
|
12
|
+
// Generate per-policy context types and validator functions
|
|
13
|
+
for (const policy of policies) {
|
|
14
|
+
emitPolicyValidator(lines, policy, modelLookup);
|
|
15
|
+
}
|
|
16
|
+
return lines.join("\n");
|
|
17
|
+
};
|
|
18
|
+
function emitImports(lines, policies) {
|
|
19
|
+
const imports = new Map();
|
|
20
|
+
for (const policy of policies) {
|
|
21
|
+
const onModelName = policy.on.model.name;
|
|
22
|
+
imports.set(onModelName.toLowerCase(), `${capitalize(onModelName)}Props`);
|
|
23
|
+
for (const rel of Object.values(policy.requires)) {
|
|
24
|
+
const reqModelName = rel.target.name;
|
|
25
|
+
imports.set(reqModelName.toLowerCase(), `${capitalize(reqModelName)}Props`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const [modelKey, propsType] of [...imports].sort(([a], [b]) => a.localeCompare(b))) {
|
|
29
|
+
lines.push(`import type { ${propsType} } from './${modelKey}.types.js';`);
|
|
30
|
+
}
|
|
31
|
+
lines.push("");
|
|
32
|
+
}
|
|
33
|
+
function emitPolicyValidator(lines, policy, modelLookup) {
|
|
34
|
+
const policyName = capitalize(policy.name);
|
|
35
|
+
const onModelTypeName = capitalize(policy.on.model.name);
|
|
36
|
+
const contextTypeName = `${policyName}Context`;
|
|
37
|
+
// Context type
|
|
38
|
+
lines.push(`export type ${contextTypeName} = {`);
|
|
39
|
+
lines.push(` ${policy.on.model.name.toLowerCase()}: ${onModelTypeName}Props;`);
|
|
40
|
+
for (const [key, rel] of Object.entries(policy.requires)) {
|
|
41
|
+
const reqTypeName = `${capitalize(rel.target.name)}Props`;
|
|
42
|
+
const suffix = rel.kind === "hasMany" ? "[]" : "";
|
|
43
|
+
lines.push(` ${key}: ${reqTypeName}${suffix};`);
|
|
44
|
+
}
|
|
45
|
+
lines.push(`};`);
|
|
46
|
+
lines.push("");
|
|
47
|
+
// Resolve required models for guard body resolution
|
|
48
|
+
const requiresModels = {};
|
|
49
|
+
for (const [key, rel] of Object.entries(policy.requires)) {
|
|
50
|
+
const model = modelLookup.get(rel.target.name);
|
|
51
|
+
if (model)
|
|
52
|
+
requiresModels[key] = model;
|
|
53
|
+
}
|
|
54
|
+
// Validator function
|
|
55
|
+
lines.push(`export function validate${policyName}(ctx: ${contextTypeName}): { valid: boolean; violations: string[]; warnings: string[] } {`);
|
|
56
|
+
lines.push(` const violations: string[] = [];`);
|
|
57
|
+
lines.push(` const warnings: string[] = [];`);
|
|
58
|
+
for (const [i, rule] of policy.rules.entries()) {
|
|
59
|
+
const rawBody = guardToSource(rule.when);
|
|
60
|
+
const guardBody = resolvePolicyGuardBody(rule.when, rawBody, requiresModels);
|
|
61
|
+
const target = rule.effect === "prevent" ? "violations" : "warnings";
|
|
62
|
+
const ruleId = `${policy.name}:rule_${String(i)}`;
|
|
63
|
+
lines.push(` if (${guardBody}) ${target}.push('${ruleId}');`);
|
|
64
|
+
}
|
|
65
|
+
lines.push(` return { valid: violations.length === 0, violations, warnings };`);
|
|
66
|
+
lines.push(`}`);
|
|
67
|
+
lines.push("");
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=policy-validator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"policy-validator.js","sourceRoot":"","sources":["../../src/codegen/policy-validator.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tests.d.ts","sourceRoot":"","sources":["../../src/codegen/tests.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AA0HnD,8EAA8E;AAC9E,eAAO,MAAM,aAAa,GAAI,OAAO,QAAQ,KAAG,MAsE/C,CAAC"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { isOptional } from "../schema/props.js";
|
|
2
|
+
import { capitalize, defaultValueForProp, getLifecycleProp, getRelations, getTransitions, relationIdField, valueToSource, } from "./utils.js";
|
|
3
|
+
/**
|
|
4
|
+
* Build a complete ctx object literal by merging prop defaults, relation
|
|
5
|
+
* defaults, and scenario-given values at codegen time. This avoids duplicate
|
|
6
|
+
* keys (which strict tsc warns about) and uses `satisfies` to prevent
|
|
7
|
+
* string-literal widening while still catching missing props.
|
|
8
|
+
*/
|
|
9
|
+
const buildCtxLiteral = (scenario, fromState, tg) => {
|
|
10
|
+
const parts = [];
|
|
11
|
+
// Start with prop defaults, overriding with scenario-given values
|
|
12
|
+
for (const [key, prop] of tg.propEntries) {
|
|
13
|
+
if (key === tg.lifecycleKey)
|
|
14
|
+
continue; // lifecycle handled separately
|
|
15
|
+
const scenarioValue = scenario.given[key];
|
|
16
|
+
if (scenarioValue !== undefined) {
|
|
17
|
+
parts.push(`${key}: ${valueToSource(scenarioValue)}`);
|
|
18
|
+
}
|
|
19
|
+
else if (!isOptional(prop)) {
|
|
20
|
+
parts.push(`${key}: ${defaultValueForProp(prop)}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Lifecycle state
|
|
24
|
+
if (tg.lifecycleKey) {
|
|
25
|
+
parts.push(`${tg.lifecycleKey}: '${fromState}'`);
|
|
26
|
+
}
|
|
27
|
+
// Relation defaults
|
|
28
|
+
for (const [fieldName, defaultValue] of tg.relationEntries) {
|
|
29
|
+
parts.push(`${fieldName}: ${defaultValue}`);
|
|
30
|
+
}
|
|
31
|
+
return `{ ${parts.join(", ")} } satisfies ${tg.propsType}`;
|
|
32
|
+
};
|
|
33
|
+
const formatSuccessCase = (name, scenario, transition, tg) => {
|
|
34
|
+
const fnName = `${tg.typeName}${capitalize(name)}`;
|
|
35
|
+
const ctxLiteral = buildCtxLiteral(scenario, transition.from, tg);
|
|
36
|
+
const givenStr = JSON.stringify(scenario.given);
|
|
37
|
+
const lines = [
|
|
38
|
+
` it('given ${givenStr}, when ${name}, then ${tg.lifecycleKey ?? "transition"} = ${scenario.expected}', () => {`,
|
|
39
|
+
` const ctx = ${ctxLiteral};`,
|
|
40
|
+
` const result = ${fnName}(ctx);`,
|
|
41
|
+
];
|
|
42
|
+
if (tg.lifecycleKey) {
|
|
43
|
+
lines.push(` expect(result.${tg.lifecycleKey}).toBe('${scenario.expected}');`);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
lines.push(` expect(result).toBeDefined();`);
|
|
47
|
+
}
|
|
48
|
+
lines.push(` });`);
|
|
49
|
+
return lines;
|
|
50
|
+
};
|
|
51
|
+
const formatFailureCase = (name, scenario, transition, tg) => {
|
|
52
|
+
const fnName = `${tg.typeName}${capitalize(name)}`;
|
|
53
|
+
const ctxLiteral = buildCtxLiteral(scenario, transition.from, tg);
|
|
54
|
+
const givenStr = JSON.stringify(scenario.given);
|
|
55
|
+
return [
|
|
56
|
+
` it('given ${givenStr}, when ${name}, then stays ${scenario.expected}', () => {`,
|
|
57
|
+
` const ctx = ${ctxLiteral};`,
|
|
58
|
+
` expect(() => ${fnName}(ctx)).toThrow();`,
|
|
59
|
+
` });`,
|
|
60
|
+
];
|
|
61
|
+
};
|
|
62
|
+
const formatScenario = (name, scenario, transition, tg) => {
|
|
63
|
+
const succeeds = scenario.expected === transition.to;
|
|
64
|
+
return succeeds
|
|
65
|
+
? formatSuccessCase(name, scenario, transition, tg)
|
|
66
|
+
: formatFailureCase(name, scenario, transition, tg);
|
|
67
|
+
};
|
|
68
|
+
/** Generate unit tests from transition scenarios in Given/When/Then style. */
|
|
69
|
+
export const generateTests = (model) => {
|
|
70
|
+
const transitions = getTransitions(model);
|
|
71
|
+
if (transitions.length === 0)
|
|
72
|
+
return "";
|
|
73
|
+
const typeName = capitalize(model.name);
|
|
74
|
+
const modelName = model.name.toLowerCase();
|
|
75
|
+
const propsType = `${typeName}Props`;
|
|
76
|
+
const lifecycleKey = getLifecycleProp(model)?.key;
|
|
77
|
+
// Collect prop entries for building complete ctx objects
|
|
78
|
+
const propEntries = model.props
|
|
79
|
+
? Object.entries(model.props)
|
|
80
|
+
: [];
|
|
81
|
+
// Build relation field entries
|
|
82
|
+
const relations = getRelations(model);
|
|
83
|
+
const relationEntries = [];
|
|
84
|
+
for (const [key, rel] of relations) {
|
|
85
|
+
const fieldName = relationIdField(key, rel.kind);
|
|
86
|
+
const defaultValue = rel.kind === "belongsTo" ? "''" : "[]";
|
|
87
|
+
relationEntries.push([fieldName, defaultValue]);
|
|
88
|
+
}
|
|
89
|
+
// Collect transition function names that have scenarios
|
|
90
|
+
const transitionFnNames = [];
|
|
91
|
+
for (const [name, transition] of transitions) {
|
|
92
|
+
if (transition.scenarios && transition.scenarios.length > 0) {
|
|
93
|
+
transitionFnNames.push(`${typeName}${capitalize(name)}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (transitionFnNames.length === 0)
|
|
97
|
+
return "";
|
|
98
|
+
const tg = {
|
|
99
|
+
lifecycleKey,
|
|
100
|
+
propEntries,
|
|
101
|
+
propsType,
|
|
102
|
+
relationEntries,
|
|
103
|
+
typeName,
|
|
104
|
+
};
|
|
105
|
+
const lines = [];
|
|
106
|
+
lines.push(`import { describe, it, expect } from 'vitest';`);
|
|
107
|
+
// Import types for satisfies assertions
|
|
108
|
+
lines.push(`import type { ${propsType} } from './${modelName}.types.js';`);
|
|
109
|
+
// Convention: generated transition module lives at ./${modelName}.transitions.js
|
|
110
|
+
lines.push(`import { ${transitionFnNames.join(", ")} } from './${modelName}.transitions.js';`);
|
|
111
|
+
lines.push("");
|
|
112
|
+
lines.push(`describe('${typeName}', () => {`);
|
|
113
|
+
for (const [name, transition] of transitions) {
|
|
114
|
+
if (!transition.scenarios || transition.scenarios.length === 0)
|
|
115
|
+
continue;
|
|
116
|
+
for (const scenario of transition.scenarios) {
|
|
117
|
+
lines.push(...formatScenario(name, scenario, transition, tg));
|
|
118
|
+
lines.push("");
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
lines.push(`});`);
|
|
122
|
+
lines.push("");
|
|
123
|
+
return lines.join("\n");
|
|
124
|
+
};
|
|
125
|
+
//# sourceMappingURL=tests.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tests.js","sourceRoot":"","sources":["../../src/codegen/tests.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAChD,OAAO,EACL,UAAU,EACV,mBAAmB,EACnB,gBAAgB,EAChB,YAAY,EACZ,cAAc,EACd,eAAe,EACf,aAAa,GACd,MAAM,YAAY,CAAC;AAWpB;;;;;GAKG;AACH,MAAM,eAAe,GAAG,CACtB,QAAkB,EAClB,SAAiB,EACjB,EAAc,EACN,EAAE;IACV,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,kEAAkE;IAClE,KAAK,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACzC,IAAI,GAAG,KAAK,EAAE,CAAC,YAAY;YAAE,SAAS,CAAC,+BAA+B;QAEtE,MAAM,aAAa,GAAI,QAAQ,CAAC,KAAiC,CAAC,GAAG,CAAC,CAAC;QAEvE,IAAI,aAAa,KAAK,SAAS,EAAE,CAAC;YAChC,KAAK,CAAC,IAAI,CAAC,GAAG,GAAG,KAAK,aAAa,CAAC,aAAa,CAAC,EAAE,CAAC,CAAC;QACxD,CAAC;aAAM,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7B,KAAK,CAAC,IAAI,CAAC,GAAG,GAAG,KAAK,mBAAmB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACrD,CAAC;IACH,CAAC;IAED,kBAAkB;IAClB,IAAI,EAAE,CAAC,YAAY,EAAE,CAAC;QACpB,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,YAAY,MAAM,SAAS,GAAG,CAAC,CAAC;IACnD,CAAC;IAED,oBAAoB;IACpB,KAAK,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,IAAI,EAAE,CAAC,eAAe,EAAE,CAAC;QAC3D,KAAK,CAAC,IAAI,CAAC,GAAG,SAAS,KAAK,YAAY,EAAE,CAAC,CAAC;IAC9C,CAAC;IAED,OAAO,KAAK,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,SAAS,EAAE,CAAC;AAC7D,CAAC,CAAC;AAEF,MAAM,iBAAiB,GAAG,CACxB,IAAY,EACZ,QAAkB,EAClB,UAA0B,EAC1B,EAAc,EACJ,EAAE;IACZ,MAAM,MAAM,GAAG,GAAG,EAAE,CAAC,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;IACnD,MAAM,UAAU,GAAG,eAAe,CAAC,QAAQ,EAAE,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAClE,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAChD,MAAM,KAAK,GAAG;QACZ,eAAe,QAAQ,UAAU,IAAI,UAAU,EAAE,CAAC,YAAY,IAAI,YAAY,MAAM,QAAQ,CAAC,QAAQ,YAAY;QACjH,mBAAmB,UAAU,GAAG;QAChC,sBAAsB,MAAM,QAAQ;KACrC,CAAC;IAEF,IAAI,EAAE,CAAC,YAAY,EAAE,CAAC;QACpB,KAAK,CAAC,IAAI,CACR,qBAAqB,EAAE,CAAC,YAAY,WAAW,QAAQ,CAAC,QAAQ,KAAK,CACtE,CAAC;IACJ,CAAC;SAAM,CAAC;QACN,KAAK,CAAC,IAAI,CAAC,mCAAmC,CAAC,CAAC;IAClD,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAEpB,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF,MAAM,iBAAiB,GAAG,CACxB,IAAY,EACZ,QAAkB,EAClB,UAA0B,EAC1B,EAAc,EACJ,EAAE;IACZ,MAAM,MAAM,GAAG,GAAG,EAAE,CAAC,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;IACnD,MAAM,UAAU,GAAG,eAAe,CAAC,QAAQ,EAAE,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;IAClE,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAEhD,OAAO;QACL,eAAe,QAAQ,UAAU,IAAI,gBAAgB,QAAQ,CAAC,QAAQ,YAAY;QAClF,mBAAmB,UAAU,GAAG;QAChC,oBAAoB,MAAM,mBAAmB;QAC7C,OAAO;KACR,CAAC;AACJ,CAAC,CAAC;AAEF,MAAM,cAAc,GAAG,CACrB,IAAY,EACZ,QAAkB,EAClB,UAA0B,EAC1B,EAAc,EACJ,EAAE;IACZ,MAAM,QAAQ,GAAG,QAAQ,CAAC,QAAQ,KAAK,UAAU,CAAC,EAAE,CAAC;IAErD,OAAO,QAAQ;QACb,CAAC,CAAC,iBAAiB,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,CAAC;QACnD,CAAC,CAAC,iBAAiB,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC;AACxD,CAAC,CAAC;AAEF,8EAA8E;AAC9E,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAe,EAAU,EAAE;IACvD,MAAM,WAAW,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IAE1C,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAExC,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;IAC3C,MAAM,SAAS,GAAG,GAAG,QAAQ,OAAO,CAAC;IACrC,MAAM,YAAY,GAAG,gBAAgB,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC;IAElD,yDAAyD;IACzD,MAAM,WAAW,GAAwB,KAAK,CAAC,KAAK;QAClD,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC;QAC7B,CAAC,CAAC,EAAE,CAAC;IAEP,+BAA+B;IAC/B,MAAM,SAAS,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC;IACtC,MAAM,eAAe,GAAuB,EAAE,CAAC;IAE/C,KAAK,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,SAAS,EAAE,CAAC;QACnC,MAAM,SAAS,GAAG,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC;QACjD,MAAM,YAAY,GAAG,GAAG,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC;QAE5D,eAAe,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,YAAY,CAAC,CAAC,CAAC;IAClD,CAAC;IAED,wDAAwD;IACxD,MAAM,iBAAiB,GAAa,EAAE,CAAC;IAEvC,KAAK,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,WAAW,EAAE,CAAC;QAC7C,IAAI,UAAU,CAAC,SAAS,IAAI,UAAU,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5D,iBAAiB,CAAC,IAAI,CAAC,GAAG,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IAED,IAAI,iBAAiB,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAE9C,MAAM,EAAE,GAAe;QACrB,YAAY;QACZ,WAAW;QACX,SAAS;QACT,eAAe;QACf,QAAQ;KACT,CAAC;IAEF,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,KAAK,CAAC,IAAI,CAAC,gDAAgD,CAAC,CAAC;IAC7D,wCAAwC;IACxC,KAAK,CAAC,IAAI,CAAC,iBAAiB,SAAS,cAAc,SAAS,aAAa,CAAC,CAAC;IAC3E,iFAAiF;IACjF,KAAK,CAAC,IAAI,CACR,YAAY,iBAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,cAAc,SAAS,mBAAmB,CACnF,CAAC;IACF,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,aAAa,QAAQ,YAAY,CAAC,CAAC;IAE9C,KAAK,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,WAAW,EAAE,CAAC;QAC7C,IAAI,CAAC,UAAU,CAAC,SAAS,IAAI,UAAU,CAAC,SAAS,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QAEzE,KAAK,MAAM,QAAQ,IAAI,UAAU,CAAC,SAAS,EAAE,CAAC;YAC5C,KAAK,CAAC,IAAI,CAAC,GAAG,cAAc,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC;YAC9D,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACjB,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAClB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transitions.d.ts","sourceRoot":"","sources":["../../src/codegen/transitions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAUnD,0EAA0E;AAC1E,eAAO,MAAM,mBAAmB,GAAI,OAAO,QAAQ,KAAG,MAiDrD,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { capitalize, getLifecycleProp, getTransitions, guardToSource, resolveGuardBody, } from "./utils.js";
|
|
2
|
+
/** Generate transition functions with runtime state checks and guards. */
|
|
3
|
+
export const generateTransitions = (model) => {
|
|
4
|
+
const transitions = getTransitions(model);
|
|
5
|
+
if (transitions.length === 0)
|
|
6
|
+
return "";
|
|
7
|
+
const typeName = capitalize(model.name);
|
|
8
|
+
const modelName = model.name.toLowerCase();
|
|
9
|
+
const propsType = `${typeName}Props`;
|
|
10
|
+
const lines = [];
|
|
11
|
+
// Convention: generated transition module imports types from ./${modelName}.types
|
|
12
|
+
lines.push(`import type { ${propsType} } from './${modelName}.types.js';`);
|
|
13
|
+
lines.push("");
|
|
14
|
+
const lifecycleKey = getLifecycleProp(model)?.key;
|
|
15
|
+
for (const [name, transition] of transitions) {
|
|
16
|
+
const fnName = `${typeName}${capitalize(name)}`;
|
|
17
|
+
lines.push(`export function ${fnName}(ctx: ${propsType}): ${propsType} {`);
|
|
18
|
+
if (lifecycleKey) {
|
|
19
|
+
lines.push(` if (ctx.${lifecycleKey} !== '${transition.from}') {`);
|
|
20
|
+
lines.push(` throw new Error(\`Cannot ${name}: expected ${lifecycleKey} '${transition.from}', got '\${ctx.${lifecycleKey}}'\`);`);
|
|
21
|
+
lines.push(` }`);
|
|
22
|
+
}
|
|
23
|
+
if (transition.guard) {
|
|
24
|
+
const rawBody = guardToSource(transition.guard);
|
|
25
|
+
const guardBody = resolveGuardBody(transition.guard, rawBody, model);
|
|
26
|
+
lines.push(` if (!(${guardBody})) {`);
|
|
27
|
+
lines.push(` throw new Error('Guard failed for transition ${name}');`);
|
|
28
|
+
lines.push(` }`);
|
|
29
|
+
}
|
|
30
|
+
if (lifecycleKey) {
|
|
31
|
+
lines.push(` return { ...ctx, ${lifecycleKey}: '${transition.to}' };`);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
lines.push(` return ctx;`);
|
|
35
|
+
}
|
|
36
|
+
lines.push(`}`);
|
|
37
|
+
lines.push("");
|
|
38
|
+
}
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=transitions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transitions.js","sourceRoot":"","sources":["../../src/codegen/transitions.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,gBAAgB,GACjB,MAAM,YAAY,CAAC;AAEpB,0EAA0E;AAC1E,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,KAAe,EAAU,EAAE;IAC7D,MAAM,WAAW,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IAE1C,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAExC,MAAM,QAAQ,GAAG,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IACxC,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;IAC3C,MAAM,SAAS,GAAG,GAAG,QAAQ,OAAO,CAAC;IACrC,MAAM,KAAK,GAAa,EAAE,CAAC;IAE3B,kFAAkF;IAClF,KAAK,CAAC,IAAI,CAAC,iBAAiB,SAAS,cAAc,SAAS,aAAa,CAAC,CAAC;IAC3E,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IAEf,MAAM,YAAY,GAAG,gBAAgB,CAAC,KAAK,CAAC,EAAE,GAAG,CAAC;IAElD,KAAK,MAAM,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,WAAW,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,GAAG,QAAQ,GAAG,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAEhD,KAAK,CAAC,IAAI,CAAC,mBAAmB,MAAM,SAAS,SAAS,MAAM,SAAS,IAAI,CAAC,CAAC;QAE3E,IAAI,YAAY,EAAE,CAAC;YACjB,KAAK,CAAC,IAAI,CAAC,aAAa,YAAY,SAAS,UAAU,CAAC,IAAI,MAAM,CAAC,CAAC;YACpE,KAAK,CAAC,IAAI,CACR,gCAAgC,IAAI,cAAc,YAAY,KAAK,UAAU,CAAC,IAAI,kBAAkB,YAAY,QAAQ,CACzH,CAAC;YACF,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACpB,CAAC;QAED,IAAI,UAAU,CAAC,KAAK,EAAE,CAAC;YACrB,MAAM,OAAO,GAAG,aAAa,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;YAChD,MAAM,SAAS,GAAG,gBAAgB,CAAC,UAAU,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;YAErE,KAAK,CAAC,IAAI,CAAC,WAAW,SAAS,MAAM,CAAC,CAAC;YACvC,KAAK,CAAC,IAAI,CAAC,oDAAoD,IAAI,KAAK,CAAC,CAAC;YAC1E,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACpB,CAAC;QAED,IAAI,YAAY,EAAE,CAAC;YACjB,KAAK,CAAC,IAAI,CAAC,sBAAsB,YAAY,MAAM,UAAU,CAAC,EAAE,MAAM,CAAC,CAAC;QAC1E,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC;QAC9B,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAChB,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACjB,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/codegen/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAUnD,+EAA+E;AAC/E,eAAO,MAAM,aAAa,GAAI,OAAO,QAAQ,KAAG,MAqD/C,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { isOptional } from "../schema/props.js";
|
|
2
|
+
import { capitalize, getRelations, propKindToTS, relationIdField, } from "./utils.js";
|
|
3
|
+
/** Generate TypeScript type definitions from a model's props and relations. */
|
|
4
|
+
export const generateTypes = (model) => {
|
|
5
|
+
const hasProps = model.props && Object.keys(model.props).length > 0;
|
|
6
|
+
const relations = getRelations(model);
|
|
7
|
+
if (!hasProps && relations.length === 0)
|
|
8
|
+
return "";
|
|
9
|
+
const lines = [];
|
|
10
|
+
const typeName = capitalize(model.name);
|
|
11
|
+
const propEntries = model.props ? Object.entries(model.props) : [];
|
|
12
|
+
// Generate union types for lifecycle and oneOf props
|
|
13
|
+
for (const [key, prop] of propEntries) {
|
|
14
|
+
if (prop.kind === "lifecycle" || prop.kind === "oneOf") {
|
|
15
|
+
const unionName = `${typeName}${capitalize(key)}`;
|
|
16
|
+
const values = prop.values
|
|
17
|
+
.map((v) => `'${v}'`)
|
|
18
|
+
.join(" | ");
|
|
19
|
+
lines.push(`export type ${unionName} = ${values};`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (lines.length > 0)
|
|
23
|
+
lines.push("");
|
|
24
|
+
// Generate props interface
|
|
25
|
+
const propsFields = propEntries.map(([key, prop]) => {
|
|
26
|
+
let tsType;
|
|
27
|
+
if (prop.kind === "lifecycle" || prop.kind === "oneOf") {
|
|
28
|
+
tsType = `${typeName}${capitalize(key)}`;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
tsType = propKindToTS(prop);
|
|
32
|
+
}
|
|
33
|
+
const opt = isOptional(prop) ? "?" : "";
|
|
34
|
+
return ` ${key}${opt}: ${tsType};`;
|
|
35
|
+
});
|
|
36
|
+
// Add relation fields: belongsTo → string (foreign key id), hasMany → string[] (ids)
|
|
37
|
+
for (const [key, rel] of relations) {
|
|
38
|
+
const targetName = rel.target.name;
|
|
39
|
+
const fieldName = relationIdField(key, rel.kind);
|
|
40
|
+
const tsType = rel.kind === "belongsTo" ? "string" : "string[]";
|
|
41
|
+
propsFields.push(` ${fieldName}: ${tsType}; // ${rel.kind} ${targetName}`);
|
|
42
|
+
}
|
|
43
|
+
lines.push(`export type ${typeName}Props = {`);
|
|
44
|
+
lines.push(...propsFields);
|
|
45
|
+
lines.push(`};`);
|
|
46
|
+
return lines.join("\n") + "\n";
|
|
47
|
+
};
|
|
48
|
+
//# sourceMappingURL=types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/codegen/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { ActionDef } from "../schema/actions.js";
|
|
2
|
+
import type { ConstraintData } from "../schema/constraints.js";
|
|
3
|
+
import type { ModelDef } from "../schema/model.js";
|
|
4
|
+
import type { PolicyDef } from "../schema/policy.js";
|
|
5
|
+
import type { PropDef } from "../schema/props.js";
|
|
6
|
+
import type { RelationDef } from "../schema/relations.js";
|
|
7
|
+
import type { TransitionData } from "../schema/transitions.js";
|
|
8
|
+
/** Thrown by guardToSource when the guard function cannot be parsed into embeddable source. */
|
|
9
|
+
export type GuardParseError = Error & {
|
|
10
|
+
readonly guardParse: true;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* ModelDef's default generics use structural bases (TransitionDefBase, ConstraintDefBase)
|
|
14
|
+
* that omit guard/scenarios/preventedAction fields. At runtime, model() always stores the
|
|
15
|
+
* full TransitionData/ConstraintData/ActionDef objects. These accessors centralize the
|
|
16
|
+
* single unavoidable cast so all generators can work with properly typed data.
|
|
17
|
+
*/
|
|
18
|
+
export declare const getTransitions: (model: ModelDef) => [string, TransitionData][];
|
|
19
|
+
export declare const getConstraints: (model: ModelDef) => [string, ConstraintData][];
|
|
20
|
+
export declare const getActions: (model: ModelDef) => [string, ActionDef][];
|
|
21
|
+
export declare const getRelations: (model: ModelDef) => [string, RelationDef][];
|
|
22
|
+
export declare const capitalize: (s: string) => string;
|
|
23
|
+
/** Converts PascalCase to kebab-case: "ContrastThresholds" → "contrast-thresholds" */
|
|
24
|
+
export declare const toKebabCase: (name: string) => string;
|
|
25
|
+
/**
|
|
26
|
+
* Derive the id field name for a relation key. Appends Id/Ids to key as-is.
|
|
27
|
+
* belongsTo "customer" → "customerId"
|
|
28
|
+
* hasMany "item" → "itemIds"
|
|
29
|
+
*
|
|
30
|
+
* Use singular relation keys for clean output (e.g. `item: hasMany(Pet)` not `items`).
|
|
31
|
+
*/
|
|
32
|
+
export declare const relationIdField: (key: string, kind: "belongsTo" | "hasMany") => string;
|
|
33
|
+
/** Map a PropDef kind to its TypeScript type string. */
|
|
34
|
+
export declare const propKindToTS: (prop: PropDef) => string;
|
|
35
|
+
/** Map a PropDef kind to its Zod validator string. */
|
|
36
|
+
export declare const propKindToZod: (prop: PropDef) => string;
|
|
37
|
+
/** Find the single lifecycle prop in a model. Returns null if none. */
|
|
38
|
+
export declare const getLifecycleProp: (model: ModelDef) => null | {
|
|
39
|
+
key: string;
|
|
40
|
+
values: readonly string[];
|
|
41
|
+
};
|
|
42
|
+
export declare const guardToSource: (guard: (ctx: Record<string, unknown>) => unknown) => string;
|
|
43
|
+
/**
|
|
44
|
+
* Resolve external closure references in a guard body by probing the guard
|
|
45
|
+
* function with known enum/lifecycle values from the model.
|
|
46
|
+
*
|
|
47
|
+
* Guards like `(ctx) => ctx.status === orderStates.cancelled` serialize to a
|
|
48
|
+
* body containing `orderStates.cancelled` — an identifier that doesn't exist
|
|
49
|
+
* in generated code. This function detects such references and replaces them
|
|
50
|
+
* with their resolved string literal values.
|
|
51
|
+
*
|
|
52
|
+
* Works by finding `ctx.prop === ref` or `ref === ctx.prop` patterns, then
|
|
53
|
+
* calling the guard with each possible enum value until one matches.
|
|
54
|
+
*
|
|
55
|
+
* Limitations:
|
|
56
|
+
* - Only matches single-dot refs like `states.cancelled`, not deeper paths.
|
|
57
|
+
* This is intentional — lifecycle.states() produces `{ name: name }` objects.
|
|
58
|
+
* - Only matches `===` / `==` operators. `!==` / `!=` are not matched by the
|
|
59
|
+
* regex and will pass through unchanged (the external ref stays in the body).
|
|
60
|
+
*/
|
|
61
|
+
export declare const resolveGuardBody: (guard: (ctx: Record<string, unknown>) => unknown, body: string, model: ModelDef) => string;
|
|
62
|
+
/** Serialize a JS value to embeddable source code (string literal, number, boolean). */
|
|
63
|
+
export declare const valueToSource: (value: unknown) => string;
|
|
64
|
+
/** Return a sensible default value (as source code) for a PropDef kind. */
|
|
65
|
+
export declare const defaultValueForProp: (prop: PropDef) => string;
|
|
66
|
+
/** Convert action fields (Record<string, PropDef>) to a TypeScript type literal. */
|
|
67
|
+
export declare const fieldsToTSType: (fields: Record<string, PropDef> | undefined) => string;
|
|
68
|
+
/**
|
|
69
|
+
* Build default values for all props in a model (as runtime JS values, not source).
|
|
70
|
+
* Used by resolvePolicyGuardBody to probe guard functions.
|
|
71
|
+
*/
|
|
72
|
+
export declare const buildModelDefaults: (model: ModelDef) => Record<string, unknown>;
|
|
73
|
+
/**
|
|
74
|
+
* Resolve external closure references in a policy guard body.
|
|
75
|
+
*
|
|
76
|
+
* Policy guards access nested ctx: `ctx.customer.status === customerStates.suspended`.
|
|
77
|
+
* This function resolves `customerStates.suspended` to `'suspended'` by probing the
|
|
78
|
+
* guard function with known enum values from the required models.
|
|
79
|
+
*
|
|
80
|
+
* @param guard - The guard function (closure with captured variables)
|
|
81
|
+
* @param body - Raw guard body from guardToSource
|
|
82
|
+
* @param requiresModels - Map of requires key → resolved ModelDef
|
|
83
|
+
*/
|
|
84
|
+
export declare const resolvePolicyGuardBody: (guard: PolicyDef["rules"][number]["when"], body: string, requiresModels: Record<string, ModelDef>) => string;
|
|
85
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/codegen/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAC1D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAW/D,+FAA+F;AAC/F,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG;IAAE,QAAQ,CAAC,UAAU,EAAE,IAAI,CAAA;CAAE,CAAC;AAWpE;;;;;GAKG;AACH,eAAO,MAAM,cAAc,GAAI,OAAO,QAAQ,KAAG,CAAC,MAAM,EAAE,cAAc,CAAC,EAIxE,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,OAAO,QAAQ,KAAG,CAAC,MAAM,EAAE,cAAc,CAAC,EAIxE,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,OAAO,QAAQ,KAAG,CAAC,MAAM,EAAE,SAAS,CAAC,EAI/D,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,OAAO,QAAQ,KAAG,CAAC,MAAM,EAAE,WAAW,CAAC,EAInE,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,GAAG,MAAM,KAAG,MACC,CAAC;AAEzC,sFAAsF;AACtF,eAAO,MAAM,WAAW,GAAI,MAAM,MAAM,KAAG,MAIzB,CAAC;AAEnB;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAC1B,KAAK,MAAM,EACX,MAAM,WAAW,GAAG,SAAS,KAC5B,MAIF,CAAC;AAaF,wDAAwD;AACxD,eAAO,MAAM,YAAY,GAAI,MAAM,OAAO,KAAG,MAuB5C,CAAC;AAQF,sDAAsD;AACtD,eAAO,MAAM,aAAa,GAAI,MAAM,OAAO,KAAG,MAuB7C,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,gBAAgB,GAC3B,OAAO,QAAQ,KACd,IAAI,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAA;CAUjD,CAAC;AAkBF,eAAO,MAAM,aAAa,GACxB,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,KAC/C,MA0DF,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,gBAAgB,GAC3B,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,EAChD,MAAM,MAAM,EACZ,OAAO,QAAQ,KACd,MAkDF,CAAC;AAEF,wFAAwF;AACxF,eAAO,MAAM,aAAa,GAAI,OAAO,OAAO,KAAG,MAQ9C,CAAC;AAEF,2EAA2E;AAC3E,eAAO,MAAM,mBAAmB,GAAI,MAAM,OAAO,KAAG,MAyBnD,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,cAAc,GACzB,QAAQ,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,KAC1C,MAUF,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,kBAAkB,GAC7B,OAAO,QAAQ,KACd,MAAM,CAAC,MAAM,EAAE,OAAO,CAiCxB,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,sBAAsB,GACjC,OAAO,SAAS,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,EACzC,MAAM,MAAM,EACZ,gBAAgB,MAAM,CAAC,MAAM,EAAE,QAAQ,CAAC,KACvC,MA4DF,CAAC"}
|