@metaobjectsdev/codegen-ts 1.0.0-rc.4 → 1.0.0-rc.5
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/dist/column-mapper.d.ts +12 -0
- package/dist/column-mapper.d.ts.map +1 -1
- package/dist/column-mapper.js +16 -0
- package/dist/column-mapper.js.map +1 -1
- package/dist/generator-registry.d.ts.map +1 -1
- package/dist/generator-registry.js +1 -0
- package/dist/generator-registry.js.map +1 -1
- package/dist/generators/agent-ui-page.d.ts.map +1 -1
- package/dist/generators/agent-ui-page.js +11 -9
- package/dist/generators/agent-ui-page.js.map +1 -1
- package/dist/generators/api-model.d.ts +2 -1
- package/dist/generators/api-model.d.ts.map +1 -1
- package/dist/generators/api-model.js +12 -6
- package/dist/generators/api-model.js.map +1 -1
- package/dist/generators/barrel.d.ts.map +1 -1
- package/dist/generators/barrel.js +2 -1
- package/dist/generators/barrel.js.map +1 -1
- package/dist/generators/callable-file.d.ts.map +1 -1
- package/dist/generators/callable-file.js +2 -1
- package/dist/generators/callable-file.js.map +1 -1
- package/dist/generators/docs-data-builder.d.ts.map +1 -1
- package/dist/generators/docs-data-builder.js +6 -5
- package/dist/generators/docs-data-builder.js.map +1 -1
- package/dist/generators/entity-file.d.ts.map +1 -1
- package/dist/generators/entity-file.js +2 -1
- package/dist/generators/entity-file.js.map +1 -1
- package/dist/generators/names-file.d.ts +22 -1
- package/dist/generators/names-file.d.ts.map +1 -1
- package/dist/generators/names-file.js +12 -18
- package/dist/generators/names-file.js.map +1 -1
- package/dist/generators/queries-file.d.ts.map +1 -1
- package/dist/generators/queries-file.js +2 -1
- package/dist/generators/queries-file.js.map +1 -1
- package/dist/generators/render-helper-file.js +1 -1
- package/dist/generators/render-helper-file.js.map +1 -1
- package/dist/generators/requirements-markdown.d.ts.map +1 -1
- package/dist/generators/requirements-markdown.js +26 -1
- package/dist/generators/requirements-markdown.js.map +1 -1
- package/dist/generators/routes-file-hono.d.ts.map +1 -1
- package/dist/generators/routes-file-hono.js +2 -1
- package/dist/generators/routes-file-hono.js.map +1 -1
- package/dist/generators/routes-file.d.ts.map +1 -1
- package/dist/generators/routes-file.js +2 -1
- package/dist/generators/routes-file.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -5
- package/dist/index.js.map +1 -1
- package/dist/names.d.ts.map +1 -1
- package/dist/names.js +5 -4
- package/dist/names.js.map +1 -1
- package/dist/overwrite-policy.d.ts +9 -7
- package/dist/overwrite-policy.d.ts.map +1 -1
- package/dist/overwrite-policy.js +22 -0
- package/dist/overwrite-policy.js.map +1 -1
- package/dist/runner.d.ts +24 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +96 -8
- package/dist/runner.js.map +1 -1
- package/dist/templates/callable-file.d.ts.map +1 -1
- package/dist/templates/callable-file.js +2 -1
- package/dist/templates/callable-file.js.map +1 -1
- package/dist/templates/drizzle-schema.d.ts.map +1 -1
- package/dist/templates/drizzle-schema.js +39 -14
- package/dist/templates/drizzle-schema.js.map +1 -1
- package/dist/templates/entity-file.d.ts.map +1 -1
- package/dist/templates/entity-file.js +4 -2
- package/dist/templates/entity-file.js.map +1 -1
- package/dist/templates/filter-allowlist.d.ts.map +1 -1
- package/dist/templates/filter-allowlist.js +10 -6
- package/dist/templates/filter-allowlist.js.map +1 -1
- package/dist/templates/filter-shared.d.ts +42 -1
- package/dist/templates/filter-shared.d.ts.map +1 -1
- package/dist/templates/filter-shared.js +68 -5
- package/dist/templates/filter-shared.js.map +1 -1
- package/dist/templates/inferred-types.d.ts.map +1 -1
- package/dist/templates/inferred-types.js +9 -6
- package/dist/templates/inferred-types.js.map +1 -1
- package/dist/templates/projection-decl.d.ts.map +1 -1
- package/dist/templates/projection-decl.js +4 -2
- package/dist/templates/projection-decl.js.map +1 -1
- package/dist/templates/queries-file.d.ts.map +1 -1
- package/dist/templates/queries-file.js +6 -5
- package/dist/templates/queries-file.js.map +1 -1
- package/dist/templates/relations-block.d.ts.map +1 -1
- package/dist/templates/relations-block.js +2 -1
- package/dist/templates/relations-block.js.map +1 -1
- package/dist/templates/render-helper.d.ts +12 -1
- package/dist/templates/render-helper.d.ts.map +1 -1
- package/dist/templates/render-helper.js +30 -7
- package/dist/templates/render-helper.js.map +1 -1
- package/dist/templates/routes-file-hono.d.ts.map +1 -1
- package/dist/templates/routes-file-hono.js +2 -1
- package/dist/templates/routes-file-hono.js.map +1 -1
- package/dist/templates/routes-file.d.ts.map +1 -1
- package/dist/templates/routes-file.js +18 -10
- package/dist/templates/routes-file.js.map +1 -1
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +7 -4
- package/dist/templates/zod-validators.js.map +1 -1
- package/package.json +6 -6
- package/src/column-mapper.ts +17 -0
- package/src/generator-registry.ts +1 -0
- package/src/generators/agent-ui-page.ts +10 -10
- package/src/generators/api-model.ts +14 -7
- package/src/generators/barrel.ts +2 -1
- package/src/generators/callable-file.ts +2 -1
- package/src/generators/docs-data-builder.ts +6 -5
- package/src/generators/entity-file.ts +2 -1
- package/src/generators/names-file.ts +33 -4
- package/src/generators/queries-file.ts +2 -1
- package/src/generators/render-helper-file.ts +1 -1
- package/src/generators/requirements-markdown.ts +28 -1
- package/src/generators/routes-file-hono.ts +2 -1
- package/src/generators/routes-file.ts +2 -1
- package/src/index.ts +17 -5
- package/src/names.ts +5 -4
- package/src/overwrite-policy.ts +19 -9
- package/src/reference/barrel.ts +2 -1
- package/src/reference/entity.ts +2 -2
- package/src/reference/names.ts +26 -4
- package/src/reference/queries.ts +3 -3
- package/src/reference/routes-hono.ts +2 -1
- package/src/reference/routes.ts +2 -1
- package/src/runner.ts +112 -7
- package/src/templates/callable-file.ts +2 -1
- package/src/templates/drizzle-schema.ts +43 -13
- package/src/templates/entity-file.ts +4 -2
- package/src/templates/filter-allowlist.ts +9 -6
- package/src/templates/filter-shared.ts +91 -6
- package/src/templates/inferred-types.ts +9 -6
- package/src/templates/projection-decl.ts +4 -2
- package/src/templates/queries-file.ts +6 -5
- package/src/templates/relations-block.ts +2 -1
- package/src/templates/render-helper.ts +31 -6
- package/src/templates/routes-file-hono.ts +2 -1
- package/src/templates/routes-file.ts +18 -10
- package/src/templates/zod-validators.ts +7 -4
package/src/overwrite-policy.ts
CHANGED
|
@@ -45,20 +45,30 @@ import { spawnSync } from "node:child_process";
|
|
|
45
45
|
import { tmpdir } from "node:os";
|
|
46
46
|
import { createHash, randomBytes } from "node:crypto";
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Every outcome a write decision can have. An `as const` array rather than a bare
|
|
50
|
+
* union so a consumer that MAPS these — the CLI's reporting layer does — can be
|
|
51
|
+
* gated against the full set instead of against a second list of its own. The CLI
|
|
52
|
+
* silently folded "overwrite" into "new" for exactly as long as no test could
|
|
53
|
+
* enumerate what it was folding.
|
|
54
|
+
*/
|
|
55
|
+
export const WRITE_STATUSES = [
|
|
56
|
+
"new",
|
|
57
|
+
"unchanged",
|
|
58
|
+
"overwrite",
|
|
59
|
+
"merged",
|
|
60
|
+
"conflict",
|
|
61
|
+
"refused",
|
|
62
|
+
"skipped",
|
|
56
63
|
/** FR-038 §8 — deleted because it was generated by a previous run, is no longer
|
|
57
64
|
* generated, and was never edited by hand. Reported as a file outcome rather
|
|
58
65
|
* than a warning because a deletion is exactly as consequential as a write,
|
|
59
66
|
* and a run summary that lists writes but hides deletions is how a silent
|
|
60
67
|
* deletion happens. */
|
|
61
|
-
|
|
68
|
+
"removed",
|
|
69
|
+
] as const;
|
|
70
|
+
|
|
71
|
+
export type WriteStatus = (typeof WRITE_STATUSES)[number];
|
|
62
72
|
|
|
63
73
|
/**
|
|
64
74
|
* "overwrite" — default; three-way merge if .gen-state exists, else write-if-
|
package/src/reference/barrel.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
barrelModuleSpecifier,
|
|
27
27
|
formatTs,
|
|
28
28
|
GENERATED_HEADER,
|
|
29
|
+
effectivePackage,
|
|
29
30
|
} from "@metaobjectsdev/codegen-ts";
|
|
30
31
|
|
|
31
32
|
interface BarrelEntry {
|
|
@@ -63,7 +64,7 @@ export const barrel = function barrel(opts?: BarrelOpts): Generator {
|
|
|
63
64
|
path: "index.ts",
|
|
64
65
|
content: await formatTs(
|
|
65
66
|
renderBarrel(
|
|
66
|
-
entities.map((e) => ({ name: rc.valueObjectEmittedName(e), package: e
|
|
67
|
+
entities.map((e) => ({ name: rc.valueObjectEmittedName(e), package: effectivePackage(e) })),
|
|
67
68
|
rc.extStyle,
|
|
68
69
|
rc.selfTarget,
|
|
69
70
|
rc.entityModuleTarget,
|
package/src/reference/entity.ts
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
// primitives below, or (with a globally-installed / linked CLI, where the project and
|
|
33
33
|
// the CLI resolve ts-poet to different physical copies) every section renders
|
|
34
34
|
// standalone with its own duplicate import header.
|
|
35
|
-
import { joinCode, type Code } from "@metaobjectsdev/codegen-ts";
|
|
35
|
+
import { joinCode, type Code, effectivePackage } from "@metaobjectsdev/codegen-ts";
|
|
36
36
|
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
37
37
|
import {
|
|
38
38
|
perEntity,
|
|
@@ -164,7 +164,7 @@ export const entityFile = function entityFile(opts?: EntityFileOpts): Generator
|
|
|
164
164
|
return [];
|
|
165
165
|
}
|
|
166
166
|
return {
|
|
167
|
-
path: entityOutputPath(ctx.config.outputLayout ?? "flat", entity
|
|
167
|
+
path: entityOutputPath(ctx.config.outputLayout ?? "flat", effectivePackage(entity), `${entity.name}.ts`),
|
|
168
168
|
content: await formatTs(renderEntity(entity, ctx.renderContext, { allowlists })),
|
|
169
169
|
};
|
|
170
170
|
});
|
package/src/reference/names.ts
CHANGED
|
@@ -39,11 +39,26 @@ import {
|
|
|
39
39
|
type EmittedFile,
|
|
40
40
|
type GenContext,
|
|
41
41
|
type Generator,
|
|
42
|
+
effectivePackage,
|
|
42
43
|
} from "@metaobjectsdev/codegen-ts";
|
|
43
44
|
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
44
45
|
|
|
45
|
-
export
|
|
46
|
-
|
|
46
|
+
export interface NamesFileOpts {
|
|
47
|
+
/**
|
|
48
|
+
* Narrow which objects get a names artifact. ANDed with the generator's own
|
|
49
|
+
* gates, so it can only narrow — the abstract bases a MATCHED object extends
|
|
50
|
+
* are still emitted, because the matched object's artifact `extends` them.
|
|
51
|
+
*
|
|
52
|
+
* A multi-package model is why this exists: one metadata tree can drive several
|
|
53
|
+
* consumers, and only some of its objects belong to the tier being generated.
|
|
54
|
+
*/
|
|
55
|
+
filter?: (entity: MetaObject) => boolean;
|
|
56
|
+
/** Named output target, as on every other generator. */
|
|
57
|
+
target?: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function namesFile(opts?: NamesFileOpts): Generator {
|
|
61
|
+
const generator: Generator = {
|
|
47
62
|
name: "names",
|
|
48
63
|
// §A6 — the marker the runner aggregates into ResolvedGenConfig.includeNames, so the
|
|
49
64
|
// entity generator can tell whether this artifact will exist. Exactly the mechanism
|
|
@@ -63,13 +78,13 @@ export function namesFile(): Generator {
|
|
|
63
78
|
// its entity sits at <pkg>/<Entity>.ts — an unresolvable import, and a hard
|
|
64
79
|
// conflicting-duplicate-path failure as soon as two packages declare a
|
|
65
80
|
// same-bare-named entity.
|
|
66
|
-
entityOutputPath(layout, obj
|
|
81
|
+
entityOutputPath(layout, effectivePackage(obj), `${obj.name}.names.ts`);
|
|
67
82
|
|
|
68
83
|
const superSpecifierFor = (obj: MetaObject): string | undefined => {
|
|
69
84
|
const sup = namesArtifactSuperOf(obj);
|
|
70
85
|
return sup === undefined
|
|
71
86
|
? undefined
|
|
72
|
-
: crossEntitySpecifier(layout, obj
|
|
87
|
+
: crossEntitySpecifier(layout, effectivePackage(obj), effectivePackage(sup), `${sup.name}.names`, extStyle);
|
|
73
88
|
};
|
|
74
89
|
|
|
75
90
|
const out: EmittedFile[] = [];
|
|
@@ -122,4 +137,11 @@ export function namesFile(): Generator {
|
|
|
122
137
|
return out;
|
|
123
138
|
},
|
|
124
139
|
};
|
|
140
|
+
if (opts?.filter) {
|
|
141
|
+
generator.filter = opts.filter;
|
|
142
|
+
}
|
|
143
|
+
if (opts?.target) {
|
|
144
|
+
generator.target = opts.target;
|
|
145
|
+
}
|
|
146
|
+
return generator;
|
|
125
147
|
}
|
package/src/reference/queries.ts
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
// primitives below, or (with a globally-installed / linked CLI, where the project and
|
|
31
31
|
// the CLI resolve ts-poet to different physical copies) every section renders
|
|
32
32
|
// standalone with its own duplicate import header.
|
|
33
|
-
import { code, joinCode, type Code } from "@metaobjectsdev/codegen-ts";
|
|
33
|
+
import { code, joinCode, type Code, effectivePackage } from "@metaobjectsdev/codegen-ts";
|
|
34
34
|
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
35
35
|
import {
|
|
36
36
|
perEntity,
|
|
@@ -71,7 +71,7 @@ function renderQueries(obj: MetaObject, ctx: RenderContext): string {
|
|
|
71
71
|
const entityFileName = entityModuleSpecifier(
|
|
72
72
|
ctx.selfTarget,
|
|
73
73
|
ctx.entityModuleTarget,
|
|
74
|
-
obj
|
|
74
|
+
effectivePackage(obj),
|
|
75
75
|
entityName,
|
|
76
76
|
ctx.extStyle,
|
|
77
77
|
);
|
|
@@ -162,7 +162,7 @@ export const queriesFile = function queriesFile(opts?: QueriesFileOpts): Generat
|
|
|
162
162
|
throw new Error("queries-file: renderContext is required (provided by runGen)");
|
|
163
163
|
}
|
|
164
164
|
return {
|
|
165
|
-
path: entityOutputPath(ctx.config.outputLayout ?? "flat", entity
|
|
165
|
+
path: entityOutputPath(ctx.config.outputLayout ?? "flat", effectivePackage(entity), `${entity.name}.queries.ts`),
|
|
166
166
|
content: await formatTs(renderQueries(entity, ctx.renderContext)),
|
|
167
167
|
};
|
|
168
168
|
}),
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
hasAnyRdbSource,
|
|
39
39
|
formatTs,
|
|
40
40
|
entityOutputPath,
|
|
41
|
+
effectivePackage,
|
|
41
42
|
} from "@metaobjectsdev/codegen-ts";
|
|
42
43
|
|
|
43
44
|
export interface RoutesFileHonoOpts {
|
|
@@ -100,7 +101,7 @@ export const routesFileHono = function routesFileHono(opts?: RoutesFileHonoOpts)
|
|
|
100
101
|
return {
|
|
101
102
|
path: entityOutputPath(
|
|
102
103
|
ctx.config.outputLayout ?? "flat",
|
|
103
|
-
entity
|
|
104
|
+
effectivePackage(entity),
|
|
104
105
|
`${entity.name}.routes.hono.ts`,
|
|
105
106
|
),
|
|
106
107
|
content: await formatTs(renderRoutesFileHono(entity, ctx.renderContext, resolveExpose(entity, opts?.expose))),
|
package/src/reference/routes.ts
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
hasAnyRdbSource,
|
|
47
47
|
formatTs,
|
|
48
48
|
entityOutputPath,
|
|
49
|
+
effectivePackage,
|
|
49
50
|
} from "@metaobjectsdev/codegen-ts";
|
|
50
51
|
|
|
51
52
|
export interface RoutesFileOpts {
|
|
@@ -79,7 +80,7 @@ export const routesFile = function routesFile(opts?: RoutesFileOpts): Generator
|
|
|
79
80
|
throw new Error("routes-file: renderContext is required (provided by runGen)");
|
|
80
81
|
}
|
|
81
82
|
return {
|
|
82
|
-
path: entityOutputPath(ctx.config.outputLayout ?? "flat", entity
|
|
83
|
+
path: entityOutputPath(ctx.config.outputLayout ?? "flat", effectivePackage(entity), `${entity.name}.routes.ts`),
|
|
83
84
|
content: await formatTs(renderRoutesFile(entity, ctx.renderContext, resolveExpose(entity, opts?.expose))),
|
|
84
85
|
};
|
|
85
86
|
}),
|
package/src/runner.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { buildRelationMap } from "./relation-resolver.js";
|
|
|
19
19
|
import { makeRenderContext } from "./render-context.js";
|
|
20
20
|
import { sweepOrphans, type OrphanJob } from "./orphan-sweep.js";
|
|
21
21
|
import { refusedOrphanMessage } from "./reconcile-orphans.js";
|
|
22
|
+
import { effectivePackage } from "./docs-paths.js";
|
|
22
23
|
import {
|
|
23
24
|
decideAndWrite,
|
|
24
25
|
previewWriteStatus,
|
|
@@ -140,6 +141,20 @@ function orderable(v: string | undefined): [number, number, number] | undefined
|
|
|
140
141
|
return m === null ? undefined : [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
/** True when `a` sorts strictly before `b` under `[major, minor, patch]` order. The
|
|
145
|
+
* shared comparison the two "did we just cross a release boundary" predicates below
|
|
146
|
+
* reduce to once both sides are `orderable()` — their SILENCE rules (what makes a
|
|
147
|
+
* boundary crossing worth noting at all) stay separate; only this pure ordering is
|
|
148
|
+
* common between them. */
|
|
149
|
+
function versionBefore(a: readonly [number, number, number], b: readonly [number, number, number]): boolean {
|
|
150
|
+
for (let i = 0; i < 3; i++) {
|
|
151
|
+
const ai = a[i] as number;
|
|
152
|
+
const bi = b[i] as number;
|
|
153
|
+
if (ai !== bi) return ai < bi;
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
143
158
|
/**
|
|
144
159
|
* True exactly on the FIRST gen after upgrading past the release that moved the API base
|
|
145
160
|
* URL out of the entity descriptor, and only for a project that actually had a prefix to
|
|
@@ -166,12 +181,49 @@ export function shouldNoteBaseUrlMove(
|
|
|
166
181
|
const was = orderable(recordedEngine);
|
|
167
182
|
const move = orderable(moveVersion);
|
|
168
183
|
if (was === undefined || move === undefined) return false;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
184
|
+
return versionBefore(was, move);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The release in which `<Entity>Names` became the way a physical name is spelled once. */
|
|
188
|
+
const NAMES_ARTIFACT_VERSION = "1.0.0";
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* True exactly on the FIRST gen after crossing into the release that made
|
|
192
|
+
* `<Entity>Names` the doctrine, for a project that has DB-backed objects and has not
|
|
193
|
+
* wired `namesFile()`. Exported for test.
|
|
194
|
+
*
|
|
195
|
+
* The problem it addresses was reported from three separate adopter estates: on
|
|
196
|
+
* TypeScript `generators: [...]` IS the complete suite — there is no default set to
|
|
197
|
+
* inherit — so an EXISTING project emits no names artifact until the line is added, and
|
|
198
|
+
* `meta gen` said nothing about it. One estate carried ~200 physical names as string
|
|
199
|
+
* literals in hand-written SQL and the whole run contained zero occurrences of the word
|
|
200
|
+
* "names". `meta init` scaffolds the generator, so the gap is only ever for projects that
|
|
201
|
+
* already exist — which is exactly the population that cannot be reached by a scaffold.
|
|
202
|
+
*
|
|
203
|
+
* Keyed on the #232 engine stamp for the same reason `shouldNoteBaseUrlMove` is: whether
|
|
204
|
+
* an adopter has DECIDED against the artifact is not something `meta gen` can see, so a
|
|
205
|
+
* condition-only trigger would nag for ever with no way to satisfy it — the cry-wolf
|
|
206
|
+
* failure that got the `timestampMode` warning deleted from this file. Crossing the
|
|
207
|
+
* version boundary is an event that happens once.
|
|
208
|
+
*
|
|
209
|
+
* Silence is the default for an absent or unorderable recorded version, matching
|
|
210
|
+
* `shouldNoteBaseUrlMove`: no gen history means nothing has been generated the old way,
|
|
211
|
+
* and a project generating for the first time under 1.0 is not migrating.
|
|
212
|
+
*/
|
|
213
|
+
export function shouldNoteNamesArtifactAbsent(
|
|
214
|
+
namesWired: boolean,
|
|
215
|
+
hasDbBackedObject: boolean,
|
|
216
|
+
recordedEngine: string | undefined,
|
|
217
|
+
sinceVersion: string = NAMES_ARTIFACT_VERSION,
|
|
218
|
+
): boolean {
|
|
219
|
+
if (namesWired) return false;
|
|
220
|
+
// Nothing with a physical name to spell — an all-value / sourceless model has no
|
|
221
|
+
// artifact to miss, and saying so would be noise rather than a finding.
|
|
222
|
+
if (!hasDbBackedObject) return false;
|
|
223
|
+
const was = orderable(recordedEngine);
|
|
224
|
+
const since = orderable(sinceVersion);
|
|
225
|
+
if (was === undefined || since === undefined) return false;
|
|
226
|
+
return versionBefore(was, since);
|
|
175
227
|
}
|
|
176
228
|
|
|
177
229
|
export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
@@ -411,7 +463,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
411
463
|
const key = o.subType === OBJECT_SUBTYPE_VALUE
|
|
412
464
|
? (valueObjectNames.get(o.resolutionKey()) ?? o.name)
|
|
413
465
|
: o.name;
|
|
414
|
-
packageOf.set(key, o
|
|
466
|
+
packageOf.set(key, effectivePackage(o));
|
|
415
467
|
}
|
|
416
468
|
|
|
417
469
|
// Auto-detect: is the OPT-IN Hono routes generator in the active suite? If so,
|
|
@@ -442,6 +494,29 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
442
494
|
// prompt-generator-gate.ts. Self-extinguishing; warning only.
|
|
443
495
|
warnMissingPromptGenerators(root, config.generators, (m) => warnings.push(m));
|
|
444
496
|
|
|
497
|
+
// <Entity>Names is opt-in on TypeScript and an existing project gets no signal that
|
|
498
|
+
// it exists. Fires ONCE, on the first gen after crossing the release that made it the
|
|
499
|
+
// doctrine — see shouldNoteNamesArtifactAbsent for why it is keyed on the engine stamp
|
|
500
|
+
// rather than on the condition alone.
|
|
501
|
+
if (
|
|
502
|
+
shouldNoteNamesArtifactAbsent(
|
|
503
|
+
namesTargets.size > 0,
|
|
504
|
+
safeEntities.some((e) => hasAnyRdbSource(e)),
|
|
505
|
+
recordedEngine,
|
|
506
|
+
)
|
|
507
|
+
) {
|
|
508
|
+
warnings.push(
|
|
509
|
+
`no <Entity>Names artifact: namesFile() is not in this run's generators, so every ` +
|
|
510
|
+
`physical table/column name your hand-written SQL, upserts and fixtures spell is a ` +
|
|
511
|
+
`string literal that nothing checks against the metadata. Add it — ` +
|
|
512
|
+
`import { namesFile } from "@metaobjectsdev/codegen-ts/generators" (or the copy ` +
|
|
513
|
+
`'meta eject names' writes) — then reference <Entity>Names instead of the literals. ` +
|
|
514
|
+
`A typed ORM handle already in a name position is CORRECT and should stay; the ` +
|
|
515
|
+
`constants are for where no handle exists (raw SQL, excluded.<column>). ` +
|
|
516
|
+
`This note appears once.`,
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
445
520
|
// A retired `@emit*` codegen flag still sitting in the metadata suppresses nothing now.
|
|
446
521
|
// Named here rather than left to be discovered as a file that reappeared. Scoped to
|
|
447
522
|
// the run's own entity set (the same set every generator sees), so `meta gen <entity>`
|
|
@@ -455,6 +530,10 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
455
530
|
// directory their policy's relative paths are measured from. Collected here
|
|
456
531
|
// because `writeOutDir` is resolved per generator inside this loop.
|
|
457
532
|
const orphanJobs: OrphanJob[] = [];
|
|
533
|
+
// Generators that ran and produced no file. See the note at the `files.length === 0`
|
|
534
|
+
// check below. A Set because a generator may run once per target.
|
|
535
|
+
const emptyGenerators = new Set<string>();
|
|
536
|
+
|
|
458
537
|
for (const generator of config.generators) {
|
|
459
538
|
// ADR-0025: `meta docs` is the single docs door. A `meta gen` config that
|
|
460
539
|
// still lists a deprecated doc generator is warned + skipped, not run — the
|
|
@@ -547,6 +626,17 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
547
626
|
let files: EmittedFile[];
|
|
548
627
|
try {
|
|
549
628
|
files = await generator.generate(ctx);
|
|
629
|
+
// A generator the author DELIBERATELY WIRED that matched nothing is a fact
|
|
630
|
+
// worth one line. `namesFile()` on a model with no DB-backed object emitted
|
|
631
|
+
// zero files with no line, no count and no warning, and an adopter could not
|
|
632
|
+
// tell that from a run that had done the work. 0.21.4 gave `tanstackGrid()`
|
|
633
|
+
// a self-extinguishing warning for exactly this shape.
|
|
634
|
+
//
|
|
635
|
+
// Collected rather than warned per generator: one aggregated line names every
|
|
636
|
+
// silent generator, so a suite with several inapplicable ones costs one line,
|
|
637
|
+
// not five. Emitting nothing is often CORRECT — a form generator on a model
|
|
638
|
+
// with no forms — so this reports, it never fails.
|
|
639
|
+
if (files.length === 0) emptyGenerators.add(generator.name);
|
|
550
640
|
} catch (err) {
|
|
551
641
|
const msg = err instanceof Error ? err.message : String(err);
|
|
552
642
|
// `cause` preserves the original throw. Without it a `runGen` caller sees a
|
|
@@ -579,6 +669,21 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
579
669
|
}
|
|
580
670
|
}
|
|
581
671
|
|
|
672
|
+
// One line for every wired generator that matched nothing — see the note at the
|
|
673
|
+
// collection site. Deliberately AFTER the loop so the message is a single line
|
|
674
|
+
// naming all of them, and deliberately a warning: emitting nothing is frequently
|
|
675
|
+
// the correct outcome, and a gate that failed on it would be wrong more often
|
|
676
|
+
// than right.
|
|
677
|
+
if (emptyGenerators.size > 0) {
|
|
678
|
+
const names = [...emptyGenerators].sort();
|
|
679
|
+
warnings.push(
|
|
680
|
+
`${names.length} wired generator(s) matched nothing and wrote no file: ` +
|
|
681
|
+
`${names.join(", ")}. That is often correct — a generator whose metadata this ` +
|
|
682
|
+
`model does not declare has nothing to emit. It is reported because a run that ` +
|
|
683
|
+
`emitted nothing and a run that did the work otherwise look identical.`,
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
|
|
582
687
|
// 5. Write phase.
|
|
583
688
|
const writes: WriteResult[] = [];
|
|
584
689
|
const conflicts: WriteResult[] = [];
|
|
@@ -35,6 +35,7 @@ import { GENERATED_HEADER } from "../constants.js";
|
|
|
35
35
|
import { crossEntitySpecifier } from "../import-path.js";
|
|
36
36
|
import { resolveObjectNames } from "../names.js";
|
|
37
37
|
import type { RenderContext } from "../render-context.js";
|
|
38
|
+
import { effectivePackage } from "../docs-paths.js";
|
|
38
39
|
|
|
39
40
|
const CALLABLE_KINDS: ReadonlySet<string> = new Set([
|
|
40
41
|
SOURCE_KIND_STORED_PROC,
|
|
@@ -128,7 +129,7 @@ export function renderCallableFile(entity: MetaObject, ctx?: RenderContext): str
|
|
|
128
129
|
const namesImport = namesConst === undefined
|
|
129
130
|
? ""
|
|
130
131
|
: `import { ${namesConst} } from "${crossEntitySpecifier(
|
|
131
|
-
ctx!.selfTarget.outputLayout, entity
|
|
132
|
+
ctx!.selfTarget.outputLayout, effectivePackage(entity), effectivePackage(entity),
|
|
132
133
|
`${entity.name}.names`, ctx!.extStyle,
|
|
133
134
|
)}";\n`;
|
|
134
135
|
// The identifier must stay an IDENTIFIER. A bare interpolation into drizzle's `sql` tag
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
import { code, imp, joinCode, type Code } from "ts-poet";
|
|
6
6
|
import { MetaObject, MetaField, MetaIndex, isMetaObject, stripPackage } from "@metaobjectsdev/metadata";
|
|
7
7
|
import {
|
|
8
|
-
FIELD_SUBTYPE_LONG,
|
|
9
8
|
IDENTITY_ATTR_FIELDS, IDENTITY_ATTR_GENERATION,
|
|
10
9
|
IDENTITY_ATTR_EXPR, IDENTITY_ATTR_USING, IDENTITY_ATTR_ORDERS, IDENTITY_ATTR_WHERE,
|
|
11
10
|
GENERATION_INCREMENT, GENERATION_UUID,
|
|
@@ -14,7 +13,10 @@ import {
|
|
|
14
13
|
} from "@metaobjectsdev/metadata";
|
|
15
14
|
import { fieldDeclaringPackage, type RenderContext } from "../render-context.js";
|
|
16
15
|
import { crossEntitySpecifier, valueObjectModuleSpecifier } from "../import-path.js";
|
|
17
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
mapColumnType, PG_IDENTITY_CAPABLE_FNS,
|
|
18
|
+
type ColumnSpec, type EnumIntCustomType,
|
|
19
|
+
} from "../column-mapper.js";
|
|
18
20
|
import { tableNameFromEntity } from "../naming.js";
|
|
19
21
|
import {
|
|
20
22
|
namesRef, physicalNameExpr, sourceSchemaExpr, indexNameExpr, columnExpr,
|
|
@@ -23,6 +25,7 @@ import { resolveTableSchema } from "@metaobjectsdev/metadata";
|
|
|
23
25
|
import { renderRelationsBlock } from "./relations-block.js";
|
|
24
26
|
import { renderDocsFor } from "./jsdoc.js";
|
|
25
27
|
import { collectTphSubtypeFields } from "./tph-discriminator.js";
|
|
28
|
+
import { effectivePackage } from "../docs-paths.js";
|
|
26
29
|
|
|
27
30
|
/**
|
|
28
31
|
* Render the Drizzle table definition for one entity, including:
|
|
@@ -102,6 +105,10 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
|
|
|
102
105
|
// `isMetaObject`, never `instanceof`: two physical copies of @metaobjectsdev/metadata in
|
|
103
106
|
// one process give the class object and the instance different identities, so the check
|
|
104
107
|
// would return false for a real node — silently.
|
|
108
|
+
// `obj` never changes across this function, so its effective package is computed once
|
|
109
|
+
// and reused everywhere below (the `fromPackage` passed to `namesRef` here, and the
|
|
110
|
+
// per-field `renderColumn` calls in both the own-field and TPH-subtype-fold loops).
|
|
111
|
+
const pkg = effectivePackage(obj);
|
|
105
112
|
const columnNameExpr = (field: MetaField, dbName: string): Code => {
|
|
106
113
|
if (names === undefined || names.resolved.fields[field.name] !== undefined) {
|
|
107
114
|
return columnExpr(names, field.name, dbName);
|
|
@@ -111,7 +118,7 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
|
|
|
111
118
|
// base's), not to the subtype's own package.
|
|
112
119
|
const ownerNames =
|
|
113
120
|
owner !== undefined && isMetaObject(owner) && owner !== obj
|
|
114
|
-
? namesRef(owner, ctx,
|
|
121
|
+
? namesRef(owner, ctx, pkg)
|
|
115
122
|
: undefined;
|
|
116
123
|
return columnExpr(ownerNames, field.name, dbName);
|
|
117
124
|
};
|
|
@@ -202,7 +209,7 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
|
|
|
202
209
|
enumIntTypes.set(spec.enumIntCustomType.fnConstName, spec.enumIntCustomType);
|
|
203
210
|
}
|
|
204
211
|
const fieldDocs = renderDocsFor(child);
|
|
205
|
-
const columnLine = renderColumn(spec, columnNameExpr(child, spec.dbName), child, ctx, isPk, pkGeneration, fkInfo, isComposite,
|
|
212
|
+
const columnLine = renderColumn(spec, columnNameExpr(child, spec.dbName), child, ctx, isPk, pkGeneration, fkInfo, isComposite, pkg, obj.name);
|
|
206
213
|
columnLines.push(fieldDocs ? code` ${fieldDocs}\n${columnLine}` : columnLine);
|
|
207
214
|
if (spec.checkConstraint !== undefined) checkConstraints.push(checkEntry(child, spec));
|
|
208
215
|
}
|
|
@@ -222,7 +229,7 @@ export function renderDrizzleSchema(obj: MetaObject, ctx: RenderContext): Code {
|
|
|
222
229
|
}
|
|
223
230
|
const fieldDocs = renderDocsFor(child);
|
|
224
231
|
const columnLine = renderColumn(
|
|
225
|
-
spec, columnNameExpr(child, spec.dbName), child, ctx, false, undefined, fkMap.get(child.name), isComposite,
|
|
232
|
+
spec, columnNameExpr(child, spec.dbName), child, ctx, false, undefined, fkMap.get(child.name), isComposite, pkg, obj.name, true,
|
|
226
233
|
);
|
|
227
234
|
columnLines.push(fieldDocs ? code` ${fieldDocs}\n${columnLine}` : columnLine);
|
|
228
235
|
// Enum CHECK constraints stay valid under TPH: `NULL IN (...)` is NULL
|
|
@@ -532,15 +539,38 @@ function renderColumn(
|
|
|
532
539
|
// Composite PKs don't use .primaryKey() per-column; table callback owns it.
|
|
533
540
|
pkSuffix = isComposite ? "" : ".primaryKey({ autoIncrement: true })";
|
|
534
541
|
} else {
|
|
535
|
-
// Postgres:
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
542
|
+
// Postgres: the column KEEPS the type the column-mapper derived and
|
|
543
|
+
// GAINS the identity clause. It does not get a second spelling.
|
|
544
|
+
//
|
|
545
|
+
// This used to substitute `serial`/`bigserial` for the mapped
|
|
546
|
+
// `integer`/`bigint`, and that was the 1.0 defect wearing another hat:
|
|
547
|
+
// migrate emits `… GENERATED BY DEFAULT AS IDENTITY` (emit/postgres.ts
|
|
548
|
+
// renderColumn) for the same field, and `serial` is NOT that column.
|
|
549
|
+
// `serial` is historical sugar for integer + an owned sequence + a
|
|
550
|
+
// genuine DEFAULT nextval(...); an identity column carries neither a
|
|
551
|
+
// default nor a droppable sequence. So the generated Drizzle schema
|
|
552
|
+
// misrepresented the live PK of every integer-keyed table migrate had
|
|
553
|
+
// created — one question, two doors, two answers, and nothing compared
|
|
554
|
+
// them. migrate owns the DDL (ADR-0015), so codegen is what changes —
|
|
555
|
+
// the same call #293 made when the two named a CHECK constraint
|
|
556
|
+
// differently.
|
|
557
|
+
//
|
|
558
|
+
// The clause is gated on the MAPPED column function rather than on the
|
|
559
|
+
// field subtype, so the two cannot drift apart again: only Drizzle's
|
|
560
|
+
// int family (integer/bigint/smallint, via PgIntColumnBaseBuilder)
|
|
561
|
+
// carries .generatedByDefaultAsIdentity(). A subtype outside that
|
|
562
|
+
// family with @generation:"increment" is a combination Postgres cannot
|
|
563
|
+
// express at all — migrate emits `TEXT GENERATED BY DEFAULT AS
|
|
564
|
+
// IDENTITY` and fails at apply — and the honest thing here is to leave
|
|
565
|
+
// the declared type alone and let that failure be loud, rather than
|
|
566
|
+
// silently substituting an integer column for the declared one.
|
|
543
567
|
pkSuffix = isComposite ? "" : ".primaryKey()";
|
|
568
|
+
if (
|
|
569
|
+
spec.enumIntCustomType === undefined &&
|
|
570
|
+
PG_IDENTITY_CAPABLE_FNS.has(spec.fnName)
|
|
571
|
+
) {
|
|
572
|
+
pkSuffix += ".generatedByDefaultAsIdentity()";
|
|
573
|
+
}
|
|
544
574
|
}
|
|
545
575
|
} else if (pkGeneration === GENERATION_UUID) {
|
|
546
576
|
pkSuffix = isComposite
|
|
@@ -30,6 +30,7 @@ import { namesRef, namesConstArg, physicalNameExpr, sourceSchemaExpr } from "../
|
|
|
30
30
|
import { hasWritableRdbSource } from "../source-detect.js";
|
|
31
31
|
import { renderValueObjectFile } from "./value-object-file.js";
|
|
32
32
|
import { isAbstract } from "../instance-artifacts.js";
|
|
33
|
+
import { effectivePackage } from "../docs-paths.js";
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Render-time options for the entity-file composer.
|
|
@@ -139,14 +140,15 @@ export function renderEntityFile(
|
|
|
139
140
|
if (writeThrough) {
|
|
140
141
|
const camel = entity.name.charAt(0).toLowerCase() + entity.name.slice(1);
|
|
141
142
|
const fields = entity.fields();
|
|
143
|
+
const entityPkg = effectivePackage(entity);
|
|
142
144
|
// ADR-0044/#228 — resolve a view column's `@objectRef` to the value object's
|
|
143
145
|
// EMITTED name + module TOGETHER (lock-step), so the read-view artifact imports
|
|
144
146
|
// `AcmeAlphaNote` from `./AcmeAlphaNote.js` (not a bare `Note` → `./Note.js`)
|
|
145
147
|
// under a cross-package short-name collision.
|
|
146
148
|
const voRef = (field: MetaField): { name: string; module: string } => {
|
|
147
149
|
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
148
|
-
const name = ctx.resolveValueObjectName(typeof ref === "string" ? ref : "", fieldDeclaringPackage(field,
|
|
149
|
-
const module = valueObjectModuleSpecifier(name, ctx.packageOf,
|
|
150
|
+
const name = ctx.resolveValueObjectName(typeof ref === "string" ? ref : "", fieldDeclaringPackage(field, entityPkg));
|
|
151
|
+
const module = valueObjectModuleSpecifier(name, ctx.packageOf, entityPkg, ctx.outputLayout, ctx.extStyle);
|
|
150
152
|
return { name, module };
|
|
151
153
|
};
|
|
152
154
|
// ONE selection of the replica source: `projectionViewName` names it, and this is the
|
|
@@ -2,7 +2,6 @@ import { code, type Code } from "ts-poet";
|
|
|
2
2
|
import { MetaField, MetaObject } from "@metaobjectsdev/metadata";
|
|
3
3
|
import {
|
|
4
4
|
FIELD_ATTR_FILTERABLE,
|
|
5
|
-
FIELD_ATTR_SORTABLE_DEFAULT_ORDER,
|
|
6
5
|
FIELD_SUBTYPE_BOOLEAN,
|
|
7
6
|
FIELD_SUBTYPE_INT,
|
|
8
7
|
FIELD_SUBTYPE_LONG,
|
|
@@ -15,7 +14,7 @@ import {
|
|
|
15
14
|
FIELD_SUBTYPE_CURRENCY,
|
|
16
15
|
opsForField,
|
|
17
16
|
} from "@metaobjectsdev/metadata";
|
|
18
|
-
import { sortableFields } from "./filter-shared.js";
|
|
17
|
+
import { sortableFields, declaredSortDefaultOrder } from "./filter-shared.js";
|
|
19
18
|
import type { RenderContext } from "../render-context.js";
|
|
20
19
|
|
|
21
20
|
const NUMBER_SUBTYPES = new Set<string>([
|
|
@@ -108,11 +107,15 @@ export const ${entity.name}SortAllowlist = {} as const satisfies SortAllowlist;
|
|
|
108
107
|
}
|
|
109
108
|
const rows = sortable
|
|
110
109
|
.map((f) => {
|
|
111
|
-
|
|
110
|
+
// The DECLARED order only — an undeclared field stays `{}` so the runtime's own
|
|
111
|
+
// `?? "asc"` remains the single place the fallback is spelled. Read through the
|
|
112
|
+
// shared resolver, never off the attr here, so this allowlist and the grid tier
|
|
113
|
+
// cannot answer one declaration two ways.
|
|
114
|
+
const defaultOrder = declaredSortDefaultOrder(f);
|
|
112
115
|
const rule =
|
|
113
|
-
defaultOrder ===
|
|
114
|
-
? `{
|
|
115
|
-
: `{}`;
|
|
116
|
+
defaultOrder === undefined
|
|
117
|
+
? `{}`
|
|
118
|
+
: `{ defaultOrder: ${JSON.stringify(defaultOrder)} as const }`;
|
|
116
119
|
return ` ${f.name}: ${rule}`;
|
|
117
120
|
})
|
|
118
121
|
.join(",\n");
|