turbine-orm 0.28.0 → 0.28.2

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.
@@ -0,0 +1,232 @@
1
+ /**
2
+ * turbine-orm — Where-filter type guards and shape helpers
3
+ *
4
+ * Pure detection / fingerprint utilities used by the query builder's WHERE
5
+ * compiler. Kept out of builder.ts so the class file stays about SQL assembly
6
+ * and execution rather than filter-shape bookkeeping.
7
+ */
8
+ import { ValidationError } from '../errors.js';
9
+ import { OPERATOR_KEYS } from './utils.js';
10
+ // ---------------------------------------------------------------------------
11
+ // Where-operator detection
12
+ // ---------------------------------------------------------------------------
13
+ /** Check if a value is a where operator object (has at least one known operator key) */
14
+ export function isWhereOperator(value) {
15
+ if (value === null ||
16
+ value === undefined ||
17
+ typeof value !== 'object' ||
18
+ Array.isArray(value) ||
19
+ value instanceof Date) {
20
+ return false;
21
+ }
22
+ const keys = Object.keys(value);
23
+ return keys.length > 0 && keys.every((k) => OPERATOR_KEYS.has(k));
24
+ }
25
+ /**
26
+ * True for a *plain object literal* that reached an equality fallthrough
27
+ * without matching any known filter shape — the misspelled-operator case.
28
+ * Class instances (Buffer for bytea, Decimal wrappers, ...) are legitimate
29
+ * bind values and return false, as do arrays and Dates.
30
+ */
31
+ export function isUnmatchedPlainObject(value) {
32
+ if (typeof value !== 'object' || value === null || Array.isArray(value) || value instanceof Date)
33
+ return false;
34
+ if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value))
35
+ return false;
36
+ const proto = Object.getPrototypeOf(value);
37
+ return proto === Object.prototype || proto === null;
38
+ }
39
+ /**
40
+ * Fingerprint the SHAPE of a where-operator object. Null-valued `equals` /
41
+ * `not` compile to parameterless `IS NULL` / `IS NOT NULL` (different SQL, no
42
+ * param pushed), so null-ness is part of the shape — without it a cache entry
43
+ * warmed by `{ not: 5 }` would serve `{ not: null }` with a desynced param list.
44
+ */
45
+ export function fingerprintOperatorShape(value) {
46
+ const obj = value;
47
+ const opKeys = Object.keys(obj)
48
+ .filter((k) => k !== 'mode')
49
+ .map((k) => ((k === 'equals' || k === 'not') && obj[k] === null ? `${k}:null` : k))
50
+ .sort();
51
+ const modeStr = value.mode === 'insensitive' ? ':i' : '';
52
+ return `op(${opKeys.join(',')}${modeStr})`;
53
+ }
54
+ /**
55
+ * Guard for the value of an `equals` operator reaching the plain-equality
56
+ * operator path. A plain object literal can only legitimately be an equality
57
+ * value on a json/jsonb column — and those route to the JSONB filter branch
58
+ * BEFORE the operator branch, so any plain object that reaches here is a
59
+ * mistake (e.g. `{ equals: { foo: 1 } }` on a text column). Shared by the
60
+ * SQL-build path and the cache-hit param-collect path so a warmed cache can
61
+ * never skip the check.
62
+ */
63
+ export function assertBindableEqualsOperand(value, column) {
64
+ if (!isUnmatchedPlainObject(value))
65
+ return;
66
+ throw new ValidationError(`[turbine] Plain-object value for operator 'equals' on ${column}: ` +
67
+ `objects are only valid 'equals' values on JSON (json/jsonb) columns, ` +
68
+ `where 'equals' is the JSONB containment filter.`);
69
+ }
70
+ /**
71
+ * Object keys in sorted order, mirroring the canonical order used by every
72
+ * cache fingerprint. The SQL-build and cache-hit param-collect paths MUST
73
+ * enumerate object keys in this exact order: fingerprints sort keys, so two
74
+ * where clauses with the same fields in different insertion order share one
75
+ * cache entry — if build/collect iterated insertion order, the cached SQL's
76
+ * `$N` placeholders would bind the wrong values (cross-tenant-leak class).
77
+ * Array order (OR/AND members) is positional and is never sorted.
78
+ */
79
+ export function sortedKeys(obj) {
80
+ return Object.keys(obj).sort();
81
+ }
82
+ /** {@link sortedKeys}, but yielding `[key, value]` pairs. */
83
+ export function sortedEntries(obj) {
84
+ return Object.entries(obj).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
85
+ }
86
+ // ---------------------------------------------------------------------------
87
+ // Atomic-update / JSONB / Array / text-search / vector key sets
88
+ // ---------------------------------------------------------------------------
89
+ /** Known atomic-update operator keys — used to detect operator objects vs plain JSON values */
90
+ export const UPDATE_OPERATOR_KEYS = new Set(['set', 'increment', 'decrement', 'multiply', 'divide']);
91
+ /** Known JSONB operator keys */
92
+ export const JSONB_OPERATOR_KEYS = new Set(['path', 'equals', 'contains', 'hasKey']);
93
+ /**
94
+ * JSONB operator keys that are *unique* to {@link JsonFilter} — they cannot
95
+ * appear in any other where-filter shape, so the presence of one of these is
96
+ * an unambiguous signal that the user meant a JSON filter. Used by the
97
+ * strict-validation path so that `{ contains: 'foo' }` (which is also a valid
98
+ * `WhereOperator` for LIKE) is not misclassified. Note `equals` is NOT in this
99
+ * set: on non-JSON columns it is a plain equality operator (`WhereOperator`),
100
+ * so it must fall through instead of throwing.
101
+ */
102
+ export const JSONB_UNIQUE_KEYS = new Set(['path', 'hasKey']);
103
+ /** Check if a value is a JSONB filter object */
104
+ export function isJsonFilter(value) {
105
+ if (value === null ||
106
+ value === undefined ||
107
+ typeof value !== 'object' ||
108
+ Array.isArray(value) ||
109
+ value instanceof Date) {
110
+ return false;
111
+ }
112
+ const keys = Object.keys(value);
113
+ return keys.length > 0 && keys.some((k) => JSONB_OPERATOR_KEYS.has(k));
114
+ }
115
+ /**
116
+ * Returns the first JSON-unique key found in `value`, or `null` if none.
117
+ * Used to drive the strict-validation error message.
118
+ */
119
+ export function findJsonUniqueKey(value) {
120
+ for (const k of Object.keys(value)) {
121
+ if (JSONB_UNIQUE_KEYS.has(k))
122
+ return k;
123
+ }
124
+ return null;
125
+ }
126
+ /** Known Array operator keys */
127
+ export const ARRAY_OPERATOR_KEYS = new Set(['has', 'hasEvery', 'hasSome', 'isEmpty']);
128
+ /**
129
+ * Array operator keys that are *unique* to {@link ArrayFilter}. None of the
130
+ * array operators currently overlap with `WhereOperator` or `JsonFilter`, so
131
+ * this set equals {@link ARRAY_OPERATOR_KEYS}; it is kept as a separate
132
+ * constant so a future overlap (e.g. a `contains` for arrays) is easy to
133
+ * carve out.
134
+ */
135
+ export const ARRAY_UNIQUE_KEYS = new Set(['has', 'hasEvery', 'hasSome', 'isEmpty']);
136
+ /** Check if a value is an Array filter object */
137
+ export function isArrayFilter(value) {
138
+ if (value === null ||
139
+ value === undefined ||
140
+ typeof value !== 'object' ||
141
+ Array.isArray(value) ||
142
+ value instanceof Date) {
143
+ return false;
144
+ }
145
+ const keys = Object.keys(value);
146
+ return keys.length > 0 && keys.some((k) => ARRAY_OPERATOR_KEYS.has(k));
147
+ }
148
+ /**
149
+ * Returns the first array-unique key found in `value`, or `null` if none.
150
+ * Used to drive the strict-validation error message.
151
+ */
152
+ export function findArrayUniqueKey(value) {
153
+ for (const k of Object.keys(value)) {
154
+ if (ARRAY_UNIQUE_KEYS.has(k))
155
+ return k;
156
+ }
157
+ return null;
158
+ }
159
+ /** Known text search operator keys */
160
+ export const TEXT_SEARCH_KEYS = new Set(['search', 'config']);
161
+ /** Check if a value is a TextSearchFilter object */
162
+ export function isTextSearchFilter(value) {
163
+ if (value === null ||
164
+ value === undefined ||
165
+ typeof value !== 'object' ||
166
+ Array.isArray(value) ||
167
+ value instanceof Date) {
168
+ return false;
169
+ }
170
+ const keys = Object.keys(value);
171
+ // Must have 'search' key and only known text search keys
172
+ return keys.includes('search') && keys.every((k) => TEXT_SEARCH_KEYS.has(k));
173
+ }
174
+ /**
175
+ * Validate a text search config name. Only alphanumeric characters and
176
+ * underscores are allowed to prevent SQL injection via the config parameter.
177
+ */
178
+ export function validateTextSearchConfig(config) {
179
+ return /^[a-zA-Z0-9_]+$/.test(config);
180
+ }
181
+ /**
182
+ * pgvector distance metric → operator allow-list. This is the ONLY mapping
183
+ * from a user-supplied metric token to a SQL operator; any token not present
184
+ * here is rejected, so a user value can never become an arbitrary operator.
185
+ *
186
+ * - `l2` → `<->` (Euclidean / L2 distance)
187
+ * - `cosine` → `<=>` (cosine distance)
188
+ * - `ip` → `<#>` (negative inner product)
189
+ */
190
+ export const VECTOR_METRIC_OPERATORS = {
191
+ l2: '<->',
192
+ cosine: '<=>',
193
+ ip: '<#>',
194
+ };
195
+ /** Comparison keys allowed on a {@link VectorDistanceFilter}. */
196
+ export const VECTOR_DISTANCE_COMPARATORS = {
197
+ lt: '<',
198
+ lte: '<=',
199
+ gt: '>',
200
+ gte: '>=',
201
+ };
202
+ /** Check if a value is a vector distance WHERE filter: `{ distance: { to, metric } }` */
203
+ export function isVectorFilter(value) {
204
+ if (value === null || typeof value !== 'object' || Array.isArray(value) || value instanceof Date) {
205
+ return false;
206
+ }
207
+ const dist = value.distance;
208
+ return (typeof dist === 'object' &&
209
+ dist !== null &&
210
+ !Array.isArray(dist) &&
211
+ 'to' in dist &&
212
+ 'metric' in dist);
213
+ }
214
+ /** Check if an orderBy value is a vector KNN ordering: `{ distance: { to, metric } }` */
215
+ export function isVectorOrderBy(value) {
216
+ return isVectorFilter(value);
217
+ }
218
+ /** Check if an orderBy value is an explicit `{ sort, nulls? }` spec. */
219
+ export function isOrderBySpec(value) {
220
+ return typeof value === 'object' && value !== null && !Array.isArray(value) && 'sort' in value;
221
+ }
222
+ /**
223
+ * Normalize an orderBy value into `{ direction, nulls }`. Accepts a plain
224
+ * direction string or an {@link OrderBySpec}. Used by every ORDER BY compile
225
+ * path (findMany, groupBy, relation inner subqueries).
226
+ */
227
+ export function normalizeOrderBy(value) {
228
+ if (isOrderBySpec(value)) {
229
+ return { dir: value.sort.toLowerCase() === 'desc' ? 'DESC' : 'ASC', nulls: value.nulls };
230
+ }
231
+ return { dir: String(value).toLowerCase() === 'desc' ? 'DESC' : 'ASC' };
232
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbine-orm",
3
- "version": "0.28.0",
3
+ "version": "0.28.2",
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": {
@@ -91,7 +91,7 @@
91
91
  "pretest:coverage": "npm run gen:studio"
92
92
  },
93
93
  "engines": {
94
- "node": ">=18.0.0"
94
+ "node": ">=20.0.0"
95
95
  },
96
96
  "dependencies": {
97
97
  "@types/pg": "^8.11.11",