@metaobjectsdev/codegen-ts 0.6.0 → 0.7.0-rc.10

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.
Files changed (119) hide show
  1. package/README.md +161 -4
  2. package/dist/column-mapper.d.ts +16 -0
  3. package/dist/column-mapper.d.ts.map +1 -1
  4. package/dist/column-mapper.js +73 -2
  5. package/dist/column-mapper.js.map +1 -1
  6. package/dist/generators/docs-file.d.ts +8 -0
  7. package/dist/generators/docs-file.d.ts.map +1 -0
  8. package/dist/generators/docs-file.js +52 -0
  9. package/dist/generators/docs-file.js.map +1 -0
  10. package/dist/generators/entity-file.d.ts +15 -0
  11. package/dist/generators/entity-file.d.ts.map +1 -1
  12. package/dist/generators/entity-file.js +2 -1
  13. package/dist/generators/entity-file.js.map +1 -1
  14. package/dist/generators/index.d.ts +4 -0
  15. package/dist/generators/index.d.ts.map +1 -1
  16. package/dist/generators/index.js +4 -0
  17. package/dist/generators/index.js.map +1 -1
  18. package/dist/generators/output-parser-file.d.ts +9 -0
  19. package/dist/generators/output-parser-file.d.ts.map +1 -0
  20. package/dist/generators/output-parser-file.js +37 -0
  21. package/dist/generators/output-parser-file.js.map +1 -0
  22. package/dist/generators/prompt-render-file.d.ts +9 -0
  23. package/dist/generators/prompt-render-file.d.ts.map +1 -0
  24. package/dist/generators/prompt-render-file.js +70 -0
  25. package/dist/generators/prompt-render-file.js.map +1 -0
  26. package/dist/generators/queries-file.d.ts +1 -1
  27. package/dist/generators/queries-file.d.ts.map +1 -1
  28. package/dist/generators/queries-file.js +11 -3
  29. package/dist/generators/queries-file.js.map +1 -1
  30. package/dist/generators/routes-file-hono.d.ts +21 -0
  31. package/dist/generators/routes-file-hono.d.ts.map +1 -0
  32. package/dist/generators/routes-file-hono.js +38 -0
  33. package/dist/generators/routes-file-hono.js.map +1 -0
  34. package/dist/index.d.ts +2 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/metaobjects-config.d.ts +10 -1
  39. package/dist/metaobjects-config.d.ts.map +1 -1
  40. package/dist/metaobjects-config.js +2 -1
  41. package/dist/metaobjects-config.js.map +1 -1
  42. package/dist/naming.d.ts +3 -12
  43. package/dist/naming.d.ts.map +1 -1
  44. package/dist/naming.js +14 -44
  45. package/dist/naming.js.map +1 -1
  46. package/dist/payload-codegen.d.ts +8 -0
  47. package/dist/payload-codegen.d.ts.map +1 -1
  48. package/dist/payload-codegen.js +33 -3
  49. package/dist/payload-codegen.js.map +1 -1
  50. package/dist/projection/extract-view-spec.d.ts +1 -1
  51. package/dist/projection/extract-view-spec.js +1 -1
  52. package/dist/source-detect.d.ts +10 -0
  53. package/dist/source-detect.d.ts.map +1 -0
  54. package/dist/source-detect.js +30 -0
  55. package/dist/source-detect.js.map +1 -0
  56. package/dist/templates/docs-file.d.ts +48 -0
  57. package/dist/templates/docs-file.d.ts.map +1 -0
  58. package/dist/templates/docs-file.js +445 -0
  59. package/dist/templates/docs-file.js.map +1 -0
  60. package/dist/templates/drizzle-schema.js +27 -3
  61. package/dist/templates/drizzle-schema.js.map +1 -1
  62. package/dist/templates/entity-file.d.ts +15 -1
  63. package/dist/templates/entity-file.d.ts.map +1 -1
  64. package/dist/templates/entity-file.js +15 -5
  65. package/dist/templates/entity-file.js.map +1 -1
  66. package/dist/templates/inferred-types.d.ts +9 -0
  67. package/dist/templates/inferred-types.d.ts.map +1 -1
  68. package/dist/templates/inferred-types.js +88 -2
  69. package/dist/templates/inferred-types.js.map +1 -1
  70. package/dist/templates/output-parser.d.ts +8 -0
  71. package/dist/templates/output-parser.d.ts.map +1 -0
  72. package/dist/templates/output-parser.js +129 -0
  73. package/dist/templates/output-parser.js.map +1 -0
  74. package/dist/templates/projection-decl.d.ts +1 -1
  75. package/dist/templates/projection-decl.js +1 -1
  76. package/dist/templates/queries-file.d.ts.map +1 -1
  77. package/dist/templates/queries-file.js +15 -4
  78. package/dist/templates/queries-file.js.map +1 -1
  79. package/dist/templates/queries.d.ts.map +1 -1
  80. package/dist/templates/queries.js +11 -30
  81. package/dist/templates/queries.js.map +1 -1
  82. package/dist/templates/routes-file-hono.d.ts +4 -0
  83. package/dist/templates/routes-file-hono.d.ts.map +1 -0
  84. package/dist/templates/routes-file-hono.js +119 -0
  85. package/dist/templates/routes-file-hono.js.map +1 -0
  86. package/dist/templates/value-object-file.d.ts +3 -0
  87. package/dist/templates/value-object-file.d.ts.map +1 -0
  88. package/dist/templates/value-object-file.js +27 -0
  89. package/dist/templates/value-object-file.js.map +1 -0
  90. package/dist/templates/zod-validators.d.ts +10 -0
  91. package/dist/templates/zod-validators.d.ts.map +1 -1
  92. package/dist/templates/zod-validators.js +108 -30
  93. package/dist/templates/zod-validators.js.map +1 -1
  94. package/package.json +5 -4
  95. package/src/column-mapper.ts +86 -1
  96. package/src/generators/docs-file.ts +64 -0
  97. package/src/generators/entity-file.ts +17 -1
  98. package/src/generators/index.ts +4 -0
  99. package/src/generators/output-parser-file.ts +50 -0
  100. package/src/generators/prompt-render-file.ts +95 -0
  101. package/src/generators/queries-file.ts +13 -4
  102. package/src/generators/routes-file-hono.ts +48 -0
  103. package/src/index.ts +2 -2
  104. package/src/metaobjects-config.ts +11 -2
  105. package/src/naming.ts +22 -46
  106. package/src/payload-codegen.ts +34 -2
  107. package/src/projection/extract-view-spec.ts +1 -1
  108. package/src/source-detect.ts +28 -0
  109. package/src/templates/docs-file.ts +545 -0
  110. package/src/templates/drizzle-schema.ts +27 -3
  111. package/src/templates/entity-file.ts +36 -5
  112. package/src/templates/inferred-types.ts +117 -3
  113. package/src/templates/output-parser.ts +143 -0
  114. package/src/templates/projection-decl.ts +1 -1
  115. package/src/templates/queries-file.ts +18 -4
  116. package/src/templates/queries.ts +11 -33
  117. package/src/templates/routes-file-hono.ts +142 -0
  118. package/src/templates/value-object-file.ts +30 -0
  119. package/src/templates/zod-validators.ts +121 -35
