@metaobjectsdev/codegen-ts 1.0.0-rc.4 → 1.0.0-rc.6
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/constants.d.ts +10 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +10 -0
- package/dist/constants.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/generator.d.ts +15 -0
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js.map +1 -1
- package/dist/generators/agent-docs-file.d.ts.map +1 -1
- package/dist/generators/agent-docs-file.js +12 -3
- package/dist/generators/agent-docs-file.js.map +1 -1
- package/dist/generators/agent-schema-input.d.ts +11 -0
- package/dist/generators/agent-schema-input.d.ts.map +1 -1
- package/dist/generators/agent-schema-page.d.ts.map +1 -1
- package/dist/generators/agent-schema-page.js +13 -1
- package/dist/generators/agent-schema-page.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 +13 -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 +9 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16 -7
- package/dist/index.js.map +1 -1
- package/dist/metaobjects-config.d.ts +10 -0
- package/dist/metaobjects-config.d.ts.map +1 -1
- package/dist/metaobjects-config.js +14 -1
- package/dist/metaobjects-config.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 +23 -9
- package/dist/overwrite-policy.d.ts.map +1 -1
- package/dist/overwrite-policy.js +48 -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 +143 -13
- 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/dist/ui-tier-gate.d.ts +44 -0
- package/dist/ui-tier-gate.d.ts.map +1 -0
- package/dist/ui-tier-gate.js +68 -0
- package/dist/ui-tier-gate.js.map +1 -0
- package/package.json +6 -6
- package/src/column-mapper.ts +17 -0
- package/src/constants.ts +11 -0
- package/src/generator-registry.ts +1 -0
- package/src/generator.ts +15 -0
- package/src/generators/agent-docs-file.ts +11 -2
- package/src/generators/agent-schema-input.ts +11 -0
- package/src/generators/agent-schema-page.ts +14 -1
- 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 +34 -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 +23 -7
- package/src/metaobjects-config.ts +20 -1
- package/src/names.ts +5 -4
- package/src/overwrite-policy.ts +62 -11
- package/src/reference/barrel.ts +2 -1
- package/src/reference/entity.ts +2 -2
- package/src/reference/names.ts +30 -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 +162 -12
- 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/ui-tier-gate.ts +79 -0
package/src/runner.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { join, relative, resolve, isAbsolute, dirname } from "node:path";
|
|
2
2
|
import { warnMissingPromptGenerators } from "./prompt-generator-gate.js";
|
|
3
|
+
import { runEmitsHonoRoutes, runEmitsUiTier, warnUnmarkedUiGenerators } from "./ui-tier-gate.js";
|
|
3
4
|
import { warnRetiredCodegenAttrs } from "./retired-codegen-attrs.js";
|
|
4
5
|
import { tmpdir } from "node:os";
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -19,6 +20,7 @@ import { buildRelationMap } from "./relation-resolver.js";
|
|
|
19
20
|
import { makeRenderContext } from "./render-context.js";
|
|
20
21
|
import { sweepOrphans, type OrphanJob } from "./orphan-sweep.js";
|
|
21
22
|
import { refusedOrphanMessage } from "./reconcile-orphans.js";
|
|
23
|
+
import { effectivePackage } from "./docs-paths.js";
|
|
22
24
|
import {
|
|
23
25
|
decideAndWrite,
|
|
24
26
|
previewWriteStatus,
|
|
@@ -140,6 +142,20 @@ function orderable(v: string | undefined): [number, number, number] | undefined
|
|
|
140
142
|
return m === null ? undefined : [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
141
143
|
}
|
|
142
144
|
|
|
145
|
+
/** True when `a` sorts strictly before `b` under `[major, minor, patch]` order. The
|
|
146
|
+
* shared comparison the two "did we just cross a release boundary" predicates below
|
|
147
|
+
* reduce to once both sides are `orderable()` — their SILENCE rules (what makes a
|
|
148
|
+
* boundary crossing worth noting at all) stay separate; only this pure ordering is
|
|
149
|
+
* common between them. */
|
|
150
|
+
function versionBefore(a: readonly [number, number, number], b: readonly [number, number, number]): boolean {
|
|
151
|
+
for (let i = 0; i < 3; i++) {
|
|
152
|
+
const ai = a[i] as number;
|
|
153
|
+
const bi = b[i] as number;
|
|
154
|
+
if (ai !== bi) return ai < bi;
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
143
159
|
/**
|
|
144
160
|
* True exactly on the FIRST gen after upgrading past the release that moved the API base
|
|
145
161
|
* URL out of the entity descriptor, and only for a project that actually had a prefix to
|
|
@@ -166,12 +182,49 @@ export function shouldNoteBaseUrlMove(
|
|
|
166
182
|
const was = orderable(recordedEngine);
|
|
167
183
|
const move = orderable(moveVersion);
|
|
168
184
|
if (was === undefined || move === undefined) return false;
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
185
|
+
return versionBefore(was, move);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The release in which `<Entity>Names` became the way a physical name is spelled once. */
|
|
189
|
+
const NAMES_ARTIFACT_VERSION = "1.0.0";
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* True exactly on the FIRST gen after crossing into the release that made
|
|
193
|
+
* `<Entity>Names` the doctrine, for a project that has DB-backed objects and has not
|
|
194
|
+
* wired `namesFile()`. Exported for test.
|
|
195
|
+
*
|
|
196
|
+
* The problem it addresses was reported from three separate adopter estates: on
|
|
197
|
+
* TypeScript `generators: [...]` IS the complete suite — there is no default set to
|
|
198
|
+
* inherit — so an EXISTING project emits no names artifact until the line is added, and
|
|
199
|
+
* `meta gen` said nothing about it. One estate carried ~200 physical names as string
|
|
200
|
+
* literals in hand-written SQL and the whole run contained zero occurrences of the word
|
|
201
|
+
* "names". `meta init` scaffolds the generator, so the gap is only ever for projects that
|
|
202
|
+
* already exist — which is exactly the population that cannot be reached by a scaffold.
|
|
203
|
+
*
|
|
204
|
+
* Keyed on the #232 engine stamp for the same reason `shouldNoteBaseUrlMove` is: whether
|
|
205
|
+
* an adopter has DECIDED against the artifact is not something `meta gen` can see, so a
|
|
206
|
+
* condition-only trigger would nag for ever with no way to satisfy it — the cry-wolf
|
|
207
|
+
* failure that got the `timestampMode` warning deleted from this file. Crossing the
|
|
208
|
+
* version boundary is an event that happens once.
|
|
209
|
+
*
|
|
210
|
+
* Silence is the default for an absent or unorderable recorded version, matching
|
|
211
|
+
* `shouldNoteBaseUrlMove`: no gen history means nothing has been generated the old way,
|
|
212
|
+
* and a project generating for the first time under 1.0 is not migrating.
|
|
213
|
+
*/
|
|
214
|
+
export function shouldNoteNamesArtifactAbsent(
|
|
215
|
+
namesWired: boolean,
|
|
216
|
+
hasDbBackedObject: boolean,
|
|
217
|
+
recordedEngine: string | undefined,
|
|
218
|
+
sinceVersion: string = NAMES_ARTIFACT_VERSION,
|
|
219
|
+
): boolean {
|
|
220
|
+
if (namesWired) return false;
|
|
221
|
+
// Nothing with a physical name to spell — an all-value / sourceless model has no
|
|
222
|
+
// artifact to miss, and saying so would be noise rather than a finding.
|
|
223
|
+
if (!hasDbBackedObject) return false;
|
|
224
|
+
const was = orderable(recordedEngine);
|
|
225
|
+
const since = orderable(sinceVersion);
|
|
226
|
+
if (was === undefined || since === undefined) return false;
|
|
227
|
+
return versionBefore(was, since);
|
|
175
228
|
}
|
|
176
229
|
|
|
177
230
|
export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
@@ -411,13 +464,26 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
411
464
|
const key = o.subType === OBJECT_SUBTYPE_VALUE
|
|
412
465
|
? (valueObjectNames.get(o.resolutionKey()) ?? o.name)
|
|
413
466
|
: o.name;
|
|
414
|
-
packageOf.set(key, o
|
|
467
|
+
packageOf.set(key, effectivePackage(o));
|
|
415
468
|
}
|
|
416
469
|
|
|
417
470
|
// Auto-detect: is the OPT-IN Hono routes generator in the active suite? If so,
|
|
418
471
|
// surface it on every generator's ctx.config so api-docs documents the Hono
|
|
419
472
|
// CRUD surface it actually emits (rather than silently omitting it).
|
|
420
|
-
|
|
473
|
+
// The explicit config key WINS over the marker aggregation, at both doors. `meta docs`
|
|
474
|
+
// already read `loadedConfig?.includeHonoRoutes ?? …`, and the runner did not — so an
|
|
475
|
+
// adopter who set the key got the surface documented and not emitted, or the reverse.
|
|
476
|
+
// The key is settable because `MetaobjectsGenConfig extends Omit<ResolvedGenConfig,…>`,
|
|
477
|
+
// and it is the escape hatch for an owned generator whose name was changed; an escape
|
|
478
|
+
// hatch honoured by one of two doors is the defect these flags exist to close.
|
|
479
|
+
const includeHonoRoutes = config.includeHonoRoutes ?? runEmitsHonoRoutes(config.generators);
|
|
480
|
+
|
|
481
|
+
// Same auto-detection for the CLIENT UI tier. `agent/ui.md` describes forms, grids
|
|
482
|
+
// and the endpoints their hooks call; whether any of that is emitted is a generator
|
|
483
|
+
// fact, and the page's own gate could only see metadata. Run-scoped like
|
|
484
|
+
// includeHonoRoutes — the page asks "is this surface in the run?", never "does it
|
|
485
|
+
// land in my target?".
|
|
486
|
+
const includeUiTier = config.includeUiTier ?? runEmitsUiTier(config.generators);
|
|
421
487
|
|
|
422
488
|
// §A6 — same auto-detection for the OPT-IN names generator. The entity tier may only
|
|
423
489
|
// REFERENCE `<Entity>Names` when something in this run actually emits it; the names
|
|
@@ -441,6 +507,30 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
441
507
|
// this, said nothing — while `meta verify` reported the template "clean". See
|
|
442
508
|
// prompt-generator-gate.ts. Self-extinguishing; warning only.
|
|
443
509
|
warnMissingPromptGenerators(root, config.generators, (m) => warnings.push(m));
|
|
510
|
+
warnUnmarkedUiGenerators(config.generators, (m) => warnings.push(m));
|
|
511
|
+
|
|
512
|
+
// <Entity>Names is opt-in on TypeScript and an existing project gets no signal that
|
|
513
|
+
// it exists. Fires ONCE, on the first gen after crossing the release that made it the
|
|
514
|
+
// doctrine — see shouldNoteNamesArtifactAbsent for why it is keyed on the engine stamp
|
|
515
|
+
// rather than on the condition alone.
|
|
516
|
+
if (
|
|
517
|
+
shouldNoteNamesArtifactAbsent(
|
|
518
|
+
namesTargets.size > 0,
|
|
519
|
+
safeEntities.some((e) => hasAnyRdbSource(e)),
|
|
520
|
+
recordedEngine,
|
|
521
|
+
)
|
|
522
|
+
) {
|
|
523
|
+
warnings.push(
|
|
524
|
+
`no <Entity>Names artifact: namesFile() is not in this run's generators, so every ` +
|
|
525
|
+
`physical table/column name your hand-written SQL, upserts and fixtures spell is a ` +
|
|
526
|
+
`string literal that nothing checks against the metadata. Add it — ` +
|
|
527
|
+
`import { namesFile } from "@metaobjectsdev/codegen-ts/generators" (or the copy ` +
|
|
528
|
+
`'meta eject names' writes) — then reference <Entity>Names instead of the literals. ` +
|
|
529
|
+
`A typed ORM handle already in a name position is CORRECT and should stay; the ` +
|
|
530
|
+
`constants are for where no handle exists (raw SQL, excluded.<column>). ` +
|
|
531
|
+
`This note appears once.`,
|
|
532
|
+
);
|
|
533
|
+
}
|
|
444
534
|
|
|
445
535
|
// A retired `@emit*` codegen flag still sitting in the metadata suppresses nothing now.
|
|
446
536
|
// Named here rather than left to be discovered as a file that reappeared. Scoped to
|
|
@@ -455,6 +545,10 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
455
545
|
// directory their policy's relative paths are measured from. Collected here
|
|
456
546
|
// because `writeOutDir` is resolved per generator inside this loop.
|
|
457
547
|
const orphanJobs: OrphanJob[] = [];
|
|
548
|
+
// Generators that ran and produced no file. See the note at the `files.length === 0`
|
|
549
|
+
// check below. A Set because a generator may run once per target.
|
|
550
|
+
const emptyGenerators = new Set<string>();
|
|
551
|
+
|
|
458
552
|
for (const generator of config.generators) {
|
|
459
553
|
// ADR-0025: `meta docs` is the single docs door. A `meta gen` config that
|
|
460
554
|
// still lists a deprecated doc generator is warned + skipped, not run — the
|
|
@@ -519,6 +613,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
519
613
|
outputLayout: selfTarget.outputLayout,
|
|
520
614
|
includeHonoRoutes,
|
|
521
615
|
includeNames: namesTargets.has(selfTarget.name),
|
|
616
|
+
includeUiTier,
|
|
522
617
|
},
|
|
523
618
|
renderContext,
|
|
524
619
|
...(projectRoot !== undefined && { projectRoot }),
|
|
@@ -547,6 +642,17 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
547
642
|
let files: EmittedFile[];
|
|
548
643
|
try {
|
|
549
644
|
files = await generator.generate(ctx);
|
|
645
|
+
// A generator the author DELIBERATELY WIRED that matched nothing is a fact
|
|
646
|
+
// worth one line. `namesFile()` on a model with no DB-backed object emitted
|
|
647
|
+
// zero files with no line, no count and no warning, and an adopter could not
|
|
648
|
+
// tell that from a run that had done the work. 0.21.4 gave `tanstackGrid()`
|
|
649
|
+
// a self-extinguishing warning for exactly this shape.
|
|
650
|
+
//
|
|
651
|
+
// Collected rather than warned per generator: one aggregated line names every
|
|
652
|
+
// silent generator, so a suite with several inapplicable ones costs one line,
|
|
653
|
+
// not five. Emitting nothing is often CORRECT — a form generator on a model
|
|
654
|
+
// with no forms — so this reports, it never fails.
|
|
655
|
+
if (files.length === 0) emptyGenerators.add(generator.name);
|
|
550
656
|
} catch (err) {
|
|
551
657
|
const msg = err instanceof Error ? err.message : String(err);
|
|
552
658
|
// `cause` preserves the original throw. Without it a `runGen` caller sees a
|
|
@@ -579,6 +685,21 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
579
685
|
}
|
|
580
686
|
}
|
|
581
687
|
|
|
688
|
+
// One line for every wired generator that matched nothing — see the note at the
|
|
689
|
+
// collection site. Deliberately AFTER the loop so the message is a single line
|
|
690
|
+
// naming all of them, and deliberately a warning: emitting nothing is frequently
|
|
691
|
+
// the correct outcome, and a gate that failed on it would be wrong more often
|
|
692
|
+
// than right.
|
|
693
|
+
if (emptyGenerators.size > 0) {
|
|
694
|
+
const names = [...emptyGenerators].sort();
|
|
695
|
+
warnings.push(
|
|
696
|
+
`${names.length} wired generator(s) matched nothing and wrote no file: ` +
|
|
697
|
+
`${names.join(", ")}. That is often correct — a generator whose metadata this ` +
|
|
698
|
+
`model does not declare has nothing to emit. It is reported because a run that ` +
|
|
699
|
+
`emitted nothing and a run that did the work otherwise look identical.`,
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
|
|
582
703
|
// 5. Write phase.
|
|
583
704
|
const writes: WriteResult[] = [];
|
|
584
705
|
const conflicts: WriteResult[] = [];
|
|
@@ -614,10 +735,15 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
614
735
|
`no codegen hash manifest — so 'meta gen' cannot tell your edits from its own ` +
|
|
615
736
|
`stale output, and it will not guess. This is the expected first run for a ` +
|
|
616
737
|
`project created before the manifest was committed. ` +
|
|
617
|
-
`ONE-TIME FIX:
|
|
618
|
-
|
|
619
|
-
`
|
|
620
|
-
`
|
|
738
|
+
`ONE-TIME FIX: re-run with --baseline=adopt — it records the files you have as ` +
|
|
739
|
+
`the merge base and writes NOTHING — then commit ` +
|
|
740
|
+
`'.metaobjects/.gen-state/.hashes.json' (un-ignore it in .metaobjects/.gitignore ` +
|
|
741
|
+
`with '.gen-state/*' + '!.gen-state/.hashes.json') and run 'meta gen' again, where ` +
|
|
742
|
+
`the regeneration arrives as its own reviewable diff. Adopting DECLARES these files ` +
|
|
743
|
+
`to be generated output, so an edit already inside one of them is part of the ` +
|
|
744
|
+
`baseline and that regeneration will replace it — commit before you run it. ` +
|
|
745
|
+
`To write fresh output NOW and DISCARD any hand edits in these files, ` +
|
|
746
|
+
`--baseline=fresh does both in one step. Files: ${names.join(", ")}${more}.`,
|
|
621
747
|
);
|
|
622
748
|
return;
|
|
623
749
|
}
|
|
@@ -657,6 +783,28 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
657
783
|
);
|
|
658
784
|
};
|
|
659
785
|
|
|
786
|
+
// `--baseline=adopt` — the counterpart of the aggregate above, and aggregated for the
|
|
787
|
+
// same reason: every adopted file has one cause and one next step. Reported even though
|
|
788
|
+
// nothing failed, because a run that writes nothing and exits 0 is otherwise
|
|
789
|
+
// indistinguishable from a no-op, and the file it DID produce (`.hashes.json`) is the
|
|
790
|
+
// one the adopter now has to commit.
|
|
791
|
+
const reportAdoptions = (): void => {
|
|
792
|
+
const adopted = writes.filter((w) => w.status === "adopted");
|
|
793
|
+
if (adopted.length === 0) return;
|
|
794
|
+
const names = adopted.slice(0, MAX_NAMED).map((w) => relativeForDisplay(w.path));
|
|
795
|
+
const more = adopted.length > MAX_NAMED ? `, and ${adopted.length - MAX_NAMED} more` : "";
|
|
796
|
+
warnings.push(
|
|
797
|
+
`Recorded ${adopted.length} existing file(s) as the codegen baseline and wrote ` +
|
|
798
|
+
`nothing. NEXT: commit '.metaobjects/.gen-state/.hashes.json' (un-ignore it in ` +
|
|
799
|
+
`.metaobjects/.gitignore with '.gen-state/*' + '!.gen-state/.hashes.json'), then run ` +
|
|
800
|
+
`'meta gen' — these files regenerate normally from here, as their own reviewable ` +
|
|
801
|
+
`diff. Adopting DECLARED them to be generated output: an edit already inside one is ` +
|
|
802
|
+
`part of the baseline and that regeneration will replace it, so check the diff ` +
|
|
803
|
+
`against git. Edits you make FROM NOW ON merge, because the base now exists. ` +
|
|
804
|
+
`Files: ${names.join(", ")}${more}.`,
|
|
805
|
+
);
|
|
806
|
+
};
|
|
807
|
+
|
|
660
808
|
const sweep = (dryRun: boolean): void => {
|
|
661
809
|
if (projectRoot === undefined || orphanJobs.length === 0) return;
|
|
662
810
|
|
|
@@ -758,6 +906,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
758
906
|
});
|
|
759
907
|
}
|
|
760
908
|
reportRefusals();
|
|
909
|
+
reportAdoptions();
|
|
761
910
|
// A preview that hides a pending deletion is worse than no preview at all, so
|
|
762
911
|
// the sweep still runs — in decide-and-report mode, touching nothing.
|
|
763
912
|
sweep(true);
|
|
@@ -791,6 +940,7 @@ export async function runGen(opts: RunGenOpts): Promise<RunGenResult> {
|
|
|
791
940
|
}
|
|
792
941
|
|
|
793
942
|
reportRefusals();
|
|
943
|
+
reportAdoptions();
|
|
794
944
|
|
|
795
945
|
// Sweep AFTER the writes: writing is the primary job, and a deletion that runs
|
|
796
946
|
// first would be unrecoverable if a later write threw. Ordering cannot change
|
|
@@ -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");
|
|
@@ -1,10 +1,31 @@
|
|
|
1
|
-
// Shared
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Shared answers to "how does this entity sort?" — used by every tier that has to
|
|
2
|
+
// answer it, so none of them can answer it differently.
|
|
3
|
+
//
|
|
4
|
+
// Two questions live here:
|
|
5
|
+
// WHICH fields sort — isSortableField / sortableFields, read by the generated
|
|
6
|
+
// <Entity>SortAllowlist (filter-allowlist.ts), the client
|
|
7
|
+
// <Entity>Filter sort union (filter-type.ts) and the agent
|
|
8
|
+
// UI page.
|
|
9
|
+
// WHICH WAY one sorts — declaredSortDefaultOrder / sortDefaultOrder /
|
|
10
|
+
// resolveGridDefaultSort, read by the same allowlist, the
|
|
11
|
+
// generated grid const + grid hook, and the agent UI page.
|
|
12
|
+
//
|
|
13
|
+
// Keeping both here is what prevents a client/server mismatch: a header the grid
|
|
14
|
+
// renders clickable for a field the endpoint rejects, or a grid whose initial sort
|
|
15
|
+
// runs the opposite way from the endpoint it queries.
|
|
5
16
|
|
|
6
|
-
import { MetaField, MetaObject } from "@metaobjectsdev/metadata";
|
|
7
|
-
import {
|
|
17
|
+
import { MetaField, MetaObject, type MetaData } from "@metaobjectsdev/metadata";
|
|
18
|
+
import {
|
|
19
|
+
FIELD_ATTR_FILTERABLE,
|
|
20
|
+
FIELD_ATTR_SORTABLE,
|
|
21
|
+
FIELD_ATTR_SORTABLE_DEFAULT_ORDER,
|
|
22
|
+
LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_FIELD,
|
|
23
|
+
LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_ORDER,
|
|
24
|
+
} from "@metaobjectsdev/metadata";
|
|
25
|
+
|
|
26
|
+
/** A resolved sort direction. Never widened to `string`: every door below either
|
|
27
|
+
* returns one of these two or says it has no answer. */
|
|
28
|
+
export type SortOrder = "asc" | "desc";
|
|
8
29
|
|
|
9
30
|
/**
|
|
10
31
|
* Returns true if the given field should be included in sort operations.
|
|
@@ -28,3 +49,67 @@ export function sortableFields(entity: MetaObject): MetaField[] {
|
|
|
28
49
|
// fields() returns effective fields, so inherited fields (from extends:/super:) are included in sort ops.
|
|
29
50
|
return entity.fields().filter(isSortableField);
|
|
30
51
|
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The direction `@sortableDefaultOrder` DECLARES on this field, or undefined when
|
|
55
|
+
* the field declares none.
|
|
56
|
+
*
|
|
57
|
+
* The declared value is what the generated `<Entity>SortAllowlist` carries, so an
|
|
58
|
+
* undeclared field stays `{}` there and the runtime's own `?? "asc"` remains the
|
|
59
|
+
* single place the fallback is spelled. Callers that need a concrete direction
|
|
60
|
+
* rather than a declaration use `sortDefaultOrder`.
|
|
61
|
+
*
|
|
62
|
+
* ADR-0039: resolving — `@sortableDefaultOrder` may be inherited via `extends`.
|
|
63
|
+
*/
|
|
64
|
+
export function declaredSortDefaultOrder(field: MetaField): SortOrder | undefined {
|
|
65
|
+
const v = field.attr(FIELD_ATTR_SORTABLE_DEFAULT_ORDER);
|
|
66
|
+
return v === "asc" || v === "desc" ? v : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The direction a sort runs when it names this field without giving one. */
|
|
70
|
+
export function sortDefaultOrder(field: MetaField): SortOrder {
|
|
71
|
+
return declaredSortDefaultOrder(field) ?? "asc";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A grid's initial sort: the field it runs on and the direction it runs. */
|
|
75
|
+
export interface GridDefaultSort {
|
|
76
|
+
field: string;
|
|
77
|
+
order: SortOrder;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* A `layout.dataGrid`'s initial sort, resolved once for every tier that renders it.
|
|
82
|
+
*
|
|
83
|
+
* `@defaultSortOrder` wins when the layout declares one. When it does not, the
|
|
84
|
+
* NAMED FIELD's `@sortableDefaultOrder` applies — that is what the attribute has
|
|
85
|
+
* always been registered to mean ("the direction a sort takes when it names this
|
|
86
|
+
* field but omits the order"), and it is the same rule the runtime applies to
|
|
87
|
+
* `?sort=<field>` with no `:order`. Without this the two ends of one declaration
|
|
88
|
+
* disagree: the endpoint returns the field's declared order while the grid that
|
|
89
|
+
* queries it renders the opposite.
|
|
90
|
+
*
|
|
91
|
+
* Returns undefined when the layout names no field — a grid with no initial sort.
|
|
92
|
+
*
|
|
93
|
+
* The field lookup cannot miss on a LOADED model: `validateDataGridSortFields`
|
|
94
|
+
* refuses `@defaultSortField` naming a field the entity does not have
|
|
95
|
+
* (`ERR_BAD_DEFAULT_SORT_FIELD`). "asc" is the answer for a hand-built node that
|
|
96
|
+
* never went through the loader, so codegen degrades rather than throwing.
|
|
97
|
+
*/
|
|
98
|
+
// `layout` is typed as the base MetaData rather than MetaLayout because the three
|
|
99
|
+
// call sites hold the node at different static types (`entity.layouts()` gives
|
|
100
|
+
// MetaLayout, the agent page's own `dataGrids()` gives MetaData) and this reads
|
|
101
|
+
// nothing but `attr()`, which is declared there. Widening beats a cast per caller.
|
|
102
|
+
export function resolveGridDefaultSort(
|
|
103
|
+
entity: MetaObject,
|
|
104
|
+
layout: MetaData,
|
|
105
|
+
): GridDefaultSort | undefined {
|
|
106
|
+
// ADR-0039: resolving — a layout may inherit its grid attrs via extends.
|
|
107
|
+
const fieldName = layout.attr(LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_FIELD);
|
|
108
|
+
if (typeof fieldName !== "string" || fieldName === "") return undefined;
|
|
109
|
+
|
|
110
|
+
const declared = layout.attr(LAYOUT_DATA_GRID_ATTR_DEFAULT_SORT_ORDER);
|
|
111
|
+
if (declared === "asc" || declared === "desc") return { field: fieldName, order: declared };
|
|
112
|
+
|
|
113
|
+
const field = entity.fields().find((f) => f.name === fieldName);
|
|
114
|
+
return { field: fieldName, order: field === undefined ? "asc" : sortDefaultOrder(field) };
|
|
115
|
+
}
|
|
@@ -42,6 +42,7 @@ import { renderDocsFor } from "./jsdoc.js";
|
|
|
42
42
|
import { sharedEnumForField } from "../enum-shared.js";
|
|
43
43
|
import { sharedEnumImportSpecifier, providedEnumImportSpecifier } from "../enum-import.js";
|
|
44
44
|
import { fieldDeclaringPackage, type RenderContext } from "../render-context.js";
|
|
45
|
+
import { effectivePackage } from "../docs-paths.js";
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* Emit Drizzle's InferSelectModel / InferInsertModel aliases for an entity.
|
|
@@ -179,7 +180,7 @@ export function renderEnumTypeAliases(entity: MetaObject, ctx?: RenderContext):
|
|
|
179
180
|
// configured external module; never redeclare the union here.
|
|
180
181
|
const spec = shared.provided
|
|
181
182
|
? providedEnumImportSpecifier(ctx!, shared.name)
|
|
182
|
-
: sharedEnumImportSpecifier(ctx!, entity
|
|
183
|
+
: sharedEnumImportSpecifier(ctx!, effectivePackage(entity));
|
|
183
184
|
// Re-export the runtime constant beside the type for a MATERIALIZED shared enum, so a
|
|
184
185
|
// consumer reaches its members the same way it reaches an inline enum's, without having
|
|
185
186
|
// to know which kind it is or which module the shared one landed in.
|
|
@@ -305,9 +306,10 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
|
|
|
305
306
|
// layout/package/extStyle-aware helper (the SAME one the Zod schema +
|
|
306
307
|
// Drizzle .$type<> use) so all three agree. Without a ctx (bare unit-test
|
|
307
308
|
// calls) fall back to the bare name + flat same-dir specifier.
|
|
308
|
-
const
|
|
309
|
+
const entityPkg = effectivePackage(entity);
|
|
310
|
+
const refName = ctx ? ctx.resolveValueObjectName(ref, fieldDeclaringPackage(field, entityPkg)) : stripPackage(ref);
|
|
309
311
|
const moduleSpec = ctx
|
|
310
|
-
? valueObjectModuleSpecifier(refName, ctx.packageOf,
|
|
312
|
+
? valueObjectModuleSpecifier(refName, ctx.packageOf, entityPkg, ctx.outputLayout, ctx.extStyle)
|
|
311
313
|
: `./${refName}.js`;
|
|
312
314
|
const refImp = imp(`${refName}@${moduleSpec}`);
|
|
313
315
|
return field.resolvedIsArray() ? code`${refImp}[]` : code`${refImp}`;
|
|
@@ -319,9 +321,10 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
|
|
|
319
321
|
if (field.subType === FIELD_SUBTYPE_MAP) {
|
|
320
322
|
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
321
323
|
if (typeof ref === "string" && ref.length > 0) {
|
|
322
|
-
const
|
|
324
|
+
const entityPkg = effectivePackage(entity);
|
|
325
|
+
const refName = ctx ? ctx.resolveValueObjectName(ref, fieldDeclaringPackage(field, entityPkg)) : stripPackage(ref);
|
|
323
326
|
const moduleSpec = ctx
|
|
324
|
-
? valueObjectModuleSpecifier(refName, ctx.packageOf,
|
|
327
|
+
? valueObjectModuleSpecifier(refName, ctx.packageOf, entityPkg, ctx.outputLayout, ctx.extStyle)
|
|
325
328
|
: `./${refName}.js`;
|
|
326
329
|
const refImp = imp(`${refName}@${moduleSpec}`);
|
|
327
330
|
return code`Record<string, ${refImp}>`;
|
|
@@ -349,7 +352,7 @@ function valueObjectFieldType(entity: MetaObject, field: MetaField, ctx?: Render
|
|
|
349
352
|
if (shared !== undefined) {
|
|
350
353
|
const spec = shared.provided
|
|
351
354
|
? providedEnumImportSpecifier(ctx, shared.name)
|
|
352
|
-
: sharedEnumImportSpecifier(ctx, entity
|
|
355
|
+
: sharedEnumImportSpecifier(ctx, effectivePackage(entity));
|
|
353
356
|
const sym = imp(`t:${shared.name}@${spec}`);
|
|
354
357
|
return field.resolvedIsArray() ? code`${sym}[]` : code`${sym}`;
|
|
355
358
|
}
|
|
@@ -23,6 +23,7 @@ import { renderFilterType } from "./filter-type.js";
|
|
|
23
23
|
import { buildUiFieldDescriptor, resourcePath } from "./entity-ui-descriptor.js";
|
|
24
24
|
import { renderExistingViewDecl, renderViewReadZodObject } from "./view-decl.js";
|
|
25
25
|
import { primaryIdentityFieldNames } from "./zod-validators.js";
|
|
26
|
+
import { effectivePackage } from "../docs-paths.js";
|
|
26
27
|
|
|
27
28
|
// ---------------------------------------------------------------------------
|
|
28
29
|
// Public interface
|
|
@@ -103,14 +104,15 @@ export function renderProjectionDecl(
|
|
|
103
104
|
// package-qualified on a cross-package short-name collision, so the projection's
|
|
104
105
|
// VO import matches the entity's. Layout/package/extStyle-aware when a render
|
|
105
106
|
// context is present, else a flat same-dir import (zodFieldExpr's fallback).
|
|
107
|
+
const projectionPkg = effectivePackage(projection);
|
|
106
108
|
const voRef = (field: MetaField): { name: string; module: string } => {
|
|
107
109
|
const ref = field.attr(FIELD_ATTR_OBJECT_REF);
|
|
108
110
|
const rawRef = typeof ref === "string" ? ref : "";
|
|
109
111
|
const name = ctx
|
|
110
|
-
? ctx.resolveValueObjectName(rawRef, fieldDeclaringPackage(field,
|
|
112
|
+
? ctx.resolveValueObjectName(rawRef, fieldDeclaringPackage(field, projectionPkg))
|
|
111
113
|
: stripPackage(rawRef);
|
|
112
114
|
const module = ctx
|
|
113
|
-
? valueObjectModuleSpecifier(name, ctx.packageOf,
|
|
115
|
+
? valueObjectModuleSpecifier(name, ctx.packageOf, projectionPkg, ctx.outputLayout, ctx.extStyle)
|
|
114
116
|
: `./${name}.js`;
|
|
115
117
|
return { name, module };
|
|
116
118
|
};
|