turbine-orm 0.36.1 → 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 +6 -4
- package/dist/cjs/cli/index.js +53 -25
- package/dist/cjs/cli/studio-demo.js +310 -0
- package/dist/cjs/cli/studio-ui.generated.js +1 -1
- package/dist/cjs/cli/studio.js +502 -127
- 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/index.d.ts +3 -1
- package/dist/cli/index.js +53 -25
- package/dist/cli/studio-demo.d.ts +43 -0
- package/dist/cli/studio-demo.js +306 -0
- package/dist/cli/studio-ui.generated.js +1 -1
- package/dist/cli/studio.d.ts +58 -8
- package/dist/cli/studio.js +501 -127
- 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
package/dist/errors.d.ts
CHANGED
|
@@ -57,6 +57,17 @@ export type ErrorMessageMode = 'safe' | 'verbose';
|
|
|
57
57
|
export declare function setErrorMessageMode(mode: ErrorMessageMode): void;
|
|
58
58
|
/** Returns the current NotFoundError message mode. Exported for tests. */
|
|
59
59
|
export declare function getErrorMessageMode(): ErrorMessageMode;
|
|
60
|
+
/**
|
|
61
|
+
* Render a user-supplied `where` / `connect` target for a "no row found" error
|
|
62
|
+
* message, honoring the global {@link ErrorMessageMode}. In 'safe' mode (the
|
|
63
|
+
* default) only the key names are shown (`keys [email, id]`) so that PII values
|
|
64
|
+
* never leak into logs; in 'verbose' mode the full JSON serialization is used.
|
|
65
|
+
*
|
|
66
|
+
* This mirrors {@link NotFoundError}'s redaction so that every "no row found"
|
|
67
|
+
* message in the library follows one convention, including the nested-write
|
|
68
|
+
* connect/update failures which historically embedded the raw values.
|
|
69
|
+
*/
|
|
70
|
+
export declare function describeTargetForMessage(target: unknown): string;
|
|
60
71
|
/**
|
|
61
72
|
* Thrown when a record is not found (findUniqueOrThrow, findFirstOrThrow,
|
|
62
73
|
* update/delete against a non-matching row, etc.)
|
package/dist/errors.js
CHANGED
|
@@ -65,6 +65,32 @@ export function setErrorMessageMode(mode) {
|
|
|
65
65
|
export function getErrorMessageMode() {
|
|
66
66
|
return errorMessageMode;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Render a user-supplied `where` / `connect` target for a "no row found" error
|
|
70
|
+
* message, honoring the global {@link ErrorMessageMode}. In 'safe' mode (the
|
|
71
|
+
* default) only the key names are shown (`keys [email, id]`) so that PII values
|
|
72
|
+
* never leak into logs; in 'verbose' mode the full JSON serialization is used.
|
|
73
|
+
*
|
|
74
|
+
* This mirrors {@link NotFoundError}'s redaction so that every "no row found"
|
|
75
|
+
* message in the library follows one convention, including the nested-write
|
|
76
|
+
* connect/update failures which historically embedded the raw values.
|
|
77
|
+
*/
|
|
78
|
+
export function describeTargetForMessage(target) {
|
|
79
|
+
if (errorMessageMode === 'verbose') {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.stringify(target);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return '[unserializable]';
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// safe mode: key names only
|
|
88
|
+
if (target === null || target === undefined || typeof target !== 'object') {
|
|
89
|
+
return 'keys []';
|
|
90
|
+
}
|
|
91
|
+
const keys = Object.keys(target);
|
|
92
|
+
return `keys [${keys.join(', ')}]`;
|
|
93
|
+
}
|
|
68
94
|
/**
|
|
69
95
|
* Render a `where` clause for error messages. In 'safe' mode (the default),
|
|
70
96
|
* only the keys are shown; values are stripped to avoid leaking PII into logs.
|
package/dist/nested-write.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* `client.ts` directly — the transaction handle is passed in via
|
|
12
12
|
* `NestedWriteContext`.
|
|
13
13
|
*/
|
|
14
|
-
import { CircularRelationError, RelationError, ValidationError } from './errors.js';
|
|
14
|
+
import { CircularRelationError, describeTargetForMessage, RelationError, ValidationError } from './errors.js';
|
|
15
15
|
import { normalizeKeyColumns } from './schema.js';
|
|
16
16
|
const MAX_DEPTH = 10;
|
|
17
17
|
const CREATE_ONLY_OPS = new Set(['create', 'connect', 'connectOrCreate']);
|
|
@@ -32,7 +32,7 @@ export function extractRelationFields(data, tableMeta) {
|
|
|
32
32
|
const scalars = {};
|
|
33
33
|
const relations = {};
|
|
34
34
|
for (const [key, value] of Object.entries(data)) {
|
|
35
|
-
if (
|
|
35
|
+
if (Object.hasOwn(tableMeta.relations, key) &&
|
|
36
36
|
value !== null &&
|
|
37
37
|
typeof value === 'object' &&
|
|
38
38
|
!Array.isArray(value) &&
|
|
@@ -52,7 +52,7 @@ export function extractRelationFields(data, tableMeta) {
|
|
|
52
52
|
*/
|
|
53
53
|
export function hasRelationFields(data, tableMeta) {
|
|
54
54
|
for (const key of Object.keys(data)) {
|
|
55
|
-
if (
|
|
55
|
+
if (Object.hasOwn(tableMeta.relations, key)) {
|
|
56
56
|
const val = data[key];
|
|
57
57
|
if (val !== null && typeof val === 'object' && !Array.isArray(val) && !(val instanceof Date)) {
|
|
58
58
|
return true;
|
|
@@ -207,7 +207,7 @@ export async function executeNestedUpdate(ctx, tableName, where, data, depth = 0
|
|
|
207
207
|
else {
|
|
208
208
|
parentRow = (await ctx.tx.table(tableName).findUnique({ where }));
|
|
209
209
|
if (!parentRow) {
|
|
210
|
-
throw new ValidationError(`[turbine] update: no ${tableName} row found matching ${
|
|
210
|
+
throw new ValidationError(`[turbine] update: no ${tableName} row found matching ${describeTargetForMessage(where)}.`);
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
213
|
// Process each relation
|
|
@@ -290,7 +290,7 @@ async function processHasManyCreate(ctx, rel, ops, parentRow, depth, path, relNa
|
|
|
290
290
|
if (items.length > 0) {
|
|
291
291
|
// Check if any items have nested relations (need per-row recursion)
|
|
292
292
|
const childTable = ctx.schema.tables[rel.to];
|
|
293
|
-
const hasNested = childTable && items.some((item) => Object.keys(item).some((k) =>
|
|
293
|
+
const hasNested = childTable && items.some((item) => Object.keys(item).some((k) => Object.hasOwn(childTable.relations ?? {}, k)));
|
|
294
294
|
if (hasNested) {
|
|
295
295
|
// Per-row recursive create for items with nested relations
|
|
296
296
|
for (const item of items) {
|
|
@@ -350,7 +350,7 @@ async function resolveBelongsToForCreate(ctx, rel, ops, parentTable, depth, path
|
|
|
350
350
|
const target = items[0];
|
|
351
351
|
relatedRow = (await ctx.tx.table(rel.to).findUnique({ where: target }));
|
|
352
352
|
if (!relatedRow) {
|
|
353
|
-
throw new ValidationError(`[turbine] connect on "${relName}": no ${rel.to} row found matching ${
|
|
353
|
+
throw new ValidationError(`[turbine] connect on "${relName}": no ${rel.to} row found matching ${describeTargetForMessage(target)}.`);
|
|
354
354
|
}
|
|
355
355
|
}
|
|
356
356
|
}
|
|
@@ -405,7 +405,7 @@ async function processBelongsToCreate(ctx, rel, ops, parentRow, parentTable, dep
|
|
|
405
405
|
const target = items[0];
|
|
406
406
|
const existing = await ctx.tx.table(rel.to).findUnique({ where: target });
|
|
407
407
|
if (!existing) {
|
|
408
|
-
throw new ValidationError(`[turbine] connect on "${relName}": no ${rel.to} row found matching ${
|
|
408
|
+
throw new ValidationError(`[turbine] connect on "${relName}": no ${rel.to} row found matching ${describeTargetForMessage(target)}.`);
|
|
409
409
|
}
|
|
410
410
|
const updateData = {};
|
|
411
411
|
const relatedTable = ctx.schema.tables[rel.to];
|
|
@@ -435,7 +435,7 @@ async function batchConnect(ctx, rel, items, parentRow) {
|
|
|
435
435
|
for (const target of items) {
|
|
436
436
|
const existing = await ctx.tx.table(rel.to).findUnique({ where: target });
|
|
437
437
|
if (!existing) {
|
|
438
|
-
throw new ValidationError(`[turbine] connect: no ${rel.to} row found matching ${
|
|
438
|
+
throw new ValidationError(`[turbine] connect: no ${rel.to} row found matching ${describeTargetForMessage(target)}.`);
|
|
439
439
|
}
|
|
440
440
|
}
|
|
441
441
|
// Build FK update data to point children at parent
|
|
@@ -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": {
|