@metaobjectsdev/codegen-ts 0.24.4 → 0.25.0
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/client-directive.d.ts +18 -0
- package/dist/client-directive.d.ts.map +1 -0
- package/dist/client-directive.js +37 -0
- package/dist/client-directive.js.map +1 -0
- 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 +7 -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 -0
- package/dist/generators/index.d.ts.map +1 -1
- package/dist/generators/index.js +4 -0
- 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 +15 -12
- 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 +21 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -4
- package/dist/index.js.map +1 -1
- package/dist/metaobjects-config.d.ts +67 -1
- package/dist/metaobjects-config.d.ts.map +1 -1
- package/dist/metaobjects-config.js +10 -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 +434 -0
- package/dist/names.js.map +1 -0
- package/dist/naming.d.ts +17 -0
- package/dist/naming.d.ts.map +1 -1
- package/dist/naming.js +21 -0
- package/dist/naming.js.map +1 -1
- 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/payload-codegen.d.ts.map +1 -1
- package/dist/payload-codegen.js +4 -3
- package/dist/payload-codegen.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/prompt-generator-gate.d.ts +27 -0
- package/dist/prompt-generator-gate.d.ts.map +1 -0
- package/dist/prompt-generator-gate.js +62 -0
- package/dist/prompt-generator-gate.js.map +1 -0
- package/dist/reference-templates.d.ts +22 -5
- package/dist/reference-templates.d.ts.map +1 -1
- package/dist/reference-templates.js +55 -21
- package/dist/reference-templates.js.map +1 -1
- package/dist/render-context.d.ts +20 -1
- package/dist/render-context.d.ts.map +1 -1
- package/dist/render-context.js +2 -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 +115 -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/output-parser.d.ts.map +1 -1
- package/dist/templates/output-parser.js +54 -8
- package/dist/templates/output-parser.js.map +1 -1
- 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/render-helper.d.ts.map +1 -1
- package/dist/templates/render-helper.js +3 -1
- package/dist/templates/render-helper.js.map +1 -1
- package/dist/templates/requirement-test.d.ts.map +1 -1
- package/dist/templates/requirement-test.js +48 -21
- 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/client-directive.ts +36 -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 +7 -0
- 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 +10 -0
- package/src/generators/names-file.ts +98 -0
- package/src/generators/requirements-markdown.ts +36 -2
- package/src/generators/routes-file-hono.ts +26 -15
- package/src/generators/routes-file.ts +20 -7
- package/src/generators/template-generator.ts +20 -6
- package/src/index.ts +63 -5
- package/src/metaobjects-config.ts +78 -2
- package/src/names.ts +630 -0
- package/src/naming.ts +23 -0
- package/src/overwrite-policy.ts +15 -3
- package/src/payload-codegen.ts +4 -3
- package/src/projection/extract-view-spec.ts +85 -6
- package/src/prompt-generator-gate.ts +73 -0
- package/src/reference/barrel.ts +3 -0
- package/src/reference/entity.ts +28 -2
- package/src/reference/names.ts +125 -0
- package/src/reference/queries.ts +14 -2
- package/src/reference/routes-hono.ts +113 -0
- package/src/reference/routes.ts +30 -9
- package/src/reference-templates.ts +61 -21
- package/src/render-context.ts +22 -1
- package/src/retired-codegen-attrs.ts +52 -0
- package/src/routes-expose.ts +86 -0
- package/src/runner.ts +129 -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/output-parser.ts +54 -7
- package/src/templates/projection-decl.ts +72 -31
- package/src/templates/queries-file.ts +17 -2
- package/src/templates/render-helper.ts +3 -1
- package/src/templates/requirement-test.ts +49 -18
- 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
package/src/overwrite-policy.ts
CHANGED
|
@@ -610,10 +610,22 @@ export function decideAndWrite(
|
|
|
610
610
|
return {
|
|
611
611
|
path,
|
|
612
612
|
status: "refused",
|
|
613
|
+
// The hint states WHAT HAPPENED, per file, and points at the recovery rather
|
|
614
|
+
// than restating it. The two remedies it used to name were both WRONG for the
|
|
615
|
+
// generator with the most at stake: `requirementTests()` emits a stub whose
|
|
616
|
+
// entire value is the body you write over it, and whose own header says "do not
|
|
617
|
+
// rename the test — the name is the link", so "move your edits into a
|
|
618
|
+
// non-generated file" asks for something the artifact forbids, and
|
|
619
|
+
// "--baseline=fresh to discard them" discards the only content the file has.
|
|
620
|
+
// The sequence that actually keeps an edit is three commands long and identical
|
|
621
|
+
// for every file in the run, so `runner.ts` prints it once; a doc pointer is
|
|
622
|
+
// what a direct `decideAndWrite` caller (a public export — no runner involved)
|
|
623
|
+
// gets instead, and it is never wrong.
|
|
613
624
|
conflictHint: kase.hasRecord
|
|
614
|
-
? "this file has been edited since it was generated
|
|
615
|
-
"
|
|
616
|
-
"
|
|
625
|
+
? "this file has been edited since it was generated, and there is no " +
|
|
626
|
+
".gen-state snapshot body on this machine to merge against — it was NOT " +
|
|
627
|
+
"overwritten, and your version is intact on disk. Recovery: " +
|
|
628
|
+
"docs/features/own-your-codegen.md."
|
|
617
629
|
: "no record of generating this file, and its content differs from fresh " +
|
|
618
630
|
"output — it was NOT overwritten. Move it aside, or re-run with " +
|
|
619
631
|
"--baseline=fresh to overwrite it and adopt fresh output as the baseline.",
|
package/src/payload-codegen.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
stripPackage,
|
|
42
42
|
} from "@metaobjectsdev/metadata";
|
|
43
43
|
import { enumValues } from "./enum-meta.js";
|
|
44
|
+
import { templateSymbolBase } from "./naming.js";
|
|
44
45
|
import { enumUnionAliasName, enumUnionString } from "./templates/inferred-types.js";
|
|
45
46
|
import { assignEmittedNames } from "./naming/collision-names.js";
|
|
46
47
|
|
|
@@ -258,9 +259,9 @@ export function generatePayloadInterfacesBatch(
|
|
|
258
259
|
return out.length === 0 ? "" : out.join("\n\n") + "\n";
|
|
259
260
|
}
|
|
260
261
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
// Delegates to the shared authority so this emitter cannot drift from the parser and
|
|
263
|
+
// render-helper again (naming.ts / templateSymbolBase).
|
|
264
|
+
const pascal = templateSymbolBase;
|
|
264
265
|
|
|
265
266
|
/** Emit a typed render handle binding a template's @textRef + @format and typing its payload. */
|
|
266
267
|
export function generateRenderHandle(root: MetaData, templateName: string): string {
|
|
@@ -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
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { MetaData } from "@metaobjectsdev/metadata";
|
|
2
|
+
import { TYPE_TEMPLATE, TEMPLATE_SUBTYPE_PROMPT, TEMPLATE_ATTR_RESPONSE_REF } from "@metaobjectsdev/metadata";
|
|
3
|
+
import type { Generator } from "./generator.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Generators that turn a declared `template.*` into runnable prompt code. Names, not
|
|
7
|
+
* identities, because an adopter may wrap or re-export them (ADR-0034 scaffold-and-own
|
|
8
|
+
* makes owning a copy the encouraged path) and a wrapper keeps the name.
|
|
9
|
+
*/
|
|
10
|
+
const PROMPT_GENERATOR_NAMES: ReadonlySet<string> = new Set([
|
|
11
|
+
"prompt-render",
|
|
12
|
+
"output-parser",
|
|
13
|
+
"render-helper",
|
|
14
|
+
"output-prompt",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A declared prompt with no prompt generator wired produced NOTHING and said NOTHING.
|
|
19
|
+
*
|
|
20
|
+
* `meta gen` emitted the payload value objects (they are `object.value` nodes, which the
|
|
21
|
+
* entity generator picks up) and stopped there: no `render<Name>()`, no parser, no
|
|
22
|
+
* response-format fragment. `meta verify` then reported "1 template(s) clean", which reads
|
|
23
|
+
* as confirmation that the prompt is fine. So the fourth pillar produced two type files and
|
|
24
|
+
* a green gate, and the adopter had neither a send side nor a receive side.
|
|
25
|
+
*
|
|
26
|
+
* The wiring IS documented — but only in the prompts skill's per-language reference
|
|
27
|
+
* fragment, which SKILL.md points at in its final line, while the skill body walks the
|
|
28
|
+
* entire declaration without once saying a generator is required. Found by declaring a
|
|
29
|
+
* `template.prompt` in a from-scratch app exactly as that skill teaches.
|
|
30
|
+
*
|
|
31
|
+
* This follows the `layout.dataGrid` precedent (#287, data-grid-gate.ts): tell the adopter
|
|
32
|
+
* at `meta gen` time rather than adding a doc line that gets missed the same way. It is a
|
|
33
|
+
* WARNING — the exit code is untouched — and it is **self-extinguishing**: wire any one
|
|
34
|
+
* prompt generator and it goes quiet forever, so a project that has made this choice
|
|
35
|
+
* deliberately is never nagged.
|
|
36
|
+
*
|
|
37
|
+
* It must live in the runner rather than in a generator, because the whole condition is
|
|
38
|
+
* that the generator which would speak up is not running.
|
|
39
|
+
*/
|
|
40
|
+
export function warnMissingPromptGenerators(
|
|
41
|
+
root: MetaData,
|
|
42
|
+
generators: readonly Generator[],
|
|
43
|
+
warn: (msg: string) => void,
|
|
44
|
+
): void {
|
|
45
|
+
if (generators.some((g) => PROMPT_GENERATOR_NAMES.has(g.name))) return;
|
|
46
|
+
|
|
47
|
+
// ADR-0039: resolving children — a template may arrive through an overlay or extends.
|
|
48
|
+
const templates = root
|
|
49
|
+
.children()
|
|
50
|
+
.filter((c) => c.type === TYPE_TEMPLATE && c.subType === TEMPLATE_SUBTYPE_PROMPT);
|
|
51
|
+
if (templates.length === 0) return;
|
|
52
|
+
|
|
53
|
+
const names = templates.map((t) => t.name).join(", ");
|
|
54
|
+
// A @responseRef is what asks for the inbound tier (ADR-0052), so a responding prompt
|
|
55
|
+
// is missing strictly more than an outbound-only one. Name that, rather than making the
|
|
56
|
+
// adopter infer which half is absent.
|
|
57
|
+
const responding = templates.filter((t) => typeof t.attr(TEMPLATE_ATTR_RESPONSE_REF) === "string");
|
|
58
|
+
const receiveHalf =
|
|
59
|
+
responding.length > 0
|
|
60
|
+
? ` ${responding.length === templates.length ? "All" : `${responding.length} of them`} ` +
|
|
61
|
+
`declare a @responseRef, so the parser and response-format fragment are missing too — ` +
|
|
62
|
+
`add outputParser().`
|
|
63
|
+
: "";
|
|
64
|
+
|
|
65
|
+
warn(
|
|
66
|
+
`${templates.length} declared template.prompt (${names}) generated no prompt code: ` +
|
|
67
|
+
`no prompt generator is wired, so there is no render function to send one and nothing ` +
|
|
68
|
+
`read a reply. Add promptRender() to \`generators\` in metaobjects.config.ts ` +
|
|
69
|
+
`(import it from "@metaobjectsdev/codegen-ts/generators").${receiveHalf} ` +
|
|
70
|
+
`The payload value objects were emitted regardless — those are object.value nodes, ` +
|
|
71
|
+
`which is why this looked like it had worked.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
package/src/reference/barrel.ts
CHANGED
|
@@ -6,6 +6,9 @@
|
|
|
6
6
|
// shebang is `#!/usr/bin/env node` — so it runs under NODE even in a Bun project. Do not
|
|
7
7
|
// reach for `Bun.*` globals here; they are undefined and take the whole run down with
|
|
8
8
|
// `Bun is not defined`. Use `node:` builtins instead.
|
|
9
|
+
// targets: nothing framework-specific — it re-exports whatever the other generators
|
|
10
|
+
// emitted. `extStyle` decides whether the re-export specifiers carry a
|
|
11
|
+
// `.js` extension.
|
|
9
12
|
// use-when: you want a single `index.ts` re-exporting every generated entity module.
|
|
10
13
|
// emits: <target>/index.ts with one `export * from "./<Entity>"` per entity, alphabetical.
|
|
11
14
|
// customize: the export form (star vs named), ordering, grouping by package, what to include/exclude.
|
package/src/reference/entity.ts
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
// shebang is `#!/usr/bin/env node` — so it runs under NODE even in a Bun project. Do not
|
|
7
7
|
// reach for `Bun.*` globals here; they are undefined and take the whole run down with
|
|
8
8
|
// `Bun is not defined`. Use `node:` builtins instead.
|
|
9
|
+
// targets: Drizzle ORM + Zod. The emitted module is a Drizzle table plus Zod
|
|
10
|
+
// insert/update schemas; the column mapping follows `dialect`. On the
|
|
11
|
+
// default (vanilla) path, replace the `renderDrizzleSchema` /
|
|
12
|
+
// `renderZodValidators` calls to target a different ORM or validator — the
|
|
13
|
+
// metadata walk that feeds them is ORM-neutral. (The `isWriteThrough`
|
|
14
|
+
// branch calls `renderEntityFile` instead — a narrow #214 read-view case,
|
|
15
|
+
// not the default.)
|
|
9
16
|
// use-when: ALWAYS — this is the entity-module generator. It owns the shape of each
|
|
10
17
|
// generated <Entity>.ts (the Drizzle table, Zod schemas, inferred types,
|
|
11
18
|
// constants, filter allowlists). Start here and adapt the assembly.
|
|
@@ -54,11 +61,14 @@ import {
|
|
|
54
61
|
isWriteThrough,
|
|
55
62
|
isAbstract,
|
|
56
63
|
hasWritableRdbSource,
|
|
64
|
+
isTphSubtype,
|
|
57
65
|
// engine composer — used for the delegated write-through variant:
|
|
58
66
|
renderEntityFile,
|
|
59
67
|
// engine plumbing:
|
|
60
68
|
formatTs,
|
|
61
69
|
entityOutputPath,
|
|
70
|
+
namesRef,
|
|
71
|
+
namesConstArg,
|
|
62
72
|
GENERATED_HEADER,
|
|
63
73
|
} from "@metaobjectsdev/codegen-ts";
|
|
64
74
|
|
|
@@ -77,6 +87,10 @@ function renderEntity(entity: MetaObject, ctx: RenderContext, opts?: RenderEntit
|
|
|
77
87
|
}
|
|
78
88
|
// Projection → read-only view declaration + read schema.
|
|
79
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.
|
|
80
94
|
return renderProjectionDecl(entity, ctx.loadedRoot, {
|
|
81
95
|
columnNamingStrategy: ctx.columnNamingStrategy,
|
|
82
96
|
dialect: ctx.dialect,
|
|
@@ -85,10 +99,18 @@ function renderEntity(entity: MetaObject, ctx: RenderContext, opts?: RenderEntit
|
|
|
85
99
|
allowlists,
|
|
86
100
|
ctx,
|
|
87
101
|
includeViewDecl: runtime,
|
|
102
|
+
names: namesRef(entity, ctx),
|
|
88
103
|
});
|
|
89
104
|
}
|
|
90
105
|
// Value-only / contract target → interface + Zod, no Drizzle table.
|
|
91
|
-
|
|
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)) {
|
|
92
114
|
return renderValueObjectFile(entity, ctx.apiPrefix, ctx);
|
|
93
115
|
}
|
|
94
116
|
// #214 — a write-through entity read-view (writable table + a read-only replica view +
|
|
@@ -103,12 +125,16 @@ function renderEntity(entity: MetaObject, ctx: RenderContext, opts?: RenderEntit
|
|
|
103
125
|
const enumAliases = renderEnumTypeAliases(entity, ctx);
|
|
104
126
|
const tphBlock = renderTphDiscriminatorUnion(entity, ctx.loadedRoot);
|
|
105
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);
|
|
106
132
|
const sections: Code[] = [
|
|
107
133
|
renderDrizzleSchema(entity, ctx),
|
|
108
134
|
renderInferredTypes(entity, tphBase, ctx),
|
|
109
135
|
...(enumAliases !== null ? [enumAliases] : []),
|
|
110
136
|
renderZodValidators(entity, ctx),
|
|
111
|
-
renderEntityConstants(entity, ctx.apiPrefix),
|
|
137
|
+
renderEntityConstants(entity, ctx.apiPrefix, namesConstArg(constantsNames)),
|
|
112
138
|
...(allowlists ? [renderFilterAllowlist(entity, undefined, ctx), renderSortAllowlist(entity)] : []),
|
|
113
139
|
renderFilterType(entity),
|
|
114
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
|
@@ -6,6 +6,11 @@
|
|
|
6
6
|
// shebang is `#!/usr/bin/env node` — so it runs under NODE even in a Bun project. Do not
|
|
7
7
|
// reach for `Bun.*` globals here; they are undefined and take the whole run down with
|
|
8
8
|
// `Bun is not defined`. Use `node:` builtins instead.
|
|
9
|
+
// targets: Drizzle. Emitted helpers take `db` as a PARAMETER rather than importing
|
|
10
|
+
// a module singleton, so they compose with any caller that already holds a
|
|
11
|
+
// connection — including a server-rendered component. Swap the
|
|
12
|
+
// `render<Verb>Fn` primitives (findById/list/create/update/deleteById) and
|
|
13
|
+
// the inline `Db` type block above them to emit for another query builder.
|
|
9
14
|
// use-when: you want generated typed CRUD finders (find<E>ById, list<E>s, create/update/delete)
|
|
10
15
|
// over Drizzle. Drop it if you hand-write your data access.
|
|
11
16
|
// emits: <target>/<Entity>.queries.ts per source-backed object (any source.rdb kind,
|
|
@@ -72,14 +77,21 @@ function renderQueries(obj: MetaObject, ctx: RenderContext): string {
|
|
|
72
77
|
|
|
73
78
|
// `db` is parameter-passed into every finder (ADR-0008). Emit the dialect-correct
|
|
74
79
|
// Drizzle type alias so signatures typecheck without the consumer constructing one.
|
|
80
|
+
// Keep EVERY type argument as open as Drizzle's own constraint allows — a `Db` this
|
|
81
|
+
// alias cannot name is a helper nobody can call, and uncompilable generated code looks
|
|
82
|
+
// exactly like unused generated code from outside. Both the driver axis (base classes,
|
|
83
|
+
// not `NodePgDatabase` / `<"async">`) and the schema axis (`Record<string, unknown>`,
|
|
84
|
+
// Drizzle's own `TFullSchema extends …` bound, NOT its `Record<string, never>` default)
|
|
85
|
+
// have been re-pinned once already; do not narrow either again. A schema-carrying
|
|
86
|
+
// `drizzle(client, { schema })` must assign, and so must a schema-less `drizzle(client)`.
|
|
75
87
|
const dbTypeImport =
|
|
76
88
|
ctx.dialect === "postgres"
|
|
77
89
|
? `import type { PgDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";`
|
|
78
90
|
: `import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";`;
|
|
79
91
|
const dbTypeAlias =
|
|
80
92
|
ctx.dialect === "postgres"
|
|
81
|
-
? `type Db = PgDatabase<PgQueryResultHKT, Record<string,
|
|
82
|
-
: `type Db = BaseSQLiteDatabase<"sync" | "async", unknown
|
|
93
|
+
? `type Db = PgDatabase<PgQueryResultHKT, Record<string, unknown>>;`
|
|
94
|
+
: `type Db = BaseSQLiteDatabase<"sync" | "async", unknown, Record<string, unknown>>;`;
|
|
83
95
|
|
|
84
96
|
const literalImports = code`
|
|
85
97
|
${dbTypeImport}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// REFERENCE TEMPLATE — copy this into your repo (e.g. codegen/generators/routes-hono.ts) and own it.
|
|
2
|
+
// Then import it LOCALLY in metaobjects.config.ts:
|
|
3
|
+
// import { routesFileHono } from "./codegen/generators/routes-hono.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`.
|
|
9
|
+
// targets: Hono. The emitted file imports `mountCrudRoutes` from
|
|
10
|
+
// `@metaobjectsdev/runtime-ts/hono` and takes its persistence client as
|
|
11
|
+
// INJECTED DEPS (`register<Entity>Routes(app, { db })`) rather than a
|
|
12
|
+
// module-singleton import — which is what makes it portable to any host
|
|
13
|
+
// that can hand Hono a request. If your framework is not Hono, THIS is the
|
|
14
|
+
// file to retarget: swap the mount helper and the exported signature; the
|
|
15
|
+
// metadata walk above it is framework-neutral and stays as-is.
|
|
16
|
+
// use-when: you want generated Hono CRUD routes per entity.
|
|
17
|
+
// emits: <target>/<Entity>.routes.hono.ts — full CRUD for write-through entities,
|
|
18
|
+
// read-only (GET list + GET :id) for projections. Skipped for any sourceless
|
|
19
|
+
// object and for TPH subtypes.
|
|
20
|
+
// customize: this generator is YOURS — edit it freely. For the emitted route
|
|
21
|
+
// composition, call `renderRoutesFileHono` (exported from the engine) and
|
|
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).
|
|
27
|
+
// composes-with: entity.ts (imports the table/schemas/allowlists), queries.ts.
|
|
28
|
+
|
|
29
|
+
import { type MetaObject } from "@metaobjectsdev/metadata";
|
|
30
|
+
import {
|
|
31
|
+
perEntity,
|
|
32
|
+
type Generator,
|
|
33
|
+
type GeneratorFactory,
|
|
34
|
+
renderRoutesFileHono,
|
|
35
|
+
resolveExpose,
|
|
36
|
+
type ExposeOption,
|
|
37
|
+
isTphSubtype,
|
|
38
|
+
hasAnyRdbSource,
|
|
39
|
+
formatTs,
|
|
40
|
+
entityOutputPath,
|
|
41
|
+
} from "@metaobjectsdev/codegen-ts";
|
|
42
|
+
|
|
43
|
+
export interface RoutesFileHonoOpts {
|
|
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;
|
|
55
|
+
target?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const routesFileHono = function routesFileHono(opts?: RoutesFileHonoOpts): Generator {
|
|
59
|
+
const userFilter = opts?.filter ?? (() => true);
|
|
60
|
+
// Eligibility, minus the TPH question — stated ONCE so the emit set and the warn set
|
|
61
|
+
// below cannot drift apart. They differ only by `!isTphSubtype` vs `isTphSubtype`, and
|
|
62
|
+
// written out twice a later edit to one silently makes an entity either stop emitting
|
|
63
|
+
// without being named as held back, or get warned about while still emitting.
|
|
64
|
+
// (Same shape as tanstack's grid generator, which factors it the same way.)
|
|
65
|
+
const passesOtherGates = (e: MetaObject): boolean =>
|
|
66
|
+
hasAnyRdbSource(e) && userFilter(e);
|
|
67
|
+
const generator: Generator = {
|
|
68
|
+
name: "routes-file-hono",
|
|
69
|
+
// Marks this as the Hono routes generator so the runner can aggregate
|
|
70
|
+
// `ctx.config.includeHonoRoutes` and api-docs auto-documents the Hono surface.
|
|
71
|
+
emitsHonoRoutes: true,
|
|
72
|
+
// TPH subtypes are EXCLUDED, matching the Fastify generator. A TPH subtype shares
|
|
73
|
+
// its base's table, so mounting vanilla CRUD for it produced routes with no
|
|
74
|
+
// discriminator scoping at all: the list returned EVERY subtype's rows, and
|
|
75
|
+
// get/patch/delete by id happily operated on rows belonging to a different
|
|
76
|
+
// subtype. Silently wrong data, which is worse than no route. Fastify dispatches
|
|
77
|
+
// these to a discriminator-aware renderer; the Hono runtime has no discriminator
|
|
78
|
+
// support yet, so this fails CLOSED and the run says so (see the warning below)
|
|
79
|
+
// rather than shipping an artifact that returns the wrong rows.
|
|
80
|
+
filter: (e: MetaObject) => passesOtherGates(e) && !isTphSubtype(e),
|
|
81
|
+
generate: async (ctx) => {
|
|
82
|
+
// One note per run naming every TPH subtype held back, so the gap is visible at
|
|
83
|
+
// `meta gen` time rather than discovered as missing endpoints in production.
|
|
84
|
+
const skipped = ctx.entities.filter((e) => passesOtherGates(e) && isTphSubtype(e));
|
|
85
|
+
if (skipped.length > 0) {
|
|
86
|
+
ctx.warn(
|
|
87
|
+
`no Hono routes emitted for the TPH subtype(s) ${skipped.map((e) => e.name).join(", ")} — ` +
|
|
88
|
+
"the Hono adapter has no discriminator scoping yet, so per-subtype CRUD would " +
|
|
89
|
+
"return and mutate OTHER subtypes' rows. Use routesFile() (Fastify), which " +
|
|
90
|
+
"dispatches TPH correctly, or hand-write the scoped routes.",
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return emit(ctx);
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
const emit = perEntity(async (entity, ctx) => {
|
|
97
|
+
if (!ctx.renderContext) {
|
|
98
|
+
throw new Error("routes-file-hono: renderContext is required (provided by runGen)");
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
path: entityOutputPath(
|
|
102
|
+
ctx.config.outputLayout ?? "flat",
|
|
103
|
+
entity.package,
|
|
104
|
+
`${entity.name}.routes.hono.ts`,
|
|
105
|
+
),
|
|
106
|
+
content: await formatTs(renderRoutesFileHono(entity, ctx.renderContext, resolveExpose(entity, opts?.expose))),
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
if (opts?.target) {
|
|
110
|
+
generator.target = opts.target;
|
|
111
|
+
}
|
|
112
|
+
return generator;
|
|
113
|
+
} as GeneratorFactory<RoutesFileHonoOpts>;
|