@metaobjectsdev/codegen-ts 0.24.5 → 1.0.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.
- package/README.md +37 -12
- package/dist/api-surface.d.ts +29 -0
- package/dist/api-surface.d.ts.map +1 -1
- package/dist/api-surface.js +47 -0
- package/dist/api-surface.js.map +1 -1
- package/dist/column-mapper.d.ts +12 -0
- package/dist/column-mapper.d.ts.map +1 -1
- package/dist/column-mapper.js +1 -0
- package/dist/column-mapper.js.map +1 -1
- package/dist/constants.d.ts +24 -11
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +54 -19
- package/dist/constants.js.map +1 -1
- package/dist/db-emitting.d.ts +6 -0
- package/dist/db-emitting.d.ts.map +1 -0
- package/dist/db-emitting.js +26 -0
- package/dist/db-emitting.js.map +1 -0
- package/dist/generator-registry.d.ts.map +1 -1
- package/dist/generator-registry.js +14 -1
- package/dist/generator-registry.js.map +1 -1
- package/dist/generator.d.ts +5 -0
- package/dist/generator.d.ts.map +1 -1
- package/dist/generator.js.map +1 -1
- package/dist/generators/agent-docs-file.d.ts +19 -0
- package/dist/generators/agent-docs-file.d.ts.map +1 -0
- package/dist/generators/agent-docs-file.js +257 -0
- package/dist/generators/agent-docs-file.js.map +1 -0
- package/dist/generators/agent-requirements-page.d.ts +22 -0
- package/dist/generators/agent-requirements-page.d.ts.map +1 -0
- package/dist/generators/agent-requirements-page.js +175 -0
- package/dist/generators/agent-requirements-page.js.map +1 -0
- package/dist/generators/agent-schema-input.d.ts +69 -0
- package/dist/generators/agent-schema-input.d.ts.map +1 -0
- package/dist/generators/agent-schema-input.js +23 -0
- package/dist/generators/agent-schema-input.js.map +1 -0
- package/dist/generators/agent-schema-page.d.ts +21 -0
- package/dist/generators/agent-schema-page.d.ts.map +1 -0
- package/dist/generators/agent-schema-page.js +241 -0
- package/dist/generators/agent-schema-page.js.map +1 -0
- package/dist/generators/agent-ui-page.d.ts +22 -0
- package/dist/generators/agent-ui-page.d.ts.map +1 -0
- package/dist/generators/agent-ui-page.js +253 -0
- package/dist/generators/agent-ui-page.js.map +1 -0
- package/dist/generators/api-docs-file.d.ts.map +1 -1
- package/dist/generators/api-docs-file.js +13 -6
- package/dist/generators/api-docs-file.js.map +1 -1
- package/dist/generators/api-model.d.ts +6 -0
- package/dist/generators/api-model.d.ts.map +1 -1
- package/dist/generators/api-model.js +54 -35
- package/dist/generators/api-model.js.map +1 -1
- package/dist/generators/callable-file.js +1 -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 +47 -2
- package/dist/generators/docs-data-builder.js.map +1 -1
- package/dist/generators/docs-data.d.ts +6 -2
- package/dist/generators/docs-data.d.ts.map +1 -1
- package/dist/generators/index.d.ts +3 -8
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +19 -14
- package/dist/generators/index.js.map +1 -1
- package/dist/generators/names-file.d.ts +17 -0
- package/dist/generators/names-file.d.ts.map +1 -0
- package/dist/generators/names-file.js +95 -0
- package/dist/generators/names-file.js.map +1 -0
- package/dist/generators/requirements-markdown.d.ts +18 -1
- package/dist/generators/requirements-markdown.d.ts.map +1 -1
- package/dist/generators/requirements-markdown.js +11 -9
- package/dist/generators/requirements-markdown.js.map +1 -1
- package/dist/generators/routes-file-hono.d.ts +17 -2
- package/dist/generators/routes-file-hono.d.ts.map +1 -1
- package/dist/generators/routes-file-hono.js +9 -11
- package/dist/generators/routes-file-hono.js.map +1 -1
- package/dist/generators/routes-file.d.ts +17 -2
- package/dist/generators/routes-file.d.ts.map +1 -1
- package/dist/generators/routes-file.js +10 -8
- package/dist/generators/routes-file.js.map +1 -1
- package/dist/generators/template-generator.d.ts.map +1 -1
- package/dist/generators/template-generator.js +20 -6
- package/dist/generators/template-generator.js.map +1 -1
- package/dist/index.d.ts +20 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +43 -4
- package/dist/index.js.map +1 -1
- package/dist/metaobjects-config.d.ts +38 -1
- package/dist/metaobjects-config.d.ts.map +1 -1
- package/dist/metaobjects-config.js +9 -1
- package/dist/metaobjects-config.js.map +1 -1
- package/dist/names.d.ts +275 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +433 -0
- package/dist/names.js.map +1 -0
- package/dist/overwrite-policy.d.ts.map +1 -1
- package/dist/overwrite-policy.js +15 -3
- package/dist/overwrite-policy.js.map +1 -1
- package/dist/projection/extract-view-spec.d.ts +25 -1
- package/dist/projection/extract-view-spec.d.ts.map +1 -1
- package/dist/projection/extract-view-spec.js +79 -6
- package/dist/projection/extract-view-spec.js.map +1 -1
- package/dist/reference-templates.d.ts +1 -1
- package/dist/reference-templates.d.ts.map +1 -1
- package/dist/reference-templates.js +1 -1
- package/dist/reference-templates.js.map +1 -1
- package/dist/render-context.d.ts +12 -1
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js +1 -0
- package/dist/render-context.js.map +1 -1
- package/dist/retired-codegen-attrs.d.ts +32 -0
- package/dist/retired-codegen-attrs.d.ts.map +1 -0
- package/dist/retired-codegen-attrs.js +47 -0
- package/dist/retired-codegen-attrs.js.map +1 -0
- package/dist/routes-expose.d.ts +63 -0
- package/dist/routes-expose.d.ts.map +1 -0
- package/dist/routes-expose.js +57 -0
- package/dist/routes-expose.js.map +1 -0
- package/dist/runner.d.ts +18 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +109 -7
- package/dist/runner.js.map +1 -1
- package/dist/templates/callable-file.d.ts +7 -2
- package/dist/templates/callable-file.d.ts.map +1 -1
- package/dist/templates/callable-file.js +54 -9
- 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 +236 -65
- package/dist/templates/drizzle-schema.js.map +1 -1
- package/dist/templates/entity-constants.d.ts +15 -6
- package/dist/templates/entity-constants.d.ts.map +1 -1
- package/dist/templates/entity-constants.js +85 -161
- package/dist/templates/entity-constants.js.map +1 -1
- package/dist/templates/entity-file.d.ts.map +1 -1
- package/dist/templates/entity-file.js +38 -4
- package/dist/templates/entity-file.js.map +1 -1
- package/dist/templates/entity-meta-file.d.ts +15 -1
- package/dist/templates/entity-meta-file.d.ts.map +1 -1
- package/dist/templates/entity-meta-file.js +12 -2
- package/dist/templates/entity-meta-file.js.map +1 -1
- package/dist/templates/entity-ui-descriptor.d.ts +112 -0
- package/dist/templates/entity-ui-descriptor.d.ts.map +1 -0
- package/dist/templates/entity-ui-descriptor.js +238 -0
- package/dist/templates/entity-ui-descriptor.js.map +1 -0
- package/dist/templates/field-meta.d.ts +46 -9
- package/dist/templates/field-meta.d.ts.map +1 -1
- package/dist/templates/field-meta.js +95 -21
- package/dist/templates/field-meta.js.map +1 -1
- package/dist/templates/names-decl.d.ts +38 -0
- package/dist/templates/names-decl.d.ts.map +1 -0
- package/dist/templates/names-decl.js +104 -0
- package/dist/templates/names-decl.js.map +1 -0
- package/dist/templates/projection-decl.d.ts +21 -0
- package/dist/templates/projection-decl.d.ts.map +1 -1
- package/dist/templates/projection-decl.js +53 -29
- 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 +17 -2
- package/dist/templates/queries-file.js.map +1 -1
- package/dist/templates/requirement-test.d.ts.map +1 -1
- package/dist/templates/requirement-test.js +13 -2
- package/dist/templates/requirement-test.js.map +1 -1
- package/dist/templates/routes-file-hono.d.ts +2 -1
- package/dist/templates/routes-file-hono.d.ts.map +1 -1
- package/dist/templates/routes-file-hono.js +11 -2
- package/dist/templates/routes-file-hono.js.map +1 -1
- package/dist/templates/routes-file.d.ts +2 -1
- package/dist/templates/routes-file.d.ts.map +1 -1
- package/dist/templates/routes-file.js +54 -21
- package/dist/templates/routes-file.js.map +1 -1
- package/dist/templates/value-object-file.d.ts.map +1 -1
- package/dist/templates/value-object-file.js +15 -1
- package/dist/templates/value-object-file.js.map +1 -1
- package/dist/templates/view-decl.d.ts +37 -1
- package/dist/templates/view-decl.d.ts.map +1 -1
- package/dist/templates/view-decl.js +27 -3
- package/dist/templates/view-decl.js.map +1 -1
- package/dist/templates/zod-validators.d.ts +21 -0
- package/dist/templates/zod-validators.d.ts.map +1 -1
- package/dist/templates/zod-validators.js +40 -5
- package/dist/templates/zod-validators.js.map +1 -1
- package/dist/view-context.d.ts +24 -0
- package/dist/view-context.d.ts.map +1 -0
- package/dist/view-context.js +81 -0
- package/dist/view-context.js.map +1 -0
- package/package.json +6 -6
- package/src/api-surface.ts +53 -0
- package/src/column-mapper.ts +13 -0
- package/src/constants.ts +91 -16
- package/src/db-emitting.ts +31 -0
- package/src/generator-registry.ts +14 -4
- package/src/generator.ts +5 -0
- package/src/generators/agent-docs-file.ts +297 -0
- package/src/generators/agent-requirements-page.ts +199 -0
- package/src/generators/agent-schema-input.ts +88 -0
- package/src/generators/agent-schema-page.ts +297 -0
- package/src/generators/agent-ui-page.ts +290 -0
- package/src/generators/api-docs-file.ts +13 -6
- package/src/generators/api-model.ts +63 -34
- package/src/generators/callable-file.ts +1 -1
- package/src/generators/docs-data-builder.ts +46 -2
- package/src/generators/docs-data.ts +6 -2
- package/src/generators/index.ts +25 -14
- package/src/generators/names-file.ts +98 -0
- package/src/generators/requirements-markdown.ts +36 -2
- package/src/generators/routes-file-hono.ts +19 -10
- package/src/generators/routes-file.ts +20 -7
- package/src/generators/template-generator.ts +20 -6
- package/src/index.ts +64 -4
- package/src/metaobjects-config.ts +48 -2
- package/src/names.ts +629 -0
- package/src/overwrite-policy.ts +15 -3
- package/src/projection/extract-view-spec.ts +85 -6
- package/src/reference/entity.ts +21 -2
- package/src/reference/names.ts +125 -0
- package/src/reference/queries.ts +19 -3
- package/src/reference/routes-hono.ts +19 -9
- package/src/reference/routes.ts +25 -9
- package/src/reference-templates.ts +1 -1
- package/src/render-context.ts +13 -1
- package/src/retired-codegen-attrs.ts +52 -0
- package/src/routes-expose.ts +86 -0
- package/src/runner.ts +122 -11
- package/src/templates/callable-file.ts +60 -8
- package/src/templates/drizzle-schema.ts +248 -60
- package/src/templates/entity-constants.ts +95 -184
- package/src/templates/entity-file.ts +46 -4
- package/src/templates/entity-meta-file.ts +16 -3
- package/src/templates/entity-ui-descriptor.ts +305 -0
- package/src/templates/field-meta.ts +100 -19
- package/src/templates/names-decl.ts +165 -0
- package/src/templates/projection-decl.ts +72 -31
- package/src/templates/queries-file.ts +17 -2
- package/src/templates/requirement-test.ts +13 -2
- package/src/templates/routes-file-hono.ts +14 -2
- package/src/templates/routes-file.ts +69 -24
- package/src/templates/value-object-file.ts +15 -1
- package/src/templates/view-decl.ts +53 -3
- package/src/templates/zod-validators.ts +41 -5
- package/src/view-context.ts +88 -0
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
TYPE_RELATIONSHIP,
|
|
7
7
|
isMetaObject,
|
|
8
8
|
isReadOnlySource,
|
|
9
|
+
SOURCE_ROLE_PRIMARY,
|
|
10
|
+
SOURCE_ATTR_SCHEMA,
|
|
9
11
|
ORIGIN_SUBTYPE_PASSTHROUGH,
|
|
10
12
|
ORIGIN_SUBTYPE_AGGREGATE,
|
|
11
13
|
ORIGIN_SUBTYPE_COMPUTED,
|
|
@@ -49,7 +51,7 @@ import {
|
|
|
49
51
|
resolveObjectRef,
|
|
50
52
|
type AggregateFunction,
|
|
51
53
|
} from "@metaobjectsdev/metadata";
|
|
52
|
-
import { type MetaData, type MetaField, type MetaRoot, MetaObject } from "@metaobjectsdev/metadata";
|
|
54
|
+
import { type MetaData, type MetaField, type MetaRoot, type MetaSource, MetaObject } from "@metaobjectsdev/metadata";
|
|
53
55
|
import { intValueMapOf } from "../enum-meta.js";
|
|
54
56
|
import {
|
|
55
57
|
columnNameFromField,
|
|
@@ -331,16 +333,58 @@ function resolveHop(
|
|
|
331
333
|
function viewName(projection: MetaObject, ctx: ExtractContext): string {
|
|
332
334
|
// The read-only source carries the physical view name. FR-016: physicalName
|
|
333
335
|
// implements the four-step rule (kind-matching alias → legacy @table →
|
|
334
|
-
// source.name → entity-name fallback), so
|
|
335
|
-
// @view / @materializedView / legacy @table for projection
|
|
336
|
+
// source.name → entity-name fallback), so `viewSource.physicalName` below
|
|
337
|
+
// correctly resolves @view / @materializedView / legacy @table for projection
|
|
338
|
+
// sources.
|
|
336
339
|
// ADR-0039: own — projection source classification (mirrors C# projection
|
|
337
|
-
// OwnSources / IsReadOnlyProjection): the view name comes from the
|
|
338
|
-
// OWN read-only source, not one inherited via extends.
|
|
339
|
-
|
|
340
|
+
// OwnSources / IsReadOnlyProjection): the view name comes from the object's
|
|
341
|
+
// OWN read-only source, not one inherited via extends. `ERR_PROJECTION_
|
|
342
|
+
// INHERITED_SOURCE` (subtype-rules.ts) already refuses a concrete projection
|
|
343
|
+
// that would otherwise inherit a source, so own-only never drops a legally
|
|
344
|
+
// reachable source here.
|
|
345
|
+
//
|
|
346
|
+
// This function serves TWO shapes — a plain projection, and a write-through
|
|
347
|
+
// ENTITY's replica view (extractViewSpec's `writeThrough` branch, #213/#214;
|
|
348
|
+
// the parameter name is historical) — which need DIFFERENT selection rules,
|
|
349
|
+
// hence "prefer role:primary, else the first read-only source" rather than
|
|
350
|
+
// "require role:primary":
|
|
351
|
+
// - A plain projection: EVERY own source must be read-only-kind
|
|
352
|
+
// (ERR_PROJECTION_SOURCE_WRITABLE), so if it has any sources at all,
|
|
353
|
+
// exactly one is role:"primary" (ERR_SOURCE_NO_PRIMARY /
|
|
354
|
+
// ERR_SOURCE_MULTIPLE_PRIMARY) and that one IS read-only — the primary
|
|
355
|
+
// branch always fires when there is a source.
|
|
356
|
+
// - A write-through entity: its role:"primary" source is the WRITABLE
|
|
357
|
+
// table (never read-only), so the primary branch never fires; its
|
|
358
|
+
// read-only companion (the replica view) is intentionally NOT primary.
|
|
359
|
+
// The fallback (first read-only own source) is what names it.
|
|
360
|
+
//
|
|
361
|
+
// Selecting the role:"primary" read-only source PREFERENTIALLY over
|
|
362
|
+
// declaration order (rather than "first read-only source in file order") is
|
|
363
|
+
// what makes this agree with `resolveTableName()` (the resolver every §A6
|
|
364
|
+
// site defers to via `resolveObjectNames`): a plain projection declaring a
|
|
365
|
+
// role:"replica" source before its role:"primary" one would otherwise bind a
|
|
366
|
+
// different physical view depending on whether the names generator happened
|
|
367
|
+
// to be active in the run.
|
|
368
|
+
const readOnlySources = projection.ownChildren().filter(isReadOnlySource);
|
|
369
|
+
const viewSource =
|
|
370
|
+
readOnlySources.find((c) => c.role === SOURCE_ROLE_PRIMARY) ?? readOnlySources[0];
|
|
340
371
|
const explicit = viewSource?.physicalName;
|
|
341
372
|
// physicalName always returns a string; empty string means the source had
|
|
342
373
|
// neither alias nor a name and the owning entity name was empty (impossible
|
|
343
374
|
// for a real projection). Fall through to the helper anyway for safety.
|
|
375
|
+
//
|
|
376
|
+
// This fallback fires only when the object has NO own read-only source at
|
|
377
|
+
// all. It differs from `resolveTableName()`'s own no-source fallback
|
|
378
|
+
// (`v_<snake(name)>` here vs `pluralize(toSnakeCase(name))` there), but that
|
|
379
|
+
// difference is UNOBSERVABLE: whenever a sourceless object reaches this
|
|
380
|
+
// fallback, `resolveObjectNames()` also finds no primary source and returns
|
|
381
|
+
// `undefined` before ever calling `resolveTableName()` — so no §A6 site has
|
|
382
|
+
// a names constant to reference either way, and both the ON and OFF arms
|
|
383
|
+
// land on THIS function's literal on the identical no-source condition. A
|
|
384
|
+
// sourceless projection also fails `isProjection()` itself
|
|
385
|
+
// (projection-detector.ts requires a read-only-kind source), so codegen's
|
|
386
|
+
// dispatch never reaches `renderProjectionDecl`/this function for it in the
|
|
387
|
+
// first place — doubly unreachable.
|
|
344
388
|
return explicit !== undefined && explicit !== ""
|
|
345
389
|
? explicit
|
|
346
390
|
: viewNameFromProjection(projection.name, ctx.columnNamingStrategy);
|
|
@@ -363,6 +407,41 @@ export function projectionViewName(
|
|
|
363
407
|
return viewName(projection, { columnNamingStrategy });
|
|
364
408
|
}
|
|
365
409
|
|
|
410
|
+
/**
|
|
411
|
+
* The `@schema` of the SAME source node {@link projectionViewName} names, or undefined.
|
|
412
|
+
*
|
|
413
|
+
* It re-uses that function's selection rule ("prefer role:primary read-only, else the first
|
|
414
|
+
* read-only own source") deliberately, rather than asking the object for its schema: for a
|
|
415
|
+
* write-through ENTITY the object's primary source is the WRITABLE table, so
|
|
416
|
+
* `resolveTableSchema` would hand back the table's schema and qualify the replica view with
|
|
417
|
+
* it. A view and the table it replicates need not live in the same schema, and a name paired
|
|
418
|
+
* with someone else's schema is worse than no schema at all — it is confidently wrong.
|
|
419
|
+
*/
|
|
420
|
+
export function projectionViewSchema(projection: MetaObject): string | undefined {
|
|
421
|
+
const schema = projectionViewSource(projection)?.attr(SOURCE_ATTR_SCHEMA);
|
|
422
|
+
return typeof schema === "string" && schema !== "" ? schema : undefined;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* The source node {@link projectionViewName} names — the ONE selection, so the name, the
|
|
427
|
+
* schema and the role cannot be picked by three functions that agree until they do not.
|
|
428
|
+
*
|
|
429
|
+
* Hoisted out of `projectionViewSchema`, which used to be the only place the rule was
|
|
430
|
+
* written twice-removed from `viewName`. The third caller is what forced it: the names
|
|
431
|
+
* artifact keys its sources by ROLE, so a consumer reaching for a write-through entity's
|
|
432
|
+
* replica view has to know which role that view plays, and guessing `"replica"` would be
|
|
433
|
+
* exactly the second derivation this file exists to avoid.
|
|
434
|
+
*/
|
|
435
|
+
export function projectionViewSource(projection: MetaObject): MetaSource | undefined {
|
|
436
|
+
const readOnlySources = projection.ownChildren().filter(isReadOnlySource);
|
|
437
|
+
return readOnlySources.find((c) => c.role === SOURCE_ROLE_PRIMARY) ?? readOnlySources[0];
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** The ROLE of that source — the key its entry has in `<Entity>Names.sources`. */
|
|
441
|
+
export function projectionViewRole(projection: MetaObject): string | undefined {
|
|
442
|
+
return projectionViewSource(projection)?.role;
|
|
443
|
+
}
|
|
444
|
+
|
|
366
445
|
/**
|
|
367
446
|
* FR-024 (ADR-0029): the entity NAMED by a node's dotted extends ref — the
|
|
368
447
|
* owner part of `<owner>.<child>...` resolved as an object. Mirrors the
|
package/src/reference/entity.ts
CHANGED
|
@@ -61,11 +61,14 @@ import {
|
|
|
61
61
|
isWriteThrough,
|
|
62
62
|
isAbstract,
|
|
63
63
|
hasWritableRdbSource,
|
|
64
|
+
isTphSubtype,
|
|
64
65
|
// engine composer — used for the delegated write-through variant:
|
|
65
66
|
renderEntityFile,
|
|
66
67
|
// engine plumbing:
|
|
67
68
|
formatTs,
|
|
68
69
|
entityOutputPath,
|
|
70
|
+
namesRef,
|
|
71
|
+
namesConstArg,
|
|
69
72
|
GENERATED_HEADER,
|
|
70
73
|
} from "@metaobjectsdev/codegen-ts";
|
|
71
74
|
|
|
@@ -84,6 +87,10 @@ function renderEntity(entity: MetaObject, ctx: RenderContext, opts?: RenderEntit
|
|
|
84
87
|
}
|
|
85
88
|
// Projection → read-only view declaration + read schema.
|
|
86
89
|
if (isProjection(entity)) {
|
|
90
|
+
// §A6/§B2 — same `namesRef` pair renderDrizzleSchema builds, so the projection's view
|
|
91
|
+
// name + per-field dbCol reference the exact constant the names artifact exports
|
|
92
|
+
// (undefined when the artifact is not in this run). `namesRef`'s `{ resolved, symbol }`
|
|
93
|
+
// return is exactly the shape `ProjectionDeclOpts.names` wants.
|
|
87
94
|
return renderProjectionDecl(entity, ctx.loadedRoot, {
|
|
88
95
|
columnNamingStrategy: ctx.columnNamingStrategy,
|
|
89
96
|
dialect: ctx.dialect,
|
|
@@ -92,10 +99,18 @@ function renderEntity(entity: MetaObject, ctx: RenderContext, opts?: RenderEntit
|
|
|
92
99
|
allowlists,
|
|
93
100
|
ctx,
|
|
94
101
|
includeViewDecl: runtime,
|
|
102
|
+
names: namesRef(entity, ctx),
|
|
95
103
|
});
|
|
96
104
|
}
|
|
97
105
|
// Value-only / contract target → interface + Zod, no Drizzle table.
|
|
98
|
-
|
|
106
|
+
//
|
|
107
|
+
// A TPH subtype (FR-017) routes here too. It INHERITS the discriminator base's writable
|
|
108
|
+
// source.rdb through `extends`, so `hasWritableRdbSource` is true for it under the
|
|
109
|
+
// ADR-0039 resolving read — but the base owns the single shared table. Without the
|
|
110
|
+
// isTphSubtype clause this template fell through to the vanilla path and emitted a
|
|
111
|
+
// SECOND Drizzle table bound to the base's physical name, carrying only the subtype's
|
|
112
|
+
// own columns.
|
|
113
|
+
if (!runtime || !hasWritableRdbSource(entity) || isTphSubtype(entity)) {
|
|
99
114
|
return renderValueObjectFile(entity, ctx.apiPrefix, ctx);
|
|
100
115
|
}
|
|
101
116
|
// #214 — a write-through entity read-view (writable table + a read-only replica view +
|
|
@@ -110,12 +125,16 @@ function renderEntity(entity: MetaObject, ctx: RenderContext, opts?: RenderEntit
|
|
|
110
125
|
const enumAliases = renderEnumTypeAliases(entity, ctx);
|
|
111
126
|
const tphBlock = renderTphDiscriminatorUnion(entity, ctx.loadedRoot);
|
|
112
127
|
const tphBase = tphBlock !== null && isTphDiscriminatorBase(entity, ctx.loadedRoot);
|
|
128
|
+
// §A6/§B2 — same `namesRef` pair renderDrizzleSchema (and the projection branch above)
|
|
129
|
+
// build, so the descriptor's $table references the exact constant the names artifact
|
|
130
|
+
// exports (undefined when the artifact is not in this run).
|
|
131
|
+
const constantsNames = namesRef(entity, ctx);
|
|
113
132
|
const sections: Code[] = [
|
|
114
133
|
renderDrizzleSchema(entity, ctx),
|
|
115
134
|
renderInferredTypes(entity, tphBase, ctx),
|
|
116
135
|
...(enumAliases !== null ? [enumAliases] : []),
|
|
117
136
|
renderZodValidators(entity, ctx),
|
|
118
|
-
renderEntityConstants(entity, ctx.apiPrefix),
|
|
137
|
+
renderEntityConstants(entity, ctx.apiPrefix, namesConstArg(constantsNames)),
|
|
119
138
|
...(allowlists ? [renderFilterAllowlist(entity, undefined, ctx), renderSortAllowlist(entity)] : []),
|
|
120
139
|
renderFilterType(entity),
|
|
121
140
|
...(tphBlock !== null ? [tphBlock] : []),
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// REFERENCE TEMPLATE — copy this into your repo (e.g. codegen/generators/names.ts) and own it.
|
|
2
|
+
// Then import it LOCALLY in metaobjects.config.ts:
|
|
3
|
+
// import { namesFile } from "./codegen/generators/names.js";
|
|
4
|
+
//
|
|
5
|
+
// RUNTIME: this file executes under whatever runs `meta gen`, and the published CLI's
|
|
6
|
+
// shebang is `#!/usr/bin/env node` — so it runs under NODE even in a Bun project. Do not
|
|
7
|
+
// reach for `Bun.*` globals here; they are undefined and take the whole run down with
|
|
8
|
+
// `Bun is not defined`. Use `node:` builtins instead.
|
|
9
|
+
// targets: the emit step. Replace `renderNamesDecl` to change the artifact's SHAPE
|
|
10
|
+
// (e.g. flat `SUBSCRIBER_TABLE` constants instead of a nested object); keep
|
|
11
|
+
// `resolveObjectNames` so the names still come from the same resolver as the DDL.
|
|
12
|
+
// use-when: you want the physical database names (table/view + column names) for each
|
|
13
|
+
// object available as importable constants, so a hand-written consumer never
|
|
14
|
+
// has to embed a name as a string literal a second time.
|
|
15
|
+
// emits: <target>/<Entity>.names.ts per concrete object with a primary source
|
|
16
|
+
// (under outputLayout: "package", <target>/<pkg>/<Entity>.names.ts — beside
|
|
17
|
+
// the entity module it describes). An object with no primary source (#248)
|
|
18
|
+
// gets no names artifact — EXCEPT an abstract base that a sourced object
|
|
19
|
+
// extends, which gets a fragment (columns, no physical name) so its
|
|
20
|
+
// children extend it instead of restating every inherited column.
|
|
21
|
+
// customize: swap `renderNamesDecl` for your own shape; keep `resolveObjectNames` as the
|
|
22
|
+
// one resolver so the constant and the DDL it describes cannot disagree.
|
|
23
|
+
// composes-with: entity.ts, routes.ts — both reference these constants instead of
|
|
24
|
+
// embedding the names a second time. queries.ts never embeds a physical
|
|
25
|
+
// name of its own: it reads/writes through the Drizzle table object
|
|
26
|
+
// entity.ts builds (columns keyed by FIELD name), so there is nothing in
|
|
27
|
+
// it to wire.
|
|
28
|
+
//
|
|
29
|
+
// The composition here is deliberately a SEPARATE generator, never a boolean on the entity
|
|
30
|
+
// generator — a new artifact is a MINOR under docs/compatibility-policy.md and adds zero
|
|
31
|
+
// bytes to existing files, where a flag would move every $table-carrying golden for the
|
|
32
|
+
// same functionality.
|
|
33
|
+
import {
|
|
34
|
+
crossEntitySpecifier,
|
|
35
|
+
entityOutputPath,
|
|
36
|
+
namesArtifactSuperOf,
|
|
37
|
+
renderNamesDecl,
|
|
38
|
+
resolveObjectNames,
|
|
39
|
+
type EmittedFile,
|
|
40
|
+
type GenContext,
|
|
41
|
+
type Generator,
|
|
42
|
+
} from "@metaobjectsdev/codegen-ts";
|
|
43
|
+
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
44
|
+
|
|
45
|
+
export function namesFile(): Generator {
|
|
46
|
+
return {
|
|
47
|
+
name: "names",
|
|
48
|
+
// §A6 — the marker the runner aggregates into ResolvedGenConfig.includeNames, so the
|
|
49
|
+
// entity generator can tell whether this artifact will exist. Exactly the mechanism
|
|
50
|
+
// routesFileHono already uses via emitsHonoRoutes/includeHonoRoutes.
|
|
51
|
+
emitsNames: true,
|
|
52
|
+
generate: (ctx: GenContext): EmittedFile[] => {
|
|
53
|
+
const layout = ctx.config.outputLayout ?? "flat";
|
|
54
|
+
const extStyle = ctx.config.extStyle ?? "js";
|
|
55
|
+
// The strategy lives on the RENDER CONTEXT, not on ResolvedGenConfig — `ctx.config`
|
|
56
|
+
// carries outDir/extStyle/dbImport/dialect and nothing about naming.
|
|
57
|
+
const strategy = ctx.renderContext?.columnNamingStrategy;
|
|
58
|
+
|
|
59
|
+
const pathOf = (obj: MetaObject): string =>
|
|
60
|
+
// entityOutputPath, not a bare filename: §A6 makes the entity module IMPORT these
|
|
61
|
+
// constants, so the artifact has to land in the same directory the entity module
|
|
62
|
+
// does. Under outputLayout: "package" a bare name puts it at the target ROOT while
|
|
63
|
+
// its entity sits at <pkg>/<Entity>.ts — an unresolvable import, and a hard
|
|
64
|
+
// conflicting-duplicate-path failure as soon as two packages declare a
|
|
65
|
+
// same-bare-named entity.
|
|
66
|
+
entityOutputPath(layout, obj.package, `${obj.name}.names.ts`);
|
|
67
|
+
|
|
68
|
+
const superSpecifierFor = (obj: MetaObject): string | undefined => {
|
|
69
|
+
const sup = namesArtifactSuperOf(obj);
|
|
70
|
+
return sup === undefined
|
|
71
|
+
? undefined
|
|
72
|
+
: crossEntitySpecifier(layout, obj.package, sup.package, `${sup.name}.names`, extStyle);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const out: EmittedFile[] = [];
|
|
76
|
+
// Pass 1 — every matched object that participates in the database (#248).
|
|
77
|
+
// `emitted` tracks what pass 1 actually WROTE, not what it looked at: a matched
|
|
78
|
+
// abstract base emits nothing here, and seeding it as already-emitted is what would
|
|
79
|
+
// make pass 2 skip the very object it exists to produce.
|
|
80
|
+
const emitted = new Set<string>();
|
|
81
|
+
const participants = ctx.entities.filter(ctx.matches);
|
|
82
|
+
for (const entity of participants) {
|
|
83
|
+
const content = renderNamesDecl(entity, {
|
|
84
|
+
strategy, superSpecifier: superSpecifierFor(entity),
|
|
85
|
+
});
|
|
86
|
+
if (content === "") continue; // no primary source ⇒ no names artifact (#248)
|
|
87
|
+
emitted.add(entity.resolutionKey());
|
|
88
|
+
out.push({ path: pathOf(entity), content });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Pass 2 — the abstract bases those participants EXTEND. Each carries the columns it
|
|
92
|
+
// declares, so a child states them once rather than restating its parent's.
|
|
93
|
+
//
|
|
94
|
+
// Reached by walking UP from a participant, never by scanning for abstracts: that is
|
|
95
|
+
// what keeps #248 intact. A sourceless object nothing persistable extends — an
|
|
96
|
+
// `object.value`, say — is not reached, so it acquires no artifact and no phantom
|
|
97
|
+
// participation. Two children of one base both reach it and emit the same file at
|
|
98
|
+
// the same path with the same bytes; the runner collapses byte-identical duplicates
|
|
99
|
+
// (#266), so it is written once. `emitted` keeps that from even arising, and keeps
|
|
100
|
+
// the walk from re-rendering the same base once per child.
|
|
101
|
+
for (const entity of participants) {
|
|
102
|
+
if (resolveObjectNames(entity, strategy) === undefined) continue;
|
|
103
|
+
for (let sup = namesArtifactSuperOf(entity); sup !== undefined;
|
|
104
|
+
sup = namesArtifactSuperOf(sup)) {
|
|
105
|
+
const key = sup.resolutionKey();
|
|
106
|
+
if (emitted.has(key)) break; // already emitted, and so is everything above it
|
|
107
|
+
emitted.add(key);
|
|
108
|
+
const content = renderNamesDecl(sup, {
|
|
109
|
+
strategy, superSpecifier: superSpecifierFor(sup),
|
|
110
|
+
// "Fragment" means "declares no source". Hardcoding `true` is right for the
|
|
111
|
+
// shape this pass was written for — an abstract base with columns and no table
|
|
112
|
+
// — and wrong for the one it also reaches: `meta gen --entities <Subtype>`
|
|
113
|
+
// walks up to a TPH BASE, which owns the shared table, and a fragment renders
|
|
114
|
+
// no source at all. The engine derives this itself now (`renderNamesDecl`
|
|
115
|
+
// consults the object), so the flag says only "this is an ancestor render".
|
|
116
|
+
fragment: true,
|
|
117
|
+
});
|
|
118
|
+
if (content === "") continue;
|
|
119
|
+
out.push({ path: pathOf(sup), content });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
}
|
package/src/reference/queries.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
renderFindByIdFn,
|
|
42
42
|
renderListFn,
|
|
43
43
|
renderCreateFn,
|
|
44
|
+
renderInsertPreservingFn,
|
|
44
45
|
renderUpdateFn,
|
|
45
46
|
renderDeleteByIdFn,
|
|
46
47
|
renderReverseFinderFns,
|
|
@@ -50,6 +51,7 @@ import {
|
|
|
50
51
|
isWriteThrough,
|
|
51
52
|
isTphSubtype,
|
|
52
53
|
hasAnyRdbSource,
|
|
54
|
+
hasAutoSetFields,
|
|
53
55
|
renderQueriesFile, // engine composer — used for the delegated variants
|
|
54
56
|
formatTs,
|
|
55
57
|
entityOutputPath,
|
|
@@ -77,20 +79,33 @@ function renderQueries(obj: MetaObject, ctx: RenderContext): string {
|
|
|
77
79
|
|
|
78
80
|
// `db` is parameter-passed into every finder (ADR-0008). Emit the dialect-correct
|
|
79
81
|
// Drizzle type alias so signatures typecheck without the consumer constructing one.
|
|
82
|
+
// Keep EVERY type argument as open as Drizzle's own constraint allows — a `Db` this
|
|
83
|
+
// alias cannot name is a helper nobody can call, and uncompilable generated code looks
|
|
84
|
+
// exactly like unused generated code from outside. Both the driver axis (base classes,
|
|
85
|
+
// not `NodePgDatabase` / `<"async">`) and the schema axis (`Record<string, unknown>`,
|
|
86
|
+
// Drizzle's own `TFullSchema extends …` bound, NOT its `Record<string, never>` default)
|
|
87
|
+
// have been re-pinned once already; do not narrow either again. A schema-carrying
|
|
88
|
+
// `drizzle(client, { schema })` must assign, and so must a schema-less `drizzle(client)`.
|
|
80
89
|
const dbTypeImport =
|
|
81
90
|
ctx.dialect === "postgres"
|
|
82
91
|
? `import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";`
|
|
83
92
|
: `import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";`;
|
|
84
93
|
const dbTypeAlias =
|
|
85
94
|
ctx.dialect === "postgres"
|
|
86
|
-
? `type Db = PgDatabase<PgQueryResultHKT, Record<string,
|
|
87
|
-
: `type Db = BaseSQLiteDatabase<"sync" | "async", unknown
|
|
95
|
+
? `type Db = PgDatabase<PgQueryResultHKT, Record<string, unknown>>;`
|
|
96
|
+
: `type Db = BaseSQLiteDatabase<"sync" | "async", unknown, Record<string, unknown>>;`;
|
|
97
|
+
|
|
98
|
+
// #203 — an @autoSet entity additionally imports its preserving-shape schema and emits
|
|
99
|
+
// the `insertPreserving<Entity>` escape hatch after `create<Entity>`. OWNED: drop both
|
|
100
|
+
// halves together if you never insert rows carrying their own timestamps.
|
|
101
|
+
const autoSet = hasAutoSetFields(obj);
|
|
102
|
+
const preservingImport = autoSet ? `, ${entityName}InsertPreservingSchema` : "";
|
|
88
103
|
|
|
89
104
|
const literalImports = code`
|
|
90
105
|
${dbTypeImport}
|
|
91
106
|
${dbTypeAlias}
|
|
92
107
|
|
|
93
|
-
import { ${varName}, type ${entityName}, type ${entityName}Patch, ${entityName}InsertSchema, ${entityName}UpdateSchema } from ${JSON.stringify(entityFileName)};
|
|
108
|
+
import { ${varName}, type ${entityName}, type ${entityName}Patch, ${entityName}InsertSchema${preservingImport}, ${entityName}UpdateSchema } from ${JSON.stringify(entityFileName)};
|
|
94
109
|
`;
|
|
95
110
|
|
|
96
111
|
const sections: Code[] = [
|
|
@@ -98,6 +113,7 @@ import { ${varName}, type ${entityName}, type ${entityName}Patch, ${entityName}I
|
|
|
98
113
|
renderFindByIdFn(obj, ctx),
|
|
99
114
|
renderListFn(obj, ctx),
|
|
100
115
|
renderCreateFn(obj, ctx),
|
|
116
|
+
...(autoSet ? [renderInsertPreservingFn(obj, ctx)] : []),
|
|
101
117
|
renderUpdateFn(obj, ctx),
|
|
102
118
|
renderDeleteByIdFn(obj, ctx),
|
|
103
119
|
];
|
|
@@ -19,7 +19,11 @@
|
|
|
19
19
|
// object and for TPH subtypes.
|
|
20
20
|
// customize: this generator is YOURS — edit it freely. For the emitted route
|
|
21
21
|
// composition, call `renderRoutesFileHono` (exported from the engine) and
|
|
22
|
-
// wrap its result, or replace the call entirely.
|
|
22
|
+
// wrap its result, or replace the call entirely. Decide per generator what
|
|
23
|
+
// you consume: wire only the generators whose output you actually import,
|
|
24
|
+
// and narrow this one with its `filter`. There is no `@emit*` metadata
|
|
25
|
+
// attribute — those were never registered vocabulary, so `meta verify`
|
|
26
|
+
// rejects them (ERR_UNKNOWN_ATTR).
|
|
23
27
|
// composes-with: entity.ts (imports the table/schemas/allowlists), queries.ts.
|
|
24
28
|
|
|
25
29
|
import { type MetaObject } from "@metaobjectsdev/metadata";
|
|
@@ -28,15 +32,26 @@ import {
|
|
|
28
32
|
type Generator,
|
|
29
33
|
type GeneratorFactory,
|
|
30
34
|
renderRoutesFileHono,
|
|
35
|
+
resolveExpose,
|
|
36
|
+
type ExposeOption,
|
|
31
37
|
isTphSubtype,
|
|
32
38
|
hasAnyRdbSource,
|
|
33
39
|
formatTs,
|
|
34
40
|
entityOutputPath,
|
|
35
|
-
CODEGEN_ATTR_EMIT_ROUTES,
|
|
36
41
|
} from "@metaobjectsdev/codegen-ts";
|
|
37
42
|
|
|
38
43
|
export interface RoutesFileHonoOpts {
|
|
39
44
|
filter?: (entity: MetaObject) => boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Which CRUD verbs the emitted file mounts (#348). Verbs, or a per-entity function;
|
|
47
|
+
* absent means all five and emits byte-identical output.
|
|
48
|
+
*
|
|
49
|
+
* routesFileHono({ expose: (e) => e.name === "AuditEntry" ? ["list", "get"] : undefined })
|
|
50
|
+
*
|
|
51
|
+
* A `filter` cannot express this — it decides whether the file emits AT ALL, so it can
|
|
52
|
+
* only remove the whole surface, not restrict it to a subset of verbs.
|
|
53
|
+
*/
|
|
54
|
+
expose?: ExposeOption;
|
|
40
55
|
target?: string;
|
|
41
56
|
}
|
|
42
57
|
|
|
@@ -48,17 +63,12 @@ export const routesFileHono = function routesFileHono(opts?: RoutesFileHonoOpts)
|
|
|
48
63
|
// without being named as held back, or get warned about while still emitting.
|
|
49
64
|
// (Same shape as tanstack's grid generator, which factors it the same way.)
|
|
50
65
|
const passesOtherGates = (e: MetaObject): boolean =>
|
|
51
|
-
|
|
52
|
-
e.attr(CODEGEN_ATTR_EMIT_ROUTES) !== false
|
|
53
|
-
&& hasAnyRdbSource(e)
|
|
54
|
-
&& userFilter(e);
|
|
66
|
+
hasAnyRdbSource(e) && userFilter(e);
|
|
55
67
|
const generator: Generator = {
|
|
56
68
|
name: "routes-file-hono",
|
|
57
69
|
// Marks this as the Hono routes generator so the runner can aggregate
|
|
58
70
|
// `ctx.config.includeHonoRoutes` and api-docs auto-documents the Hono surface.
|
|
59
71
|
emitsHonoRoutes: true,
|
|
60
|
-
// ADR-0039: resolving — a concrete entity may inherit @emitRoutes via extends.
|
|
61
|
-
//
|
|
62
72
|
// TPH subtypes are EXCLUDED, matching the Fastify generator. A TPH subtype shares
|
|
63
73
|
// its base's table, so mounting vanilla CRUD for it produced routes with no
|
|
64
74
|
// discriminator scoping at all: the list returned EVERY subtype's rows, and
|
|
@@ -93,7 +103,7 @@ export const routesFileHono = function routesFileHono(opts?: RoutesFileHonoOpts)
|
|
|
93
103
|
entity.package,
|
|
94
104
|
`${entity.name}.routes.hono.ts`,
|
|
95
105
|
),
|
|
96
|
-
content: await formatTs(renderRoutesFileHono(entity, ctx.renderContext)),
|
|
106
|
+
content: await formatTs(renderRoutesFileHono(entity, ctx.renderContext, resolveExpose(entity, opts?.expose))),
|
|
97
107
|
};
|
|
98
108
|
});
|
|
99
109
|
if (opts?.target) {
|
package/src/reference/routes.ts
CHANGED
|
@@ -12,14 +12,20 @@
|
|
|
12
12
|
// framework; see also the routes-hono template, whose deps-injected shape
|
|
13
13
|
// ports more easily to hosts that hand you a request.
|
|
14
14
|
// use-when: you want generated Fastify REST routes per entity. Drop it and hand-write routes
|
|
15
|
-
// if you need bespoke endpoints — or keep it and
|
|
15
|
+
// if you need bespoke endpoints — or keep it and register extra handlers YOURSELF
|
|
16
|
+
// alongside the generated `register<Entity>Routes(app)` call. Nothing here
|
|
17
|
+
// discovers a sibling module: a `<Entity>.extra.ts` next to the output is a naming
|
|
18
|
+
// convention, not a plugin point, so its handlers only mount if your server calls them.
|
|
16
19
|
// emits: <target>/<Entity>.routes.ts — full CRUD for write-through entities, read-only
|
|
17
20
|
// (GET list + GET :id) for projections, polymorphic + per-subtype for TPH bases.
|
|
18
21
|
// Skipped for any sourceless object (incl. every object.value, source-less by
|
|
19
22
|
// value purity) and for TPH subtypes — no source.rdb means no table/allowlist
|
|
20
23
|
// for a routes file to import (#248 R2).
|
|
21
|
-
// customize: this generator (filter, output path,
|
|
22
|
-
//
|
|
24
|
+
// customize: this generator (filter, output path, target) is YOURS — edit it freely.
|
|
25
|
+
// Decide per generator what you consume: wire only the generators whose
|
|
26
|
+
// output you actually import, and narrow this one with its `filter`. There
|
|
27
|
+
// is no `@emit*` metadata attribute — those were never registered
|
|
28
|
+
// vocabulary, so `meta verify` rejects them (ERR_UNKNOWN_ATTR). The route *composition* itself is richer than the others
|
|
23
29
|
// (M:N junction traversal, TPH per-subtype route sets), so it stays in the engine via
|
|
24
30
|
// `renderRoutesFile`. To own the composition too, copy `renderRoutesFile`'s body out
|
|
25
31
|
// of the package source — it dispatches projection → mountReadOnlyCrudRoutes,
|
|
@@ -34,15 +40,26 @@ import {
|
|
|
34
40
|
type Generator,
|
|
35
41
|
type GeneratorFactory,
|
|
36
42
|
renderRoutesFile,
|
|
43
|
+
resolveExpose,
|
|
44
|
+
type ExposeOption,
|
|
37
45
|
isTphSubtype,
|
|
38
46
|
hasAnyRdbSource,
|
|
39
47
|
formatTs,
|
|
40
48
|
entityOutputPath,
|
|
41
|
-
CODEGEN_ATTR_EMIT_ROUTES,
|
|
42
49
|
} from "@metaobjectsdev/codegen-ts";
|
|
43
50
|
|
|
44
51
|
export interface RoutesFileOpts {
|
|
45
52
|
filter?: (entity: MetaObject) => boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Which CRUD verbs the emitted file mounts (#348). Verbs, or a per-entity function;
|
|
55
|
+
* absent means all five and emits byte-identical output.
|
|
56
|
+
*
|
|
57
|
+
* routesFile({ expose: (e) => e.name === "AuditEntry" ? ["list", "get"] : undefined })
|
|
58
|
+
*
|
|
59
|
+
* A `filter` cannot express this — it decides whether the file emits AT ALL, so it can
|
|
60
|
+
* only remove the whole surface, not restrict it to a subset of verbs.
|
|
61
|
+
*/
|
|
62
|
+
expose?: ExposeOption;
|
|
46
63
|
target?: string;
|
|
47
64
|
}
|
|
48
65
|
|
|
@@ -50,21 +67,20 @@ export const routesFile = function routesFile(opts?: RoutesFileOpts): Generator
|
|
|
50
67
|
const userFilter = opts?.filter ?? (() => true);
|
|
51
68
|
const generator: Generator = {
|
|
52
69
|
name: "routes-file",
|
|
53
|
-
//
|
|
54
|
-
//
|
|
70
|
+
// TPH subtypes get no standalone routes file (their routes live in the discriminator
|
|
71
|
+
// base's); AND-composed with your filter.
|
|
55
72
|
// #248 R2: an object with no declared/inherited source.rdb (of ANY kind) isn't
|
|
56
73
|
// backed by any store — routes against it would import Drizzle table/allowlist
|
|
57
74
|
// exports the entity file never emits. Gated by hasAnyRdbSource.
|
|
58
75
|
filter: (e: MetaObject) =>
|
|
59
|
-
|
|
60
|
-
e.attr(CODEGEN_ATTR_EMIT_ROUTES) !== false && hasAnyRdbSource(e) && !isTphSubtype(e) && userFilter(e),
|
|
76
|
+
hasAnyRdbSource(e) && !isTphSubtype(e) && userFilter(e),
|
|
61
77
|
generate: perEntity(async (entity, ctx) => {
|
|
62
78
|
if (!ctx.renderContext) {
|
|
63
79
|
throw new Error("routes-file: renderContext is required (provided by runGen)");
|
|
64
80
|
}
|
|
65
81
|
return {
|
|
66
82
|
path: entityOutputPath(ctx.config.outputLayout ?? "flat", entity.package, `${entity.name}.routes.ts`),
|
|
67
|
-
content: await formatTs(renderRoutesFile(entity, ctx.renderContext)),
|
|
83
|
+
content: await formatTs(renderRoutesFile(entity, ctx.renderContext, resolveExpose(entity, opts?.expose))),
|
|
68
84
|
};
|
|
69
85
|
}),
|
|
70
86
|
};
|
|
@@ -13,7 +13,7 @@ import { dirname, join } from "node:path";
|
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
14
|
|
|
15
15
|
/** Basenames (no extension) of the copyable reference generators shipped in `src/reference/`. */
|
|
16
|
-
export const REFERENCE_GENERATOR_NAMES = ["entity", "queries", "routes", "routes-hono", "barrel"] as const;
|
|
16
|
+
export const REFERENCE_GENERATOR_NAMES = ["entity", "queries", "routes", "routes-hono", "barrel", "names"] as const;
|
|
17
17
|
export type ReferenceGeneratorName = (typeof REFERENCE_GENERATOR_NAMES)[number];
|
|
18
18
|
|
|
19
19
|
/** A directory is a reference root iff it holds the first template the reader was told to expect. */
|
package/src/render-context.ts
CHANGED
|
@@ -65,6 +65,16 @@ export interface RenderContext {
|
|
|
65
65
|
emitAbstractShapes: boolean;
|
|
66
66
|
/** Output layout mode: "flat" (default) — all files in outDir; "package" — sub-paths from entity metadata package. */
|
|
67
67
|
outputLayout: OutputLayout;
|
|
68
|
+
/**
|
|
69
|
+
* §A6 — whether the run emits the `<Entity>Names` artifact (the runner aggregates the
|
|
70
|
+
* suite's `emitsNames` markers; `ctx.config.includeNames` carries the same fact for
|
|
71
|
+
* generators). A template may reference those constants ONLY when this is true: the
|
|
72
|
+
* names generator is opt-in under ADR-0034 scaffold-and-own, so an unconditional
|
|
73
|
+
* import would break every project that has not enabled it. Defaults to FALSE, which
|
|
74
|
+
* is what keeps output byte-identical for such a project — and for every bare-context
|
|
75
|
+
* caller (unit tests, generators invoked outside runGen).
|
|
76
|
+
*/
|
|
77
|
+
includeNames: boolean;
|
|
68
78
|
/**
|
|
69
79
|
* Resolve an entity name to its Drizzle collection (table) variable name,
|
|
70
80
|
* applying the project's pluralization config + per-entity overrides. Every
|
|
@@ -108,7 +118,7 @@ export interface RenderContext {
|
|
|
108
118
|
}
|
|
109
119
|
|
|
110
120
|
/** Optional shape — `extStyle`, `omImport`, `columnNamingStrategy`, `apiPrefix`, `outputLayout`, and `packageOf` default if omitted. `packageOf` defaults to an empty Map (correct for flat layout; `runGen` always provides the real map). `collectionName` is built from `pluralizeCollections` + `collectionNameOverrides` (both default to always-pluralize). */
|
|
111
|
-
export type RenderContextInput = Omit<RenderContext, "extStyle" | "omImport" | "columnNamingStrategy" | "timestampMode" | "clientDirective" | "apiPrefix" | "emitAbstractShapes" | "outputLayout" | "packageOf" | "valueObjectNames" | "valueObjectEmittedName" | "resolveValueObjectName" | "selfTarget" | "entityModuleTarget" | "collectionName"> & {
|
|
121
|
+
export type RenderContextInput = Omit<RenderContext, "extStyle" | "omImport" | "columnNamingStrategy" | "timestampMode" | "clientDirective" | "apiPrefix" | "emitAbstractShapes" | "outputLayout" | "includeNames" | "packageOf" | "valueObjectNames" | "valueObjectEmittedName" | "resolveValueObjectName" | "selfTarget" | "entityModuleTarget" | "collectionName"> & {
|
|
112
122
|
extStyle?: ExtStyle;
|
|
113
123
|
omImport?: string;
|
|
114
124
|
columnNamingStrategy?: ColumnNamingStrategy;
|
|
@@ -117,6 +127,7 @@ export type RenderContextInput = Omit<RenderContext, "extStyle" | "omImport" | "
|
|
|
117
127
|
apiPrefix?: string;
|
|
118
128
|
emitAbstractShapes?: boolean;
|
|
119
129
|
outputLayout?: OutputLayout;
|
|
130
|
+
includeNames?: boolean;
|
|
120
131
|
packageOf?: Map<string, string | undefined>;
|
|
121
132
|
/** ADR-0044/#228 value-object emitted-name map (resolutionKey → emitted name).
|
|
122
133
|
* Defaults to an empty Map — bare names, byte-identical to pre-#228 output.
|
|
@@ -196,6 +207,7 @@ export function makeRenderContext(opts: RenderContextInput): RenderContext {
|
|
|
196
207
|
apiPrefix: opts.apiPrefix ?? "",
|
|
197
208
|
emitAbstractShapes: opts.emitAbstractShapes ?? true,
|
|
198
209
|
outputLayout,
|
|
210
|
+
includeNames: opts.includeNames ?? false,
|
|
199
211
|
packageOf: opts.packageOf ?? new Map(),
|
|
200
212
|
valueObjectNames,
|
|
201
213
|
valueObjectEmittedName: (obj: MetaData) => valueObjectNames.get(obj.resolutionKey()) ?? obj.name,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { MetaObject } from "@metaobjectsdev/metadata";
|
|
2
|
+
import { RETIRED_CODEGEN_ATTRS } from "./constants.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `@emitRoutes` / `@emitTanstack` / `@emitForm` / `@emitGrid` / `@emitAngular` were read
|
|
6
|
+
* off metadata by generator filters and were never registered metamodel vocabulary. Under
|
|
7
|
+
* the strict loader — which is what `meta verify` runs — every one of them is
|
|
8
|
+
* `ERR_UNKNOWN_ATTR`, while `meta gen` loads non-strict and honoured them. That is the
|
|
9
|
+
* defect: an adopter who authored the documented opt-out got working suppression AND a red
|
|
10
|
+
* `meta verify`, with nothing connecting the two.
|
|
11
|
+
*
|
|
12
|
+
* The reads are gone (see constants.ts for why registering them was refused rather than
|
|
13
|
+
* the other way round). But a project can be sitting on a WORKING `@emitRoutes: false`
|
|
14
|
+
* today, and deleting the read alone would start writing that suppressed file with no
|
|
15
|
+
* explanation — a silent behaviour change on upgrade, which is the one outcome not open
|
|
16
|
+
* to us. So the run says it.
|
|
17
|
+
*
|
|
18
|
+
* This follows the `layout.dataGrid` precedent (#287, data-grid-gate.ts) and the prompt
|
|
19
|
+
* generator gate beside it: tell the adopter at `meta gen` time rather than in a doc line
|
|
20
|
+
* that gets missed the same way the original one was. Warning only — the exit code is
|
|
21
|
+
* untouched, and `--dry-run` reports it too, since generators run before the write phase
|
|
22
|
+
* branches.
|
|
23
|
+
*
|
|
24
|
+
* It lives in the RUNNER, not in a generator, for two reasons. It is a model-level
|
|
25
|
+
* observation, so it must fire exactly ONCE per run rather than once per generator that
|
|
26
|
+
* happens to be wired; and the attribute is equally stale whether or not the generator it
|
|
27
|
+
* used to suppress is wired at all, so a generator-local check would go quiet in exactly
|
|
28
|
+
* the project that dropped the generator and kept the attribute.
|
|
29
|
+
*
|
|
30
|
+
* Self-extinguishing: removing the attribute from the metadata silences it forever. That
|
|
31
|
+
* is also the fix, since the same edit is what makes `meta verify` pass.
|
|
32
|
+
*/
|
|
33
|
+
export function warnRetiredCodegenAttrs(
|
|
34
|
+
entities: readonly MetaObject[],
|
|
35
|
+
warn: (msg: string) => void,
|
|
36
|
+
): void {
|
|
37
|
+
for (const { name, replacement } of RETIRED_CODEGEN_ATTRS) {
|
|
38
|
+
// ADR-0039: resolving — an INHERITED flag suppressed emission exactly as an own one
|
|
39
|
+
// did, so an own-only read here would leave the inheriting adopter unwarned while
|
|
40
|
+
// their output silently changed. `hasAttr` rather than a value comparison: an
|
|
41
|
+
// `@emitForm: true` was a no-op that still fails `meta verify`, so it is just as
|
|
42
|
+
// stale as the `false` that did something.
|
|
43
|
+
const carriers = entities.filter((e) => e.hasAttr(name));
|
|
44
|
+
if (carriers.length === 0) continue;
|
|
45
|
+
warn(
|
|
46
|
+
`@${name} on ${carriers.map((e) => e.name).join(", ")} is no longer read by any ` +
|
|
47
|
+
`generator — it was never registered metamodel vocabulary, so \`meta verify\` ` +
|
|
48
|
+
`rejects it outright with ERR_UNKNOWN_ATTR. Decide per generator what you ` +
|
|
49
|
+
`consume: ${replacement}. Remove the attribute to silence this.`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
}
|