Skip to content

Commit f953dba

Browse files
committed
Codegen: splice only @generated interior; TableClass in table.ts
Update mode no longer rebuilds imports or the class shell — only the span between @generated-start/end is rewritten (@expose state preserved). Move TableClass to table.ts and use it on from/insert/scope; drop unused RelationQuery from relation.ts.
1 parent 2d9ff50 commit f953dba

4 files changed

Lines changed: 49 additions & 65 deletions

File tree

src/relation.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,9 @@
1616
// Step 1: thread parent context unchanged (incl. undefined → same as
1717
// `scope(undefined)` today). Attenuation / mapContext is a later step.
1818

19-
import type { TableBase } from "./table";
19+
import type { TableBase, TableClass } from "./table";
2020
import type { SqlValue } from "./types/sql-value";
21-
import type { QueryBuilder, RowType } from "./builder/query";
22-
23-
// Concrete table class: statics from TableBase + constructible instance.
24-
type TableClass = typeof TableBase & (new () => TableBase);
21+
import type { QueryBuilder } from "./builder/query";
2522

2623
// Column keys on a table instance — only fields that hold SqlValue
2724
// (methods / constructor / non-column fields are excluded).
@@ -111,7 +108,3 @@ export const Relation = {
111108
return relate(row, Target, fk as { [k: string]: SqlValue<any> }, card);
112109
},
113110
};
114-
115-
// Re-export the shape name so callers can annotate without importing QueryBuilder guts.
116-
export type RelationQuery<T extends TableClass, Card extends "one" | "maybe" | "many"> =
117-
QueryBuilder<{ [K in T["tsAlias"]]: InstanceType<T> }, InstanceType<T> & RowType, [], Card>;

src/table.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ export abstract class TableBase {
7777
return this.database.scopedIdent(name);
7878
}
7979

80-
// Entry point for query builders: e.g., `Users.from()`
81-
static from<T extends typeof TableBase & (new () => TableBase)>(this: T) {
80+
// Entry point for query builders: e.g., `Users.from()`
81+
static from<T extends TableClass>(this: T) {
8282
return new QueryBuilder<
8383
{ [K in T["tsAlias"]]: InstanceType<T> },
8484
InstanceType<T>,
@@ -90,7 +90,7 @@ export abstract class TableBase {
9090
});
9191
}
9292

93-
static insert<T extends typeof TableBase & (new () => TableBase)>(
93+
static insert<T extends TableClass>(
9494
this: T,
9595
...rows: [InsertRow<InstanceType<T>>, ...InsertRow<InstanceType<T>>[]]
9696
): InsertBuilder<T["tableName"], InstanceType<T>> {
@@ -102,13 +102,13 @@ export abstract class TableBase {
102102
});
103103
}
104104

