turbine-orm 0.13.3 → 0.15.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/studio.js +45 -7
- package/dist/cjs/client.js +48 -1
- package/dist/cjs/dialect.js +6 -4
- package/dist/cjs/errors.js +44 -1
- package/dist/cjs/generate.js +95 -1
- package/dist/cjs/index.js +10 -1
- package/dist/cjs/introspect.js +14 -4
- package/dist/cjs/nested-write.js +467 -0
- package/dist/cjs/query/builder.js +212 -16
- package/dist/cli/studio.d.ts +10 -2
- package/dist/cli/studio.js +45 -7
- package/dist/client.d.ts +23 -0
- package/dist/client.js +47 -1
- package/dist/dialect.d.ts +3 -3
- package/dist/dialect.js +6 -4
- package/dist/errors.d.ts +23 -0
- package/dist/errors.js +41 -0
- package/dist/generate.js +95 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -2
- package/dist/introspect.js +15 -5
- package/dist/nested-write.d.ts +95 -0
- package/dist/nested-write.js +461 -0
- package/dist/query/builder.d.ts +28 -12
- package/dist/query/builder.js +180 -17
- package/dist/query/index.d.ts +1 -1
- package/dist/query/types.d.ts +76 -8
- package/dist/schema.d.ts +9 -3
- 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,10 @@ 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;
|
|
131
158
|
constructor(pool, table, schema, middlewares, options) {
|
|
132
159
|
this.pool = pool;
|
|
133
160
|
this.table = table;
|
|
@@ -146,12 +173,14 @@ export class QueryInterface {
|
|
|
146
173
|
this.preparedStatementsEnabled = options?.preparedStatements ?? true;
|
|
147
174
|
this.sqlCacheEnabled = options?.sqlCache !== false;
|
|
148
175
|
this.dialect = options?.dialect ?? postgresDialect;
|
|
176
|
+
this.txScoped = options?._txScoped ?? false;
|
|
177
|
+
this.options = options;
|
|
149
178
|
// Pre-compute column type lookup maps (TASK-26)
|
|
150
179
|
this.columnPgTypeMap = new Map();
|
|
151
180
|
this.columnArrayTypeMap = new Map();
|
|
152
181
|
for (const col of this.tableMeta.columns) {
|
|
153
|
-
this.columnPgTypeMap.set(col.name, col.pgType);
|
|
154
|
-
this.columnArrayTypeMap.set(col.name, col.pgArrayType);
|
|
182
|
+
this.columnPgTypeMap.set(col.name, col.dialectType ?? col.pgType);
|
|
183
|
+
this.columnArrayTypeMap.set(col.name, col.arrayType ?? col.pgArrayType);
|
|
155
184
|
}
|
|
156
185
|
}
|
|
157
186
|
/** Quote an identifier through the active SQL dialect. */
|
|
@@ -723,6 +752,9 @@ export class QueryInterface {
|
|
|
723
752
|
// -------------------------------------------------------------------------
|
|
724
753
|
async create(args) {
|
|
725
754
|
return this.executeWithMiddleware('create', args, async () => {
|
|
755
|
+
if (hasRelationFields(args.data, this.tableMeta)) {
|
|
756
|
+
return this.nestedCreate(args);
|
|
757
|
+
}
|
|
726
758
|
const deferred = this.buildCreate(args);
|
|
727
759
|
const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
|
|
728
760
|
return deferred.transform(result);
|
|
@@ -805,6 +837,9 @@ export class QueryInterface {
|
|
|
805
837
|
// -------------------------------------------------------------------------
|
|
806
838
|
async update(args) {
|
|
807
839
|
return this.executeWithMiddleware('update', args, async () => {
|
|
840
|
+
if (hasRelationFields(args.data, this.tableMeta)) {
|
|
841
|
+
return this.nestedUpdate(args);
|
|
842
|
+
}
|
|
808
843
|
const deferred = this.buildUpdate(args);
|
|
809
844
|
const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
|
|
810
845
|
return deferred.transform(result);
|
|
@@ -813,32 +848,62 @@ export class QueryInterface {
|
|
|
813
848
|
buildUpdate(args) {
|
|
814
849
|
const dataObj = args.data;
|
|
815
850
|
const whereObj = args.where;
|
|
851
|
+
const lock = args.optimisticLock;
|
|
816
852
|
const setFp = this.fingerprintSet(dataObj);
|
|
817
853
|
const whereFp = this.fingerprintWhere(whereObj);
|
|
818
|
-
const ck = `u:${setFp}|${whereFp}`;
|
|
854
|
+
const ck = lock ? null : `u:${setFp}|${whereFp}`;
|
|
819
855
|
const params = [];
|
|
820
|
-
const
|
|
856
|
+
const buildSql = () => {
|
|
821
857
|
const freshParams = [];
|
|
822
858
|
const setEntries = Object.entries(dataObj).filter(([, v]) => v !== undefined);
|
|
823
859
|
const setClauses = setEntries.map(([k, v]) => this.buildSetClause(k, v, freshParams));
|
|
860
|
+
if (lock) {
|
|
861
|
+
const versionCol = this.toSqlColumn(lock.field);
|
|
862
|
+
setClauses.push(`${versionCol} = ${versionCol} + 1`);
|
|
863
|
+
}
|
|
824
864
|
const whereClause = this.buildWhereClause(whereObj, freshParams);
|
|
825
|
-
|
|
865
|
+
let whereSql = whereClause ? ` WHERE ${whereClause}` : '';
|
|
866
|
+
if (lock) {
|
|
867
|
+
const versionCol = this.toSqlColumn(lock.field);
|
|
868
|
+
freshParams.push(lock.expected);
|
|
869
|
+
const versionCheck = `${versionCol} = ${this.p(freshParams.length)}`;
|
|
870
|
+
whereSql = whereSql ? `${whereSql} AND ${versionCheck}` : ` WHERE ${versionCheck}`;
|
|
871
|
+
}
|
|
826
872
|
this.assertMutationHasPredicate('update', whereSql, args.allowFullTableScan);
|
|
827
873
|
return `UPDATE ${this.q(this.table)} SET ${setClauses.join(', ')}${whereSql}${this.dialect.buildReturningClause('*')}`;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
874
|
+
};
|
|
875
|
+
let sql;
|
|
876
|
+
let preparedName;
|
|
877
|
+
if (ck) {
|
|
878
|
+
const entry = this.acquireSql(ck, buildSql);
|
|
879
|
+
sql = entry.sql;
|
|
880
|
+
preparedName = entry.name;
|
|
881
|
+
if (whereFp === '') {
|
|
882
|
+
this.assertMutationHasPredicate('update', '', args.allowFullTableScan);
|
|
883
|
+
}
|
|
832
884
|
}
|
|
833
|
-
|
|
885
|
+
else {
|
|
886
|
+
sql = buildSql();
|
|
887
|
+
}
|
|
888
|
+
// Collect params: SET first, then WHERE, then version check (same order as fresh build)
|
|
834
889
|
this.collectSetParams(dataObj, params);
|
|
835
890
|
this.collectWhereParams(whereObj, params);
|
|
891
|
+
if (lock) {
|
|
892
|
+
params.push(lock.expected);
|
|
893
|
+
}
|
|
836
894
|
return {
|
|
837
|
-
sql
|
|
895
|
+
sql,
|
|
838
896
|
params,
|
|
839
897
|
transform: (result) => {
|
|
840
898
|
const row = result.rows[0];
|
|
841
899
|
if (!row) {
|
|
900
|
+
if (lock) {
|
|
901
|
+
throw new OptimisticLockError({
|
|
902
|
+
table: this.table,
|
|
903
|
+
versionField: lock.field,
|
|
904
|
+
expectedVersion: lock.expected,
|
|
905
|
+
});
|
|
906
|
+
}
|
|
842
907
|
throw new NotFoundError({
|
|
843
908
|
table: this.table,
|
|
844
909
|
where: args.where,
|
|
@@ -848,7 +913,75 @@ export class QueryInterface {
|
|
|
848
913
|
return this.parseRow(row, this.table);
|
|
849
914
|
},
|
|
850
915
|
tag: `${this.table}.update`,
|
|
851
|
-
preparedName
|
|
916
|
+
preparedName,
|
|
917
|
+
};
|
|
918
|
+
}
|
|
919
|
+
// -------------------------------------------------------------------------
|
|
920
|
+
// Nested write helpers (shared by create + update)
|
|
921
|
+
// -------------------------------------------------------------------------
|
|
922
|
+
async nestedCreate(args) {
|
|
923
|
+
const data = args.data;
|
|
924
|
+
if (this.txScoped) {
|
|
925
|
+
const ctx = this.buildNestedCtx();
|
|
926
|
+
return executeNestedCreate(ctx, this.table, data);
|
|
927
|
+
}
|
|
928
|
+
return this.runInImplicitTx(async (ctx) => {
|
|
929
|
+
const result = await executeNestedCreate(ctx, this.table, data);
|
|
930
|
+
return result;
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
async nestedUpdate(args) {
|
|
934
|
+
const data = args.data;
|
|
935
|
+
const where = args.where;
|
|
936
|
+
if (this.txScoped) {
|
|
937
|
+
const ctx = this.buildNestedCtx();
|
|
938
|
+
return executeNestedUpdate(ctx, this.table, where, data);
|
|
939
|
+
}
|
|
940
|
+
return this.runInImplicitTx(async (ctx) => {
|
|
941
|
+
const result = await executeNestedUpdate(ctx, this.table, where, data);
|
|
942
|
+
return result;
|
|
943
|
+
});
|
|
944
|
+
}
|
|
945
|
+
async runInImplicitTx(fn) {
|
|
946
|
+
const client = await this.pool.connect();
|
|
947
|
+
try {
|
|
948
|
+
await client.query('BEGIN');
|
|
949
|
+
const { TransactionClient } = await import('../client.js');
|
|
950
|
+
// biome-ignore lint/suspicious/noExplicitAny: MiddlewareFn and Middleware are structurally identical
|
|
951
|
+
const tx = new TransactionClient(client, this.schema, this.middlewares, this.options);
|
|
952
|
+
// biome-ignore lint/suspicious/noExplicitAny: TransactionClient satisfies NestedWriteContext['tx'] at runtime
|
|
953
|
+
const ctx = { schema: this.schema, tx: tx };
|
|
954
|
+
const result = await fn(ctx);
|
|
955
|
+
await client.query('COMMIT');
|
|
956
|
+
return result;
|
|
957
|
+
}
|
|
958
|
+
catch (err) {
|
|
959
|
+
try {
|
|
960
|
+
await client.query('ROLLBACK');
|
|
961
|
+
}
|
|
962
|
+
catch {
|
|
963
|
+
// Best-effort rollback — connection may have died.
|
|
964
|
+
}
|
|
965
|
+
throw err;
|
|
966
|
+
}
|
|
967
|
+
finally {
|
|
968
|
+
client.release();
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
buildNestedCtx() {
|
|
972
|
+
const pool = this.pool;
|
|
973
|
+
const schema = this.schema;
|
|
974
|
+
const middlewares = this.middlewares;
|
|
975
|
+
const opts = { ...this.options, _txScoped: true };
|
|
976
|
+
return {
|
|
977
|
+
schema,
|
|
978
|
+
tx: this.makeTxProxy(pool, schema, middlewares, opts),
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
// biome-ignore lint/suspicious/noExplicitAny: bridges MiddlewareFn[] ↔ Middleware[] and QI ↔ NestedWriteContext type gap
|
|
982
|
+
makeTxProxy(pool, schema, middlewares, opts) {
|
|
983
|
+
return {
|
|
984
|
+
table: (name) => new QueryInterface(pool, name, schema, middlewares, opts),
|
|
852
985
|
};
|
|
853
986
|
}
|
|
854
987
|
// -------------------------------------------------------------------------
|
|
@@ -1538,6 +1671,12 @@ export class QueryInterface {
|
|
|
1538
1671
|
parts.push(`${key}:arr(${this.fingerprintArrayFilter(value)})`);
|
|
1539
1672
|
continue;
|
|
1540
1673
|
}
|
|
1674
|
+
// Text search filter
|
|
1675
|
+
if (typeof value === 'object' && !Array.isArray(value) && isTextSearchFilter(value)) {
|
|
1676
|
+
const cfg = value.config ?? 'english';
|
|
1677
|
+
parts.push(`${key}:fts(${cfg})`);
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1541
1680
|
// Plain equality
|
|
1542
1681
|
parts.push(`${key}:eq`);
|
|
1543
1682
|
}
|
|
@@ -1653,6 +1792,11 @@ export class QueryInterface {
|
|
|
1653
1792
|
continue;
|
|
1654
1793
|
}
|
|
1655
1794
|
}
|
|
1795
|
+
// Text search filter
|
|
1796
|
+
if (typeof value === 'object' && !Array.isArray(value) && isTextSearchFilter(value)) {
|
|
1797
|
+
params.push(value.search);
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1656
1800
|
// Operator objects
|
|
1657
1801
|
if (isWhereOperator(value)) {
|
|
1658
1802
|
this.collectOperatorParams(value, params);
|
|
@@ -2027,6 +2171,12 @@ export class QueryInterface {
|
|
|
2027
2171
|
`(actual type: ${colType}); cannot apply array operator '${arrayKey}'.`);
|
|
2028
2172
|
}
|
|
2029
2173
|
}
|
|
2174
|
+
// Handle full-text search filter
|
|
2175
|
+
if (typeof value === 'object' && !Array.isArray(value) && isTextSearchFilter(value)) {
|
|
2176
|
+
const tsClause = this.buildTextSearchClause(column, value, params);
|
|
2177
|
+
andClauses.push(tsClause);
|
|
2178
|
+
continue;
|
|
2179
|
+
}
|
|
2030
2180
|
// Handle operator objects
|
|
2031
2181
|
if (isWhereOperator(value)) {
|
|
2032
2182
|
const opClauses = this.buildOperatorClauses(column, value, params);
|
|
@@ -2669,6 +2819,18 @@ export class QueryInterface {
|
|
|
2669
2819
|
}
|
|
2670
2820
|
return clauses;
|
|
2671
2821
|
}
|
|
2822
|
+
/**
|
|
2823
|
+
* Build SQL clause for full-text search using to_tsvector @@ to_tsquery.
|
|
2824
|
+
* The config name is validated to prevent injection (only alphanumeric + underscore).
|
|
2825
|
+
*/
|
|
2826
|
+
buildTextSearchClause(column, filter, params) {
|
|
2827
|
+
const config = filter.config ?? 'english';
|
|
2828
|
+
if (!validateTextSearchConfig(config)) {
|
|
2829
|
+
throw new ValidationError(`[turbine] Invalid text search config "${config}": only alphanumeric characters and underscores are allowed.`);
|
|
2830
|
+
}
|
|
2831
|
+
params.push(filter.search);
|
|
2832
|
+
return `to_tsvector('${config}', ${column}) @@ to_tsquery('${config}', ${this.p(params.length)})`;
|
|
2833
|
+
}
|
|
2672
2834
|
/**
|
|
2673
2835
|
* Get the Postgres array type for a column (used by UNNEST in createMany).
|
|
2674
2836
|
* Uses pre-computed Map for O(1) lookup instead of linear scan.
|
|
@@ -2677,12 +2839,13 @@ export class QueryInterface {
|
|
|
2677
2839
|
const arrayType = this.columnArrayTypeMap.get(column);
|
|
2678
2840
|
if (arrayType)
|
|
2679
2841
|
return arrayType;
|
|
2680
|
-
// Fallback heuristic for unknown columns
|
|
2842
|
+
// Fallback heuristic for unknown columns, routed through the active dialect
|
|
2843
|
+
// so non-Postgres packages can supply their own bulk-insert cast shape.
|
|
2681
2844
|
if (column === 'id' || column.endsWith('_id'))
|
|
2682
|
-
return '
|
|
2845
|
+
return this.dialect.arrayType?.('int8') ?? 'text[]';
|
|
2683
2846
|
if (column.endsWith('_at'))
|
|
2684
|
-
return 'timestamptz[]';
|
|
2685
|
-
return 'text[]';
|
|
2847
|
+
return this.dialect.arrayType?.('timestamptz') ?? 'text[]';
|
|
2848
|
+
return this.dialect.arrayType?.('text') ?? 'text[]';
|
|
2686
2849
|
}
|
|
2687
2850
|
}
|
|
2688
2851
|
//# sourceMappingURL=builder.js.map
|
package/dist/query/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
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';
|
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/dist/schema.d.ts
CHANGED
|
@@ -21,7 +21,9 @@ export interface TableMetadata {
|
|
|
21
21
|
reverseColumnMap: Record<string, string>;
|
|
22
22
|
/** snake_case columns that are timestamp/date types (need Date parsing) */
|
|
23
23
|
dateColumns: Set<string>;
|
|
24
|
-
/** snake_case column →
|
|
24
|
+
/** snake_case column → dialect-native database type. */
|
|
25
|
+
dialectTypes?: Record<string, string>;
|
|
26
|
+
/** snake_case column → Postgres type for UNNEST casts. Back-compat alias for dialectTypes. */
|
|
25
27
|
pgTypes: Record<string, string>;
|
|
26
28
|
/** All snake_case column names in ordinal order */
|
|
27
29
|
allColumns: string[];
|
|
@@ -39,7 +41,9 @@ export interface ColumnMetadata {
|
|
|
39
41
|
name: string;
|
|
40
42
|
/** camelCase field name for TypeScript */
|
|
41
43
|
field: string;
|
|
42
|
-
/**
|
|
44
|
+
/** Dialect-native database type (e.g. PostgreSQL 'int8', MySQL 'bigint', SQLite 'INTEGER'). */
|
|
45
|
+
dialectType?: string;
|
|
46
|
+
/** Postgres base type (e.g. 'int8', 'text', 'timestamptz'). Back-compat alias for dialectType. */
|
|
43
47
|
pgType: string;
|
|
44
48
|
/** TypeScript type string (e.g. 'number', 'string', 'Date') */
|
|
45
49
|
tsType: string;
|
|
@@ -49,7 +53,9 @@ export interface ColumnMetadata {
|
|
|
49
53
|
hasDefault: boolean;
|
|
50
54
|
/** Whether this is an array column */
|
|
51
55
|
isArray: boolean;
|
|
52
|
-
/**
|
|
56
|
+
/** Dialect-specific array/bulk-insert type token when needed. */
|
|
57
|
+
arrayType?: string;
|
|
58
|
+
/** Postgres array type for UNNEST (e.g. 'bigint[]'). Back-compat alias for arrayType. */
|
|
53
59
|
pgArrayType: string;
|
|
54
60
|
/** Max character length (for varchar) */
|
|
55
61
|
maxLength?: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|
|
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/",
|