@metaobjectsdev/codegen-ts 0.5.0-rc.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.
Files changed (198) hide show
  1. package/LICENSE +189 -0
  2. package/README.md +101 -0
  3. package/dist/column-mapper.d.ts +38 -0
  4. package/dist/column-mapper.d.ts.map +1 -0
  5. package/dist/column-mapper.js +205 -0
  6. package/dist/column-mapper.js.map +1 -0
  7. package/dist/constants.d.ts +7 -0
  8. package/dist/constants.d.ts.map +1 -0
  9. package/dist/constants.js +8 -0
  10. package/dist/constants.js.map +1 -0
  11. package/dist/errors.d.ts +7 -0
  12. package/dist/errors.d.ts.map +1 -0
  13. package/dist/errors.js +11 -0
  14. package/dist/errors.js.map +1 -0
  15. package/dist/format.d.ts +2 -0
  16. package/dist/format.d.ts.map +1 -0
  17. package/dist/format.js +47 -0
  18. package/dist/format.js.map +1 -0
  19. package/dist/generator.d.ts +44 -0
  20. package/dist/generator.d.ts.map +1 -0
  21. package/dist/generator.js +17 -0
  22. package/dist/generator.js.map +1 -0
  23. package/dist/generators/barrel.d.ts +6 -0
  24. package/dist/generators/barrel.d.ts.map +1 -0
  25. package/dist/generators/barrel.js +17 -0
  26. package/dist/generators/barrel.js.map +1 -0
  27. package/dist/generators/entity-file.d.ts +8 -0
  28. package/dist/generators/entity-file.d.ts.map +1 -0
  29. package/dist/generators/entity-file.js +27 -0
  30. package/dist/generators/entity-file.js.map +1 -0
  31. package/dist/generators/index.d.ts +5 -0
  32. package/dist/generators/index.d.ts.map +1 -0
  33. package/dist/generators/index.js +5 -0
  34. package/dist/generators/index.js.map +1 -0
  35. package/dist/generators/queries-file.d.ts +8 -0
  36. package/dist/generators/queries-file.d.ts.map +1 -0
  37. package/dist/generators/queries-file.js +26 -0
  38. package/dist/generators/queries-file.js.map +1 -0
  39. package/dist/generators/routes-file.d.ts +12 -0
  40. package/dist/generators/routes-file.d.ts.map +1 -0
  41. package/dist/generators/routes-file.js +30 -0
  42. package/dist/generators/routes-file.js.map +1 -0
  43. package/dist/import-path.d.ts +41 -0
  44. package/dist/import-path.d.ts.map +1 -0
  45. package/dist/import-path.js +95 -0
  46. package/dist/import-path.js.map +1 -0
  47. package/dist/index.d.ts +29 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +21 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/metaobjects-config.d.ts +56 -0
  52. package/dist/metaobjects-config.d.ts.map +1 -0
  53. package/dist/metaobjects-config.js +42 -0
  54. package/dist/metaobjects-config.js.map +1 -0
  55. package/dist/naming.d.ts +29 -0
  56. package/dist/naming.d.ts.map +1 -0
  57. package/dist/naming.js +67 -0
  58. package/dist/naming.js.map +1 -0
  59. package/dist/overwrite-policy.d.ts +8 -0
  60. package/dist/overwrite-policy.d.ts.map +1 -0
  61. package/dist/overwrite-policy.js +23 -0
  62. package/dist/overwrite-policy.js.map +1 -0
  63. package/dist/pk-resolver.d.ts +18 -0
  64. package/dist/pk-resolver.d.ts.map +1 -0
  65. package/dist/pk-resolver.js +36 -0
  66. package/dist/pk-resolver.js.map +1 -0
  67. package/dist/projection/extract-view-spec.d.ts +18 -0
  68. package/dist/projection/extract-view-spec.d.ts.map +1 -0
  69. package/dist/projection/extract-view-spec.js +272 -0
  70. package/dist/projection/extract-view-spec.js.map +1 -0
  71. package/dist/projection/index.d.ts +5 -0
  72. package/dist/projection/index.d.ts.map +1 -0
  73. package/dist/projection/index.js +5 -0
  74. package/dist/projection/index.js.map +1 -0
  75. package/dist/projection/projection-detector.d.ts +4 -0
  76. package/dist/projection/projection-detector.d.ts.map +1 -0
  77. package/dist/projection/projection-detector.js +13 -0
  78. package/dist/projection/projection-detector.js.map +1 -0
  79. package/dist/projection/view-ddl-emit.d.ts +10 -0
  80. package/dist/projection/view-ddl-emit.d.ts.map +1 -0
  81. package/dist/projection/view-ddl-emit.js +47 -0
  82. package/dist/projection/view-ddl-emit.js.map +1 -0
  83. package/dist/projection/view-spec.d.ts +56 -0
  84. package/dist/projection/view-spec.d.ts.map +1 -0
  85. package/dist/projection/view-spec.js +2 -0
  86. package/dist/projection/view-spec.js.map +1 -0
  87. package/dist/relation-resolver.d.ts +21 -0
  88. package/dist/relation-resolver.d.ts.map +1 -0
  89. package/dist/relation-resolver.js +62 -0
  90. package/dist/relation-resolver.js.map +1 -0
  91. package/dist/render-context.d.ts +65 -0
  92. package/dist/render-context.d.ts.map +1 -0
  93. package/dist/render-context.js +28 -0
  94. package/dist/render-context.js.map +1 -0
  95. package/dist/runner.d.ts +17 -0
  96. package/dist/runner.d.ts.map +1 -0
  97. package/dist/runner.js +135 -0
  98. package/dist/runner.js.map +1 -0
  99. package/dist/templates/barrel.d.ts +8 -0
  100. package/dist/templates/barrel.d.ts.map +1 -0
  101. package/dist/templates/barrel.js +12 -0
  102. package/dist/templates/barrel.js.map +1 -0
  103. package/dist/templates/drizzle-schema.d.ts +13 -0
  104. package/dist/templates/drizzle-schema.d.ts.map +1 -0
  105. package/dist/templates/drizzle-schema.js +251 -0
  106. package/dist/templates/drizzle-schema.js.map +1 -0
  107. package/dist/templates/entity-constants.d.ts +4 -0
  108. package/dist/templates/entity-constants.d.ts.map +1 -0
  109. package/dist/templates/entity-constants.js +215 -0
  110. package/dist/templates/entity-constants.js.map +1 -0
  111. package/dist/templates/entity-file.d.ts +4 -0
  112. package/dist/templates/entity-file.d.ts.map +1 -0
  113. package/dist/templates/entity-file.js +45 -0
  114. package/dist/templates/entity-file.js.map +1 -0
  115. package/dist/templates/field-meta.d.ts +24 -0
  116. package/dist/templates/field-meta.d.ts.map +1 -0
  117. package/dist/templates/field-meta.js +117 -0
  118. package/dist/templates/field-meta.js.map +1 -0
  119. package/dist/templates/filter-allowlist.d.ts +5 -0
  120. package/dist/templates/filter-allowlist.d.ts.map +1 -0
  121. package/dist/templates/filter-allowlist.js +86 -0
  122. package/dist/templates/filter-allowlist.js.map +1 -0
  123. package/dist/templates/filter-shared.d.ts +15 -0
  124. package/dist/templates/filter-shared.d.ts.map +1 -0
  125. package/dist/templates/filter-shared.js +30 -0
  126. package/dist/templates/filter-shared.js.map +1 -0
  127. package/dist/templates/filter-type.d.ts +4 -0
  128. package/dist/templates/filter-type.d.ts.map +1 -0
  129. package/dist/templates/filter-type.js +78 -0
  130. package/dist/templates/filter-type.js.map +1 -0
  131. package/dist/templates/inferred-types.d.ts +4 -0
  132. package/dist/templates/inferred-types.d.ts.map +1 -0
  133. package/dist/templates/inferred-types.js +14 -0
  134. package/dist/templates/inferred-types.js.map +1 -0
  135. package/dist/templates/projection-decl.d.ts +21 -0
  136. package/dist/templates/projection-decl.d.ts.map +1 -0
  137. package/dist/templates/projection-decl.js +116 -0
  138. package/dist/templates/projection-decl.js.map +1 -0
  139. package/dist/templates/queries-file.d.ts +4 -0
  140. package/dist/templates/queries-file.d.ts.map +1 -0
  141. package/dist/templates/queries-file.js +39 -0
  142. package/dist/templates/queries-file.js.map +1 -0
  143. package/dist/templates/queries.d.ts +9 -0
  144. package/dist/templates/queries.d.ts.map +1 -0
  145. package/dist/templates/queries.js +115 -0
  146. package/dist/templates/queries.js.map +1 -0
  147. package/dist/templates/relations-block.d.ts +9 -0
  148. package/dist/templates/relations-block.d.ts.map +1 -0
  149. package/dist/templates/relations-block.js +45 -0
  150. package/dist/templates/relations-block.js.map +1 -0
  151. package/dist/templates/routes-file.d.ts +4 -0
  152. package/dist/templates/routes-file.d.ts.map +1 -0
  153. package/dist/templates/routes-file.js +158 -0
  154. package/dist/templates/routes-file.js.map +1 -0
  155. package/dist/templates/zod-validators.d.ts +4 -0
  156. package/dist/templates/zod-validators.d.ts.map +1 -0
  157. package/dist/templates/zod-validators.js +129 -0
  158. package/dist/templates/zod-validators.js.map +1 -0
  159. package/package.json +59 -0
  160. package/src/column-mapper.ts +266 -0
  161. package/src/constants.ts +10 -0
  162. package/src/errors.ts +10 -0
  163. package/src/format.ts +50 -0
  164. package/src/generator.ts +73 -0
  165. package/src/generators/barrel.ts +28 -0
  166. package/src/generators/entity-file.ts +33 -0
  167. package/src/generators/index.ts +4 -0
  168. package/src/generators/queries-file.ts +32 -0
  169. package/src/generators/routes-file.ts +36 -0
  170. package/src/import-path.ts +153 -0
  171. package/src/index.ts +45 -0
  172. package/src/metaobjects-config.ts +95 -0
  173. package/src/naming.ts +84 -0
  174. package/src/overwrite-policy.ts +39 -0
  175. package/src/pk-resolver.ts +47 -0
  176. package/src/projection/extract-view-spec.ts +372 -0
  177. package/src/projection/index.ts +4 -0
  178. package/src/projection/projection-detector.ts +26 -0
  179. package/src/projection/view-ddl-emit.ts +66 -0
  180. package/src/projection/view-spec.ts +62 -0
  181. package/src/relation-resolver.ts +87 -0
  182. package/src/render-context.ts +93 -0
  183. package/src/runner.ts +178 -0
  184. package/src/templates/barrel.ts +23 -0
  185. package/src/templates/drizzle-schema.ts +286 -0
  186. package/src/templates/entity-constants.ts +248 -0
  187. package/src/templates/entity-file.ts +51 -0
  188. package/src/templates/field-meta.ts +150 -0
  189. package/src/templates/filter-allowlist.ts +104 -0
  190. package/src/templates/filter-shared.ts +30 -0
  191. package/src/templates/filter-type.ts +93 -0
  192. package/src/templates/inferred-types.ts +16 -0
  193. package/src/templates/projection-decl.ts +146 -0
  194. package/src/templates/queries-file.ts +56 -0
  195. package/src/templates/queries.ts +132 -0
  196. package/src/templates/relations-block.ts +65 -0
  197. package/src/templates/routes-file.ts +179 -0
  198. package/src/templates/zod-validators.ts +140 -0
