turbine-orm 0.75.0 → 0.76.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 +48 -7
- package/dist/cjs/cli/compile-query.d.ts +22 -2
- package/dist/cjs/cli/compile-query.js +49 -5
- package/dist/cjs/cli/config.d.ts +2 -0
- package/dist/cjs/cli/config.js +1 -1
- package/dist/cjs/cli/destructive.js +78 -43
- package/dist/cjs/cli/index.d.ts +95 -1
- package/dist/cjs/cli/index.js +609 -145
- package/dist/cjs/cli/mcp.js +30 -1
- package/dist/cjs/cli/pii-predicate-guard.d.ts +25 -0
- package/dist/cjs/cli/pii-predicate-guard.js +72 -12
- package/dist/cjs/cli/rate-limit.js +38 -1
- package/dist/cjs/cli/studio.js +26 -5
- package/dist/cjs/cli/ui.d.ts +33 -0
- package/dist/cjs/cli/ui.js +53 -7
- package/dist/cjs/client.d.ts +13 -1
- package/dist/cjs/client.js +1 -1
- package/dist/cjs/errors.d.ts +12 -1
- package/dist/cjs/errors.js +11 -2
- package/dist/cjs/generate.d.ts +26 -0
- package/dist/cjs/generate.js +174 -27
- package/dist/cjs/index.d.ts +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/introspect.d.ts +17 -0
- package/dist/cjs/introspect.js +100 -1
- package/dist/cjs/mssql.d.ts +18 -0
- package/dist/cjs/mssql.js +20 -1
- package/dist/cjs/pipeline.js +44 -6
- package/dist/cjs/powql.js +51 -17
- package/dist/cjs/query/batched-loader.js +3 -3
- package/dist/cjs/query/builder.js +1 -1
- package/dist/cjs/query/relations.d.ts +5 -0
- package/dist/cjs/query/relations.js +141 -69
- package/dist/cjs/query/utils.d.ts +13 -0
- package/dist/cjs/query/utils.js +16 -0
- package/dist/cjs/serverless.d.ts +1 -1
- package/dist/cjs/serverless.js +1 -1
- package/dist/cjs/sqlite.d.ts +33 -1
- package/dist/cjs/sqlite.js +84 -3
- package/dist/cli/compile-query.d.ts +22 -2
- package/dist/cli/compile-query.js +50 -6
- package/dist/cli/config.d.ts +2 -0
- package/dist/cli/config.js +1 -1
- package/dist/cli/destructive.js +78 -43
- package/dist/cli/index.d.ts +95 -1
- package/dist/cli/index.js +604 -147
- package/dist/cli/mcp.js +30 -1
- package/dist/cli/pii-predicate-guard.d.ts +25 -0
- package/dist/cli/pii-predicate-guard.js +73 -13
- package/dist/cli/rate-limit.js +38 -1
- package/dist/cli/studio.js +27 -6
- package/dist/cli/ui.d.ts +33 -0
- package/dist/cli/ui.js +51 -7
- package/dist/client.d.ts +13 -1
- package/dist/client.js +1 -1
- package/dist/errors.d.ts +12 -1
- package/dist/errors.js +11 -2
- package/dist/generate.d.ts +26 -0
- package/dist/generate.js +172 -27
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/introspect.d.ts +17 -0
- package/dist/introspect.js +98 -1
- package/dist/mssql.d.ts +18 -0
- package/dist/mssql.js +20 -1
- package/dist/pipeline.js +44 -6
- package/dist/powql.js +53 -19
- package/dist/query/batched-loader.js +4 -4
- package/dist/query/builder.js +2 -2
- package/dist/query/relations.d.ts +5 -0
- package/dist/query/relations.js +141 -70
- package/dist/query/utils.d.ts +13 -0
- package/dist/query/utils.js +15 -0
- package/dist/serverless.d.ts +1 -1
- package/dist/serverless.js +1 -1
- package/dist/sqlite.d.ts +33 -1
- package/dist/sqlite.js +85 -4
- package/package.json +2 -2
package/dist/cjs/pipeline.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
23
|
exports.executePipeline = executePipeline;
|
|
24
24
|
exports.pipelineSupported = pipelineSupported;
|
|
25
|
+
const dialect_js_1 = require("./dialect.js");
|
|
25
26
|
const errors_js_1 = require("./errors.js");
|
|
26
27
|
const pipeline_submittable_js_1 = require("./pipeline-submittable.js");
|
|
27
28
|
/**
|
|
@@ -32,7 +33,7 @@ const pipeline_submittable_js_1 = require("./pipeline-submittable.js");
|
|
|
32
33
|
* The caller is responsible for acquiring the client and releasing it after
|
|
33
34
|
* this function completes (in the finally block).
|
|
34
35
|
*/
|
|
35
|
-
async function runSequential(client, queries, options = {}) {
|
|
36
|
+
async function runSequential(client, queries, dialect, options = {}) {
|
|
36
37
|
const { transactional = true } = options;
|
|
37
38
|
if (!transactional)
|
|
38
39
|
return runIndependent(client, queries);
|
|
@@ -44,7 +45,7 @@ async function runSequential(client, queries, options = {}) {
|
|
|
44
45
|
// `TURBINE_E004` nor a retryable flag for the one failure that is most
|
|
45
46
|
// worth retrying.
|
|
46
47
|
try {
|
|
47
|
-
await client.query(
|
|
48
|
+
await client.query(dialect.beginStatement());
|
|
48
49
|
}
|
|
49
50
|
catch (err) {
|
|
50
51
|
throw (0, errors_js_1.wrapPgError)(err);
|
|
@@ -61,7 +62,7 @@ async function runSequential(client, queries, options = {}) {
|
|
|
61
62
|
results.push(q.transform(raw));
|
|
62
63
|
}
|
|
63
64
|
try {
|
|
64
|
-
await client.query(
|
|
65
|
+
await client.query(dialect.commitStatement());
|
|
65
66
|
}
|
|
66
67
|
catch (err) {
|
|
67
68
|
throw (0, errors_js_1.wrapPgError)(err);
|
|
@@ -70,7 +71,7 @@ async function runSequential(client, queries, options = {}) {
|
|
|
70
71
|
}
|
|
71
72
|
catch (err) {
|
|
72
73
|
try {
|
|
73
|
-
await client.query(
|
|
74
|
+
await client.query(dialect.rollbackStatement());
|
|
74
75
|
}
|
|
75
76
|
catch {
|
|
76
77
|
// Best-effort rollback
|
|
@@ -78,6 +79,40 @@ async function runSequential(client, queries, options = {}) {
|
|
|
78
79
|
throw err;
|
|
79
80
|
}
|
|
80
81
|
}
|
|
82
|
+
/**
|
|
83
|
+
* The dialect whose transaction keywords this batch must use.
|
|
84
|
+
*
|
|
85
|
+
* `executePipeline` is handed a POOL and nothing else, so the pool is the only
|
|
86
|
+
* place the engine can be read from; the engine pool shims are the objects that
|
|
87
|
+
* know their own dialect, and one of them (`MssqlPool`) publishes it for exactly
|
|
88
|
+
* this. Anything else, a real `pg.Pool`, a serverless HTTP pool, a test mock,
|
|
89
|
+
* keeps PostgreSQL, which is what every one of them already spoke.
|
|
90
|
+
*
|
|
91
|
+
* This existed as three hard-coded strings, `BEGIN` / `COMMIT` / `ROLLBACK`,
|
|
92
|
+
* which is the one engine-specific decision in this file and the one it was not
|
|
93
|
+
* making. On SQL Server a bare `BEGIN` opens a statement BLOCK, not a
|
|
94
|
+
* transaction: `MssqlTxClient` matches the dialect's `BEGIN TRANSACTION` and
|
|
95
|
+
* nothing else, so a bare `BEGIN` missed that branch, reached the server as a
|
|
96
|
+
* block opener with no `END`, and was rejected; the `COMMIT` and `ROLLBACK`
|
|
97
|
+
* that followed then found no open transaction and silently did nothing. A
|
|
98
|
+
* pipeline that documents itself as atomic was neither atomic nor rolled back.
|
|
99
|
+
*
|
|
100
|
+
* Duck-typed rather than `instanceof`, deliberately: importing an engine module
|
|
101
|
+
* here would pull an optional peer's whole module graph into the Postgres path.
|
|
102
|
+
* The three methods tested are exactly the three called below, so a partial
|
|
103
|
+
* object can never be accepted and then fail at the call site.
|
|
104
|
+
*/
|
|
105
|
+
function poolDialect(pool) {
|
|
106
|
+
const candidate = pool.dialect;
|
|
107
|
+
if (candidate !== null &&
|
|
108
|
+
typeof candidate === 'object' &&
|
|
109
|
+
typeof candidate.beginStatement === 'function' &&
|
|
110
|
+
typeof candidate.commitStatement === 'function' &&
|
|
111
|
+
typeof candidate.rollbackStatement === 'function') {
|
|
112
|
+
return candidate;
|
|
113
|
+
}
|
|
114
|
+
return dialect_js_1.postgresDialect;
|
|
115
|
+
}
|
|
81
116
|
/**
|
|
82
117
|
* Sequential fallback for `{ transactional: false }`: each query is
|
|
83
118
|
* INDEPENDENT, which is what that option promises and what the real pipeline
|
|
@@ -181,8 +216,11 @@ async function executePipeline(pool, queries, options) {
|
|
|
181
216
|
const results = await (0, pipeline_submittable_js_1.runPipelined)(client, queries, pipelineOptions);
|
|
182
217
|
return results;
|
|
183
218
|
}
|
|
184
|
-
// Sequential fallback, reuses the same client
|
|
185
|
-
|
|
219
|
+
// Sequential fallback, reuses the same client. This is the path every
|
|
220
|
+
// non-Postgres engine takes (none of their clients expose pg's wire
|
|
221
|
+
// internals), so it is the one that has to speak the engine's transaction
|
|
222
|
+
// keywords rather than Postgres's.
|
|
223
|
+
return await runSequential(client, queries, poolDialect(pool), options);
|
|
186
224
|
}
|
|
187
225
|
finally {
|
|
188
226
|
client.release();
|
package/dist/cjs/powql.js
CHANGED
|
@@ -185,8 +185,17 @@ function rejectUnsupportedFilter(value, field) {
|
|
|
185
185
|
if ('path' in value || 'hasKey' in value) {
|
|
186
186
|
throw new errors_js_1.UnsupportedFeatureError('JSON path/key filters', 'PowDB', `field "${field}"`);
|
|
187
187
|
}
|
|
188
|
-
|
|
189
|
-
|
|
188
|
+
// Enumerated from the SHARED {@link ARRAY_OPERATOR_KEYS} rather than
|
|
189
|
+
// re-listed here, because the hand-written copy had already drifted from it:
|
|
190
|
+
// `has` was missing, so `{ tags: { has: 'x' } }` was not recognised as an
|
|
191
|
+
// array filter at all, fell through to the bare-object equality branch, and
|
|
192
|
+
// bound the whole `{ has: 'x' }` OBJECT as a scalar parameter. A silent wrong
|
|
193
|
+
// answer where its three siblings raise E017. Two lists is how that happened;
|
|
194
|
+
// reading the one the SQL side reads is what stops it happening again.
|
|
195
|
+
for (const key of filters_js_1.ARRAY_OPERATOR_KEYS) {
|
|
196
|
+
if (key in value) {
|
|
197
|
+
throw new errors_js_1.UnsupportedFeatureError('array filters', 'PowDB', `field "${field}"`);
|
|
198
|
+
}
|
|
190
199
|
}
|
|
191
200
|
if ('search' in value) {
|
|
192
201
|
throw new errors_js_1.UnsupportedFeatureError('full-text search filters', 'PowDB', `field "${field}"`);
|
|
@@ -216,7 +225,7 @@ class PowqlInterface {
|
|
|
216
225
|
this.options = options;
|
|
217
226
|
const meta = schema.tables[table];
|
|
218
227
|
if (!meta) {
|
|
219
|
-
throw new errors_js_1.ValidationError(`[turbine] Unknown table "${table}".
|
|
228
|
+
throw new errors_js_1.ValidationError(`[turbine] Unknown table "${table}". ${(0, utils_js_1.availableClause)(Object.keys(schema.tables), 'The schema has no tables.')}`);
|
|
220
229
|
}
|
|
221
230
|
this.meta = meta;
|
|
222
231
|
this.defaultLimit = options.defaultLimit;
|
|
@@ -1189,8 +1198,18 @@ class PowqlInterface {
|
|
|
1189
1198
|
stripWritePii(entity) {
|
|
1190
1199
|
if (!entity)
|
|
1191
1200
|
return entity;
|
|
1201
|
+
// Same PRIMARY-KEY exemption as the SQL engines' `piiColumns` / `piiFields`
|
|
1202
|
+
// (query/writes.ts), and for the same measured reason: the returned row has
|
|
1203
|
+
// to stay ADDRESSABLE. Without it a PII-tagged key column came back deleted,
|
|
1204
|
+
// so the caller's follow-up `update`/`delete` built its where from a partial
|
|
1205
|
+
// key, the empty-where guard did not fire (the other member was present),
|
|
1206
|
+
// and the write hit every row sharing the remaining member. The policy is
|
|
1207
|
+
// "tag sensitive data, not keys": a PII PK is documented out of scope for
|
|
1208
|
+
// stripping. This docstring already cross-referenced the SQL rule; only the
|
|
1209
|
+
// exemption itself was missing.
|
|
1210
|
+
const pk = new Set(this.meta.primaryKey);
|
|
1192
1211
|
for (const col of this.meta.columns) {
|
|
1193
|
-
if (col.pii)
|
|
1212
|
+
if (col.pii && !pk.has(col.name))
|
|
1194
1213
|
delete entity[col.field];
|
|
1195
1214
|
}
|
|
1196
1215
|
return entity;
|
|
@@ -1458,18 +1477,40 @@ class PowqlInterface {
|
|
|
1458
1477
|
if (args.cursor) {
|
|
1459
1478
|
throw new errors_js_1.UnsupportedFeatureError('cursor pagination', 'PowDB', 'use limit/offset instead');
|
|
1460
1479
|
}
|
|
1480
|
+
// `distinct` names COLUMNS: Postgres compiles it to `SELECT DISTINCT ON
|
|
1481
|
+
// (col)`, one row per distinct value of those columns. PowQL's `distinct`
|
|
1482
|
+
// keyword is ROW-WIDE (`SELECT DISTINCT *`) and takes no column list, and
|
|
1483
|
+
// the language has no window functions to rebuild per-column distinct with,
|
|
1484
|
+
// so there is nothing faithful to emit. Accepting the option and emitting
|
|
1485
|
+
// the row-wide keyword returned a DIFFERENT row set under the same
|
|
1486
|
+
// argument, with no error and no warning, which is the one outcome the
|
|
1487
|
+
// dialect seam exists to prevent.
|
|
1488
|
+
//
|
|
1489
|
+
// Refused with the same E017 the other non-Postgres engines already raise
|
|
1490
|
+
// (query/builder.ts's `dialect.name !== 'postgresql'` gate) and in the same
|
|
1491
|
+
// words, so `distinct` now means one thing across every engine and matches
|
|
1492
|
+
// what the README documents. Refused BEFORE the column names are resolved,
|
|
1493
|
+
// matching that gate's position, so `distinct: ['nope']` reports the
|
|
1494
|
+
// unsupported feature rather than the typo on every engine alike.
|
|
1495
|
+
if (args.distinct?.length) {
|
|
1496
|
+
throw new errors_js_1.UnsupportedFeatureError('DISTINCT ON (findMany distinct)', 'PowDB', "findMany({ distinct }) requires PostgreSQL: PowQL's `distinct` is row-wide, not per-column. " +
|
|
1497
|
+
'Group in application code, or use groupBy({ by }) for one row per combination.');
|
|
1498
|
+
}
|
|
1461
1499
|
const resolvedWhere = await this.resolveRelationFilters(args.where, args.timeout);
|
|
1462
1500
|
const { cols, forcedPk } = this.projectionPlan(args.select, args.omit, (0, types_js_1.resolveUnsafeFlag)(args.includePii, 'includePii'));
|
|
1463
1501
|
// Partition the `with` clause: nested-projection blocks vs loader residue.
|
|
1464
|
-
// A
|
|
1465
|
-
//
|
|
1466
|
-
//
|
|
1467
|
-
//
|
|
1502
|
+
// A relation whose field name collides with a projected parent column stays
|
|
1503
|
+
// on the loaders (its block key would duplicate the column's). A parent
|
|
1504
|
+
// `distinct` used to be a third exclusion here (distinct over a row
|
|
1505
|
+
// containing a JSON array is not a defined comparison); the refusal above
|
|
1506
|
+
// now settles that one statement earlier, so the branch cannot be reached
|
|
1507
|
+
// with a `distinct` present and testing for it again would be dead code
|
|
1508
|
+
// implying the option is still accepted.
|
|
1468
1509
|
const withClause = args.with;
|
|
1469
1510
|
const nestedPlans = [];
|
|
1470
1511
|
const linkPlans = [];
|
|
1471
1512
|
let residualWith = withClause;
|
|
1472
|
-
if (withClause &&
|
|
1513
|
+
if (withClause && this.nestedProjectionsPreferred(args)) {
|
|
1473
1514
|
const residue = {};
|
|
1474
1515
|
for (const [relName, opt] of Object.entries(withClause)) {
|
|
1475
1516
|
if (!opt)
|
|
@@ -1500,13 +1541,6 @@ class PowqlInterface {
|
|
|
1500
1541
|
const alias = nest ? 't0' : undefined;
|
|
1501
1542
|
let where = this.buildWhere(resolvedWhere, params, alias);
|
|
1502
1543
|
where = this.applyGlobalFilter(where, params, args.skipGlobalFilters, alias);
|
|
1503
|
-
// PowQL's `distinct` is row-wide, so these names never reach the emitted
|
|
1504
|
-
// statement. They are still caller-supplied names, and a name resolves or
|
|
1505
|
-
// throws: reading the array for its LENGTH alone let `distinct: ['nope']`
|
|
1506
|
-
// succeed here while every SQL engine refuses it. Validation only.
|
|
1507
|
-
for (const key of args.distinct ?? [])
|
|
1508
|
-
this.column(key);
|
|
1509
|
-
const distinct = args.distinct?.length ? ' distinct' : '';
|
|
1510
1544
|
const filter = where ? ` filter ${where}` : '';
|
|
1511
1545
|
const order = this.buildOrder(args.orderBy, params, alias);
|
|
1512
1546
|
const limit = this.effectiveLimit(args);
|
|
@@ -1534,7 +1568,7 @@ class PowqlInterface {
|
|
|
1534
1568
|
else {
|
|
1535
1569
|
projection = this.projection(cols);
|
|
1536
1570
|
}
|
|
1537
|
-
const powql = `${this.qt}${nest ? ' as t0' : ''}${
|
|
1571
|
+
const powql = `${this.qt}${nest ? ' as t0' : ''}${filter}${order}${limitClause}${offsetClause} ${projection}`;
|
|
1538
1572
|
return { powql, resolvedWhere, nestedPlans, linkPlans, residualWith, forcedPk };
|
|
1539
1573
|
}
|
|
1540
1574
|
/** Build + run the findMany select; returns raw rows, the serving wire, the resolved where, and the `with` partition. */
|
|
@@ -429,7 +429,7 @@ function resolveCountRelations(parentMeta, countSpec) {
|
|
|
429
429
|
const rel = (0, utils_js_1.ownLookup)(parentMeta.relations, relName);
|
|
430
430
|
if (!rel) {
|
|
431
431
|
throw new errors_js_1.RelationError(`[turbine] Unknown relation "${relName}" in _count on table "${parentMeta.name}". ` +
|
|
432
|
-
|
|
432
|
+
(0, utils_js_1.availableClause)(Object.keys(parentMeta.relations), 'It has no relations.'));
|
|
433
433
|
}
|
|
434
434
|
if (!isToMany(rel)) {
|
|
435
435
|
throw new errors_js_1.ValidationError(`[turbine] _count is only supported for to-many relations; "${relName}" on ` +
|
|
@@ -505,7 +505,7 @@ async function loadRelationsBatched(ctx, parents, withClause, timeout, depth = 0
|
|
|
505
505
|
// E005 for this exact shape (relations.ts), and under 'auto' the two
|
|
506
506
|
// must refuse identically or the error CODE depends on table size.
|
|
507
507
|
throw new errors_js_1.RelationError(`[turbine] Unknown relation "${relName}" on table "${ctx.parentMeta.name}". ` +
|
|
508
|
-
|
|
508
|
+
(0, utils_js_1.availableClause)(Object.keys(ctx.parentMeta.relations), 'It has no relations.'));
|
|
509
509
|
}
|
|
510
510
|
resolved.push({ relName, rel, options: spec === true ? {} : spec });
|
|
511
511
|
}
|
|
@@ -533,7 +533,7 @@ async function loadRelationsBatched(ctx, parents, withClause, timeout, depth = 0
|
|
|
533
533
|
// not a line in a correctness fix. Until then both strategies say no.
|
|
534
534
|
if (hasCount && depth > 0) {
|
|
535
535
|
throw new errors_js_1.RelationError(`[turbine] Unknown relation "_count" on table "${ctx.parentMeta.name}". ` +
|
|
536
|
-
|
|
536
|
+
`${(0, utils_js_1.availableClause)(Object.keys(ctx.parentMeta.relations), 'It has no relations.')} ` +
|
|
537
537
|
'(`_count` is supported on the top-level `with` only, on every relationLoadStrategy.)');
|
|
538
538
|
}
|
|
539
539
|
// Fix key order BEFORE anything is awaited: the loads below all write their
|
|
@@ -707,7 +707,7 @@ class QueryInterface {
|
|
|
707
707
|
this.schema = schema;
|
|
708
708
|
const meta = schema.tables[table];
|
|
709
709
|
if (!meta) {
|
|
710
|
-
throw new errors_js_1.ValidationError(`[turbine] Unknown table "${table}".
|
|
710
|
+
throw new errors_js_1.ValidationError(`[turbine] Unknown table "${table}". ${(0, utils_js_1.availableClause)(Object.keys(schema.tables), 'The schema has no tables.')}`);
|
|
711
711
|
}
|
|
712
712
|
this.tableMeta = meta;
|
|
713
713
|
this.middlewares = middlewares ?? [];
|
|
@@ -71,6 +71,11 @@ export declare function resolveProjection(qi: BuilderCtx, table: string, meta: T
|
|
|
71
71
|
* the existing call sites in builder.ts.
|
|
72
72
|
*/
|
|
73
73
|
export declare function resolveColumns(qi: BuilderCtx, select?: Record<string, boolean>, omit?: Record<string, boolean>, includePii?: boolean): string[] | null;
|
|
74
|
+
/**
|
|
75
|
+
* Read a relation spec as options. See {@link EMPTY_WITH_OPTIONS} for why this
|
|
76
|
+
* is a shared authority rather than a guard repeated per walker.
|
|
77
|
+
*/
|
|
78
|
+
export declare function relationOptions(spec: true | WithOptions): WithOptions;
|
|
74
79
|
/**
|
|
75
80
|
* Produce a fingerprint for a `with` clause tree. Recursion mirrors
|
|
76
81
|
* buildSelectWithRelations / buildRelationSubquery.
|