@@ -0,0 +1,64 @@
1
+ // docsFile() — emits `<Entity>.md` next to each generated entity module.
2
+ //
3
+ // Markdown is port-agnostic: the same metadata produces the same documentation
4
+ // page regardless of dialect, output layout, or runtime target. The page covers
5
+ // description / type / source / package preamble; storage table + identity +
6
+ // relationships for table-backed entities; validation entry points; "used by"
7
+ // template cross-references; and the generated-code surface (which sibling
8
+ // files codegen produces). See `templates/docs-file.ts` for the body.
9
+
10
+ import type { MetaObject } from "@metaobjectsdev/metadata";
11
+ import { perEntity, type Generator, type GeneratorFactory } from "../generator.js";
12
+ import { renderDocsFile } from "../templates/docs-file.js";
13
+ import { entityOutputPath } from "../import-path.js";
14
+
15
+ export interface DocsFileOpts {
16
+ filter?: (entity: MetaObject) => boolean;
17
+ target?: string;
18
+ }
19
+
20
+ export const docsFile = function docsFile(opts?: DocsFileOpts): Generator {
21
+ const generator: Generator = {
22
+ name: "docs-file",
23
+ generate: perEntity((entity, ctx) => {
24
+ if (!ctx.renderContext) {
25
+ throw new Error("docs-file: renderContext is required (provided by runGen)");
26
+ }
27
+ const rc = ctx.renderContext;
28
+ const generatorNames = readGeneratorNames(ctx);
29
+ return {
30
+ path: entityOutputPath(
31
+ ctx.config.outputLayout ?? "flat",
32
+ entity.package,
33
+ `${entity.name}.md`,
34
+ ),
35
+ content: renderDocsFile(entity, {
36
+ dialect: rc.dialect,
37
+ columnNamingStrategy: rc.columnNamingStrategy,
38
+ loadedRoot: rc.loadedRoot,
39
+ generatorNames,
40
+ }),
41
+ };
42
+ }),
43
+ };
44
+ if (opts?.filter) {
45
+ generator.filter = opts.filter;
46
+ }
47
+ if (opts?.target) {
48
+ generator.target = opts.target;
49
+ }
50
+ return generator;
51
+ } as GeneratorFactory<DocsFileOpts>;
52
+
53
+ /**
54
+ * `GenContext` does not currently carry the full generator list — that lives on
55
+ * the resolved config inside the runner. Rather than thread a new field through
56
+ * just to populate one optional markdown section, we always list every
57
+ * potential companion. The "Generated code" section becomes "files that may be
58
+ * generated alongside this one" — adopters cross-reference their own
59
+ * `metaobjects.config.ts` to confirm which they actually wired in. This matches
60
+ * the spec guidance "list them all and let the reader figure out which exist."
61
+ */
62
+ function readGeneratorNames(_ctx: unknown): ReadonlySet<string> {
63
+ return new Set(["queries-file", "routes-file", "routes-file-hono"]);
64
+ }
@@ -7,9 +7,25 @@ import { entityOutputPath } from "../import-path.js";
7
7
  export interface EntityFileOpts {
8
8
  filter?: (entity: MetaObject) => boolean;
9
9
  target?: string;
10
+ /**
11
+ * Whether generated entity files include the Fastify-flavored
12
+ * `<Entity>FilterAllowlist` + `<Entity>SortAllowlist` blocks (and the
13
+ * `@metaobjectsdev/runtime-ts/drizzle-fastify` type-only imports they
14
+ * require). Default `true` for back-compat.
15
+ *
16
+ * Set to `false` for Worker/Lambda-style consumers that don't mount
17
+ * Fastify-style server routes — the generated entity file then has no
18
+ * `runtime-ts/drizzle-fastify` imports at all and `@metaobjectsdev/runtime-ts`
19
+ * can be omitted from the consumer's dependency tree.
20
+ *
21
+ * The client-side `<Entity>Filter` type is always emitted — it's a pure
22
+ * client-side type with no runtime-ts dependency.
23
+ */
24
+ allowlists?: boolean;
10
25
  }
