turbine-orm 0.14.0 → 0.16.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/dist/adapters/cockroachdb.js +1 -1
- package/dist/adapters/index.d.ts +7 -4
- package/dist/adapters/index.js +1 -1
- package/dist/adapters/yugabytedb.js +1 -1
- package/dist/cjs/adapters/cockroachdb.js +1 -1
- package/dist/cjs/adapters/index.js +1 -1
- package/dist/cjs/adapters/yugabytedb.js +1 -1
- package/dist/cjs/cli/index.js +64 -0
- package/dist/cjs/cli/observe-ui.js +182 -0
- package/dist/cjs/cli/observe.js +242 -0
- package/dist/cjs/cli/studio.js +45 -7
- package/dist/cjs/client.js +102 -1
- package/dist/cjs/errors.js +44 -1
- package/dist/cjs/generate.js +86 -0
- package/dist/cjs/index.js +10 -1
- package/dist/cjs/nested-write.js +557 -0
- package/dist/cjs/observe.js +145 -0
- package/dist/cjs/query/builder.js +271 -23
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +64 -0
- package/dist/cli/observe-ui.d.ts +2 -0
- package/dist/cli/observe-ui.js +180 -0
- package/dist/cli/observe.d.ts +20 -0
- package/dist/cli/observe.js +237 -0
- package/dist/cli/studio.d.ts +10 -2
- package/dist/cli/studio.js +45 -7
- package/dist/client.d.ts +32 -2
- package/dist/client.js +102 -2
- package/dist/errors.d.ts +23 -0
- package/dist/errors.js +41 -0
- package/dist/generate.js +86 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +4 -2
- package/dist/nested-write.d.ts +95 -0
- package/dist/nested-write.js +551 -0
- package/dist/observe.d.ts +36 -0
- package/dist/observe.js +141 -0
- package/dist/query/builder.d.ts +45 -12
- package/dist/query/builder.js +239 -24
- package/dist/query/index.d.ts +2 -2
- package/dist/query/types.d.ts +76 -8
- package/package.json +2 -2
package/dist/query/builder.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* metadata — nothing is hardcoded.
|
|
12
12
|
*/
|
|
13
13
|
import { postgresDialect } from '../dialect.js';
|
|
14
|
-
import { CircularRelationError, NotFoundError, RelationError, TimeoutError, ValidationError, wrapPgError, } from '../errors.js';
|
|
14
|
+
import { CircularRelationError, NotFoundError, OptimisticLockError, RelationError, TimeoutError, ValidationError, wrapPgError, } from '../errors.js';
|
|
15
|
+
import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from '../nested-write.js';
|
|
15
16
|
import { camelToSnake, snakeToCamel } from '../schema.js';
|
|
16
17
|
import { escapeLike, LRUCache, OPERATOR_KEYS, sqlToPreparedName } from './utils.js';
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
@@ -97,6 +98,28 @@ function findArrayUniqueKey(value) {
|
|
|
97
98
|
}
|
|
98
99
|
return null;
|
|
99
100
|
}
|
|
101
|
+
/** Known text search operator keys */
|
|
102
|
+
const TEXT_SEARCH_KEYS = new Set(['search', 'config']);
|
|
103
|
+
/** Check if a value is a TextSearchFilter object */
|
|
104
|
+
function isTextSearchFilter(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
|
+
// Must have 'search' key and only known text search keys
|
|
114
|
+
return keys.includes('search') && keys.every((k) => TEXT_SEARCH_KEYS.has(k));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Validate a text search config name. Only alphanumeric characters and
|
|
118
|
+
* underscores are allowed to prevent SQL injection via the config parameter.
|
|
119
|
+
*/
|
|
120
|
+
function validateTextSearchConfig(config) {
|
|
121
|
+
return /^[a-zA-Z0-9_]+$/.test(config);
|
|
122
|
+
}
|
|
100
123
|
// biome-ignore lint/complexity/noBannedTypes: {} means "no relations known" — intentional for untyped table access
|
|
101
124
|
export class QueryInterface {
|
|
102
125
|
pool;
|
|
@@ -128,6 +151,12 @@ export class QueryInterface {
|
|
|
128
151
|
columnArrayTypeMap;
|
|
129
152
|
/** Tracks tables that have already triggered a deep-with warning (one-time) */
|
|
130
153
|
deepWithWarned = new Set();
|
|
154
|
+
/** True when this QI runs inside an active transaction (set via _txScoped option). */
|
|
155
|
+
txScoped;
|
|
156
|
+
/** Original options reference — forwarded to child QIs in nested writes. */
|
|
157
|
+
options;
|
|
158
|
+
/** Set by executeWithMiddleware so queryWithTimeout can include it in events. */
|
|
159
|
+
currentAction = 'raw';
|
|
131
160
|
constructor(pool, table, schema, middlewares, options) {
|
|
132
161
|
this.pool = pool;
|
|
133
162
|
this.table = table;
|
|
@@ -146,6 +175,8 @@ export class QueryInterface {
|
|
|
146
175
|
this.preparedStatementsEnabled = options?.preparedStatements ?? true;
|
|
147
176
|
this.sqlCacheEnabled = options?.sqlCache !== false;
|
|
148
177
|
this.dialect = options?.dialect ?? postgresDialect;
|
|
178
|
+
this.txScoped = options?._txScoped ?? false;
|
|
179
|
+
this.options = options;
|
|
149
180
|
// Pre-compute column type lookup maps (TASK-26)
|
|
150
181
|
this.columnPgTypeMap = new Map();
|
|
151
182
|
this.columnArrayTypeMap = new Map();
|
|
@@ -207,12 +238,25 @@ export class QueryInterface {
|
|
|
207
238
|
resetUnlimitedWarnings() {
|
|
208
239
|
this.warnedTables.clear();
|
|
209
240
|
}
|
|
241
|
+
emitQueryEvent(sql, params, duration, action, rows, error) {
|
|
242
|
+
const onQuery = this.options?._onQuery;
|
|
243
|
+
if (!onQuery)
|
|
244
|
+
return;
|
|
245
|
+
try {
|
|
246
|
+
onQuery({ sql, params, duration, model: this.table, action, rows, timestamp: new Date(), error });
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
// Listener errors must never crash a query
|
|
250
|
+
}
|
|
251
|
+
}
|
|
210
252
|
/**
|
|
211
253
|
* Execute a pool.query with an optional timeout.
|
|
212
254
|
* If timeout is set, races the query against a timer and rejects on expiry.
|
|
213
255
|
* pg driver errors are translated to typed Turbine errors via wrapPgError.
|
|
214
256
|
*/
|
|
215
257
|
async queryWithTimeout(sql, params, timeout, preparedName) {
|
|
258
|
+
const start = performance.now();
|
|
259
|
+
const action = this.currentAction;
|
|
216
260
|
// Build the query argument — use object form with `name` for prepared
|
|
217
261
|
// statements, or the plain (text, values) form otherwise.
|
|
218
262
|
const usePrepared = preparedName && this.preparedStatementsEnabled;
|
|
@@ -221,10 +265,14 @@ export class QueryInterface {
|
|
|
221
265
|
: this.pool.query(sql, params);
|
|
222
266
|
if (!timeout) {
|
|
223
267
|
try {
|
|
224
|
-
|
|
268
|
+
const result = await exec;
|
|
269
|
+
this.emitQueryEvent(sql, params, performance.now() - start, action, result.rowCount ?? 0);
|
|
270
|
+
return result;
|
|
225
271
|
}
|
|
226
272
|
catch (err) {
|
|
227
|
-
|
|
273
|
+
const wrapped = wrapPgError(err);
|
|
274
|
+
this.emitQueryEvent(sql, params, performance.now() - start, action, 0, wrapped instanceof Error ? wrapped : undefined);
|
|
275
|
+
throw wrapped;
|
|
228
276
|
}
|
|
229
277
|
}
|
|
230
278
|
let timer;
|
|
@@ -232,10 +280,14 @@ export class QueryInterface {
|
|
|
232
280
|
timer = setTimeout(() => reject(new TimeoutError(timeout)), timeout);
|
|
233
281
|
});
|
|
234
282
|
try {
|
|
235
|
-
|
|
283
|
+
const result = await Promise.race([exec, timeoutPromise]);
|
|
284
|
+
this.emitQueryEvent(sql, params, performance.now() - start, action, result.rowCount ?? 0);
|
|
285
|
+
return result;
|
|
236
286
|
}
|
|
237
287
|
catch (err) {
|
|
238
|
-
|
|
288
|
+
const wrapped = wrapPgError(err);
|
|
289
|
+
this.emitQueryEvent(sql, params, performance.now() - start, action, 0, wrapped instanceof Error ? wrapped : undefined);
|
|
290
|
+
throw wrapped;
|
|
239
291
|
}
|
|
240
292
|
finally {
|
|
241
293
|
clearTimeout(timer);
|
|
@@ -251,6 +303,7 @@ export class QueryInterface {
|
|
|
251
303
|
* To intercept queries before SQL generation, use the raw() method instead.
|
|
252
304
|
*/
|
|
253
305
|
async executeWithMiddleware(action, args, executor) {
|
|
306
|
+
this.currentAction = action;
|
|
254
307
|
if (this.middlewares.length === 0) {
|
|
255
308
|
return executor();
|
|
256
309
|
}
|
|
@@ -270,7 +323,6 @@ export class QueryInterface {
|
|
|
270
323
|
// -------------------------------------------------------------------------
|
|
271
324
|
// findUnique
|
|
272
325
|
// -------------------------------------------------------------------------
|
|
273
|
-
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
274
326
|
async findUnique(args) {
|
|
275
327
|
return this.executeWithMiddleware('findUnique', args, async () => {
|
|
276
328
|
const deferred = this.buildFindUnique(args);
|
|
@@ -374,7 +426,6 @@ export class QueryInterface {
|
|
|
374
426
|
// -------------------------------------------------------------------------
|
|
375
427
|
// findMany
|
|
376
428
|
// -------------------------------------------------------------------------
|
|
377
|
-
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
378
429
|
async findMany(args) {
|
|
379
430
|
this.maybeWarnUnlimited(args);
|
|
380
431
|
// Dev-only: warn on deeply nested with clauses
|
|
@@ -576,7 +627,6 @@ export class QueryInterface {
|
|
|
576
627
|
* }
|
|
577
628
|
* ```
|
|
578
629
|
*/
|
|
579
|
-
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
580
630
|
async *findManyStream(args) {
|
|
581
631
|
const batchSize = Math.max(1, Math.floor(Number(args?.batchSize ?? 1000)));
|
|
582
632
|
const hasRelations = !!args?.with;
|
|
@@ -585,6 +635,7 @@ export class QueryInterface {
|
|
|
585
635
|
...args,
|
|
586
636
|
limit: batchSize + 1,
|
|
587
637
|
});
|
|
638
|
+
this.currentAction = 'findManyStream';
|
|
588
639
|
const speculativeResult = await this.queryWithTimeout(speculativeDeferred.sql, speculativeDeferred.params, args?.timeout);
|
|
589
640
|
if (speculativeResult.rows.length <= batchSize) {
|
|
590
641
|
// Small drain — yield all rows and return, no cursor needed
|
|
@@ -633,7 +684,6 @@ export class QueryInterface {
|
|
|
633
684
|
// -------------------------------------------------------------------------
|
|
634
685
|
// findFirst — like findMany but returns a single row or null
|
|
635
686
|
// -------------------------------------------------------------------------
|
|
636
|
-
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
637
687
|
async findFirst(args) {
|
|
638
688
|
return this.executeWithMiddleware('findFirst', (args ?? {}), async () => {
|
|
639
689
|
const deferred = this.buildFindFirst(args);
|
|
@@ -659,7 +709,6 @@ export class QueryInterface {
|
|
|
659
709
|
// -------------------------------------------------------------------------
|
|
660
710
|
// findFirstOrThrow — like findFirst but throws if no record found
|
|
661
711
|
// -------------------------------------------------------------------------
|
|
662
|
-
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
663
712
|
async findFirstOrThrow(args) {
|
|
664
713
|
return this.executeWithMiddleware('findFirstOrThrow', (args ?? {}), async () => {
|
|
665
714
|
const deferred = this.buildFindFirstOrThrow(args);
|
|
@@ -690,7 +739,6 @@ export class QueryInterface {
|
|
|
690
739
|
// -------------------------------------------------------------------------
|
|
691
740
|
// findUniqueOrThrow — like findUnique but throws if no record found
|
|
692
741
|
// -------------------------------------------------------------------------
|
|
693
|
-
// biome-ignore lint/complexity/noBannedTypes: {} means "no with clause" — matches TypedWithClause default
|
|
694
742
|
async findUniqueOrThrow(args) {
|
|
695
743
|
return this.executeWithMiddleware('findUniqueOrThrow', args, async () => {
|
|
696
744
|
const deferred = this.buildFindUniqueOrThrow(args);
|
|
@@ -723,6 +771,9 @@ export class QueryInterface {
|
|
|
723
771
|
// -------------------------------------------------------------------------
|
|
724
772
|
async create(args) {
|
|
725
773
|
return this.executeWithMiddleware('create', args, async () => {
|
|
774
|
+
if (hasRelationFields(args.data, this.tableMeta)) {
|
|
775
|
+
return this.nestedCreate(args);
|
|
776
|
+
}
|
|
726
777
|
const deferred = this.buildCreate(args);
|
|
727
778
|
const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
|
|
728
779
|
return deferred.transform(result);
|
|
@@ -805,6 +856,9 @@ export class QueryInterface {
|
|
|
805
856
|
// -------------------------------------------------------------------------
|
|
806
857
|
async update(args) {
|
|
807
858
|
return this.executeWithMiddleware('update', args, async () => {
|
|
859
|
+
if (hasRelationFields(args.data, this.tableMeta)) {
|
|
860
|
+
return this.nestedUpdate(args);
|
|
861
|
+
}
|
|
808
862
|
const deferred = this.buildUpdate(args);
|
|
809
863
|
const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
|
|
810
864
|
return deferred.transform(result);
|
|
@@ -813,32 +867,62 @@ export class QueryInterface {
|
|
|
813
867
|
buildUpdate(args) {
|
|
814
868
|
const dataObj = args.data;
|
|
815
869
|
const whereObj = args.where;
|
|
870
|
+
const lock = args.optimisticLock;
|
|
816
871
|
const setFp = this.fingerprintSet(dataObj);
|
|
817
872
|
const whereFp = this.fingerprintWhere(whereObj);
|
|
818
|
-
const ck = `u:${setFp}|${whereFp}`;
|
|
873
|
+
const ck = lock ? null : `u:${setFp}|${whereFp}`;
|
|
819
874
|
const params = [];
|
|
820
|
-
const
|
|
875
|
+
const buildSql = () => {
|
|
821
876
|
const freshParams = [];
|
|
822
877
|
const setEntries = Object.entries(dataObj).filter(([, v]) => v !== undefined);
|
|
823
878
|
const setClauses = setEntries.map(([k, v]) => this.buildSetClause(k, v, freshParams));
|
|
879
|
+
if (lock) {
|
|
880
|
+
const versionCol = this.toSqlColumn(lock.field);
|
|
881
|
+
setClauses.push(`${versionCol} = ${versionCol} + 1`);
|
|
882
|
+
}
|
|
824
883
|
const whereClause = this.buildWhereClause(whereObj, freshParams);
|
|
825
|
-
|
|
884
|
+
let whereSql = whereClause ? ` WHERE ${whereClause}` : '';
|
|
885
|
+
if (lock) {
|
|
886
|
+
const versionCol = this.toSqlColumn(lock.field);
|
|
887
|
+
freshParams.push(lock.expected);
|
|
888
|
+
const versionCheck = `${versionCol} = ${this.p(freshParams.length)}`;
|
|
889
|
+
whereSql = whereSql ? `${whereSql} AND ${versionCheck}` : ` WHERE ${versionCheck}`;
|
|
890
|
+
}
|
|
826
891
|
this.assertMutationHasPredicate('update', whereSql, args.allowFullTableScan);
|
|
827
892
|
return `UPDATE ${this.q(this.table)} SET ${setClauses.join(', ')}${whereSql}${this.dialect.buildReturningClause('*')}`;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
893
|
+
};
|
|
894
|
+
let sql;
|
|
895
|
+
let preparedName;
|
|
896
|
+
if (ck) {
|
|
897
|
+
const entry = this.acquireSql(ck, buildSql);
|
|
898
|
+
sql = entry.sql;
|
|
899
|
+
preparedName = entry.name;
|
|
900
|
+
if (whereFp === '') {
|
|
901
|
+
this.assertMutationHasPredicate('update', '', args.allowFullTableScan);
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
else {
|
|
905
|
+
sql = buildSql();
|
|
832
906
|
}
|
|
833
|
-
// Collect params: SET first, then WHERE (same order as fresh build)
|
|
907
|
+
// Collect params: SET first, then WHERE, then version check (same order as fresh build)
|
|
834
908
|
this.collectSetParams(dataObj, params);
|
|
835
909
|
this.collectWhereParams(whereObj, params);
|
|
910
|
+
if (lock) {
|
|
911
|
+
params.push(lock.expected);
|
|
912
|
+
}
|
|
836
913
|
return {
|
|
837
|
-
sql
|
|
914
|
+
sql,
|
|
838
915
|
params,
|
|
839
916
|
transform: (result) => {
|
|
840
917
|
const row = result.rows[0];
|
|
841
918
|
if (!row) {
|
|
919
|
+
if (lock) {
|
|
920
|
+
throw new OptimisticLockError({
|
|
921
|
+
table: this.table,
|
|
922
|
+
versionField: lock.field,
|
|
923
|
+
expectedVersion: lock.expected,
|
|
924
|
+
});
|
|
925
|
+
}
|
|
842
926
|
throw new NotFoundError({
|
|
843
927
|
table: this.table,
|
|
844
928
|
where: args.where,
|
|
@@ -848,7 +932,75 @@ export class QueryInterface {
|
|
|
848
932
|
return this.parseRow(row, this.table);
|
|
849
933
|
},
|
|
850
934
|
tag: `${this.table}.update`,
|
|
851
|
-
preparedName
|
|
935
|
+
preparedName,
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
// -------------------------------------------------------------------------
|
|
939
|
+
// Nested write helpers (shared by create + update)
|
|
940
|
+
// -------------------------------------------------------------------------
|
|
941
|
+
async nestedCreate(args) {
|
|
942
|
+
const data = args.data;
|
|
943
|
+
if (this.txScoped) {
|
|
944
|
+
const ctx = this.buildNestedCtx();
|
|
945
|
+
return executeNestedCreate(ctx, this.table, data);
|
|
946
|
+
}
|
|
947
|
+
return this.runInImplicitTx(async (ctx) => {
|
|
948
|
+
const result = await executeNestedCreate(ctx, this.table, data);
|
|
949
|
+
return result;
|
|
950
|
+
});
|
|
951
|
+
}
|
|
952
|
+
async nestedUpdate(args) {
|
|
953
|
+
const data = args.data;
|
|
954
|
+
const where = args.where;
|
|
955
|
+
if (this.txScoped) {
|
|
956
|
+
const ctx = this.buildNestedCtx();
|
|
957
|
+
return executeNestedUpdate(ctx, this.table, where, data);
|
|
958
|
+
}
|
|
959
|
+
return this.runInImplicitTx(async (ctx) => {
|
|
960
|
+
const result = await executeNestedUpdate(ctx, this.table, where, data);
|
|
961
|
+
return result;
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
async runInImplicitTx(fn) {
|
|
965
|
+
const client = await this.pool.connect();
|
|
966
|
+
try {
|
|
967
|
+
await client.query('BEGIN');
|
|
968
|
+
const { TransactionClient } = await import('../client.js');
|
|
969
|
+
// biome-ignore lint/suspicious/noExplicitAny: MiddlewareFn and Middleware are structurally identical
|
|
970
|
+
const tx = new TransactionClient(client, this.schema, this.middlewares, this.options);
|
|
971
|
+
// biome-ignore lint/suspicious/noExplicitAny: TransactionClient satisfies NestedWriteContext['tx'] at runtime
|
|
972
|
+
const ctx = { schema: this.schema, tx: tx };
|
|
973
|
+
const result = await fn(ctx);
|
|
974
|
+
await client.query('COMMIT');
|
|
975
|
+
return result;
|
|
976
|
+
}
|
|
977
|
+
catch (err) {
|
|
978
|
+
try {
|
|
979
|
+
await client.query('ROLLBACK');
|
|
980
|
+
}
|
|
981
|
+
catch {
|
|
982
|
+
// Best-effort rollback — connection may have died.
|
|
983
|
+
}
|
|
984
|
+
throw err;
|
|
985
|
+
}
|
|
986
|
+
finally {
|
|
987
|
+
client.release();
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
buildNestedCtx() {
|
|
991
|
+
const pool = this.pool;
|
|
992
|
+
const schema = this.schema;
|
|
993
|
+
const middlewares = this.middlewares;
|
|
994
|
+
const opts = { ...this.options, _txScoped: true };
|
|
995
|
+
return {
|
|
996
|
+
schema,
|
|
997
|
+
tx: this.makeTxProxy(pool, schema, middlewares, opts),
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
// biome-ignore lint/suspicious/noExplicitAny: bridges MiddlewareFn[] ↔ Middleware[] and QI ↔ NestedWriteContext type gap
|
|
1001
|
+
makeTxProxy(pool, schema, middlewares, opts) {
|
|
1002
|
+
return {
|
|
1003
|
+
table: (name) => new QueryInterface(pool, name, schema, middlewares, opts),
|
|
852
1004
|
};
|
|
853
1005
|
}
|
|
854
1006
|
// -------------------------------------------------------------------------
|
|
@@ -1500,7 +1652,11 @@ export class QueryInterface {
|
|
|
1500
1652
|
const relDef = this.tableMeta.relations[key];
|
|
1501
1653
|
if (relDef && typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
1502
1654
|
const filterObj = value;
|
|
1503
|
-
if ('some' in filterObj ||
|
|
1655
|
+
if ('some' in filterObj ||
|
|
1656
|
+
'every' in filterObj ||
|
|
1657
|
+
'none' in filterObj ||
|
|
1658
|
+
'is' in filterObj ||
|
|
1659
|
+
'isNot' in filterObj) {
|
|
1504
1660
|
const relParts = [];
|
|
1505
1661
|
if (filterObj.some !== undefined)
|
|
1506
1662
|
relParts.push(`some(${this.fingerprintRelFilter(relDef.to, filterObj.some)})`);
|
|
@@ -1508,6 +1664,10 @@ export class QueryInterface {
|
|
|
1508
1664
|
relParts.push(`every(${this.fingerprintRelFilter(relDef.to, filterObj.every)})`);
|
|
1509
1665
|
if (filterObj.none !== undefined)
|
|
1510
1666
|
relParts.push(`none(${this.fingerprintRelFilter(relDef.to, filterObj.none)})`);
|
|
1667
|
+
if (filterObj.is !== undefined)
|
|
1668
|
+
relParts.push(`is(${this.fingerprintRelFilter(relDef.to, filterObj.is)})`);
|
|
1669
|
+
if (filterObj.isNot !== undefined)
|
|
1670
|
+
relParts.push(`isNot(${this.fingerprintRelFilter(relDef.to, filterObj.isNot)})`);
|
|
1511
1671
|
parts.push(`${key}:{${relParts.join(',')}}`);
|
|
1512
1672
|
continue;
|
|
1513
1673
|
}
|
|
@@ -1538,6 +1698,12 @@ export class QueryInterface {
|
|
|
1538
1698
|
parts.push(`${key}:arr(${this.fingerprintArrayFilter(value)})`);
|
|
1539
1699
|
continue;
|
|
1540
1700
|
}
|
|
1701
|
+
// Text search filter
|
|
1702
|
+
if (typeof value === 'object' && !Array.isArray(value) && isTextSearchFilter(value)) {
|
|
1703
|
+
const cfg = value.config ?? 'english';
|
|
1704
|
+
parts.push(`${key}:fts(${cfg})`);
|
|
1705
|
+
continue;
|
|
1706
|
+
}
|
|
1541
1707
|
// Plain equality
|
|
1542
1708
|
parts.push(`${key}:eq`);
|
|
1543
1709
|
}
|
|
@@ -1623,13 +1789,21 @@ export class QueryInterface {
|
|
|
1623
1789
|
const relationDef = this.tableMeta.relations[key];
|
|
1624
1790
|
if (relationDef && typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
1625
1791
|
const filterObj = value;
|
|
1626
|
-
if ('some' in filterObj ||
|
|
1792
|
+
if ('some' in filterObj ||
|
|
1793
|
+
'every' in filterObj ||
|
|
1794
|
+
'none' in filterObj ||
|
|
1795
|
+
'is' in filterObj ||
|
|
1796
|
+
'isNot' in filterObj) {
|
|
1627
1797
|
if (filterObj.some !== undefined)
|
|
1628
1798
|
this.collectRelFilterParams(relationDef.to, filterObj.some, params);
|
|
1629
1799
|
if (filterObj.none !== undefined)
|
|
1630
1800
|
this.collectRelFilterParams(relationDef.to, filterObj.none, params);
|
|
1631
1801
|
if (filterObj.every !== undefined)
|
|
1632
1802
|
this.collectRelFilterParams(relationDef.to, filterObj.every, params);
|
|
1803
|
+
if (filterObj.is !== undefined)
|
|
1804
|
+
this.collectRelFilterParams(relationDef.to, filterObj.is, params);
|
|
1805
|
+
if (filterObj.isNot !== undefined)
|
|
1806
|
+
this.collectRelFilterParams(relationDef.to, filterObj.isNot, params);
|
|
1633
1807
|
continue;
|
|
1634
1808
|
}
|
|
1635
1809
|
}
|
|
@@ -1653,6 +1827,11 @@ export class QueryInterface {
|
|
|
1653
1827
|
continue;
|
|
1654
1828
|
}
|
|
1655
1829
|
}
|
|
1830
|
+
// Text search filter
|
|
1831
|
+
if (typeof value === 'object' && !Array.isArray(value) && isTextSearchFilter(value)) {
|
|
1832
|
+
params.push(value.search);
|
|
1833
|
+
continue;
|
|
1834
|
+
}
|
|
1656
1835
|
// Operator objects
|
|
1657
1836
|
if (isWhereOperator(value)) {
|
|
1658
1837
|
this.collectOperatorParams(value, params);
|
|
@@ -1977,7 +2156,11 @@ export class QueryInterface {
|
|
|
1977
2156
|
if (relationDef && typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
1978
2157
|
const filterObj = value;
|
|
1979
2158
|
// Check if this is a relation filter (has some/every/none keys)
|
|
1980
|
-
if ('some' in filterObj ||
|
|
2159
|
+
if ('some' in filterObj ||
|
|
2160
|
+
'every' in filterObj ||
|
|
2161
|
+
'none' in filterObj ||
|
|
2162
|
+
'is' in filterObj ||
|
|
2163
|
+
'isNot' in filterObj) {
|
|
1981
2164
|
const relClause = this.buildRelationFilter(key, relationDef, filterObj, params);
|
|
1982
2165
|
if (relClause)
|
|
1983
2166
|
andClauses.push(relClause);
|
|
@@ -2027,6 +2210,12 @@ export class QueryInterface {
|
|
|
2027
2210
|
`(actual type: ${colType}); cannot apply array operator '${arrayKey}'.`);
|
|
2028
2211
|
}
|
|
2029
2212
|
}
|
|
2213
|
+
// Handle full-text search filter
|
|
2214
|
+
if (typeof value === 'object' && !Array.isArray(value) && isTextSearchFilter(value)) {
|
|
2215
|
+
const tsClause = this.buildTextSearchClause(column, value, params);
|
|
2216
|
+
andClauses.push(tsClause);
|
|
2217
|
+
continue;
|
|
2218
|
+
}
|
|
2030
2219
|
// Handle operator objects
|
|
2031
2220
|
if (isWhereOperator(value)) {
|
|
2032
2221
|
const opClauses = this.buildOperatorClauses(column, value, params);
|
|
@@ -2088,6 +2277,20 @@ export class QueryInterface {
|
|
|
2088
2277
|
// "every" with empty filter = true (all match trivially)
|
|
2089
2278
|
}
|
|
2090
2279
|
}
|
|
2280
|
+
// "is": EXISTS — for to-one relations (same SQL as "some")
|
|
2281
|
+
if (filterObj.is !== undefined) {
|
|
2282
|
+
const subWhere = filterObj.is;
|
|
2283
|
+
const filterClause = this.buildSubWhereForRelation(targetTable, subWhere, params);
|
|
2284
|
+
const fullWhere = filterClause ? `${correlation} AND ${filterClause}` : correlation;
|
|
2285
|
+
clauses.push(`EXISTS (SELECT 1 FROM ${qt} WHERE ${fullWhere})`);
|
|
2286
|
+
}
|
|
2287
|
+
// "isNot": NOT EXISTS — for to-one relations (same SQL as "none")
|
|
2288
|
+
if (filterObj.isNot !== undefined) {
|
|
2289
|
+
const subWhere = filterObj.isNot;
|
|
2290
|
+
const filterClause = this.buildSubWhereForRelation(targetTable, subWhere, params);
|
|
2291
|
+
const fullWhere = filterClause ? `${correlation} AND ${filterClause}` : correlation;
|
|
2292
|
+
clauses.push(`NOT EXISTS (SELECT 1 FROM ${qt} WHERE ${fullWhere})`);
|
|
2293
|
+
}
|
|
2091
2294
|
return clauses.length > 0 ? clauses.join(' AND ') : null;
|
|
2092
2295
|
}
|
|
2093
2296
|
/**
|
|
@@ -2669,6 +2872,18 @@ export class QueryInterface {
|
|
|
2669
2872
|
}
|
|
2670
2873
|
return clauses;
|
|
2671
2874
|
}
|
|
2875
|
+
/**
|
|
2876
|
+
* Build SQL clause for full-text search using to_tsvector @@ to_tsquery.
|
|
2877
|
+
* The config name is validated to prevent injection (only alphanumeric + underscore).
|
|
2878
|
+
*/
|
|
2879
|
+
buildTextSearchClause(column, filter, params) {
|
|
2880
|
+
const config = filter.config ?? 'english';
|
|
2881
|
+
if (!validateTextSearchConfig(config)) {
|
|
2882
|
+
throw new ValidationError(`[turbine] Invalid text search config "${config}": only alphanumeric characters and underscores are allowed.`);
|
|
2883
|
+
}
|
|
2884
|
+
params.push(filter.search);
|
|
2885
|
+
return `to_tsvector('${config}', ${column}) @@ to_tsquery('${config}', ${this.p(params.length)})`;
|
|
2886
|
+
}
|
|
2672
2887
|
/**
|
|
2673
2888
|
* Get the Postgres array type for a column (used by UNNEST in createMany).
|
|
2674
2889
|
* Uses pre-computed Map for O(1) lookup instead of linear scan.
|
package/dist/query/index.d.ts
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* `import { … } from './query/index.js'` is a drop-in replacement for the
|
|
6
6
|
* former monolithic `import { … } from './query.js'`.
|
|
7
7
|
*/
|
|
8
|
-
export type { AggregateArgs, AggregateResult, ArrayFilter, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, JsonFilter, OrderDirection, RelationDescriptor, RelationFilter, TypedWithClause, UpdateArgs, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithResult, } from './types.js';
|
|
8
|
+
export type { AggregateArgs, AggregateResult, ArrayFilter, ConnectOrCreateOp, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, JsonFilter, NestedCreateOp, NestedUpdateOp, OmitResult, OrderDirection, QueryResult, RelationDescriptor, RelationFilter, SelectResult, TextSearchFilter, TypedWithClause, UpdateArgs, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithResult, } from './types.js';
|
|
9
9
|
export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, ColumnTypeInput, CreateIndexStatementInput, CreateTableStatementInput, Dialect, InsertStatementInput, UpsertStatementInput, } from '../dialect.js';
|
|
10
10
|
export { postgresDialect } from '../dialect.js';
|
|
11
11
|
export type { SqlCacheEntry } from './utils.js';
|
|
12
12
|
export { buildCorrelation, escapeLike, escSingleQuote, fnv1a64Hex, LRUCache, OPERATOR_KEYS, quoteIdent, sqlToPreparedName, } from './utils.js';
|
|
13
|
-
export type { DeferredQuery, MiddlewareFn, QueryInterfaceOptions } from './builder.js';
|
|
13
|
+
export type { DeferredQuery, MiddlewareFn, QueryEvent, QueryEventListener, QueryInterfaceOptions } from './builder.js';
|
|
14
14
|
export { QueryInterface } from './builder.js';
|
|
15
15
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/query/types.d.ts
CHANGED
|
@@ -26,8 +26,9 @@ export interface WhereOperator<V = unknown> {
|
|
|
26
26
|
* - An operator object ({ gt: 5, lte: 10 })
|
|
27
27
|
* - A JSONB filter object ({ contains, equals, path, hasKey })
|
|
28
28
|
* - An array filter object ({ has, hasEvery, hasSome, isEmpty })
|
|
29
|
+
* - A text search filter object ({ search, config? })
|
|
29
30
|
*/
|
|
30
|
-
export type WhereValue<V = unknown> = V | WhereOperator<V> | JsonFilter | ArrayFilter | null;
|
|
31
|
+
export type WhereValue<V = unknown> = V | WhereOperator<V> | JsonFilter | ArrayFilter | TextSearchFilter | null;
|
|
31
32
|
/**
|
|
32
33
|
* Where clause type: each field can be a plain value, null, or operator object.
|
|
33
34
|
* Special keys: OR for disjunctive conditions.
|
|
@@ -162,18 +163,40 @@ export type WithResult<T, R extends object, W> = [keyof R] extends [never] ? T :
|
|
|
162
163
|
with?: infer NestedW;
|
|
163
164
|
} ? NestedW extends object ? ApplyCardinality<R[K], WithResult<RelationTarget<R[K]>, RelationRelations<R[K]> & object, NestedW>> : ApplyCardinality<R[K], RelationTarget<R[K]>> : ApplyCardinality<R[K], RelationTarget<R[K]>>;
|
|
164
165
|
} : T;
|
|
165
|
-
|
|
166
|
+
/** Extract keys from a boolean record where the value is `true`. */
|
|
167
|
+
type TrueKeys<S extends Record<string, boolean>> = {
|
|
168
|
+
[K in keyof S]: S[K] extends true ? K : never;
|
|
169
|
+
}[keyof S];
|
|
170
|
+
/** Pick only the fields from T that are selected (value = true) in S. */
|
|
171
|
+
export type SelectResult<T, S extends Record<string, boolean> | undefined> = S extends Record<string, boolean> ? Pick<T, Extract<keyof T, TrueKeys<S>>> : T;
|
|
172
|
+
/** Omit the fields from T that are marked (value = true) in O. */
|
|
173
|
+
export type OmitResult<T, O extends Record<string, boolean> | undefined> = O extends Record<string, boolean> ? Omit<T, Extract<keyof T, TrueKeys<O>>> : T;
|
|
174
|
+
/**
|
|
175
|
+
* Apply select or omit field narrowing to a base type. Select takes priority —
|
|
176
|
+
* when both are provided, only select is applied (matching runtime behavior).
|
|
177
|
+
*/
|
|
178
|
+
export type FieldResult<T, S extends Record<string, boolean> | undefined, O extends Record<string, boolean> | undefined> = S extends Record<string, boolean> ? SelectResult<T, S> : OmitResult<T, O>;
|
|
179
|
+
/**
|
|
180
|
+
* Compute the full query result type: apply field narrowing to the base entity,
|
|
181
|
+
* then add relation additions from the `with` clause. Relations are unaffected
|
|
182
|
+
* by select/omit (they are separate JSON subqueries at the SQL level).
|
|
183
|
+
*
|
|
184
|
+
* Short-circuits to plain WithResult when neither select nor omit is provided,
|
|
185
|
+
* preserving exact type equality with the pre-narrowing era.
|
|
186
|
+
*/
|
|
187
|
+
export type QueryResult<T, R extends object, W, S extends Record<string, boolean> | undefined, O extends Record<string, boolean> | undefined> = S extends undefined ? O extends undefined ? WithResult<T, R, W> : O extends Record<string, boolean> ? Omit<WithResult<T, R, W>, Extract<keyof T, TrueKeys<O>>> : WithResult<T, R, W> : S extends Record<string, boolean> ? Pick<WithResult<T, R, W>, Extract<keyof T, TrueKeys<S>> | Exclude<keyof WithResult<T, R, W>, keyof T>> : WithResult<T, R, W>;
|
|
188
|
+
export interface FindUniqueArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> {
|
|
166
189
|
where: WhereClause<T>;
|
|
167
|
-
select?:
|
|
168
|
-
omit?:
|
|
190
|
+
select?: S;
|
|
191
|
+
omit?: O;
|
|
169
192
|
with?: W;
|
|
170
193
|
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
171
194
|
timeout?: number;
|
|
172
195
|
}
|
|
173
|
-
export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R
|
|
196
|
+
export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> {
|
|
174
197
|
where?: WhereClause<T>;
|
|
175
|
-
select?:
|
|
176
|
-
omit?:
|
|
198
|
+
select?: S;
|
|
199
|
+
omit?: O;
|
|
177
200
|
orderBy?: Record<string, OrderDirection>;
|
|
178
201
|
limit?: number;
|
|
179
202
|
offset?: number;
|
|
@@ -187,7 +210,7 @@ export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClaus
|
|
|
187
210
|
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
188
211
|
timeout?: number;
|
|
189
212
|
}
|
|
190
|
-
export interface FindManyStreamArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R
|
|
213
|
+
export interface FindManyStreamArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> extends FindManyArgs<T, R, W, S, O> {
|
|
191
214
|
/**
|
|
192
215
|
* Number of rows to fetch per internal FETCH batch (default: 1000).
|
|
193
216
|
*
|
|
@@ -256,6 +279,27 @@ export interface UpdateArgs<T> {
|
|
|
256
279
|
* when an unconditional mutation is the intended behaviour.
|
|
257
280
|
*/
|
|
258
281
|
allowFullTableScan?: boolean;
|
|
282
|
+
/**
|
|
283
|
+
* Optimistic locking — prevents lost updates in concurrent scenarios.
|
|
284
|
+
* Specify the version field and its expected value. The update adds a
|
|
285
|
+
* WHERE check on the version and auto-increments it. If the row was
|
|
286
|
+
* modified by another transaction, throws `OptimisticLockError`.
|
|
287
|
+
*
|
|
288
|
+
* @example
|
|
289
|
+
* ```ts
|
|
290
|
+
* await db.posts.update({
|
|
291
|
+
* where: { id: 1 },
|
|
292
|
+
* data: { title: 'new title' },
|
|
293
|
+
* optimisticLock: { field: 'version', expected: 3 },
|
|
294
|
+
* });
|
|
295
|
+
* // Generates: UPDATE posts SET title=$1, version=version+1
|
|
296
|
+
* // WHERE id=$2 AND version=$3 RETURNING *
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
299
|
+
optimisticLock?: {
|
|
300
|
+
field: keyof T & string;
|
|
301
|
+
expected: number;
|
|
302
|
+
};
|
|
259
303
|
}
|
|
260
304
|
export interface UpdateManyArgs<T> {
|
|
261
305
|
where: WhereClause<T>;
|
|
@@ -286,6 +330,23 @@ export interface UpsertArgs<T> {
|
|
|
286
330
|
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
287
331
|
timeout?: number;
|
|
288
332
|
}
|
|
333
|
+
export interface ConnectOrCreateOp<T> {
|
|
334
|
+
where: Partial<T>;
|
|
335
|
+
create: Partial<T>;
|
|
336
|
+
}
|
|
337
|
+
export interface NestedCreateOp<T> {
|
|
338
|
+
create?: Partial<T> | Partial<T>[];
|
|
339
|
+
connect?: Partial<T> | Partial<T>[];
|
|
340
|
+
connectOrCreate?: ConnectOrCreateOp<T> | ConnectOrCreateOp<T>[];
|
|
341
|
+
}
|
|
342
|
+
export interface NestedUpdateOp<T> {
|
|
343
|
+
create?: Partial<T> | Partial<T>[];
|
|
344
|
+
connect?: Partial<T> | Partial<T>[];
|
|
345
|
+
connectOrCreate?: ConnectOrCreateOp<T> | ConnectOrCreateOp<T>[];
|
|
346
|
+
disconnect?: Partial<T> | Partial<T>[];
|
|
347
|
+
set?: Partial<T>[];
|
|
348
|
+
delete?: Partial<T> | Partial<T>[];
|
|
349
|
+
}
|
|
289
350
|
export interface CountArgs<T> {
|
|
290
351
|
where?: WhereClause<T>;
|
|
291
352
|
/** Query timeout in milliseconds. Rejects with an error if exceeded. */
|
|
@@ -361,5 +422,12 @@ export interface ArrayFilter {
|
|
|
361
422
|
/** Check if array is empty: array_length(column, 1) IS NULL */
|
|
362
423
|
isEmpty?: boolean;
|
|
363
424
|
}
|
|
425
|
+
/** Full-text search filter using PostgreSQL to_tsvector / to_tsquery */
|
|
426
|
+
export interface TextSearchFilter {
|
|
427
|
+
/** The search query string passed to to_tsquery */
|
|
428
|
+
search: string;
|
|
429
|
+
/** PostgreSQL text search configuration name (defaults to 'english') */
|
|
430
|
+
config?: string;
|
|
431
|
+
}
|
|
364
432
|
export {};
|
|
365
433
|
//# sourceMappingURL=types.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "Postgres-native TypeScript ORM — runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. 1 dependency, ~110KB",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"status": "tsx src/cli/index.ts status",
|
|
50
50
|
"examples": "tsx examples/examples.ts",
|
|
51
51
|
"test": "tsx --test src/test/*.test.ts",
|
|
52
|
-
"test:unit": "tsx --test src/test/schema-builder.test.ts src/test/errors.test.ts src/test/stress.test.ts src/test/migrate.test.ts src/test/update-operators.test.ts src/test/empty-where-guard.test.ts src/test/cli.test.ts src/test/serverless.test.ts src/test/pipeline.test.ts src/test/pipeline-submittable.test.ts src/test/with-inference.test.ts src/test/operator-validation.test.ts src/test/unlimited-warning.test.ts src/test/generate-relations.test.ts src/test/stream-and-parse.test.ts src/test/sql-cache.test.ts src/test/dialect.test.ts src/test/studio.test.ts src/test/sql-injection.test.ts src/test/cli-flags.test.ts src/test/cockroachdb-adapter.test.ts src/test/yugabytedb-adapter.test.ts src/test/pg-compat.test.ts src/test/relation-filter-validation.test.ts src/test/client-coverage.test.ts src/test/schema-diff.test.ts src/test/composite-fk.test.ts",
|
|
52
|
+
"test:unit": "tsx --test src/test/schema-builder.test.ts src/test/errors.test.ts src/test/stress.test.ts src/test/migrate.test.ts src/test/update-operators.test.ts src/test/empty-where-guard.test.ts src/test/cli.test.ts src/test/serverless.test.ts src/test/pipeline.test.ts src/test/pipeline-submittable.test.ts src/test/with-inference.test.ts src/test/operator-validation.test.ts src/test/unlimited-warning.test.ts src/test/generate-relations.test.ts src/test/stream-and-parse.test.ts src/test/sql-cache.test.ts src/test/dialect.test.ts src/test/studio.test.ts src/test/sql-injection.test.ts src/test/cli-flags.test.ts src/test/cockroachdb-adapter.test.ts src/test/yugabytedb-adapter.test.ts src/test/pg-compat.test.ts src/test/relation-filter-validation.test.ts src/test/client-coverage.test.ts src/test/schema-diff.test.ts src/test/composite-fk.test.ts src/test/retry.test.ts src/test/text-search.test.ts src/test/optimistic-lock.test.ts src/test/sql-safety-property.test.ts src/test/nested-write.test.ts src/test/nested-write-update-upsert.test.ts src/test/cursor-pagination.test.ts src/test/client-branches.test.ts src/test/is-isNot-filter.test.ts src/test/event-emitter.test.ts src/test/observe.test.ts",
|
|
53
53
|
"test:coverage": "c8 tsx --test src/test/schema-builder.test.ts src/test/errors.test.ts src/test/stress.test.ts src/test/migrate.test.ts src/test/update-operators.test.ts src/test/empty-where-guard.test.ts src/test/cli.test.ts src/test/serverless.test.ts src/test/pipeline.test.ts src/test/pipeline-submittable.test.ts src/test/with-inference.test.ts src/test/operator-validation.test.ts src/test/unlimited-warning.test.ts src/test/generate-relations.test.ts src/test/stream-and-parse.test.ts src/test/sql-cache.test.ts src/test/dialect.test.ts src/test/studio.test.ts src/test/sql-injection.test.ts src/test/cli-flags.test.ts",
|
|
54
54
|
"lint": "biome check src/",
|
|
55
55
|
"lint:fix": "biome check --write src/",
|