turbine-orm 0.23.2 → 0.25.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 +4 -1
- package/dist/cjs/cli/config.js +14 -0
- package/dist/cjs/cli/index.js +60 -0
- package/dist/cjs/schema-builder.js +29 -1
- package/dist/cjs/schema-sql.js +19 -5
- package/dist/cjs/serverless.js +35 -3
- package/dist/cli/config.d.ts +14 -0
- package/dist/cli/config.js +13 -0
- package/dist/cli/index.js +61 -1
- package/dist/schema-builder.d.ts +6 -3
- package/dist/schema-builder.js +29 -1
- package/dist/schema-sql.js +19 -5
- package/dist/serverless.d.ts +32 -4
- package/dist/serverless.js +35 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -51,8 +51,9 @@ Performance is at parity with Prisma and Drizzle — the real reasons to choose
|
|
|
51
51
|
## Quick Start
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
# 1. Install
|
|
54
|
+
# 1. Install (the CLI also needs tsx to load .ts config/schema files)
|
|
55
55
|
npm install turbine-orm
|
|
56
|
+
npm install --save-dev tsx
|
|
56
57
|
|
|
57
58
|
# 2. Initialize project
|
|
58
59
|
npx turbine init --url postgres://user:pass@localhost:5432/mydb
|
|
@@ -61,6 +62,8 @@ npx turbine init --url postgres://user:pass@localhost:5432/mydb
|
|
|
61
62
|
npx turbine generate
|
|
62
63
|
```
|
|
63
64
|
|
|
65
|
+
> **CLI prerequisites.** The `turbine` CLI loads your `turbine.config.ts` / `turbine/schema.ts` directly, so a fresh project needs (1) `tsx` installed — otherwise `.ts` config loading fails with *"Loading .ts config / schema files requires tsx to be installed"* — and (2) `"type": "module"` in `package.json`, since Turbine is ESM. Without it you'll hit `Error [ERR_REQUIRE_ESM]: Cannot require() ES Module`. `create-next-app` sets neither by default. See [USING-TURBINE-ORM.md §0](docs/USING-TURBINE-ORM.md) for details.
|
|
66
|
+
|
|
64
67
|
Works with both ESM and CommonJS:
|
|
65
68
|
|
|
66
69
|
```typescript
|
package/dist/cjs/cli/config.js
CHANGED
|
@@ -39,6 +39,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
39
39
|
};
|
|
40
40
|
})();
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.looksLikeSchemaFilePath = looksLikeSchemaFilePath;
|
|
42
43
|
exports.loadConfig = loadConfig;
|
|
43
44
|
exports.findConfigFile = findConfigFile;
|
|
44
45
|
exports.resolveConfig = resolveConfig;
|
|
@@ -46,6 +47,19 @@ exports.configTemplate = configTemplate;
|
|
|
46
47
|
const node_fs_1 = require("node:fs");
|
|
47
48
|
const node_path_1 = require("node:path");
|
|
48
49
|
const node_url_1 = require("node:url");
|
|
50
|
+
/**
|
|
51
|
+
* Heuristic: does a configured `schema` value actually look like a schema FILE
|
|
52
|
+
* path rather than a Postgres schema name? `schema` is the Postgres namespace
|
|
53
|
+
* to introspect (default `public`); the schema-builder file goes in `schemaFile`.
|
|
54
|
+
* A value containing a path separator or a JS/TS extension is almost certainly a
|
|
55
|
+
* mis-set `schemaFile` — introspecting `WHERE table_schema = './turbine/schema.ts'`
|
|
56
|
+
* silently matches zero tables. Used by `turbine generate` to fail loudly.
|
|
57
|
+
*/
|
|
58
|
+
function looksLikeSchemaFilePath(schema) {
|
|
59
|
+
if (!schema)
|
|
60
|
+
return false;
|
|
61
|
+
return schema.includes('/') || schema.includes('\\') || /\.(ts|mts|cts|js|mjs|cjs|json)$/i.test(schema);
|
|
62
|
+
}
|
|
49
63
|
// ---------------------------------------------------------------------------
|
|
50
64
|
// Config file names, in priority order
|
|
51
65
|
// ---------------------------------------------------------------------------
|
package/dist/cjs/cli/index.js
CHANGED
|
@@ -120,6 +120,9 @@ function parseArgs() {
|
|
|
120
120
|
case '--allow-drift':
|
|
121
121
|
result.allowDrift = true;
|
|
122
122
|
break;
|
|
123
|
+
case '--allow-empty':
|
|
124
|
+
result.allowEmpty = true;
|
|
125
|
+
break;
|
|
123
126
|
case '--force':
|
|
124
127
|
case '-f':
|
|
125
128
|
result.force = true;
|
|
@@ -242,10 +245,28 @@ async function loadSchemaFile(schemaFile) {
|
|
|
242
245
|
(0, ui_js_1.newline)();
|
|
243
246
|
console.log(` ${(0, ui_js_1.dim)('Hint: install')} ${(0, ui_js_1.cyan)('tsx')} ${(0, ui_js_1.dim)('to load .ts files:')} ${(0, ui_js_1.cyan)('npm install --save-dev tsx')}`);
|
|
244
247
|
}
|
|
248
|
+
printCjsHintIfApplicable(err);
|
|
245
249
|
}
|
|
246
250
|
process.exit(1);
|
|
247
251
|
}
|
|
248
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* When a config/schema import blows up with the CommonJS-vs-ESM interop error
|
|
255
|
+
* (`Cannot require() ES Module` / `ERR_REQUIRE_ESM`), the root cause is almost
|
|
256
|
+
* always a project whose `package.json` lacks `"type": "module"`: tsx transpiles
|
|
257
|
+
* the `.ts` file to CJS and then can't `require()` Turbine's ESM build. Point the
|
|
258
|
+
* user at the one-line fix instead of leaving them with a raw Node stack trace.
|
|
259
|
+
*/
|
|
260
|
+
function printCjsHintIfApplicable(err) {
|
|
261
|
+
const msg = err.message;
|
|
262
|
+
if (msg.includes('ERR_REQUIRE_ESM') ||
|
|
263
|
+
msg.includes('require() of ES Module') ||
|
|
264
|
+
msg.includes('Cannot require() ES Module')) {
|
|
265
|
+
(0, ui_js_1.newline)();
|
|
266
|
+
console.log(` ${(0, ui_js_1.dim)('Hint: add')} ${(0, ui_js_1.cyan)('"type": "module"')} ${(0, ui_js_1.dim)('to your')} ${(0, ui_js_1.cyan)('package.json')}${(0, ui_js_1.dim)('.')}`);
|
|
267
|
+
console.log(` ${(0, ui_js_1.dim)('Turbine is an ESM package; without it, Node/tsx tries to')} ${(0, ui_js_1.cyan)('require()')} ${(0, ui_js_1.dim)('it and fails.')}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
249
270
|
// ---------------------------------------------------------------------------
|
|
250
271
|
// Command: init
|
|
251
272
|
// ---------------------------------------------------------------------------
|
|
@@ -443,6 +464,24 @@ async function cmdGenerate(args, config) {
|
|
|
443
464
|
(0, ui_js_1.banner)();
|
|
444
465
|
const url = requireUrl(config);
|
|
445
466
|
const startTime = performance.now();
|
|
467
|
+
// Guard: `schema` is the Postgres NAMESPACE to introspect (default `public`),
|
|
468
|
+
// NOT the path to your schema-builder file — that goes in `schemaFile`. If the
|
|
469
|
+
// configured `schema` looks like a file path, introspection would silently
|
|
470
|
+
// match zero tables and emit an empty client. Fail loudly instead.
|
|
471
|
+
if (!args.allowEmpty && (0, config_js_1.looksLikeSchemaFilePath)(config.schema)) {
|
|
472
|
+
(0, ui_js_1.error)(`The "schema" option looks like a file path: ${(0, ui_js_1.cyan)(config.schema)}`);
|
|
473
|
+
(0, ui_js_1.newline)();
|
|
474
|
+
console.log(` ${(0, ui_js_1.dim)('"schema" is the Postgres schema NAME to introspect')} ${(0, ui_js_1.dim)('(default:')} ${(0, ui_js_1.cyan)('public')}${(0, ui_js_1.dim)(').')}`);
|
|
475
|
+
console.log(` ${(0, ui_js_1.dim)('The path to your defineSchema() file belongs in')} ${(0, ui_js_1.cyan)('schemaFile')}${(0, ui_js_1.dim)('.')}`);
|
|
476
|
+
(0, ui_js_1.newline)();
|
|
477
|
+
console.log(` ${(0, ui_js_1.dim)('Fix your')} ${(0, ui_js_1.cyan)('turbine.config.ts')}${(0, ui_js_1.dim)(':')}`);
|
|
478
|
+
console.log(` ${(0, ui_js_1.green)('schema:')} ${(0, ui_js_1.cyan)("'public'")}${(0, ui_js_1.dim)(", // or omit — introspects the 'public' schema")}`);
|
|
479
|
+
console.log(` ${(0, ui_js_1.green)('schemaFile:')} ${(0, ui_js_1.cyan)(`'${config.schema}'`)}${(0, ui_js_1.dim)(', // your defineSchema() file (used by `turbine push`)')}`);
|
|
480
|
+
(0, ui_js_1.newline)();
|
|
481
|
+
console.log(` ${(0, ui_js_1.dim)('Re-run with')} ${(0, ui_js_1.cyan)('--allow-empty')} ${(0, ui_js_1.dim)('to introspect this literal schema name anyway.')}`);
|
|
482
|
+
(0, ui_js_1.newline)();
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
446
485
|
(0, ui_js_1.label)('Database', (0, ui_js_1.redactUrl)(url));
|
|
447
486
|
(0, ui_js_1.label)('Schema', config.schema);
|
|
448
487
|
(0, ui_js_1.label)('Output', config.out);
|
|
@@ -459,6 +498,24 @@ async function cmdGenerate(args, config) {
|
|
|
459
498
|
const totalColumns = Object.values(schema.tables).reduce((sum, t) => sum + t.columns.length, 0);
|
|
460
499
|
const totalRelations = Object.values(schema.tables).reduce((sum, t) => sum + Object.keys(t.relations).length, 0);
|
|
461
500
|
spinner.succeed(`Found ${(0, ui_js_1.bold)(String(tableNames.length))} tables, ${(0, ui_js_1.bold)(String(totalColumns))} columns, ${(0, ui_js_1.bold)(String(totalRelations))} relations`);
|
|
501
|
+
// Guard: zero tables means the generated client would be empty. That is almost
|
|
502
|
+
// always a misconfiguration (wrong `schema`, an include/exclude that filtered
|
|
503
|
+
// everything, or a database with no tables yet) rather than intent. Fail loudly
|
|
504
|
+
// instead of silently emitting an empty typed client.
|
|
505
|
+
if (tableNames.length === 0 && !args.allowEmpty) {
|
|
506
|
+
(0, ui_js_1.newline)();
|
|
507
|
+
(0, ui_js_1.error)(`Introspection matched 0 tables in schema ${(0, ui_js_1.cyan)(config.schema)} — refusing to generate an empty client.`);
|
|
508
|
+
(0, ui_js_1.newline)();
|
|
509
|
+
console.log(` ${(0, ui_js_1.dim)('Common causes:')}`);
|
|
510
|
+
console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.cyan)('schema')} ${(0, ui_js_1.dim)('points at the wrong Postgres namespace')} ${(0, ui_js_1.dim)('(it is the schema NAME, default')} ${(0, ui_js_1.cyan)('public')}${(0, ui_js_1.dim)(').')}`);
|
|
511
|
+
console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.dim)('You meant to set')} ${(0, ui_js_1.cyan)('schemaFile')} ${(0, ui_js_1.dim)('(your defineSchema() file), not')} ${(0, ui_js_1.cyan)('schema')}${(0, ui_js_1.dim)('.')}`);
|
|
512
|
+
console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.cyan)('include')}/${(0, ui_js_1.cyan)('exclude')} ${(0, ui_js_1.dim)('filtered out every table.')}`);
|
|
513
|
+
console.log(` ${(0, ui_js_1.dim)('•')} ${(0, ui_js_1.dim)('The database has no tables yet — run')} ${(0, ui_js_1.cyan)('turbine push')} ${(0, ui_js_1.dim)('or a migration first.')}`);
|
|
514
|
+
(0, ui_js_1.newline)();
|
|
515
|
+
console.log(` ${(0, ui_js_1.dim)('If an empty client is genuinely what you want, re-run with')} ${(0, ui_js_1.cyan)('--allow-empty')}${(0, ui_js_1.dim)('.')}`);
|
|
516
|
+
(0, ui_js_1.newline)();
|
|
517
|
+
process.exit(1);
|
|
518
|
+
}
|
|
462
519
|
// Print table summary
|
|
463
520
|
if (args.verbose) {
|
|
464
521
|
(0, ui_js_1.newline)();
|
|
@@ -1134,6 +1191,7 @@ function showGenerateHelp() {
|
|
|
1134
1191
|
console.log(` ${(0, ui_js_1.cyan)('--schema, -s')} ${(0, ui_js_1.dim)('<name>')} Postgres schema ${(0, ui_js_1.dim)('(default: public)')}`);
|
|
1135
1192
|
console.log(` ${(0, ui_js_1.cyan)('--include')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to include`);
|
|
1136
1193
|
console.log(` ${(0, ui_js_1.cyan)('--exclude')} ${(0, ui_js_1.dim)('<tables>')} Comma-separated tables to exclude`);
|
|
1194
|
+
console.log(` ${(0, ui_js_1.cyan)('--allow-empty')} Generate even when introspection matches 0 tables`);
|
|
1137
1195
|
(0, ui_js_1.newline)();
|
|
1138
1196
|
}
|
|
1139
1197
|
function showPushHelp() {
|
|
@@ -1342,6 +1400,8 @@ async function main() {
|
|
|
1342
1400
|
catch (err) {
|
|
1343
1401
|
if (args.command !== 'init') {
|
|
1344
1402
|
(0, ui_js_1.warn)(`Could not load config: ${err instanceof Error ? err.message : String(err)}`);
|
|
1403
|
+
if (err instanceof Error)
|
|
1404
|
+
printCjsHintIfApplicable(err);
|
|
1345
1405
|
}
|
|
1346
1406
|
}
|
|
1347
1407
|
const overrides = {
|
|
@@ -30,16 +30,28 @@ exports.table = table;
|
|
|
30
30
|
exports.applyManyToManyRelations = applyManyToManyRelations;
|
|
31
31
|
/** Maps shorthand names to actual Postgres type strings */
|
|
32
32
|
const TYPE_MAP = {
|
|
33
|
-
serial
|
|
33
|
+
// `serial` maps to SERIAL (int4). Its values fit in a JS `number` and pg
|
|
34
|
+
// returns them as numbers — so the generated `number` type is accurate.
|
|
35
|
+
// For 64-bit auto-increment keys use `bigserial` (int8), noting that values
|
|
36
|
+
// above Number.MAX_SAFE_INTEGER read back as string. (Changed in 0.24.0;
|
|
37
|
+
// `serial` previously emitted BIGSERIAL.)
|
|
38
|
+
serial: 'SERIAL',
|
|
39
|
+
bigserial: 'BIGSERIAL',
|
|
34
40
|
bigint: 'BIGINT',
|
|
35
41
|
integer: 'INTEGER',
|
|
36
42
|
smallint: 'SMALLINT',
|
|
37
43
|
text: 'TEXT',
|
|
38
44
|
varchar: 'VARCHAR',
|
|
39
45
|
boolean: 'BOOLEAN',
|
|
46
|
+
// `timestamp` is an honest alias for TIMESTAMPTZ (timezone-aware) — Turbine
|
|
47
|
+
// has always emitted TIMESTAMPTZ for it. `timestamptz` is the explicit spelling.
|
|
40
48
|
timestamp: 'TIMESTAMPTZ',
|
|
49
|
+
timestamptz: 'TIMESTAMPTZ',
|
|
41
50
|
date: 'DATE',
|
|
51
|
+
// `json` is an alias for JSONB (Turbine has always emitted JSONB). `jsonb`
|
|
52
|
+
// is the explicit spelling.
|
|
42
53
|
json: 'JSONB',
|
|
54
|
+
jsonb: 'JSONB',
|
|
43
55
|
uuid: 'UUID',
|
|
44
56
|
real: 'REAL',
|
|
45
57
|
double: 'DOUBLE PRECISION',
|
|
@@ -184,6 +196,11 @@ class ColumnBuilder {
|
|
|
184
196
|
};
|
|
185
197
|
}
|
|
186
198
|
serial() {
|
|
199
|
+
// See TYPE_MAP: `serial` is SERIAL (int4) as of 0.24.0. Use bigserial() for 64-bit.
|
|
200
|
+
this._config.type = 'SERIAL';
|
|
201
|
+
return this;
|
|
202
|
+
}
|
|
203
|
+
bigserial() {
|
|
187
204
|
this._config.type = 'BIGSERIAL';
|
|
188
205
|
return this;
|
|
189
206
|
}
|
|
@@ -216,6 +233,10 @@ class ColumnBuilder {
|
|
|
216
233
|
this._config.type = 'TIMESTAMPTZ';
|
|
217
234
|
return this;
|
|
218
235
|
}
|
|
236
|
+
timestamptz() {
|
|
237
|
+
this._config.type = 'TIMESTAMPTZ';
|
|
238
|
+
return this;
|
|
239
|
+
}
|
|
219
240
|
date() {
|
|
220
241
|
this._config.type = 'DATE';
|
|
221
242
|
return this;
|
|
@@ -224,6 +245,10 @@ class ColumnBuilder {
|
|
|
224
245
|
this._config.type = 'JSONB';
|
|
225
246
|
return this;
|
|
226
247
|
}
|
|
248
|
+
jsonb() {
|
|
249
|
+
this._config.type = 'JSONB';
|
|
250
|
+
return this;
|
|
251
|
+
}
|
|
227
252
|
uuid() {
|
|
228
253
|
this._config.type = 'UUID';
|
|
229
254
|
return this;
|
|
@@ -276,14 +301,17 @@ exports.ColumnBuilder = ColumnBuilder;
|
|
|
276
301
|
/** Type guard: is `prop` a known nullary ColumnBuilder type method? */
|
|
277
302
|
function isNullaryColumnType(prop) {
|
|
278
303
|
return (prop === 'serial' ||
|
|
304
|
+
prop === 'bigserial' ||
|
|
279
305
|
prop === 'bigint' ||
|
|
280
306
|
prop === 'integer' ||
|
|
281
307
|
prop === 'smallint' ||
|
|
282
308
|
prop === 'text' ||
|
|
283
309
|
prop === 'boolean' ||
|
|
284
310
|
prop === 'timestamp' ||
|
|
311
|
+
prop === 'timestamptz' ||
|
|
285
312
|
prop === 'date' ||
|
|
286
313
|
prop === 'json' ||
|
|
314
|
+
prop === 'jsonb' ||
|
|
287
315
|
prop === 'uuid' ||
|
|
288
316
|
prop === 'real' ||
|
|
289
317
|
prop === 'doublePrecision' ||
|
package/dist/cjs/schema-sql.js
CHANGED
|
@@ -16,6 +16,15 @@ exports.schemaToSQLString = schemaToSQLString;
|
|
|
16
16
|
const pg_1 = __importDefault(require("pg"));
|
|
17
17
|
const dialect_js_1 = require("./dialect.js");
|
|
18
18
|
const schema_js_1 = require("./schema.js");
|
|
19
|
+
/**
|
|
20
|
+
* Whether a resolved column type is an auto-increment pseudo-type (SERIAL /
|
|
21
|
+
* BIGSERIAL). These carry an implicit sequence default and NOT NULL, and their
|
|
22
|
+
* underlying integer width (int4 vs int8) is never auto-migrated by diff — a
|
|
23
|
+
* width change on a live PK is destructive and must be done by hand.
|
|
24
|
+
*/
|
|
25
|
+
function isSerialType(type) {
|
|
26
|
+
return type === 'SERIAL' || type === 'BIGSERIAL';
|
|
27
|
+
}
|
|
19
28
|
// ---------------------------------------------------------------------------
|
|
20
29
|
// SQL Generation — SchemaDef → CREATE TABLE statements
|
|
21
30
|
// ---------------------------------------------------------------------------
|
|
@@ -163,7 +172,7 @@ function generateColumnDef(fieldName, config, resolveRef, dialect = dialect_js_1
|
|
|
163
172
|
// 2. Is a serial (BIGSERIAL implies NOT NULL), OR
|
|
164
173
|
// 3. Has a primary key (PKs are NOT NULL)
|
|
165
174
|
// A column is left nullable if .nullable() was called.
|
|
166
|
-
const isSerial = config.type
|
|
175
|
+
const isSerial = isSerialType(config.type);
|
|
167
176
|
const implicitNotNull = isSerial || config.isPrimaryKey;
|
|
168
177
|
const notNull = config.isNotNull && !implicitNotNull;
|
|
169
178
|
// DEFAULT
|
|
@@ -352,9 +361,13 @@ async function schemaDiff(schema, connectionString) {
|
|
|
352
361
|
result.reverseStatements.unshift(reverseSql);
|
|
353
362
|
continue;
|
|
354
363
|
}
|
|
355
|
-
// Check type mismatch
|
|
364
|
+
// Check type mismatch. Serial/bigserial columns are exempt: their
|
|
365
|
+
// underlying int4/int8 width must never be auto-altered on a live table
|
|
366
|
+
// (a downcast on a PK loses data / breaks the sequence). This also
|
|
367
|
+
// preserves back-compat for DBs whose `serial` columns were created as
|
|
368
|
+
// BIGSERIAL (int8) before 0.24.0 — `push` won't try to shrink them.
|
|
356
369
|
const expectedUdt = schemaTypeToUdt(config);
|
|
357
|
-
if (expectedUdt && dbCol.udtName !== expectedUdt) {
|
|
370
|
+
if (expectedUdt && !isSerialType(config.type) && dbCol.udtName !== expectedUdt) {
|
|
358
371
|
const sqlType = config.type === 'VARCHAR' && config.maxLength ? `VARCHAR(${config.maxLength})` : config.type;
|
|
359
372
|
const oldSqlType = udtToSqlType(dbCol.udtName, dbCol.maxLength);
|
|
360
373
|
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} TYPE ${sqlType} USING ${dialect.quoteIdentifier(snakeName)}::${sqlType};`;
|
|
@@ -364,7 +377,7 @@ async function schemaDiff(schema, connectionString) {
|
|
|
364
377
|
result.reverseStatements.unshift(reverseSql);
|
|
365
378
|
}
|
|
366
379
|
// Check NOT NULL mismatch
|
|
367
|
-
const shouldBeNotNull = config.isNotNull || config.isPrimaryKey || config.type
|
|
380
|
+
const shouldBeNotNull = config.isNotNull || config.isPrimaryKey || isSerialType(config.type);
|
|
368
381
|
const isCurrentlyNullable = dbCol.isNullable;
|
|
369
382
|
if (shouldBeNotNull && isCurrentlyNullable && !config.isNullable) {
|
|
370
383
|
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET NOT NULL;`;
|
|
@@ -381,7 +394,7 @@ async function schemaDiff(schema, connectionString) {
|
|
|
381
394
|
result.reverseStatements.unshift(reverseSql);
|
|
382
395
|
}
|
|
383
396
|
// Check DEFAULT value mismatch
|
|
384
|
-
const isSerial = config.type
|
|
397
|
+
const isSerial = isSerialType(config.type);
|
|
385
398
|
if (!isSerial) {
|
|
386
399
|
const schemaDefault = config.defaultValue ? normalizeDefault(config.defaultValue) : null;
|
|
387
400
|
const dbDefault = dbCol.columnDefault;
|
|
@@ -460,6 +473,7 @@ async function schemaDiff(schema, connectionString) {
|
|
|
460
473
|
*/
|
|
461
474
|
function schemaTypeToUdt(config) {
|
|
462
475
|
const map = {
|
|
476
|
+
SERIAL: 'int4',
|
|
463
477
|
BIGSERIAL: 'int8',
|
|
464
478
|
BIGINT: 'int8',
|
|
465
479
|
INTEGER: 'int4',
|
package/dist/cjs/serverless.js
CHANGED
|
@@ -89,12 +89,29 @@ const client_js_1 = require("./client.js");
|
|
|
89
89
|
* manage its own `pg.Pool`. The caller retains ownership of the pool's
|
|
90
90
|
* lifecycle — `db.disconnect()` is a no-op.
|
|
91
91
|
*
|
|
92
|
+
* ## Typed table accessors
|
|
93
|
+
*
|
|
94
|
+
* By default `turbineHttp` returns the base {@link TurbineClient}, so you
|
|
95
|
+
* reach tables through `db.table('users')`. To get the *generated*, fully
|
|
96
|
+
* typed accessors (`db.users.findMany()`) — identical to what the TCP-path
|
|
97
|
+
* `turbine()` factory gives you — pass your generated client type as the
|
|
98
|
+
* `TClient` type argument. The runtime object is the same; the generated
|
|
99
|
+
* subclass only adds `declare readonly` accessor typings, and the base
|
|
100
|
+
* constructor already creates those accessors at runtime for every table in
|
|
101
|
+
* the schema, so the assertion is sound (not a lie about the shape).
|
|
102
|
+
*
|
|
103
|
+
* This closes the "identical typed code across transports" gap: the edge
|
|
104
|
+
* client is now as typed as the direct one, with no `as` casts at the call
|
|
105
|
+
* site.
|
|
106
|
+
*
|
|
107
|
+
* @typeParam TClient - The generated `TurbineClient` subclass (from
|
|
108
|
+
* `./generated/turbine`). Defaults to the base client for back-compat.
|
|
92
109
|
* @param pool - Any pg-compatible pool (Neon, Vercel Postgres, etc.)
|
|
93
110
|
* @param schema - Introspected or hand-written schema metadata
|
|
94
111
|
* @param options - Optional logging / defaultLimit / warnOnUnlimited
|
|
95
|
-
* @returns A TurbineClient instance
|
|
112
|
+
* @returns A TurbineClient instance (typed as `TClient`)
|
|
96
113
|
*
|
|
97
|
-
* @example
|
|
114
|
+
* @example Untyped (back-compat) — reach tables via `db.table(...)`
|
|
98
115
|
* ```ts
|
|
99
116
|
* import { Pool } from '@neondatabase/serverless';
|
|
100
117
|
* import { turbineHttp } from 'turbine-orm/serverless';
|
|
@@ -102,10 +119,25 @@ const client_js_1 = require("./client.js");
|
|
|
102
119
|
*
|
|
103
120
|
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
104
121
|
* const db = turbineHttp(pool, SCHEMA);
|
|
105
|
-
*
|
|
106
122
|
* const users = await db.table('users').findMany({ limit: 10 });
|
|
107
123
|
* ```
|
|
124
|
+
*
|
|
125
|
+
* @example Typed — generated accessors, identical to the TCP client
|
|
126
|
+
* ```ts
|
|
127
|
+
* import { Pool } from '@neondatabase/serverless';
|
|
128
|
+
* import { turbineHttp } from 'turbine-orm/serverless';
|
|
129
|
+
* import type { TurbineClient } from './generated/turbine';
|
|
130
|
+
* import { SCHEMA } from './generated/turbine/metadata.js';
|
|
131
|
+
*
|
|
132
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
133
|
+
* const db = turbineHttp<TurbineClient>(pool, SCHEMA);
|
|
134
|
+
* const users = await db.users.findMany({ limit: 10 }); // fully typed, no cast
|
|
135
|
+
* ```
|
|
108
136
|
*/
|
|
109
137
|
function turbineHttp(pool, schema, options = {}) {
|
|
138
|
+
// The generated subclass only layers `declare readonly` accessor typings
|
|
139
|
+
// over the base client; the base constructor materializes those same
|
|
140
|
+
// accessors at runtime (Object.defineProperty per schema table). So the
|
|
141
|
+
// returned instance genuinely has TClient's shape — the assertion is safe.
|
|
110
142
|
return new client_js_1.TurbineClient({ pool, ...options }, schema);
|
|
111
143
|
}
|
package/dist/cli/config.d.ts
CHANGED
|
@@ -33,6 +33,20 @@ export interface TurbineCliConfig {
|
|
|
33
33
|
*/
|
|
34
34
|
adapter?: import('../adapters/index.js').DatabaseAdapter;
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Alias for {@link TurbineCliConfig}. Some docs and examples import the config
|
|
38
|
+
* type as `TurbineConfig`; both names refer to the same shape.
|
|
39
|
+
*/
|
|
40
|
+
export type TurbineConfig = TurbineCliConfig;
|
|
41
|
+
/**
|
|
42
|
+
* Heuristic: does a configured `schema` value actually look like a schema FILE
|
|
43
|
+
* path rather than a Postgres schema name? `schema` is the Postgres namespace
|
|
44
|
+
* to introspect (default `public`); the schema-builder file goes in `schemaFile`.
|
|
45
|
+
* A value containing a path separator or a JS/TS extension is almost certainly a
|
|
46
|
+
* mis-set `schemaFile` — introspecting `WHERE table_schema = './turbine/schema.ts'`
|
|
47
|
+
* silently matches zero tables. Used by `turbine generate` to fail loudly.
|
|
48
|
+
*/
|
|
49
|
+
export declare function looksLikeSchemaFilePath(schema: string): boolean;
|
|
36
50
|
/**
|
|
37
51
|
* Attempt to load a turbine config file from the current directory.
|
|
38
52
|
* Returns the config if found, or an empty object.
|
package/dist/cli/config.js
CHANGED
|
@@ -7,6 +7,19 @@
|
|
|
7
7
|
import { existsSync } from 'node:fs';
|
|
8
8
|
import { join, resolve } from 'node:path';
|
|
9
9
|
import { pathToFileURL } from 'node:url';
|
|
10
|
+
/**
|
|
11
|
+
* Heuristic: does a configured `schema` value actually look like a schema FILE
|
|
12
|
+
* path rather than a Postgres schema name? `schema` is the Postgres namespace
|
|
13
|
+
* to introspect (default `public`); the schema-builder file goes in `schemaFile`.
|
|
14
|
+
* A value containing a path separator or a JS/TS extension is almost certainly a
|
|
15
|
+
* mis-set `schemaFile` — introspecting `WHERE table_schema = './turbine/schema.ts'`
|
|
16
|
+
* silently matches zero tables. Used by `turbine generate` to fail loudly.
|
|
17
|
+
*/
|
|
18
|
+
export function looksLikeSchemaFilePath(schema) {
|
|
19
|
+
if (!schema)
|
|
20
|
+
return false;
|
|
21
|
+
return schema.includes('/') || schema.includes('\\') || /\.(ts|mts|cts|js|mjs|cjs|json)$/i.test(schema);
|
|
22
|
+
}
|
|
10
23
|
// ---------------------------------------------------------------------------
|
|
11
24
|
// Config file names, in priority order
|
|
12
25
|
// ---------------------------------------------------------------------------
|
package/dist/cli/index.js
CHANGED
|
@@ -26,7 +26,7 @@ import { pathToFileURL } from 'node:url';
|
|
|
26
26
|
import { generate } from '../generate.js';
|
|
27
27
|
import { introspect } from '../introspect.js';
|
|
28
28
|
import { schemaDiff, schemaPush } from '../schema-sql.js';
|
|
29
|
-
import { configTemplate, findConfigFile, loadConfig, resolveConfig } from './config.js';
|
|
29
|
+
import { configTemplate, findConfigFile, loadConfig, looksLikeSchemaFilePath, resolveConfig } from './config.js';
|
|
30
30
|
import { canResolveTsx, getTsLoaderError, needsTsLoader, registerTsLoader } from './loader.js';
|
|
31
31
|
import { createMigration, listMigrationFiles, migrateDown, migrateStatus, migrateUp } from './migrate.js';
|
|
32
32
|
import { startObserve } from './observe.js';
|
|
@@ -85,6 +85,9 @@ function parseArgs() {
|
|
|
85
85
|
case '--allow-drift':
|
|
86
86
|
result.allowDrift = true;
|
|
87
87
|
break;
|
|
88
|
+
case '--allow-empty':
|
|
89
|
+
result.allowEmpty = true;
|
|
90
|
+
break;
|
|
88
91
|
case '--force':
|
|
89
92
|
case '-f':
|
|
90
93
|
result.force = true;
|
|
@@ -207,10 +210,28 @@ async function loadSchemaFile(schemaFile) {
|
|
|
207
210
|
newline();
|
|
208
211
|
console.log(` ${dim('Hint: install')} ${cyan('tsx')} ${dim('to load .ts files:')} ${cyan('npm install --save-dev tsx')}`);
|
|
209
212
|
}
|
|
213
|
+
printCjsHintIfApplicable(err);
|
|
210
214
|
}
|
|
211
215
|
process.exit(1);
|
|
212
216
|
}
|
|
213
217
|
}
|
|
218
|
+
/**
|
|
219
|
+
* When a config/schema import blows up with the CommonJS-vs-ESM interop error
|
|
220
|
+
* (`Cannot require() ES Module` / `ERR_REQUIRE_ESM`), the root cause is almost
|
|
221
|
+
* always a project whose `package.json` lacks `"type": "module"`: tsx transpiles
|
|
222
|
+
* the `.ts` file to CJS and then can't `require()` Turbine's ESM build. Point the
|
|
223
|
+
* user at the one-line fix instead of leaving them with a raw Node stack trace.
|
|
224
|
+
*/
|
|
225
|
+
function printCjsHintIfApplicable(err) {
|
|
226
|
+
const msg = err.message;
|
|
227
|
+
if (msg.includes('ERR_REQUIRE_ESM') ||
|
|
228
|
+
msg.includes('require() of ES Module') ||
|
|
229
|
+
msg.includes('Cannot require() ES Module')) {
|
|
230
|
+
newline();
|
|
231
|
+
console.log(` ${dim('Hint: add')} ${cyan('"type": "module"')} ${dim('to your')} ${cyan('package.json')}${dim('.')}`);
|
|
232
|
+
console.log(` ${dim('Turbine is an ESM package; without it, Node/tsx tries to')} ${cyan('require()')} ${dim('it and fails.')}`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
214
235
|
// ---------------------------------------------------------------------------
|
|
215
236
|
// Command: init
|
|
216
237
|
// ---------------------------------------------------------------------------
|
|
@@ -408,6 +429,24 @@ async function cmdGenerate(args, config) {
|
|
|
408
429
|
banner();
|
|
409
430
|
const url = requireUrl(config);
|
|
410
431
|
const startTime = performance.now();
|
|
432
|
+
// Guard: `schema` is the Postgres NAMESPACE to introspect (default `public`),
|
|
433
|
+
// NOT the path to your schema-builder file — that goes in `schemaFile`. If the
|
|
434
|
+
// configured `schema` looks like a file path, introspection would silently
|
|
435
|
+
// match zero tables and emit an empty client. Fail loudly instead.
|
|
436
|
+
if (!args.allowEmpty && looksLikeSchemaFilePath(config.schema)) {
|
|
437
|
+
error(`The "schema" option looks like a file path: ${cyan(config.schema)}`);
|
|
438
|
+
newline();
|
|
439
|
+
console.log(` ${dim('"schema" is the Postgres schema NAME to introspect')} ${dim('(default:')} ${cyan('public')}${dim(').')}`);
|
|
440
|
+
console.log(` ${dim('The path to your defineSchema() file belongs in')} ${cyan('schemaFile')}${dim('.')}`);
|
|
441
|
+
newline();
|
|
442
|
+
console.log(` ${dim('Fix your')} ${cyan('turbine.config.ts')}${dim(':')}`);
|
|
443
|
+
console.log(` ${green('schema:')} ${cyan("'public'")}${dim(", // or omit — introspects the 'public' schema")}`);
|
|
444
|
+
console.log(` ${green('schemaFile:')} ${cyan(`'${config.schema}'`)}${dim(', // your defineSchema() file (used by `turbine push`)')}`);
|
|
445
|
+
newline();
|
|
446
|
+
console.log(` ${dim('Re-run with')} ${cyan('--allow-empty')} ${dim('to introspect this literal schema name anyway.')}`);
|
|
447
|
+
newline();
|
|
448
|
+
process.exit(1);
|
|
449
|
+
}
|
|
411
450
|
label('Database', redactUrl(url));
|
|
412
451
|
label('Schema', config.schema);
|
|
413
452
|
label('Output', config.out);
|
|
@@ -424,6 +463,24 @@ async function cmdGenerate(args, config) {
|
|
|
424
463
|
const totalColumns = Object.values(schema.tables).reduce((sum, t) => sum + t.columns.length, 0);
|
|
425
464
|
const totalRelations = Object.values(schema.tables).reduce((sum, t) => sum + Object.keys(t.relations).length, 0);
|
|
426
465
|
spinner.succeed(`Found ${bold(String(tableNames.length))} tables, ${bold(String(totalColumns))} columns, ${bold(String(totalRelations))} relations`);
|
|
466
|
+
// Guard: zero tables means the generated client would be empty. That is almost
|
|
467
|
+
// always a misconfiguration (wrong `schema`, an include/exclude that filtered
|
|
468
|
+
// everything, or a database with no tables yet) rather than intent. Fail loudly
|
|
469
|
+
// instead of silently emitting an empty typed client.
|
|
470
|
+
if (tableNames.length === 0 && !args.allowEmpty) {
|
|
471
|
+
newline();
|
|
472
|
+
error(`Introspection matched 0 tables in schema ${cyan(config.schema)} — refusing to generate an empty client.`);
|
|
473
|
+
newline();
|
|
474
|
+
console.log(` ${dim('Common causes:')}`);
|
|
475
|
+
console.log(` ${dim('•')} ${cyan('schema')} ${dim('points at the wrong Postgres namespace')} ${dim('(it is the schema NAME, default')} ${cyan('public')}${dim(').')}`);
|
|
476
|
+
console.log(` ${dim('•')} ${dim('You meant to set')} ${cyan('schemaFile')} ${dim('(your defineSchema() file), not')} ${cyan('schema')}${dim('.')}`);
|
|
477
|
+
console.log(` ${dim('•')} ${cyan('include')}/${cyan('exclude')} ${dim('filtered out every table.')}`);
|
|
478
|
+
console.log(` ${dim('•')} ${dim('The database has no tables yet — run')} ${cyan('turbine push')} ${dim('or a migration first.')}`);
|
|
479
|
+
newline();
|
|
480
|
+
console.log(` ${dim('If an empty client is genuinely what you want, re-run with')} ${cyan('--allow-empty')}${dim('.')}`);
|
|
481
|
+
newline();
|
|
482
|
+
process.exit(1);
|
|
483
|
+
}
|
|
427
484
|
// Print table summary
|
|
428
485
|
if (args.verbose) {
|
|
429
486
|
newline();
|
|
@@ -1099,6 +1156,7 @@ function showGenerateHelp() {
|
|
|
1099
1156
|
console.log(` ${cyan('--schema, -s')} ${dim('<name>')} Postgres schema ${dim('(default: public)')}`);
|
|
1100
1157
|
console.log(` ${cyan('--include')} ${dim('<tables>')} Comma-separated tables to include`);
|
|
1101
1158
|
console.log(` ${cyan('--exclude')} ${dim('<tables>')} Comma-separated tables to exclude`);
|
|
1159
|
+
console.log(` ${cyan('--allow-empty')} Generate even when introspection matches 0 tables`);
|
|
1102
1160
|
newline();
|
|
1103
1161
|
}
|
|
1104
1162
|
function showPushHelp() {
|
|
@@ -1307,6 +1365,8 @@ async function main() {
|
|
|
1307
1365
|
catch (err) {
|
|
1308
1366
|
if (args.command !== 'init') {
|
|
1309
1367
|
warn(`Could not load config: ${err instanceof Error ? err.message : String(err)}`);
|
|
1368
|
+
if (err instanceof Error)
|
|
1369
|
+
printCjsHintIfApplicable(err);
|
|
1310
1370
|
}
|
|
1311
1371
|
}
|
|
1312
1372
|
const overrides = {
|
package/dist/schema-builder.d.ts
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import type { SchemaMetadata } from './schema.js';
|
|
26
26
|
/** Shorthand type names that map to Postgres column types */
|
|
27
|
-
export type ColumnTypeName = 'serial' | 'bigint' | 'integer' | 'smallint' | 'text' | 'varchar' | 'boolean' | 'timestamp' | 'date' | 'json' | 'uuid' | 'real' | 'double' | 'numeric' | 'bytea';
|
|
27
|
+
export type ColumnTypeName = 'serial' | 'bigserial' | 'bigint' | 'integer' | 'smallint' | 'text' | 'varchar' | 'boolean' | 'timestamp' | 'timestamptz' | 'date' | 'json' | 'jsonb' | 'uuid' | 'real' | 'double' | 'numeric' | 'bytea';
|
|
28
28
|
/** Column definition as a plain object. This is what users write. */
|
|
29
29
|
export interface ColumnDef {
|
|
30
30
|
/** Column type (required) */
|
|
@@ -45,7 +45,7 @@ export interface ColumnDef {
|
|
|
45
45
|
maxLength?: number;
|
|
46
46
|
}
|
|
47
47
|
/** Postgres-level column type (uppercase, as used in DDL) */
|
|
48
|
-
export type ColumnType = 'BIGSERIAL' | 'BIGINT' | 'INTEGER' | 'SMALLINT' | 'TEXT' | 'BOOLEAN' | 'TIMESTAMPTZ' | 'JSONB' | 'UUID' | 'REAL' | 'DOUBLE PRECISION' | 'NUMERIC' | 'BYTEA' | 'DATE' | 'VARCHAR';
|
|
48
|
+
export type ColumnType = 'SERIAL' | 'BIGSERIAL' | 'BIGINT' | 'INTEGER' | 'SMALLINT' | 'TEXT' | 'BOOLEAN' | 'TIMESTAMPTZ' | 'JSONB' | 'UUID' | 'REAL' | 'DOUBLE PRECISION' | 'NUMERIC' | 'BYTEA' | 'DATE' | 'VARCHAR';
|
|
49
49
|
export interface ColumnConfig {
|
|
50
50
|
type: ColumnType;
|
|
51
51
|
isPrimaryKey: boolean;
|
|
@@ -164,6 +164,7 @@ export declare class ColumnBuilder {
|
|
|
164
164
|
private _config;
|
|
165
165
|
constructor();
|
|
166
166
|
serial(): this;
|
|
167
|
+
bigserial(): this;
|
|
167
168
|
bigint(): this;
|
|
168
169
|
integer(): this;
|
|
169
170
|
smallint(): this;
|
|
@@ -171,8 +172,10 @@ export declare class ColumnBuilder {
|
|
|
171
172
|
varchar(length: number): this;
|
|
172
173
|
boolean(): this;
|
|
173
174
|
timestamp(): this;
|
|
175
|
+
timestamptz(): this;
|
|
174
176
|
date(): this;
|
|
175
177
|
json(): this;
|
|
178
|
+
jsonb(): this;
|
|
176
179
|
uuid(): this;
|
|
177
180
|
real(): this;
|
|
178
181
|
doublePrecision(): this;
|
|
@@ -188,7 +191,7 @@ export declare class ColumnBuilder {
|
|
|
188
191
|
}
|
|
189
192
|
/** @deprecated Use defineSchema() with plain objects instead */
|
|
190
193
|
type ColumnProxy = {
|
|
191
|
-
[K in 'serial' | 'bigint' | 'integer' | 'smallint' | 'text' | 'boolean' | 'timestamp' | 'date' | 'json' | 'uuid' | 'real' | 'doublePrecision' | 'numeric' | 'bytea']: () => ColumnBuilder;
|
|
194
|
+
[K in 'serial' | 'bigserial' | 'bigint' | 'integer' | 'smallint' | 'text' | 'boolean' | 'timestamp' | 'timestamptz' | 'date' | 'json' | 'jsonb' | 'uuid' | 'real' | 'doublePrecision' | 'numeric' | 'bytea']: () => ColumnBuilder;
|
|
192
195
|
} & {
|
|
193
196
|
varchar: (length: number) => ColumnBuilder;
|
|
194
197
|
};
|
package/dist/schema-builder.js
CHANGED
|
@@ -24,16 +24,28 @@
|
|
|
24
24
|
*/
|
|
25
25
|
/** Maps shorthand names to actual Postgres type strings */
|
|
26
26
|
const TYPE_MAP = {
|
|
27
|
-
serial
|
|
27
|
+
// `serial` maps to SERIAL (int4). Its values fit in a JS `number` and pg
|
|
28
|
+
// returns them as numbers — so the generated `number` type is accurate.
|
|
29
|
+
// For 64-bit auto-increment keys use `bigserial` (int8), noting that values
|
|
30
|
+
// above Number.MAX_SAFE_INTEGER read back as string. (Changed in 0.24.0;
|
|
31
|
+
// `serial` previously emitted BIGSERIAL.)
|
|
32
|
+
serial: 'SERIAL',
|
|
33
|
+
bigserial: 'BIGSERIAL',
|
|
28
34
|
bigint: 'BIGINT',
|
|
29
35
|
integer: 'INTEGER',
|
|
30
36
|
smallint: 'SMALLINT',
|
|
31
37
|
text: 'TEXT',
|
|
32
38
|
varchar: 'VARCHAR',
|
|
33
39
|
boolean: 'BOOLEAN',
|
|
40
|
+
// `timestamp` is an honest alias for TIMESTAMPTZ (timezone-aware) — Turbine
|
|
41
|
+
// has always emitted TIMESTAMPTZ for it. `timestamptz` is the explicit spelling.
|
|
34
42
|
timestamp: 'TIMESTAMPTZ',
|
|
43
|
+
timestamptz: 'TIMESTAMPTZ',
|
|
35
44
|
date: 'DATE',
|
|
45
|
+
// `json` is an alias for JSONB (Turbine has always emitted JSONB). `jsonb`
|
|
46
|
+
// is the explicit spelling.
|
|
36
47
|
json: 'JSONB',
|
|
48
|
+
jsonb: 'JSONB',
|
|
37
49
|
uuid: 'UUID',
|
|
38
50
|
real: 'REAL',
|
|
39
51
|
double: 'DOUBLE PRECISION',
|
|
@@ -178,6 +190,11 @@ export class ColumnBuilder {
|
|
|
178
190
|
};
|
|
179
191
|
}
|
|
180
192
|
serial() {
|
|
193
|
+
// See TYPE_MAP: `serial` is SERIAL (int4) as of 0.24.0. Use bigserial() for 64-bit.
|
|
194
|
+
this._config.type = 'SERIAL';
|
|
195
|
+
return this;
|
|
196
|
+
}
|
|
197
|
+
bigserial() {
|
|
181
198
|
this._config.type = 'BIGSERIAL';
|
|
182
199
|
return this;
|
|
183
200
|
}
|
|
@@ -210,6 +227,10 @@ export class ColumnBuilder {
|
|
|
210
227
|
this._config.type = 'TIMESTAMPTZ';
|
|
211
228
|
return this;
|
|
212
229
|
}
|
|
230
|
+
timestamptz() {
|
|
231
|
+
this._config.type = 'TIMESTAMPTZ';
|
|
232
|
+
return this;
|
|
233
|
+
}
|
|
213
234
|
date() {
|
|
214
235
|
this._config.type = 'DATE';
|
|
215
236
|
return this;
|
|
@@ -218,6 +239,10 @@ export class ColumnBuilder {
|
|
|
218
239
|
this._config.type = 'JSONB';
|
|
219
240
|
return this;
|
|
220
241
|
}
|
|
242
|
+
jsonb() {
|
|
243
|
+
this._config.type = 'JSONB';
|
|
244
|
+
return this;
|
|
245
|
+
}
|
|
221
246
|
uuid() {
|
|
222
247
|
this._config.type = 'UUID';
|
|
223
248
|
return this;
|
|
@@ -269,14 +294,17 @@ export class ColumnBuilder {
|
|
|
269
294
|
/** Type guard: is `prop` a known nullary ColumnBuilder type method? */
|
|
270
295
|
function isNullaryColumnType(prop) {
|
|
271
296
|
return (prop === 'serial' ||
|
|
297
|
+
prop === 'bigserial' ||
|
|
272
298
|
prop === 'bigint' ||
|
|
273
299
|
prop === 'integer' ||
|
|
274
300
|
prop === 'smallint' ||
|
|
275
301
|
prop === 'text' ||
|
|
276
302
|
prop === 'boolean' ||
|
|
277
303
|
prop === 'timestamp' ||
|
|
304
|
+
prop === 'timestamptz' ||
|
|
278
305
|
prop === 'date' ||
|
|
279
306
|
prop === 'json' ||
|
|
307
|
+
prop === 'jsonb' ||
|
|
280
308
|
prop === 'uuid' ||
|
|
281
309
|
prop === 'real' ||
|
|
282
310
|
prop === 'doublePrecision' ||
|
package/dist/schema-sql.js
CHANGED
|
@@ -7,6 +7,15 @@
|
|
|
7
7
|
import pg from 'pg';
|
|
8
8
|
import { postgresDialect } from './dialect.js';
|
|
9
9
|
import { camelToSnake } from './schema.js';
|
|
10
|
+
/**
|
|
11
|
+
* Whether a resolved column type is an auto-increment pseudo-type (SERIAL /
|
|
12
|
+
* BIGSERIAL). These carry an implicit sequence default and NOT NULL, and their
|
|
13
|
+
* underlying integer width (int4 vs int8) is never auto-migrated by diff — a
|
|
14
|
+
* width change on a live PK is destructive and must be done by hand.
|
|
15
|
+
*/
|
|
16
|
+
function isSerialType(type) {
|
|
17
|
+
return type === 'SERIAL' || type === 'BIGSERIAL';
|
|
18
|
+
}
|
|
10
19
|
// ---------------------------------------------------------------------------
|
|
11
20
|
// SQL Generation — SchemaDef → CREATE TABLE statements
|
|
12
21
|
// ---------------------------------------------------------------------------
|
|
@@ -154,7 +163,7 @@ function generateColumnDef(fieldName, config, resolveRef, dialect = postgresDial
|
|
|
154
163
|
// 2. Is a serial (BIGSERIAL implies NOT NULL), OR
|
|
155
164
|
// 3. Has a primary key (PKs are NOT NULL)
|
|
156
165
|
// A column is left nullable if .nullable() was called.
|
|
157
|
-
const isSerial = config.type
|
|
166
|
+
const isSerial = isSerialType(config.type);
|
|
158
167
|
const implicitNotNull = isSerial || config.isPrimaryKey;
|
|
159
168
|
const notNull = config.isNotNull && !implicitNotNull;
|
|
160
169
|
// DEFAULT
|
|
@@ -343,9 +352,13 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
343
352
|
result.reverseStatements.unshift(reverseSql);
|
|
344
353
|
continue;
|
|
345
354
|
}
|
|
346
|
-
// Check type mismatch
|
|
355
|
+
// Check type mismatch. Serial/bigserial columns are exempt: their
|
|
356
|
+
// underlying int4/int8 width must never be auto-altered on a live table
|
|
357
|
+
// (a downcast on a PK loses data / breaks the sequence). This also
|
|
358
|
+
// preserves back-compat for DBs whose `serial` columns were created as
|
|
359
|
+
// BIGSERIAL (int8) before 0.24.0 — `push` won't try to shrink them.
|
|
347
360
|
const expectedUdt = schemaTypeToUdt(config);
|
|
348
|
-
if (expectedUdt && dbCol.udtName !== expectedUdt) {
|
|
361
|
+
if (expectedUdt && !isSerialType(config.type) && dbCol.udtName !== expectedUdt) {
|
|
349
362
|
const sqlType = config.type === 'VARCHAR' && config.maxLength ? `VARCHAR(${config.maxLength})` : config.type;
|
|
350
363
|
const oldSqlType = udtToSqlType(dbCol.udtName, dbCol.maxLength);
|
|
351
364
|
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} TYPE ${sqlType} USING ${dialect.quoteIdentifier(snakeName)}::${sqlType};`;
|
|
@@ -355,7 +368,7 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
355
368
|
result.reverseStatements.unshift(reverseSql);
|
|
356
369
|
}
|
|
357
370
|
// Check NOT NULL mismatch
|
|
358
|
-
const shouldBeNotNull = config.isNotNull || config.isPrimaryKey || config.type
|
|
371
|
+
const shouldBeNotNull = config.isNotNull || config.isPrimaryKey || isSerialType(config.type);
|
|
359
372
|
const isCurrentlyNullable = dbCol.isNullable;
|
|
360
373
|
if (shouldBeNotNull && isCurrentlyNullable && !config.isNullable) {
|
|
361
374
|
const sql = `ALTER TABLE ${dialect.quoteIdentifier(tableName)} ALTER COLUMN ${dialect.quoteIdentifier(snakeName)} SET NOT NULL;`;
|
|
@@ -372,7 +385,7 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
372
385
|
result.reverseStatements.unshift(reverseSql);
|
|
373
386
|
}
|
|
374
387
|
// Check DEFAULT value mismatch
|
|
375
|
-
const isSerial = config.type
|
|
388
|
+
const isSerial = isSerialType(config.type);
|
|
376
389
|
if (!isSerial) {
|
|
377
390
|
const schemaDefault = config.defaultValue ? normalizeDefault(config.defaultValue) : null;
|
|
378
391
|
const dbDefault = dbCol.columnDefault;
|
|
@@ -451,6 +464,7 @@ export async function schemaDiff(schema, connectionString) {
|
|
|
451
464
|
*/
|
|
452
465
|
function schemaTypeToUdt(config) {
|
|
453
466
|
const map = {
|
|
467
|
+
SERIAL: 'int4',
|
|
454
468
|
BIGSERIAL: 'int8',
|
|
455
469
|
BIGINT: 'int8',
|
|
456
470
|
INTEGER: 'int4',
|
package/dist/serverless.d.ts
CHANGED
|
@@ -94,12 +94,29 @@ export interface TurbineHttpOptions extends Pick<TurbineConfig, 'logging' | 'def
|
|
|
94
94
|
* manage its own `pg.Pool`. The caller retains ownership of the pool's
|
|
95
95
|
* lifecycle — `db.disconnect()` is a no-op.
|
|
96
96
|
*
|
|
97
|
+
* ## Typed table accessors
|
|
98
|
+
*
|
|
99
|
+
* By default `turbineHttp` returns the base {@link TurbineClient}, so you
|
|
100
|
+
* reach tables through `db.table('users')`. To get the *generated*, fully
|
|
101
|
+
* typed accessors (`db.users.findMany()`) — identical to what the TCP-path
|
|
102
|
+
* `turbine()` factory gives you — pass your generated client type as the
|
|
103
|
+
* `TClient` type argument. The runtime object is the same; the generated
|
|
104
|
+
* subclass only adds `declare readonly` accessor typings, and the base
|
|
105
|
+
* constructor already creates those accessors at runtime for every table in
|
|
106
|
+
* the schema, so the assertion is sound (not a lie about the shape).
|
|
107
|
+
*
|
|
108
|
+
* This closes the "identical typed code across transports" gap: the edge
|
|
109
|
+
* client is now as typed as the direct one, with no `as` casts at the call
|
|
110
|
+
* site.
|
|
111
|
+
*
|
|
112
|
+
* @typeParam TClient - The generated `TurbineClient` subclass (from
|
|
113
|
+
* `./generated/turbine`). Defaults to the base client for back-compat.
|
|
97
114
|
* @param pool - Any pg-compatible pool (Neon, Vercel Postgres, etc.)
|
|
98
115
|
* @param schema - Introspected or hand-written schema metadata
|
|
99
116
|
* @param options - Optional logging / defaultLimit / warnOnUnlimited
|
|
100
|
-
* @returns A TurbineClient instance
|
|
117
|
+
* @returns A TurbineClient instance (typed as `TClient`)
|
|
101
118
|
*
|
|
102
|
-
* @example
|
|
119
|
+
* @example Untyped (back-compat) — reach tables via `db.table(...)`
|
|
103
120
|
* ```ts
|
|
104
121
|
* import { Pool } from '@neondatabase/serverless';
|
|
105
122
|
* import { turbineHttp } from 'turbine-orm/serverless';
|
|
@@ -107,8 +124,19 @@ export interface TurbineHttpOptions extends Pick<TurbineConfig, 'logging' | 'def
|
|
|
107
124
|
*
|
|
108
125
|
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
109
126
|
* const db = turbineHttp(pool, SCHEMA);
|
|
110
|
-
*
|
|
111
127
|
* const users = await db.table('users').findMany({ limit: 10 });
|
|
112
128
|
* ```
|
|
129
|
+
*
|
|
130
|
+
* @example Typed — generated accessors, identical to the TCP client
|
|
131
|
+
* ```ts
|
|
132
|
+
* import { Pool } from '@neondatabase/serverless';
|
|
133
|
+
* import { turbineHttp } from 'turbine-orm/serverless';
|
|
134
|
+
* import type { TurbineClient } from './generated/turbine';
|
|
135
|
+
* import { SCHEMA } from './generated/turbine/metadata.js';
|
|
136
|
+
*
|
|
137
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
138
|
+
* const db = turbineHttp<TurbineClient>(pool, SCHEMA);
|
|
139
|
+
* const users = await db.users.findMany({ limit: 10 }); // fully typed, no cast
|
|
140
|
+
* ```
|
|
113
141
|
*/
|
|
114
|
-
export declare function turbineHttp(pool: PgCompatPool, schema: SchemaMetadata, options?: TurbineHttpOptions):
|
|
142
|
+
export declare function turbineHttp<TClient extends TurbineClient = TurbineClient>(pool: PgCompatPool, schema: SchemaMetadata, options?: TurbineHttpOptions): TClient;
|
package/dist/serverless.js
CHANGED
|
@@ -86,12 +86,29 @@ import { TurbineClient } from './client.js';
|
|
|
86
86
|
* manage its own `pg.Pool`. The caller retains ownership of the pool's
|
|
87
87
|
* lifecycle — `db.disconnect()` is a no-op.
|
|
88
88
|
*
|
|
89
|
+
* ## Typed table accessors
|
|
90
|
+
*
|
|
91
|
+
* By default `turbineHttp` returns the base {@link TurbineClient}, so you
|
|
92
|
+
* reach tables through `db.table('users')`. To get the *generated*, fully
|
|
93
|
+
* typed accessors (`db.users.findMany()`) — identical to what the TCP-path
|
|
94
|
+
* `turbine()` factory gives you — pass your generated client type as the
|
|
95
|
+
* `TClient` type argument. The runtime object is the same; the generated
|
|
96
|
+
* subclass only adds `declare readonly` accessor typings, and the base
|
|
97
|
+
* constructor already creates those accessors at runtime for every table in
|
|
98
|
+
* the schema, so the assertion is sound (not a lie about the shape).
|
|
99
|
+
*
|
|
100
|
+
* This closes the "identical typed code across transports" gap: the edge
|
|
101
|
+
* client is now as typed as the direct one, with no `as` casts at the call
|
|
102
|
+
* site.
|
|
103
|
+
*
|
|
104
|
+
* @typeParam TClient - The generated `TurbineClient` subclass (from
|
|
105
|
+
* `./generated/turbine`). Defaults to the base client for back-compat.
|
|
89
106
|
* @param pool - Any pg-compatible pool (Neon, Vercel Postgres, etc.)
|
|
90
107
|
* @param schema - Introspected or hand-written schema metadata
|
|
91
108
|
* @param options - Optional logging / defaultLimit / warnOnUnlimited
|
|
92
|
-
* @returns A TurbineClient instance
|
|
109
|
+
* @returns A TurbineClient instance (typed as `TClient`)
|
|
93
110
|
*
|
|
94
|
-
* @example
|
|
111
|
+
* @example Untyped (back-compat) — reach tables via `db.table(...)`
|
|
95
112
|
* ```ts
|
|
96
113
|
* import { Pool } from '@neondatabase/serverless';
|
|
97
114
|
* import { turbineHttp } from 'turbine-orm/serverless';
|
|
@@ -99,10 +116,25 @@ import { TurbineClient } from './client.js';
|
|
|
99
116
|
*
|
|
100
117
|
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
101
118
|
* const db = turbineHttp(pool, SCHEMA);
|
|
102
|
-
*
|
|
103
119
|
* const users = await db.table('users').findMany({ limit: 10 });
|
|
104
120
|
* ```
|
|
121
|
+
*
|
|
122
|
+
* @example Typed — generated accessors, identical to the TCP client
|
|
123
|
+
* ```ts
|
|
124
|
+
* import { Pool } from '@neondatabase/serverless';
|
|
125
|
+
* import { turbineHttp } from 'turbine-orm/serverless';
|
|
126
|
+
* import type { TurbineClient } from './generated/turbine';
|
|
127
|
+
* import { SCHEMA } from './generated/turbine/metadata.js';
|
|
128
|
+
*
|
|
129
|
+
* const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
130
|
+
* const db = turbineHttp<TurbineClient>(pool, SCHEMA);
|
|
131
|
+
* const users = await db.users.findMany({ limit: 10 }); // fully typed, no cast
|
|
132
|
+
* ```
|
|
105
133
|
*/
|
|
106
134
|
export function turbineHttp(pool, schema, options = {}) {
|
|
135
|
+
// The generated subclass only layers `declare readonly` accessor typings
|
|
136
|
+
// over the base client; the base constructor materializes those same
|
|
137
|
+
// accessors at runtime (Object.defineProperty per schema table). So the
|
|
138
|
+
// returned instance genuinely has TClient's shape — the assertion is safe.
|
|
107
139
|
return new TurbineClient({ pool, ...options }, schema);
|
|
108
140
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Postgres-native TypeScript ORM — runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|