105-
static update<T extends typeof TableBase & (new () => TableBase)>(
105+
static update<T extends TableClass>(
106106
this: T,
107107
): UpdateBuilder<T["tableName"], InstanceType<T>> {
108108
return new UpdateBuilder({ instance: this.rowType() });
109109
}
110110

111-
static delete<T extends typeof TableBase & (new () => TableBase)>(
111+
static delete<T extends TableClass>(
112112
this: T,
113113
): DeleteBuilder<T["tableName"], InstanceType<T>> {
114114
return new DeleteBuilder({ instance: this.rowType() });
@@ -134,7 +134,7 @@ export abstract class TableBase {
134134
// `Database<C>.Table`). Tables declared with the default `C =
135135
// undefined` accept anything via the `unknown` widening; tables that
136136
// pin a `C` reject mismatched scopes at compile time.
137-
static scope<T extends typeof TableBase & (new () => TableBase)>(
137+
static scope<T extends TableClass>(
138138
this: T,
139139
ctx: T["context"] | undefined,
140140
) {
@@ -194,6 +194,10 @@ export const Table = <Name extends string, C = undefined>(
194194
return obj[name] as NonNullable<Obj[Name]>;
195195
};
196196

197-
export const isTableClass = (x: unknown): x is typeof TableBase => {
197+
// Concrete table class: TableBase statics + constructible row instance.
198+
// Used by Relation helpers, from/insert/scope, and Fromable checks.
199+
export type TableClass = typeof TableBase & (new () => TableBase);
200+
201+
export const isTableClass = (x: unknown): x is TableClass => {
198202
return typeof x === "function" && x.prototype instanceof TableBase;
199203
};

src/tables/generate.test.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,7 @@ export class Dogs extends db.Table("dogs") {
150150
await validate(out);
151151
expect(out).toMatchInlineSnapshot(`
152152
"import { db } from "../db";
153-
import { Relation, expose } from "typegres";
154-
import { Int8, Text } from "typegres/postgres";
155-
import { Teams } from "./teams";
153+
import { Int8, Text } from "typegres";
156154
157155
export class Dogs extends db.Table("dogs") {
158156
// @generated-start
@@ -184,9 +182,8 @@ export class Dogs extends db.Table("dogs") {
184182
await validate(out);
185183
expect(out).toMatchInlineSnapshot(`
186184
"import { db } from "../db";
187-
import { Relation, expose } from "typegres";
188-
import { Int8, Text } from "typegres/postgres";
189-
import { Teams } from "./teams";
185+
import { Int8, Text } from "typegres";
186+
import { expose } from "typegres";
190187
191188
export class Dogs extends db.Table("dogs") {
192189
// @generated-start
@@ -217,9 +214,7 @@ export class Dogs extends db.Table("dogs") {
217214
await validate(out);
218215
expect(out).toMatchInlineSnapshot(`
219216
"import { db } from "../db";
220-
import { Relation, expose } from "typegres";
221-
import { Int8, Text } from "typegres/postgres";
222-
import { Teams } from "./teams";
217+
import { Int8, Text, expose } from "typegres";
223218
224219
export class Dogs extends db.Table("dogs") {
225220
// @generated-start
@@ -254,7 +249,6 @@ export class Dogs extends db.Table("dogs") {
254249
expect(out).toMatchInlineSnapshot(`
255250
"import { db } from "../db";
256251
import { expose } from "typegres";
257-
import { Int8, Text } from "typegres/postgres";
258252
259253
export class Dogs extends db.Table("dogs") {
260254
// @generated-start
@@ -285,8 +279,7 @@ export class Dogs extends db.Table("dogs") {
285279
await validate(out);
286280
expect(out).toMatchInlineSnapshot(`
287281
"import { db } from "../db";
288-
import { expose } from "typegres";
289-
import { Int8, Text } from "typegres/postgres";
282+
import { Int8, Text, expose } from "typegres";
290283
291284
export class Dogs extends db.Table("dogs") {
292285
// @generated-start
@@ -298,9 +291,9 @@ export class Dogs extends db.Table("dogs") {
298291
`);
299292
});
300293

301-
test("update mode rewrites managed imports; preserves post-block user methods", async () => {
294+
test("update mode preserves header and post-block methods; only the block changes", async () => {
302295
// Stale imports + a hand-written method after the generated block.
303-
// Regen rebuilds imports from schema and keeps the user method.
296+
// Header (including comments) is byte-preserved; only the generated interior is rewritten.
304297
const existing = `import { db } from "../db";
305298
import { Int8 } from "typegres";
306299
// my custom comment (header comments are not preserved)
@@ -321,8 +314,8 @@ export class Dogs extends db.Table("dogs") {
321314
await validate(out);
322315
expect(out).toMatchInlineSnapshot(`
323316
"import { db } from "../db";
324-
import { expose } from "typegres";
325-
import { Int8 } from "typegres/postgres";
317+
import { Int8 } from "typegres";
318+
// my custom comment (header comments are not preserved)
326319
327320
export class Dogs extends db.Table("dogs") {
328321
// @generated-start

src/tables/generate.ts

Lines changed: 27 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -223,34 +223,29 @@ const parseExistingDecorations = (
223223
const START_MARKER = "// @generated-start";
224224
const END_MARKER = "// @generated-end";
225225

226-
// Pure generation entry point — no DB, no fs. `existing` (if provided)
227-
// must contain @generated-start/@generated-end markers; only content
228-
// between them is replaced, and per-entry `@expose()` state is preserved.
229-
// Without `existing`, returns a brand-new full file.
226+
// Pure generation entry point — no DB, no fs.
227+
//
228+
// New file: full skeleton (managed imports + class shell + generated body).
229+
// Update (`existing`): **only** the interior of @generated-start/end is
230+
// rewritten (plus per-entry `@expose()` preservation). Header imports,
231+
// class declaration, and everything after @generated-end are left alone.
232+
// Callers are responsible for imports when the schema gains types/relations
233+
// (tsc will fail until they add them — better than clobbering the header).
230234
export const generateTable = (
231235
tableName: string,
232236
columns: ColumnInfo[],
233237
relations: Relation[],
234238
opts: { typeImportPath: string; dbImport: string; existing?: string },
235239
): string => {
236240
if (opts.existing !== undefined) {
237-
return updateFile(
238-
opts.existing,
239-
tableName,
240-
columns,
241-
relations,
242-
opts.typeImportPath,
243-
opts.dbImport,
244-
);
241+
return updateBlock(opts.existing, columns, relations);
245242
}
246-
return emitFile(
243+
return emitNewFile(
247244
tableName,
248245
columns,
249246
relations,
250247
opts.typeImportPath,
251248
opts.dbImport,
252-
bodyLines(columns, relations, () => true),
253-
`${END_MARKER}\n}\n`,
254249
);
255250
};
256251

@@ -296,41 +291,37 @@ const bodyLines = (
296291
: colLines;
297292
};
298293

299-
// Assemble a full table file. `tail` is END_MARKER through EOF — for new
300-
// files that's just the end marker + class close; for updates it's the
301-
// preserved user methods after the generated block.
302-
const emitFile = (
294+
// Brand-new table file: managed imports + class shell + empty user tail.
295+
const emitNewFile = (
303296
tableName: string,
304297
columns: ColumnInfo[],
305298
relations: Relation[],
306299
typeImportPath: string,
307300
dbImport: string,
308-
body: string[],
309-
tail: string,
310301
): string => {
311302
const imports = buildImportLines(tableName, columns, relations, typeImportPath, dbImport);
303+
const body = bodyLines(columns, relations, () => true);
312304
return `${imports.join("\n")}
313305
314306
export class ${pgNameToClassName(tableName)} extends db.Table("${tableName}") {
315307
${START_MARKER}
316308
${body.join("\n")}
317-
${tail}`;
309+
${END_MARKER}
310+
}
311+
`;
318312
};
319313

320-
// Update mode: rebuild managed imports + generated block from schema;
321-
// preserve only the post-END_MARKER tail (user methods after the block).
322-
// Per-entry `@expose()` state inside the block is preserved.
323-
const updateFile = (
314+
// Update mode: splice only between the markers. Prefix (imports, comments,
315+
// class line) and suffix (END_MARKER through EOF, including user methods)
316+
// are byte-preserved. `@expose` on/off inside the prior block is preserved.
317+
const updateBlock = (
324318
existing: string,
325-
tableName: string,
326319
columns: ColumnInfo[],
327320
relations: Relation[],
328-
typeImportPath: string,
329-
dbImport: string,
330321
): string => {
331322
const startIdx = existing.indexOf(START_MARKER);
332323
const endIdx = existing.indexOf(END_MARKER);
333-
if (startIdx === -1 || endIdx === -1) {
324+
if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) {
334325
throw new Error("Missing @generated-start or @generated-end markers");
335326
}
336327

@@ -340,9 +331,12 @@ const updateFile = (
340331
const body = bodyLines(columns, relations, (name, kind) =>
341332
kind === "col" ? (prior.cols.get(name) ?? true) : (prior.rels.get(name) ?? true),
342333
);
343-
// From END_MARKER through EOF — keeps hand-written methods below the block.
344-
const tail = existing.slice(endIdx);
345-
return emitFile(tableName, columns, relations, typeImportPath, dbImport, body, tail);
334+
335+
const prefix = existing.slice(0, startIdx + START_MARKER.length);
336+
const suffix = existing.slice(endIdx); // begins with END_MARKER
337+
// Normalize interior to "\n" + indented lines + "\n " before end marker,
338+
// matching emitNewFile layout regardless of prior whitespace.
339+
return `${prefix}\n${body.join("\n")}\n ${suffix}`;
346340
};
347341

348342
// --- Main ---

0 commit comments

Comments
 (0)