graphddb 0.4.3 → 0.5.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 +12 -2
- package/dist/{chunk-CPTV3H2U.js → chunk-B3GWIWT6.js} +401 -26
- package/dist/{chunk-BROCT574.js → chunk-EMJHTUA4.js} +1 -1
- package/dist/{chunk-G5RWWBAL.js → chunk-FI63YKK5.js} +83 -153
- package/dist/cli.js +831 -9
- package/dist/index.d.ts +150 -3
- package/dist/index.js +45 -6
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +2 -2
- package/dist/{types-BWOrWcbd.d.ts → types-B9rJ1z3H.d.ts} +278 -114
- package/docs/cdc-projection.md +167 -0
- package/docs/cloudformation.md +294 -0
- package/docs/design-patterns.md +18 -13
- package/docs/spec.md +9 -4
- package/package.json +4 -2
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
buildBridgeBundle,
|
|
4
|
-
|
|
5
|
-
} from "./chunk-
|
|
4
|
+
buildManifest
|
|
5
|
+
} from "./chunk-FI63YKK5.js";
|
|
6
6
|
import {
|
|
7
|
-
MetadataRegistry
|
|
8
|
-
|
|
7
|
+
MetadataRegistry,
|
|
8
|
+
normalizeSelectSpec
|
|
9
|
+
} from "./chunk-B3GWIWT6.js";
|
|
9
10
|
|
|
10
11
|
// src/cli.ts
|
|
11
12
|
import { createRequire } from "module";
|
|
@@ -299,6 +300,146 @@ var commandDefinitions = {
|
|
|
299
300
|
}
|
|
300
301
|
}
|
|
301
302
|
]
|
|
303
|
+
},
|
|
304
|
+
"generate.cloudformation": {
|
|
305
|
+
"effects": {
|
|
306
|
+
"risk_level": "low",
|
|
307
|
+
"reads": [
|
|
308
|
+
"ts-definitions"
|
|
309
|
+
],
|
|
310
|
+
"writes": [
|
|
311
|
+
"generated-code"
|
|
312
|
+
]
|
|
313
|
+
},
|
|
314
|
+
"options": [
|
|
315
|
+
{
|
|
316
|
+
"name": "entry",
|
|
317
|
+
"schema": {
|
|
318
|
+
"type": "string"
|
|
319
|
+
},
|
|
320
|
+
"file": {
|
|
321
|
+
"mode": "read",
|
|
322
|
+
"exists": true,
|
|
323
|
+
"media_type": "application/typescript",
|
|
324
|
+
"encoding": "utf-8"
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
"name": "out",
|
|
329
|
+
"schema": {
|
|
330
|
+
"type": "string"
|
|
331
|
+
}
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
"name": "format",
|
|
335
|
+
"schema": {
|
|
336
|
+
"type": "string",
|
|
337
|
+
"enum": [
|
|
338
|
+
"yaml",
|
|
339
|
+
"json"
|
|
340
|
+
],
|
|
341
|
+
"default": "yaml"
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
{
|
|
345
|
+
"name": "gsi-projection",
|
|
346
|
+
"schema": {
|
|
347
|
+
"type": "string",
|
|
348
|
+
"enum": [
|
|
349
|
+
"ALL",
|
|
350
|
+
"KEYS_ONLY"
|
|
351
|
+
],
|
|
352
|
+
"default": "ALL"
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
"name": "stream",
|
|
357
|
+
"schema": {
|
|
358
|
+
"type": "string",
|
|
359
|
+
"enum": [
|
|
360
|
+
"auto",
|
|
361
|
+
"on",
|
|
362
|
+
"off"
|
|
363
|
+
],
|
|
364
|
+
"default": "auto"
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
"name": "billing-mode",
|
|
369
|
+
"schema": {
|
|
370
|
+
"type": "string",
|
|
371
|
+
"enum": [
|
|
372
|
+
"PAY_PER_REQUEST",
|
|
373
|
+
"PROVISIONED"
|
|
374
|
+
],
|
|
375
|
+
"default": "PAY_PER_REQUEST"
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
"name": "rcu",
|
|
380
|
+
"schema": {
|
|
381
|
+
"type": "integer",
|
|
382
|
+
"minimum": 1
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
"name": "wcu",
|
|
387
|
+
"schema": {
|
|
388
|
+
"type": "integer",
|
|
389
|
+
"minimum": 1
|
|
390
|
+
}
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
"name": "autoscale",
|
|
394
|
+
"schema": {
|
|
395
|
+
"type": "boolean"
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
"name": "autoscale-min",
|
|
400
|
+
"schema": {
|
|
401
|
+
"type": "integer",
|
|
402
|
+
"minimum": 1
|
|
403
|
+
}
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
"name": "autoscale-max",
|
|
407
|
+
"schema": {
|
|
408
|
+
"type": "integer",
|
|
409
|
+
"minimum": 1
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
"name": "autoscale-target",
|
|
414
|
+
"schema": {
|
|
415
|
+
"type": "integer",
|
|
416
|
+
"minimum": 1,
|
|
417
|
+
"maximum": 100
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
"name": "pitr",
|
|
422
|
+
"schema": {
|
|
423
|
+
"type": "boolean"
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
"name": "table-class",
|
|
428
|
+
"schema": {
|
|
429
|
+
"type": "string",
|
|
430
|
+
"enum": [
|
|
431
|
+
"STANDARD",
|
|
432
|
+
"STANDARD_INFREQUENT_ACCESS"
|
|
433
|
+
]
|
|
434
|
+
}
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
"name": "sse",
|
|
438
|
+
"schema": {
|
|
439
|
+
"type": "boolean"
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
]
|
|
302
443
|
}
|
|
303
444
|
};
|
|
304
445
|
function deriveCommandPolicy(command_id, optionValues) {
|
|
@@ -325,8 +466,8 @@ function deriveCommandPolicy(command_id, optionValues) {
|
|
|
325
466
|
}
|
|
326
467
|
|
|
327
468
|
// src/generated/contract.ts
|
|
328
|
-
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";
|
|
329
|
-
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}';
|
|
469
|
+
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\n # \u2500\u2500 generate cloudformation \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.cloudformation:\n summary: >-\n Generate an AWS CloudFormation template (DynamoDB table + GSIs) from\n TypeScript GraphDDB definitions.\n description: >-\n Loads the TypeScript entity models and builds the serializable\n manifest, then emits an AWS CloudFormation template describing the\n DynamoDB table(s) the single-table design maps onto: one\n `AWS::DynamoDB::Table` per physical table, with the base key schema\n (`PK` / `SK`), the unioned Global Secondary Indexes\n (`<indexName>PK` / `<indexName>SK`), `PAY_PER_REQUEST` billing, a\n `TableName` parameter (default = physical name), and Outputs\n (table name + ARN). Output is deterministic and, by default, YAML.\n usage:\n - graphddb generate cloudformation --entry models.ts --out table.yaml\n - graphddb generate cloudformation --entry models.ts --format json\n - graphddb generate cloudformation --entry models.ts --gsi-projection KEYS_ONLY --out table.yaml\n - graphddb generate cloudformation --entry models.ts --stream on --out table.yaml\n - graphddb generate cloudformation --entry models.ts --pitr --sse --table-class STANDARD_INFREQUENT_ACCESS --out table.yaml\n - graphddb generate cloudformation --entry models.ts --billing-mode PROVISIONED --rcu 10 --wcu 5 --out table.yaml\n - graphddb generate cloudformation --entry models.ts --billing-mode PROVISIONED --autoscale --autoscale-min 5 --autoscale-max 100 --autoscale-target 70 --out table.yaml\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).\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: out\n aliases: [o]\n description: >-\n Output file for the generated CloudFormation template. Written to\n stdout when omitted.\n value_name: file\n schema:\n type: string\n\n - name: format\n aliases: [f]\n description: Output serialization format (yaml or json).\n value_name: format\n schema:\n type: string\n enum: [yaml, json]\n default: yaml\n\n - name: gsi-projection\n description: >-\n GSI ProjectionType. ALL (default) projects every attribute;\n KEYS_ONLY projects only the key attributes.\n value_name: projection\n schema:\n type: string\n enum: [ALL, KEYS_ONLY]\n default: ALL\n\n - name: stream\n description: >-\n DynamoDB Streams (StreamSpecification NEW_AND_OLD_IMAGES) emission.\n auto (default) emits a stream only when the model set uses\n stream-based maintenance (updateMode:'stream'); on always emits;\n off never emits. When enabled, a StreamArn Output is added too.\n value_name: mode\n schema:\n type: string\n enum: [auto, on, off]\n default: auto\n\n - name: billing-mode\n description: >-\n DynamoDB billing mode. PAY_PER_REQUEST (default) is on-demand\n (no provisioned throughput). PROVISIONED emits BillingMode:\n PROVISIONED with ProvisionedThroughput (see --rcu / --wcu) on the\n table and every GSI, and unlocks Application Auto Scaling\n (--autoscale).\n value_name: mode\n schema:\n type: string\n enum: [PAY_PER_REQUEST, PROVISIONED]\n default: PAY_PER_REQUEST\n\n - name: rcu\n description: >-\n Provisioned ReadCapacityUnits (a positive integer) applied to the\n table and every GSI. Only valid with --billing-mode PROVISIONED\n (rejected with PAY_PER_REQUEST). Defaults to 5 when omitted.\n value_name: units\n schema:\n type: integer\n minimum: 1\n\n - name: wcu\n description: >-\n Provisioned WriteCapacityUnits (a positive integer) applied to the\n table and every GSI. Only valid with --billing-mode PROVISIONED\n (rejected with PAY_PER_REQUEST). Defaults to 5 when omitted.\n value_name: units\n schema:\n type: integer\n minimum: 1\n\n - name: autoscale\n description: >-\n Enable Application Auto Scaling. Emits an\n AWS::ApplicationAutoScaling::ScalableTarget +\n AWS::ApplicationAutoScaling::ScalingPolicy pair for the table read\n and write capacity AND for each GSI read and write capacity. Only\n valid with --billing-mode PROVISIONED (autoscaling is invalid on\n on-demand and is rejected). Off by default \u2014 no autoscaling\n resources are emitted unless this flag is passed.\n schema:\n type: boolean\n\n - name: autoscale-min\n description: >-\n Auto Scaling minimum capacity (MinCapacity) for every scalable\n target. Must be <= --autoscale-max. Only meaningful with\n --autoscale. Defaults to 5 when omitted.\n value_name: units\n schema:\n type: integer\n minimum: 1\n\n - name: autoscale-max\n description: >-\n Auto Scaling maximum capacity (MaxCapacity) for every scalable\n target. Must be >= --autoscale-min. Only meaningful with\n --autoscale. Defaults to 100 when omitted.\n value_name: units\n schema:\n type: integer\n minimum: 1\n\n - name: autoscale-target\n description: >-\n Auto Scaling target utilization percentage (TargetValue of the\n TargetTrackingScaling policy). Only meaningful with --autoscale.\n Defaults to 70 when omitted.\n value_name: percent\n schema:\n type: integer\n minimum: 1\n maximum: 100\n\n - name: pitr\n description: >-\n Enable point-in-time recovery (PointInTimeRecoverySpecification\n PointInTimeRecoveryEnabled:true). Off by default \u2014 the property is\n omitted entirely unless this flag is passed.\n schema:\n type: boolean\n\n - name: table-class\n description: >-\n DynamoDB table class. Omitted by default (AWS defaults to\n STANDARD); pass STANDARD or STANDARD_INFREQUENT_ACCESS to emit\n TableClass.\n value_name: class\n schema:\n type: string\n enum: [STANDARD, STANDARD_INFREQUENT_ACCESS]\n\n - name: sse\n description: >-\n Enable server-side encryption with the AWS-owned/managed key\n (SSESpecification SSEEnabled:true). Off by default \u2014 the property is\n omitted entirely unless this flag is passed.\n schema:\n type: boolean\n\n exits:\n '0':\n description: Generation succeeded.\n stdout:\n format: json\n schema:\n $ref: '#/components/schemas/GenerateResult'\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";
|
|
470
|
+
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 "generate.cloudformation": {\n "summary": "Generate an AWS CloudFormation template (DynamoDB table + GSIs) from TypeScript GraphDDB definitions.",\n "description": "Loads the TypeScript entity models and builds the serializable manifest, then emits an AWS CloudFormation template describing the DynamoDB table(s) the single-table design maps onto: one `AWS::DynamoDB::Table` per physical table, with the base key schema (`PK` / `SK`), the unioned Global Secondary Indexes (`<indexName>PK` / `<indexName>SK`), `PAY_PER_REQUEST` billing, a `TableName` parameter (default = physical name), and Outputs (table name + ARN). Output is deterministic and, by default, YAML.",\n "usage": [\n "graphddb generate cloudformation --entry models.ts --out table.yaml",\n "graphddb generate cloudformation --entry models.ts --format json",\n "graphddb generate cloudformation --entry models.ts --gsi-projection KEYS_ONLY --out table.yaml",\n "graphddb generate cloudformation --entry models.ts --stream on --out table.yaml",\n "graphddb generate cloudformation --entry models.ts --pitr --sse --table-class STANDARD_INFREQUENT_ACCESS --out table.yaml",\n "graphddb generate cloudformation --entry models.ts --billing-mode PROVISIONED --rcu 10 --wcu 5 --out table.yaml",\n "graphddb generate cloudformation --entry models.ts --billing-mode PROVISIONED --autoscale --autoscale-min 5 --autoscale-max 100 --autoscale-target 70 --out table.yaml"\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).",\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": "out",\n "aliases": [\n "o"\n ],\n "description": "Output file for the generated CloudFormation template. Written to stdout when omitted.",\n "value_name": "file",\n "schema": {\n "type": "string"\n }\n },\n {\n "name": "format",\n "aliases": [\n "f"\n ],\n "description": "Output serialization format (yaml or json).",\n "value_name": "format",\n "schema": {\n "type": "string",\n "enum": [\n "yaml",\n "json"\n ],\n "default": "yaml"\n }\n },\n {\n "name": "gsi-projection",\n "description": "GSI ProjectionType. ALL (default) projects every attribute; KEYS_ONLY projects only the key attributes.",\n "value_name": "projection",\n "schema": {\n "type": "string",\n "enum": [\n "ALL",\n "KEYS_ONLY"\n ],\n "default": "ALL"\n }\n },\n {\n "name": "stream",\n "description": "DynamoDB Streams (StreamSpecification NEW_AND_OLD_IMAGES) emission. auto (default) emits a stream only when the model set uses stream-based maintenance (updateMode:\'stream\'); on always emits; off never emits. When enabled, a StreamArn Output is added too.",\n "value_name": "mode",\n "schema": {\n "type": "string",\n "enum": [\n "auto",\n "on",\n "off"\n ],\n "default": "auto"\n }\n },\n {\n "name": "billing-mode",\n "description": "DynamoDB billing mode. PAY_PER_REQUEST (default) is on-demand (no provisioned throughput). PROVISIONED emits BillingMode: PROVISIONED with ProvisionedThroughput (see --rcu / --wcu) on the table and every GSI, and unlocks Application Auto Scaling (--autoscale).",\n "value_name": "mode",\n "schema": {\n "type": "string",\n "enum": [\n "PAY_PER_REQUEST",\n "PROVISIONED"\n ],\n "default": "PAY_PER_REQUEST"\n }\n },\n {\n "name": "rcu",\n "description": "Provisioned ReadCapacityUnits (a positive integer) applied to the table and every GSI. Only valid with --billing-mode PROVISIONED (rejected with PAY_PER_REQUEST). Defaults to 5 when omitted.",\n "value_name": "units",\n "schema": {\n "type": "integer",\n "minimum": 1\n }\n },\n {\n "name": "wcu",\n "description": "Provisioned WriteCapacityUnits (a positive integer) applied to the table and every GSI. Only valid with --billing-mode PROVISIONED (rejected with PAY_PER_REQUEST). Defaults to 5 when omitted.",\n "value_name": "units",\n "schema": {\n "type": "integer",\n "minimum": 1\n }\n },\n {\n "name": "autoscale",\n "description": "Enable Application Auto Scaling. Emits an AWS::ApplicationAutoScaling::ScalableTarget + AWS::ApplicationAutoScaling::ScalingPolicy pair for the table read and write capacity AND for each GSI read and write capacity. Only valid with --billing-mode PROVISIONED (autoscaling is invalid on on-demand and is rejected). Off by default \u2014 no autoscaling resources are emitted unless this flag is passed.",\n "schema": {\n "type": "boolean"\n }\n },\n {\n "name": "autoscale-min",\n "description": "Auto Scaling minimum capacity (MinCapacity) for every scalable target. Must be <= --autoscale-max. Only meaningful with --autoscale. Defaults to 5 when omitted.",\n "value_name": "units",\n "schema": {\n "type": "integer",\n "minimum": 1\n }\n },\n {\n "name": "autoscale-max",\n "description": "Auto Scaling maximum capacity (MaxCapacity) for every scalable target. Must be >= --autoscale-min. Only meaningful with --autoscale. Defaults to 100 when omitted.",\n "value_name": "units",\n "schema": {\n "type": "integer",\n "minimum": 1\n }\n },\n {\n "name": "autoscale-target",\n "description": "Auto Scaling target utilization percentage (TargetValue of the TargetTrackingScaling policy). Only meaningful with --autoscale. Defaults to 70 when omitted.",\n "value_name": "percent",\n "schema": {\n "type": "integer",\n "minimum": 1,\n "maximum": 100\n }\n },\n {\n "name": "pitr",\n "description": "Enable point-in-time recovery (PointInTimeRecoverySpecification PointInTimeRecoveryEnabled:true). Off by default \u2014 the property is omitted entirely unless this flag is passed.",\n "schema": {\n "type": "boolean"\n }\n },\n {\n "name": "table-class",\n "description": "DynamoDB table class. Omitted by default (AWS defaults to STANDARD); pass STANDARD or STANDARD_INFREQUENT_ACCESS to emit TableClass.",\n "value_name": "class",\n "schema": {\n "type": "string",\n "enum": [\n "STANDARD",\n "STANDARD_INFREQUENT_ACCESS"\n ]\n }\n },\n {\n "name": "sse",\n "description": "Enable server-side encryption with the AWS-owned/managed key (SSESpecification SSEEnabled:true). Off by default \u2014 the property is omitted entirely unless this flag is passed.",\n "schema": {\n "type": "boolean"\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 },\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}';
|
|
330
471
|
|
|
331
472
|
// src/generated/program.ts
|
|
332
473
|
function createProgram(handlers2, version) {
|
|
@@ -343,6 +484,15 @@ function createProgram(handlers2, version) {
|
|
|
343
484
|
}
|
|
344
485
|
await handlers2.generatePython(opts, globalOpts);
|
|
345
486
|
});
|
|
487
|
+
__cmd_generate.command("cloudformation").description("Generate an AWS CloudFormation template (DynamoDB table + GSIs) from TypeScript GraphDDB definitions.").option("-e, --entry <file>", "Entry module that registers the entity models (side-effecting import).").option("-o, --out <file>", "Output file for the generated CloudFormation template. Written to stdout when omitted.").option("-f, --format <format>", "Output serialization format (yaml or json).", "yaml").option("--gsi-projection <projection>", "GSI ProjectionType. ALL (default) projects every attribute; KEYS_ONLY projects only the key attributes.", "ALL").option("--stream <mode>", "DynamoDB Streams (StreamSpecification NEW_AND_OLD_IMAGES) emission. auto (default) emits a stream only when the model set uses stream-based maintenance (updateMode:'stream'); on always emits; off never emits. When enabled, a StreamArn Output is added too.", "auto").option("--billing-mode <mode>", "DynamoDB billing mode. PAY_PER_REQUEST (default) is on-demand (no provisioned throughput). PROVISIONED emits BillingMode: PROVISIONED with ProvisionedThroughput (see --rcu / --wcu) on the table and every GSI, and unlocks Application Auto Scaling (--autoscale).", "PAY_PER_REQUEST").option("--rcu <units>", "Provisioned ReadCapacityUnits (a positive integer) applied to the table and every GSI. Only valid with --billing-mode PROVISIONED (rejected with PAY_PER_REQUEST). Defaults to 5 when omitted.").option("--wcu <units>", "Provisioned WriteCapacityUnits (a positive integer) applied to the table and every GSI. Only valid with --billing-mode PROVISIONED (rejected with PAY_PER_REQUEST). Defaults to 5 when omitted.").option("--autoscale", "Enable Application Auto Scaling. Emits an AWS::ApplicationAutoScaling::ScalableTarget + AWS::ApplicationAutoScaling::ScalingPolicy pair for the table read and write capacity AND for each GSI read and write capacity. Only valid with --billing-mode PROVISIONED (autoscaling is invalid on on-demand and is rejected). Off by default \u2014 no autoscaling resources are emitted unless this flag is passed.").option("--autoscale-min <units>", "Auto Scaling minimum capacity (MinCapacity) for every scalable target. Must be <= --autoscale-max. Only meaningful with --autoscale. Defaults to 5 when omitted.").option("--autoscale-max <units>", "Auto Scaling maximum capacity (MaxCapacity) for every scalable target. Must be >= --autoscale-min. Only meaningful with --autoscale. Defaults to 100 when omitted.").option("--autoscale-target <percent>", "Auto Scaling target utilization percentage (TargetValue of the TargetTrackingScaling policy). Only meaningful with --autoscale. Defaults to 70 when omitted.").option("--pitr", "Enable point-in-time recovery (PointInTimeRecoverySpecification PointInTimeRecoveryEnabled:true). Off by default \u2014 the property is omitted entirely unless this flag is passed.").option("--table-class <class>", "DynamoDB table class. Omitted by default (AWS defaults to STANDARD); pass STANDARD or STANDARD_INFREQUENT_ACCESS to emit TableClass.").option("--sse", "Enable server-side encryption with the AWS-owned/managed key (SSESpecification SSEEnabled:true). Off by default \u2014 the property is omitted entirely unless this flag is passed.").action(async (opts, cmd) => {
|
|
488
|
+
const globalOpts = cmd.optsWithGlobals();
|
|
489
|
+
if (globalOpts.introspect) {
|
|
490
|
+
const policy = deriveCommandPolicy("generate.cloudformation", opts);
|
|
491
|
+
console.log(JSON.stringify(policy, null, 2));
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
await handlers2.generateCloudformation(opts, globalOpts);
|
|
495
|
+
});
|
|
346
496
|
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) => {
|
|
347
497
|
if (commands.length === 0 && !opts.all) {
|
|
348
498
|
process.stderr.write(JSON.stringify({ code: "INVALID_ARGS", message: "Specify command IDs or use --all" }) + "\n");
|
|
@@ -360,7 +510,7 @@ function createProgram(handlers2, version) {
|
|
|
360
510
|
type: "cli-contracts/extract",
|
|
361
511
|
extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
362
512
|
specVersion: doc.cli_contracts ?? "0.1.0",
|
|
363
|
-
commands: ["graphddb.generate.python"]
|
|
513
|
+
commands: ["graphddb.generate.python", "graphddb.generate.cloudformation"]
|
|
364
514
|
};
|
|
365
515
|
}
|
|
366
516
|
Object.assign(out, doc);
|
|
@@ -377,7 +527,7 @@ function createProgram(handlers2, version) {
|
|
|
377
527
|
yamlLines.push("extractedAt: " + (/* @__PURE__ */ new Date()).toISOString());
|
|
378
528
|
yamlLines.push("spec_version: " + (doc.cli_contracts ?? "0.1.0"));
|
|
379
529
|
yamlLines.push("commands:");
|
|
380
|
-
for (const id of ["graphddb.generate.python"]) {
|
|
530
|
+
for (const id of ["graphddb.generate.python", "graphddb.generate.cloudformation"]) {
|
|
381
531
|
yamlLines.push(" - " + id);
|
|
382
532
|
}
|
|
383
533
|
}
|
|
@@ -418,6 +568,7 @@ function createProgram(handlers2, version) {
|
|
|
418
568
|
|
|
419
569
|
// src/cli/handlers.ts
|
|
420
570
|
import { mkdirSync, writeFileSync } from "fs";
|
|
571
|
+
import { dirname, relative } from "path";
|
|
421
572
|
import { pathToFileURL } from "url";
|
|
422
573
|
import { isAbsolute, resolve } from "path";
|
|
423
574
|
|
|
@@ -818,6 +969,487 @@ function generateInit(queries, commands, transactions = {}) {
|
|
|
818
969
|
return lines.join("\n");
|
|
819
970
|
}
|
|
820
971
|
|
|
972
|
+
// src/codegen/cloudformation.ts
|
|
973
|
+
var DEFAULT_CAPACITY = 5;
|
|
974
|
+
var DEFAULT_AUTOSCALE_MIN = 5;
|
|
975
|
+
var DEFAULT_AUTOSCALE_MAX = 100;
|
|
976
|
+
var DEFAULT_AUTOSCALE_TARGET = 70;
|
|
977
|
+
var AUTOSCALING_SERVICE_LINKED_ROLE_ARN = {
|
|
978
|
+
"Fn::Sub": "arn:aws:iam::${AWS::AccountId}:role/aws-service-role/dynamodb.application-autoscaling.amazonaws.com/AWSServiceRoleForApplicationAutoScaling_DynamoDBTable"
|
|
979
|
+
};
|
|
980
|
+
var STREAM_VIEW_TYPE = "NEW_AND_OLD_IMAGES";
|
|
981
|
+
var BASE_PK = "PK";
|
|
982
|
+
var BASE_SK = "SK";
|
|
983
|
+
function gsiPkAttr(indexName) {
|
|
984
|
+
return `${indexName}PK`;
|
|
985
|
+
}
|
|
986
|
+
function gsiSkAttr(indexName) {
|
|
987
|
+
return `${indexName}SK`;
|
|
988
|
+
}
|
|
989
|
+
function gsiHasSk(gsi) {
|
|
990
|
+
return gsi.skTemplate !== null;
|
|
991
|
+
}
|
|
992
|
+
function unionGsis(entities) {
|
|
993
|
+
const byName = /* @__PURE__ */ new Map();
|
|
994
|
+
for (const entity of entities) {
|
|
995
|
+
for (const gsi of entity.gsis) {
|
|
996
|
+
const hasSk = gsiHasSk(gsi);
|
|
997
|
+
const existing = byName.get(gsi.indexName);
|
|
998
|
+
if (existing) {
|
|
999
|
+
const existingHasSk = existing.skAttr !== null;
|
|
1000
|
+
if (existingHasSk !== hasSk) {
|
|
1001
|
+
throw new Error(
|
|
1002
|
+
`CloudFormation generation: GSI '${gsi.indexName}' on table '${entity.table}' is declared with an inconsistent sort key across entities (some with a sort key, some without). A single DynamoDB table has exactly one GSI per index name; align the sort-key presence for '${gsi.indexName}' across all entities sharing this table.`
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
byName.set(gsi.indexName, {
|
|
1008
|
+
indexName: gsi.indexName,
|
|
1009
|
+
pkAttr: gsiPkAttr(gsi.indexName),
|
|
1010
|
+
skAttr: hasSk ? gsiSkAttr(gsi.indexName) : null
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
return [...byName.values()].sort(
|
|
1015
|
+
(a, b) => a.indexName.localeCompare(b.indexName)
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
function resolveTableTtl(entities, logicalName) {
|
|
1019
|
+
let ttl = null;
|
|
1020
|
+
for (const entity of entities) {
|
|
1021
|
+
if (entity.ttlAttribute === void 0) continue;
|
|
1022
|
+
if (ttl !== null && ttl !== entity.ttlAttribute) {
|
|
1023
|
+
throw new Error(
|
|
1024
|
+
`CloudFormation generation: table '${logicalName}' has more than one TTL attribute ('${ttl}' and '${entity.ttlAttribute}') across the entities sharing it. DynamoDB allows exactly one TTL attribute per table; align the \`@ttl\` attribute across all entities on this table.`
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
ttl = entity.ttlAttribute;
|
|
1028
|
+
}
|
|
1029
|
+
return ttl;
|
|
1030
|
+
}
|
|
1031
|
+
function buildTableSpecs(manifest) {
|
|
1032
|
+
const entitiesByLogical = /* @__PURE__ */ new Map();
|
|
1033
|
+
for (const name of Object.keys(manifest.entities).sort()) {
|
|
1034
|
+
const entity = manifest.entities[name];
|
|
1035
|
+
const list = entitiesByLogical.get(entity.table) ?? [];
|
|
1036
|
+
list.push(entity);
|
|
1037
|
+
entitiesByLogical.set(entity.table, list);
|
|
1038
|
+
}
|
|
1039
|
+
const specs = [];
|
|
1040
|
+
for (const logicalName of [...entitiesByLogical.keys()].sort()) {
|
|
1041
|
+
const entities = entitiesByLogical.get(logicalName);
|
|
1042
|
+
const physicalName = manifest.tables[logicalName]?.physicalName ?? logicalName;
|
|
1043
|
+
const hasSk = entities.some((e) => e.key?.skTemplate != null);
|
|
1044
|
+
const gsis = unionGsis(entities);
|
|
1045
|
+
const ttlAttribute = resolveTableTtl(entities, logicalName);
|
|
1046
|
+
specs.push({ logicalName, physicalName, hasSk, gsis, ttlAttribute });
|
|
1047
|
+
}
|
|
1048
|
+
return specs;
|
|
1049
|
+
}
|
|
1050
|
+
function tableLogicalId(logicalName) {
|
|
1051
|
+
const cleaned = logicalName.replace(/[^A-Za-z0-9]/g, "");
|
|
1052
|
+
return `${cleaned}Table`;
|
|
1053
|
+
}
|
|
1054
|
+
function buildAttributeDefinitions(spec) {
|
|
1055
|
+
const names = [BASE_PK];
|
|
1056
|
+
if (spec.hasSk) names.push(BASE_SK);
|
|
1057
|
+
for (const gsi of spec.gsis) {
|
|
1058
|
+
names.push(gsi.pkAttr);
|
|
1059
|
+
if (gsi.skAttr) names.push(gsi.skAttr);
|
|
1060
|
+
}
|
|
1061
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1062
|
+
const out = [];
|
|
1063
|
+
for (const name of names) {
|
|
1064
|
+
if (seen.has(name)) continue;
|
|
1065
|
+
seen.add(name);
|
|
1066
|
+
out.push({ AttributeName: name, AttributeType: "S" });
|
|
1067
|
+
}
|
|
1068
|
+
return out;
|
|
1069
|
+
}
|
|
1070
|
+
function buildKeySchema(spec) {
|
|
1071
|
+
const schema = [{ AttributeName: BASE_PK, KeyType: "HASH" }];
|
|
1072
|
+
if (spec.hasSk) schema.push({ AttributeName: BASE_SK, KeyType: "RANGE" });
|
|
1073
|
+
return schema;
|
|
1074
|
+
}
|
|
1075
|
+
function buildGsiResource(gsi, projectionType, throughput) {
|
|
1076
|
+
const keySchema = [
|
|
1077
|
+
{ AttributeName: gsi.pkAttr, KeyType: "HASH" }
|
|
1078
|
+
];
|
|
1079
|
+
if (gsi.skAttr) {
|
|
1080
|
+
keySchema.push({ AttributeName: gsi.skAttr, KeyType: "RANGE" });
|
|
1081
|
+
}
|
|
1082
|
+
const resource = {
|
|
1083
|
+
IndexName: gsi.indexName,
|
|
1084
|
+
KeySchema: keySchema,
|
|
1085
|
+
Projection: { ProjectionType: projectionType }
|
|
1086
|
+
};
|
|
1087
|
+
if (throughput !== null) {
|
|
1088
|
+
resource.ProvisionedThroughput = {
|
|
1089
|
+
ReadCapacityUnits: throughput.rcu,
|
|
1090
|
+
WriteCapacityUnits: throughput.wcu
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
return resource;
|
|
1094
|
+
}
|
|
1095
|
+
function buildTableResource(spec, options) {
|
|
1096
|
+
const properties = {
|
|
1097
|
+
TableName: { Ref: "TableName" },
|
|
1098
|
+
BillingMode: options.billingMode,
|
|
1099
|
+
AttributeDefinitions: buildAttributeDefinitions(spec),
|
|
1100
|
+
KeySchema: buildKeySchema(spec)
|
|
1101
|
+
};
|
|
1102
|
+
if (options.throughput !== null) {
|
|
1103
|
+
properties.ProvisionedThroughput = {
|
|
1104
|
+
ReadCapacityUnits: options.throughput.rcu,
|
|
1105
|
+
WriteCapacityUnits: options.throughput.wcu
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
if (spec.gsis.length > 0) {
|
|
1109
|
+
properties.GlobalSecondaryIndexes = spec.gsis.map(
|
|
1110
|
+
(gsi) => buildGsiResource(gsi, options.gsiProjection, options.throughput)
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
if (options.streamsEnabled) {
|
|
1114
|
+
properties.StreamSpecification = { StreamViewType: STREAM_VIEW_TYPE };
|
|
1115
|
+
}
|
|
1116
|
+
if (spec.ttlAttribute !== null) {
|
|
1117
|
+
properties.TimeToLiveSpecification = {
|
|
1118
|
+
AttributeName: spec.ttlAttribute,
|
|
1119
|
+
Enabled: true
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
if (options.pitr) {
|
|
1123
|
+
properties.PointInTimeRecoverySpecification = {
|
|
1124
|
+
PointInTimeRecoveryEnabled: true
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
if (options.tableClass !== null) {
|
|
1128
|
+
properties.TableClass = options.tableClass;
|
|
1129
|
+
}
|
|
1130
|
+
if (options.sse) {
|
|
1131
|
+
properties.SSESpecification = { SSEEnabled: true };
|
|
1132
|
+
}
|
|
1133
|
+
return {
|
|
1134
|
+
Type: "AWS::DynamoDB::Table",
|
|
1135
|
+
Properties: properties
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
function gsiLogicalFragment(indexName) {
|
|
1139
|
+
return indexName.replace(/[^A-Za-z0-9]/g, "");
|
|
1140
|
+
}
|
|
1141
|
+
function scalableDimension(kind, isGsi) {
|
|
1142
|
+
const scope = isGsi ? "index" : "table";
|
|
1143
|
+
return `dynamodb:${scope}:${kind}CapacityUnits`;
|
|
1144
|
+
}
|
|
1145
|
+
function predefinedMetricType(kind) {
|
|
1146
|
+
return `DynamoDB${kind}CapacityUtilization`;
|
|
1147
|
+
}
|
|
1148
|
+
function autoscaleResourceId(tableLogicalId2, indexName) {
|
|
1149
|
+
const path = indexName === null ? `table/\${${tableLogicalId2}}` : `table/\${${tableLogicalId2}}/index/${indexName}`;
|
|
1150
|
+
return { "Fn::Sub": path };
|
|
1151
|
+
}
|
|
1152
|
+
function addAutoscalePair(out, tableLogicalId2, indexName, kind, settings) {
|
|
1153
|
+
const isGsi = indexName !== null;
|
|
1154
|
+
const idBase = isGsi ? `${tableLogicalId2}${gsiLogicalFragment(indexName)}${kind}` : `${tableLogicalId2}${kind}`;
|
|
1155
|
+
const targetId = `${idBase}ScalableTarget`;
|
|
1156
|
+
const policyId = `${idBase}ScalingPolicy`;
|
|
1157
|
+
const targetProps = {
|
|
1158
|
+
MaxCapacity: settings.max,
|
|
1159
|
+
MinCapacity: settings.min,
|
|
1160
|
+
ResourceId: autoscaleResourceId(tableLogicalId2, indexName),
|
|
1161
|
+
RoleARN: AUTOSCALING_SERVICE_LINKED_ROLE_ARN,
|
|
1162
|
+
ScalableDimension: scalableDimension(kind, isGsi),
|
|
1163
|
+
ServiceNamespace: "dynamodb"
|
|
1164
|
+
};
|
|
1165
|
+
const target = {
|
|
1166
|
+
Type: "AWS::ApplicationAutoScaling::ScalableTarget",
|
|
1167
|
+
// The table must exist before its (or its GSI's) capacity can be tracked.
|
|
1168
|
+
// The Fn::Sub ResourceId already creates an implicit dependency on the
|
|
1169
|
+
// table's Ref, but we make ordering explicit — critical for a GSI, whose
|
|
1170
|
+
// index must be live before a `dynamodb:index:...` target is created.
|
|
1171
|
+
DependsOn: [tableLogicalId2],
|
|
1172
|
+
Properties: targetProps
|
|
1173
|
+
};
|
|
1174
|
+
const policy = {
|
|
1175
|
+
Type: "AWS::ApplicationAutoScaling::ScalingPolicy",
|
|
1176
|
+
Properties: {
|
|
1177
|
+
PolicyName: policyId,
|
|
1178
|
+
PolicyType: "TargetTrackingScaling",
|
|
1179
|
+
ScalingTargetId: { Ref: targetId },
|
|
1180
|
+
TargetTrackingScalingPolicyConfiguration: {
|
|
1181
|
+
TargetValue: settings.target,
|
|
1182
|
+
PredefinedMetricSpecification: {
|
|
1183
|
+
PredefinedMetricType: predefinedMetricType(kind)
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
};
|
|
1188
|
+
out[targetId] = target;
|
|
1189
|
+
out[policyId] = policy;
|
|
1190
|
+
}
|
|
1191
|
+
function addAutoscalingResources(out, tableLogicalId2, spec, settings) {
|
|
1192
|
+
for (const kind of ["Read", "Write"]) {
|
|
1193
|
+
addAutoscalePair(out, tableLogicalId2, null, kind, settings);
|
|
1194
|
+
}
|
|
1195
|
+
for (const gsi of spec.gsis) {
|
|
1196
|
+
for (const kind of ["Read", "Write"]) {
|
|
1197
|
+
addAutoscalePair(out, tableLogicalId2, gsi.indexName, kind, settings);
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
function entityUsesStreamMaintenance(metadata) {
|
|
1202
|
+
for (const rel of metadata.relations) {
|
|
1203
|
+
if (rel.options?.write?.updateMode === "stream") return true;
|
|
1204
|
+
if (rel.options?.versioned?.updateMode === "stream") return true;
|
|
1205
|
+
}
|
|
1206
|
+
for (const agg of metadata.aggregates) {
|
|
1207
|
+
if (agg.options.write?.updateMode === "stream") return true;
|
|
1208
|
+
}
|
|
1209
|
+
for (const from of metadata.maintainedFrom ?? []) {
|
|
1210
|
+
if (from.updateMode === "stream") return true;
|
|
1211
|
+
}
|
|
1212
|
+
return false;
|
|
1213
|
+
}
|
|
1214
|
+
function detectStreamMaintenance(registry = MetadataRegistry) {
|
|
1215
|
+
for (const [, metadata] of registry.getAll()) {
|
|
1216
|
+
if (entityUsesStreamMaintenance(metadata)) return true;
|
|
1217
|
+
}
|
|
1218
|
+
return false;
|
|
1219
|
+
}
|
|
1220
|
+
function resolveStreamsEnabled(mode, streamMaintenanceDetected) {
|
|
1221
|
+
if (mode === "on") return true;
|
|
1222
|
+
if (mode === "off") return false;
|
|
1223
|
+
return streamMaintenanceDetected;
|
|
1224
|
+
}
|
|
1225
|
+
function buildCloudformationTemplate(manifest, options = {}) {
|
|
1226
|
+
const billingMode = options.billingMode ?? "PAY_PER_REQUEST";
|
|
1227
|
+
const provisioned = billingMode === "PROVISIONED";
|
|
1228
|
+
const rcuGiven = options.rcu !== void 0;
|
|
1229
|
+
const wcuGiven = options.wcu !== void 0;
|
|
1230
|
+
if (!provisioned && (rcuGiven || wcuGiven)) {
|
|
1231
|
+
throw new Error(
|
|
1232
|
+
`CloudFormation generation: --rcu/--wcu require --billing-mode PROVISIONED. PAY_PER_REQUEST (on-demand) tables have no provisioned capacity; drop --rcu/--wcu or switch to PROVISIONED.`
|
|
1233
|
+
);
|
|
1234
|
+
}
|
|
1235
|
+
if (!provisioned && options.autoscale) {
|
|
1236
|
+
throw new Error(
|
|
1237
|
+
`CloudFormation generation: --autoscale requires --billing-mode PROVISIONED. Application Auto Scaling is invalid on a PAY_PER_REQUEST (on-demand) table; switch to PROVISIONED or drop --autoscale.`
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
const throughput = provisioned ? {
|
|
1241
|
+
rcu: options.rcu ?? DEFAULT_CAPACITY,
|
|
1242
|
+
wcu: options.wcu ?? DEFAULT_CAPACITY
|
|
1243
|
+
} : null;
|
|
1244
|
+
const autoscaleSettings = options.autoscale ? {
|
|
1245
|
+
min: options.autoscaleMin ?? DEFAULT_AUTOSCALE_MIN,
|
|
1246
|
+
max: options.autoscaleMax ?? DEFAULT_AUTOSCALE_MAX,
|
|
1247
|
+
target: options.autoscaleTarget ?? DEFAULT_AUTOSCALE_TARGET
|
|
1248
|
+
} : null;
|
|
1249
|
+
if (autoscaleSettings !== null && autoscaleSettings.min > autoscaleSettings.max) {
|
|
1250
|
+
throw new Error(
|
|
1251
|
+
`CloudFormation generation: --autoscale-min (${autoscaleSettings.min}) must be <= --autoscale-max (${autoscaleSettings.max}).`
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
const gsiProjection = options.gsiProjection ?? "ALL";
|
|
1255
|
+
const streamsEnabled = resolveStreamsEnabled(
|
|
1256
|
+
options.stream ?? "auto",
|
|
1257
|
+
options.streamMaintenanceDetected ?? false
|
|
1258
|
+
);
|
|
1259
|
+
const pitr = options.pitr ?? false;
|
|
1260
|
+
const tableClass = options.tableClass ?? null;
|
|
1261
|
+
const sse = options.sse ?? false;
|
|
1262
|
+
const c5Options = { pitr, tableClass, sse };
|
|
1263
|
+
const specs = buildTableSpecs(manifest);
|
|
1264
|
+
const parameters = {};
|
|
1265
|
+
const resources = {};
|
|
1266
|
+
const outputs = {};
|
|
1267
|
+
if (specs.length === 1) {
|
|
1268
|
+
const spec = specs[0];
|
|
1269
|
+
parameters.TableName = {
|
|
1270
|
+
Type: "String",
|
|
1271
|
+
Default: spec.physicalName,
|
|
1272
|
+
Description: `Name of the ${spec.logicalName} DynamoDB table.`
|
|
1273
|
+
};
|
|
1274
|
+
const logicalId = tableLogicalId(spec.logicalName);
|
|
1275
|
+
resources[logicalId] = buildTableResource(spec, {
|
|
1276
|
+
gsiProjection,
|
|
1277
|
+
billingMode,
|
|
1278
|
+
streamsEnabled,
|
|
1279
|
+
throughput,
|
|
1280
|
+
...c5Options
|
|
1281
|
+
});
|
|
1282
|
+
if (autoscaleSettings !== null) {
|
|
1283
|
+
addAutoscalingResources(resources, logicalId, spec, autoscaleSettings);
|
|
1284
|
+
}
|
|
1285
|
+
outputs.TableName = {
|
|
1286
|
+
Description: `Name of the ${spec.logicalName} DynamoDB table.`,
|
|
1287
|
+
Value: { Ref: logicalId }
|
|
1288
|
+
};
|
|
1289
|
+
outputs.TableArn = {
|
|
1290
|
+
Description: `ARN of the ${spec.logicalName} DynamoDB table.`,
|
|
1291
|
+
Value: { "Fn::GetAtt": [logicalId, "Arn"] }
|
|
1292
|
+
};
|
|
1293
|
+
if (streamsEnabled) {
|
|
1294
|
+
outputs.StreamArn = {
|
|
1295
|
+
Description: `DynamoDB Streams ARN of the ${spec.logicalName} table.`,
|
|
1296
|
+
Value: { "Fn::GetAtt": [logicalId, "StreamArn"] }
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
} else {
|
|
1300
|
+
for (const spec of specs) {
|
|
1301
|
+
const logicalId = tableLogicalId(spec.logicalName);
|
|
1302
|
+
const paramName = `${logicalId}Name`;
|
|
1303
|
+
parameters[paramName] = {
|
|
1304
|
+
Type: "String",
|
|
1305
|
+
Default: spec.physicalName,
|
|
1306
|
+
Description: `Name of the ${spec.logicalName} DynamoDB table.`
|
|
1307
|
+
};
|
|
1308
|
+
const resource = buildTableResource(spec, {
|
|
1309
|
+
gsiProjection,
|
|
1310
|
+
billingMode,
|
|
1311
|
+
streamsEnabled,
|
|
1312
|
+
throughput,
|
|
1313
|
+
...c5Options
|
|
1314
|
+
});
|
|
1315
|
+
resource.Properties.TableName = {
|
|
1316
|
+
Ref: paramName
|
|
1317
|
+
};
|
|
1318
|
+
resources[logicalId] = resource;
|
|
1319
|
+
if (autoscaleSettings !== null) {
|
|
1320
|
+
addAutoscalingResources(resources, logicalId, spec, autoscaleSettings);
|
|
1321
|
+
}
|
|
1322
|
+
outputs[`${logicalId}Name`] = {
|
|
1323
|
+
Description: `Name of the ${spec.logicalName} DynamoDB table.`,
|
|
1324
|
+
Value: { Ref: logicalId }
|
|
1325
|
+
};
|
|
1326
|
+
outputs[`${logicalId}Arn`] = {
|
|
1327
|
+
Description: `ARN of the ${spec.logicalName} DynamoDB table.`,
|
|
1328
|
+
Value: { "Fn::GetAtt": [logicalId, "Arn"] }
|
|
1329
|
+
};
|
|
1330
|
+
if (streamsEnabled) {
|
|
1331
|
+
outputs[`${logicalId}StreamArn`] = {
|
|
1332
|
+
Description: `DynamoDB Streams ARN of the ${spec.logicalName} table.`,
|
|
1333
|
+
Value: { "Fn::GetAtt": [logicalId, "StreamArn"] }
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
return {
|
|
1339
|
+
AWSTemplateFormatVersion: "2010-09-09",
|
|
1340
|
+
Description: "DynamoDB table(s) for a GraphDDB single-table design (generated by graphddb generate cloudformation).",
|
|
1341
|
+
Parameters: parameters,
|
|
1342
|
+
Resources: resources,
|
|
1343
|
+
Outputs: outputs
|
|
1344
|
+
};
|
|
1345
|
+
}
|
|
1346
|
+
function isPlainYamlScalar(value) {
|
|
1347
|
+
if (value.length === 0) return false;
|
|
1348
|
+
if (value !== value.trim()) return false;
|
|
1349
|
+
if (/[:#{}\[\],&*!|>'"%@`]/.test(value)) return false;
|
|
1350
|
+
if (/^[?-]/.test(value)) return false;
|
|
1351
|
+
if (/^(true|false|null|~|yes|no|on|off)$/i.test(value)) return false;
|
|
1352
|
+
if (/^[+-]?(\d|\.\d)/.test(value)) return false;
|
|
1353
|
+
return true;
|
|
1354
|
+
}
|
|
1355
|
+
function yamlScalar(value) {
|
|
1356
|
+
if (value === null) return "null";
|
|
1357
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
1358
|
+
if (typeof value === "number") return String(value);
|
|
1359
|
+
const str = String(value);
|
|
1360
|
+
if (isPlainYamlScalar(str)) return str;
|
|
1361
|
+
return `"${str.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
1362
|
+
}
|
|
1363
|
+
function yamlKey(key) {
|
|
1364
|
+
return isPlainYamlScalar(key) ? key : `"${key.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
1365
|
+
}
|
|
1366
|
+
function isPlainObject(value) {
|
|
1367
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1368
|
+
}
|
|
1369
|
+
function toYaml(value, indent) {
|
|
1370
|
+
const pad = " ".repeat(indent);
|
|
1371
|
+
if (Array.isArray(value)) {
|
|
1372
|
+
const lines = [];
|
|
1373
|
+
for (const item of value) {
|
|
1374
|
+
if (isPlainObject(item) || Array.isArray(item)) {
|
|
1375
|
+
const child = toYaml(item, indent + 1);
|
|
1376
|
+
if (child.length === 0) {
|
|
1377
|
+
lines.push(`${pad}-`);
|
|
1378
|
+
} else {
|
|
1379
|
+
lines.push(`${pad}- ${child[0].slice((indent + 1) * 2)}`);
|
|
1380
|
+
for (let i = 1; i < child.length; i++) lines.push(child[i]);
|
|
1381
|
+
}
|
|
1382
|
+
} else {
|
|
1383
|
+
lines.push(`${pad}- ${yamlScalar(item)}`);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
return lines;
|
|
1387
|
+
}
|
|
1388
|
+
if (isPlainObject(value)) {
|
|
1389
|
+
const lines = [];
|
|
1390
|
+
for (const [key, val] of Object.entries(value)) {
|
|
1391
|
+
const k = yamlKey(key);
|
|
1392
|
+
if (isPlainObject(val) || Array.isArray(val)) {
|
|
1393
|
+
const child = toYaml(val, indent + 1);
|
|
1394
|
+
if (child.length === 0) {
|
|
1395
|
+
lines.push(`${pad}${k}: ${Array.isArray(val) ? "[]" : "{}"}`);
|
|
1396
|
+
} else {
|
|
1397
|
+
lines.push(`${pad}${k}:`);
|
|
1398
|
+
for (const line of child) lines.push(line);
|
|
1399
|
+
}
|
|
1400
|
+
} else {
|
|
1401
|
+
lines.push(`${pad}${k}: ${yamlScalar(val)}`);
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return lines;
|
|
1405
|
+
}
|
|
1406
|
+
return [`${pad}${yamlScalar(value)}`];
|
|
1407
|
+
}
|
|
1408
|
+
function templateToYaml(template) {
|
|
1409
|
+
return toYaml(template, 0).join("\n") + "\n";
|
|
1410
|
+
}
|
|
1411
|
+
function templateToJson(template) {
|
|
1412
|
+
return JSON.stringify(template, null, 2) + "\n";
|
|
1413
|
+
}
|
|
1414
|
+
function emitCloudformation(manifest, options = {}) {
|
|
1415
|
+
const template = buildCloudformationTemplate(manifest, options);
|
|
1416
|
+
return (options.format ?? "yaml") === "json" ? templateToJson(template) : templateToYaml(template);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// src/codegen/cdc-registry.ts
|
|
1420
|
+
var TS_HEADER = PY_HEADER.replace("# ", "// ");
|
|
1421
|
+
function cdcProjectedModelNames(registry = MetadataRegistry) {
|
|
1422
|
+
const names = [];
|
|
1423
|
+
for (const [cls, metadata] of registry.getAll()) {
|
|
1424
|
+
if (metadata.cdcProjected) names.push(cls.name);
|
|
1425
|
+
}
|
|
1426
|
+
return names.sort();
|
|
1427
|
+
}
|
|
1428
|
+
function generateCdcRegistry(options = {}) {
|
|
1429
|
+
const registry = options.registry ?? MetadataRegistry;
|
|
1430
|
+
const modelsModule = options.modelsModule ?? "./models.js";
|
|
1431
|
+
const names = cdcProjectedModelNames(registry);
|
|
1432
|
+
const lines = [TS_HEADER, ""];
|
|
1433
|
+
if (names.length === 0) {
|
|
1434
|
+
lines.push(
|
|
1435
|
+
"// (no @cdcProjected models \u2014 nothing to augment CdcModelRegistry with)",
|
|
1436
|
+
"",
|
|
1437
|
+
"export {};",
|
|
1438
|
+
""
|
|
1439
|
+
);
|
|
1440
|
+
return lines.join("\n");
|
|
1441
|
+
}
|
|
1442
|
+
for (const name of names) {
|
|
1443
|
+
lines.push(`import type { ${name} } from '${modelsModule}';`);
|
|
1444
|
+
}
|
|
1445
|
+
lines.push("", "declare module 'graphddb' {", " interface CdcModelRegistry {");
|
|
1446
|
+
for (const name of names) {
|
|
1447
|
+
lines.push(` ${name}: ${name};`);
|
|
1448
|
+
}
|
|
1449
|
+
lines.push(" }", "}", "");
|
|
1450
|
+
return lines.join("\n");
|
|
1451
|
+
}
|
|
1452
|
+
|
|
821
1453
|
// src/codegen/index.ts
|
|
822
1454
|
var OUTPUT_FILES = [
|
|
823
1455
|
"manifest.json",
|
|
@@ -930,17 +1562,207 @@ var handlers = {
|
|
|
930
1562
|
for (const name of OUTPUT_FILES) {
|
|
931
1563
|
writeFileSync(resolve(outDir, name), files[name], "utf8");
|
|
932
1564
|
}
|
|
1565
|
+
const emittedFiles = [...OUTPUT_FILES];
|
|
1566
|
+
if (cdcProjectedModelNames().length > 0) {
|
|
1567
|
+
const entryAbs = isAbsolute(entry) ? entry : resolve(process.cwd(), entry);
|
|
1568
|
+
let modelsModule = relative(outDir, entryAbs).replace(/\\/g, "/").replace(/\.[mc]?ts$/, "");
|
|
1569
|
+
if (!modelsModule.startsWith(".")) modelsModule = `./${modelsModule}`;
|
|
1570
|
+
const cdcRegistryFile = "cdc-registry.ts";
|
|
1571
|
+
writeFileSync(
|
|
1572
|
+
resolve(outDir, cdcRegistryFile),
|
|
1573
|
+
generateCdcRegistry({ modelsModule }),
|
|
1574
|
+
"utf8"
|
|
1575
|
+
);
|
|
1576
|
+
emittedFiles.push(cdcRegistryFile);
|
|
1577
|
+
}
|
|
933
1578
|
process.stdout.write(
|
|
934
1579
|
JSON.stringify({
|
|
935
1580
|
status: "ok",
|
|
936
1581
|
outDir: out,
|
|
937
|
-
files:
|
|
1582
|
+
files: emittedFiles
|
|
938
1583
|
}) + "\n"
|
|
939
1584
|
);
|
|
940
1585
|
} catch (err) {
|
|
941
1586
|
const message = err instanceof Error ? err.message : String(err);
|
|
942
1587
|
fail("GENERATION_FAILED", message, 1);
|
|
943
1588
|
}
|
|
1589
|
+
},
|
|
1590
|
+
/**
|
|
1591
|
+
* `generate cloudformation` (issue #168, Epic #167 — CFn generator C1).
|
|
1592
|
+
*
|
|
1593
|
+
* Loads the entry model module (side-effecting import registers the entities),
|
|
1594
|
+
* builds the manifest from the shared {@link MetadataRegistry}, and emits an
|
|
1595
|
+
* AWS CloudFormation template (DynamoDB Table + unioned GSIs). Writes to
|
|
1596
|
+
* `--out` when given, else to stdout. Mirrors `generatePython`'s tsx-based
|
|
1597
|
+
* entry loading and JSON error / exit shape.
|
|
1598
|
+
*/
|
|
1599
|
+
async generateCloudformation(options) {
|
|
1600
|
+
const {
|
|
1601
|
+
entry,
|
|
1602
|
+
out,
|
|
1603
|
+
format,
|
|
1604
|
+
gsiProjection,
|
|
1605
|
+
stream,
|
|
1606
|
+
billingMode,
|
|
1607
|
+
rcu,
|
|
1608
|
+
wcu,
|
|
1609
|
+
autoscale,
|
|
1610
|
+
autoscaleMin,
|
|
1611
|
+
autoscaleMax,
|
|
1612
|
+
autoscaleTarget,
|
|
1613
|
+
pitr,
|
|
1614
|
+
tableClass,
|
|
1615
|
+
sse
|
|
1616
|
+
} = options;
|
|
1617
|
+
if (!entry) fail("INVALID_ARGS", "Missing required option --entry.", 2);
|
|
1618
|
+
const fmt = format === "json" ? "json" : "yaml";
|
|
1619
|
+
if (format !== void 0 && format !== "yaml" && format !== "json") {
|
|
1620
|
+
fail("INVALID_ARGS", `Invalid --format '${format}'. Use yaml or json.`, 2);
|
|
1621
|
+
}
|
|
1622
|
+
let projection = "ALL";
|
|
1623
|
+
if (gsiProjection !== void 0) {
|
|
1624
|
+
if (gsiProjection !== "ALL" && gsiProjection !== "KEYS_ONLY") {
|
|
1625
|
+
fail(
|
|
1626
|
+
"INVALID_ARGS",
|
|
1627
|
+
`Invalid --gsi-projection '${gsiProjection}'. Use ALL or KEYS_ONLY.`,
|
|
1628
|
+
2
|
|
1629
|
+
);
|
|
1630
|
+
}
|
|
1631
|
+
projection = gsiProjection;
|
|
1632
|
+
}
|
|
1633
|
+
let streamMode = "auto";
|
|
1634
|
+
if (stream !== void 0) {
|
|
1635
|
+
if (stream !== "auto" && stream !== "on" && stream !== "off") {
|
|
1636
|
+
fail(
|
|
1637
|
+
"INVALID_ARGS",
|
|
1638
|
+
`Invalid --stream '${stream}'. Use auto, on, or off.`,
|
|
1639
|
+
2
|
|
1640
|
+
);
|
|
1641
|
+
}
|
|
1642
|
+
streamMode = stream;
|
|
1643
|
+
}
|
|
1644
|
+
let billing = "PAY_PER_REQUEST";
|
|
1645
|
+
if (billingMode !== void 0) {
|
|
1646
|
+
if (billingMode !== "PAY_PER_REQUEST" && billingMode !== "PROVISIONED") {
|
|
1647
|
+
fail(
|
|
1648
|
+
"INVALID_ARGS",
|
|
1649
|
+
`Invalid --billing-mode '${billingMode}'. Use PAY_PER_REQUEST or PROVISIONED.`,
|
|
1650
|
+
2
|
|
1651
|
+
);
|
|
1652
|
+
}
|
|
1653
|
+
billing = billingMode;
|
|
1654
|
+
}
|
|
1655
|
+
const parsePositiveInt = (raw, flag) => {
|
|
1656
|
+
if (raw === void 0) return void 0;
|
|
1657
|
+
if (!/^\d+$/.test(raw)) {
|
|
1658
|
+
fail(
|
|
1659
|
+
"INVALID_ARGS",
|
|
1660
|
+
`Invalid ${flag} '${raw}'. Expected a positive integer.`,
|
|
1661
|
+
2
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
const n = Number(raw);
|
|
1665
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
1666
|
+
fail(
|
|
1667
|
+
"INVALID_ARGS",
|
|
1668
|
+
`Invalid ${flag} '${raw}'. Expected a positive integer.`,
|
|
1669
|
+
2
|
|
1670
|
+
);
|
|
1671
|
+
}
|
|
1672
|
+
return n;
|
|
1673
|
+
};
|
|
1674
|
+
const rcuNum = parsePositiveInt(rcu, "--rcu");
|
|
1675
|
+
const wcuNum = parsePositiveInt(wcu, "--wcu");
|
|
1676
|
+
const autoscaleMinNum = parsePositiveInt(autoscaleMin, "--autoscale-min");
|
|
1677
|
+
const autoscaleMaxNum = parsePositiveInt(autoscaleMax, "--autoscale-max");
|
|
1678
|
+
const autoscaleTargetNum = parsePositiveInt(
|
|
1679
|
+
autoscaleTarget,
|
|
1680
|
+
"--autoscale-target"
|
|
1681
|
+
);
|
|
1682
|
+
if (autoscaleTargetNum !== void 0 && autoscaleTargetNum > 100) {
|
|
1683
|
+
fail(
|
|
1684
|
+
"INVALID_ARGS",
|
|
1685
|
+
`Invalid --autoscale-target '${autoscaleTarget}'. Expected 1-100 (a target utilization percentage).`,
|
|
1686
|
+
2
|
|
1687
|
+
);
|
|
1688
|
+
}
|
|
1689
|
+
const provisioned = billing === "PROVISIONED";
|
|
1690
|
+
const autoscaleOn = Boolean(autoscale);
|
|
1691
|
+
if (!provisioned && (rcuNum !== void 0 || wcuNum !== void 0)) {
|
|
1692
|
+
fail(
|
|
1693
|
+
"INVALID_ARGS",
|
|
1694
|
+
`--rcu/--wcu require --billing-mode PROVISIONED. PAY_PER_REQUEST (on-demand) tables have no provisioned capacity.`,
|
|
1695
|
+
2
|
|
1696
|
+
);
|
|
1697
|
+
}
|
|
1698
|
+
if (!provisioned && autoscaleOn) {
|
|
1699
|
+
fail(
|
|
1700
|
+
"INVALID_ARGS",
|
|
1701
|
+
`--autoscale requires --billing-mode PROVISIONED. Application Auto Scaling is invalid on a PAY_PER_REQUEST (on-demand) table.`,
|
|
1702
|
+
2
|
|
1703
|
+
);
|
|
1704
|
+
}
|
|
1705
|
+
const effMin = autoscaleMinNum ?? 5;
|
|
1706
|
+
const effMax = autoscaleMaxNum ?? 100;
|
|
1707
|
+
if (autoscaleOn && effMin > effMax) {
|
|
1708
|
+
fail(
|
|
1709
|
+
"INVALID_ARGS",
|
|
1710
|
+
`--autoscale-min (${effMin}) must be <= --autoscale-max (${effMax}).`,
|
|
1711
|
+
2
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
let tblClass;
|
|
1715
|
+
if (tableClass !== void 0) {
|
|
1716
|
+
if (tableClass !== "STANDARD" && tableClass !== "STANDARD_INFREQUENT_ACCESS") {
|
|
1717
|
+
fail(
|
|
1718
|
+
"INVALID_ARGS",
|
|
1719
|
+
`Invalid --table-class '${tableClass}'. Use STANDARD or STANDARD_INFREQUENT_ACCESS.`,
|
|
1720
|
+
2
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
tblClass = tableClass;
|
|
1724
|
+
}
|
|
1725
|
+
try {
|
|
1726
|
+
await importModule(entry);
|
|
1727
|
+
const manifest = buildManifest();
|
|
1728
|
+
const streamMaintenanceDetected = streamMode === "auto" ? detectStreamMaintenance() : false;
|
|
1729
|
+
const template = emitCloudformation(manifest, {
|
|
1730
|
+
format: fmt,
|
|
1731
|
+
gsiProjection: projection,
|
|
1732
|
+
billingMode: billing,
|
|
1733
|
+
stream: streamMode,
|
|
1734
|
+
streamMaintenanceDetected,
|
|
1735
|
+
pitr: Boolean(pitr),
|
|
1736
|
+
tableClass: tblClass,
|
|
1737
|
+
sse: Boolean(sse),
|
|
1738
|
+
// C6 (#174): provisioned throughput + autoscaling. Pass the parsed
|
|
1739
|
+
// values through; the emitter applies defaults and enforces the same
|
|
1740
|
+
// loud-reject invariants as a backstop.
|
|
1741
|
+
rcu: rcuNum,
|
|
1742
|
+
wcu: wcuNum,
|
|
1743
|
+
autoscale: autoscaleOn,
|
|
1744
|
+
autoscaleMin: autoscaleMinNum,
|
|
1745
|
+
autoscaleMax: autoscaleMaxNum,
|
|
1746
|
+
autoscaleTarget: autoscaleTargetNum
|
|
1747
|
+
});
|
|
1748
|
+
if (out) {
|
|
1749
|
+
const outFile = isAbsolute(out) ? out : resolve(process.cwd(), out);
|
|
1750
|
+
mkdirSync(dirname(outFile), { recursive: true });
|
|
1751
|
+
writeFileSync(outFile, template, "utf8");
|
|
1752
|
+
process.stdout.write(
|
|
1753
|
+
JSON.stringify({
|
|
1754
|
+
status: "ok",
|
|
1755
|
+
outDir: dirname(out),
|
|
1756
|
+
files: [out]
|
|
1757
|
+
}) + "\n"
|
|
1758
|
+
);
|
|
1759
|
+
} else {
|
|
1760
|
+
process.stdout.write(template);
|
|
1761
|
+
}
|
|
1762
|
+
} catch (err) {
|
|
1763
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1764
|
+
fail("GENERATION_FAILED", message, 1);
|
|
1765
|
+
}
|
|
944
1766
|
}
|
|
945
1767
|
};
|
|
946
1768
|
|