Skip to content

Commit aacba4c

Browse files
committed
fix(codegen-ts): derived flattened parent produces no flat member keys
1 parent 5a2ff16 commit aacba4c

3 files changed

Lines changed: 54 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,12 @@ loading. A port that reports `1.2` does not know that vocabulary; upgrade every
333333

334334
### Fixed
335335

336+
- **TypeScript: a derived flattened `field.object` no longer produces flat member keys (#524
337+
follow-up).** A flattened field that carries an `origin.*` owns no column (the Drizzle table and
338+
`meta migrate` skip it), but the flat keys (`shipToStreet`) still appeared in the read, create and
339+
update schemas, the filter and sort allowlists, the OpenAPI bodies and the form, so a write dropped
340+
them silently. The flattened member expansion now skips a derived parent, so every generated
341+
surface agrees with the table.
336342
- **TypeScript: Drizzle now binds a `field.object` with `@storage: flattened` to its member columns
337343
(#524).** The generator used to emit one `jsonb` column named for the field, while `meta migrate`
338344
creates one column per member of the value object and none for the field, so every generated

‎server/typescript/packages/codegen-ts/src/flattened.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ export function isFlattenedObjectField(field: MetaField): boolean {
5151
/** The members of a flattened field, or undefined when it is not flattened (or names no value object). */
5252
export function flattenedMembersOf(field: MetaField, strategy: ColumnNamingStrategy): FlattenedMember[] | undefined {
5353
if (!isFlattenedObjectField(field)) return undefined;
54+
// A derived (origin-bearing) field is materialized on the read side and owns no column, so its
55+
// members are no columns either; drizzle-schema and migrate's expected schema skip it too.
56+
if (field.isDerived()) return undefined;
5457
const valueObject = refTarget(field, FIELD_ATTR_OBJECT_REF);
5558
if (valueObject === undefined || !isMetaObject(valueObject)) return undefined;
5659
// ADR-0039: effective fields — members inherited via extends are columns too (as migrate does).

‎server/typescript/packages/codegen-ts/test/flattened-storage-e2e.test.ts‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,51 @@ describe("flattened storage: a derived value-object member keeps its column", ()
196196
});
197197
});
198198

199+
describe("flattened storage: a derived flattened parent has no member keys or columns", () => {
200+
const DERIVED_PARENT = JSON.stringify({
201+
"metadata.root": { package: "acme", children: [
202+
{ "object.value": { name: "Address", children: [
203+
{ "field.string": { name: "street" } },
204+
{ "field.string": { name: "city", "@filterable": true } },
205+
] } },
206+
{ "object.entity": { name: "Owner", children: [
207+
{ "source.rdb": { "@table": "owners" } },
208+
{ "field.long": { name: "id" } },
209+
{ "field.object": { name: "home", "@objectRef": "Address", "@storage": "flattened" } },
210+
{ "identity.primary": { name: "pk", "@fields": "id", "@generation": "increment" } },
211+
] } },
212+
{ "object.entity": { name: "Parcel", children: [
213+
{ "source.rdb": { "@table": "parcels", "@role": "primary" } },
214+
{ "source.rdb": { "@kind": "view", "@view": "v_parcels", "@role": "replica" } },
215+
{ "field.long": { name: "id" } },
216+
{ "field.string": { name: "ref" } },
217+
{ "field.long": { name: "ownerId" } },
218+
{ "field.object": { name: "shipTo", "@objectRef": "Address", "@storage": "flattened", children: [
219+
{ "origin.passthrough": { "@from": "Owner.home", "@via": "Parcel.owner" } },
220+
] } },
221+
{ "identity.primary": { name: "pk", "@fields": "id", "@generation": "increment" } },
222+
{ "identity.reference": { name: "ownerRef", "@fields": "ownerId", "@references": "Owner" } },
223+
{ "relationship.association": { name: "owner", "@cardinality": "one", "@objectRef": "Owner" } },
224+
] } },
225+
] },
226+
});
227+
228+
test("no flat keys on the surface, in the schemas or allowlists, and no Drizzle member columns", async () => {
229+
const { root, errors } = await new MetaDataLoader({ strict: true }).load([new InMemoryStringSource(DERIVED_PARENT, { id: "dp.json" })]);
230+
expect(errors).toEqual([]);
231+
const parcel = root.findObject("Parcel")!;
232+
expect(flatSurfaceFields(parcel).map((f) => f.name)).toEqual(["id", "ref", "ownerId", "shipTo"]);
233+
234+
const outDir = await generate(root, "postgres", [entityFile(), queriesFile()], tmpdir());
235+
const entity = readFileSync(allTs(outDir).find((f) => f.endsWith("Parcel.ts"))!, "utf8");
236+
const table = entity.slice(entity.indexOf("Table(\"parcels\""), entity.indexOf("});", entity.indexOf("Table(\"parcels\"")));
237+
expect([...table.matchAll(/^\s+\w+: \w+\("([^"]+)"/gm)].map((m) => m[1]!).sort()).toEqual(["id", "owner_id", "ref"]);
238+
for (const file of allTs(outDir).filter((f) => /Parcel[.\w]*\.ts$/.test(f))) {
239+
expect(readFileSync(file, "utf8")).not.toMatch(/shipTo(Street|City)|ship_to_/);
240+
}
241+
});
242+
});
243+
199244
describe("flattened storage: agent/ui.md reports the flat keys with their flags", () => {
200245
test("a filterable member's row is marked filterable and sortable", async () => {
201246
const page = renderAgentUiPage(await load());

0 commit comments

Comments
 (0)