Skip to content

Commit 5d12334

Browse files
committed
feat(ts): eject --undo, gen --strict, init --with-docs, entityTypeNames, api-docs follow the generator list (#490)
1 parent 9195d55 commit 5d12334

48 files changed

Lines changed: 1176 additions & 94 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎docs/ports/typescript.md‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,19 @@ model walks, the naming and type-mapping tables), which the copies import from
120120
`meta eject docs-site` copies instead
121121
([what the copy holds](../features/own-your-codegen.md#what-the-copy-holds)).
122122

123+
A copy that never changed is a cost: it stays at the release it was copied from and misses
124+
every later fix. `meta gen` prints one line naming the owned copies that are still
125+
the shipped reference, unchanged, and `meta eject --undo` takes them back: with no names it
126+
removes every such copy (and the template modules only it used), and with names it removes
127+
those, refusing the whole call, and removing nothing, if a named copy differs (`--force`
128+
removes it anyway). It never edits `metaobjects.config.ts`; it prints the import to swap
129+
back to the package's. There is no `meta doctor`: `meta eject --list` is the report.
130+
A copy you changed stays yours.
131+
132+
`meta gen` loads leniently, so an attribute the registry does not know (a typo such as
133+
`@sensitve`) is a warning and the run goes on. `meta gen --strict` makes it an error, as
134+
`meta verify` does: it exits 1 and writes nothing.
135+
123136
Documentation is generated the same way, from the same list: there is no docs command
124137
(ADR-0034 Amendment 9). `docsFile()`, `apiDocsFile()`, `requirementsFile()`,
125138
`agentDocsFile()`, `metamodelDocsFile()` and `docsSiteFile()` write to the target named
@@ -473,6 +486,33 @@ operands by those options. `meta migrate` warns for a decimal declared with `@pr
473486
on SQLite or D1. `meta verify --codegen` names the `wire` block that reproduces committed output
474487
generated under another setting.
475488

489+
### Types exported per entity
490+
491+
Code that talks to a generated entity imports these names, so they are a contract. Each
492+
entity module (`<Entity>.ts`) exports six types. `entityTypeNames(obj)`, exported from
493+
`@metaobjectsdev/codegen-ts`, returns the same six spellings for an owned generator, so it
494+
need not rebuild them from the entity name.
495+
496+
| Type | Derived from | Use it for |
497+
| --- | --- | --- |
498+
| `<Entity>` | the Drizzle table, `InferSelectModel` | a row read back from the database: what `find<Entity>ById` and `create<Entity>` return |
499+
| `<Entity>Insert` | the Drizzle table, `InferInsertModel` | a row as the database accepts it, before validation |
500+
| `<Entity>Update` | `Partial<<Entity>Insert>` | the same, every column optional |
501+
| `<Entity>Create` | `z.input` of `<Entity>InsertSchema` | **the argument of `create<Entity>`**, and what a client posts |
502+
| `<Entity>Patch` | `z.input` of `<Entity>UpdateSchema` | **the argument of `update<Entity>`**, and what a client patches |
503+
| `<Entity>Filter` | the `@filterable` and `@sortable` fields | the `filter`, `sort`, `limit` and `offset` options of `list<Entity>` and the generated hooks |
504+
505+
`Create` and `Patch` are the pair to use at a call site. They come from the Zod schemas the
506+
functions validate with, so a renamed or dropped field is a compile error at every call.
507+
`Insert` and `Update` come from the table and describe what the database takes. They are
508+
looser than the validators, so prefer `Create` and `Patch` at the API boundary.
509+
510+
When the entity is a TPH discriminator base, the row type is `<Entity>Row`, because
511+
`<Entity>` is the discriminated union over its subtypes. A read-only projection has no
512+
table to insert into, so do not expect `Create` or `Patch` there; read its generated module
513+
for what it exports. `test/entity-type-names.test.ts` in `codegen-ts` checks the table
514+
against the golden `Subscriber` module.
515+
476516
### `<Entity>Names` — the physical names, as constants
477517

478518
`meta init` scaffolds a `names` generator, so a new project gets `<Entity>.names.ts`

‎examples/advanced-modeling/codegen/generators/queries.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ export const queriesFile = function queriesFile(opts?: QueriesFileOpts): Generat
160160

161161
const generator: Generator = {
162162
name: "queries-file",
163+
// Read by the api reference (`apiDocsFile()`): it documents the query helpers only when
164+
// a generator in the run writes them.
165+
emitsQueries: true,
163166
filter,
164167
generate: perEntity(async (entity, ctx) => {
165168
if (!ctx.renderContext) {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// The six type names the entity module exports for an entity. A client written against the
2+
// generated entity (a typed API client, a form, a service layer) imports these by name, so
3+
// the spelling is part of the generator's contract, not an accident of three templates.
4+
// `entityTypeNames` is the one place that spelling lives: the templates that emit the names
5+
// call it, and an owned generator that refers to them calls it too, so a rename cannot
6+
// leave one side behind. `docs/ports/typescript.md` ("Types exported per entity") is the
7+
// reader-facing table; test/entity-type-names.test.ts pins it to the golden output.
8+
9+
import type { MetaData } from "@metaobjectsdev/metadata";
10+
11+
export interface EntityTypeNames {
12+
/** The row type read back from the table: `InferSelectModel` of the Drizzle table. */
13+
row: string;
14+
/** What an insert accepts: `InferInsertModel` of the Drizzle table. */
15+
insert: string;
16+
/** `Partial<Insert>`. */
17+
update: string;
18+
/** The Zod insert schema's INPUT type, the argument of `create<Entity>`. */
19+
create: string;
20+
/** The Zod update schema's INPUT type: every settable field optional, the argument of `update<Entity>`. */
21+
patch: string;
22+
/** The client filter object: filterable fields, `sort`, `limit`, `offset`, `or`, `and`. */
23+
filter: string;
24+
}
25+
26+
export interface EntityTypeNamesOpts {
27+
/**
28+
* The row type is `<Entity>Row` instead of `<Entity>` when the entity is a TPH
29+
* discriminator base, because its discriminated-union type owns the bare name.
30+
*/
31+
tphBase?: boolean;
32+
/** Use this spelling instead of `obj.name` (a value object renamed on a name collision). */
33+
name?: string;
34+
}
35+
36+
export function entityTypeNames(obj: MetaData, opts: EntityTypeNamesOpts = {}): EntityTypeNames {
37+
const base = opts.name ?? obj.name;
38+
return {
39+
row: opts.tphBase ? `${base}Row` : base,
40+
insert: `${base}Insert`,
41+
update: `${base}Update`,
42+
create: `${base}Create`,
43+
patch: `${base}Patch`,
44+
filter: `${base}Filter`,
45+
};
46+
}

‎examples/advanced-modeling/codegen/templates/filter-type.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
opsForField,
1717
} from "@metaobjectsdev/metadata";
1818
import { isSortableField } from "./filter-shared.js";
19+
import { entityTypeNames } from "./entity-type-names.js";
1920
import { enumValues } from "@metaobjectsdev/codegen-ts/engine";
2021

2122
// currency is integer minor units (allowlist `integer: true`, row schema `z.number().int()`).
@@ -79,6 +80,7 @@ function renderFieldUnion(field: MetaField): string {
7980
* the server's per-subtype allowlist.
8081
*/
8182
export function renderFilterType(entity: MetaObject, exclude?: string): Code {
83+
const filterName = entityTypeNames(entity).filter;
8284
// fields() returns effective fields, so inherited fields (from extends:/super:) are included in filter types.
8385
const allFields = entity.fields().filter((c) => c.name !== exclude);
8486
const filterableFieldsList = allFields.filter((c) => c.attr(FIELD_ATTR_FILTERABLE) === true);
@@ -93,12 +95,12 @@ export function renderFilterType(entity: MetaObject, exclude?: string): Code {
9395

9496
if (fieldLines.length === 0 && sortFieldNames.length === 0) {
9597
return code`
96-
export type ${entity.name}Filter = {
98+
export type ${filterName} = {
9799
limit?: number;
98100
offset?: number;
99101
sort?: string;
100-
or?: ${entity.name}Filter[];
101-
and?: ${entity.name}Filter[];
102+
or?: ${filterName}[];
103+
and?: ${filterName}[];
102104
};
103105
`;
104106
}
@@ -112,13 +114,13 @@ export type ${entity.name}Filter = {
112114
})();
113115

114116
return code`
115-
export type ${entity.name}Filter = {
117+
export type ${filterName} = {
116118
limit?: number;
117119
offset?: number;
118120
sort?: ${sortType};
119121
${fieldLines.join("\n")}
120-
or?: ${entity.name}Filter[];
121-
and?: ${entity.name}Filter[];
122+
or?: ${filterName}[];
123+
and?: ${filterName}[];
122124
};
123125
`;
124126
}

‎examples/advanced-modeling/codegen/templates/inferred-types.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { sharedEnumForField } from "@metaobjectsdev/codegen-ts/engine";
4444
import { sharedEnumImportSpecifier, providedEnumImportSpecifier } from "@metaobjectsdev/codegen-ts/engine";
4545
import type { RenderContext } from "@metaobjectsdev/codegen-ts/engine";
4646
import { effectivePackage } from "@metaobjectsdev/codegen-ts/engine";
47+
import { entityTypeNames } from "./entity-type-names.js";
4748

4849
/**
4950
* Emit Drizzle's InferSelectModel / InferInsertModel aliases for an entity.
@@ -71,7 +72,7 @@ export function renderInferredTypes(
7172
const insertSym = imp("t:InferInsertModel@drizzle-orm");
7273
const docs = renderDocsFor(entity);
7374
const docsPrefix = docs ? `${docs}\n` : "";
74-
const rowName = tphBase ? `${entity.name}Row` : entity.name;
75+
const { row: rowName, insert: insertName, update: updateName } = entityTypeNames(entity, { tphBase });
7576
// #214 — a write-through entity read-view routes READS to the replica view, so the
7677
// read row type <Entity> is emitted from the VIEW's read schema (z.infer, carrying
7778
// the derived fields) by the entity-file composer, NOT here. `skipRow` suppresses
@@ -80,14 +81,14 @@ export function renderInferredTypes(
8081
// inferred from the write TABLE (derived-free, #213).
8182
if (skipRow) {
8283
return code`
83-
export type ${entity.name}Insert = ${insertSym}<typeof ${varName}>;
84-
export type ${entity.name}Update = Partial<${entity.name}Insert>;
84+
export type ${insertName} = ${insertSym}<typeof ${varName}>;
85+
export type ${updateName} = Partial<${insertName}>;
8586
`;
8687
}
8788
return code`
8889
${docsPrefix}export type ${rowName} = ${selectSym}<typeof ${varName}>;
89-
export type ${entity.name}Insert = ${insertSym}<typeof ${varName}>;
90-
export type ${entity.name}Update = Partial<${entity.name}Insert>;
90+
export type ${insertName} = ${insertSym}<typeof ${varName}>;
91+
export type ${updateName} = Partial<${insertName}>;
9192
`;
9293
}
9394

‎examples/advanced-modeling/codegen/templates/zod-validators.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
// object-array field collapsed to z.array(z.string()) and the JSON Schema sent
1010
// downstream (e.g. to LLM tool_use input_schema) lost the nested object shape.
1111

12+
import { entityTypeNames } from "./entity-type-names.js";
1213
import { code, joinCode, imp, type Code } from "@metaobjectsdev/codegen-ts/engine";
1314
import { MetaObject, MetaField, type MetaRoot, stripPackage } from "@metaobjectsdev/metadata";
1415
import {
@@ -529,6 +530,7 @@ export function renderZodValidators(obj: MetaObject, ctx?: RenderContext): Code
529530
const insertSchemaName = `${objName}InsertSchema`;
530531
const updateSchemaName = `${objName}UpdateSchema`;
531532
const preservingSchemaName = `${objName}InsertPreservingSchema`;
533+
const { create: createName, patch: patchName } = entityTypeNames(obj, { name: objName });
532534

533535
const docs = renderDocsFor(obj);
534536
const docsPrefix = docs ? `${docs}\n` : "";
@@ -561,11 +563,11 @@ ${joinCode(updateFieldLines, { on: ",\n" })}
561563
/** Typed create shape for ${objName}: the insert schema's INPUT (pre-transform) type. A
562564
* renamed/dropped/misspelt field is a compile error at every \`create${objName}\` call site;
563565
* the schema still validates at runtime. */
564-
export type ${objName}Create = ${z}.input<typeof ${insertSchemaName}>;
566+
export type ${createName} = ${z}.input<typeof ${insertSchemaName}>;
565567
566568
/** Typed patch shape for ${objName}: every settable field, optional (FR-035 PATCH). A
567569
* renamed/dropped field is a compile error at every \`update${objName}\` call site. */
568-
export type ${objName}Patch = ${z}.input<typeof ${updateSchemaName}>;${preservingBlock}
570+
export type ${patchName} = ${z}.input<typeof ${updateSchemaName}>;${preservingBlock}
569571
`;
570572
}
571573

‎examples/showcase/codegen/generators/queries.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,9 @@ export const queriesFile = function queriesFile(opts?: QueriesFileOpts): Generat
160160

161161
const generator: Generator = {
162162
name: "queries-file",
163+
// Read by the api reference (`apiDocsFile()`): it documents the query helpers only when
164+
// a generator in the run writes them.
165+
emitsQueries: true,
163166
filter,
164167
generate: perEntity(async (entity, ctx) => {
165168
if (!ctx.renderContext) {
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// The six type names the entity module exports for an entity. A client written against the
2+
// generated entity (a typed API client, a form, a service layer) imports these by name, so
3+
// the spelling is part of the generator's contract, not an accident of three templates.
4+
// `entityTypeNames` is the one place that spelling lives: the templates that emit the names
5+
// call it, and an owned generator that refers to them calls it too, so a rename cannot
6+
// leave one side behind. `docs/ports/typescript.md` ("Types exported per entity") is the
7+
// reader-facing table; test/entity-type-names.test.ts pins it to the golden output.
8+
9+
import type { MetaData } from "@metaobjectsdev/metadata";
10+
11+
export interface EntityTypeNames {
12+
/** The row type read back from the table: `InferSelectModel` of the Drizzle table. */
13+
row: string;
14+
/** What an insert accepts: `InferInsertModel` of the Drizzle table. */
15+
insert: string;
16+
/** `Partial<Insert>`. */
17+
update: string;
18+
/** The Zod insert schema's INPUT type, the argument of `create<Entity>`. */
19+
create: string;
20+
/** The Zod update schema's INPUT type: every settable field optional, the argument of `update<Entity>`. */
21+
patch: string;
22+
/** The client filter object: filterable fields, `sort`, `limit`, `offset`, `or`, `and`. */
23+
filter: string;
24+
}
25+
26+
export interface EntityTypeNamesOpts {
27+
/**
28+
* The row type is `<Entity>Row` instead of `<Entity>` when the entity is a TPH
29+
* discriminator base, because its discriminated-union type owns the bare name.
30+
*/
31+
tphBase?: boolean;
32+
/** Use this spelling instead of `obj.name` (a value object renamed on a name collision). */
33+
name?: string;
34+
}
35+
36+
export function entityTypeNames(obj: MetaData, opts: EntityTypeNamesOpts = {}): EntityTypeNames {
37+
const base = opts.name ?? obj.name;
38+
return {
39+
row: opts.tphBase ? `${base}Row` : base,
40+
insert: `${base}Insert`,
41+
update: `${base}Update`,
42+
create: `${base}Create`,
43+
patch: `${base}Patch`,
44+
filter: `${base}Filter`,
45+
};
46+
}

‎examples/showcase/codegen/templates/filter-type.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
opsForField,
1717
} from "@metaobjectsdev/metadata";
1818
import { isSortableField } from "./filter-shared.js";
19+
import { entityTypeNames } from "./entity-type-names.js";
1920
import { enumValues } from "@metaobjectsdev/codegen-ts/engine";
2021

2122
// currency is integer minor units (allowlist `integer: true`, row schema `z.number().int()`).
@@ -79,6 +80,7 @@ function renderFieldUnion(field: MetaField): string {
7980
* the server's per-subtype allowlist.
8081
*/
8182
export function renderFilterType(entity: MetaObject, exclude?: string): Code {
83+
const filterName = entityTypeNames(entity).filter;
8284
// fields() returns effective fields, so inherited fields (from extends:/super:) are included in filter types.
8385
const allFields = entity.fields().filter((c) => c.name !== exclude);
8486
const filterableFieldsList = allFields.filter((c) => c.attr(FIELD_ATTR_FILTERABLE) === true);
@@ -93,12 +95,12 @@ export function renderFilterType(entity: MetaObject, exclude?: string): Code {
9395

9496
if (fieldLines.length === 0 && sortFieldNames.length === 0) {
9597
return code`
96-
export type ${entity.name}Filter = {
98+
export type ${filterName} = {
9799
limit?: number;
98100
offset?: number;
99101
sort?: string;
100-
or?: ${entity.name}Filter[];
101-
and?: ${entity.name}Filter[];
102+
or?: ${filterName}[];
103+
and?: ${filterName}[];
102104
};
103105
`;
104106
}
@@ -112,13 +114,13 @@ export type ${entity.name}Filter = {
112114
})();
113115

114116
return code`
115-
export type ${entity.name}Filter = {
117+
export type ${filterName} = {
116118
limit?: number;
117119
offset?: number;
118120
sort?: ${sortType};
119121
${fieldLines.join("\n")}
120-
or?: ${entity.name}Filter[];
121-
and?: ${entity.name}Filter[];
122+
or?: ${filterName}[];
123+
and?: ${filterName}[];
122124
};
123125
`;
124126
}

‎examples/showcase/codegen/templates/inferred-types.ts‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { sharedEnumForField } from "@metaobjectsdev/codegen-ts/engine";
4444
import { sharedEnumImportSpecifier, providedEnumImportSpecifier } from "@metaobjectsdev/codegen-ts/engine";
4545
import type { RenderContext } from "@metaobjectsdev/codegen-ts/engine";
4646
import { effectivePackage } from "@metaobjectsdev/codegen-ts/engine";
47+
import { entityTypeNames } from "./entity-type-names.js";
4748

4849
/**
4950
* Emit Drizzle's InferSelectModel / InferInsertModel aliases for an entity.
@@ -71,7 +72,7 @@ export function renderInferredTypes(
7172
const insertSym = imp("t:InferInsertModel@drizzle-orm");
7273
const docs = renderDocsFor(entity);
7374
const docsPrefix = docs ? `${docs}\n` : "";
74-
const rowName = tphBase ? `${entity.name}Row` : entity.name;
75+
const { row: rowName, insert: insertName, update: updateName } = entityTypeNames(entity, { tphBase });
7576
// #214 — a write-through entity read-view routes READS to the replica view, so the
7677
// read row type <Entity> is emitted from the VIEW's read schema (z.infer, carrying
7778
// the derived fields) by the entity-file composer, NOT here. `skipRow` suppresses
@@ -80,14 +81,14 @@ export function renderInferredTypes(
8081
// inferred from the write TABLE (derived-free, #213).
8182
if (skipRow) {
8283
return code`
83-
export type ${entity.name}Insert = ${insertSym}<typeof ${varName}>;
84-
export type ${entity.name}Update = Partial<${entity.name}Insert>;
84+
export type ${insertName} = ${insertSym}<typeof ${varName}>;
85+
export type ${updateName} = Partial<${insertName}>;
8586
`;
8687
}
8788
return code`
8889
${docsPrefix}export type ${rowName} = ${selectSym}<typeof ${varName}>;
89-
export type ${entity.name}Insert = ${insertSym}<typeof ${varName}>;
90-
export type ${entity.name}Update = Partial<${entity.name}Insert>;
90+
export type ${insertName} = ${insertSym}<typeof ${varName}>;
91+
export type ${updateName} = Partial<${insertName}>;
9192
`;
9293
}
9394

0 commit comments

Comments
 (0)