graphddb 0.1.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/README.md +553 -0
- package/dist/chunk-347U24SB.js +1818 -0
- package/dist/chunk-6LEHSX45.js +4276 -0
- package/dist/chunk-UNRQ5YJT.js +461 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +890 -0
- package/dist/index.d.ts +5309 -0
- package/dist/index.js +2887 -0
- package/dist/testing/index.d.ts +182 -0
- package/dist/testing/index.js +898 -0
- package/dist/types-CDrWiPxp.d.ts +1203 -0
- package/package.json +76 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
buildBridgeBundle,
|
|
4
|
+
normalizeSelectSpec
|
|
5
|
+
} from "./chunk-6LEHSX45.js";
|
|
6
|
+
import {
|
|
7
|
+
MetadataRegistry
|
|
8
|
+
} from "./chunk-347U24SB.js";
|
|
9
|
+
|
|
10
|
+
// src/generated/program.ts
|
|
11
|
+
import { Command } from "commander";
|
|
12
|
+
|
|
13
|
+
// src/generated/policy-runtime.ts
|
|
14
|
+
var RISK_ORDER = {
|
|
15
|
+
low: 0,
|
|
16
|
+
medium: 1,
|
|
17
|
+
high: 2,
|
|
18
|
+
critical: 3
|
|
19
|
+
};
|
|
20
|
+
function maxRiskLevel(...levels) {
|
|
21
|
+
let max = "low";
|
|
22
|
+
for (const level of levels) {
|
|
23
|
+
if (RISK_ORDER[level] > RISK_ORDER[max]) {
|
|
24
|
+
max = level;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return max;
|
|
28
|
+
}
|
|
29
|
+
function isOptionActive(definition, value, specified) {
|
|
30
|
+
const schemaType = definition.schema?.type;
|
|
31
|
+
if (schemaType === "boolean") {
|
|
32
|
+
return value === true;
|
|
33
|
+
}
|
|
34
|
+
if (definition.repeatable) {
|
|
35
|
+
return specified && Array.isArray(value) && value.length > 0;
|
|
36
|
+
}
|
|
37
|
+
return specified && value != null;
|
|
38
|
+
}
|
|
39
|
+
function derivePolicy(input) {
|
|
40
|
+
const sideEffects = /* @__PURE__ */ new Set();
|
|
41
|
+
const reads = [];
|
|
42
|
+
const writes = [];
|
|
43
|
+
const networkEffects = [];
|
|
44
|
+
const riskLevels = [];
|
|
45
|
+
let executionMode;
|
|
46
|
+
let explicitConfirmation;
|
|
47
|
+
if (input.command_effects) {
|
|
48
|
+
const ce = input.command_effects;
|
|
49
|
+
riskLevels.push(ce.risk_level ?? "low");
|
|
50
|
+
if (ce.writes) {
|
|
51
|
+
sideEffects.add("file_write");
|
|
52
|
+
for (const w of ce.writes) {
|
|
53
|
+
writes.push({
|
|
54
|
+
kind: "semantic",
|
|
55
|
+
target: w.target,
|
|
56
|
+
description: w.description,
|
|
57
|
+
overwrite: w.overwrite,
|
|
58
|
+
destructive: w.destructive,
|
|
59
|
+
...w.idempotent !== void 0 ? { idempotent: w.idempotent } : {},
|
|
60
|
+
...w.idempotency_key ? { idempotency_key: w.idempotency_key } : {},
|
|
61
|
+
...w.idempotent_note ? { idempotent_note: w.idempotent_note } : {},
|
|
62
|
+
source: `command:${input.command_id}`
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (ce.reads) {
|
|
67
|
+
for (const r of ce.reads) {
|
|
68
|
+
reads.push({
|
|
69
|
+
kind: "semantic",
|
|
70
|
+
target: r.target,
|
|
71
|
+
description: r.description,
|
|
72
|
+
source: `command:${input.command_id}`
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (ce.network) {
|
|
77
|
+
sideEffects.add("network");
|
|
78
|
+
if (typeof ce.network === "object") {
|
|
79
|
+
networkEffects.push({
|
|
80
|
+
...ce.network.description ? { description: ce.network.description } : {},
|
|
81
|
+
...ce.network.domains ? { domains: ce.network.domains } : {},
|
|
82
|
+
...ce.network.idempotent !== void 0 ? { idempotent: ce.network.idempotent } : {},
|
|
83
|
+
...ce.network.idempotency_key ? { idempotency_key: ce.network.idempotency_key } : {},
|
|
84
|
+
...ce.network.idempotent_note ? { idempotent_note: ce.network.idempotent_note } : {},
|
|
85
|
+
source: `command:${input.command_id}`
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (ce.execution_mode) {
|
|
90
|
+
executionMode = ce.execution_mode;
|
|
91
|
+
}
|
|
92
|
+
if (ce.requires_confirmation !== void 0) {
|
|
93
|
+
explicitConfirmation = ce.requires_confirmation;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
for (const [optName, optInput] of Object.entries(input.options)) {
|
|
97
|
+
const { definition, value, specified } = optInput;
|
|
98
|
+
const active = isOptionActive(definition, value, specified);
|
|
99
|
+
if (!active) continue;
|
|
100
|
+
if (definition.file) {
|
|
101
|
+
const filePath = typeof value === "string" ? value : void 0;
|
|
102
|
+
const mode = definition.file.mode;
|
|
103
|
+
if (mode === "read" || mode === "readWrite") {
|
|
104
|
+
reads.push({
|
|
105
|
+
kind: "option-file",
|
|
106
|
+
option: optName,
|
|
107
|
+
path: filePath,
|
|
108
|
+
source: `option:${optName}`
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (mode === "write" || mode === "append" || mode === "readWrite") {
|
|
112
|
+
sideEffects.add("file_write");
|
|
113
|
+
writes.push({
|
|
114
|
+
kind: "option-file",
|
|
115
|
+
option: optName,
|
|
116
|
+
path: filePath,
|
|
117
|
+
mode,
|
|
118
|
+
source: `option:${optName}`
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (definition.effects) {
|
|
123
|
+
const eff = definition.effects;
|
|
124
|
+
riskLevels.push(eff.risk_level ?? "low");
|
|
125
|
+
if (eff.writes) {
|
|
126
|
+
sideEffects.add("file_write");
|
|
127
|
+
for (const w of eff.writes) {
|
|
128
|
+
writes.push({
|
|
129
|
+
kind: "semantic",
|
|
130
|
+
target: w.target,
|
|
131
|
+
description: w.description,
|
|
132
|
+
overwrite: w.overwrite,
|
|
133
|
+
destructive: w.destructive,
|
|
134
|
+
...w.idempotent !== void 0 ? { idempotent: w.idempotent } : {},
|
|
135
|
+
...w.idempotency_key ? { idempotency_key: w.idempotency_key } : {},
|
|
136
|
+
...w.idempotent_note ? { idempotent_note: w.idempotent_note } : {},
|
|
137
|
+
source: `option:${optName}`
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (eff.reads) {
|
|
142
|
+
for (const r of eff.reads) {
|
|
143
|
+
reads.push({
|
|
144
|
+
kind: "semantic",
|
|
145
|
+
target: r.target,
|
|
146
|
+
description: r.description,
|
|
147
|
+
source: `option:${optName}`
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (eff.network) {
|
|
152
|
+
sideEffects.add("network");
|
|
153
|
+
if (typeof eff.network === "object") {
|
|
154
|
+
networkEffects.push({
|
|
155
|
+
...eff.network.description ? { description: eff.network.description } : {},
|
|
156
|
+
...eff.network.domains ? { domains: eff.network.domains } : {},
|
|
157
|
+
...eff.network.idempotent !== void 0 ? { idempotent: eff.network.idempotent } : {},
|
|
158
|
+
...eff.network.idempotency_key ? { idempotency_key: eff.network.idempotency_key } : {},
|
|
159
|
+
...eff.network.idempotent_note ? { idempotent_note: eff.network.idempotent_note } : {},
|
|
160
|
+
source: `option:${optName}`
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (eff.execution_mode && !executionMode) {
|
|
165
|
+
executionMode = eff.execution_mode;
|
|
166
|
+
}
|
|
167
|
+
if (eff.requires_confirmation !== void 0 && explicitConfirmation === void 0) {
|
|
168
|
+
explicitConfirmation = eff.requires_confirmation;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const finalRiskLevel = riskLevels.length > 0 ? maxRiskLevel(...riskLevels) : "low";
|
|
173
|
+
const requiresConfirmation = explicitConfirmation ?? (finalRiskLevel === "high" || finalRiskLevel === "critical");
|
|
174
|
+
let requiresSecrets;
|
|
175
|
+
if (input.env) {
|
|
176
|
+
const secrets = [];
|
|
177
|
+
for (const [envName, envVar] of Object.entries(input.env)) {
|
|
178
|
+
if (envVar.sensitive) {
|
|
179
|
+
secrets.push(envName);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (secrets.length > 0) {
|
|
183
|
+
requiresSecrets = secrets;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const semanticWrites = writes.filter((w) => w.kind === "semantic");
|
|
187
|
+
const idempotent = semanticWrites.every((w) => w.idempotent === true) && networkEffects.every((n) => n.idempotent === true);
|
|
188
|
+
return {
|
|
189
|
+
risk_level: finalRiskLevel,
|
|
190
|
+
requires_confirmation: requiresConfirmation,
|
|
191
|
+
idempotent,
|
|
192
|
+
side_effects: [...sideEffects],
|
|
193
|
+
reads,
|
|
194
|
+
writes,
|
|
195
|
+
...networkEffects.length > 0 ? { network: networkEffects } : {},
|
|
196
|
+
...executionMode ? { execution_mode: executionMode } : {},
|
|
197
|
+
...requiresSecrets ? { requires_secrets: requiresSecrets } : {}
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/generated/policy.ts
|
|
202
|
+
var commandDefinitions = {
|
|
203
|
+
"generate.python": {
|
|
204
|
+
"effects": {
|
|
205
|
+
"risk_level": "low",
|
|
206
|
+
"reads": [
|
|
207
|
+
"ts-definitions"
|
|
208
|
+
],
|
|
209
|
+
"writes": [
|
|
210
|
+
"generated-code"
|
|
211
|
+
]
|
|
212
|
+
},
|
|
213
|
+
"options": [
|
|
214
|
+
{
|
|
215
|
+
"name": "entry",
|
|
216
|
+
"schema": {
|
|
217
|
+
"type": "string"
|
|
218
|
+
},
|
|
219
|
+
"file": {
|
|
220
|
+
"mode": "read",
|
|
221
|
+
"exists": true,
|
|
222
|
+
"media_type": "application/typescript",
|
|
223
|
+
"encoding": "utf-8"
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"name": "queries",
|
|
228
|
+
"schema": {
|
|
229
|
+
"type": "string"
|
|
230
|
+
},
|
|
231
|
+
"file": {
|
|
232
|
+
"mode": "read",
|
|
233
|
+
"exists": true,
|
|
234
|
+
"media_type": "application/typescript",
|
|
235
|
+
"encoding": "utf-8"
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
"name": "commands",
|
|
240
|
+
"schema": {
|
|
241
|
+
"type": "string"
|
|
242
|
+
},
|
|
243
|
+
"file": {
|
|
244
|
+
"mode": "read",
|
|
245
|
+
"exists": true,
|
|
246
|
+
"media_type": "application/typescript",
|
|
247
|
+
"encoding": "utf-8"
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
"name": "transactions",
|
|
252
|
+
"schema": {
|
|
253
|
+
"type": "string"
|
|
254
|
+
},
|
|
255
|
+
"file": {
|
|
256
|
+
"mode": "read",
|
|
257
|
+
"exists": true,
|
|
258
|
+
"media_type": "application/typescript",
|
|
259
|
+
"encoding": "utf-8"
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
"name": "contracts",
|
|
264
|
+
"schema": {
|
|
265
|
+
"type": "string"
|
|
266
|
+
},
|
|
267
|
+
"file": {
|
|
268
|
+
"mode": "read",
|
|
269
|
+
"exists": true,
|
|
270
|
+
"media_type": "application/typescript",
|
|
271
|
+
"encoding": "utf-8"
|
|
272
|
+
}
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
"name": "contexts",
|
|
276
|
+
"schema": {
|
|
277
|
+
"type": "string"
|
|
278
|
+
},
|
|
279
|
+
"file": {
|
|
280
|
+
"mode": "read",
|
|
281
|
+
"exists": true,
|
|
282
|
+
"media_type": "application/typescript",
|
|
283
|
+
"encoding": "utf-8"
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
"name": "dataclass",
|
|
288
|
+
"schema": {
|
|
289
|
+
"type": "boolean"
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
"name": "out",
|
|
294
|
+
"schema": {
|
|
295
|
+
"type": "string"
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
]
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
function deriveCommandPolicy(command_id, optionValues) {
|
|
302
|
+
const def = commandDefinitions[command_id];
|
|
303
|
+
if (!def) throw new Error(`Unknown command: ${command_id}`);
|
|
304
|
+
const cmdDef = def;
|
|
305
|
+
const options = {};
|
|
306
|
+
const active_options = [];
|
|
307
|
+
for (const optDef of cmdDef.options ?? []) {
|
|
308
|
+
const value = optionValues[optDef.name];
|
|
309
|
+
const specified = optDef.name in optionValues && value !== void 0;
|
|
310
|
+
options[optDef.name] = { value, specified, definition: optDef };
|
|
311
|
+
if (isOptionActive(optDef, value, specified)) {
|
|
312
|
+
active_options.push(optDef.name);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
const policy = derivePolicy({
|
|
316
|
+
command_id,
|
|
317
|
+
command_effects: cmdDef.effects,
|
|
318
|
+
options,
|
|
319
|
+
env: cmdDef.env
|
|
320
|
+
});
|
|
321
|
+
return { command: command_id, active_options, policy };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/generated/contract.ts
|
|
325
|
+
var CONTRACT_YAML = "cli_contracts: 0.1.0\n\ninfo:\n title: GraphDDB CLI\n version: 0.1.0\n description: >-\n Contract for the graphddb code-generation CLI. The CLI reads TypeScript\n GraphDDB model / query / command definitions (the single source of truth)\n and emits Python bindings (manifest.json, operations.json, types.py,\n repositories.py, __init__.py) for the Python bridge runtime.\n license:\n name: MIT\n contact:\n name: foo-log-inc\n url: https://github.com/foo-log-inc/graphddb\n\nartifact_slots:\n ts-definitions:\n description: >-\n TypeScript GraphDDB definition modules (entity models, defineQueries,\n defineCommands) used as the single source of truth.\n direction: read\n generated-code:\n description: >-\n Generated Python bindings and JSON specs written to the output directory\n (manifest.json, operations.json, types.py, repositories.py, __init__.py).\n direction: write\n\ncommand_sets:\n graphddb:\n summary: Graph data modeling on DynamoDB \u2014 code generation CLI.\n x-stdin: >-\n No command reads from stdin. All inputs are provided via file options.\n\n commands:\n # \u2500\u2500 generate python \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n generate.python:\n summary: Generate Python bindings from TypeScript GraphDDB definitions.\n description: >-\n Loads the TypeScript entity models, query definitions, and command\n definitions, builds the serializable manifest / operations bundle, and\n writes the Python bridge bindings (manifest.json, operations.json,\n types.py, repositories.py, __init__.py) to the output directory.\n Output is deterministic for a given set of inputs.\n usage:\n - graphddb generate python --entry models.ts --queries queries.ts --commands commands.ts --out generated/\n - graphddb generate python --entry definitions.ts --out generated/\n\n effects:\n risk_level: low\n reads:\n - ts-definitions\n writes:\n - generated-code\n\n x-agent:\n recommended_before_use:\n - \"Ensure the TypeScript definitions type-check and lint cleanly.\"\n\n options:\n - name: entry\n aliases: [e]\n required: true\n description: >-\n Entry module that registers the entity models (side-effecting\n import). May also export queries / commands.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: queries\n aliases: [q]\n description: >-\n Module exporting `queries` (a defineQueries map). Defaults to the\n entry module when omitted.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: commands\n aliases: [c]\n description: >-\n Module exporting `commands` (a defineCommands map). Defaults to the\n entry module when omitted.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: transactions\n aliases: [t]\n description: >-\n Module exporting `transactions` (a defineTransactions map).\n Defaults to the commands module (or the entry module) when omitted.\n Optional \u2014 a project with no declarative transactions omits it.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: contracts\n description: >-\n Module exporting `contracts` (a map of publicQueryModel /\n publicCommandModel results \u2014 the CQRS Contract layer). Defaults to\n the entry module when omitted. Optional \u2014 a project with no\n contracts omits it, and the output gains no `contracts` content.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: contexts\n description: >-\n Module exporting `contexts` (context-ownership: context name \u2192 the\n Model / Contract names that belong to it). Defaults to the\n contracts module (or the entry module) when omitted. Optional \u2014\n consumed by the boundary lint to tell own from foreign.\n value_name: file\n schema:\n type: string\n file:\n mode: read\n exists: true\n media_type: application/typescript\n encoding: utf-8\n\n - name: dataclass\n description: >-\n Emit Python result / element types as @dataclass classes instead\n of TypedDict (decimal / Optional / Connection mapped accordingly).\n schema:\n type: boolean\n\n - name: out\n aliases: [o]\n required: true\n description: Output directory for the generated Python bindings.\n value_name: dir\n schema:\n type: string\n\n exits:\n '0':\n description: Generation succeeded.\n stdout:\n format: json\n schema:\n $ref: '#/components/schemas/GenerateResult'\n files:\n - path: '{options.out}/manifest.json'\n required: true\n media_type: application/json\n description: Entity / table / key / relation metadata.\n - path: '{options.out}/operations.json'\n required: true\n media_type: application/json\n description: Per-query / per-command execution specs.\n - path: '{options.out}/types.py'\n required: true\n media_type: text/x-python\n description: TypedDict result types derived from query selects.\n - path: '{options.out}/repositories.py'\n required: true\n media_type: text/x-python\n description: Per-entity repository classes (sync methods).\n - path: '{options.out}/__init__.py'\n required: true\n media_type: text/x-python\n description: Package re-exports for the generated bindings.\n\n '1':\n description: Unexpected error.\n stderr:\n format: json\n schema:\n $ref: '#/components/schemas/Error'\n\n '2':\n description: Invalid arguments (missing/unreadable input or output).\n stderr:\n format: json\n schema:\n $ref: '#/components/schemas/Error'\n\ncomponents:\n schemas:\n GenerateResult:\n type: object\n required: [status, outDir, files]\n properties:\n status:\n type: string\n enum: [ok]\n outDir:\n type: string\n description: Output directory the files were written to.\n files:\n type: array\n description: Paths of the generated files (relative to outDir).\n items:\n type: string\n Error:\n type: object\n required: [code, message]\n properties:\n code:\n type: string\n message:\n type: string\n";
|
|
326
|
+
var CONTRACT_JSON_STR = '{\n "cli_contracts": "0.1.0",\n "info": {\n "title": "GraphDDB CLI",\n "version": "0.1.0",\n "description": "Contract for the graphddb code-generation CLI. The CLI reads TypeScript GraphDDB model / query / command definitions (the single source of truth) and emits Python bindings (manifest.json, operations.json, types.py, repositories.py, __init__.py) for the Python bridge runtime.",\n "license": {\n "name": "MIT"\n },\n "contact": {\n "name": "foo-log-inc",\n "url": "https://github.com/foo-log-inc/graphddb"\n }\n },\n "artifact_slots": {\n "ts-definitions": {\n "description": "TypeScript GraphDDB definition modules (entity models, defineQueries, defineCommands) used as the single source of truth.",\n "direction": "read"\n },\n "generated-code": {\n "description": "Generated Python bindings and JSON specs written to the output directory (manifest.json, operations.json, types.py, repositories.py, __init__.py).",\n "direction": "write"\n }\n },\n "command_sets": {\n "graphddb": {\n "summary": "Graph data modeling on DynamoDB \u2014 code generation CLI.",\n "x-stdin": "No command reads from stdin. All inputs are provided via file options.",\n "commands": {\n "generate.python": {\n "summary": "Generate Python bindings from TypeScript GraphDDB definitions.",\n "description": "Loads the TypeScript entity models, query definitions, and command definitions, builds the serializable manifest / operations bundle, and writes the Python bridge bindings (manifest.json, operations.json, types.py, repositories.py, __init__.py) to the output directory. Output is deterministic for a given set of inputs.",\n "usage": [\n "graphddb generate python --entry models.ts --queries queries.ts --commands commands.ts --out generated/",\n "graphddb generate python --entry definitions.ts --out generated/"\n ],\n "effects": {\n "risk_level": "low",\n "reads": [\n "ts-definitions"\n ],\n "writes": [\n "generated-code"\n ]\n },\n "x-agent": {\n "recommended_before_use": [\n "Ensure the TypeScript definitions type-check and lint cleanly."\n ]\n },\n "options": [\n {\n "name": "entry",\n "aliases": [\n "e"\n ],\n "required": true,\n "description": "Entry module that registers the entity models (side-effecting import). May also export queries / commands.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "queries",\n "aliases": [\n "q"\n ],\n "description": "Module exporting `queries` (a defineQueries map). Defaults to the entry module when omitted.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "commands",\n "aliases": [\n "c"\n ],\n "description": "Module exporting `commands` (a defineCommands map). Defaults to the entry module when omitted.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "transactions",\n "aliases": [\n "t"\n ],\n "description": "Module exporting `transactions` (a defineTransactions map). Defaults to the commands module (or the entry module) when omitted. Optional \u2014 a project with no declarative transactions omits it.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "contracts",\n "description": "Module exporting `contracts` (a map of publicQueryModel / publicCommandModel results \u2014 the CQRS Contract layer). Defaults to the entry module when omitted. Optional \u2014 a project with no contracts omits it, and the output gains no `contracts` content.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "contexts",\n "description": "Module exporting `contexts` (context-ownership: context name \u2192 the Model / Contract names that belong to it). Defaults to the contracts module (or the entry module) when omitted. Optional \u2014 consumed by the boundary lint to tell own from foreign.",\n "value_name": "file",\n "schema": {\n "type": "string"\n },\n "file": {\n "mode": "read",\n "exists": true,\n "media_type": "application/typescript",\n "encoding": "utf-8"\n }\n },\n {\n "name": "dataclass",\n "description": "Emit Python result / element types as @dataclass classes instead of TypedDict (decimal / Optional / Connection mapped accordingly).",\n "schema": {\n "type": "boolean"\n }\n },\n {\n "name": "out",\n "aliases": [\n "o"\n ],\n "required": true,\n "description": "Output directory for the generated Python bindings.",\n "value_name": "dir",\n "schema": {\n "type": "string"\n }\n }\n ],\n "exits": {\n "0": {\n "description": "Generation succeeded.",\n "stdout": {\n "format": "json",\n "schema": {\n "$ref": "#/components/schemas/GenerateResult"\n }\n },\n "files": [\n {\n "path": "{options.out}/manifest.json",\n "required": true,\n "media_type": "application/json",\n "description": "Entity / table / key / relation metadata."\n },\n {\n "path": "{options.out}/operations.json",\n "required": true,\n "media_type": "application/json",\n "description": "Per-query / per-command execution specs."\n },\n {\n "path": "{options.out}/types.py",\n "required": true,\n "media_type": "text/x-python",\n "description": "TypedDict result types derived from query selects."\n },\n {\n "path": "{options.out}/repositories.py",\n "required": true,\n "media_type": "text/x-python",\n "description": "Per-entity repository classes (sync methods)."\n },\n {\n "path": "{options.out}/__init__.py",\n "required": true,\n "media_type": "text/x-python",\n "description": "Package re-exports for the generated bindings."\n }\n ]\n },\n "1": {\n "description": "Unexpected error.",\n "stderr": {\n "format": "json",\n "schema": {\n "$ref": "#/components/schemas/Error"\n }\n }\n },\n "2": {\n "description": "Invalid arguments (missing/unreadable input or output).",\n "stderr": {\n "format": "json",\n "schema": {\n "$ref": "#/components/schemas/Error"\n }\n }\n }\n }\n }\n }\n }\n },\n "components": {\n "schemas": {\n "GenerateResult": {\n "type": "object",\n "required": [\n "status",\n "outDir",\n "files"\n ],\n "properties": {\n "status": {\n "type": "string",\n "enum": [\n "ok"\n ]\n },\n "outDir": {\n "type": "string",\n "description": "Output directory the files were written to."\n },\n "files": {\n "type": "array",\n "description": "Paths of the generated files (relative to outDir).",\n "items": {\n "type": "string"\n }\n }\n }\n },\n "Error": {\n "type": "object",\n "required": [\n "code",\n "message"\n ],\n "properties": {\n "code": {\n "type": "string"\n },\n "message": {\n "type": "string"\n }\n }\n }\n }\n }\n}';
|
|
327
|
+
|
|
328
|
+
// src/generated/program.ts
|
|
329
|
+
function createProgram(handlers2, version) {
|
|
330
|
+
const program = new Command();
|
|
331
|
+
program.name("graphddb").version(version, "-V, --version").description("Graph data modeling on DynamoDB \u2014 code generation CLI.");
|
|
332
|
+
program.option("--introspect", "Output derived policy as JSON without executing the command");
|
|
333
|
+
const __cmd_generate = program.command("generate");
|
|
334
|
+
__cmd_generate.command("python").description("Generate Python bindings from TypeScript GraphDDB definitions.").option("-e, --entry <file>", "Entry module that registers the entity models (side-effecting import). May also export queries / commands.").option("-q, --queries <file>", "Module exporting `queries` (a defineQueries map). Defaults to the entry module when omitted.").option("-c, --commands <file>", "Module exporting `commands` (a defineCommands map). Defaults to the entry module when omitted.").option("-t, --transactions <file>", "Module exporting `transactions` (a defineTransactions map). Defaults to the commands module (or the entry module) when omitted. Optional \u2014 a project with no declarative transactions omits it.").option("--contracts <file>", "Module exporting `contracts` (a map of publicQueryModel / publicCommandModel results \u2014 the CQRS Contract layer). Defaults to the entry module when omitted. Optional \u2014 a project with no contracts omits it, and the output gains no `contracts` content.").option("--contexts <file>", "Module exporting `contexts` (context-ownership: context name \u2192 the Model / Contract names that belong to it). Defaults to the contracts module (or the entry module) when omitted. Optional \u2014 consumed by the boundary lint to tell own from foreign.").option("--dataclass", "Emit Python result / element types as @dataclass classes instead of TypedDict (decimal / Optional / Connection mapped accordingly).").option("-o, --out <dir>", "Output directory for the generated Python bindings.").action(async (opts, cmd) => {
|
|
335
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
336
|
+
if (globalOpts.introspect) {
|
|
337
|
+
const policy = deriveCommandPolicy("generate.python", opts);
|
|
338
|
+
console.log(JSON.stringify(policy, null, 2));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
await handlers2.generatePython(opts, globalOpts);
|
|
342
|
+
});
|
|
343
|
+
program.command("extract").description("Extract contract specification for this CLI tool.").argument("[commands...]", "Command IDs to extract. Use dot notation.").option("-a, --all", "Extract all commands.", false).option("--include-meta", "Include extraction metadata.", true).option("-F, --format <format>", "Output format (yaml or json).", "yaml").action(async (commands, opts, cmd) => {
|
|
344
|
+
if (commands.length === 0 && !opts.all) {
|
|
345
|
+
process.stderr.write(JSON.stringify({ code: "INVALID_ARGS", message: "Specify command IDs or use --all" }) + "\n");
|
|
346
|
+
process.exit(2);
|
|
347
|
+
}
|
|
348
|
+
const format = opts.format || "yaml";
|
|
349
|
+
const doc = JSON.parse(CONTRACT_JSON_STR);
|
|
350
|
+
const cmdIds = opts.all ? [] : commands;
|
|
351
|
+
if (cmdIds.length === 0) {
|
|
352
|
+
if (format === "json") {
|
|
353
|
+
const out = {};
|
|
354
|
+
if (opts.includeMeta) {
|
|
355
|
+
out._meta = {
|
|
356
|
+
source: "embedded",
|
|
357
|
+
type: "cli-contracts/extract",
|
|
358
|
+
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
359
|
+
specVersion: doc.cli_contracts ?? "0.1.0",
|
|
360
|
+
commands: ["graphddb.generate.python"]
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
Object.assign(out, doc);
|
|
364
|
+
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
365
|
+
} else {
|
|
366
|
+
const yamlLines = [];
|
|
367
|
+
yamlLines.push("# graphddb extract");
|
|
368
|
+
yamlLines.push("# source: embedded");
|
|
369
|
+
yamlLines.push("# type: cli-contracts/command-extract");
|
|
370
|
+
if (opts.includeMeta) {
|
|
371
|
+
yamlLines.push("---");
|
|
372
|
+
yamlLines.push("source: embedded");
|
|
373
|
+
yamlLines.push("type: cli-contracts/command-extract");
|
|
374
|
+
yamlLines.push("extractedAt: " + (/* @__PURE__ */ new Date()).toISOString());
|
|
375
|
+
yamlLines.push("spec_version: " + (doc.cli_contracts ?? "0.1.0"));
|
|
376
|
+
yamlLines.push("commands:");
|
|
377
|
+
for (const id of ["graphddb.generate.python"]) {
|
|
378
|
+
yamlLines.push(" - " + id);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
yamlLines.push("---");
|
|
382
|
+
yamlLines.push(CONTRACT_YAML);
|
|
383
|
+
process.stdout.write(yamlLines.join("\n") + "\n");
|
|
384
|
+
}
|
|
385
|
+
} else {
|
|
386
|
+
const filtered = {
|
|
387
|
+
cli_contracts: doc.cli_contracts,
|
|
388
|
+
info: doc.info,
|
|
389
|
+
command_sets: {}
|
|
390
|
+
};
|
|
391
|
+
const fcs = filtered.command_sets;
|
|
392
|
+
for (const [setId, cs] of Object.entries(doc.command_sets ?? {})) {
|
|
393
|
+
const cmds = cs.commands;
|
|
394
|
+
if (!cmds) continue;
|
|
395
|
+
const matched = {};
|
|
396
|
+
for (const [cmdId, cmdDef] of Object.entries(cmds)) {
|
|
397
|
+
const fullId = setId + "." + cmdId;
|
|
398
|
+
if (cmdIds.some((id) => id === cmdId || id === fullId || cmdId.startsWith(id + "."))) {
|
|
399
|
+
matched[cmdId] = cmdDef;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (Object.keys(matched).length > 0) {
|
|
403
|
+
const setCopy = { ...cs };
|
|
404
|
+
setCopy.commands = matched;
|
|
405
|
+
fcs[setId] = setCopy;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
if (doc.components) filtered.components = doc.components;
|
|
409
|
+
process.stdout.write(JSON.stringify(filtered, null, 2) + "\n");
|
|
410
|
+
}
|
|
411
|
+
process.exit(0);
|
|
412
|
+
});
|
|
413
|
+
return program;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// src/cli/handlers.ts
|
|
417
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
418
|
+
import { pathToFileURL } from "url";
|
|
419
|
+
import { isAbsolute, resolve } from "path";
|
|
420
|
+
|
|
421
|
+
// src/codegen/python.ts
|
|
422
|
+
var PY_HEADER = "# DO NOT EDIT. Generated by GraphDDB.";
|
|
423
|
+
function toPascalCase(name) {
|
|
424
|
+
return name.replace(/[_-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/\s+/).filter(Boolean).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("");
|
|
425
|
+
}
|
|
426
|
+
function toSnakeCase(name) {
|
|
427
|
+
return name.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s.]+/g, "_").toLowerCase();
|
|
428
|
+
}
|
|
429
|
+
function entityBaseName(entity) {
|
|
430
|
+
return entity.replace(/Model$/, "");
|
|
431
|
+
}
|
|
432
|
+
function pyScalarType(type) {
|
|
433
|
+
switch (type) {
|
|
434
|
+
case "number":
|
|
435
|
+
case "numberSet":
|
|
436
|
+
return "float";
|
|
437
|
+
case "boolean":
|
|
438
|
+
return "bool";
|
|
439
|
+
case "binary":
|
|
440
|
+
return "bytes";
|
|
441
|
+
case "list":
|
|
442
|
+
return "list";
|
|
443
|
+
case "map":
|
|
444
|
+
return "dict";
|
|
445
|
+
case "stringSet":
|
|
446
|
+
case "string":
|
|
447
|
+
default:
|
|
448
|
+
return "str";
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
function pyParamType(spec) {
|
|
452
|
+
if (spec.type === "literal" && spec.literals && spec.literals.length > 0) {
|
|
453
|
+
const lits = spec.literals.map((l) => typeof l === "string" ? JSON.stringify(l) : String(l)).join(", ");
|
|
454
|
+
return `Literal[${lits}]`;
|
|
455
|
+
}
|
|
456
|
+
return spec.type === "number" ? "float" : "str";
|
|
457
|
+
}
|
|
458
|
+
function txElementTypeName(txName, paramName) {
|
|
459
|
+
return `${toPascalCase(txName)}${toPascalCase(paramName)}Item`;
|
|
460
|
+
}
|
|
461
|
+
function pyTransactionParamType(txName, paramName, spec) {
|
|
462
|
+
if (spec.type === "array") {
|
|
463
|
+
return `list[${txElementTypeName(txName, paramName)}]`;
|
|
464
|
+
}
|
|
465
|
+
return pyParamType(spec);
|
|
466
|
+
}
|
|
467
|
+
function collectResultTypes(manifest, entity, select, typeName, out, seen) {
|
|
468
|
+
const meta = manifest.entities[entity];
|
|
469
|
+
const fields = [];
|
|
470
|
+
for (const field of Object.keys(select).sort()) {
|
|
471
|
+
const value = select[field];
|
|
472
|
+
const relation = meta?.relations[field];
|
|
473
|
+
if (relation) {
|
|
474
|
+
const spec = normalizeSelectSpec(value);
|
|
475
|
+
const childSelect = spec.select ?? {};
|
|
476
|
+
const itemTypeName = `${typeName}${toPascalCase(field)}Item`;
|
|
477
|
+
collectResultTypes(
|
|
478
|
+
manifest,
|
|
479
|
+
relation.target,
|
|
480
|
+
childSelect,
|
|
481
|
+
itemTypeName,
|
|
482
|
+
out,
|
|
483
|
+
seen
|
|
484
|
+
);
|
|
485
|
+
if (relation.type === "hasMany") {
|
|
486
|
+
const connName = `${itemTypeName}Connection`;
|
|
487
|
+
if (!seen.has(connName)) {
|
|
488
|
+
seen.add(connName);
|
|
489
|
+
out.push({
|
|
490
|
+
name: connName,
|
|
491
|
+
fields: [
|
|
492
|
+
["items", `list[${itemTypeName}]`],
|
|
493
|
+
["cursor", "str | None"]
|
|
494
|
+
]
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
fields.push([field, connName]);
|
|
498
|
+
} else {
|
|
499
|
+
fields.push([field, `${itemTypeName} | None`]);
|
|
500
|
+
}
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (value === true) {
|
|
504
|
+
const fieldType = meta?.fields[field]?.type ?? "string";
|
|
505
|
+
fields.push([field, pyScalarType(fieldType)]);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (!seen.has(typeName)) {
|
|
509
|
+
seen.add(typeName);
|
|
510
|
+
out.push({ name: typeName, fields });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
function selectOf(def) {
|
|
514
|
+
return def.select ?? {};
|
|
515
|
+
}
|
|
516
|
+
function queryResultTypeName(queryName) {
|
|
517
|
+
return `${toPascalCase(queryName)}Result`;
|
|
518
|
+
}
|
|
519
|
+
function renderTypedDict(cls) {
|
|
520
|
+
if (cls.fields.length === 0) {
|
|
521
|
+
return `class ${cls.name}(TypedDict):
|
|
522
|
+
pass`;
|
|
523
|
+
}
|
|
524
|
+
const lines = cls.fields.map(([name, type]) => ` ${name}: ${type}`);
|
|
525
|
+
return `class ${cls.name}(TypedDict):
|
|
526
|
+
${lines.join("\n")}`;
|
|
527
|
+
}
|
|
528
|
+
function renderDataclass(cls) {
|
|
529
|
+
if (cls.fields.length === 0) {
|
|
530
|
+
return `@dataclass
|
|
531
|
+
class ${cls.name}:
|
|
532
|
+
pass`;
|
|
533
|
+
}
|
|
534
|
+
const lines = cls.fields.map(
|
|
535
|
+
([name, type]) => type.endsWith("| None") ? ` ${name}: ${type} = None` : ` ${name}: ${type}`
|
|
536
|
+
);
|
|
537
|
+
const required = lines.filter((l) => !l.endsWith("= None"));
|
|
538
|
+
const optional = lines.filter((l) => l.endsWith("= None"));
|
|
539
|
+
return `@dataclass
|
|
540
|
+
class ${cls.name}:
|
|
541
|
+
${[...required, ...optional].join("\n")}`;
|
|
542
|
+
}
|
|
543
|
+
function collectTransactionElementTypes(transactions, out, seen) {
|
|
544
|
+
for (const txName of Object.keys(transactions).sort()) {
|
|
545
|
+
const spec = transactions[txName];
|
|
546
|
+
for (const paramName of Object.keys(spec.params).sort()) {
|
|
547
|
+
const p = spec.params[paramName];
|
|
548
|
+
if (p.type !== "array" || !p.element) continue;
|
|
549
|
+
const name = txElementTypeName(txName, paramName);
|
|
550
|
+
if (seen.has(name)) continue;
|
|
551
|
+
seen.add(name);
|
|
552
|
+
const fields = [];
|
|
553
|
+
for (const field of Object.keys(p.element).sort()) {
|
|
554
|
+
fields.push([field, pyParamType(p.element[field])]);
|
|
555
|
+
}
|
|
556
|
+
out.push({ name, fields });
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function generateTypes(manifest, queries, transactions = {}, options = {}) {
|
|
561
|
+
const classes = [];
|
|
562
|
+
const seen = /* @__PURE__ */ new Set();
|
|
563
|
+
for (const queryName of Object.keys(queries).sort()) {
|
|
564
|
+
const def = queries[queryName];
|
|
565
|
+
collectResultTypes(
|
|
566
|
+
manifest,
|
|
567
|
+
def.entity.name,
|
|
568
|
+
selectOf(def),
|
|
569
|
+
queryResultTypeName(queryName),
|
|
570
|
+
classes,
|
|
571
|
+
seen
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
collectTransactionElementTypes(transactions, classes, seen);
|
|
575
|
+
const useDataclass = Boolean(options.dataclass);
|
|
576
|
+
const render = useDataclass ? renderDataclass : renderTypedDict;
|
|
577
|
+
const body = classes.length === 0 ? "# (no query result types)" : classes.map(render).join("\n\n\n");
|
|
578
|
+
const usesLiteral = classes.some(
|
|
579
|
+
(c) => c.fields.some(([, type]) => type.includes("Literal["))
|
|
580
|
+
);
|
|
581
|
+
const lines = [PY_HEADER, "", "from __future__ import annotations", ""];
|
|
582
|
+
if (useDataclass) {
|
|
583
|
+
lines.push("from dataclasses import dataclass");
|
|
584
|
+
if (usesLiteral) lines.push("from typing import Literal");
|
|
585
|
+
} else {
|
|
586
|
+
lines.push(
|
|
587
|
+
usesLiteral ? "from typing import Literal, TypedDict" : "from typing import TypedDict"
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
lines.push("", "", body, "");
|
|
591
|
+
return lines.join("\n");
|
|
592
|
+
}
|
|
593
|
+
function methodParams(def) {
|
|
594
|
+
const params = def.params;
|
|
595
|
+
const out = [];
|
|
596
|
+
for (const name of Object.keys(params).sort()) {
|
|
597
|
+
const d = params[name];
|
|
598
|
+
const spec = d.literals === void 0 ? { type: d.kind, required: d.required } : {
|
|
599
|
+
type: d.kind,
|
|
600
|
+
required: d.required,
|
|
601
|
+
literals: d.literals
|
|
602
|
+
};
|
|
603
|
+
out.push({ argName: toSnakeCase(name), pyType: pyParamType(spec), originalName: name });
|
|
604
|
+
}
|
|
605
|
+
return out;
|
|
606
|
+
}
|
|
607
|
+
function collectRepositories(queries, commands) {
|
|
608
|
+
const byEntity = /* @__PURE__ */ new Map();
|
|
609
|
+
const ensure = (entity) => {
|
|
610
|
+
const cls = `${entityBaseName(entity)}Repository`;
|
|
611
|
+
let methods = byEntity.get(cls);
|
|
612
|
+
if (!methods) {
|
|
613
|
+
methods = [];
|
|
614
|
+
byEntity.set(cls, methods);
|
|
615
|
+
}
|
|
616
|
+
return methods;
|
|
617
|
+
};
|
|
618
|
+
for (const name of Object.keys(queries).sort()) {
|
|
619
|
+
const def = queries[name];
|
|
620
|
+
ensure(def.entity.name).push({
|
|
621
|
+
methodName: toSnakeCase(name),
|
|
622
|
+
params: methodParams(def),
|
|
623
|
+
returnType: `${queryResultTypeName(name)} | None`,
|
|
624
|
+
kind: "query",
|
|
625
|
+
operationId: name
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
for (const name of Object.keys(commands).sort()) {
|
|
629
|
+
const def = commands[name];
|
|
630
|
+
ensure(def.entity.name).push({
|
|
631
|
+
methodName: toSnakeCase(name),
|
|
632
|
+
params: methodParams(def),
|
|
633
|
+
returnType: "None",
|
|
634
|
+
kind: "command",
|
|
635
|
+
operationId: name
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
return [...byEntity.keys()].sort().map((className) => ({
|
|
639
|
+
className,
|
|
640
|
+
methods: byEntity.get(className).slice().sort((a, b) => a.methodName.localeCompare(b.methodName))
|
|
641
|
+
}));
|
|
642
|
+
}
|
|
643
|
+
function renderMethod(m) {
|
|
644
|
+
const sig = m.params.map((p) => `${p.argName}: ${p.pyType}`).join(", ");
|
|
645
|
+
const head = ` def ${m.methodName}(self${sig ? ", " + sig : ""}) -> ${m.returnType}:`;
|
|
646
|
+
const paramsDict = m.params.map((p) => `"${p.originalName}": ${p.argName}`).join(", ");
|
|
647
|
+
const paramsArg = paramsDict ? `{${paramsDict}}` : "{}";
|
|
648
|
+
if (m.kind === "query") {
|
|
649
|
+
return [
|
|
650
|
+
head,
|
|
651
|
+
` return self._runtime.execute_query(`,
|
|
652
|
+
` query_id="${m.operationId}",`,
|
|
653
|
+
` params=${paramsArg},`,
|
|
654
|
+
` )`
|
|
655
|
+
].join("\n");
|
|
656
|
+
}
|
|
657
|
+
return [
|
|
658
|
+
head,
|
|
659
|
+
` self._runtime.execute_command(`,
|
|
660
|
+
` command_id="${m.operationId}",`,
|
|
661
|
+
` params=${paramsArg},`,
|
|
662
|
+
` )`
|
|
663
|
+
].join("\n");
|
|
664
|
+
}
|
|
665
|
+
function renderRepoClass(cls) {
|
|
666
|
+
const methods = cls.methods.length === 0 ? [" pass"] : cls.methods.map(renderMethod);
|
|
667
|
+
return [
|
|
668
|
+
`class ${cls.className}:`,
|
|
669
|
+
` def __init__(self, runtime: GraphDDBRuntime) -> None:`,
|
|
670
|
+
` self._runtime = runtime`,
|
|
671
|
+
"",
|
|
672
|
+
methods.join("\n\n")
|
|
673
|
+
].join("\n");
|
|
674
|
+
}
|
|
675
|
+
function transactionMethodParams(txName, spec) {
|
|
676
|
+
const params = [];
|
|
677
|
+
const elementTypes = [];
|
|
678
|
+
for (const name of Object.keys(spec.params).sort()) {
|
|
679
|
+
const p = spec.params[name];
|
|
680
|
+
if (p.type === "array") elementTypes.push(txElementTypeName(txName, name));
|
|
681
|
+
params.push({
|
|
682
|
+
argName: toSnakeCase(name),
|
|
683
|
+
pyType: pyTransactionParamType(txName, name, p),
|
|
684
|
+
originalName: name
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
return { params, elementTypes };
|
|
688
|
+
}
|
|
689
|
+
function collectTransactionMethods(transactions) {
|
|
690
|
+
return Object.keys(transactions).sort().map((name) => {
|
|
691
|
+
const { params, elementTypes } = transactionMethodParams(
|
|
692
|
+
name,
|
|
693
|
+
transactions[name]
|
|
694
|
+
);
|
|
695
|
+
return {
|
|
696
|
+
methodName: toSnakeCase(name),
|
|
697
|
+
params,
|
|
698
|
+
transactionId: name,
|
|
699
|
+
elementTypes
|
|
700
|
+
};
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
function renderTxMethod(m) {
|
|
704
|
+
const sig = m.params.map((p) => `${p.argName}: ${p.pyType}`).join(", ");
|
|
705
|
+
const head = ` def ${m.methodName}(self${sig ? ", " + sig : ""}) -> None:`;
|
|
706
|
+
const paramsDict = m.params.map((p) => `"${p.originalName}": ${p.argName}`).join(", ");
|
|
707
|
+
const paramsArg = paramsDict ? `{${paramsDict}}` : "{}";
|
|
708
|
+
return [
|
|
709
|
+
head,
|
|
710
|
+
` self._runtime.execute_transaction(`,
|
|
711
|
+
` transaction_id="${m.transactionId}",`,
|
|
712
|
+
` params=${paramsArg},`,
|
|
713
|
+
` )`
|
|
714
|
+
].join("\n");
|
|
715
|
+
}
|
|
716
|
+
function renderTransactionsClass(methods) {
|
|
717
|
+
const body = methods.length === 0 ? [" pass"] : methods.map(renderTxMethod);
|
|
718
|
+
return [
|
|
719
|
+
`class TransactionsRepository:`,
|
|
720
|
+
` def __init__(self, runtime: GraphDDBRuntime) -> None:`,
|
|
721
|
+
` self._runtime = runtime`,
|
|
722
|
+
"",
|
|
723
|
+
body.join("\n\n")
|
|
724
|
+
].join("\n");
|
|
725
|
+
}
|
|
726
|
+
function generateRepositories(queries, commands, transactions = {}) {
|
|
727
|
+
const classes = collectRepositories(queries, commands);
|
|
728
|
+
const txMethods = collectTransactionMethods(transactions);
|
|
729
|
+
const resultTypes = /* @__PURE__ */ new Set();
|
|
730
|
+
for (const name of Object.keys(queries)) {
|
|
731
|
+
resultTypes.add(queryResultTypeName(name));
|
|
732
|
+
}
|
|
733
|
+
const elementTypes = /* @__PURE__ */ new Set();
|
|
734
|
+
for (const m of txMethods) for (const t of m.elementTypes) elementTypes.add(t);
|
|
735
|
+
const sortedTypes = [...resultTypes, ...elementTypes].sort();
|
|
736
|
+
const importTypes = sortedTypes.length === 0 ? "" : `from .types import (
|
|
737
|
+
${sortedTypes.map((t) => ` ${t},`).join("\n")}
|
|
738
|
+
)
|
|
739
|
+
`;
|
|
740
|
+
const repoBodies = classes.map(renderRepoClass);
|
|
741
|
+
if (txMethods.length > 0) repoBodies.push(renderTransactionsClass(txMethods));
|
|
742
|
+
const body = repoBodies.length === 0 ? "# (no repositories)" : repoBodies.join("\n\n\n");
|
|
743
|
+
const usesLiteral = classes.some(
|
|
744
|
+
(c) => c.methods.some((m) => m.params.some((p) => p.pyType.startsWith("Literal[")))
|
|
745
|
+
);
|
|
746
|
+
const lines = [PY_HEADER, "", "from __future__ import annotations", ""];
|
|
747
|
+
if (usesLiteral) lines.push("from typing import Literal", "");
|
|
748
|
+
lines.push("from graphddb_runtime import GraphDDBRuntime");
|
|
749
|
+
if (importTypes) lines.push(importTypes.replace(/\n$/, ""));
|
|
750
|
+
lines.push("", body, "");
|
|
751
|
+
return lines.join("\n");
|
|
752
|
+
}
|
|
753
|
+
function repositoryClassNames(queries, commands, transactions = {}) {
|
|
754
|
+
const names = collectRepositories(queries, commands).map((c) => c.className);
|
|
755
|
+
if (Object.keys(transactions).length > 0) names.push("TransactionsRepository");
|
|
756
|
+
return names;
|
|
757
|
+
}
|
|
758
|
+
function generateInit(queries, commands, transactions = {}) {
|
|
759
|
+
const repos = repositoryClassNames(queries, commands, transactions);
|
|
760
|
+
const resultTypes = Object.keys(queries).sort().map(queryResultTypeName);
|
|
761
|
+
const lines = [PY_HEADER, "", "from __future__ import annotations", ""];
|
|
762
|
+
if (resultTypes.length > 0) {
|
|
763
|
+
lines.push(`from .types import (`);
|
|
764
|
+
for (const t of resultTypes) lines.push(` ${t},`);
|
|
765
|
+
lines.push(`)`);
|
|
766
|
+
}
|
|
767
|
+
if (repos.length > 0) {
|
|
768
|
+
lines.push(`from .repositories import (`);
|
|
769
|
+
for (const r of repos) lines.push(` ${r},`);
|
|
770
|
+
lines.push(`)`);
|
|
771
|
+
}
|
|
772
|
+
const all = [...resultTypes, ...repos].sort();
|
|
773
|
+
lines.push("");
|
|
774
|
+
lines.push("__all__ = [");
|
|
775
|
+
for (const name of all) lines.push(` "${name}",`);
|
|
776
|
+
lines.push("]");
|
|
777
|
+
lines.push("");
|
|
778
|
+
return lines.join("\n");
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/codegen/index.ts
|
|
782
|
+
var OUTPUT_FILES = [
|
|
783
|
+
"manifest.json",
|
|
784
|
+
"operations.json",
|
|
785
|
+
"types.py",
|
|
786
|
+
"repositories.py",
|
|
787
|
+
"__init__.py"
|
|
788
|
+
];
|
|
789
|
+
function renderBundle(queries = {}, commands = {}, registry = MetadataRegistry, transactions = {}, options = {}) {
|
|
790
|
+
const bundle = buildBridgeBundle(
|
|
791
|
+
queries,
|
|
792
|
+
commands,
|
|
793
|
+
registry,
|
|
794
|
+
transactions,
|
|
795
|
+
options.contracts ?? {}
|
|
796
|
+
);
|
|
797
|
+
const txSpecs = bundle.operations.transactions ?? {};
|
|
798
|
+
return {
|
|
799
|
+
"manifest.json": JSON.stringify(bundle.manifest, null, 2) + "\n",
|
|
800
|
+
"operations.json": JSON.stringify(bundle.operations, null, 2) + "\n",
|
|
801
|
+
"types.py": generateTypes(bundle.manifest, queries, txSpecs, options),
|
|
802
|
+
"repositories.py": generateRepositories(queries, commands, txSpecs),
|
|
803
|
+
"__init__.py": generateInit(queries, commands, txSpecs)
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// src/cli/handlers.ts
|
|
808
|
+
function fail(code, message, exitCode) {
|
|
809
|
+
process.stderr.write(JSON.stringify({ code, message }) + "\n");
|
|
810
|
+
process.exit(exitCode);
|
|
811
|
+
}
|
|
812
|
+
function toUrl(file) {
|
|
813
|
+
const abs = isAbsolute(file) ? file : resolve(process.cwd(), file);
|
|
814
|
+
return pathToFileURL(abs).href;
|
|
815
|
+
}
|
|
816
|
+
async function loadDefinitionMap(file, exportName) {
|
|
817
|
+
const mod = await import(toUrl(file));
|
|
818
|
+
const value = mod[exportName];
|
|
819
|
+
if (value === void 0 || value === null) {
|
|
820
|
+
fail(
|
|
821
|
+
"INVALID_ARGS",
|
|
822
|
+
`Module '${file}' has no '${exportName}' export.`,
|
|
823
|
+
2
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
return value;
|
|
827
|
+
}
|
|
828
|
+
async function loadOptionalMap(file, exportName) {
|
|
829
|
+
const mod = await import(toUrl(file));
|
|
830
|
+
const value = mod[exportName];
|
|
831
|
+
if (value === void 0 || value === null) return {};
|
|
832
|
+
return value;
|
|
833
|
+
}
|
|
834
|
+
var handlers = {
|
|
835
|
+
async generatePython(options) {
|
|
836
|
+
const {
|
|
837
|
+
entry,
|
|
838
|
+
queries: queriesPath,
|
|
839
|
+
commands: commandsPath,
|
|
840
|
+
transactions: transactionsPath,
|
|
841
|
+
contracts: contractsPath,
|
|
842
|
+
contexts: contextsPath,
|
|
843
|
+
dataclass,
|
|
844
|
+
out
|
|
845
|
+
} = options;
|
|
846
|
+
if (!entry) fail("INVALID_ARGS", "Missing required option --entry.", 2);
|
|
847
|
+
if (!out) fail("INVALID_ARGS", "Missing required option --out.", 2);
|
|
848
|
+
try {
|
|
849
|
+
await import(toUrl(entry));
|
|
850
|
+
const queries = await loadDefinitionMap(queriesPath ?? entry, "queries");
|
|
851
|
+
const commands = await loadDefinitionMap(commandsPath ?? entry, "commands");
|
|
852
|
+
const transactions = await loadOptionalMap(
|
|
853
|
+
transactionsPath ?? commandsPath ?? entry,
|
|
854
|
+
"transactions"
|
|
855
|
+
);
|
|
856
|
+
const contracts = await loadOptionalMap(
|
|
857
|
+
contractsPath ?? entry,
|
|
858
|
+
"contracts"
|
|
859
|
+
);
|
|
860
|
+
const contexts = await loadOptionalMap(
|
|
861
|
+
contextsPath ?? contractsPath ?? entry,
|
|
862
|
+
"contexts"
|
|
863
|
+
);
|
|
864
|
+
const files = renderBundle(queries, commands, void 0, transactions, {
|
|
865
|
+
dataclass: Boolean(dataclass),
|
|
866
|
+
contracts: { contracts, contexts }
|
|
867
|
+
});
|
|
868
|
+
void dataclass;
|
|
869
|
+
const outDir = isAbsolute(out) ? out : resolve(process.cwd(), out);
|
|
870
|
+
mkdirSync(outDir, { recursive: true });
|
|
871
|
+
for (const name of OUTPUT_FILES) {
|
|
872
|
+
writeFileSync(resolve(outDir, name), files[name], "utf8");
|
|
873
|
+
}
|
|
874
|
+
process.stdout.write(
|
|
875
|
+
JSON.stringify({
|
|
876
|
+
status: "ok",
|
|
877
|
+
outDir: out,
|
|
878
|
+
files: [...OUTPUT_FILES]
|
|
879
|
+
}) + "\n"
|
|
880
|
+
);
|
|
881
|
+
} catch (err) {
|
|
882
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
883
|
+
fail("GENERATION_FAILED", message, 1);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
|
|
888
|
+
// src/cli.ts
|
|
889
|
+
var VERSION = "0.1.0";
|
|
890
|
+
createProgram(handlers, VERSION).parseAsync(process.argv);
|