turbine-orm 0.27.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -13
- package/dist/cjs/cli/config.js +20 -3
- package/dist/cjs/cli/destructive.js +47 -31
- package/dist/cjs/cli/index.js +273 -71
- package/dist/cjs/cli/mcp.js +788 -0
- package/dist/cjs/cli/migrate.js +95 -20
- package/dist/cjs/cli/studio.js +3 -2
- package/dist/cjs/client.js +267 -34
- package/dist/cjs/dialect.js +2 -0
- package/dist/cjs/generate.js +171 -7
- package/dist/cjs/index.js +4 -1
- package/dist/cjs/introspect.js +177 -4
- package/dist/cjs/query/batched-loader.js +148 -0
- package/dist/cjs/query/builder.js +714 -133
- package/dist/cjs/schema-builder.js +59 -4
- package/dist/cjs/schema-sql.js +315 -6
- package/dist/cjs/seed.js +66 -0
- package/dist/cli/config.d.ts +9 -2
- package/dist/cli/config.js +19 -3
- package/dist/cli/destructive.js +47 -31
- package/dist/cli/index.d.ts +52 -1
- package/dist/cli/index.js +272 -74
- package/dist/cli/mcp.d.ts +17 -0
- package/dist/cli/mcp.js +781 -0
- package/dist/cli/migrate.d.ts +37 -0
- package/dist/cli/migrate.js +92 -20
- package/dist/cli/studio.d.ts +3 -2
- package/dist/cli/studio.js +3 -2
- package/dist/client.d.ts +136 -1
- package/dist/client.js +267 -34
- package/dist/dialect.d.ts +17 -0
- package/dist/dialect.js +2 -0
- package/dist/generate.d.ts +17 -0
- package/dist/generate.js +171 -10
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -0
- package/dist/introspect.d.ts +20 -1
- package/dist/introspect.js +175 -4
- package/dist/query/batched-loader.d.ts +29 -2
- package/dist/query/batched-loader.js +148 -1
- package/dist/query/builder.d.ts +156 -8
- package/dist/query/builder.js +715 -134
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +113 -8
- package/dist/schema-builder.d.ts +73 -8
- package/dist/schema-builder.js +59 -4
- package/dist/schema-sql.d.ts +67 -0
- package/dist/schema-sql.js +310 -6
- package/dist/schema.d.ts +53 -0
- package/dist/seed.d.ts +4 -0
- package/dist/seed.js +63 -0
- package/package.json +2 -3
package/dist/generate.js
CHANGED
|
@@ -10,11 +10,29 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { join, relative, resolve } from 'node:path';
|
|
13
|
-
import { singularize, snakeToPascal } from './schema.js';
|
|
13
|
+
import { pgTypeToTs, singularize, snakeToPascal, } from './schema.js';
|
|
14
14
|
/** Get the TypeScript type name for a table (singularized PascalCase) */
|
|
15
15
|
function entityName(tableName) {
|
|
16
16
|
return snakeToPascal(singularize(tableName));
|
|
17
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the TypeScript type for a column, mapping enum-typed columns to their
|
|
20
|
+
* generated string-literal union (PascalCase enum name) instead of the
|
|
21
|
+
* `unknown` that {@link pgTypeToTs} yields for user-defined types. Falls back to
|
|
22
|
+
* the introspected `col.tsType` for every non-enum column.
|
|
23
|
+
*/
|
|
24
|
+
function columnTsType(col, enums) {
|
|
25
|
+
const dt = col.dialectType ?? col.pgType;
|
|
26
|
+
const isArray = col.isArray || dt.startsWith('_');
|
|
27
|
+
const base = isArray && dt.startsWith('_') ? dt.slice(1) : dt;
|
|
28
|
+
if (Object.hasOwn(enums, base)) {
|
|
29
|
+
let t = snakeToPascal(base);
|
|
30
|
+
if (isArray)
|
|
31
|
+
t += '[]';
|
|
32
|
+
return col.nullable ? `${t} | null` : t;
|
|
33
|
+
}
|
|
34
|
+
return col.tsType;
|
|
35
|
+
}
|
|
18
36
|
/** Escape a value for embedding in a single-quoted TypeScript string literal */
|
|
19
37
|
function escSQ(value) {
|
|
20
38
|
return value.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
@@ -44,6 +62,12 @@ export function generate(options) {
|
|
|
44
62
|
const indexContent = generateIndex(options.schema);
|
|
45
63
|
writeFileSync(join(outDir, 'index.ts'), indexContent, 'utf-8');
|
|
46
64
|
files.push('index.ts');
|
|
65
|
+
// Generate zod.ts (optional — --zod flag)
|
|
66
|
+
if (options.zod) {
|
|
67
|
+
const zodContent = generateZod(options.schema);
|
|
68
|
+
writeFileSync(join(outDir, 'zod.ts'), zodContent, 'utf-8');
|
|
69
|
+
files.push('zod.ts');
|
|
70
|
+
}
|
|
47
71
|
return { outDir, files };
|
|
48
72
|
}
|
|
49
73
|
// ---------------------------------------------------------------------------
|
|
@@ -104,7 +128,7 @@ export function generateTypes(schema) {
|
|
|
104
128
|
const pkNote = table.primaryKey.includes(col.name) ? ' (primary key)' : '';
|
|
105
129
|
const nullNote = col.nullable ? ' (nullable)' : '';
|
|
106
130
|
lines.push(` /** Column: ${col.name} — ${col.pgType}${pkNote}${nullNote} */`);
|
|
107
|
-
lines.push(` ${col.field}: ${col.
|
|
131
|
+
lines.push(` ${col.field}: ${columnTsType(col, schema.enums)};`);
|
|
108
132
|
}
|
|
109
133
|
lines.push('}');
|
|
110
134
|
lines.push('');
|
|
@@ -114,15 +138,18 @@ export function generateTypes(schema) {
|
|
|
114
138
|
lines.push(`/** Input type for creating a row in \`${table.name}\` */`);
|
|
115
139
|
lines.push(`export type ${typeName}Create = {`);
|
|
116
140
|
for (const col of table.columns) {
|
|
141
|
+
// STORED generated columns are computed by the database — never writable.
|
|
142
|
+
if (col.isGeneratedStored)
|
|
143
|
+
continue;
|
|
117
144
|
const isPk = table.primaryKey.includes(col.name);
|
|
118
145
|
const isOptional = col.hasDefault || col.nullable || isPk;
|
|
119
146
|
if (isOptional) {
|
|
120
147
|
const reason = isPk ? 'auto-generated' : col.hasDefault ? 'has default' : 'nullable';
|
|
121
148
|
lines.push(` /** Optional: ${reason} */`);
|
|
122
|
-
lines.push(` ${col.field}?: ${col.
|
|
149
|
+
lines.push(` ${col.field}?: ${columnTsType(col, schema.enums)};`);
|
|
123
150
|
}
|
|
124
151
|
else {
|
|
125
|
-
lines.push(` ${col.field}: ${col.
|
|
152
|
+
lines.push(` ${col.field}: ${columnTsType(col, schema.enums)};`);
|
|
126
153
|
}
|
|
127
154
|
}
|
|
128
155
|
lines.push('};');
|
|
@@ -130,11 +157,11 @@ export function generateTypes(schema) {
|
|
|
130
157
|
// --- Update input type (all fields optional except PK) ---
|
|
131
158
|
// Numeric columns additionally accept `UpdateOperatorInput<number>` so
|
|
132
159
|
// users can write `{ viewCount: { increment: 1 } }` without an `as any`.
|
|
133
|
-
const nonPkCols = table.columns.filter((c) => !table.primaryKey.includes(c.name));
|
|
160
|
+
const nonPkCols = table.columns.filter((c) => !table.primaryKey.includes(c.name) && !c.isGeneratedStored);
|
|
134
161
|
lines.push(`/** Input type for updating a row in \`${table.name}\` */`);
|
|
135
162
|
lines.push(`export type ${typeName}Update = {`);
|
|
136
163
|
for (const col of nonPkCols) {
|
|
137
|
-
lines.push(` ${col.field}?: ${updateFieldType(col.
|
|
164
|
+
lines.push(` ${col.field}?: ${updateFieldType(columnTsType(col, schema.enums))};`);
|
|
138
165
|
}
|
|
139
166
|
lines.push('};');
|
|
140
167
|
lines.push('');
|
|
@@ -267,9 +294,122 @@ export function generateTypes(schema) {
|
|
|
267
294
|
return lines.join('\n');
|
|
268
295
|
}
|
|
269
296
|
// ---------------------------------------------------------------------------
|
|
297
|
+
// zod.ts generator (H1 — `turbine generate --zod`)
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
/**
|
|
300
|
+
* Map a TypeScript primitive (as produced by {@link pgTypeToTs}) to its Zod
|
|
301
|
+
* expression. `Date` uses `z.coerce.date()` — the generated schemas double as
|
|
302
|
+
* request-body validators where dates arrive as ISO strings, and coercion keeps
|
|
303
|
+
* both a `Date` and a valid date-string acceptable (documented decision).
|
|
304
|
+
*/
|
|
305
|
+
function zodScalar(ts) {
|
|
306
|
+
switch (ts) {
|
|
307
|
+
case 'number':
|
|
308
|
+
return 'z.number()';
|
|
309
|
+
case 'string':
|
|
310
|
+
return 'z.string()';
|
|
311
|
+
case 'boolean':
|
|
312
|
+
return 'z.boolean()';
|
|
313
|
+
case 'Date':
|
|
314
|
+
return 'z.coerce.date()';
|
|
315
|
+
case 'bigint':
|
|
316
|
+
return 'z.bigint()';
|
|
317
|
+
case 'Buffer':
|
|
318
|
+
return 'z.instanceof(Uint8Array)';
|
|
319
|
+
case 'number[]':
|
|
320
|
+
// pgvector — `pgTypeToTs('vector')` yields `number[]`.
|
|
321
|
+
return 'z.array(z.number())';
|
|
322
|
+
default:
|
|
323
|
+
// json/jsonb and any unmapped user-defined type.
|
|
324
|
+
return 'z.unknown()';
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Base Zod expression for a column, resolving enums → `z.enum([...])`, arrays →
|
|
329
|
+
* `.array()`, and vectors → `z.array(z.number())`. Does NOT append
|
|
330
|
+
* `.nullable()` / `.optional()` — callers layer those on per-schema.
|
|
331
|
+
*/
|
|
332
|
+
function zodBaseType(col, enums) {
|
|
333
|
+
const dt = col.dialectType ?? col.pgType;
|
|
334
|
+
const isArray = col.isArray || dt.startsWith('_');
|
|
335
|
+
const base = isArray && dt.startsWith('_') ? dt.slice(1) : dt;
|
|
336
|
+
let expr;
|
|
337
|
+
if (Object.hasOwn(enums, base)) {
|
|
338
|
+
expr = `z.enum([${enums[base].map((l) => `'${escSQ(l)}'`).join(', ')}])`;
|
|
339
|
+
}
|
|
340
|
+
else {
|
|
341
|
+
expr = zodScalar(pgTypeToTs(base, false));
|
|
342
|
+
}
|
|
343
|
+
if (isArray)
|
|
344
|
+
expr += '.array()';
|
|
345
|
+
return expr;
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Generate the contents of `zod.ts`. Emits, per table, `XSchema` (the full
|
|
349
|
+
* row), `XCreateSchema` (PK/defaulted/nullable columns optional, STORED
|
|
350
|
+
* generated columns omitted), and `XUpdateSchema` (PK + STORED generated
|
|
351
|
+
* columns omitted, every remaining column optional). Exported so tests can pin
|
|
352
|
+
* the output without writing files.
|
|
353
|
+
*/
|
|
354
|
+
export function generateZod(schema) {
|
|
355
|
+
const lines = [...generatedFileHeader()];
|
|
356
|
+
// `zod` is a USER dependency — this generated file imports it, but the Turbine
|
|
357
|
+
// library runtime never does, so Zod stays out of the package's dep graph.
|
|
358
|
+
lines.push("import { z } from 'zod';");
|
|
359
|
+
lines.push('');
|
|
360
|
+
for (const table of Object.values(schema.tables)) {
|
|
361
|
+
const typeName = entityName(table.name);
|
|
362
|
+
// Full-row schema.
|
|
363
|
+
lines.push(`/** Zod schema for a \`${table.name}\` row */`);
|
|
364
|
+
lines.push(`export const ${typeName}Schema = z.object({`);
|
|
365
|
+
for (const col of table.columns) {
|
|
366
|
+
let expr = zodBaseType(col, schema.enums);
|
|
367
|
+
if (col.nullable)
|
|
368
|
+
expr += '.nullable()';
|
|
369
|
+
lines.push(` ${col.field}: ${expr},`);
|
|
370
|
+
}
|
|
371
|
+
lines.push('});');
|
|
372
|
+
lines.push('');
|
|
373
|
+
// Create schema — STORED generated columns can never be written; PK,
|
|
374
|
+
// defaulted, and nullable columns are optional.
|
|
375
|
+
lines.push(`/** Zod schema for creating a \`${table.name}\` row */`);
|
|
376
|
+
lines.push(`export const ${typeName}CreateSchema = z.object({`);
|
|
377
|
+
for (const col of table.columns) {
|
|
378
|
+
if (col.isGeneratedStored)
|
|
379
|
+
continue;
|
|
380
|
+
const isPk = table.primaryKey.includes(col.name);
|
|
381
|
+
let expr = zodBaseType(col, schema.enums);
|
|
382
|
+
if (col.nullable)
|
|
383
|
+
expr += '.nullable()';
|
|
384
|
+
if (col.hasDefault || col.nullable || isPk)
|
|
385
|
+
expr += '.optional()';
|
|
386
|
+
lines.push(` ${col.field}: ${expr},`);
|
|
387
|
+
}
|
|
388
|
+
lines.push('});');
|
|
389
|
+
lines.push('');
|
|
390
|
+
// Update schema — PK and STORED generated columns omitted; all else optional.
|
|
391
|
+
lines.push(`/** Zod schema for updating a \`${table.name}\` row */`);
|
|
392
|
+
lines.push(`export const ${typeName}UpdateSchema = z.object({`);
|
|
393
|
+
for (const col of table.columns) {
|
|
394
|
+
if (col.isGeneratedStored)
|
|
395
|
+
continue;
|
|
396
|
+
if (table.primaryKey.includes(col.name))
|
|
397
|
+
continue;
|
|
398
|
+
let expr = zodBaseType(col, schema.enums);
|
|
399
|
+
if (col.nullable)
|
|
400
|
+
expr += '.nullable()';
|
|
401
|
+
expr += '.optional()';
|
|
402
|
+
lines.push(` ${col.field}: ${expr},`);
|
|
403
|
+
}
|
|
404
|
+
lines.push('});');
|
|
405
|
+
lines.push('');
|
|
406
|
+
}
|
|
407
|
+
return lines.join('\n');
|
|
408
|
+
}
|
|
409
|
+
// ---------------------------------------------------------------------------
|
|
270
410
|
// metadata.ts generator
|
|
271
411
|
// ---------------------------------------------------------------------------
|
|
272
|
-
function generateMetadata(schema) {
|
|
412
|
+
export function generateMetadata(schema) {
|
|
273
413
|
const lines = [
|
|
274
414
|
...generatedFileHeader(),
|
|
275
415
|
"import type { SchemaMetadata } from 'turbine-orm';",
|
|
@@ -348,6 +488,9 @@ function generateMetadata(schema) {
|
|
|
348
488
|
lines.push(` { name: '${escSQ(idx.name)}', columns: [${idx.columns.map((c) => `'${escSQ(c)}'`).join(', ')}], unique: ${idx.unique}, definition: ${JSON.stringify(idx.definition)} },`);
|
|
349
489
|
}
|
|
350
490
|
lines.push(' ],');
|
|
491
|
+
// isView — read-only marker; the runtime write guard reads it.
|
|
492
|
+
if (table.isView)
|
|
493
|
+
lines.push(' isView: true,');
|
|
351
494
|
lines.push(' },');
|
|
352
495
|
}
|
|
353
496
|
lines.push(' },');
|
|
@@ -368,7 +511,7 @@ function generateMetadata(schema) {
|
|
|
368
511
|
// ---------------------------------------------------------------------------
|
|
369
512
|
// index.ts generator (configured client with typed table accessors)
|
|
370
513
|
// ---------------------------------------------------------------------------
|
|
371
|
-
function generateIndex(schema) {
|
|
514
|
+
export function generateIndex(schema) {
|
|
372
515
|
const tableEntries = Object.values(schema.tables);
|
|
373
516
|
const lines = [
|
|
374
517
|
...generatedFileHeader(),
|
|
@@ -405,7 +548,7 @@ function generateIndex(schema) {
|
|
|
405
548
|
const hasRelations = Object.keys(table.relations).length > 0;
|
|
406
549
|
const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
|
|
407
550
|
lines.push(` /** Query interface for the \`${table.name}\` table (transaction-scoped) */`);
|
|
408
|
-
lines.push(` declare readonly ${accessor}:
|
|
551
|
+
lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
|
|
409
552
|
}
|
|
410
553
|
lines.push('}');
|
|
411
554
|
lines.push('');
|
|
@@ -447,7 +590,7 @@ function generateIndex(schema) {
|
|
|
447
590
|
const hasRelations = Object.keys(table.relations).length > 0;
|
|
448
591
|
const genericArgs = hasRelations ? `${typeName}, ${typeName}Relations` : typeName;
|
|
449
592
|
lines.push(` /** Query interface for the \`${table.name}\` table */`);
|
|
450
|
-
lines.push(` declare readonly ${accessor}:
|
|
593
|
+
lines.push(` declare readonly ${accessor}: ${accessorType(table, genericArgs)};`);
|
|
451
594
|
}
|
|
452
595
|
lines.push('');
|
|
453
596
|
lines.push(' constructor(config?: TurbineConfig) {');
|
|
@@ -491,6 +634,18 @@ function generateIndex(schema) {
|
|
|
491
634
|
// ---------------------------------------------------------------------------
|
|
492
635
|
// Helpers
|
|
493
636
|
// ---------------------------------------------------------------------------
|
|
637
|
+
/**
|
|
638
|
+
* The generated table-accessor type. A view (`isView`) without a primary key
|
|
639
|
+
* cannot be looked up by unique key, so its `findUnique`-family methods are
|
|
640
|
+
* excluded via `Omit`. Everything else is a plain `QueryInterface<…>`.
|
|
641
|
+
*/
|
|
642
|
+
function accessorType(table, genericArgs) {
|
|
643
|
+
const base = `QueryInterface<${genericArgs}>`;
|
|
644
|
+
if (table.isView && table.primaryKey.length === 0) {
|
|
645
|
+
return `Omit<${base}, 'findUnique' | 'findUniqueOrThrow'>`;
|
|
646
|
+
}
|
|
647
|
+
return base;
|
|
648
|
+
}
|
|
494
649
|
function serializeColumn(col) {
|
|
495
650
|
const parts = [
|
|
496
651
|
`name: '${escSQ(col.name)}'`,
|
|
@@ -508,6 +663,12 @@ function serializeColumn(col) {
|
|
|
508
663
|
// output stays byte-identical for the common client-default columns.
|
|
509
664
|
if (col.isGenerated)
|
|
510
665
|
parts.push(`isGenerated: true`);
|
|
666
|
+
// STORED generated columns — the runtime write guard reads isGeneratedStored.
|
|
667
|
+
if (col.isGeneratedStored)
|
|
668
|
+
parts.push(`isGeneratedStored: true`);
|
|
669
|
+
if (col.generationExpression !== undefined) {
|
|
670
|
+
parts.push(`generationExpression: '${escSQ(col.generationExpression)}'`);
|
|
671
|
+
}
|
|
511
672
|
if (col.maxLength !== undefined)
|
|
512
673
|
parts.push(`maxLength: ${col.maxLength}`);
|
|
513
674
|
return `{ ${parts.join(', ')} }`;
|
package/dist/index.d.ts
CHANGED
|
@@ -43,11 +43,12 @@ export { type IntrospectOptions, introspect } from './introspect.js';
|
|
|
43
43
|
export { executeNestedCreate, executeNestedUpdate, hasRelationFields, type NestedWriteContext, } from './nested-write.js';
|
|
44
44
|
export type { ObserveConfig, ObserveHandle } from './observe.js';
|
|
45
45
|
export { executePipeline, type PipelineOptions, type PipelineResults, pipelineSupported } from './pipeline.js';
|
|
46
|
-
export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GroupByArgs, type HavingClause, type JsonFilter, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type SelectResult, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
|
|
46
|
+
export { type AggregateArgs, type AggregateResult, type ArrayFilter, type ConnectOrCreateOp, type CountArgs, type CreateArgs, type CreateDataInput, type CreateManyArgs, type DeferredQuery, type DeleteArgs, type DeleteManyArgs, type FieldResult, type FindManyArgs, type FindManyStreamArgs, type FindUniqueArgs, type GlobalFilters, type GroupByArgs, type HavingClause, type JsonFilter, type MiddlewareFn, type NestedCreateOp, type NestedUpdateOp, type NestedUpdateOpItem, type NestedUpsertOpItem, type OmitResult, type OrderByClause, type OrderDirection, type QueryEvent, type QueryEventListener, QueryInterface, type QueryResult, type RelationDescriptor, type RelationFilter, type SelectResult, type SkipGlobalFilters, type TextSearchFilter, type TypedWithClause, type UpdateArgs, type UpdateDataInput, type UpdateInput, type UpdateManyArgs, type UpdateOperatorInput, type UpsertArgs, type VectorDistanceFilter, type VectorFilter, type VectorMetric, type VectorOrderBy, type VectorOrderByDistance, type WhereClause, type WhereOperator, type WhereValue, type WithClause, type WithOptions, type WithResult, } from './query/index.js';
|
|
47
47
|
export { type ActiveSubscription, type NotificationHandler, type Subscription, validateChannel } from './realtime.js';
|
|
48
|
-
export type { ColumnMetadata, IndexMetadata, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
48
|
+
export type { CheckMetadata, ColumnMetadata, IndexMetadata, ReferentialAction, RelationDef, SchemaMetadata, TableMetadata, } from './schema.js';
|
|
49
49
|
export { camelToSnake, isDateType, normalizeKeyColumns, pgArrayType, pgTypeToTs, singularize, snakeToCamel, snakeToPascal, } from './schema.js';
|
|
50
|
-
export { applyManyToManyRelations, ColumnBuilder, type ColumnConfig, type ColumnDef, type ColumnType, type ColumnTypeName, column, defineSchema, type ManyToManyDef, type SchemaDef, type TableDef, table, } from './schema-builder.js';
|
|
50
|
+
export { applyManyToManyRelations, type CheckDef, ColumnBuilder, type ColumnConfig, type ColumnDef, type ColumnType, type ColumnTypeName, column, type DefineSchemaOptions, defineSchema, type ManyToManyDef, type ReferenceDef, type SchemaDef, type TableDef, table, } from './schema-builder.js';
|
|
51
51
|
export { type AlterColumnDef, type AlterDef, type DiffResult, type PushResult, type SchemaSqlOptions, schemaDiff, schemaPush, schemaToSQL, schemaToSQLString, } from './schema-sql.js';
|
|
52
|
+
export { type DefinedSeed, defineSeed, type SeedFunction } from './seed.js';
|
|
52
53
|
export { type TurbineHttpOptions, turbineHttp } from './serverless.js';
|
|
53
54
|
export { buildTypedSql, TypedSqlQuery } from './typed-sql.js';
|
package/dist/index.js
CHANGED
|
@@ -58,6 +58,8 @@ export { applyManyToManyRelations, ColumnBuilder, column, defineSchema,
|
|
|
58
58
|
table, } from './schema-builder.js';
|
|
59
59
|
// Schema SQL — generate DDL, diff, and push
|
|
60
60
|
export { schemaDiff, schemaPush, schemaToSQL, schemaToSQLString, } from './schema-sql.js';
|
|
61
|
+
// Seed helper
|
|
62
|
+
export { defineSeed } from './seed.js';
|
|
61
63
|
// Serverless / edge factory
|
|
62
64
|
export { turbineHttp } from './serverless.js';
|
|
63
65
|
// Typed raw SQL — Turbine's TypedSQL escape hatch
|
package/dist/introspect.d.ts
CHANGED
|
@@ -8,7 +8,13 @@
|
|
|
8
8
|
* This is the foundation of `npx turbine generate`.
|
|
9
9
|
*/
|
|
10
10
|
import { type Dialect } from './dialect.js';
|
|
11
|
-
import { type SchemaMetadata } from './schema.js';
|
|
11
|
+
import { type ReferentialAction, type SchemaMetadata } from './schema.js';
|
|
12
|
+
/**
|
|
13
|
+
* Map a `pg_constraint.confdeltype` / `confupdtype` character to a
|
|
14
|
+
* {@link ReferentialAction}. Postgres encodes: `a` = NO ACTION, `r` = RESTRICT,
|
|
15
|
+
* `c` = CASCADE, `n` = SET NULL, `d` = SET DEFAULT.
|
|
16
|
+
*/
|
|
17
|
+
export declare function pgConfActionToReferential(ch: string): ReferentialAction;
|
|
12
18
|
export interface IntrospectOptions {
|
|
13
19
|
/** Postgres connection string */
|
|
14
20
|
connectionString: string;
|
|
@@ -18,6 +24,13 @@ export interface IntrospectOptions {
|
|
|
18
24
|
include?: string[];
|
|
19
25
|
/** Tables to exclude (default: none). Applied after include. */
|
|
20
26
|
exclude?: string[];
|
|
27
|
+
/**
|
|
28
|
+
* Also introspect **views** and **materialized views** as read-only
|
|
29
|
+
* {@link TableMetadata} entries (`isView: true`). Off by default. Write
|
|
30
|
+
* builders reject views (E003); a view without a primary key is excluded from
|
|
31
|
+
* the generated `findUnique`-family accessor types.
|
|
32
|
+
*/
|
|
33
|
+
includeViews?: boolean;
|
|
21
34
|
/**
|
|
22
35
|
* Dialect whose {@link Dialect.introspector} drives the catalog reads.
|
|
23
36
|
* Defaults to {@link postgresDialect}. Engines plug their own introspector
|
|
@@ -37,3 +50,9 @@ export declare function introspect(options: IntrospectOptions): Promise<SchemaMe
|
|
|
37
50
|
* `postgresDialect.introspector`; call {@link introspect} for dialect routing.
|
|
38
51
|
*/
|
|
39
52
|
export declare function introspectPostgresCatalog(options: IntrospectOptions): Promise<SchemaMetadata>;
|
|
53
|
+
/**
|
|
54
|
+
* Recover the raw check expression from `pg_get_constraintdef` output, which
|
|
55
|
+
* wraps it as `CHECK ((expr))`. Strips the leading `CHECK ` keyword and one
|
|
56
|
+
* balanced outer paren pair; leaves anything unexpected untouched.
|
|
57
|
+
*/
|
|
58
|
+
export declare function stripCheckWrapper(def: string): string;
|
package/dist/introspect.js
CHANGED
|
@@ -10,6 +10,25 @@
|
|
|
10
10
|
import pg from 'pg';
|
|
11
11
|
import { postgresDialect } from './dialect.js';
|
|
12
12
|
import { isDateType, pgTypeToTs, singularize, snakeToCamel, } from './schema.js';
|
|
13
|
+
/**
|
|
14
|
+
* Map a `pg_constraint.confdeltype` / `confupdtype` character to a
|
|
15
|
+
* {@link ReferentialAction}. Postgres encodes: `a` = NO ACTION, `r` = RESTRICT,
|
|
16
|
+
* `c` = CASCADE, `n` = SET NULL, `d` = SET DEFAULT.
|
|
17
|
+
*/
|
|
18
|
+
export function pgConfActionToReferential(ch) {
|
|
19
|
+
switch (ch) {
|
|
20
|
+
case 'c':
|
|
21
|
+
return 'cascade';
|
|
22
|
+
case 'r':
|
|
23
|
+
return 'restrict';
|
|
24
|
+
case 'n':
|
|
25
|
+
return 'set null';
|
|
26
|
+
case 'd':
|
|
27
|
+
return 'set default';
|
|
28
|
+
default:
|
|
29
|
+
return 'no action';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
13
32
|
// ---------------------------------------------------------------------------
|
|
14
33
|
// SQL queries (all parameterized, no interpolation)
|
|
15
34
|
// ---------------------------------------------------------------------------
|
|
@@ -29,6 +48,8 @@ const SQL_COLUMNS = `
|
|
|
29
48
|
is_nullable,
|
|
30
49
|
column_default,
|
|
31
50
|
is_identity,
|
|
51
|
+
is_generated,
|
|
52
|
+
generation_expression,
|
|
32
53
|
ordinal_position,
|
|
33
54
|
character_maximum_length
|
|
34
55
|
FROM information_schema.columns
|
|
@@ -84,6 +105,65 @@ const SQL_INDEXES = `
|
|
|
84
105
|
FROM pg_indexes
|
|
85
106
|
WHERE schemaname = $1
|
|
86
107
|
`;
|
|
108
|
+
// Foreign-key referential actions (ON DELETE / ON UPDATE) live in pg_catalog,
|
|
109
|
+
// not information_schema. Keyed by constraint name for join with SQL_FOREIGN_KEYS.
|
|
110
|
+
const SQL_FK_ACTIONS = `
|
|
111
|
+
SELECT con.conname, con.confdeltype, con.confupdtype
|
|
112
|
+
FROM pg_constraint con
|
|
113
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = con.connamespace
|
|
114
|
+
WHERE con.contype = 'f'
|
|
115
|
+
AND n.nspname = $1
|
|
116
|
+
`;
|
|
117
|
+
// CHECK constraints (contype = 'c'). NOT NULL is stored as attnotnull, not a
|
|
118
|
+
// check constraint, so it never appears here.
|
|
119
|
+
const SQL_CHECKS = `
|
|
120
|
+
SELECT rel.relname AS table_name, con.conname, pg_get_constraintdef(con.oid) AS definition
|
|
121
|
+
FROM pg_constraint con
|
|
122
|
+
JOIN pg_catalog.pg_class rel ON rel.oid = con.conrelid
|
|
123
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = con.connamespace
|
|
124
|
+
WHERE con.contype = 'c'
|
|
125
|
+
AND n.nspname = $1
|
|
126
|
+
`;
|
|
127
|
+
// Views (relkind 'v') — column metadata comes free from information_schema.columns.
|
|
128
|
+
const SQL_VIEWS = `
|
|
129
|
+
SELECT table_name
|
|
130
|
+
FROM information_schema.views
|
|
131
|
+
WHERE table_schema = $1
|
|
132
|
+
ORDER BY table_name
|
|
133
|
+
`;
|
|
134
|
+
// Materialized views (relkind 'm') — NOT in information_schema; read from pg_catalog.
|
|
135
|
+
const SQL_MATVIEWS = `
|
|
136
|
+
SELECT matviewname AS table_name
|
|
137
|
+
FROM pg_matviews
|
|
138
|
+
WHERE schemaname = $1
|
|
139
|
+
ORDER BY matviewname
|
|
140
|
+
`;
|
|
141
|
+
// Materialized-view columns — information_schema.columns omits matviews, so pull
|
|
142
|
+
// them from pg_attribute. Aliased to mirror SQL_COLUMNS so the same row-mapping
|
|
143
|
+
// applies (array types surface as data_type 'ARRAY' + a '_'-prefixed udt_name).
|
|
144
|
+
const SQL_MATVIEW_COLUMNS = `
|
|
145
|
+
SELECT
|
|
146
|
+
c.relname AS table_name,
|
|
147
|
+
a.attname AS column_name,
|
|
148
|
+
t.typname AS udt_name,
|
|
149
|
+
CASE WHEN t.typcategory = 'A' THEN 'ARRAY' ELSE 'base' END AS data_type,
|
|
150
|
+
CASE WHEN a.attnotnull THEN 'NO' ELSE 'YES' END AS is_nullable,
|
|
151
|
+
NULL AS column_default,
|
|
152
|
+
'NO' AS is_identity,
|
|
153
|
+
'NEVER' AS is_generated,
|
|
154
|
+
NULL AS generation_expression,
|
|
155
|
+
a.attnum AS ordinal_position,
|
|
156
|
+
NULL::int AS character_maximum_length
|
|
157
|
+
FROM pg_catalog.pg_attribute a
|
|
158
|
+
JOIN pg_catalog.pg_class c ON c.oid = a.attrelid
|
|
159
|
+
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
|
|
160
|
+
JOIN pg_catalog.pg_type t ON t.oid = a.atttypid
|
|
161
|
+
WHERE n.nspname = $1
|
|
162
|
+
AND c.relkind = 'm'
|
|
163
|
+
AND a.attnum > 0
|
|
164
|
+
AND NOT a.attisdropped
|
|
165
|
+
ORDER BY c.relname, a.attnum
|
|
166
|
+
`;
|
|
87
167
|
const SQL_ENUMS = `
|
|
88
168
|
SELECT t.typname, e.enumlabel
|
|
89
169
|
FROM pg_type t
|
|
@@ -124,17 +204,45 @@ export async function introspectPostgresCatalog(options) {
|
|
|
124
204
|
});
|
|
125
205
|
try {
|
|
126
206
|
// Run all information_schema queries in parallel
|
|
127
|
-
const [tablesResult, columnsResult, pkResult, fkResult, uniqueResult, indexResult, enumResult] = await Promise.all([
|
|
207
|
+
const [tablesResult, columnsResult, pkResult, fkResult, fkActionsResult, uniqueResult, indexResult, checkResult, enumResult,] = await Promise.all([
|
|
128
208
|
pool.query(SQL_TABLES, [schema]),
|
|
129
209
|
pool.query(SQL_COLUMNS, [schema]),
|
|
130
210
|
pool.query(SQL_PRIMARY_KEYS, [schema]),
|
|
131
211
|
pool.query(SQL_FOREIGN_KEYS, [schema]),
|
|
212
|
+
pool.query(SQL_FK_ACTIONS, [schema]),
|
|
132
213
|
pool.query(SQL_UNIQUE_CONSTRAINTS, [schema]),
|
|
133
214
|
pool.query(SQL_INDEXES, [schema]),
|
|
215
|
+
pool.query(SQL_CHECKS, [schema]),
|
|
134
216
|
pool.query(SQL_ENUMS, [schema]),
|
|
135
217
|
]);
|
|
136
|
-
//
|
|
137
|
-
|
|
218
|
+
// Views + materialized views (opt-in). Regular-view columns are already in
|
|
219
|
+
// columnsResult (information_schema.columns); matview columns need a separate
|
|
220
|
+
// pg_catalog read, which we splice into the column rows below.
|
|
221
|
+
const viewNameSet = new Set();
|
|
222
|
+
const matviewColumnRows = [];
|
|
223
|
+
if (options.includeViews) {
|
|
224
|
+
const [viewsResult, matviewsResult, matviewColsResult] = await Promise.all([
|
|
225
|
+
pool.query(SQL_VIEWS, [schema]),
|
|
226
|
+
pool.query(SQL_MATVIEWS, [schema]),
|
|
227
|
+
pool.query(SQL_MATVIEW_COLUMNS, [schema]),
|
|
228
|
+
]);
|
|
229
|
+
for (const r of viewsResult.rows)
|
|
230
|
+
viewNameSet.add(r.table_name);
|
|
231
|
+
for (const r of matviewsResult.rows)
|
|
232
|
+
viewNameSet.add(r.table_name);
|
|
233
|
+
matviewColumnRows.push(...matviewColsResult.rows);
|
|
234
|
+
}
|
|
235
|
+
// constraint_name → { onDelete, onUpdate } referential actions.
|
|
236
|
+
const fkActions = new Map();
|
|
237
|
+
for (const row of fkActionsResult.rows) {
|
|
238
|
+
fkActions.set(row.conname, {
|
|
239
|
+
onDelete: pgConfActionToReferential(row.confdeltype),
|
|
240
|
+
onUpdate: pgConfActionToReferential(row.confupdtype),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
// Filter tables by include/exclude. Views/matviews join the base tables as
|
|
244
|
+
// candidates so include/exclude apply uniformly.
|
|
245
|
+
let tableNames = [...tablesResult.rows.map((r) => r.table_name), ...viewNameSet];
|
|
138
246
|
if (options.include?.length) {
|
|
139
247
|
const includeSet = new Set(options.include);
|
|
140
248
|
tableNames = tableNames.filter((t) => includeSet.has(t));
|
|
@@ -145,8 +253,10 @@ export async function introspectPostgresCatalog(options) {
|
|
|
145
253
|
}
|
|
146
254
|
const tableSet = new Set(tableNames);
|
|
147
255
|
// ----- Group columns by table -----
|
|
256
|
+
// Base-table + regular-view columns come from information_schema.columns;
|
|
257
|
+
// materialized-view columns are appended from the pg_catalog read.
|
|
148
258
|
const columnsByTable = new Map();
|
|
149
|
-
for (const row of columnsResult.rows) {
|
|
259
|
+
for (const row of [...columnsResult.rows, ...matviewColumnRows]) {
|
|
150
260
|
const tableName = row.table_name;
|
|
151
261
|
if (!tableSet.has(tableName))
|
|
152
262
|
continue;
|
|
@@ -169,6 +279,11 @@ export async function introspectPostgresCatalog(options) {
|
|
|
169
279
|
// (gen_random_uuid(), now()), which Turbine must still synthesize.
|
|
170
280
|
isGenerated: (typeof row.column_default === 'string' && row.column_default.includes('nextval(')) ||
|
|
171
281
|
row.is_identity === 'YES',
|
|
282
|
+
// GENERATED ALWAYS AS (expr) STORED — computed by the database, never
|
|
283
|
+
// writable. Distinct from isGenerated (serial/identity, which a client
|
|
284
|
+
// MAY override). is_generated is 'ALWAYS' for STORED columns, else 'NEVER'.
|
|
285
|
+
isGeneratedStored: row.is_generated === 'ALWAYS',
|
|
286
|
+
generationExpression: row.is_generated === 'ALWAYS' && row.generation_expression ? row.generation_expression : undefined,
|
|
172
287
|
isArray,
|
|
173
288
|
arrayType,
|
|
174
289
|
pgArrayType: arrayType,
|
|
@@ -223,6 +338,20 @@ export async function introspectPostgresCatalog(options) {
|
|
|
223
338
|
definition: row.indexdef,
|
|
224
339
|
});
|
|
225
340
|
}
|
|
341
|
+
// ----- Group check constraints by table -----
|
|
342
|
+
// pg_get_constraintdef yields e.g. `CHECK ((price >= 0))`; strip the leading
|
|
343
|
+
// `CHECK ` and the outermost paren pair to recover the raw expression.
|
|
344
|
+
const checksByTable = new Map();
|
|
345
|
+
for (const row of checkResult.rows) {
|
|
346
|
+
if (!tableSet.has(row.table_name))
|
|
347
|
+
continue;
|
|
348
|
+
if (!checksByTable.has(row.table_name))
|
|
349
|
+
checksByTable.set(row.table_name, []);
|
|
350
|
+
checksByTable.get(row.table_name).push({
|
|
351
|
+
name: row.conname,
|
|
352
|
+
expression: stripCheckWrapper(row.definition),
|
|
353
|
+
});
|
|
354
|
+
}
|
|
226
355
|
// ----- Collect enums -----
|
|
227
356
|
const enums = {};
|
|
228
357
|
for (const row of enumResult.rows) {
|
|
@@ -272,6 +401,13 @@ export async function introspectPostgresCatalog(options) {
|
|
|
272
401
|
? snakeToCamel(fk.sourceColumns[0].replace(/_id$/, ''))
|
|
273
402
|
: snakeToCamel(fk.constraintName.replace(/^fk_/, '').replace(/_fkey$/, ''))
|
|
274
403
|
: singularize(snakeToCamel(fk.targetTable));
|
|
404
|
+
// Referential actions (omit the 'no action' default to keep metadata lean).
|
|
405
|
+
const actions = fkActions.get(fk.constraintName);
|
|
406
|
+
const actionFields = {};
|
|
407
|
+
if (actions?.onDelete && actions.onDelete !== 'no action')
|
|
408
|
+
actionFields.onDelete = actions.onDelete;
|
|
409
|
+
if (actions?.onUpdate && actions.onUpdate !== 'no action')
|
|
410
|
+
actionFields.onUpdate = actions.onUpdate;
|
|
275
411
|
if (!relationsByTable.has(fk.sourceTable))
|
|
276
412
|
relationsByTable.set(fk.sourceTable, {});
|
|
277
413
|
relationsByTable.get(fk.sourceTable)[belongsToName] = {
|
|
@@ -281,6 +417,7 @@ export async function introspectPostgresCatalog(options) {
|
|
|
281
417
|
to: fk.targetTable,
|
|
282
418
|
foreignKey,
|
|
283
419
|
referenceKey,
|
|
420
|
+
...actionFields,
|
|
284
421
|
};
|
|
285
422
|
// --- hasMany on the target (parent) table ---
|
|
286
423
|
// e.g. posts.user_id → users.id creates users.posts (hasMany)
|
|
@@ -298,6 +435,7 @@ export async function introspectPostgresCatalog(options) {
|
|
|
298
435
|
to: fk.sourceTable,
|
|
299
436
|
foreignKey,
|
|
300
437
|
referenceKey,
|
|
438
|
+
...actionFields,
|
|
301
439
|
};
|
|
302
440
|
}
|
|
303
441
|
// ----- Conservative many-to-many auto-detection (PURELY ADDITIVE) -----
|
|
@@ -415,6 +553,8 @@ export async function introspectPostgresCatalog(options) {
|
|
|
415
553
|
uniqueColumns: uniqueByTable.get(tableName) ?? [],
|
|
416
554
|
relations: relationsByTable.get(tableName) ?? {},
|
|
417
555
|
indexes: indexesByTable.get(tableName) ?? [],
|
|
556
|
+
checks: checksByTable.get(tableName) ?? [],
|
|
557
|
+
...(viewNameSet.has(tableName) ? { isView: true } : {}),
|
|
418
558
|
};
|
|
419
559
|
}
|
|
420
560
|
return { tables, enums };
|
|
@@ -423,3 +563,34 @@ export async function introspectPostgresCatalog(options) {
|
|
|
423
563
|
await pool.end();
|
|
424
564
|
}
|
|
425
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* Recover the raw check expression from `pg_get_constraintdef` output, which
|
|
568
|
+
* wraps it as `CHECK ((expr))`. Strips the leading `CHECK ` keyword and one
|
|
569
|
+
* balanced outer paren pair; leaves anything unexpected untouched.
|
|
570
|
+
*/
|
|
571
|
+
export function stripCheckWrapper(def) {
|
|
572
|
+
let s = def.trim();
|
|
573
|
+
const m = /^CHECK\s*\((.*)\)$/is.exec(s);
|
|
574
|
+
if (m)
|
|
575
|
+
s = m[1].trim();
|
|
576
|
+
// pg double-wraps single expressions: `(price >= 0)` → unwrap one more pair
|
|
577
|
+
// only when the parens are balanced across the whole string.
|
|
578
|
+
if (s.startsWith('(') && s.endsWith(')')) {
|
|
579
|
+
let depth = 0;
|
|
580
|
+
let balanced = true;
|
|
581
|
+
for (let i = 0; i < s.length; i++) {
|
|
582
|
+
if (s[i] === '(')
|
|
583
|
+
depth++;
|
|
584
|
+
else if (s[i] === ')') {
|
|
585
|
+
depth--;
|
|
586
|
+
if (depth === 0 && i < s.length - 1) {
|
|
587
|
+
balanced = false;
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (balanced)
|
|
593
|
+
s = s.slice(1, -1).trim();
|
|
594
|
+
}
|
|
595
|
+
return s;
|
|
596
|
+
}
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
* @module
|
|
48
48
|
*/
|
|
49
49
|
import type pg from 'pg';
|
|
50
|
-
import { type SchemaMetadata, type TableMetadata } from '../schema.js';
|
|
50
|
+
import { type RelationDef, type SchemaMetadata, type TableMetadata } from '../schema.js';
|
|
51
51
|
import type { ReselectExecutor } from './builder.js';
|
|
52
|
-
import type { WithClause } from './types.js';
|
|
52
|
+
import type { SkipGlobalFilters, WithClause, WithCount } from './types.js';
|
|
53
53
|
/**
|
|
54
54
|
* A DeferredQuery, minimally typed for what the loader consumes. Kept local to
|
|
55
55
|
* avoid a value import of builder.ts (which imports this module).
|
|
@@ -88,6 +88,23 @@ export interface RelationLoadContext {
|
|
|
88
88
|
inClauseParam: (values: unknown[]) => unknown;
|
|
89
89
|
/** Placeholder for a 1-indexed parameter position (PG: `$n`). */
|
|
90
90
|
paramPlaceholder: (index: number) => string;
|
|
91
|
+
/**
|
|
92
|
+
* The query's `skipGlobalFilters` opt-out, threaded onto every child
|
|
93
|
+
* `buildFindMany` so relation row loads honor (or skip) the target table's
|
|
94
|
+
* global filter exactly as the join strategy would.
|
|
95
|
+
*/
|
|
96
|
+
skipGlobalFilters?: SkipGlobalFilters;
|
|
97
|
+
/**
|
|
98
|
+
* Render `table`'s global filter against `alias` for a raw follow-up query
|
|
99
|
+
* (the batched `_count`), numbering its `$n` placeholders AFTER
|
|
100
|
+
* `precedingParams` already-bound params. Returns `null` when no filter
|
|
101
|
+
* applies. Provided by the owning QueryInterface so this module needs no
|
|
102
|
+
* filter machinery of its own.
|
|
103
|
+
*/
|
|
104
|
+
tableGlobalFilter?: (table: string, alias: string, precedingParams: number) => {
|
|
105
|
+
clause: string;
|
|
106
|
+
params: unknown[];
|
|
107
|
+
} | null;
|
|
91
108
|
}
|
|
92
109
|
/**
|
|
93
110
|
* Adjust a `select`/`omit` pair so that `fields` are guaranteed present in the
|
|
@@ -111,6 +128,16 @@ export declare function stripFields(rows: Record<string, unknown>[], fields: str
|
|
|
111
128
|
* The caller adds these to the base query and strips the added ones afterwards.
|
|
112
129
|
*/
|
|
113
130
|
export declare function neededParentKeyFields(parentMeta: TableMetadata, withClause: WithClause): string[];
|
|
131
|
+
/**
|
|
132
|
+
* Resolve the set of to-many relations a `_count` spec selects. `true` counts
|
|
133
|
+
* every to-many relation (hasMany + manyToMany) of the table; the record form
|
|
134
|
+
* counts only the enabled names. Shared by the join builder and the batched
|
|
135
|
+
* loader so both count the exact same relations.
|
|
136
|
+
*
|
|
137
|
+
* Errors: E005 ({@link RelationError}) for an unknown relation name, E003
|
|
138
|
+
* ({@link ValidationError}) when a named relation is to-one.
|
|
139
|
+
*/
|
|
140
|
+
export declare function resolveCountRelations(parentMeta: TableMetadata, countSpec: WithCount): RelationDef[];
|
|
114
141
|
/**
|
|
115
142
|
* Load every relation in `withClause` for `parents` and attach it onto each row
|
|
116
143
|
* in place. Mirrors the join strategy's output shape exactly. Recurses for nested
|