11// Queries file composer — composes all CRUD function renderers (from queries.ts) into
22// a complete <Entity>.queries.ts file with @generated header and correct imports.
33
4- import { code , joinCode , type Code } from "ts-poet" ;
5- import { MetaObject } from "@metaobjectsdev/metadata" ;
4+ import { code , joinCode , imp , type Code } from "ts-poet" ;
5+ import {
6+ MetaObject ,
7+ OBJECT_ATTR_DISCRIMINATOR ,
8+ OBJECT_ATTR_DISCRIMINATOR_VALUE ,
9+ } from "@metaobjectsdev/metadata" ;
610import { type RenderContext } from "../render-context.js" ;
711import { entityModuleSpecifier } from "../import-path.js" ;
812import {
@@ -11,11 +15,21 @@ import {
1115 renderCreateFn ,
1216 renderUpdateFn ,
1317 renderDeleteByIdFn ,
18+ getPkInfo ,
1419} from "./queries.js" ;
15- import { variableNameFromEntity } from "../naming.js" ;
20+ import { variableNameFromEntity , pluralize } from "../naming.js" ;
1621import { GENERATED_HEADER } from "../constants.js" ;
22+ import { isTphDiscriminatorBase , tphConcreteSubtypes } from "./tph-discriminator.js" ;
1723
1824export function renderQueriesFile ( obj : MetaObject , ctx : RenderContext ) : string {
25+ // FR-017 Tier 2 — a TPH discriminator base gets a polymorphic queries file:
26+ // base reads dispatch through parse<Base>, and per-subtype CRUD targets the
27+ // single base table scoped to the discriminator value. (Subtype entities are
28+ // filtered out of the queries generator entirely — their CRUD lives here.)
29+ if ( isTphDiscriminatorBase ( obj , ctx . loadedRoot ) ) {
30+ return renderTphQueriesFile ( obj , ctx ) ;
31+ }
32+
1933 const entityName = obj . name ;
2034 // Import the entity's own file. Same target → relative "./Entity"; cross
2135 // target → importBase-qualified package path.
@@ -68,3 +82,118 @@ import { ${varName}, type ${entityName}, ${entityName}InsertSchema } from ${JSON
6882 `// Customize via ${ entityName } .extra.ts in this directory (additional queries, custom logic).\n` ;
6983 return header + body ;
7084}
85+
86+ /**
87+ * FR-017 Tier 2 — the polymorphic + per-subtype queries file for a TPH base.
88+ *
89+ * Base reads (`find<Base>ById`, `list<BasePlural>`) project every row through
90+ * `parse<Base>` so they return the discriminated union. There is intentionally
91+ * NO `create<Base>` / `update<Base>` — you cannot instantiate an abstract base.
92+ *
93+ * Each concrete subtype gets list / findById (filtered to the discriminator
94+ * value, parsed with `<Sub>Schema`) plus create / updateById / deleteById, all
95+ * against the single base table. Creates inject the discriminator value;
96+ * updates strip it (a row's subtype is immutable).
97+ */
98+ function renderTphQueriesFile ( base : MetaObject , ctx : RenderContext ) : string {
99+ const baseName = base . name ;
100+ const tableVar = variableNameFromEntity ( baseName ) ;
101+ const discField = base . ownAttr ( OBJECT_ATTR_DISCRIMINATOR ) as string ;
102+ const { fieldName : pkField , tsType : pkType } = getPkInfo ( base , ctx ) ;
103+
104+ const baseFileSpec = entityModuleSpecifier (
105+ ctx . selfTarget , ctx . entityModuleTarget , base . package , baseName , ctx . extStyle ,
106+ ) ;
107+ const tableSym = imp ( `${ tableVar } @${ baseFileSpec } ` ) ;
108+ const baseTypeSym = imp ( `t:${ baseName } @${ baseFileSpec } ` ) ;
109+ const parseSym = imp ( `parse${ baseName } @${ baseFileSpec } ` ) ;
110+ const eqSym = imp ( "eq@drizzle-orm" ) ;
111+ const andSym = imp ( "and@drizzle-orm" ) ;
112+
113+ const dbTypeImport =
114+ ctx . dialect === "postgres"
115+ ? `import type { NodePgDatabase } from "drizzle-orm/node-postgres";`
116+ : `import type { BaseSQLiteDatabase } from "drizzle-orm/sqlite-core";` ;
117+ const dbTypeAlias =
118+ ctx . dialect === "postgres"
119+ ? `type Db = NodePgDatabase<Record<string, never>>;`
120+ : `type Db = BaseSQLiteDatabase<"async", Record<string, never>>;` ;
121+
122+ // --- Polymorphic base reads ---
123+ const polymorphic = code `
124+ export async function find${ baseName } ById(db: Db, ${ pkField } : ${ pkType } ): Promise<${ baseTypeSym } | null> {
125+ const [row] = await db.select().from(${ tableSym } ).where(${ eqSym } (${ tableSym } .${ pkField } , ${ pkField } )).limit(1);
126+ return row ? ${ parseSym } (row) : null;
127+ }
128+
129+ export async function list${ pluralize ( baseName ) } (db: Db, opts?: { limit?: number; offset?: number }): Promise<${ baseTypeSym } []> {
130+ let q = db.select().from(${ tableSym } ).$dynamic();
131+ if (opts?.limit !== undefined) q = q.limit(opts.limit);
132+ if (opts?.offset !== undefined) q = q.offset(opts.offset);
133+ const rows = await q;
134+ return rows.map((r) => ${ parseSym } (r));
135+ }
136+ ` ;
137+
138+ // --- Per-subtype CRUD against the single base table ---
139+ const subtypeSections : Code [ ] = [ ] ;
140+ for ( const sub of tphConcreteSubtypes ( base , ctx . loadedRoot ) ) {
141+ const value = sub . ownAttr ( OBJECT_ATTR_DISCRIMINATOR_VALUE ) as string ;
142+ const valueLit = JSON . stringify ( value ) ;
143+ const subFileSpec = entityModuleSpecifier (
144+ ctx . selfTarget , ctx . entityModuleTarget , sub . package , sub . name , ctx . extStyle ,
145+ ) ;
146+ const subTypeSym = imp ( `t:${ sub . name } @${ subFileSpec } ` ) ;
147+ const subSchemaSym = imp ( `${ sub . name } Schema@${ subFileSpec } ` ) ;
148+ const subInsertSym = imp ( `${ sub . name } InsertSchema@${ subFileSpec } ` ) ;
149+
150+ subtypeSections . push ( code `
151+ export async function list${ pluralize ( sub . name ) } (db: Db, opts?: { limit?: number; offset?: number }): Promise<${ subTypeSym } []> {
152+ let q = db.select().from(${ tableSym } ).where(${ eqSym } (${ tableSym } .${ discField } , ${ valueLit } )).$dynamic();
153+ if (opts?.limit !== undefined) q = q.limit(opts.limit);
154+ if (opts?.offset !== undefined) q = q.offset(opts.offset);
155+ const rows = await q;
156+ return rows.map((r) => ${ subSchemaSym } .parse(r));
157+ }
158+
159+ export async function find${ sub . name } ById(db: Db, ${ pkField } : ${ pkType } ): Promise<${ subTypeSym } | null> {
160+ const [row] = await db.select().from(${ tableSym } )
161+ .where(${ andSym } (${ eqSym } (${ tableSym } .${ pkField } , ${ pkField } ), ${ eqSym } (${ tableSym } .${ discField } , ${ valueLit } ))).limit(1);
162+ return row ? ${ subSchemaSym } .parse(row) : null;
163+ }
164+
165+ export async function create${ sub . name } (db: Db, data: unknown): Promise<${ subTypeSym } > {
166+ const validated = ${ subInsertSym } .parse(data);
167+ const [row] = await db.insert(${ tableSym } ).values({ ...validated, ${ discField } : ${ valueLit } }).returning();
168+ return ${ subSchemaSym } .parse(row!);
169+ }
170+
171+ export async function update${ sub . name } ById(db: Db, ${ pkField } : ${ pkType } , data: unknown): Promise<${ subTypeSym } | null> {
172+ const validated = ${ subInsertSym } .partial().parse(data) as Record<string, unknown>;
173+ // The discriminator is immutable — a ${ sub . name } can never become another subtype.
174+ const { [${ JSON . stringify ( discField ) } ]: _disc, ...safe } = validated;
175+ const [row] = await db.update(${ tableSym } ).set(safe)
176+ .where(${ andSym } (${ eqSym } (${ tableSym } .${ pkField } , ${ pkField } ), ${ eqSym } (${ tableSym } .${ discField } , ${ valueLit } ))).returning();
177+ return row ? ${ subSchemaSym } .parse(row) : null;
178+ }
179+
180+ export async function delete${ sub . name } ById(db: Db, ${ pkField } : ${ pkType } ): Promise<boolean> {
181+ const deleted = await db.delete(${ tableSym } )
182+ .where(${ andSym } (${ eqSym } (${ tableSym } .${ pkField } , ${ pkField } ), ${ eqSym } (${ tableSym } .${ discField } , ${ valueLit } ))).returning();
183+ return deleted.length > 0;
184+ }
185+ ` ) ;
186+ }
187+
188+ const literalImports = code `
189+ ${ dbTypeImport }
190+ ${ dbTypeAlias }
191+ ` ;
192+
193+ const body = joinCode ( [ literalImports , polymorphic , ...subtypeSections ] , { on : "\n" } ) . toString ( ) ;
194+ const header =
195+ `// ${ GENERATED_HEADER } — DO NOT EDIT.\n` +
196+ `// Source metadata: ${ baseName } (${ base . fqn ( ) } ) — TPH discriminator base\n` +
197+ `// Customize via ${ baseName } .extra.ts in this directory (additional queries, custom logic).\n` ;
198+ return header + body ;
199+ }
0 commit comments