11
26
 
12
27
  export const entityFile = function entityFile(opts?: EntityFileOpts): Generator {
28
+ const allowlists = opts?.allowlists ?? true;
13
29
  const generator: Generator = {
14
30
  name: "entity-file",
15
31
  emitsEntityModule: true,
@@ -19,7 +35,7 @@ export const entityFile = function entityFile(opts?: EntityFileOpts): Generator
19
35
  }
20
36
  return {
21
37
  path: entityOutputPath(ctx.config.outputLayout ?? "flat", entity.package, `${entity.name}.ts`),
22
- content: await formatTs(renderEntityFile(entity, ctx.renderContext)),
38
+ content: await formatTs(renderEntityFile(entity, ctx.renderContext, { allowlists })),
23
39
  };
24
40
  }),
25
41
  };
@@ -1,5 +1,9 @@
1
1
  export { entityFile, type EntityFileOpts } from "./entity-file.js";
2
2
  export { queriesFile, type QueriesFileOpts } from "./queries-file.js";
3
3
  export { routesFile, type RoutesFileOpts } from "./routes-file.js";
4
+ export { routesFileHono, type RoutesFileHonoOpts } from "./routes-file-hono.js";
4
5
  export { barrel, type BarrelOpts } from "./barrel.js";
5
6
  export { mermaidErDiagram, type MermaidErOptions } from "./mermaid-er.js";
