turbine-orm 0.33.0 → 0.35.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 +2 -2
- package/dist/cjs/client.js +26 -4
- package/dist/cjs/dialect.js +1 -0
- package/dist/cjs/errors.js +41 -1
- package/dist/cjs/index-advisor.js +0 -0
- package/dist/cjs/index.js +4 -2
- package/dist/cjs/mssql.js +5 -0
- package/dist/cjs/mysql.js +4 -0
- package/dist/cjs/optional-peer-import.cjs +28 -0
- package/dist/cjs/powdb-introspect.js +222 -0
- package/dist/cjs/powdb.js +592 -72
- package/dist/cjs/powql.js +998 -134
- package/dist/cjs/query/builder.js +72 -1
- package/dist/cjs/schema-builder.js +16 -0
- package/dist/cjs/schema-metadata.js +81 -10
- package/dist/cjs/sqlite.js +3 -0
- package/dist/client.d.ts +32 -5
- package/dist/client.js +26 -4
- package/dist/dialect.d.ts +13 -0
- package/dist/dialect.js +1 -0
- package/dist/errors.d.ts +36 -0
- package/dist/errors.js +39 -0
- package/dist/index-advisor.d.ts +15 -1
- package/dist/index-advisor.js +0 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/mssql.js +5 -0
- package/dist/mysql.js +4 -0
- package/dist/optional-peer-import.cjs +28 -0
- package/dist/optional-peer-import.d.cts +19 -0
- package/dist/powdb-introspect.d.ts +84 -0
- package/dist/powdb-introspect.js +219 -0
- package/dist/powdb.d.ts +361 -19
- package/dist/powdb.js +585 -72
- package/dist/powql.d.ts +245 -8
- package/dist/powql.js +1001 -137
- package/dist/query/builder.d.ts +36 -1
- package/dist/query/builder.js +72 -1
- package/dist/query/deferred.d.ts +6 -2
- package/dist/query/types.d.ts +49 -12
- package/dist/schema-builder.d.ts +46 -1
- package/dist/schema-builder.js +15 -0
- package/dist/schema-metadata.d.ts +13 -7
- package/dist/schema-metadata.js +82 -11
- package/dist/schema.d.ts +25 -0
- package/dist/sqlite.js +3 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -863,7 +863,7 @@ const db = await turbineMssql('mssql://sa:Passw0rd!@localhost:1433/app', SCHEMA)
|
|
|
863
863
|
```
|
|
864
864
|
|
|
865
865
|
```ts
|
|
866
|
-
// PowDB — async; embedded (in-process) or networked. Schema is code-defined
|
|
866
|
+
// PowDB — async; embedded (in-process) or networked. Schema is code-defined.
|
|
867
867
|
import { turbinePowDB } from 'turbine-orm/powdb';
|
|
868
868
|
import { schema } from './schema.js'; // defineSchema({...})
|
|
869
869
|
|
|
@@ -891,7 +891,7 @@ Everything is honest about what ports and what doesn't. Features marked **PG-onl
|
|
|
891
891
|
|
|
892
892
|
**Engine notes:** SQLite uses `RETURNING` (≥ 3.35) just like Postgres. MySQL has no `RETURNING`, so writes re-`SELECT` the affected row and **`createMany` returns `[]`** (the rows ARE inserted — re-query if you need them). SQL Server returns rows via `OUTPUT`/`MERGE`; `DISTINCT ON` is Postgres-only. Only Postgres streams via a true cursor (constant memory); the other engines' `findManyStream` materializes the result then yields it in batches. Optimistic locking throws `OptimisticLockError` on all engines (on MySQL the conflict is detected from the version-checked UPDATE's affected-row count). The `turbine` CLI (`generate`, `migrate`) is currently PostgreSQL-only — point the engine factories at a hand-written or programmatically introspected `SCHEMA`.
|
|
893
893
|
|
|
894
|
-
**PowDB** speaks its own non-SQL query language (PowQL), so it sits outside the SQL matrix above. Writes use a trailing **`returning`** keyword (upsert reselects by PK). PKs are server-assigned `auto` ints **or** client UUIDs. Nested relations load client-side (N+1, including many-to-many via the junction — no `json_agg`). Nested writes cover hasMany/hasOne/belongsTo; many-to-many nested writes are not supported. Transactions are single-writer: concurrent `$transaction` calls queue FIFO (bounded by `transactionQueueTimeoutMs`); nested/re-entrant transactions throw typed errors (no savepoints). Schema is code-first via `defineSchema`
|
|
894
|
+
**PowDB** speaks its own non-SQL query language (PowQL), so it sits outside the SQL matrix above. Writes use a trailing **`returning`** keyword (upsert reselects by PK). PKs are server-assigned `auto` ints **or** client UUIDs. Nested relations load client-side (N+1, including many-to-many via the junction — no `json_agg`). Nested writes cover hasMany/hasOne/belongsTo; many-to-many nested writes are not supported. Transactions are single-writer: concurrent `$transaction` calls queue FIFO (bounded by `transactionQueueTimeoutMs`); nested/re-entrant transactions throw typed errors (no savepoints). Schema is code-first via `defineSchema` — `schemaDefToMetadata()` bridges it to any engine that needs runtime metadata, and a programmatic `describe`-based introspector exists since 0.34 (relations excluded). JSON documents are first-class on engine 0.12+: `JsonFilter` where-filters, JSON-path `orderBy`/`groupBy`, doc-field expression indexes, and a lossless native wire (0.13+) that keeps JSON `null`, missing fields, and the string `"null"` distinct. Embedded `syncMode: 'normal'` moves fsync off the commit path; the networked transport runs the same data over a socket. Cursor streaming and the Postgres-only trio (pgvector / LISTEN/NOTIFY / RLS session GUCs) throw `UnsupportedFeatureError`. Full details: **[turbineorm.dev/engines#powdb](https://turbineorm.dev/engines#powdb)**.
|
|
895
895
|
|
|
896
896
|
Full setup, signatures, and the complete support matrix: **[turbineorm.dev/engines](https://turbineorm.dev/engines)**.
|
|
897
897
|
|
package/dist/cjs/client.js
CHANGED
|
@@ -111,15 +111,27 @@ class TransactionClient {
|
|
|
111
111
|
schema;
|
|
112
112
|
middlewares;
|
|
113
113
|
queryOptions;
|
|
114
|
+
sourcePool;
|
|
114
115
|
tableCache = new Map();
|
|
115
116
|
savepointCounter = 0;
|
|
116
117
|
/** Active SQL dialect — owns savepoint keywords and raw-SQL placeholders. */
|
|
117
118
|
dialect;
|
|
118
|
-
constructor(client, schema, middlewares, queryOptions
|
|
119
|
+
constructor(client, schema, middlewares, queryOptions,
|
|
120
|
+
/**
|
|
121
|
+
* The parent pool this transaction runs on. Only its `readonly` and
|
|
122
|
+
* `capabilities` are read (both PowDB-only flags), so the transaction-scoped
|
|
123
|
+
* proxy pool built by {@link createTxPool} carries them through: without this
|
|
124
|
+
* a read-only client's `$transaction` writes bypass the E018 guard, and an
|
|
125
|
+
* older-engine client falls back to ALL_POWDB_CAPABILITIES inside the tx
|
|
126
|
+
* (emitting join PowQL a pre-0.13 engine rejects). Undefined / absent flags
|
|
127
|
+
* for a plain pg pool leave the proxy unchanged.
|
|
128
|
+
*/
|
|
129
|
+
sourcePool) {
|
|
119
130
|
this.client = client;
|
|
120
131
|
this.schema = schema;
|
|
121
132
|
this.middlewares = middlewares;
|
|
122
133
|
this.queryOptions = queryOptions;
|
|
134
|
+
this.sourcePool = sourcePool;
|
|
123
135
|
this.dialect = queryOptions?.dialect ?? dialect_js_1.postgresDialect;
|
|
124
136
|
// Auto-create typed table accessors for all tables in the schema
|
|
125
137
|
for (const tableName of Object.keys(schema.tables)) {
|
|
@@ -199,7 +211,7 @@ class TransactionClient {
|
|
|
199
211
|
const client = this.client;
|
|
200
212
|
// Return a minimal pool-compatible object that routes queries
|
|
201
213
|
// through the transaction client
|
|
202
|
-
|
|
214
|
+
const txPool = {
|
|
203
215
|
query: async (textOrConfig, values) => {
|
|
204
216
|
try {
|
|
205
217
|
if (typeof textOrConfig === 'string') {
|
|
@@ -216,6 +228,14 @@ class TransactionClient {
|
|
|
216
228
|
},
|
|
217
229
|
connect: () => Promise.resolve(client),
|
|
218
230
|
};
|
|
231
|
+
// Carry the parent pool's PowDB-only flags through so a transaction-scoped
|
|
232
|
+
// PowqlInterface reads the same read-only guard and capabilities it would
|
|
233
|
+
// outside the transaction (a plain pg pool has neither, so nothing changes).
|
|
234
|
+
if (this.sourcePool?.readonly !== undefined)
|
|
235
|
+
txPool.readonly = this.sourcePool.readonly;
|
|
236
|
+
if (this.sourcePool?.capabilities !== undefined)
|
|
237
|
+
txPool.capabilities = this.sourcePool.capabilities;
|
|
238
|
+
return txPool;
|
|
219
239
|
}
|
|
220
240
|
}
|
|
221
241
|
exports.TransactionClient = TransactionClient;
|
|
@@ -884,8 +904,10 @@ class TurbineClient {
|
|
|
884
904
|
await client.query(cfg.sql, cfg.params);
|
|
885
905
|
}
|
|
886
906
|
}
|
|
887
|
-
// Create the transaction client with typed table accessors
|
|
888
|
-
|
|
907
|
+
// Create the transaction client with typed table accessors. Pass the
|
|
908
|
+
// parent pool so its read-only guard + PowDB capabilities flow into the
|
|
909
|
+
// transaction-scoped proxy pool (see TransactionClient.createTxPool).
|
|
910
|
+
const tx = new TransactionClient(client, this.schema, this.middlewares, this.queryOptions, this.pool);
|
|
889
911
|
// Dynamically attach table accessors to tx
|
|
890
912
|
for (const tableName of Object.keys(this.schema.tables)) {
|
|
891
913
|
const camelName = tableName.replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
package/dist/cjs/dialect.js
CHANGED
package/dist/cjs/errors.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* All Turbine errors extend TurbineError which includes a `code` property.
|
|
7
7
|
*/
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
-
exports.UnsupportedFeatureError = exports.OptimisticLockError = exports.PipelineError = exports.ExclusionConstraintError = exports.CheckConstraintError = exports.SerializationFailureError = exports.DeadlockError = exports.NotNullViolationError = exports.ForeignKeyError = exports.UniqueConstraintError = exports.CircularRelationError = exports.MigrationError = exports.RelationError = exports.ConnectionError = exports.ValidationError = exports.TimeoutError = exports.NotFoundError = exports.TurbineError = exports.TurbineErrorCode = void 0;
|
|
9
|
+
exports.ReadOnlyError = exports.UnsupportedFeatureError = exports.OptimisticLockError = exports.PipelineError = exports.ExclusionConstraintError = exports.CheckConstraintError = exports.SerializationFailureError = exports.DeadlockError = exports.NotNullViolationError = exports.ForeignKeyError = exports.UniqueConstraintError = exports.CircularRelationError = exports.MigrationError = exports.RelationError = exports.ConnectionError = exports.ValidationError = exports.TimeoutError = exports.NotFoundError = exports.TurbineError = exports.TurbineErrorCode = void 0;
|
|
10
10
|
exports.setErrorMessageMode = setErrorMessageMode;
|
|
11
11
|
exports.getErrorMessageMode = getErrorMessageMode;
|
|
12
12
|
exports.wrapPgError = wrapPgError;
|
|
@@ -29,6 +29,7 @@ exports.TurbineErrorCode = {
|
|
|
29
29
|
OPTIMISTIC_LOCK: 'TURBINE_E015',
|
|
30
30
|
EXCLUSION_VIOLATION: 'TURBINE_E016',
|
|
31
31
|
UNSUPPORTED_FEATURE: 'TURBINE_E017',
|
|
32
|
+
READ_ONLY: 'TURBINE_E018',
|
|
32
33
|
};
|
|
33
34
|
/**
|
|
34
35
|
* Prefix a human message with its stable error code so logs are greppable
|
|
@@ -503,6 +504,45 @@ class UnsupportedFeatureError extends TurbineError {
|
|
|
503
504
|
}
|
|
504
505
|
}
|
|
505
506
|
exports.UnsupportedFeatureError = UnsupportedFeatureError;
|
|
507
|
+
/**
|
|
508
|
+
* Thrown when a write or DDL statement is refused because the target is
|
|
509
|
+
* read-only. Two shapes reach here, both on PowDB:
|
|
510
|
+
* - an embedded database opened read-only for snapshot serving refuses a write
|
|
511
|
+
* with `readonly mode: statement requires a writer …`;
|
|
512
|
+
* - a networked read-only role refuses a write with `permission denied: role
|
|
513
|
+
* '<role>' cannot execute write statements` (translated by `wrapPowdbError`).
|
|
514
|
+
* It is also raised locally, before the wire, when a write is issued on a pool
|
|
515
|
+
* the caller marked read-only (fail-fast). The message carries the engine text
|
|
516
|
+
* plus a hint to route writes to a writable primary.
|
|
517
|
+
*
|
|
518
|
+
* NOT retryable: the same write against the same read-only target fails
|
|
519
|
+
* identically; route it to a writable primary instead.
|
|
520
|
+
*/
|
|
521
|
+
class ReadOnlyError extends TurbineError {
|
|
522
|
+
/**
|
|
523
|
+
* Why the write was refused. `'snapshot'`: the database itself is read-only
|
|
524
|
+
* (snapshot serving, an embedded `readonly: true` open, or the client-level
|
|
525
|
+
* fail-fast flag), so NOTHING can write here and writes must route to the
|
|
526
|
+
* primary. `'rbac'`: the database is writable but THIS connection's role may
|
|
527
|
+
* not write (per-connection permission), so re-authenticating may suffice.
|
|
528
|
+
*/
|
|
529
|
+
reason;
|
|
530
|
+
/**
|
|
531
|
+
* @param detail human-readable description of the refused write (the engine
|
|
532
|
+
* message, or a local fail-fast description). A "route writes to a writable
|
|
533
|
+
* primary" hint is always appended.
|
|
534
|
+
* @param options optional driver `cause` to preserve when wrapping a refusal,
|
|
535
|
+
* and the refusal `reason` (default `'snapshot'`).
|
|
536
|
+
*/
|
|
537
|
+
constructor(detail, options) {
|
|
538
|
+
super(exports.TurbineErrorCode.READ_ONLY, `[turbine] ${detail} Route writes to a writable primary.`, {
|
|
539
|
+
cause: options?.cause,
|
|
540
|
+
});
|
|
541
|
+
this.name = 'ReadOnlyError';
|
|
542
|
+
this.reason = options?.reason ?? 'snapshot';
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
exports.ReadOnlyError = ReadOnlyError;
|
|
506
546
|
/**
|
|
507
547
|
* Parse column names out of a pg `detail` string like:
|
|
508
548
|
* "Key (email)=(foo@bar) already exists."
|
|
Binary file
|
package/dist/cjs/index.js
CHANGED
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
* ```
|
|
35
35
|
*/
|
|
36
36
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
-
exports.
|
|
38
|
-
exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.defineSeed = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.schemaDefToMetadata = exports.table = exports.defineSchema = exports.column = void 0;
|
|
37
|
+
exports.applyManyToManyRelations = exports.snakeToPascal = exports.snakeToCamel = exports.singularize = exports.pgTypeToTs = exports.pgArrayType = exports.normalizeKeyColumns = exports.isDateType = exports.camelToSnake = exports.validateChannel = exports.QueryInterface = exports.pipelineSupported = exports.executePipeline = exports.hasRelationFields = exports.executeNestedUpdate = exports.executeNestedCreate = exports.introspect = exports.generate = exports.wrapPgError = exports.ValidationError = exports.UnsupportedFeatureError = exports.UniqueConstraintError = exports.TurbineErrorCode = exports.TurbineError = exports.TimeoutError = exports.setErrorMessageMode = exports.SerializationFailureError = exports.RelationError = exports.ReadOnlyError = exports.PipelineError = exports.OptimisticLockError = exports.NotNullViolationError = exports.NotFoundError = exports.MigrationError = exports.getErrorMessageMode = exports.ForeignKeyError = exports.ExclusionConstraintError = exports.DeadlockError = exports.ConnectionError = exports.CircularRelationError = exports.CheckConstraintError = exports.postgresDialect = exports.withRetry = exports.TurbineClient = exports.TransactionClient = exports.yugabytedb = exports.timescale = exports.postgresql = exports.cockroachdb = exports.alloydb = void 0;
|
|
38
|
+
exports.TypedSqlQuery = exports.buildTypedSql = exports.turbineHttp = exports.defineSeed = exports.schemaToSQLString = exports.schemaToSQL = exports.schemaPush = exports.schemaDiff = exports.schemaDefToMetadata = exports.table = exports.isDocFieldIndexDef = exports.defineSchema = exports.column = exports.ColumnBuilder = void 0;
|
|
39
39
|
var index_js_1 = require("./adapters/index.js");
|
|
40
40
|
Object.defineProperty(exports, "alloydb", { enumerable: true, get: function () { return index_js_1.alloydb; } });
|
|
41
41
|
Object.defineProperty(exports, "cockroachdb", { enumerable: true, get: function () { return index_js_1.cockroachdb; } });
|
|
@@ -63,6 +63,7 @@ Object.defineProperty(exports, "NotFoundError", { enumerable: true, get: functio
|
|
|
63
63
|
Object.defineProperty(exports, "NotNullViolationError", { enumerable: true, get: function () { return errors_js_1.NotNullViolationError; } });
|
|
64
64
|
Object.defineProperty(exports, "OptimisticLockError", { enumerable: true, get: function () { return errors_js_1.OptimisticLockError; } });
|
|
65
65
|
Object.defineProperty(exports, "PipelineError", { enumerable: true, get: function () { return errors_js_1.PipelineError; } });
|
|
66
|
+
Object.defineProperty(exports, "ReadOnlyError", { enumerable: true, get: function () { return errors_js_1.ReadOnlyError; } });
|
|
66
67
|
Object.defineProperty(exports, "RelationError", { enumerable: true, get: function () { return errors_js_1.RelationError; } });
|
|
67
68
|
Object.defineProperty(exports, "SerializationFailureError", { enumerable: true, get: function () { return errors_js_1.SerializationFailureError; } });
|
|
68
69
|
Object.defineProperty(exports, "setErrorMessageMode", { enumerable: true, get: function () { return errors_js_1.setErrorMessageMode; } });
|
|
@@ -110,6 +111,7 @@ Object.defineProperty(exports, "applyManyToManyRelations", { enumerable: true, g
|
|
|
110
111
|
Object.defineProperty(exports, "ColumnBuilder", { enumerable: true, get: function () { return schema_builder_js_1.ColumnBuilder; } });
|
|
111
112
|
Object.defineProperty(exports, "column", { enumerable: true, get: function () { return schema_builder_js_1.column; } });
|
|
112
113
|
Object.defineProperty(exports, "defineSchema", { enumerable: true, get: function () { return schema_builder_js_1.defineSchema; } });
|
|
114
|
+
Object.defineProperty(exports, "isDocFieldIndexDef", { enumerable: true, get: function () { return schema_builder_js_1.isDocFieldIndexDef; } });
|
|
113
115
|
// Legacy compat (deprecated — use object format with defineSchema)
|
|
114
116
|
Object.defineProperty(exports, "table", { enumerable: true, get: function () { return schema_builder_js_1.table; } });
|
|
115
117
|
// Schema metadata bridge — defineSchema() → SchemaMetadata without a live DB
|
package/dist/cjs/mssql.js
CHANGED
|
@@ -483,6 +483,11 @@ exports.mssqlDialect = {
|
|
|
483
483
|
supportsLateralJoin: false,
|
|
484
484
|
// sp_getapplock / sp_releaseapplock exist (used by a future migrate adapter).
|
|
485
485
|
supportsAdvisoryLock: true,
|
|
486
|
+
// No in-band EXPLAIN: SQL Server's SHOWPLAN is a session toggle
|
|
487
|
+
// (SET SHOWPLAN_ALL ON), not a statement prefix, so a compiled query cannot
|
|
488
|
+
// be explained in one round-trip. Override the inherited Postgres `EXPLAIN`
|
|
489
|
+
// to absent → QueryInterface.explain() throws E017.
|
|
490
|
+
explainQuery: undefined,
|
|
486
491
|
// FOR JSON over zero rows is NULL → coalesced in the relation override.
|
|
487
492
|
aggSupportsInlineOrderBy: false,
|
|
488
493
|
jsonPathSupport: 'limited',
|
package/dist/cjs/mysql.js
CHANGED
|
@@ -384,6 +384,10 @@ exports.mysqlDialect = {
|
|
|
384
384
|
supportsLateralJoin: false,
|
|
385
385
|
// GET_LOCK / RELEASE_LOCK exist (used by a future migrate adapter).
|
|
386
386
|
supportsAdvisoryLock: true,
|
|
387
|
+
// Plain `EXPLAIN` (one row of tabular plan columns) works on every supported
|
|
388
|
+
// MySQL 8.0.x; the readable `FORMAT=TREE` variant only exists from 8.0.16 and
|
|
389
|
+
// the engine floor here is 8.0.0. Plan text is a diagnostic, not a contract.
|
|
390
|
+
explainQuery: { prefix: 'EXPLAIN' },
|
|
387
391
|
// JSON_ARRAYAGG has no inline ORDER BY argument → force the inner-subquery
|
|
388
392
|
// rewrite for every ordered to-many relation.
|
|
389
393
|
aggSupportsInlineOrderBy: false,
|
|
@@ -119,4 +119,32 @@ async function importOptionalPeer(specifier, allowEsmFallback = true) {
|
|
|
119
119
|
return esmCapableCopy(specifier, false);
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Merged namespace so callers can reach {@link peerPackageVersion} off the same
|
|
124
|
+
* default import (`importOptionalPeer.peerPackageVersion(...)`). Lives in this
|
|
125
|
+
* `.cts` file for the same reason the dynamic import does: a `.cts` compiles to
|
|
126
|
+
* CommonJS in BOTH build passes, so `require` / `require.resolve` are natively
|
|
127
|
+
* available and `import.meta` is never emitted (which would break the CJS build
|
|
128
|
+
* and crash CJS consumers, see `resolveEmbeddedVersion` in powdb.ts).
|
|
129
|
+
*/
|
|
130
|
+
(function (importOptionalPeer) {
|
|
131
|
+
/**
|
|
132
|
+
* Resolve an optional peer's declared `package.json` version WITHOUT loading
|
|
133
|
+
* the package itself (so an ESM-only peer never trips `require`). `require` is
|
|
134
|
+
* anchored on THIS module's location (inside the published `dist/`), so bare
|
|
135
|
+
* resolution walks up `node_modules` and finds the peer exactly where
|
|
136
|
+
* `import.meta.url` used to point, but it compiles under `module: CommonJS`
|
|
137
|
+
* too. Returns `null` when the peer / its package.json cannot be resolved.
|
|
138
|
+
*/
|
|
139
|
+
function peerPackageVersion(specifier) {
|
|
140
|
+
try {
|
|
141
|
+
const pkg = require(`${specifier}/package.json`);
|
|
142
|
+
return typeof pkg.version === 'string' ? pkg.version : null;
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
importOptionalPeer.peerPackageVersion = peerPackageVersion;
|
|
149
|
+
})(importOptionalPeer || (importOptionalPeer = {}));
|
|
122
150
|
module.exports = importOptionalPeer;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* turbine-orm/powdb — `describe`-based introspection.
|
|
4
|
+
*
|
|
5
|
+
* PowDB exposes its catalog through two ordinary rows-returning statements
|
|
6
|
+
* (keywords since engine 0.10):
|
|
7
|
+
* - `schema` → one row per type: `{ name, columns }` (columns = a count).
|
|
8
|
+
* - `describe <T>` / `schema <T>` → one row per column:
|
|
9
|
+
* `{ column, type, nullable, index }` where `type` is a PowQL type name
|
|
10
|
+
* (`str`/`int`/`float`/`bool`/`json`/`datetime`/`uuid`/`bytes`), `nullable`
|
|
11
|
+
* is `"true"`/`"false"`, and `index` is `"unique"` / `"index"` / `""`.
|
|
12
|
+
*
|
|
13
|
+
* {@link introspectPowdbDatabase} turns those into the same {@link SchemaMetadata}
|
|
14
|
+
* shape the SQL introspectors produce, so a code-first PowDB database can be
|
|
15
|
+
* introspected for bootstrap/verification. It is transport-agnostic: the caller
|
|
16
|
+
* supplies an `exec(powql)` that returns row objects **keyed by column name**.
|
|
17
|
+
* - Embedded / owned pool: `exec = async (q) => ({ rows: await db.raw([q]) })`
|
|
18
|
+
* using a live `turbinePowDB` client's `raw` tagged template.
|
|
19
|
+
* - Networked: the raw `@zvndev/powdb-client` returns POSITIONAL rows
|
|
20
|
+
* (`{ columns: string[], rows: string[][] }`), so zip them into records.
|
|
21
|
+
* A bare `(await client.query(q)).rows` would hand this function `string[][]`
|
|
22
|
+
* whose `.name` cell is `undefined` and every table would silently drop out:
|
|
23
|
+
* ```ts
|
|
24
|
+
* const exec = async (q) => {
|
|
25
|
+
* const r = await client.query(q);
|
|
26
|
+
* return { rows: r.rows.map((row) => Object.fromEntries(r.columns.map((c, i) => [c, row[i]]))) };
|
|
27
|
+
* };
|
|
28
|
+
* ```
|
|
29
|
+
* (A mis-shaped exec is now caught: if `schema` returns rows but none carry
|
|
30
|
+
* a `name`, {@link introspectPowdbDatabase} throws instead of returning an
|
|
31
|
+
* empty schema.)
|
|
32
|
+
*
|
|
33
|
+
* IMPORTANT LIMITATIONS (all documented, none silent):
|
|
34
|
+
* - Relations are ALWAYS `{}`: PowDB has no declared foreign keys, so
|
|
35
|
+
* `describe` cannot report them. The recommended flow for relation-aware
|
|
36
|
+
* metadata is code-first `defineSchema` + `schemaDefToMetadata`; use
|
|
37
|
+
* introspection to bootstrap or verify column shape.
|
|
38
|
+
* - Primary key is a HEURISTIC (`describe` has no PK concept): PowDB marks a
|
|
39
|
+
* PK column as `required unique`, so the first non-nullable `unique` column
|
|
40
|
+
* is chosen (a column named `id` wins ties). A table with no such column
|
|
41
|
+
* yields `primaryKey: []` and a warning; single-row ops on it fail loudly.
|
|
42
|
+
* - `isGenerated` is always `false`: `describe` does not expose PowDB's `auto`
|
|
43
|
+
* modifier, so an introspected int PK is treated as client-supplied unless
|
|
44
|
+
* the caller hand-edits the metadata.
|
|
45
|
+
* - Doc-field expression indexes are INVISIBLE to `describe`, so they never
|
|
46
|
+
* round-trip; only plain `unique`/`index` columns appear in `indexes`.
|
|
47
|
+
* - `datetime` / `uuid` / `bytes` columns map to read-oriented TS types
|
|
48
|
+
* (`Date` / `string` / `Uint8Array`). Turbine never emits those PowQL types
|
|
49
|
+
* on write, so writing to such a column may not round-trip.
|
|
50
|
+
*
|
|
51
|
+
* v1 is a PROGRAMMATIC API (exported from `turbine-orm/powdb`); the CLI's
|
|
52
|
+
* `turbine generate` still defaults to Postgres. Routing a `powdb://` URL
|
|
53
|
+
* through the CLI would additionally need: a `powdbDialect.introspector`
|
|
54
|
+
* wired to a networked `exec`, and `cli/config.ts` teaching the generate
|
|
55
|
+
* funnel to construct a PowDB client instead of a `pg` client for `powdb://`.
|
|
56
|
+
*/
|
|
57
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
58
|
+
exports.introspectPowdbDatabase = introspectPowdbDatabase;
|
|
59
|
+
const errors_js_1 = require("./errors.js");
|
|
60
|
+
const powdb_js_1 = require("./powdb.js");
|
|
61
|
+
const schema_js_1 = require("./schema.js");
|
|
62
|
+
/** Coerce a wire cell to string (legacy wire cells are strings; native cells may be typed). */
|
|
63
|
+
function asString(v) {
|
|
64
|
+
return v === null || v === undefined ? '' : String(v);
|
|
65
|
+
}
|
|
66
|
+
/** Coerce a `describe` `nullable` cell (`"true"`/`"false"` or a native boolean) to a JS boolean. */
|
|
67
|
+
function asBool(v) {
|
|
68
|
+
return v === true || asString(v).toLowerCase() === 'true';
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Map a PowQL type name to the {@link ColumnMetadata} TS/dialect types. The
|
|
72
|
+
* `tsType` drives read coercion (`coerceValue`) and write typing
|
|
73
|
+
* (`powqlColumnType`); `dialectType`/`pgType` carry the PowQL type name so
|
|
74
|
+
* `isFloatColumn` / `isJsonColumn` classify correctly.
|
|
75
|
+
*/
|
|
76
|
+
function mapPowqlType(powqlType) {
|
|
77
|
+
switch (powqlType) {
|
|
78
|
+
case 'int':
|
|
79
|
+
return { tsType: 'number', dialectType: 'int' };
|
|
80
|
+
case 'float':
|
|
81
|
+
return { tsType: 'number', dialectType: 'float' };
|
|
82
|
+
case 'bool':
|
|
83
|
+
return { tsType: 'boolean', dialectType: 'bool' };
|
|
84
|
+
case 'json':
|
|
85
|
+
return { tsType: 'unknown', dialectType: 'json' };
|
|
86
|
+
case 'datetime':
|
|
87
|
+
return { tsType: 'Date', dialectType: 'datetime' };
|
|
88
|
+
case 'uuid':
|
|
89
|
+
return { tsType: 'string', dialectType: 'uuid' };
|
|
90
|
+
case 'bytes':
|
|
91
|
+
return { tsType: 'Uint8Array', dialectType: 'bytes' };
|
|
92
|
+
default:
|
|
93
|
+
// `str` and any unknown future scalar fall back to string.
|
|
94
|
+
return { tsType: 'string', dialectType: 'str' };
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Read a live PowDB database into {@link SchemaMetadata} via `schema` +
|
|
99
|
+
* `describe <T>` statements run through the supplied {@link PowdbExec}.
|
|
100
|
+
*
|
|
101
|
+
* @param exec Rows-returning executor (embedded `db.raw` or networked `client.query`).
|
|
102
|
+
* @param options `include`/`exclude` table filters.
|
|
103
|
+
*/
|
|
104
|
+
async function introspectPowdbDatabase(exec, options = {}) {
|
|
105
|
+
// Gate on the engine's introspection capability (>= 0.10) when the caller
|
|
106
|
+
// knows the version, so a pre-0.10 engine gets a typed E017 hint instead of
|
|
107
|
+
// an opaque `unexpected token schema` parse error.
|
|
108
|
+
if (options.capabilities) {
|
|
109
|
+
(0, powdb_js_1.requireCapability)(options.capabilities, 'introspection', 'PowDB `describe` introspection');
|
|
110
|
+
}
|
|
111
|
+
// ----- Types (one row per table, columns `name`, `columns`) -----
|
|
112
|
+
const schemaRows = (await exec('schema')).rows;
|
|
113
|
+
let tableNames = schemaRows.map((r) => asString(r.name)).filter((n) => n.length > 0);
|
|
114
|
+
// A mis-shaped `exec` (e.g. the raw client's positional `string[][]` rows
|
|
115
|
+
// passed straight through) yields rows whose `name` cell is `undefined`, so
|
|
116
|
+
// every table filters out and the schema comes back silently empty. Refuse
|
|
117
|
+
// that instead of losing data: real rows must carry a `name`.
|
|
118
|
+
if (schemaRows.length > 0 && tableNames.length === 0) {
|
|
119
|
+
throw new errors_js_1.ValidationError(`[turbine] PowDB introspection: the \`schema\` statement returned ${schemaRows.length} row(s) but none carried a ` +
|
|
120
|
+
'`name` cell. The `exec` you supplied likely returns POSITIONAL rows (string[][]) rather than records keyed by ' +
|
|
121
|
+
'column name; zip `columns` with each row (see introspectPowdbDatabase docs).');
|
|
122
|
+
}
|
|
123
|
+
if (options.include?.length) {
|
|
124
|
+
const inc = new Set(options.include);
|
|
125
|
+
tableNames = tableNames.filter((t) => inc.has(t));
|
|
126
|
+
}
|
|
127
|
+
if (options.exclude?.length) {
|
|
128
|
+
const exc = new Set(options.exclude);
|
|
129
|
+
tableNames = tableNames.filter((t) => !exc.has(t));
|
|
130
|
+
}
|
|
131
|
+
const tables = {};
|
|
132
|
+
for (const tableName of tableNames) {
|
|
133
|
+
// `describe` needs the table name in bare-identifier position → quote it so
|
|
134
|
+
// a reserved-word / non-bare table name (`order`) does not become a parse
|
|
135
|
+
// error.
|
|
136
|
+
const describeRows = (await exec(`describe ${(0, powdb_js_1.quotePowqlIdent)(tableName)}`)).rows.map((r) => ({
|
|
137
|
+
column: asString(r.column),
|
|
138
|
+
type: asString(r.type),
|
|
139
|
+
nullable: asBool(r.nullable),
|
|
140
|
+
index: asString(r.index),
|
|
141
|
+
}));
|
|
142
|
+
const columns = [];
|
|
143
|
+
const columnMap = {};
|
|
144
|
+
const reverseColumnMap = {};
|
|
145
|
+
const dateColumns = new Set();
|
|
146
|
+
const dialectTypes = {};
|
|
147
|
+
const pgTypes = {};
|
|
148
|
+
const allColumns = [];
|
|
149
|
+
const uniqueColumns = [];
|
|
150
|
+
const indexes = [];
|
|
151
|
+
// PK heuristic candidates: non-nullable `unique` columns.
|
|
152
|
+
const pkCandidates = [];
|
|
153
|
+
for (const row of describeRows) {
|
|
154
|
+
const name = row.column;
|
|
155
|
+
const field = (0, schema_js_1.snakeToCamel)(name);
|
|
156
|
+
const { tsType, dialectType } = mapPowqlType(row.type);
|
|
157
|
+
const nullable = row.nullable;
|
|
158
|
+
const finalTs = nullable ? `${tsType} | null` : tsType;
|
|
159
|
+
const col = {
|
|
160
|
+
name,
|
|
161
|
+
field,
|
|
162
|
+
dialectType,
|
|
163
|
+
pgType: dialectType,
|
|
164
|
+
tsType: finalTs,
|
|
165
|
+
nullable,
|
|
166
|
+
// `describe` reports neither defaults nor the `auto` modifier.
|
|
167
|
+
hasDefault: false,
|
|
168
|
+
isGenerated: false,
|
|
169
|
+
isArray: false,
|
|
170
|
+
arrayType: undefined,
|
|
171
|
+
pgArrayType: 'text[]',
|
|
172
|
+
};
|
|
173
|
+
columns.push(col);
|
|
174
|
+
columnMap[field] = name;
|
|
175
|
+
reverseColumnMap[name] = field;
|
|
176
|
+
allColumns.push(name);
|
|
177
|
+
dialectTypes[name] = dialectType;
|
|
178
|
+
pgTypes[name] = dialectType;
|
|
179
|
+
if (dialectType === 'datetime')
|
|
180
|
+
dateColumns.add(name);
|
|
181
|
+
if (row.index === 'unique') {
|
|
182
|
+
uniqueColumns.push([name]);
|
|
183
|
+
if (!nullable)
|
|
184
|
+
pkCandidates.push(name);
|
|
185
|
+
}
|
|
186
|
+
if (row.index === 'unique' || row.index === 'index') {
|
|
187
|
+
indexes.push({
|
|
188
|
+
name: `${tableName}_${name}_idx`,
|
|
189
|
+
columns: [name],
|
|
190
|
+
unique: row.index === 'unique',
|
|
191
|
+
definition: `${row.index === 'unique' ? 'unique ' : ''}index on ${tableName}(${name})`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// Primary key: first non-nullable unique column, preferring one named `id`.
|
|
196
|
+
let primaryKey = [];
|
|
197
|
+
if (pkCandidates.length > 0) {
|
|
198
|
+
primaryKey = [pkCandidates.includes('id') ? 'id' : pkCandidates[0]];
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
console.warn(`[turbine] PowDB introspection: table "${tableName}" has no non-nullable unique column; ` +
|
|
202
|
+
'primaryKey is [] (single-row operations will fail). Supply a primary key via code-first ' +
|
|
203
|
+
'`defineSchema` metadata if this table needs findUnique/update/delete by id.');
|
|
204
|
+
}
|
|
205
|
+
tables[tableName] = {
|
|
206
|
+
name: tableName,
|
|
207
|
+
columns,
|
|
208
|
+
columnMap,
|
|
209
|
+
reverseColumnMap,
|
|
210
|
+
dateColumns,
|
|
211
|
+
dialectTypes,
|
|
212
|
+
pgTypes,
|
|
213
|
+
allColumns,
|
|
214
|
+
primaryKey,
|
|
215
|
+
uniqueColumns,
|
|
216
|
+
// PowDB has no declared foreign keys → no relations from introspection.
|
|
217
|
+
relations: {},
|
|
218
|
+
indexes,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
return { tables, enums: {} };
|
|
222
|
+
}
|