Skip to content

Commit d62aab5

Browse files
committed
fix(metadata): ADR-0039 — resolving accessors are the default (TS reference)
extends is a super-REFERENCE, not a flatten: inherited attrs/children/isArray live on the parent, reachable only through the resolving accessors. Reading a field/node's effective property or member set through an own-only accessor silently dropped everything inherited via extends — corrupting codegen, runtime, effective serialization, and schema. Flip own→resolving everywhere except the sanctioned emit-declared-here cases (codegen subclass-emit ownFields, own-mode canonical serializer, overlay/super-resolution walks) and the deliberately own-only @dbColumnType (physical, never inherited). New: MetaData.resolvedIsArray() — the resolving accessor for the native isArray flag (isArray is a boolean property, not an attr, so it has no attrs()-based resolution path). All codegen/runtime/effective-serializer reads of a node's array-ness now route through it. Fixed helpers + sites: - meta-field.ts getters (objectRef/column/default/defaultValue/maxLength/ precision/scale/unique/isRequired) → attr() resolving. - meta-identity.ts + meta-relationship.ts getters → resolving. - naming.resolveColumnName, serializer-json effective isArray, object-serializer, validation-passes (field.object @objectref + field.map + flattened-array), migrate-ts expected-schema (@storage/@unique/@objectRef/@default/@autoSet/ @maxLength/@precision/@scale/@localTime/isArray; @dbColumnType kept own) + referential-actions.isRequired, runtime-ts object-manager PK lookup, type-coercer jsonb (@storage resolve, @dbColumnType own), extract-object (@required/@values/@enumAlias/@normalize/isArray), codegen-ts inferred-types/ zod-validators/payload/projection/output-parser/fr010/template-data/ docs-data-builder/column-mapper, codegen-ts-react form-file. Conformance fixture (fails on pre-fix code): abstract field.string(isArray+ @maxlength) / field.decimal(@precision/@scale) / field.object(@objectref+@storage +isArray) each extended by a concrete field, plus a BaseEntity(uuid PK+timestamp) extended by a concrete entity. The effective serializer + validation own-reads both broke pre-fix (ERR_OBJECT_FIELD_WITHOUT_OBJECT_REF + effective isArray mismatch). Plus a codegen gate asserting the Drizzle schema honors every inherited value. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GF9xLEQZaPus5Y6opk398n
1 parent f956dcb commit d62aab5

28 files changed