7
+ export { promptRender, type PromptRenderOpts } from "./prompt-render-file.js";
8
+ export { outputParser, type OutputParserOpts } from "./output-parser-file.js";
9
+ export { docsFile, type DocsFileOpts } from "./docs-file.js";
@@ -0,0 +1,50 @@
1
+ // server/typescript/packages/codegen-ts/src/generators/output-parser-file.ts
2
+ //
3
+ // Stock generator that emits one <TemplateName>.output.ts file per declared
4
+ // template.output node. Wraps renderOutputParser() from templates/output-parser.ts.
5
+ //
6
+ // Consumer wiring (metaobjects.config.ts):
7
+ // generators: [..., promptRender(), outputParser()]
8
+ //
9
+ // Custom output directory:
10
+ // generators: [..., outputParser({ outDir: "src/generated/outputs" })]
11
+
12
+ import { TYPE_TEMPLATE, TEMPLATE_SUBTYPE_OUTPUT } from "@metaobjectsdev/metadata";
13
+ import {
14
+ type EmittedFile,
15
+ type Generator,
16
+ type GeneratorFactory,
17
+ oncePerRun,
18
+ } from "../generator.js";
19
+ import { renderOutputParser } from "../templates/output-parser.js";
20
+
21
+ export interface OutputParserOpts {
22
+ /** Output directory prefix relative to the target's outDir. Default: "" (root). */
23
+ outDir?: string;
24
+ /** Optional named output target (registry key). Defaults to "default". */
25
+ target?: string;
26
+ }
27
+
28
+ export const outputParser = function outputParser(opts?: OutputParserOpts): Generator {
29
+ const dirPrefix = opts?.outDir ? `${opts.outDir.replace(/\/$/, "")}/` : "";
30
+ const generator: Generator = {
31
+ name: "output-parser",
32
+ generate: oncePerRun((_entities, ctx) => {
33
+ const outputs = ctx.loadedRoot
34
+ .ownChildren()
35
+ .filter((c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_OUTPUT);
36
+ const files: EmittedFile[] = [];
37
+ for (const t of outputs) {
38
+ files.push({
39
+ path: `${dirPrefix}${t.name}.output.ts`,
40
+ content: renderOutputParser(ctx.loadedRoot, t.name),
41
+ });
42
+ }
43
+ return files;
44
+ }),
45
+ };
46
+ if (opts?.target) {
47
+ generator.target = opts.target;
48
+ }
49
+ return generator;
50
+ } as GeneratorFactory<OutputParserOpts>;
@@ -0,0 +1,95 @@
1
+ // Stock generator that wraps generatePayloadInterfaces() + generateRenderHandle()
2
+ // from payload-codegen.ts into a Generator factory. Emits ONE file aggregating
3
+ // typed payload interfaces (for object.value entities) and render handles (for
4
+ // template.prompt nodes).
5
+ //
6
+ // Consumer wiring (metaobjects.config.ts):
7
+ // generators: [..., promptRender()]
8
+ //
9
+ // Custom output path:
10
+ // generators: [..., promptRender({ outFile: "src/render/generated/prompts.ts" })]
11
+
12
+ import { TYPE_TEMPLATE, TEMPLATE_SUBTYPE_PROMPT, OBJECT_SUBTYPE_VALUE } from "@metaobjectsdev/metadata";
13
+ import {
14
+ type Generator,
15
+ type GeneratorFactory,
16
+ oncePerRun,
17
+ } from "../generator.js";
18
+ import {
19
+ generatePayloadInterfacesBatch,
20
+ generateRenderHandle,
21
+ } from "../payload-codegen.js";
22
+ import { GENERATED_HEADER } from "../constants.js";
23
+
24
+ export interface PromptRenderOpts {
25
+ /** Output file path relative to the target's outDir. Default: "prompts.ts". */
26
+ outFile?: string;
27
+ /** Optional named output target (registry key). Defaults to "default". */
28
+ target?: string;
29
+ }
30
+
31
+ // Hoisted into the emitted file once. generateRenderHandle() emits this line
32
+ // per-handle (for the standalone scenario); we strip its per-handle copies.
33
+ const RENDER_IMPORT = `import { render, type Provider } from "@metaobjectsdev/render";`;
34
+
35
+ // Matches the `import type { ... } from "./payloads.js";` that generateRenderHandle
36
+ // emits for the standalone two-file scenario. In the single-file output here the
37
+ // payload interfaces are already defined above, so the import is dead.
38
+ function isStandalonePayloadImport(line: string): boolean {
39
+ return line.startsWith("import type {") && line.includes('"./payloads.js"');
40
+ }
41
+
42
+ export const promptRender = function promptRender(opts?: PromptRenderOpts): Generator {
43
+ const outFile = opts?.outFile ?? "prompts.ts";
44
+ const generator: Generator = {
45
+ name: "prompt-render",
46
+ generate: oncePerRun((entities, ctx) => {
47
+ const payloads = entities.filter((e) => e.subType === OBJECT_SUBTYPE_VALUE);
48
+ const prompts = ctx.loadedRoot
49
+ .ownChildren()
50
+ .filter((c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_PROMPT);
51
+
52
+ if (payloads.length === 0 && prompts.length === 0) {
53
+ return [];
54
+ }
55
+
56
+ const parts: string[] = [`// ${GENERATED_HEADER} — DO NOT EDIT.`];
57
+
58
+ // Hoist the @metaobjectsdev/render import once (only when prompts emit handles).
59
+ if (prompts.length > 0) {
60
+ parts.push(RENDER_IMPORT);
61
+ }
62
+
63
+ // Emit payload interfaces with a single shared dedupe set so a lens
64
+ // referenced by multiple payloads appears exactly once.
65
+ const payloadInterfaces = generatePayloadInterfacesBatch(
66
+ ctx.loadedRoot,
67
+ payloads.map((p) => p.name),
68
+ );
69
+ if (payloadInterfaces.length > 0) {
70
+ parts.push(payloadInterfaces);
71
+ }
72
+
73
+ // Append each render handle with its per-handle imports stripped — both
74
+ // the now-hoisted render/Provider import and the standalone payloads.js
75
+ // import. Also drop any leading blank lines left behind by the strip so
76
+ // the joined output doesn't accumulate double blank gaps between parts.
77
+ for (const t of prompts) {
78
+ const lines = generateRenderHandle(ctx.loadedRoot, t.name)
79
+ .split("\n")
80
+ .filter((line) => line !== RENDER_IMPORT && !isStandalonePayloadImport(line));
81
+ while (lines.length > 0 && lines[0]!.trim() === "") lines.shift();
82
+ parts.push(lines.join("\n"));
83
+ }
84
+
85
+ return [{
86
+ path: outFile,
87
+ content: parts.filter((s) => s.length > 0).map((s) => s.trimEnd()).join("\n\n") + "\n",
88
+ }];
89
+ }),
90
+ };
91
+ if (opts?.target) {
92
+ generator.target = opts.target;
93
+ }
94
+ return generator;
95
+ } as GeneratorFactory<PromptRenderOpts>;
@@ -1,4 +1,4 @@
1
- import type { MetaObject } from "@metaobjectsdev/metadata";
1
+ import { OBJECT_SUBTYPE_VALUE, type MetaObject } from "@metaobjectsdev/metadata";
2
2
  import { perEntity, type Generator, type GeneratorFactory } from "../generator.js";
3
3
  import { renderQueriesFile } from "../templates/queries-file.js";
4
4
  import { formatTs } from "../format.js";
@@ -9,9 +9,21 @@ export interface QueriesFileOpts {
9
9
  target?: string;
10
10
  }
11
11
 
12
+ // object.value records have no primary identity, so the rendered queries module
13
+ // emits findById/updateById/deleteById against a non-existent column. Skipping
14
+ // value subtypes is unconditional — the user-supplied filter (if any) is applied
15
+ // on top via boolean AND.
16
+ const skipValueTypes = (e: MetaObject): boolean => e.subType !== OBJECT_SUBTYPE_VALUE;
17
+
12
18
  export const queriesFile = function queriesFile(opts?: QueriesFileOpts): Generator {
19
+ const userFilter = opts?.filter;
20
+ const filter: (e: MetaObject) => boolean = userFilter
21
+ ? (e) => skipValueTypes(e) && userFilter(e)
22
+ : skipValueTypes;
23
+
13
24
  const generator: Generator = {
14
25
  name: "queries-file",
26
+ filter,
15
27
  generate: perEntity(async (entity, ctx) => {
16
28
  if (!ctx.renderContext) {
17
29
  throw new Error("queries-file: renderContext is required (provided by runGen)");
@@ -22,9 +34,6 @@ export const queriesFile = function queriesFile(opts?: QueriesFileOpts): Generat
22
34
  };
23
35
  }),
24
36
  };
25
- if (opts?.filter) {
26
- generator.filter = opts.filter;
27
- }
28
37
  if (opts?.target) {
29
38
  generator.target = opts.target;
30
39
  }
@@ -0,0 +1,48 @@
1
+ import type { MetaObject } from "@metaobjectsdev/metadata";
2
+ import { perEntity, type Generator, type GeneratorFactory } from "../generator.js";
3
+ import { renderRoutesFileHono } from "../templates/routes-file-hono.js";
4
+ import { formatTs } from "../format.js";
5
+ import { entityOutputPath } from "../import-path.js";
6
+
7
+ export interface RoutesFileHonoOpts {
8
+ filter?: (entity: MetaObject) => boolean;
9
+ target?: string;
10
+ }
11
+
12
+ /**
13
+ * Hono variant of routesFile() — emits `<Entity>.routes.hono.ts` mounting
14
+ * the same five CRUD verbs (GET list / GET :id / POST / PATCH+PUT / DELETE)
15
+ * against `@metaobjectsdev/runtime-ts/hono` rather than `…/drizzle-fastify`.
16
+ *
17
+ * Same cross-port wire contract (envelope shape, status codes, filter +
18
+ * sort + withCount semantics) as the Fastify flavor. Workers / Bun / Node
19
+ * consumers running Hono can replace hand-written route registration with
20
+ * this generator output one entity at a time.
21
+ *
22
+ * Per-entity opt-out via `@emitRoutes: false` is honored. If the user
23
+ * supplies their own filter, both must pass (AND).
24
+ */
25
+ export const routesFileHono = function routesFileHono(opts?: RoutesFileHonoOpts): Generator {
26
+ const userFilter = opts?.filter ?? (() => true);
27
+ const generator: Generator = {
28
+ name: "routes-file-hono",
29
+ filter: (e: MetaObject) => e.ownAttr("emitRoutes") !== false && userFilter(e),
30
+ generate: perEntity(async (entity, ctx) => {
31
+ if (!ctx.renderContext) {
32
+ throw new Error("routes-file-hono: renderContext is required (provided by runGen)");
33
+ }
34
+ return {
35
+ path: entityOutputPath(
36
+ ctx.config.outputLayout ?? "flat",
37
+ entity.package,
38
+ `${entity.name}.routes.hono.ts`,
39
+ ),
40
+ content: await formatTs(renderRoutesFileHono(entity, ctx.renderContext)),
41
+ };
42
+ }),
43
+ };
44
+ if (opts?.target) {
45
+ generator.target = opts.target;
46
+ }
47
+ return generator;
48
+ } as GeneratorFactory<RoutesFileHonoOpts>;
package/src/index.ts CHANGED
@@ -9,7 +9,7 @@ export type { RunGenOpts, RunGenResult } from "./runner.js";
9
9
  export type { Generator, GenContext, EmittedFile, GeneratorFactory } from "./generator.js";
10
10
  export { perEntity, oncePerRun } from "./generator.js";
11
11
 
12
- export type { MetaobjectsGenConfig, NormalizedMetaobjectsGenConfig, ResolvedGenConfig, Dialect, ExtStyle, ColumnNamingStrategy } from "./metaobjects-config.js";
12
+ export type { MetaobjectsGenConfig, NormalizedMetaobjectsGenConfig, ResolvedGenConfig, Dialect, ExtStyle, ColumnNamingStrategy, MetaDataTypeProvider } from "./metaobjects-config.js";
13
13
  export { defineConfig, normalizeConfig } from "./metaobjects-config.js";
14
14
 
15
15
  export type { ColumnSpec, DefaultExpr } from "./column-mapper.js";
@@ -44,4 +44,4 @@ export { emitViewDdl } from "./projection/view-ddl-emit.js";
44
44
  export type { EmitOptions as ViewDdlEmitOptions } from "./projection/view-ddl-emit.js";
45
45
  export type { JoinNode, JoinTree, SelectColumn, SelectSpec, ViewSpec } from "./projection/view-spec.js";
46
46
  // Prompt construction (FR-004): typed payload + render-handle codegen.
47
- export { generatePayloadInterfaces, generateRenderHandle } from "./payload-codegen.js";
47
+ export { generatePayloadInterfaces, generatePayloadInterfacesBatch, generateRenderHandle } from "./payload-codegen.js";
@@ -1,9 +1,11 @@
1
+ import { DEFAULT_COLUMN_NAMING_STRATEGY, type ColumnNamingStrategy, type MetaDataTypeProvider } from "@metaobjectsdev/metadata";
1
2
  import type { Generator } from "./generator.js";
2
3
  import type { ExtStyle } from "./render-context.js";
3
4
  import type { OutputLayout, ResolvedTarget } from "./import-path.js";
4
5
 
5
6
  export type Dialect = "sqlite" | "postgres";
6
- export type ColumnNamingStrategy = "snake_case" | "literal" | "kebab-case";
7
+ /** Re-exported from metadata so codegen-ts consumers see one canonical type. */
8
+ export type { ColumnNamingStrategy, MetaDataTypeProvider } from "@metaobjectsdev/metadata";
7
9
  export type { ExtStyle };
8
10
  export type { OutputLayout };
9
11
  export type { ResolvedTarget };
@@ -39,6 +41,13 @@ export interface MetaobjectsGenConfig extends ResolvedGenConfig {
39
41
  targets?: Record<string, TargetConfig>;
40
42
  /** importBase for the default target (top-level outDir). */
41
43
  importBase?: string;
44
+ /**
45
+ * Consumer-supplied {@link MetaDataTypeProvider}s. Threaded to `loadMemory`
46
+ * by the CLI's gen/migrate commands so a project can register its own
47
+ * subtypes/attrs (e.g. a `template.toolcall` subtype) without forking the
48
+ * loader. Composed AFTER the default core+forge bundle.
49
+ */
50
+ providers?: readonly MetaDataTypeProvider[];
42
51
  }
43
52
 
44
53
  /** MetaobjectsGenConfig after applying defaults. All fields required.
@@ -87,7 +96,7 @@ export function resolveTargets(config: MetaobjectsGenConfig): Record<string, Res
87
96
  export function normalizeConfig(config: MetaobjectsGenConfig): NormalizedMetaobjectsGenConfig {
88
97
  return {
89
98
  ...config,
90
- columnNamingStrategy: config.columnNamingStrategy ?? "snake_case",
99
+ columnNamingStrategy: config.columnNamingStrategy ?? DEFAULT_COLUMN_NAMING_STRATEGY,
91
100
  apiPrefix: config.apiPrefix ?? "",
92
101
  outputLayout: config.outputLayout ?? "flat",
93
102
  targets: resolveTargets(config),
package/src/naming.ts CHANGED
@@ -1,32 +1,19 @@
1
1
  // Naming helpers — case conversion + pluralization for codegen output.
2
- // All functions are pure.
2
+ // All functions are pure. The strategy primitives (toSnakeCase, toKebabCase,
3
+ // applyColumnNamingStrategy, pluralize, DEFAULT_COLUMN_NAMING_STRATEGY) are
4
+ // re-exported from @metaobjectsdev/metadata so codegen + runtime + migrate
5
+ // share a single source of truth for how field/table names lower to columns.
3
6
 
4
- import type { ColumnNamingStrategy } from "./metaobjects-config.js";
7
+ import {
8
+ applyColumnNamingStrategy,
9
+ DEFAULT_COLUMN_NAMING_STRATEGY,
10
+ pluralize,
11
+ toKebabCase,
12
+ toSnakeCase,
13
+ type ColumnNamingStrategy,
14
+ } from "@metaobjectsdev/metadata";
5
15
 
6
- /**
7
- * Convert PascalCase or camelCase to snake_case.
8
- * Treats consecutive capitals (e.g., "APIKey") as a single word: "api_key".
9
- */
10
- export function toSnakeCase(s: string): string {
11
- return s
12
- .replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
13
- .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
14
- .toLowerCase();
15
- }
16
-
17
- /** Convert PascalCase or camelCase to kebab-case. */
18
- function toKebabCase(s: string): string {
19
- return toSnakeCase(s).replace(/_/g, "-");
20
- }
21
-
22
- /** Apply a ColumnNamingStrategy to a name. */
23
- function applyStrategy(name: string, strategy: ColumnNamingStrategy): string {
24
- switch (strategy) {
25
- case "snake_case": return toSnakeCase(name);
26
- case "literal": return name;
27
- case "kebab-case": return toKebabCase(name);
28
- }
29
- }
16
+ export { pluralize, toSnakeCase } from "@metaobjectsdev/metadata";
30
17
 
31
18
  /**
32
19
  * Convert snake_case to camelCase. Preserves already-camelCase input.
@@ -42,31 +29,20 @@ export function toPascalCase(s: string): string {
42
29
  return s.length > 0 ? s[0]!.toUpperCase() + s.slice(1) : s;
43
30
  }
44
31
 
45
- /**
46
- * Simple English pluralization. Documented imperfection per design §13 #1:
47
- * irregular plurals (Person → Persons, not People) are not handled.
48
- * Users override via source[dbTable]@name in metadata.
49
- */
50
- export function pluralize(s: string): string {
51
- if (/(s|x|z|ch|sh)$/i.test(s)) return s + "es";
52
- if (/[^aeiou]y$/i.test(s)) return s.slice(0, -1) + "ies";
53
- return s + "s";
54
- }
55
-
56
32
  /** PascalCase entity → strategy-applied plural for DB table name. */
57
33
  export function tableNameFromEntity(
58
34
  entityName: string,
59
- strategy: ColumnNamingStrategy = "snake_case",
35
+ strategy: ColumnNamingStrategy = DEFAULT_COLUMN_NAMING_STRATEGY,
60
36
  ): string {
61
- return applyStrategy(pluralize(entityName), strategy);
37
+ return applyColumnNamingStrategy(pluralize(entityName), strategy);
62
38
  }
63
39
 
64
40
  /** camelCase or PascalCase field → strategy-applied DB column name. */
65
41
  export function columnNameFromField(
66
42
  fieldName: string,
67
- strategy: ColumnNamingStrategy = "snake_case",
43
+ strategy: ColumnNamingStrategy = DEFAULT_COLUMN_NAMING_STRATEGY,
68
44
  ): string {
69
- return applyStrategy(fieldName, strategy);
45
+ return applyColumnNamingStrategy(fieldName, strategy);
70
46
  }
71
47
 
72
48
  /**
@@ -78,14 +54,14 @@ export function viewNameFromProjection(
78
54
  projectionName: string,
79
55
  strategy: ColumnNamingStrategy,
80
56
  ): string {
81
- switch (strategy) {
82
- case "snake_case": return "v_" + toSnakeCase(projectionName);
83
- case "literal": return "v_" + projectionName;
84
- case "kebab-case": return "v-" + toKebabCase(projectionName);
85
- }
57
+ const sep = strategy === "kebab-case" ? "-" : "_";
58
+ return "v" + sep + applyColumnNamingStrategy(projectionName, strategy);
86
59
  }
87
60
 
88
61
  /** PascalCase entity → camelCase plural for the Drizzle table variable. */
89
62
  export function variableNameFromEntity(entityName: string): string {
90
63
  return pluralize(toCamelCase(entityName.charAt(0).toLowerCase() + entityName.slice(1)));
91
64
  }
65
+
66
+ // Re-exported here for callers that import from codegen-ts's naming module.
67
+ export { toKebabCase };
@@ -17,6 +17,7 @@ import {
17
17
  TYPE_TEMPLATE,
18
18
  FIELD_SUBTYPE_OBJECT,
19
19
  FIELD_ATTR_OBJECT_REF,
20
+ FIELD_ATTR_REQUIRED,
20
21
  TEMPLATE_ATTR_PAYLOAD_REF,
21
22
  TEMPLATE_ATTR_TEXT_REF,
22
23
  TEMPLATE_ATTR_FORMAT,
@@ -55,7 +56,13 @@ function fieldTsType(field: MetaData): { type: string; refVo?: string } {
55
56
  if (typeof ref === "string") result.refVo = ref;
56
57
  return result;
57
58
  }
58
- return { type: SCALAR_TS[field.subType] ?? "unknown" };
59
+ const scalar = SCALAR_TS[field.subType] ?? "unknown";
60
+ return { type: field.isArray ? `${scalar}[]` : scalar };
61
+ }
62
+
63
+ /** True iff the field's @required is explicitly set to true. */
64
+ function isFieldRequired(field: MetaData): boolean {
65
+ return field.ownAttr(FIELD_ATTR_REQUIRED) === true;
59
66
  }
60
67
 
61
68
  function emitInterface(root: MetaData, voName: string, emitted: Set<string>, out: string[]): void {
@@ -67,7 +74,15 @@ function emitInterface(root: MetaData, voName: string, emitted: Set<string>, out
67
74
  const refs: string[] = [];
68
75
  for (const f of vo.children().filter((c) => c.type === TYPE_FIELD)) {
69
76
  const { type, refVo } = fieldTsType(f);
70
- lines.push(` ${f.name}: ${type};`);
77
+ // Required fields: `name: T;`
78
+ // Optional fields: `name?: T | null;` — the `| null` lets values from
79
+ // Drizzle entity rows (which return `null` for nullable columns) flow
80
+ // straight in. Without it, TS treats undefined-vs-null as a hard error
81
+ // at the entity → payload boundary.
82
+ const isRequired = isFieldRequired(f);
83
+ const tsType = isRequired ? type : `${type} | null`;
84
+ const optional = isRequired ? "" : "?";
85
+ lines.push(` ${f.name}${optional}: ${tsType};`);
71
86
  if (refVo) refs.push(refVo);
72
87
  }
73
88
  lines.push("}");
@@ -82,6 +97,23 @@ export function generatePayloadInterfaces(root: MetaData, voName: string): strin
82
97
  return out.join("\n\n") + "\n";
83
98
  }
84
99
 
100
+ /**
101
+ * Emit interfaces for several payloads at once, using a single shared dedupe
102
+ * set so nested types (e.g. lens projections referenced by multiple payloads)
103
+ * appear exactly once in the combined output.
104
+ *
105
+ * Returns the empty string when `voNames` is empty.
106
+ */
107
+ export function generatePayloadInterfacesBatch(root: MetaData, voNames: readonly string[]): string {
108
+ if (voNames.length === 0) return "";
109
+ const out: string[] = [];
110
+ const emitted = new Set<string>();
111
+ for (const name of voNames) {
112
+ emitInterface(root, name, emitted, out);
113
+ }
114
+ return out.length === 0 ? "" : out.join("\n\n") + "\n";
115
+ }
116
+
85
117
  function pascal(s: string): string {
86
118
  return s.length > 0 ? s[0]!.toUpperCase() + s.slice(1) : s;
87
119
  }
@@ -348,7 +348,7 @@ function buildGroupBy(spec: SelectSpec): string[] {
348
348
  * and extends a writable entity).
349
349
  * @param root The loader's MetaRoot — all top-level objects are
350
350
  * direct children of root (returned by `MetaDataLoader.load()`
351
- * / `FileMetaDataLoader.loadFiles()` as `result.root`).
351
+ * or `MetaDataLoader.fromDirectory()` as `result.root`).
352
352
  * @param ctx Column naming strategy for SQL identifiers.
353
353
  */
354
354
  export function extractViewSpec(
@@ -0,0 +1,28 @@
1
+ // Source-detect helpers — discriminate table-backed entities from value-only
2
+ // objects (and other in-memory / transit shapes) by inspecting source.* children.
3
+ //
4
+ // Used by the entity-file composer to pick a streamlined "value-only" emission
5
+ // path for metaobjects that declare no writable relational source. Pure
6
+ // metadata-driven, not a typeId discriminator: any object subtype can opt out
7
+ // of Drizzle table emission simply by omitting source.rdb.
8
+
9
+ import { MetaSource } from "@metaobjectsdev/metadata";
10
+ import { TYPE_SOURCE, SOURCE_SUBTYPE_RDB } from "@metaobjectsdev/metadata";
11
+ import type { MetaObject } from "@metaobjectsdev/metadata";
12
+
13
+ /**
14
+ * True when the entity declares at least one writable source.rdb child.
15
+ * Discriminates table-backed entities (full Drizzle file: table + Insert/Update
16
+ * schemas + filter allowlists + constants) from value-only objects (TS
17
+ * interface + Zod schema only). Absence of source.rdb means in-memory /
18
+ * transit data — no migration, no ORM table to point at.
19
+ */
20
+ export function hasWritableRdbSource(entity: MetaObject): boolean {
21
+ for (const child of entity.ownChildren()) {
22
+ if (child.type !== TYPE_SOURCE) continue;
23
+ if (child.subType !== SOURCE_SUBTYPE_RDB) continue;
24
+ if (!(child instanceof MetaSource)) continue;
25
+ if (child.isWritable()) return true;
26
+ }
27
+ return false;
28
+ }