turbine-orm 0.49.0 → 0.50.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 +122 -39
- package/dist/cjs/adapters/cockroachdb.d.ts +39 -0
- package/dist/cjs/adapters/index.d.ts +110 -0
- package/dist/cjs/adapters/yugabytedb.d.ts +51 -0
- package/dist/cjs/cli/config.d.ts +181 -0
- package/dist/cjs/cli/config.js +32 -6
- package/dist/cjs/cli/destructive.d.ts +38 -0
- package/dist/cjs/cli/index.d.ts +359 -0
- package/dist/cjs/cli/index.js +228 -56
- package/dist/cjs/cli/loader.d.ts +61 -0
- package/dist/cjs/cli/mcp.d.ts +42 -0
- package/dist/cjs/cli/migrate.d.ts +356 -0
- package/dist/cjs/cli/migrate.js +131 -40
- package/dist/cjs/cli/observe-ui.d.ts +1 -0
- package/dist/cjs/cli/observe-ui.js +14 -5
- package/dist/cjs/cli/observe.d.ts +25 -0
- package/dist/cjs/cli/observe.js +49 -12
- package/dist/cjs/cli/pii-tags.d.ts +53 -0
- package/dist/cjs/cli/prisma-report.d.ts +33 -0
- package/dist/cjs/cli/prisma-report.js +73 -0
- package/dist/cjs/cli/prisma-resolve.d.ts +106 -0
- package/dist/cjs/cli/prisma-resolve.js +1 -0
- package/dist/cjs/cli/prisma-schema.d.ts +176 -0
- package/dist/cjs/cli/prisma-schema.js +82 -4
- package/dist/cjs/cli/rate-limit.d.ts +32 -0
- package/dist/cjs/cli/rate-limit.js +45 -0
- package/dist/cjs/cli/studio-demo.d.ts +43 -0
- package/dist/cjs/cli/studio-ui.generated.d.ts +1 -0
- package/dist/cjs/cli/studio.d.ts +207 -0
- package/dist/cjs/cli/studio.js +136 -71
- package/dist/cjs/cli/ui.d.ts +73 -0
- package/dist/cjs/cli/ui.js +51 -9
- package/dist/cjs/client.d.ts +837 -0
- package/dist/cjs/client.js +3 -0
- package/dist/cjs/dialect.d.ts +516 -0
- package/dist/cjs/dialect.js +37 -12
- package/dist/cjs/errors.d.ts +370 -0
- package/dist/cjs/generate.d.ts +137 -0
- package/dist/cjs/generate.js +39 -6
- package/dist/cjs/index-advisor.d.ts +153 -0
- package/dist/cjs/index-stats.d.ts +384 -0
- package/dist/cjs/index.d.ts +55 -0
- package/dist/cjs/index.js +7 -2
- package/dist/cjs/introspect.d.ts +269 -0
- package/dist/cjs/mssql.d.ts +232 -0
- package/dist/cjs/mssql.js +6 -0
- package/dist/cjs/mysql.d.ts +173 -0
- package/dist/cjs/mysql.js +16 -0
- package/dist/cjs/nested-write.d.ts +96 -0
- package/dist/cjs/nested-write.js +414 -24
- package/dist/cjs/observe.d.ts +115 -0
- package/dist/cjs/optional-peer-import.d.cts +72 -0
- package/dist/cjs/pipeline-submittable.d.ts +93 -0
- package/dist/cjs/pipeline.d.ts +71 -0
- package/dist/cjs/powdb-introspect.d.ts +84 -0
- package/dist/cjs/powdb.d.ts +931 -0
- package/dist/cjs/powdb.js +106 -21
- package/dist/cjs/powql.d.ts +592 -0
- package/dist/cjs/powql.js +42 -6
- package/dist/cjs/prisma-compat.d.ts +283 -0
- package/dist/cjs/prisma-compat.js +167 -9
- package/dist/cjs/query/aggregates.d.ts +92 -0
- package/dist/cjs/query/aggregates.js +7 -3
- package/dist/cjs/query/batched-loader.d.ts +193 -0
- package/dist/cjs/query/builder.d.ts +849 -0
- package/dist/cjs/query/builder.js +571 -65
- package/dist/cjs/query/compound-unique.d.ts +51 -0
- package/dist/cjs/query/deferred.d.ts +223 -0
- package/dist/cjs/query/filters.d.ts +201 -0
- package/dist/cjs/query/index.d.ts +14 -0
- package/dist/cjs/query/index.js +6 -1
- package/dist/cjs/query/relations.d.ts +609 -0
- package/dist/cjs/query/relations.js +693 -46
- package/dist/cjs/query/types.d.ts +1300 -0
- package/dist/cjs/query/utils.d.ts +209 -0
- package/dist/cjs/query/utils.js +208 -1
- package/dist/cjs/query/warn-registry.d.ts +68 -0
- package/dist/cjs/query/warn-registry.js +9 -0
- package/dist/cjs/query/where-compile.d.ts +139 -0
- package/dist/cjs/query/where.d.ts +548 -0
- package/dist/cjs/query/where.js +58 -22
- package/dist/cjs/query/writes.d.ts +172 -0
- package/dist/cjs/query/writes.js +105 -12
- package/dist/cjs/realtime.d.ts +70 -0
- package/dist/cjs/schema-builder.d.ts +354 -0
- package/dist/cjs/schema-metadata.d.ts +83 -0
- package/dist/cjs/schema-sql.d.ts +217 -0
- package/dist/cjs/schema-sql.js +23 -5
- package/dist/cjs/schema.d.ts +356 -0
- package/dist/cjs/schema.js +125 -0
- package/dist/cjs/seed.d.ts +15 -0
- package/dist/cjs/serverless.d.ts +142 -0
- package/dist/cjs/sqlite.d.ts +143 -0
- package/dist/cjs/sqlite.js +4 -0
- package/dist/cjs/typed-sql.d.ts +102 -0
- package/dist/cli/config.d.ts +18 -4
- package/dist/cli/config.js +31 -6
- package/dist/cli/index.d.ts +123 -0
- package/dist/cli/index.js +223 -58
- package/dist/cli/migrate.d.ts +59 -10
- package/dist/cli/migrate.js +128 -41
- package/dist/cli/observe-ui.d.ts +1 -1
- package/dist/cli/observe-ui.js +14 -5
- package/dist/cli/observe.d.ts +7 -1
- package/dist/cli/observe.js +48 -12
- package/dist/cli/prisma-report.d.ts +14 -0
- package/dist/cli/prisma-report.js +72 -0
- package/dist/cli/prisma-resolve.d.ts +6 -0
- package/dist/cli/prisma-resolve.js +1 -0
- package/dist/cli/prisma-schema.d.ts +62 -2
- package/dist/cli/prisma-schema.js +81 -4
- package/dist/cli/rate-limit.d.ts +32 -0
- package/dist/cli/rate-limit.js +40 -0
- package/dist/cli/studio.d.ts +5 -5
- package/dist/cli/studio.js +135 -70
- package/dist/cli/ui.d.ts +1 -1
- package/dist/cli/ui.js +51 -9
- package/dist/client.d.ts +40 -0
- package/dist/client.js +3 -0
- package/dist/dialect.d.ts +17 -1
- package/dist/dialect.js +37 -12
- package/dist/generate.js +40 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/mssql.js +6 -0
- package/dist/mysql.js +16 -0
- package/dist/nested-write.d.ts +2 -0
- package/dist/nested-write.js +415 -25
- package/dist/powdb.d.ts +4 -2
- package/dist/powdb.js +106 -21
- package/dist/powql.d.ts +5 -0
- package/dist/powql.js +42 -6
- package/dist/prisma-compat.d.ts +2 -0
- package/dist/prisma-compat.js +166 -8
- package/dist/query/aggregates.js +7 -3
- package/dist/query/builder.d.ts +292 -21
- package/dist/query/builder.js +570 -64
- package/dist/query/deferred.d.ts +39 -0
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/relations.d.ts +173 -5
- package/dist/query/relations.js +688 -47
- package/dist/query/types.d.ts +123 -39
- package/dist/query/utils.d.ts +116 -0
- package/dist/query/utils.js +198 -0
- package/dist/query/warn-registry.d.ts +9 -0
- package/dist/query/warn-registry.js +9 -0
- package/dist/query/where.d.ts +38 -1
- package/dist/query/where.js +58 -23
- package/dist/query/writes.d.ts +42 -1
- package/dist/query/writes.js +104 -13
- package/dist/schema-sql.d.ts +14 -0
- package/dist/schema-sql.js +23 -5
- package/dist/schema.d.ts +38 -0
- package/dist/schema.js +123 -0
- package/dist/sqlite.js +4 -0
- package/package.json +77 -28
package/dist/cjs/query/where.js
CHANGED
|
@@ -47,6 +47,7 @@ exports.fingerprintScopedWhere = fingerprintScopedWhere;
|
|
|
47
47
|
exports.buildRelationFilter = buildRelationFilter;
|
|
48
48
|
exports.buildSubWhereForRelation = buildSubWhereForRelation;
|
|
49
49
|
exports.pgTypeForColumn = pgTypeForColumn;
|
|
50
|
+
exports.coerceWhereOperand = coerceWhereOperand;
|
|
50
51
|
exports.enumTypeForColumn = enumTypeForColumn;
|
|
51
52
|
exports.enumCastSuffix = enumCastSuffix;
|
|
52
53
|
exports.assertBindableEqualityValue = assertBindableEqualityValue;
|
|
@@ -220,6 +221,7 @@ function collectScalarParams(qi, key, value, params) {
|
|
|
220
221
|
meta: qi.tableMeta,
|
|
221
222
|
table: qi.table,
|
|
222
223
|
prefix: '',
|
|
224
|
+
rawColumn,
|
|
223
225
|
});
|
|
224
226
|
return;
|
|
225
227
|
default:
|
|
@@ -227,7 +229,7 @@ function collectScalarParams(qi, key, value, params) {
|
|
|
227
229
|
// the build path, so a cache hit can never silently bind a
|
|
228
230
|
// misspelled-operator object.
|
|
229
231
|
assertBindableEqualityValue(qi, rawColumn, value, getColumnPgType(qi, rawColumn), qi.table);
|
|
230
|
-
params.push(value);
|
|
232
|
+
params.push(coerceWhereOperand(qi, qi.tableMeta, rawColumn, value));
|
|
231
233
|
return;
|
|
232
234
|
}
|
|
233
235
|
}
|
|
@@ -290,24 +292,26 @@ function collectOperatorParams(qi, column, op, params, refCtx) {
|
|
|
290
292
|
resolveColumnRef(qi, v, refCtx, op.mode);
|
|
291
293
|
return true;
|
|
292
294
|
};
|
|
295
|
+
// Mirrors buildOperatorClauses' temporal bind rewrite exactly.
|
|
296
|
+
const cv = (v) => (refCtx ? coerceWhereOperand(qi, refCtx.meta, refCtx.rawColumn, v) : v);
|
|
293
297
|
if (op.equals !== undefined && op.equals !== null && !skipRef(op.equals)) {
|
|
294
298
|
(0, filters_js_1.assertBindableEqualsOperand)(op.equals, `"${column}"`);
|
|
295
|
-
params.push(op.equals);
|
|
299
|
+
params.push(cv(op.equals));
|
|
296
300
|
}
|
|
297
301
|
if (op.gt !== undefined && !skipRef(op.gt))
|
|
298
|
-
params.push(op.gt);
|
|
302
|
+
params.push(cv(op.gt));
|
|
299
303
|
if (op.gte !== undefined && !skipRef(op.gte))
|
|
300
|
-
params.push(op.gte);
|
|
304
|
+
params.push(cv(op.gte));
|
|
301
305
|
if (op.lt !== undefined && !skipRef(op.lt))
|
|
302
|
-
params.push(op.lt);
|
|
306
|
+
params.push(cv(op.lt));
|
|
303
307
|
if (op.lte !== undefined && !skipRef(op.lte))
|
|
304
|
-
params.push(op.lte);
|
|
308
|
+
params.push(cv(op.lte));
|
|
305
309
|
if (op.not !== undefined && op.not !== null && !skipRef(op.not))
|
|
306
|
-
params.push(op.not);
|
|
310
|
+
params.push(cv(op.not));
|
|
307
311
|
if (op.in !== undefined)
|
|
308
|
-
params.push(qi.inParam(op.in));
|
|
312
|
+
params.push(qi.inParam(cv(op.in)));
|
|
309
313
|
if (op.notIn !== undefined)
|
|
310
|
-
params.push(qi.inParam(op.notIn));
|
|
314
|
+
params.push(qi.inParam(cv(op.notIn)));
|
|
311
315
|
if (op.contains !== undefined)
|
|
312
316
|
params.push(`%${(0, utils_js_1.escapeLike)(op.contains)}%`);
|
|
313
317
|
if (op.startsWith !== undefined)
|
|
@@ -617,6 +621,7 @@ function buildScalarClause(qi, key, value, params, andClauses) {
|
|
|
617
621
|
meta: qi.tableMeta,
|
|
618
622
|
table: qi.table,
|
|
619
623
|
prefix: '',
|
|
624
|
+
rawColumn,
|
|
620
625
|
}));
|
|
621
626
|
return;
|
|
622
627
|
default:
|
|
@@ -624,7 +629,7 @@ function buildScalarClause(qi, key, value, params, andClauses) {
|
|
|
624
629
|
// is almost always a misspelled operator (`startWith` for `startsWith`);
|
|
625
630
|
// the guard also runs on the cache-hit param-collect path.
|
|
626
631
|
assertBindableEqualityValue(qi, rawColumn, value, getColumnPgType(qi, rawColumn), qi.table);
|
|
627
|
-
params.push(value);
|
|
632
|
+
params.push(coerceWhereOperand(qi, qi.tableMeta, rawColumn, value));
|
|
628
633
|
andClauses.push(`${column} = ${qi.p(params.length)}`);
|
|
629
634
|
return;
|
|
630
635
|
}
|
|
@@ -766,11 +771,12 @@ function buildScopedScalarClause(qi, scope, field, value, params, clauses) {
|
|
|
766
771
|
meta,
|
|
767
772
|
table: scope.table,
|
|
768
773
|
prefix: scope.qualifier,
|
|
774
|
+
rawColumn: col,
|
|
769
775
|
}));
|
|
770
776
|
return;
|
|
771
777
|
}
|
|
772
778
|
assertBindableEqualityValue(qi, col, value, pgTypeForColumn(qi, meta, col), scope.table);
|
|
773
|
-
params.push(value);
|
|
779
|
+
params.push(coerceWhereOperand(qi, meta, col, value));
|
|
774
780
|
clauses.push(`${qCol} = ${qi.p(params.length)}`);
|
|
775
781
|
}
|
|
776
782
|
/**
|
|
@@ -822,11 +828,11 @@ function collectScopedScalarParams(qi, scope, field, value, params) {
|
|
|
822
828
|
}
|
|
823
829
|
}
|
|
824
830
|
if ((0, filters_js_1.isWhereOperator)(value)) {
|
|
825
|
-
collectOperatorParams(qi, col, value, params, { meta, table: scope.table, prefix: '' });
|
|
831
|
+
collectOperatorParams(qi, col, value, params, { meta, table: scope.table, prefix: '', rawColumn: col });
|
|
826
832
|
return;
|
|
827
833
|
}
|
|
828
834
|
assertBindableEqualityValue(qi, col, value, pgTypeForColumn(qi, meta, col), scope.table);
|
|
829
|
-
params.push(value);
|
|
835
|
+
params.push(coerceWhereOperand(qi, meta, col, value));
|
|
830
836
|
}
|
|
831
837
|
/**
|
|
832
838
|
* Value-invariant fingerprint of a scoped sub-where. Same canonical
|
|
@@ -1006,6 +1012,30 @@ function buildSubWhereForRelation(qi, targetTable, subWhere, params) {
|
|
|
1006
1012
|
function pgTypeForColumn(_qi, meta, column) {
|
|
1007
1013
|
return meta.dialectTypes?.[column] ?? meta.pgTypes?.[column] ?? 'text';
|
|
1008
1014
|
}
|
|
1015
|
+
/**
|
|
1016
|
+
* Rewrite a WHERE operand bound against `column` the same way the write path
|
|
1017
|
+
* rewrites a `data` value ({@link coerceTemporalValue}): a JS `Date` on a
|
|
1018
|
+
* `time` / `timetz` column becomes a time-of-day literal (Postgres otherwise
|
|
1019
|
+
* answers `22007 invalid input syntax for type time` for the ISO timestamp the
|
|
1020
|
+
* driver would send), and on a zone-less `date` / `timestamp` column it becomes
|
|
1021
|
+
* the UTC-component literal, so a predicate matches the value a write of the
|
|
1022
|
+
* same `Date` stored.
|
|
1023
|
+
*
|
|
1024
|
+
* This is a VALUE transform only — it never changes the emitted SQL — so the
|
|
1025
|
+
* SQL-template cache is unaffected, and it is applied on the cache-hit
|
|
1026
|
+
* param-collect path as well as the build path.
|
|
1027
|
+
*
|
|
1028
|
+
* `timestamptz` and every non-temporal column are returned by identity.
|
|
1029
|
+
*/
|
|
1030
|
+
function coerceWhereOperand(qi, meta, column, value) {
|
|
1031
|
+
if (!(value instanceof Date) && !Array.isArray(value))
|
|
1032
|
+
return value;
|
|
1033
|
+
return (0, utils_js_1.coerceTemporalValue)(pgTypeForColumn(qi, meta, column), value,
|
|
1034
|
+
// Same PostgreSQL + `utcTimestamps` gate as the write path (see
|
|
1035
|
+
// `utcDateTimeWrites` in writes.ts): only the read/write-symmetric engine
|
|
1036
|
+
// gets the zone-less rewrite. Time-of-day always rewrites.
|
|
1037
|
+
qi.dialect.name === 'postgresql' && qi.utcTimestamps !== false);
|
|
1038
|
+
}
|
|
1009
1039
|
/**
|
|
1010
1040
|
* The Postgres enum type name for a column, when the schema knows one.
|
|
1011
1041
|
*
|
|
@@ -1143,6 +1173,9 @@ function columnRefSql(qi, ref, ctx, mode) {
|
|
|
1143
1173
|
*/
|
|
1144
1174
|
function buildOperatorClauses(qi, column, op, params, refCtx) {
|
|
1145
1175
|
const clauses = [];
|
|
1176
|
+
// Temporal bind rewrite, identical to `collectOperatorParams`. Value-only, so
|
|
1177
|
+
// the emitted SQL (and therefore the template cache) is untouched.
|
|
1178
|
+
const cv = (v) => (refCtx ? coerceWhereOperand(qi, refCtx.meta, refCtx.rawColumn, v) : v);
|
|
1146
1179
|
if (op.equals !== undefined) {
|
|
1147
1180
|
if (op.equals === null) {
|
|
1148
1181
|
clauses.push(`${column} IS NULL`);
|
|
@@ -1152,7 +1185,7 @@ function buildOperatorClauses(qi, column, op, params, refCtx) {
|
|
|
1152
1185
|
}
|
|
1153
1186
|
else {
|
|
1154
1187
|
(0, filters_js_1.assertBindableEqualsOperand)(op.equals, column);
|
|
1155
|
-
params.push(op.equals);
|
|
1188
|
+
params.push(cv(op.equals));
|
|
1156
1189
|
clauses.push(`${column} = ${qi.p(params.length)}`);
|
|
1157
1190
|
}
|
|
1158
1191
|
}
|
|
@@ -1161,7 +1194,7 @@ function buildOperatorClauses(qi, column, op, params, refCtx) {
|
|
|
1161
1194
|
clauses.push(`${column} > ${columnRefSql(qi, op.gt, refCtx, op.mode)}`);
|
|
1162
1195
|
}
|
|
1163
1196
|
else {
|
|
1164
|
-
params.push(op.gt);
|
|
1197
|
+
params.push(cv(op.gt));
|
|
1165
1198
|
clauses.push(`${column} > ${qi.p(params.length)}`);
|
|
1166
1199
|
}
|
|
1167
1200
|
}
|
|
@@ -1170,7 +1203,7 @@ function buildOperatorClauses(qi, column, op, params, refCtx) {
|
|
|
1170
1203
|
clauses.push(`${column} >= ${columnRefSql(qi, op.gte, refCtx, op.mode)}`);
|
|
1171
1204
|
}
|
|
1172
1205
|
else {
|
|
1173
|
-
params.push(op.gte);
|
|
1206
|
+
params.push(cv(op.gte));
|
|
1174
1207
|
clauses.push(`${column} >= ${qi.p(params.length)}`);
|
|
1175
1208
|
}
|
|
1176
1209
|
}
|
|
@@ -1179,7 +1212,7 @@ function buildOperatorClauses(qi, column, op, params, refCtx) {
|
|
|
1179
1212
|
clauses.push(`${column} < ${columnRefSql(qi, op.lt, refCtx, op.mode)}`);
|
|
1180
1213
|
}
|
|
1181
1214
|
else {
|
|
1182
|
-
params.push(op.lt);
|
|
1215
|
+
params.push(cv(op.lt));
|
|
1183
1216
|
clauses.push(`${column} < ${qi.p(params.length)}`);
|
|
1184
1217
|
}
|
|
1185
1218
|
}
|
|
@@ -1188,7 +1221,7 @@ function buildOperatorClauses(qi, column, op, params, refCtx) {
|
|
|
1188
1221
|
clauses.push(`${column} <= ${columnRefSql(qi, op.lte, refCtx, op.mode)}`);
|
|
1189
1222
|
}
|
|
1190
1223
|
else {
|
|
1191
|
-
params.push(op.lte);
|
|
1224
|
+
params.push(cv(op.lte));
|
|
1192
1225
|
clauses.push(`${column} <= ${qi.p(params.length)}`);
|
|
1193
1226
|
}
|
|
1194
1227
|
}
|
|
@@ -1200,16 +1233,16 @@ function buildOperatorClauses(qi, column, op, params, refCtx) {
|
|
|
1200
1233
|
clauses.push(`${column} != ${columnRefSql(qi, op.not, refCtx, op.mode)}`);
|
|
1201
1234
|
}
|
|
1202
1235
|
else {
|
|
1203
|
-
params.push(op.not);
|
|
1236
|
+
params.push(cv(op.not));
|
|
1204
1237
|
clauses.push(`${column} != ${qi.p(params.length)}`);
|
|
1205
1238
|
}
|
|
1206
1239
|
}
|
|
1207
1240
|
if (op.in !== undefined) {
|
|
1208
|
-
params.push(qi.inParam(op.in));
|
|
1241
|
+
params.push(qi.inParam(cv(op.in)));
|
|
1209
1242
|
clauses.push(qi.inClause(column, qi.p(params.length), false));
|
|
1210
1243
|
}
|
|
1211
1244
|
if (op.notIn !== undefined) {
|
|
1212
|
-
params.push(qi.inParam(op.notIn));
|
|
1245
|
+
params.push(qi.inParam(cv(op.notIn)));
|
|
1213
1246
|
clauses.push(qi.inClause(column, qi.p(params.length), true));
|
|
1214
1247
|
}
|
|
1215
1248
|
const buildLikeClause = (paramRef) => op.mode === 'insensitive' ? qi.dialect.buildInsensitiveLike(column, paramRef) : `${column} LIKE ${paramRef}`;
|
|
@@ -1269,7 +1302,10 @@ function vectorOperator(qi, field, rawColumn, metric) {
|
|
|
1269
1302
|
throw new errors_js_1.ValidationError(`[turbine] Column "${field}" on table "${qi.table}" is not a vector column ` +
|
|
1270
1303
|
`(actual type: ${colType}); cannot apply a vector distance operation.`);
|
|
1271
1304
|
}
|
|
1272
|
-
|
|
1305
|
+
// ownLookup, not a bare index: an inherited Object.prototype member
|
|
1306
|
+
// ("constructor", "toString", …) would otherwise resolve to a truthy builtin
|
|
1307
|
+
// and be spliced into the ORDER BY / WHERE clause as its source text.
|
|
1308
|
+
const op = (0, utils_js_1.ownLookup)(filters_js_1.VECTOR_METRIC_OPERATORS, metric);
|
|
1273
1309
|
if (!op) {
|
|
1274
1310
|
throw new errors_js_1.ValidationError(`[turbine] Unknown vector metric "${metric}" for column "${field}". ` +
|
|
1275
1311
|
`Valid metrics: ${Object.keys(filters_js_1.VECTOR_METRIC_OPERATORS).join(', ')}.`);
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm: write compilation (extracted from builder.ts)
|
|
3
|
+
*
|
|
4
|
+
* SQL builders for the mutating operations (create / createMany / update /
|
|
5
|
+
* delete / upsert / updateMany / deleteMany) plus the write-projection helpers
|
|
6
|
+
* (writeReturningColumns / writeReselectSelection / parseWriteRow, the PII
|
|
7
|
+
* column set, optimistic-lock and atomic-operator SET clauses). All functions
|
|
8
|
+
* take a {@link BuilderCtx} first argument; WHERE compilation is reused from
|
|
9
|
+
* where.ts (via `whereMod`), and the cache / dialect / row-parse primitives
|
|
10
|
+
* stay class-resident, reached through the ctx. See builder.ts for the thin
|
|
11
|
+
* delegating methods and the async execute wrappers.
|
|
12
|
+
*/
|
|
13
|
+
import type { ReturningSelection } from '../dialect.js';
|
|
14
|
+
import type { TableMetadata } from '../schema.js';
|
|
15
|
+
import type { DeferredQuery } from './deferred.js';
|
|
16
|
+
import type { CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, UpdateArgs, UpdateManyArgs, UpsertArgs } from './types.js';
|
|
17
|
+
import type { BuilderCtx } from './where.js';
|
|
18
|
+
/**
|
|
19
|
+
* Normalize one `data` value before it is bound as a write param.
|
|
20
|
+
*
|
|
21
|
+
* Two value shapes need rewriting, both a JS `Date` on a temporal column:
|
|
22
|
+
*
|
|
23
|
+
* 1. A time-of-day column (`time` / `timetz`) — the driver serializes a `Date`
|
|
24
|
+
* as a full ISO timestamp with the process offset and Postgres answers
|
|
25
|
+
* `22007 invalid input syntax for type time`. Rewritten on every engine.
|
|
26
|
+
* 2. A zone-less `date` / `timestamp` column on PostgreSQL — the driver's
|
|
27
|
+
* local-offset serialization stores the PROCESS's calendar fields, so in a
|
|
28
|
+
* non-UTC process the stored value is wrong and (because the read path
|
|
29
|
+
* interprets an offset-less value as UTC) does not round-trip. Rewritten to
|
|
30
|
+
* the UTC components, mirroring `parseDbDate`. See `toLocalDateTimeLiteral`
|
|
31
|
+
* in query/utils.ts.
|
|
32
|
+
*
|
|
33
|
+
* `timestamptz` is deliberately untouched: it stores a real instant and the
|
|
34
|
+
* driver's local-offset string is already correct for it.
|
|
35
|
+
*
|
|
36
|
+
* Every other value, including a string that is already a valid literal, is
|
|
37
|
+
* returned by identity, so this is a byte-for-byte no-op for every non-temporal
|
|
38
|
+
* column.
|
|
39
|
+
*/
|
|
40
|
+
export declare function coerceWriteValue(qi: BuilderCtx, key: string, value: unknown): unknown;
|
|
41
|
+
/**
|
|
42
|
+
* Build a `SELECT * ... WHERE <predicate>` that re-fetches the row(s) matched
|
|
43
|
+
* by a write's `where` clause. Used by the `'reselect'` result strategy to
|
|
44
|
+
* return rows from non-RETURNING engines. Reuses the same parameterized WHERE
|
|
45
|
+
* builder as reads, so no user value is interpolated.
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildReselectByWhere(qi: BuilderCtx, whereObj: Record<string, unknown>): {
|
|
48
|
+
sql: string;
|
|
49
|
+
params: unknown[];
|
|
50
|
+
};
|
|
51
|
+
export declare function buildCreate<T extends object>(qi: BuilderCtx, args: CreateArgs<T>): DeferredQuery<T>;
|
|
52
|
+
/**
|
|
53
|
+
* Build the `'reselect'` plan for {@link buildCreate}: run the INSERT, then
|
|
54
|
+
* `SELECT * WHERE pk = ?`. Returns `undefined` (skipped) unless the active
|
|
55
|
+
* dialect's result strategy is `'reselect'`, so the PostgreSQL/RETURNING path
|
|
56
|
+
* pays nothing. Not yet wired to a real non-RETURNING engine.
|
|
57
|
+
*/
|
|
58
|
+
export declare function makeCreateReselect<T extends object>(qi: BuilderCtx, insertSql: string, insertParams: unknown[], data: Record<string, unknown>): DeferredQuery<T>['reselect'];
|
|
59
|
+
export declare function buildCreateMany<T extends object>(qi: BuilderCtx, args: CreateManyArgs<T>): DeferredQuery<T[]>;
|
|
60
|
+
export declare function buildUpdate<T extends object>(qi: BuilderCtx, args: UpdateArgs<T>): DeferredQuery<T>;
|
|
61
|
+
export declare function buildDelete<T extends object>(qi: BuilderCtx, args: DeleteArgs<T>): DeferredQuery<T>;
|
|
62
|
+
export declare function buildUpsert<T extends object>(qi: BuilderCtx, args: UpsertArgs<T>): DeferredQuery<T>;
|
|
63
|
+
export declare function buildUpdateMany<T extends object>(qi: BuilderCtx, args: UpdateManyArgs<T>): DeferredQuery<{
|
|
64
|
+
count: number;
|
|
65
|
+
}>;
|
|
66
|
+
export declare function buildDeleteMany<T extends object>(qi: BuilderCtx, args: DeleteManyArgs<T>): DeferredQuery<{
|
|
67
|
+
count: number;
|
|
68
|
+
}>;
|
|
69
|
+
/**
|
|
70
|
+
* The snake_case names of a table's PII-tagged (`defineSchema` `pii: true`)
|
|
71
|
+
* columns. PII columns are excluded from default projections (findMany /
|
|
72
|
+
* findUnique / relation subqueries / batched loads) unless the query opts in
|
|
73
|
+
* via `includePii` or names the column explicitly in `select`. Returns an
|
|
74
|
+
* empty set for any table with no PII column, so untagged schemas keep their
|
|
75
|
+
* byte-identical SQL.
|
|
76
|
+
*/
|
|
77
|
+
export declare function piiColumns(_qi: BuilderCtx, meta: TableMetadata): Set<string>;
|
|
78
|
+
/**
|
|
79
|
+
* The camelCase field names of a table's PII-tagged columns: the read-side
|
|
80
|
+
* counterpart of {@link piiColumns} applied to already-parsed entities.
|
|
81
|
+
* Used to strip PII from a write's RETURNING/reselect row (writes accept no
|
|
82
|
+
* `includePii`/`select`, so their returned row always applies the default
|
|
83
|
+
* exclusion; you may still write PII fields freely).
|
|
84
|
+
*/
|
|
85
|
+
export declare function piiFields(_qi: BuilderCtx, meta: TableMetadata): string[];
|
|
86
|
+
/**
|
|
87
|
+
* The `RETURNING` / `OUTPUT` selection for a write on this table. A table with
|
|
88
|
+
* no PII column returns `'*'` (every column — byte-identical SQL to before);
|
|
89
|
+
* a table WITH PII columns returns an explicit quoted list of every non-PII
|
|
90
|
+
* column so the PII values never leave the database on a write. A PII-tagged
|
|
91
|
+
* PRIMARY KEY column is kept in the projection regardless (the returned row
|
|
92
|
+
* must stay addressable): tag sensitive data, not keys — a PII PK is
|
|
93
|
+
* documented out of scope for stripping. Writes accept no `select`/`includePii`
|
|
94
|
+
* (unlike reads), so this is the whole write-return policy at the SQL level;
|
|
95
|
+
* {@link parseWriteRow} remains as a defense-in-depth strip (a no-op once the
|
|
96
|
+
* SQL already excludes the columns). Derived purely from static per-table
|
|
97
|
+
* schema metadata, so the write SQL cache needs no extra key segment.
|
|
98
|
+
*/
|
|
99
|
+
export declare function writeReturningColumns(qi: BuilderCtx): ReturningSelection;
|
|
100
|
+
/**
|
|
101
|
+
* String form of {@link writeReturningColumns} for a `SELECT` list (the
|
|
102
|
+
* `'reselect'` result strategy re-fetches via a SELECT, not RETURNING).
|
|
103
|
+
* `'*'` when there is no PII column; otherwise the comma-joined quoted list.
|
|
104
|
+
*/
|
|
105
|
+
export declare function writeReselectSelection(qi: BuilderCtx): string;
|
|
106
|
+
/**
|
|
107
|
+
* Parse a write's returned row (create/update/upsert/delete), then strip the
|
|
108
|
+
* table's PII fields: the write-side read policy. On PII-tagged tables the
|
|
109
|
+
* statement's RETURNING/OUTPUT already omits these columns (see
|
|
110
|
+
* {@link writeReturningColumns}), so this strip is defense-in-depth and a
|
|
111
|
+
* no-op. Untagged tables incur only one `for` over a zero-length field list,
|
|
112
|
+
* so behavior is unchanged.
|
|
113
|
+
*/
|
|
114
|
+
export declare function parseWriteRow(qi: BuilderCtx, row: Record<string, unknown>): Record<string, unknown>;
|
|
115
|
+
/**
|
|
116
|
+
* Reject any write against a view (H4). Views are introspected with
|
|
117
|
+
* `isView: true` and are read-only in every engine; a write raises a
|
|
118
|
+
* {@link ValidationError} (E003) rather than emitting SQL Postgres would
|
|
119
|
+
* reject (or, worse, silently applying to an updatable view).
|
|
120
|
+
*/
|
|
121
|
+
export declare function assertWritable(qi: BuilderCtx, operation: string): void;
|
|
122
|
+
/**
|
|
123
|
+
* Reject a write whose `data` names a `GENERATED ALWAYS AS (...) STORED`
|
|
124
|
+
* column (H3). Postgres computes these from other columns and errors if you
|
|
125
|
+
* try to write them; we fail early with a clear {@link ValidationError} (E003)
|
|
126
|
+
* instead of surfacing a cryptic driver error. Undefined values are ignored
|
|
127
|
+
* (they're stripped from the statement anyway).
|
|
128
|
+
*/
|
|
129
|
+
export declare function assertNoGeneratedColumns(qi: BuilderCtx, data: Record<string, unknown>, operation: string): void;
|
|
130
|
+
/**
|
|
131
|
+
* Build a single SET clause entry for update/updateMany.
|
|
132
|
+
*
|
|
133
|
+
* Supports plain values and atomic operator objects ({ set, increment,
|
|
134
|
+
* decrement, multiply, divide }). An operator object is detected ONLY when
|
|
135
|
+
* it has EXACTLY one key that is one of the 5 operator keys — this avoids
|
|
136
|
+
* misinterpreting JSON column values like `{ set: 'x' }` as operators
|
|
137
|
+
* (real operator objects always have exactly one key, and a plain JSON
|
|
138
|
+
* payload that happens to have a single `set` key is extremely unusual).
|
|
139
|
+
* Multi-key objects are always treated as plain (JSON) values.
|
|
140
|
+
*
|
|
141
|
+
* Returns the SQL fragment (e.g., `"view_count" = "view_count" + $3`) and
|
|
142
|
+
* pushes any required params onto the shared params array so that WHERE
|
|
143
|
+
* clause numbering continues correctly afterward.
|
|
144
|
+
*/
|
|
145
|
+
export declare function buildSetClause(qi: BuilderCtx, key: string, value: unknown, params: unknown[]): string;
|
|
146
|
+
/**
|
|
147
|
+
* Reject a `data` value that reached {@link buildSetClause}'s plain-value
|
|
148
|
+
* fallthrough as a plain object with exactly ONE key that is not a known
|
|
149
|
+
* atomic operator. On a scalar column that shape can only be a misspelled
|
|
150
|
+
* operator (`{ incremnt: 1 }`, `{ bogus: 'x' }`); binding it plainly writes the
|
|
151
|
+
* JSON text `{"bogus":"x"}` into the column and silently corrupts it.
|
|
152
|
+
*
|
|
153
|
+
* Deliberately narrow. Skipped for:
|
|
154
|
+
* - json / jsonb columns, which legitimately take arbitrary objects;
|
|
155
|
+
* - class instances (Buffer for bytea, Decimal wrappers, …), arrays and Dates,
|
|
156
|
+
* which {@link isUnmatchedPlainObject} already excludes;
|
|
157
|
+
* - multi-key objects, which are never operator-shaped (a real operator object
|
|
158
|
+
* has exactly one key) and are far more likely a deliberate payload.
|
|
159
|
+
*
|
|
160
|
+
* Shared by the SQL-build path and the cache-hit param-collect path, so a
|
|
161
|
+
* warmed template can never skip the check.
|
|
162
|
+
*/
|
|
163
|
+
export declare function assertBindableSetValue(qi: BuilderCtx, key: string, value: unknown): void;
|
|
164
|
+
/**
|
|
165
|
+
* Fingerprint SET clauses for update/updateMany.
|
|
166
|
+
* Captures key names + operator types (set/increment/etc) but not values.
|
|
167
|
+
*/
|
|
168
|
+
export declare function fingerprintSet(_qi: BuilderCtx, data: Record<string, unknown>): string;
|
|
169
|
+
/**
|
|
170
|
+
* Collect SET params for update/updateMany. Mirrors buildSetClause param order.
|
|
171
|
+
*/
|
|
172
|
+
export declare function collectSetParams(qi: BuilderCtx, data: Record<string, unknown>, params: unknown[]): void;
|
package/dist/cjs/query/writes.js
CHANGED
|
@@ -45,6 +45,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
45
45
|
};
|
|
46
46
|
})();
|
|
47
47
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
48
|
+
exports.coerceWriteValue = coerceWriteValue;
|
|
48
49
|
exports.buildReselectByWhere = buildReselectByWhere;
|
|
49
50
|
exports.buildCreate = buildCreate;
|
|
50
51
|
exports.makeCreateReselect = makeCreateReselect;
|
|
@@ -62,13 +63,64 @@ exports.parseWriteRow = parseWriteRow;
|
|
|
62
63
|
exports.assertWritable = assertWritable;
|
|
63
64
|
exports.assertNoGeneratedColumns = assertNoGeneratedColumns;
|
|
64
65
|
exports.buildSetClause = buildSetClause;
|
|
66
|
+
exports.assertBindableSetValue = assertBindableSetValue;
|
|
65
67
|
exports.fingerprintSet = fingerprintSet;
|
|
66
68
|
exports.collectSetParams = collectSetParams;
|
|
67
69
|
const errors_js_1 = require("../errors.js");
|
|
68
70
|
const schema_js_1 = require("../schema.js");
|
|
69
71
|
const compound_unique_js_1 = require("./compound-unique.js");
|
|
70
72
|
const filters_js_1 = require("./filters.js");
|
|
73
|
+
const utils_js_1 = require("./utils.js");
|
|
71
74
|
const whereMod = __importStar(require("./where.js"));
|
|
75
|
+
/**
|
|
76
|
+
* Normalize one `data` value before it is bound as a write param.
|
|
77
|
+
*
|
|
78
|
+
* Two value shapes need rewriting, both a JS `Date` on a temporal column:
|
|
79
|
+
*
|
|
80
|
+
* 1. A time-of-day column (`time` / `timetz`) — the driver serializes a `Date`
|
|
81
|
+
* as a full ISO timestamp with the process offset and Postgres answers
|
|
82
|
+
* `22007 invalid input syntax for type time`. Rewritten on every engine.
|
|
83
|
+
* 2. A zone-less `date` / `timestamp` column on PostgreSQL — the driver's
|
|
84
|
+
* local-offset serialization stores the PROCESS's calendar fields, so in a
|
|
85
|
+
* non-UTC process the stored value is wrong and (because the read path
|
|
86
|
+
* interprets an offset-less value as UTC) does not round-trip. Rewritten to
|
|
87
|
+
* the UTC components, mirroring `parseDbDate`. See `toLocalDateTimeLiteral`
|
|
88
|
+
* in query/utils.ts.
|
|
89
|
+
*
|
|
90
|
+
* `timestamptz` is deliberately untouched: it stores a real instant and the
|
|
91
|
+
* driver's local-offset string is already correct for it.
|
|
92
|
+
*
|
|
93
|
+
* Every other value, including a string that is already a valid literal, is
|
|
94
|
+
* returned by identity, so this is a byte-for-byte no-op for every non-temporal
|
|
95
|
+
* column.
|
|
96
|
+
*/
|
|
97
|
+
function coerceWriteValue(qi, key, value) {
|
|
98
|
+
// Cheap shape check first: the common path costs one check and no lookup.
|
|
99
|
+
if (!(value instanceof Date) && !Array.isArray(value))
|
|
100
|
+
return value;
|
|
101
|
+
// Non-throwing column resolution: this runs on the cache-HIT param-collect
|
|
102
|
+
// path too, where an unknown key must not turn into a different error than
|
|
103
|
+
// the build path already raises.
|
|
104
|
+
const column = (0, utils_js_1.ownLookup)(qi.tableMeta.columnMap, key);
|
|
105
|
+
if (!column)
|
|
106
|
+
return value;
|
|
107
|
+
// Metadata generated by an older Turbine still carries per-column types
|
|
108
|
+
// (`pgTypes` has always been part of TableMetadata), and a column missing
|
|
109
|
+
// from the map resolves to undefined → no coercion, today's behavior.
|
|
110
|
+
return (0, utils_js_1.coerceTemporalValue)(qi.columnPgTypeMap.get(column), value, utcDateTimeWrites(qi));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Whether the zone-less `date` / `timestamp` UTC rewrite applies.
|
|
114
|
+
*
|
|
115
|
+
* PostgreSQL only: MySQL's `TIMESTAMP`/`DATETIME` and SQL Server's
|
|
116
|
+
* `datetime2` are converted or bound by their own drivers, and MySQL in
|
|
117
|
+
* particular reads a zone-less literal in the SESSION time zone, so a UTC
|
|
118
|
+
* literal would be misread there. `utcTimestamps: false` opts out — it is the
|
|
119
|
+
* same switch that turns off the UTC READ parsing, so the two stay symmetric.
|
|
120
|
+
*/
|
|
121
|
+
function utcDateTimeWrites(qi) {
|
|
122
|
+
return qi.dialect.name === 'postgresql' && qi.utcTimestamps !== false;
|
|
123
|
+
}
|
|
72
124
|
/**
|
|
73
125
|
* Build a `SELECT * ... WHERE <predicate>` that re-fetches the row(s) matched
|
|
74
126
|
* by a write's `where` clause. Used by the `'reselect'` result strategy to
|
|
@@ -86,7 +138,7 @@ function buildCreate(qi, args) {
|
|
|
86
138
|
assertNoGeneratedColumns(qi, args.data, 'create');
|
|
87
139
|
const entries = Object.entries(args.data).filter(([, v]) => v !== undefined);
|
|
88
140
|
const columns = entries.map(([k]) => qi.toSqlColumn(k));
|
|
89
|
-
const params = entries.map(([, v]) => v);
|
|
141
|
+
const params = entries.map(([k, v]) => coerceWriteValue(qi, k, v));
|
|
90
142
|
// Enum columns get an explicit `::"EnumName"` cast (see enumTypeForColumn).
|
|
91
143
|
const placeholders = entries.map(([k], i) => `${qi.p(i + 1)}${whereMod.enumCastSuffix(qi, qi.toColumn(k))}`);
|
|
92
144
|
const sql = qi.dialect.buildInsertStatement({
|
|
@@ -132,7 +184,9 @@ function makeCreateReselect(qi, insertSql, insertParams, data) {
|
|
|
132
184
|
let idx = 1;
|
|
133
185
|
for (const pk of qi.tableMeta.primaryKey) {
|
|
134
186
|
const field = qi.tableMeta.reverseColumnMap[pk] ?? (0, schema_js_1.snakeToCamel)(pk);
|
|
135
|
-
|
|
187
|
+
// Same rewrite the INSERT's own params got: a temporal PK re-fetched with
|
|
188
|
+
// a raw Date would not match the literal that was just written.
|
|
189
|
+
selParams.push(coerceWriteValue(qi, field, data[field] ?? data[pk] ?? insertId));
|
|
136
190
|
conds.push(`${qi.q(pk)} = ${qi.p(idx++)}`);
|
|
137
191
|
}
|
|
138
192
|
const where = conds.length > 0 ? ` WHERE ${conds.join(' AND ')}` : '';
|
|
@@ -157,7 +211,7 @@ function buildCreateMany(qi, args) {
|
|
|
157
211
|
const columns = keys.map((k) => qi.toColumn(k));
|
|
158
212
|
const rowValues = args.data.map((row) => {
|
|
159
213
|
const record = row;
|
|
160
|
-
return keys.map((key) => record[key]);
|
|
214
|
+
return keys.map((key) => coerceWriteValue(qi, key, record[key]));
|
|
161
215
|
});
|
|
162
216
|
// Use actual Postgres types for array casts in the default PostgreSQL dialect.
|
|
163
217
|
// Enum columns cast to `"EnumName"[]` — the generic text[] fallback would
|
|
@@ -357,7 +411,7 @@ function buildUpsert(qi, args) {
|
|
|
357
411
|
// Build the INSERT part from create data
|
|
358
412
|
const createEntries = Object.entries(args.create).filter(([, v]) => v !== undefined);
|
|
359
413
|
const columns = createEntries.map(([k]) => qi.toSqlColumn(k));
|
|
360
|
-
const createParams = createEntries.map(([, v]) => v);
|
|
414
|
+
const createParams = createEntries.map(([k, v]) => coerceWriteValue(qi, k, v));
|
|
361
415
|
// Enum columns get an explicit `::"EnumName"` cast (see enumTypeForColumn).
|
|
362
416
|
const placeholders = createEntries.map(([k], i) => `${qi.p(i + 1)}${whereMod.enumCastSuffix(qi, qi.toColumn(k))}`);
|
|
363
417
|
// The conflict target comes from `where` keys — must be unique/PK columns
|
|
@@ -371,7 +425,7 @@ function buildUpsert(qi, args) {
|
|
|
371
425
|
paramIdx++;
|
|
372
426
|
return clause;
|
|
373
427
|
});
|
|
374
|
-
const updateParams = updateEntries.map(([, v]) => v);
|
|
428
|
+
const updateParams = updateEntries.map(([k, v]) => coerceWriteValue(qi, k, v));
|
|
375
429
|
const params = [...createParams, ...updateParams];
|
|
376
430
|
// Global filter → restrict the conflict-UPDATE (soft-delete / tenancy) so an
|
|
377
431
|
// upsert never resurrects a soft-deleted row or writes across tenants. Only
|
|
@@ -611,7 +665,7 @@ function buildSetClause(qi, key, value, params) {
|
|
|
611
665
|
const op = keys[0];
|
|
612
666
|
const opValue = v[op];
|
|
613
667
|
if (op === 'set') {
|
|
614
|
-
params.push(opValue);
|
|
668
|
+
params.push(coerceWriteValue(qi, key, opValue));
|
|
615
669
|
return `${col} = ${qi.p(params.length)}${cast}`;
|
|
616
670
|
}
|
|
617
671
|
// Arithmetic operators: must be finite numbers
|
|
@@ -636,12 +690,44 @@ function buildSetClause(qi, key, value, params) {
|
|
|
636
690
|
}
|
|
637
691
|
}
|
|
638
692
|
// Fall through: multi-key objects or non-operator single-key objects
|
|
639
|
-
// are treated as plain values (e.g., JSONB column payloads)
|
|
693
|
+
// are treated as plain values (e.g., JSONB column payloads), unless the
|
|
694
|
+
// shape can only be a misspelled atomic operator (see below).
|
|
695
|
+
assertBindableSetValue(qi, key, value);
|
|
640
696
|
}
|
|
641
697
|
// Plain value (including null, Date, Buffer, arrays, JSON objects)
|
|
642
|
-
params.push(value);
|
|
698
|
+
params.push(coerceWriteValue(qi, key, value));
|
|
643
699
|
return `${col} = ${qi.p(params.length)}${cast}`;
|
|
644
700
|
}
|
|
701
|
+
/**
|
|
702
|
+
* Reject a `data` value that reached {@link buildSetClause}'s plain-value
|
|
703
|
+
* fallthrough as a plain object with exactly ONE key that is not a known
|
|
704
|
+
* atomic operator. On a scalar column that shape can only be a misspelled
|
|
705
|
+
* operator (`{ incremnt: 1 }`, `{ bogus: 'x' }`); binding it plainly writes the
|
|
706
|
+
* JSON text `{"bogus":"x"}` into the column and silently corrupts it.
|
|
707
|
+
*
|
|
708
|
+
* Deliberately narrow. Skipped for:
|
|
709
|
+
* - json / jsonb columns, which legitimately take arbitrary objects;
|
|
710
|
+
* - class instances (Buffer for bytea, Decimal wrappers, …), arrays and Dates,
|
|
711
|
+
* which {@link isUnmatchedPlainObject} already excludes;
|
|
712
|
+
* - multi-key objects, which are never operator-shaped (a real operator object
|
|
713
|
+
* has exactly one key) and are far more likely a deliberate payload.
|
|
714
|
+
*
|
|
715
|
+
* Shared by the SQL-build path and the cache-hit param-collect path, so a
|
|
716
|
+
* warmed template can never skip the check.
|
|
717
|
+
*/
|
|
718
|
+
function assertBindableSetValue(qi, key, value) {
|
|
719
|
+
if (!(0, filters_js_1.isUnmatchedPlainObject)(value))
|
|
720
|
+
return;
|
|
721
|
+
const keys = Object.keys(value);
|
|
722
|
+
if (keys.length !== 1 || filters_js_1.UPDATE_OPERATOR_KEYS.has(keys[0]))
|
|
723
|
+
return;
|
|
724
|
+
const column = qi.toColumn(key);
|
|
725
|
+
if (whereMod.isJsonColumnType(qi, whereMod.getColumnPgType(qi, column)))
|
|
726
|
+
return;
|
|
727
|
+
throw new errors_js_1.ValidationError(`[turbine] Unknown update operator "${keys[0]}" on "${qi.table}.${key}". ` +
|
|
728
|
+
`Supported: ${[...filters_js_1.UPDATE_OPERATOR_KEYS].join(', ')}. ` +
|
|
729
|
+
'A plain object is only written as a value on a json/jsonb column.');
|
|
730
|
+
}
|
|
645
731
|
/**
|
|
646
732
|
* Fingerprint SET clauses for update/updateMany.
|
|
647
733
|
* Captures key names + operator types (set/increment/etc) but not values.
|
|
@@ -668,9 +754,9 @@ function fingerprintSet(_qi, data) {
|
|
|
668
754
|
/**
|
|
669
755
|
* Collect SET params for update/updateMany. Mirrors buildSetClause param order.
|
|
670
756
|
*/
|
|
671
|
-
function collectSetParams(
|
|
757
|
+
function collectSetParams(qi, data, params) {
|
|
672
758
|
const entries = Object.entries(data).filter(([, v]) => v !== undefined);
|
|
673
|
-
for (const [, v] of entries) {
|
|
759
|
+
for (const [k, v] of entries) {
|
|
674
760
|
if (v !== null &&
|
|
675
761
|
typeof v === 'object' &&
|
|
676
762
|
!Array.isArray(v) &&
|
|
@@ -679,10 +765,17 @@ function collectSetParams(_qi, data, params) {
|
|
|
679
765
|
const obj = v;
|
|
680
766
|
const keys = Object.keys(obj);
|
|
681
767
|
if (keys.length === 1 && filters_js_1.UPDATE_OPERATOR_KEYS.has(keys[0])) {
|
|
682
|
-
|
|
768
|
+
// `set` is the only operator that binds a user value of arbitrary type;
|
|
769
|
+
// the arithmetic operators are finite numbers, which coerceWriteValue
|
|
770
|
+
// returns untouched. Mirrors buildSetClause.
|
|
771
|
+
params.push(coerceWriteValue(qi, k, obj[keys[0]]));
|
|
683
772
|
continue;
|
|
684
773
|
}
|
|
774
|
+
// Mirror the build path's misspelled-operator guard: the SET fingerprint
|
|
775
|
+
// is `key:eq` for both a plain value and a bogus operator object, so a
|
|
776
|
+
// cache HIT would otherwise bind the object and skip the check.
|
|
777
|
+
assertBindableSetValue(qi, k, v);
|
|
685
778
|
}
|
|
686
|
-
params.push(v);
|
|
779
|
+
params.push(coerceWriteValue(qi, k, v));
|
|
687
780
|
}
|
|
688
781
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* turbine-orm — LISTEN/NOTIFY realtime pub/sub
|
|
3
|
+
*
|
|
4
|
+
* Postgres LISTEN/NOTIFY is a first-class realtime primitive that neither
|
|
5
|
+
* Prisma nor Drizzle expose ergonomically. This module backs the thin
|
|
6
|
+
* `$listen` / `$notify` methods on TurbineClient.
|
|
7
|
+
*
|
|
8
|
+
* Design — **one dedicated connection per subscription**:
|
|
9
|
+
*
|
|
10
|
+
* Each `$listen(channel, handler)` acquires its OWN long-lived client from
|
|
11
|
+
* the pool, runs `LISTEN "chan"`, and keeps that connection checked out for
|
|
12
|
+
* the life of the subscription. This is the simplest correct model: each
|
|
13
|
+
* subscription owns its lifecycle, `unsubscribe()` cleanly UNLISTENs and
|
|
14
|
+
* releases exactly one connection, and there is no shared multiplexing
|
|
15
|
+
* state to reason about. The trade-off is one pool slot per active channel
|
|
16
|
+
* — for the handful of channels a typical app listens on, that's a fine
|
|
17
|
+
* price for clarity. (A future optimization could multiplex many channels
|
|
18
|
+
* over a single shared notification connection.)
|
|
19
|
+
*
|
|
20
|
+
* Serverless / HTTP-pool caveat:
|
|
21
|
+
*
|
|
22
|
+
* LISTEN requires a *persistent* TCP connection that can push asynchronous
|
|
23
|
+
* notification messages back to the client. Stateless HTTP drivers
|
|
24
|
+
* (Neon HTTP, Vercel Postgres over fetch) cannot hold such a connection, so
|
|
25
|
+
* `$listen` will surface a clear error rather than hang. `$notify` works
|
|
26
|
+
* everywhere — it's a single round-trip `SELECT pg_notify(...)`.
|
|
27
|
+
*/
|
|
28
|
+
import type { PgCompatPool } from './client.js';
|
|
29
|
+
/**
|
|
30
|
+
* Validate a LISTEN/NOTIFY channel name. Throws ValidationError on anything
|
|
31
|
+
* that isn't a plain, reasonable-length SQL identifier. This is enforced for
|
|
32
|
+
* BOTH `$listen` (where the channel is interpolated) and `$notify` (where the
|
|
33
|
+
* channel is a bound param) — defensive parity, and it catches user typos
|
|
34
|
+
* loudly.
|
|
35
|
+
*/
|
|
36
|
+
export declare function validateChannel(channel: string): void;
|
|
37
|
+
/** Handler invoked with the raw NOTIFY payload string (empty string if none). */
|
|
38
|
+
export type NotificationHandler = (payload: string) => void;
|
|
39
|
+
/**
|
|
40
|
+
* A live LISTEN subscription. Call `unsubscribe()` to UNLISTEN, detach the
|
|
41
|
+
* handler, and release the dedicated connection back to the pool.
|
|
42
|
+
*/
|
|
43
|
+
export interface Subscription {
|
|
44
|
+
/** The channel this subscription is listening on. */
|
|
45
|
+
readonly channel: string;
|
|
46
|
+
/**
|
|
47
|
+
* Stop listening: runs `UNLISTEN "chan"`, removes the notification listener,
|
|
48
|
+
* and releases the dedicated connection. Idempotent — safe to call twice.
|
|
49
|
+
*/
|
|
50
|
+
unsubscribe(): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Internal registry handle so TurbineClient can track and tear down active
|
|
54
|
+
* subscriptions on `disconnect()`.
|
|
55
|
+
*/
|
|
56
|
+
export interface ActiveSubscription extends Subscription {
|
|
57
|
+
/** Tear down WITHOUT issuing UNLISTEN (used when the pool is being ended). */
|
|
58
|
+
_forceRelease(): void;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Acquire a dedicated connection, run `LISTEN "channel"`, and wire the handler.
|
|
62
|
+
*
|
|
63
|
+
* @param pool the pg-compatible pool to check a long-lived client out of
|
|
64
|
+
* @param channel channel name — MUST already be validated by the caller
|
|
65
|
+
* @param quotedChannel the channel run through quoteIdent (interpolated into SQL)
|
|
66
|
+
* @param handler called with each notification's payload
|
|
67
|
+
* @param onClosed invoked when the subscription releases, so the client can
|
|
68
|
+
* drop it from its active-subscription registry
|
|
69
|
+
*/
|
|
70
|
+
export declare function createSubscription(pool: PgCompatPool, channel: string, quotedChannel: string, handler: NotificationHandler, onClosed: (sub: ActiveSubscription) => void): Promise<ActiveSubscription>;
|