Lines changed: 666 additions & 105 deletions
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
{
2+
"metadata.root": {
3+
"package": "acme",
4+
"children": [
5+
{
6+
"field.string": {
7+
"name": "Tags",
8+
"package": "acme",
9+
"abstract": true,
10+
"isArray": true,
11+
"@maxLength": 40
12+
}
13+
},
14+
{
15+
"field.decimal": {
16+
"name": "Money",
17+
"package": "acme",
18+
"abstract": true,
19+
"@precision": 12,
20+
"@scale": 2
21+
}
22+
},
23+
{
24+
"object.value": {
25+
"name": "Address",
26+
"children": [
27+
{
28+
"field.string": {
29+
"name": "street"
30+
}
31+
},
32+
{
33+
"field.string": {
34+
"name": "city"
35+
}
36+
}
37+
]
38+
}
39+
},
40+
{
41+
"field.object": {
42+
"name": "AddressBag",
43+
"package": "acme",
44+
"abstract": true,
45+
"isArray": true,
46+
"@objectRef": "acme::Address",
47+
"@storage": "jsonb"
48+
}
49+
},
50+
{
51+
"object.entity": {
52+
"name": "BaseEntity",
53+
"abstract": true,
54+
"children": [
55+
{
56+
"field.uuid": {
57+
"name": "id"
58+
}
59+
},
60+
{
61+
"field.timestamp": {
62+
"name": "createdAt"
63+
}
64+
},
65+
{
66+
"identity.primary": {
67+
"name": "id",
68+
"@fields": [
69+
"id"
70+
],
71+
"@generation": "uuid"
72+
}
73+
}
74+
]
75+
}
76+
},
77+
{
78+
"object.entity": {
79+
"name": "Contact",
80+
"extends": "acme::BaseEntity",
81+
"children": [
82+
{
83+
"field.uuid": {
84+
"name": "id"
85+
}
86+
},
87+
{
88+
"field.timestamp": {
89+
"name": "createdAt"
90+
}
91+
},
92+
{
93+
"identity.primary": {
94+
"name": "id",
95+
"@fields": [
96+
"id"
97+
],
98+
"@generation": "uuid"
99+
}
100+
},
101+
{
102+
"source.rdb": {
103+
"@table": "contacts"
104+
}
105+
},
106+
{
107+
"field.string": {
108+
"name": "name",
109+
"@required": true
110+
}
111+
},
112+
{
113+
"field.string": {
114+
"name": "tags",
115+
"extends": "acme::Tags",
116+
"isArray": true,
117+
"@maxLength": 40
118+
}
119+
},
120+
{
121+
"field.decimal": {
122+
"name": "balance",
123+
"extends": "acme::Money",
124+
"@precision": 12,
125+
"@scale": 2
126+
}
127+
},
128+
{
129+
"field.object": {
130+
"name": "addresses",
131+
"extends": "acme::AddressBag",
132+
"isArray": true,
133+
"@objectRef": "acme::Address",
134+
"@storage": "jsonb"
135+
}
136+
}
137+
]
138+
}
139+
}
140+
]
141+
}
142+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
{
2+
"metadata.root": {
3+
"package": "acme",
4+
"children": [
5+
{
6+
"field.string": {
7+
"name": "Tags",
8+
"package": "acme",
9+
"abstract": true,
10+
"isArray": true,
11+
"@maxLength": 40
12+
}
13+
},
14+
{
15+
"field.decimal": {
16+
"name": "Money",
17+
"package": "acme",
18+
"abstract": true,
19+
"@precision": 12,
20+
"@scale": 2
21+
}
22+
},
23+
{
24+
"object.value": {
25+
"name": "Address",
26+
"children": [
27+
{
28+
"field.string": {
29+
"name": "street"
30+
}
31+
},
32+
{
33+
"field.string": {
34+
"name": "city"
35+
}
36+
}
37+
]
38+
}
39+
},
40+
{
41+
"field.object": {
42+
"name": "AddressBag",
43+
"package": "acme",
44+
"abstract": true,
45+
"isArray": true,
46+
"@objectRef": "acme::Address",
47+
"@storage": "jsonb"
48+
}
49+
},
50+
{
51+
"object.entity": {
52+
"name": "BaseEntity",
53+
"abstract": true,
54+
"children": [
55+
{
56+
"field.uuid": {
57+
"name": "id"
58+
}
59+
},
60+
{
61+
"field.timestamp": {
62+
"name": "createdAt"
63+
}
64+
},
65+
{
66+
"identity.primary": {
67+
"name": "id",
68+
"@fields": [
69+
"id"
70+
],
71+
"@generation": "uuid"
72+
}
73+
}
74+
]
75+
}
76+
},
77+
{
78+
"object.entity": {
79+
"name": "Contact",
80+
"extends": "acme::BaseEntity",
81+
"children": [
82+
{
83+
"source.rdb": {
84+
"@table": "contacts"
85+
}
86+
},
87+
{
88+
"field.string": {
89+
"name": "name",
90+
"@required": true
91+
}
92+
},
93+
{
94+
"field.string": {
95+
"name": "tags",
96+
"extends": "acme::Tags"
97+
}
98+
},
99+
{
100+
"field.decimal": {
101+
"name": "balance",
102+
"extends": "acme::Money"
103+
}
104+
},
105+
{
106+
"field.object": {
107+
"name": "addresses",
108+
"extends": "acme::AddressBag"
109+
}
110+
}
111+
]
112+
}
113+
}
114+
]
115+
}
116+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
{
2+
"metadata.root": {
3+
"package": "acme",
4+
"children": [
5+
{
6+
"field.string": {
7+
"name": "Tags",
8+
"abstract": true,
9+
"isArray": true,
10+
"@maxLength": 40
11+
}
12+
},
13+
{
14+
"field.decimal": {
15+
"name": "Money",
16+
"abstract": true,
17+
"@precision": 12,
18+
"@scale": 2
19+
}
20+
},
21+
{
22+
"object.value": {
23+
"name": "Address",
24+
"children": [
25+
{ "field.string": { "name": "street" } },
26+
{ "field.string": { "name": "city" } }
27+
]
28+
}
29+
},
30+
{
31+
"field.object": {
32+
"name": "AddressBag",
33+
"abstract": true,
34+
"isArray": true,
35+
"@objectRef": "acme::Address",
36+
"@storage": "jsonb"
37+
}
38+
},
39+
{
40+
"object.entity": {
41+
"name": "BaseEntity",
42+
"abstract": true,
43+
"children": [
44+
{
45+
"field.uuid": {
46+
"name": "id"
47+
}
48+
},
49+
{
50+
"field.timestamp": {
51+
"name": "createdAt"
52+
}
53+
},
54+
{
55+
"identity.primary": {
56+
"name": "id",
57+
"@fields": "id",
58+
"@generation": "uuid"
59+
}
60+
}
61+
]
62+
}
63+
},
64+
{
65+
"object.entity": {
66+
"name": "Contact",
67+
"extends": "acme::BaseEntity",
68+
"children": [
69+
{
70+
"source.rdb": {
71+
"@table": "contacts"
72+
}
73+
},
74+
{
75+
"field.string": {
76+
"name": "name",
77+
"@required": true
78+
}
79+
},
80+
{
81+
"field.string": {
82+
"name": "tags",
83+
"extends": "acme::Tags"
84+
}
85+
},
86+
{
87+
"field.decimal": {
88+
"name": "balance",
89+
"extends": "acme::Money"
90+
}
91+
},
92+
{
93+
"field.object": {
94+
"name": "addresses",
95+
"extends": "acme::AddressBag"
96+
}
97+
}
98+
]
99+
}
100+
}
101+
]
102+
}
103+
}

server/typescript/packages/codegen-ts-react/src/templates/form-file.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ function renderNestedField(
189189
const nextVisited = new Set(visited);
190190
nextVisited.add(voKey);
191191

192-
if (!field.isArray) {
192+
if (!field.resolvedIsArray()) {
193193
// Single nested value object → a fieldset recursing into its fields.
194194
const inner: string[] = [];
195195
const hooks: string[] = [];

server/typescript/packages/codegen-ts/src/column-mapper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ export function mapColumnType(
230230
const dbName = field.column ?? columnNameFromField(field.name, strategy);
231231
const importModule = dialect === "sqlite" ? "drizzle-orm/sqlite-core" : "drizzle-orm/pg-core";
232232
const subType = field.subType;
233-
const isArray = field.isArray;
233+
const isArray = field.resolvedIsArray();
234234

235235
let fnName: string;
236236
let fnOptions: Record<string, unknown> | undefined;

0 commit comments

Comments
 (0)