@@ -0,0 +1,153 @@
1
+ // Package-driven output placement — path + import-specifier computation.
2
+ // In "flat" mode every function returns exactly today's value, so flat
3
+ // output is byte-identical. See
4
+ // docs/superpowers/specs/2026-05-18-phase4d-package-output-placement-design.md.
5
+
6
+ import { relative as posixRelative } from "node:path/posix";
7
+ import { PACKAGE_SEPARATOR } from "@metaobjectsdev/metadata";
8
+ import { withExt, type ExtStyle } from "./render-context.js";
9
+
10
+ export type OutputLayout = "flat" | "package";
11
+
12
+ /** "a::b::c" → "a/b/c"; undefined / "" → "". */
13
+ export function packageToPath(pkg: string | undefined): string {
14
+ if (pkg === undefined || pkg === "") return "";
15
+ return pkg.split(PACKAGE_SEPARATOR).join("/");
16
+ }
17
+
18
+ /** Output path (relative to outDir) for an entity's generated file. */
19
+ export function entityOutputPath(
20
+ layout: OutputLayout,
21
+ pkg: string | undefined,
22
+ filename: string,
23
+ ): string {
24
+ if (layout === "flat") return filename;
25
+ const dir = packageToPath(pkg);
26
+ return dir === "" ? filename : `${dir}/${filename}`;
27
+ }
28
+
29
+ /** Relative dir prefix (ending in "/") from `fromDir` to `toDir`, both
30
+ * POSIX paths relative to outDir. Same dir → "./". */
31
+ function relativeDirPrefix(fromDir: string, toDir: string): string {
32
+ let rel = posixRelative(fromDir, toDir);
33
+ if (rel === "") rel = ".";
34
+ if (!rel.startsWith(".")) rel = `./${rel}`;
35
+ return `${rel}/`;
36
+ }
37
+
38
+ /** Module specifier to import `toEntity` (in `toPkg`) from a file in `fromPkg`.
39
+ * Flat → always "./<toEntity>". */
40
+ export function crossEntitySpecifier(
41
+ layout: OutputLayout,
42
+ fromPkg: string | undefined,
43
+ toPkg: string | undefined,
44
+ toEntity: string,
45
+ extStyle: ExtStyle,
46
+ ): string {
47
+ if (layout === "flat") return withExt(`./${toEntity}`, extStyle);
48
+ const prefix = relativeDirPrefix(packageToPath(fromPkg), packageToPath(toPkg));
49
+ return withExt(`${prefix}${toEntity}`, extStyle);
50
+ }
51
+
52
+ /** Barrel (at outDir root) re-export specifier for an entity.
53
+ * Equivalent to crossEntitySpecifier with fromPkg=undefined (barrel is always at root). */
54
+ export function barrelEntrySpecifier(
55
+ layout: OutputLayout,
56
+ pkg: string | undefined,
57
+ entity: string,
58
+ extStyle: ExtStyle,
59
+ ): string {
60
+ return crossEntitySpecifier(layout, undefined, pkg, entity, extStyle);
61
+ }
62
+
63
+ /** A `dbImport` (or any module specifier) adjusted for a file at the given
64
+ * package depth. A non-relative specifier (alias / package) is depth-invariant
65
+ * and returned unchanged; a relative one gets extra "../" per package segment.
66
+ * Caller contract: a relative moduleSpec must be relative to outDir root (as
67
+ * dbImport is). */
68
+ export function relativeModuleSpecifier(
69
+ layout: OutputLayout,
70
+ pkg: string | undefined,
71
+ moduleSpec: string,
72
+ ): string {
73
+ if (layout === "flat") return moduleSpec;
74
+ const isRelative = moduleSpec.startsWith("./") || moduleSpec.startsWith("../");
75
+ if (!isRelative) return moduleSpec;
76
+ const dir = packageToPath(pkg);
77
+ const depth = dir === "" ? 0 : dir.split("/").length;
78
+ if (depth === 0) return moduleSpec;
79
+ const extra = "../".repeat(depth);
80
+ return moduleSpec.startsWith("./") ? extra + moduleSpec.slice(2) : extra + moduleSpec;
81
+ }
82
+
83
+ /** A fully-resolved output destination. Import-identity belongs to the
84
+ * destination, not the generator. */
85
+ export interface ResolvedTarget {
86
+ name: string;
87
+ outDir: string;
88
+ /** Package-specifier prefix others use to import modules produced here.
89
+ * Required only when another target imports from this one. */
90
+ importBase: string | undefined;
91
+ outputLayout: OutputLayout;
92
+ dbImport: string;
93
+ }
94
+
95
+ /** importBase + (package path when package layout) + entity, extension-less. */
96
+ function crossTargetEntityPath(
97
+ entityTarget: ResolvedTarget,
98
+ entityPkg: string | undefined,
99
+ entityName: string,
100
+ ): string {
101
+ const base = entityTarget.importBase;
102
+ if (base === undefined) {
103
+ throw new Error(
104
+ `Cannot emit cross-target import: target "${entityTarget.name}" has no importBase. ` +
105
+ `Set importBase on the target that holds the entity modules.`,
106
+ );
107
+ }
108
+ const pkgPath = entityTarget.outputLayout === "package" ? packageToPath(entityPkg) : "";
109
+ return pkgPath === "" ? `${base}/${entityName}` : `${base}/${pkgPath}/${entityName}`;
110
+ }
111
+
112
+ /** Specifier to import entity `entityName` (in `entityPkg`, produced into
113
+ * `entityTarget`) from a file emitted into `selfTarget`. Same target → relative
114
+ * (extStyle honored); cross target → extension-less importBase path. */
115
+ export function entityModuleSpecifier(
116
+ selfTarget: ResolvedTarget,
117
+ entityTarget: ResolvedTarget,
118
+ entityPkg: string | undefined,
119
+ entityName: string,
120
+ extStyle: ExtStyle,
121
+ ): string {
122
+ if (selfTarget.name === entityTarget.name) {
123
+ return crossEntitySpecifier(entityTarget.outputLayout, entityPkg, entityPkg, entityName, extStyle);
124
+ }
125
+ return crossTargetEntityPath(entityTarget, entityPkg, entityName);
126
+ }
127
+
128
+ /** A same-target sibling module (e.g. "<Entity>.columns"). Always relative,
129
+ * package-layout aware, extStyle honored. */
130
+ export function siblingSpecifier(
131
+ selfTarget: ResolvedTarget,
132
+ entityPkg: string | undefined,
133
+ basename: string,
134
+ extStyle: ExtStyle,
135
+ ): string {
136
+ return crossEntitySpecifier(selfTarget.outputLayout, entityPkg, entityPkg, basename, extStyle);
137
+ }
138
+
139
+ /** Barrel re-export specifier. Barrel sits at its target root, so same-target
140
+ * uses fromPkg=undefined (barrelEntrySpecifier); cross-target is the
141
+ * extension-less importBase path. */
142
+ export function barrelModuleSpecifier(
143
+ selfTarget: ResolvedTarget,
144
+ entityTarget: ResolvedTarget,
145
+ entityPkg: string | undefined,
146
+ entityName: string,
147
+ extStyle: ExtStyle,
148
+ ): string {
149
+ if (selfTarget.name === entityTarget.name) {
150
+ return barrelEntrySpecifier(entityTarget.outputLayout, entityPkg, entityName, extStyle);
151
+ }
152
+ return crossTargetEntityPath(entityTarget, entityPkg, entityName);
153
+ }
package/src/index.ts ADDED
@@ -0,0 +1,45 @@
1
+ // Public API surface for @metaobjectsdev/codegen-ts.
2
+ //
3
+ // Architecture: Vite-style plugin model.
4
+ // See docs/superpowers/specs/2026-05-12-pluggable-generators-design.md.
5
+
6
+ export { runGen } from "./runner.js";
7
+ export type { RunGenOpts, RunGenResult } from "./runner.js";
8
+
9
+ export type { Generator, GenContext, EmittedFile, GeneratorFactory } from "./generator.js";
10
+ export { perEntity, oncePerRun } from "./generator.js";
11
+
12
+ export type { MetaobjectsGenConfig, NormalizedMetaobjectsGenConfig, ResolvedGenConfig, Dialect, ExtStyle, ColumnNamingStrategy } from "./metaobjects-config.js";
13
+ export { defineConfig, normalizeConfig } from "./metaobjects-config.js";
14
+
15
+ export type { ColumnSpec, DefaultExpr } from "./column-mapper.js";
16
+ export { mapColumnType } from "./column-mapper.js";
17
+
18
+ export type { PkInfo } from "./pk-resolver.js";
19
+ export { buildPkMap } from "./pk-resolver.js";
20
+
21
+ export type { RelationEntry, RelationMap } from "./relation-resolver.js";
22
+ export { buildRelationMap } from "./relation-resolver.js";
23
+
24
+ export type { RenderContext } from "./render-context.js";
25
+ export { makeRenderContext } from "./render-context.js";
26
+
27
+ export type { WriteStatus, WriteResult, MergeStrategy } from "./overwrite-policy.js";
28
+ export { decideAndWrite } from "./overwrite-policy.js";
29
+
30
+ export { CodegenError } from "./errors.js";
31
+ export { GENERATED_HEADER, EXTRA_SUFFIX, DEFAULT_OUT_DIR } from "./constants.js";
32
+
33
+ export { formatTs } from "./format.js";
34
+
35
+ export { pluralize, columnNameFromField, tableNameFromEntity, viewNameFromProjection } from "./naming.js";
36
+
37
+ export { packageToPath, entityOutputPath, crossEntitySpecifier, barrelEntrySpecifier, relativeModuleSpecifier, entityModuleSpecifier, siblingSpecifier, barrelModuleSpecifier } from "./import-path.js";
38
+ export type { OutputLayout, ResolvedTarget } from "./import-path.js";
39
+
40
+ export { isProjection, isWriteThrough } from "./projection/projection-detector.js";
41
+ export { extractViewSpec } from "./projection/extract-view-spec.js";
42
+ export type { ExtractContext } from "./projection/extract-view-spec.js";
43
+ export { emitViewDdl } from "./projection/view-ddl-emit.js";
44
+ export type { EmitOptions as ViewDdlEmitOptions } from "./projection/view-ddl-emit.js";
45
+ export type { JoinNode, JoinTree, SelectColumn, SelectSpec, ViewSpec } from "./projection/view-spec.js";
@@ -0,0 +1,95 @@
1
+ import type { Generator } from "./generator.js";
2
+ import type { ExtStyle } from "./render-context.js";
3
+ import type { OutputLayout, ResolvedTarget } from "./import-path.js";
4
+
5
+ export type Dialect = "sqlite" | "postgres";
6
+ export type ColumnNamingStrategy = "snake_case" | "literal" | "kebab-case";
7
+ export type { ExtStyle };
8
+ export type { OutputLayout };
9
+ export type { ResolvedTarget };
10
+
11
+ /** The implicit target synthesized from top-level config (outDir/outputLayout/dbImport). */
12
+ export const DEFAULT_TARGET_NAME = "default";
13
+
14
+ /** User-facing per-target output config. */
15
+ export interface TargetConfig {
16
+ outDir: string;
17
+ importBase?: string;
18
+ outputLayout?: OutputLayout;
19
+ dbImport?: string;
20
+ }
21
+
22
+ /** Subset of MetaobjectsGenConfig surfaced to generators via GenContext. */
23
+ export interface ResolvedGenConfig {
24
+ outDir: string;
25
+ extStyle: ExtStyle;
26
+ dbImport: string;
27
+ dialect: Dialect;
28
+ /** "flat" (default) — all files in outDir; "package" — files placed in a sub-path derived from each entity's metadata package. */
29
+ outputLayout?: OutputLayout;
30
+ }
31
+
32
+ export interface MetaobjectsGenConfig extends ResolvedGenConfig {
33
+ generators: Generator[];
34
+ /** How field names map to DB column names when @dbColumn is omitted. Defaults to "snake_case". */
35
+ columnNamingStrategy?: ColumnNamingStrategy;
36
+ /** Path prefix applied to generated route registrations + hook fetch URLs. Defaults to "". */
37
+ apiPrefix?: string;
38
+ /** Named output destinations. Generators reference one via `target`. */
39
+ targets?: Record<string, TargetConfig>;
40
+ /** importBase for the default target (top-level outDir). */
41
+ importBase?: string;
42
+ }
43
+
44
+ /** MetaobjectsGenConfig after applying defaults. All fields required.
45
+ * `targets` is Omitted from the base so it can narrow from the user-facing
46
+ * TargetConfig to the fully-resolved ResolvedTarget (incompatible under
47
+ * exactOptionalPropertyTypes otherwise). */
48
+ export interface NormalizedMetaobjectsGenConfig extends Omit<MetaobjectsGenConfig, "targets"> {
49
+ columnNamingStrategy: ColumnNamingStrategy;
50
+ apiPrefix: string;
51
+ outputLayout: OutputLayout;
52
+ targets: Record<string, ResolvedTarget>;
53
+ }
54
+
55
+ /** Identity passthrough; exists for IDE type-inference + autocomplete. */
56
+ export function defineConfig(config: MetaobjectsGenConfig): MetaobjectsGenConfig {
57
+ return config;
58
+ }
59
+
60
+ /** Synthesize the implicit "default" target from top-level fields and resolve
61
+ * each named target (outputLayout + dbImport fall back to top-level;
62
+ * importBase does NOT inherit — it is a per-target identity). */
63
+ export function resolveTargets(config: MetaobjectsGenConfig): Record<string, ResolvedTarget> {
64
+ const layout: OutputLayout = config.outputLayout ?? "flat";
65
+ const out: Record<string, ResolvedTarget> = {
66
+ [DEFAULT_TARGET_NAME]: {
67
+ name: DEFAULT_TARGET_NAME,
68
+ outDir: config.outDir,
69
+ importBase: config.importBase,
70
+ outputLayout: layout,
71
+ dbImport: config.dbImport,
72
+ },
73
+ };
74
+ for (const [name, t] of Object.entries(config.targets ?? {})) {
75
+ out[name] = {
76
+ name,
77
+ outDir: t.outDir,
78
+ importBase: t.importBase,
79
+ outputLayout: t.outputLayout ?? layout,
80
+ dbImport: t.dbImport ?? config.dbImport,
81
+ };
82
+ }
83
+ return out;
84
+ }
85
+
86
+ /** Apply defaults to a MetaobjectsGenConfig, returning a NormalizedMetaobjectsGenConfig. */
87
+ export function normalizeConfig(config: MetaobjectsGenConfig): NormalizedMetaobjectsGenConfig {
88
+ return {
89
+ ...config,
90
+ columnNamingStrategy: config.columnNamingStrategy ?? "snake_case",
91
+ apiPrefix: config.apiPrefix ?? "",
92
+ outputLayout: config.outputLayout ?? "flat",
93
+ targets: resolveTargets(config),
94
+ };
95
+ }
package/src/naming.ts ADDED
@@ -0,0 +1,84 @@
1
+ // Naming helpers — case conversion + pluralization for codegen output.
2
+ // All functions are pure.
3
+
4
+ import type { ColumnNamingStrategy } from "./metaobjects-config.js";
5
+
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
+ }
30
+
31
+ /**
32
+ * Convert snake_case to camelCase. Preserves already-camelCase input.
33
+ */
34
+ export function toCamelCase(s: string): string {
35
+ return s.replace(/_([a-z])/g, (_, c: string) => c.toUpperCase());
36
+ }
37
+
38
+ /**
39
+ * Simple English pluralization. Documented imperfection per design §13 #1:
40
+ * irregular plurals (Person → Persons, not People) are not handled.
41
+ * Users override via source[dbTable]@name in metadata.
42
+ */
43
+ export function pluralize(s: string): string {
44
+ if (/(s|x|z|ch|sh)$/i.test(s)) return s + "es";
45
+ if (/[^aeiou]y$/i.test(s)) return s.slice(0, -1) + "ies";
46
+ return s + "s";
47
+ }
48
+
49
+ /** PascalCase entity → strategy-applied plural for DB table name. */
50
+ export function tableNameFromEntity(
51
+ entityName: string,
52
+ strategy: ColumnNamingStrategy = "snake_case",
53
+ ): string {
54
+ return applyStrategy(pluralize(entityName), strategy);
55
+ }
56
+
57
+ /** camelCase or PascalCase field → strategy-applied DB column name. */
58
+ export function columnNameFromField(
59
+ fieldName: string,
60
+ strategy: ColumnNamingStrategy = "snake_case",
61
+ ): string {
62
+ return applyStrategy(fieldName, strategy);
63
+ }
64
+
65
+ /**
66
+ * PascalCase projection name → "v_" prefix + strategy applied (not pluralized).
67
+ * E.g. "ProgramSummary" + snake_case → "v_program_summary".
68
+ * With kebab-case the separator prefix is "v-" to stay consistent.
69
+ */
70
+ export function viewNameFromProjection(
71
+ projectionName: string,
72
+ strategy: ColumnNamingStrategy,
73
+ ): string {
74
+ switch (strategy) {
75
+ case "snake_case": return "v_" + toSnakeCase(projectionName);
76
+ case "literal": return "v_" + projectionName;
77
+ case "kebab-case": return "v-" + toKebabCase(projectionName);
78
+ }
79
+ }
80
+
81
+ /** PascalCase entity → camelCase plural for the Drizzle table variable. */
82
+ export function variableNameFromEntity(entityName: string): string {
83
+ return pluralize(toCamelCase(entityName.charAt(0).toLowerCase() + entityName.slice(1)));
84
+ }
@@ -0,0 +1,39 @@
1
+ // Overwrite policy: drives the per-file write decision based on the @generated header.
2
+ // Per design §8 — read-only generated files; refuse to clobber hand-written code.
3
+
4
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
5
+ import { dirname } from "node:path";
6
+ import { GENERATED_HEADER } from "./constants.js";
7
+
8
+ export type WriteStatus = "new" | "overwrite" | "refused" | "skipped";
9
+ export type MergeStrategy = "overwrite" | "skip-existing";
10
+
11
+ export interface WriteResult {
12
+ path: string;
13
+ status: WriteStatus;
14
+ }
15
+
16
+ export function decideAndWrite(
17
+ path: string,
18
+ content: string,
19
+ strategy: MergeStrategy = "overwrite",
20
+ ): WriteResult {
21
+ // 'skip-existing' only skips overwrites, not new files.
22
+ if (!existsSync(path)) {
23
+ mkdirSync(dirname(path), { recursive: true });
24
+ writeFileSync(path, content);
25
+ return { path, status: "new" };
26
+ }
27
+
28
+ const current = readFileSync(path, "utf-8");
29
+ if (!current.includes(GENERATED_HEADER)) {
30
+ return { path, status: "refused" };
31
+ }
32
+
33
+ if (strategy === "skip-existing") {
34
+ return { path, status: "skipped" };
35
+ }
36
+
37
+ writeFileSync(path, content);
38
+ return { path, status: "overwrite" };
39
+ }
@@ -0,0 +1,47 @@
1
+ // PK resolver — pre-pass over the loaded metadata to build a name → PK info map.
2
+ // codegen uses this when emitting FK columns (per design §13 #2).
3
+
4
+ import type { MetaRoot } from "@metaobjectsdev/metadata";
5
+ import {
6
+ FIELD_SUBTYPE_LONG,
7
+ IDENTITY_ATTR_FIELDS,
8
+ IDENTITY_ATTR_GENERATION,
9
+ } from "@metaobjectsdev/metadata";
10
+
11
+ export interface PkInfo {
12
+ /** Field name within the entity (e.g., "id"). */
13
+ fieldName: string;
14
+ /** Field subType (e.g., "long", "string", "int"). */
15
+ fieldSubType: string;
16
+ /** Generation strategy: 'increment' | 'uuid' | 'assigned' (or undefined). */
17
+ generation?: string;
18
+ }
19
+
20
+ /**
21
+ * Walk the loaded metadata root, find each object's primary identity,
22
+ * resolve the referenced field's subType, and build a name → PkInfo map.
23
+ *
24
+ * Used by FK column emission: when Post.authorId references User, codegen
25
+ * looks up User's PK info to choose the matching FK column type.
26
+ */
27
+ export function buildPkMap(root: MetaRoot): Map<string, PkInfo> {
28
+ const result = new Map<string, PkInfo>();
29
+ for (const obj of root.objects()) {
30
+ // primaryIdentity() resolves the primary identity across the super-chain.
31
+ const primary = obj.primaryIdentity();
32
+ if (!primary) continue;
33
+ const fields = primary.ownAttr(IDENTITY_ATTR_FIELDS);
34
+ if (!Array.isArray(fields) && typeof fields !== "string") continue;
35
+ const fieldsList = Array.isArray(fields) ? fields : [fields];
36
+ if (fieldsList.length === 0) continue;
37
+ const pkFieldName = String(fieldsList[0]);
38
+ // findField() resolves the field across the super-chain (handles extends:).
39
+ const pkField = obj.findField(pkFieldName);
40
+ const fieldSubType = pkField?.subType ?? FIELD_SUBTYPE_LONG; // sane default
41
+ const generation = primary.ownAttr(IDENTITY_ATTR_GENERATION);
42
+ const info: PkInfo = { fieldName: pkFieldName, fieldSubType };
43
+ if (typeof generation === "string") info.generation = generation;
44
+ result.set(obj.name, info);
45
+ }
46
+ return result;
47
+ }