turbine-orm 0.37.0 → 0.38.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 +3 -3
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.js +262 -60
- package/dist/cjs/errors.js +27 -0
- package/dist/cjs/nested-write.js +7 -7
- package/dist/cjs/query/batched-loader.js +4 -3
- package/dist/cjs/query/builder.js +9 -5
- package/dist/cjs/query/relations.js +7 -6
- package/dist/cjs/query/utils.js +13 -0
- package/dist/cjs/query/where-compile.js +2 -1
- package/dist/cjs/query/where.js +3 -3
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +27 -5
- package/dist/cli/studio.js +262 -60
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +26 -0
- package/dist/nested-write.js +8 -8
- package/dist/query/batched-loader.js +4 -3
- package/dist/query/builder.js +10 -6
- package/dist/query/relations.js +7 -6
- package/dist/query/utils.d.ts +10 -0
- package/dist/query/utils.js +12 -0
- package/dist/query/where-compile.js +2 -1
- package/dist/query/where.js +4 -4
- package/package.json +1 -1
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
import { CircularRelationError, RelationError, UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
50
50
|
import { normalizeKeyColumns } from '../schema.js';
|
|
51
51
|
import { isRelationPickOrderBy } from './filters.js';
|
|
52
|
+
import { ownLookup } from './utils.js';
|
|
52
53
|
/**
|
|
53
54
|
* Max parent keys per follow-up query. On Postgres the whole key set travels as
|
|
54
55
|
* ONE array parameter (`= ANY($1)`), so this is not a bind-parameter limit — it
|
|
@@ -123,7 +124,7 @@ export function neededParentKeyFields(parentMeta, withClause) {
|
|
|
123
124
|
}
|
|
124
125
|
continue;
|
|
125
126
|
}
|
|
126
|
-
const rel = parentMeta.relations
|
|
127
|
+
const rel = ownLookup(parentMeta.relations, relName);
|
|
127
128
|
if (!rel)
|
|
128
129
|
continue; // unknown relation — the join path throws; let the loader surface it
|
|
129
130
|
for (const col of localKeyColumns(rel)) {
|
|
@@ -161,7 +162,7 @@ export function resolveCountRelations(parentMeta, countSpec) {
|
|
|
161
162
|
for (const [relName, enabled] of Object.entries(countSpec)) {
|
|
162
163
|
if (!enabled)
|
|
163
164
|
continue;
|
|
164
|
-
const rel = parentMeta.relations
|
|
165
|
+
const rel = ownLookup(parentMeta.relations, relName);
|
|
165
166
|
if (!rel) {
|
|
166
167
|
throw new RelationError(`[turbine] Unknown relation "${relName}" in _count on table "${parentMeta.name}". ` +
|
|
167
168
|
`Available: ${Object.keys(parentMeta.relations).join(', ')}`);
|
|
@@ -232,7 +233,7 @@ export async function loadRelationsBatched(ctx, parents, withClause, timeout, de
|
|
|
232
233
|
loads.push(loadCounts(ctx, parents, spec));
|
|
233
234
|
continue;
|
|
234
235
|
}
|
|
235
|
-
const rel = ctx.parentMeta.relations
|
|
236
|
+
const rel = ownLookup(ctx.parentMeta.relations, relName);
|
|
236
237
|
if (!rel) {
|
|
237
238
|
throw new ValidationError(`[turbine] Unknown relation "${relName}" on table "${ctx.parentMeta.name}". ` +
|
|
238
239
|
`Available: ${Object.keys(ctx.parentMeta.relations).join(', ')}`);
|
package/dist/query/builder.js
CHANGED
|
@@ -18,7 +18,7 @@ import * as aggMod from './aggregates.js';
|
|
|
18
18
|
import { includeKeysForBatching, loadRelationsBatched, neededParentKeyFields, rejectNestedPickOrder, stripFields, } from './batched-loader.js';
|
|
19
19
|
import { isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, isWhereOperator, sortedEntries, } from './filters.js';
|
|
20
20
|
import * as relationsMod from './relations.js';
|
|
21
|
-
import { LRUCache, parseDbDate, sqlToPreparedName } from './utils.js';
|
|
21
|
+
import { LRUCache, ownLookup, parseDbDate, sqlToPreparedName } from './utils.js';
|
|
22
22
|
import * as whereMod from './where.js';
|
|
23
23
|
import * as writesMod from './writes.js';
|
|
24
24
|
/**
|
|
@@ -856,7 +856,7 @@ export class QueryInterface {
|
|
|
856
856
|
!whereObj.NOT &&
|
|
857
857
|
whereKeys.every((k) => {
|
|
858
858
|
const v = whereObj[k];
|
|
859
|
-
return v !== null && !isWhereOperator(v) && !this.tableMeta.relations
|
|
859
|
+
return v !== null && !isWhereOperator(v) && !ownLookup(this.tableMeta.relations, k);
|
|
860
860
|
});
|
|
861
861
|
// Simple path: plain equality, no operators/null/OR
|
|
862
862
|
if (!args.with && isSimpleWhere) {
|
|
@@ -1095,7 +1095,7 @@ export class QueryInterface {
|
|
|
1095
1095
|
const withFp = args?.with ? this.withFingerprint(args.with) : '';
|
|
1096
1096
|
const orderFp = args?.orderBy
|
|
1097
1097
|
? Object.entries(args.orderBy)
|
|
1098
|
-
.map(([k, d]) => `${k}:${this.orderByEntryFingerprint(d, this.tableMeta.relations
|
|
1098
|
+
.map(([k, d]) => `${k}:${this.orderByEntryFingerprint(d, ownLookup(this.tableMeta.relations, k)?.to)}`)
|
|
1099
1099
|
.join(',')
|
|
1100
1100
|
: '';
|
|
1101
1101
|
const cursorFp = args?.cursor
|
|
@@ -1681,7 +1681,11 @@ export class QueryInterface {
|
|
|
1681
1681
|
}
|
|
1682
1682
|
/** Convert camelCase field name to snake_case column name (unquoted, for non-SQL uses) */
|
|
1683
1683
|
toColumn(field) {
|
|
1684
|
-
|
|
1684
|
+
// Prototype-safe lookup: a plain-object `columnMap` would otherwise return
|
|
1685
|
+
// an inherited member (e.g. Object.prototype.constructor) for a field named
|
|
1686
|
+
// "constructor" / "toString" / "__proto__", bypassing the unknown-field
|
|
1687
|
+
// check below and returning a non-string as the column name.
|
|
1688
|
+
const mapped = ownLookup(this.tableMeta.columnMap, field);
|
|
1685
1689
|
if (mapped)
|
|
1686
1690
|
return mapped;
|
|
1687
1691
|
// Fall back to camelToSnake ONLY if that snake_cased name also exists as a
|
|
@@ -1691,7 +1695,7 @@ export class QueryInterface {
|
|
|
1691
1695
|
// SQL injection and catching typos like `where: { emial: 'x' }` with a
|
|
1692
1696
|
// clear error instead of a cryptic Postgres "column does not exist".
|
|
1693
1697
|
const snake = camelToSnake(field);
|
|
1694
|
-
if (this.tableMeta.reverseColumnMap
|
|
1698
|
+
if (this.tableMeta.reverseColumnMap && ownLookup(this.tableMeta.reverseColumnMap, snake)) {
|
|
1695
1699
|
return snake;
|
|
1696
1700
|
}
|
|
1697
1701
|
if (this.tableMeta.allColumns?.includes(snake)) {
|
|
@@ -1786,7 +1790,7 @@ export class QueryInterface {
|
|
|
1786
1790
|
// pick.where / pick.orderBy paths). To-one relation orderBy carries the
|
|
1787
1791
|
// target's global filter once per ordered column.
|
|
1788
1792
|
if (this.isRelationOrderByValue(dir)) {
|
|
1789
|
-
const relDef = this.tableMeta.relations
|
|
1793
|
+
const relDef = ownLookup(this.tableMeta.relations, key);
|
|
1790
1794
|
if (relDef && isRelationPickOrderBy(dir)) {
|
|
1791
1795
|
this.collectRelationPickOrderParams(key, relDef, dir, params);
|
|
1792
1796
|
}
|
package/dist/query/relations.js
CHANGED
|
@@ -17,6 +17,7 @@ import { missingIndexForRelation } from '../index-advisor.js';
|
|
|
17
17
|
import { camelToSnake, normalizeKeyColumns, snakeToCamel } from '../schema.js';
|
|
18
18
|
import { resolveCountRelations } from './batched-loader.js';
|
|
19
19
|
import { isJsonPathOrderBy, isOrderBySpec, isRelationPickOrderBy, isVectorOrderBy, normalizeOrderBy, sortedEntries, } from './filters.js';
|
|
20
|
+
import { ownLookup } from './utils.js';
|
|
20
21
|
import * as whereMod from './where.js';
|
|
21
22
|
import * as writesMod from './writes.js';
|
|
22
23
|
/** Relations already warned about missing FK indexes (once per process, dev only). */
|
|
@@ -317,10 +318,10 @@ export function buildOrderBy(qi, orderBy, params, lateralSink) {
|
|
|
317
318
|
// are validated in the relation branch below, so skip them here.
|
|
318
319
|
if (process.env.NODE_ENV !== 'production') {
|
|
319
320
|
for (const [key, value] of Object.entries(orderBy)) {
|
|
320
|
-
if (isRelationOrderByValue(qi, value) && qi.tableMeta.relations
|
|
321
|
+
if (isRelationOrderByValue(qi, value) && ownLookup(qi.tableMeta.relations, key))
|
|
321
322
|
continue;
|
|
322
323
|
const snakeKey = camelToSnake(key);
|
|
323
|
-
if (!qi.tableMeta.columns.some((c) => c.name === snakeKey) && !(
|
|
324
|
+
if (!qi.tableMeta.columns.some((c) => c.name === snakeKey) && !Object.hasOwn(qi.tableMeta.columnMap, key)) {
|
|
324
325
|
console.warn(`[turbine] Unknown orderBy field "${key}" for table "${qi.tableMeta.name}". ` +
|
|
325
326
|
'This will cause a runtime error.');
|
|
326
327
|
}
|
|
@@ -402,7 +403,7 @@ export function nullsSuffix(qi, nulls) {
|
|
|
402
403
|
* camelCase-named DB columns like "sortOrder").
|
|
403
404
|
*/
|
|
404
405
|
export function resolveOrderByColumn(_qi, table, meta, key) {
|
|
405
|
-
const col = meta.columnMap
|
|
406
|
+
const col = ownLookup(meta.columnMap, key) ?? camelToSnake(key);
|
|
406
407
|
if (!meta.allColumns.includes(col)) {
|
|
407
408
|
throw new ValidationError(`[turbine] Unknown field "${key}" in orderBy on table "${table}". ` +
|
|
408
409
|
`Known fields: ${Object.keys(meta.columnMap).join(', ') || '(none)'}.`);
|
|
@@ -520,7 +521,7 @@ export function buildRelationOrderBy(qi, relName, value, alias, params, ctx, lat
|
|
|
520
521
|
.map(([col, dirValue]) => {
|
|
521
522
|
// columnMap-first resolution (camelToSnake fallback): mirrors the
|
|
522
523
|
// scalar orderBy path so camelCase-named DB columns resolve here too.
|
|
523
|
-
const snakeCol = targetMeta.columnMap
|
|
524
|
+
const snakeCol = ownLookup(targetMeta.columnMap, col) ?? camelToSnake(col);
|
|
524
525
|
if (!targetMeta.allColumns.includes(snakeCol)) {
|
|
525
526
|
throw new ValidationError(`[turbine] Unknown column "${col}" in orderBy on relation "${relName}" (table "${relDef.to}").`);
|
|
526
527
|
}
|
|
@@ -988,7 +989,7 @@ export function resolveTargetColumns(qi, spec, targetMeta, includePii) {
|
|
|
988
989
|
// opt-in and comes back regardless of the query's `includePii`.
|
|
989
990
|
const selectedFields = Object.entries(spec.select)
|
|
990
991
|
.filter(([, v]) => v)
|
|
991
|
-
.map(([k]) => targetMeta.columnMap
|
|
992
|
+
.map(([k]) => ownLookup(targetMeta.columnMap, k) ?? camelToSnake(k));
|
|
992
993
|
return selectedFields.filter((col) => targetMeta.allColumns.includes(col));
|
|
993
994
|
}
|
|
994
995
|
// Default / omit-only relation projection: PII columns are excluded unless
|
|
@@ -998,7 +999,7 @@ export function resolveTargetColumns(qi, spec, targetMeta, includePii) {
|
|
|
998
999
|
if (spec !== true && spec.omit) {
|
|
999
1000
|
const omittedFields = new Set(Object.entries(spec.omit)
|
|
1000
1001
|
.filter(([, v]) => v)
|
|
1001
|
-
.map(([k]) => targetMeta.columnMap
|
|
1002
|
+
.map(([k]) => ownLookup(targetMeta.columnMap, k) ?? camelToSnake(k)));
|
|
1002
1003
|
return targetMeta.allColumns.filter((col) => !omittedFields.has(col) && !(hasPii && piiCols.has(col)));
|
|
1003
1004
|
}
|
|
1004
1005
|
if (hasPii) {
|
package/dist/query/utils.d.ts
CHANGED
|
@@ -13,6 +13,16 @@
|
|
|
13
13
|
* quoteIdent('user name') → '"user name"'
|
|
14
14
|
*/
|
|
15
15
|
export declare function quoteIdent(name: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Prototype-safe own-property read for the plain metadata maps (columnMap,
|
|
18
|
+
* relations, reverseColumnMap). These are constructed as plain objects, so a
|
|
19
|
+
* bare `map[key]` for a user-supplied field name like "constructor",
|
|
20
|
+
* "toString", or "__proto__" returns an inherited member from
|
|
21
|
+
* `Object.prototype` — a truthy value that slips past validation and produces a
|
|
22
|
+
* cryptic `TypeError` instead of a clean `ValidationError`. Returns `undefined`
|
|
23
|
+
* unless `key` is an OWN enumerable/non-enumerable property.
|
|
24
|
+
*/
|
|
25
|
+
export declare function ownLookup<T>(map: Record<string, T>, key: string): T | undefined;
|
|
16
26
|
/**
|
|
17
27
|
* Escape single quotes for use as string keys in json_build_object().
|
|
18
28
|
* Doubles single quotes per SQL quoting rules.
|
package/dist/query/utils.js
CHANGED
|
@@ -18,6 +18,18 @@
|
|
|
18
18
|
export function quoteIdent(name) {
|
|
19
19
|
return `"${name.replace(/"/g, '""')}"`;
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Prototype-safe own-property read for the plain metadata maps (columnMap,
|
|
23
|
+
* relations, reverseColumnMap). These are constructed as plain objects, so a
|
|
24
|
+
* bare `map[key]` for a user-supplied field name like "constructor",
|
|
25
|
+
* "toString", or "__proto__" returns an inherited member from
|
|
26
|
+
* `Object.prototype` — a truthy value that slips past validation and produces a
|
|
27
|
+
* cryptic `TypeError` instead of a clean `ValidationError`. Returns `undefined`
|
|
28
|
+
* unless `key` is an OWN enumerable/non-enumerable property.
|
|
29
|
+
*/
|
|
30
|
+
export function ownLookup(map, key) {
|
|
31
|
+
return Object.hasOwn(map, key) ? map[key] : undefined;
|
|
32
|
+
}
|
|
21
33
|
/**
|
|
22
34
|
* Escape single quotes for use as string keys in json_build_object().
|
|
23
35
|
* Doubles single quotes per SQL quoting rules.
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* step.
|
|
35
35
|
*/
|
|
36
36
|
import { findArrayUniqueKey, findJsonUniqueKey, fingerprintArrayFilterShape, fingerprintJsonFilterShape, fingerprintOperatorShape, isArrayFilter, isJsonFilter, isTextSearchFilter, isUnmatchedPlainObject, isVectorFilter, isWhereOperator, sortedKeys, VECTOR_DISTANCE_COMPARATORS, } from './filters.js';
|
|
37
|
+
import { ownLookup } from './utils.js';
|
|
37
38
|
/** True when a normalized relation filter carries at least one cardinality key. */
|
|
38
39
|
function isRelationFilterObj(filterObj) {
|
|
39
40
|
return ('some' in filterObj || 'every' in filterObj || 'none' in filterObj || 'is' in filterObj || 'isNot' in filterObj);
|
|
@@ -73,7 +74,7 @@ export function walkWhere(host, where) {
|
|
|
73
74
|
events.push({ kind: 'not', condition: value });
|
|
74
75
|
continue;
|
|
75
76
|
}
|
|
76
|
-
const relDef = host.tableMeta.relations
|
|
77
|
+
const relDef = ownLookup(host.tableMeta.relations, key);
|
|
77
78
|
if (relDef && typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
78
79
|
const filterObj = host.normalizeRelationFilter(relDef, value);
|
|
79
80
|
if (isRelationFilterObj(filterObj)) {
|
package/dist/query/where.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
14
14
|
import { camelToSnake } from '../schema.js';
|
|
15
15
|
import { assertBindableEqualsOperand, findArrayUniqueKey, findJsonUniqueKey, isArrayFilter, isColumnRef, isJsonFilter, isUnmatchedPlainObject, isWhereOperator, JSON_RANGE_OPERATORS, VECTOR_DISTANCE_COMPARATORS, VECTOR_METRIC_OPERATORS, validateTextSearchConfig, } from './filters.js';
|
|
16
|
-
import { escapeLike, OPERATOR_KEYS } from './utils.js';
|
|
16
|
+
import { escapeLike, OPERATOR_KEYS, ownLookup } from './utils.js';
|
|
17
17
|
import { classifyScalarForSql, fingerprintScalarToken, walkWhere, } from './where-compile.js';
|
|
18
18
|
/**
|
|
19
19
|
* Produce a value-invariant fingerprint of a where clause.
|
|
@@ -663,7 +663,7 @@ export function buildScopedWhere(qi, scope, where, params) {
|
|
|
663
663
|
*/
|
|
664
664
|
export function buildScopedScalarClause(qi, scope, field, value, params, clauses) {
|
|
665
665
|
const meta = scope.meta;
|
|
666
|
-
const col = meta.columnMap
|
|
666
|
+
const col = ownLookup(meta.columnMap, field) ?? camelToSnake(field);
|
|
667
667
|
if (!meta.allColumns.includes(col))
|
|
668
668
|
throw scope.unknownColumn(field);
|
|
669
669
|
const qCol = `${scope.qualifier}${qi.q(col)}`;
|
|
@@ -740,7 +740,7 @@ export function collectScopedScalarParams(qi, scope, field, value, params) {
|
|
|
740
740
|
if (value === null)
|
|
741
741
|
return;
|
|
742
742
|
const meta = scope.meta;
|
|
743
|
-
const col = meta.columnMap
|
|
743
|
+
const col = ownLookup(meta.columnMap, field) ?? camelToSnake(field);
|
|
744
744
|
if (typeof value === 'object' && !Array.isArray(value) && isJsonFilter(value)) {
|
|
745
745
|
const colType = pgTypeForColumn(qi, meta, col);
|
|
746
746
|
if (isJsonColumnType(qi, colType)) {
|
|
@@ -1011,7 +1011,7 @@ export function resolveColumnRef(_qi, ref, ctx, mode) {
|
|
|
1011
1011
|
`Case-insensitive column-to-column comparison is not supported: use client.sql\`...\` ` +
|
|
1012
1012
|
`for lower(a) = lower(b).`);
|
|
1013
1013
|
}
|
|
1014
|
-
const col = ctx.meta.columnMap
|
|
1014
|
+
const col = ownLookup(ctx.meta.columnMap, ref.col) ?? camelToSnake(ref.col);
|
|
1015
1015
|
if (!ctx.meta.allColumns.includes(col)) {
|
|
1016
1016
|
throw new ValidationError(`[turbine] Unknown field "${ref.col}" referenced by { col } in where on table "${ctx.table}". ` +
|
|
1017
1017
|
`Known fields: ${Object.keys(ctx.meta.columnMap).join(', ') || '(none)'}.`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.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": {
|