turbine-orm 0.42.0 → 0.43.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/cjs/cli/prisma-resolve.js +46 -9
- package/dist/cjs/client.js +6 -6
- package/dist/cjs/prisma-compat.js +32 -6
- package/dist/cjs/query/where.js +38 -1
- package/dist/cli/prisma-resolve.js +46 -9
- package/dist/client.d.ts +6 -0
- package/dist/client.js +6 -6
- package/dist/prisma-compat.js +32 -6
- package/dist/query/where.js +39 -2
- package/package.json +1 -1
|
@@ -236,6 +236,21 @@ function relationFkColumns(model, field) {
|
|
|
236
236
|
const fieldsArg = relAttr?.args.find((a) => a.key === 'fields' && a.kind === 'array');
|
|
237
237
|
return fieldsArg?.items?.map((pf) => fieldColumn(model, pf)) ?? null;
|
|
238
238
|
}
|
|
239
|
+
/**
|
|
240
|
+
* FK columns for an INVERSE relation field (one carrying no `fields: [...]`),
|
|
241
|
+
* derived by @relation("Name") pairing: the opposing model's same-named field
|
|
242
|
+
* that owns the FK pins the columns. Returns undefined when there is no
|
|
243
|
+
* relation name or no named counterpart. This is how Prisma disambiguates two
|
|
244
|
+
* or more relations to the same target model.
|
|
245
|
+
*/
|
|
246
|
+
function pairedInverseFkColumns(model, field, targetModelName, modelsByName) {
|
|
247
|
+
const relName = relationNameOf(field);
|
|
248
|
+
const targetModel = modelsByName.get(targetModelName);
|
|
249
|
+
if (!relName || !targetModel)
|
|
250
|
+
return null;
|
|
251
|
+
const opposing = targetModel.fields.find((f) => f.type === model.name && relationNameOf(f) === relName && (relationFkColumns(targetModel, f)?.length ?? 0) > 0);
|
|
252
|
+
return opposing ? relationFkColumns(targetModel, opposing) : null;
|
|
253
|
+
}
|
|
239
254
|
function resolveRelation(model, fieldName, targetModelName, isList, modelTable, modelsByName, schema, tableMeta, noDb) {
|
|
240
255
|
const cardinality = isList ? 'many' : 'one';
|
|
241
256
|
const base = {
|
|
@@ -261,15 +276,7 @@ function resolveRelation(model, fieldName, targetModelName, isList, modelTable,
|
|
|
261
276
|
// fall back to the ambiguity handling below when there is no relation name or
|
|
262
277
|
// the named pair cannot be found.
|
|
263
278
|
if (!fkColumns || fkColumns.length === 0) {
|
|
264
|
-
|
|
265
|
-
const targetModel = modelsByName.get(targetModelName);
|
|
266
|
-
if (relName && targetModel) {
|
|
267
|
-
const opposing = targetModel.fields.find((f) => f.type === model.name &&
|
|
268
|
-
relationNameOf(f) === relName &&
|
|
269
|
-
(relationFkColumns(targetModel, f)?.length ?? 0) > 0);
|
|
270
|
-
if (opposing)
|
|
271
|
-
fkColumns = relationFkColumns(targetModel, opposing);
|
|
272
|
-
}
|
|
279
|
+
fkColumns = pairedInverseFkColumns(model, field, targetModelName, modelsByName);
|
|
273
280
|
}
|
|
274
281
|
const candidates = Object.values(tableMeta.relations).filter((def) => {
|
|
275
282
|
if (targetTable && def.to !== targetTable)
|
|
@@ -299,6 +306,36 @@ function resolveRelation(model, fieldName, targetModelName, isList, modelTable,
|
|
|
299
306
|
};
|
|
300
307
|
}
|
|
301
308
|
if (picked.length > 1) {
|
|
309
|
+
// Elimination pass: an UNNAMED pair can still resolve when every sibling
|
|
310
|
+
// relation field to the same target pins a different candidate (via its own
|
|
311
|
+
// `fields: [...]` or @relation("Name") pairing). Subtract those consumed
|
|
312
|
+
// candidates; exactly one survivor means the unnamed pair is unambiguous by
|
|
313
|
+
// elimination, which is how Prisma itself resolves it.
|
|
314
|
+
const consumed = new Set();
|
|
315
|
+
for (const sibling of model.fields) {
|
|
316
|
+
if (sibling.name === fieldName || sibling.type !== targetModelName)
|
|
317
|
+
continue;
|
|
318
|
+
const sibFks = relationFkColumns(model, sibling) ?? pairedInverseFkColumns(model, sibling, targetModelName, modelsByName);
|
|
319
|
+
if (!sibFks || sibFks.length === 0)
|
|
320
|
+
continue;
|
|
321
|
+
const sibWant = [...sibFks].sort().join(',');
|
|
322
|
+
for (const def of candidates) {
|
|
323
|
+
const fk = Array.isArray(def.foreignKey) ? def.foreignKey : [def.foreignKey];
|
|
324
|
+
if ([...fk].sort().join(',') === sibWant)
|
|
325
|
+
consumed.add(def.name);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
const surviving = picked.filter((def) => !consumed.has(def.name));
|
|
329
|
+
if (surviving.length === 1) {
|
|
330
|
+
const def = surviving[0];
|
|
331
|
+
return {
|
|
332
|
+
...base,
|
|
333
|
+
turbineName: def.name,
|
|
334
|
+
cardinality,
|
|
335
|
+
junction: def.type === 'manyToMany' ? def.through?.table : undefined,
|
|
336
|
+
status: 'resolved',
|
|
337
|
+
};
|
|
338
|
+
}
|
|
302
339
|
return {
|
|
303
340
|
...base,
|
|
304
341
|
reason: `ambiguous - ${picked.length} candidate relations match (${picked.map((d) => d.name).join(', ')})`,
|
package/dist/cjs/client.js
CHANGED
|
@@ -412,9 +412,9 @@ class TurbineClient {
|
|
|
412
412
|
}
|
|
413
413
|
else {
|
|
414
414
|
const poolConfig = {
|
|
415
|
-
max: config.poolSize ?? 10,
|
|
416
|
-
idleTimeoutMillis: config.idleTimeoutMs ?? 30_000,
|
|
417
|
-
connectionTimeoutMillis: config.connectionTimeoutMs ?? 5_000,
|
|
415
|
+
max: config.poolSize ?? config.max ?? 10,
|
|
416
|
+
idleTimeoutMillis: config.idleTimeoutMs ?? config.idleTimeoutMillis ?? 30_000,
|
|
417
|
+
connectionTimeoutMillis: config.connectionTimeoutMs ?? config.connectionTimeoutMillis ?? 5_000,
|
|
418
418
|
};
|
|
419
419
|
// Did the caller supply ANY explicit connection target? If not, and a
|
|
420
420
|
// DATABASE_URL is present in the environment, fall back to it so
|
|
@@ -463,9 +463,9 @@ class TurbineClient {
|
|
|
463
463
|
if (typeof replica === 'string') {
|
|
464
464
|
const replicaPool = new pg_1.default.Pool({
|
|
465
465
|
connectionString: replica,
|
|
466
|
-
max: config.poolSize ?? 10,
|
|
467
|
-
idleTimeoutMillis: config.idleTimeoutMs ?? 30_000,
|
|
468
|
-
connectionTimeoutMillis: config.connectionTimeoutMs ?? 5_000,
|
|
466
|
+
max: config.poolSize ?? config.max ?? 10,
|
|
467
|
+
idleTimeoutMillis: config.idleTimeoutMs ?? config.idleTimeoutMillis ?? 30_000,
|
|
468
|
+
connectionTimeoutMillis: config.connectionTimeoutMs ?? config.connectionTimeoutMillis ?? 5_000,
|
|
469
469
|
...(config.ssl !== undefined ? { ssl: config.ssl } : {}),
|
|
470
470
|
});
|
|
471
471
|
replicaPool.on('error', (err) => {
|
|
@@ -151,7 +151,16 @@ function decorate(err, prismaErrorCodes) {
|
|
|
151
151
|
}
|
|
152
152
|
return err;
|
|
153
153
|
}
|
|
154
|
-
|
|
154
|
+
const TIME_PG_TYPES = new Set(['time', 'time without time zone', 'timetz', 'time with time zone']);
|
|
155
|
+
/** `HH:MM:SS(.fff)?` (a pg `time` wire value) → Prisma's epoch-day `Date`. */
|
|
156
|
+
function timeStringToDate(v) {
|
|
157
|
+
const m = v.match(/^(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?$/);
|
|
158
|
+
if (!m)
|
|
159
|
+
return null;
|
|
160
|
+
const ms = m[4] ? Math.round(Number(`0.${m[4]}`) * 1000) : 0;
|
|
161
|
+
return new Date(Date.UTC(1970, 0, 1, Number(m[1]), Number(m[2]), Number(m[3]), ms));
|
|
162
|
+
}
|
|
163
|
+
function buildLookups(ctx, mm) {
|
|
155
164
|
const reverseFields = {};
|
|
156
165
|
let identityFields = true;
|
|
157
166
|
for (const [prismaField, turbineField] of Object.entries(mm.fields)) {
|
|
@@ -163,12 +172,17 @@ function buildLookups(mm) {
|
|
|
163
172
|
for (const [prismaRel, rel] of Object.entries(mm.relations)) {
|
|
164
173
|
reverseRelations[rel.name] = { prismaName: prismaRel, cardinality: rel.cardinality };
|
|
165
174
|
}
|
|
166
|
-
|
|
175
|
+
const timeFields = new Set();
|
|
176
|
+
for (const col of ctx.schema.tables[mm.table]?.columns ?? []) {
|
|
177
|
+
if (TIME_PG_TYPES.has(col.pgType))
|
|
178
|
+
timeFields.add(col.field);
|
|
179
|
+
}
|
|
180
|
+
return { reverseFields, identityFields, reverseRelations, timeFields };
|
|
167
181
|
}
|
|
168
182
|
function lookupsFor(ctx, mm) {
|
|
169
183
|
let l = ctx.lookups.get(mm.table);
|
|
170
184
|
if (!l) {
|
|
171
|
-
l = buildLookups(mm);
|
|
185
|
+
l = buildLookups(ctx, mm);
|
|
172
186
|
ctx.lookups.set(mm.table, l);
|
|
173
187
|
}
|
|
174
188
|
return l;
|
|
@@ -724,7 +738,11 @@ function reshapeRow(ctx, mm, row) {
|
|
|
724
738
|
out[rel.prismaName] = rv;
|
|
725
739
|
continue;
|
|
726
740
|
}
|
|
727
|
-
|
|
741
|
+
let sv = val;
|
|
742
|
+
if (typeof sv === 'string' && l.timeFields.has(key)) {
|
|
743
|
+
sv = timeStringToDate(sv) ?? sv;
|
|
744
|
+
}
|
|
745
|
+
out[l.identityFields ? key : (l.reverseFields[key] ?? key)] = sv;
|
|
728
746
|
}
|
|
729
747
|
return out;
|
|
730
748
|
}
|
|
@@ -756,7 +774,11 @@ function reshapeAggregate(ctx, mm, res) {
|
|
|
756
774
|
out[key] = reshapeAggFieldBlock(l, val, false);
|
|
757
775
|
continue;
|
|
758
776
|
}
|
|
759
|
-
|
|
777
|
+
let sv = val;
|
|
778
|
+
if (typeof sv === 'string' && l.timeFields.has(key)) {
|
|
779
|
+
sv = timeStringToDate(sv) ?? sv;
|
|
780
|
+
}
|
|
781
|
+
out[l.identityFields ? key : (l.reverseFields[key] ?? key)] = sv;
|
|
760
782
|
}
|
|
761
783
|
return out;
|
|
762
784
|
}
|
|
@@ -788,7 +810,11 @@ function reshapeGroupRow(ctx, mm, row) {
|
|
|
788
810
|
out[key] = reshapeAggFieldBlock(l, val, false);
|
|
789
811
|
continue;
|
|
790
812
|
}
|
|
791
|
-
|
|
813
|
+
let sv = val;
|
|
814
|
+
if (typeof sv === 'string' && l.timeFields.has(key)) {
|
|
815
|
+
sv = timeStringToDate(sv) ?? sv;
|
|
816
|
+
}
|
|
817
|
+
out[l.identityFields ? key : (l.reverseFields[key] ?? key)] = sv;
|
|
792
818
|
}
|
|
793
819
|
return out;
|
|
794
820
|
}
|
package/dist/cjs/query/where.js
CHANGED
|
@@ -868,7 +868,44 @@ function buildRelationFilter(qi, _relName, relDef, filterObj, params, parentTabl
|
|
|
868
868
|
const clauses = [];
|
|
869
869
|
// Correlation: link child table to parent table (supports composite FKs)
|
|
870
870
|
let correlation;
|
|
871
|
-
if (relDef.type === '
|
|
871
|
+
if (relDef.type === 'manyToMany') {
|
|
872
|
+
// The target row is related iff a junction row links it to the parent.
|
|
873
|
+
// Direct FK correlation (the other branches) would compile target.pk =
|
|
874
|
+
// parent.pk and silently match nothing, so route through the junction:
|
|
875
|
+
// EXISTS (SELECT 1 FROM junction
|
|
876
|
+
// WHERE junction.targetKey = target.pk AND junction.sourceKey = parent.ref)
|
|
877
|
+
// All bare table names (no aliases), so the scoped sub-where machinery and
|
|
878
|
+
// nested relation filters inside the branch keep their qualification. The
|
|
879
|
+
// fragment binds no params, so collectRelationFilterParams needs no mirror.
|
|
880
|
+
if (!relDef.through) {
|
|
881
|
+
throw new errors_js_1.ValidationError(`[turbine] manyToMany relation "${relDef.name}" is missing a \`through\` junction descriptor.`);
|
|
882
|
+
}
|
|
883
|
+
const qJunction = qi.q(relDef.through.table);
|
|
884
|
+
const targetKeys = (0, schema_js_1.normalizeKeyColumns)(relDef.through.targetKey);
|
|
885
|
+
const targetPk = targetMeta.primaryKey;
|
|
886
|
+
if (targetPk.length === 0) {
|
|
887
|
+
throw new errors_js_1.ValidationError(`[turbine] manyToMany relation "${relDef.name}" targets table "${targetTable}" which has no primary key; ` +
|
|
888
|
+
`cannot correlate the relation filter through the junction.`);
|
|
889
|
+
}
|
|
890
|
+
if (targetKeys.length !== targetPk.length) {
|
|
891
|
+
throw new errors_js_1.ValidationError(`[turbine] manyToMany relation "${relDef.name}": through.targetKey has ${targetKeys.length} column(s) ` +
|
|
892
|
+
`but target "${targetTable}" primary key has ${targetPk.length}. Composite keys must pair positionally.`);
|
|
893
|
+
}
|
|
894
|
+
const sourceKeys = (0, schema_js_1.normalizeKeyColumns)(relDef.through.sourceKey);
|
|
895
|
+
const refKeys = (0, schema_js_1.normalizeKeyColumns)(relDef.referenceKey);
|
|
896
|
+
if (sourceKeys.length !== refKeys.length) {
|
|
897
|
+
throw new errors_js_1.ValidationError(`[turbine] manyToMany relation "${relDef.name}": through.sourceKey has ${sourceKeys.length} column(s) ` +
|
|
898
|
+
`but referenceKey has ${refKeys.length}. Composite keys must pair positionally.`);
|
|
899
|
+
}
|
|
900
|
+
const targetLink = targetKeys
|
|
901
|
+
.map((jcol, i) => `${qJunction}.${qi.q(jcol)} = ${qt}.${qi.q(targetPk[i])}`)
|
|
902
|
+
.join(' AND ');
|
|
903
|
+
const parentLink = sourceKeys
|
|
904
|
+
.map((jcol, i) => `${qJunction}.${qi.q(jcol)} = ${qSelf}.${qi.q(refKeys[i])}`)
|
|
905
|
+
.join(' AND ');
|
|
906
|
+
correlation = `EXISTS (SELECT 1 FROM ${qJunction} WHERE ${targetLink} AND ${parentLink})`;
|
|
907
|
+
}
|
|
908
|
+
else if (relDef.type === 'hasMany' || relDef.type === 'hasOne') {
|
|
872
909
|
// parent.pk = child.fk
|
|
873
910
|
correlation = qi.dialect.buildCorrelation(qt, relDef.foreignKey, qSelf, relDef.referenceKey);
|
|
874
911
|
}
|
|
@@ -231,6 +231,21 @@ function relationFkColumns(model, field) {
|
|
|
231
231
|
const fieldsArg = relAttr?.args.find((a) => a.key === 'fields' && a.kind === 'array');
|
|
232
232
|
return fieldsArg?.items?.map((pf) => fieldColumn(model, pf)) ?? null;
|
|
233
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* FK columns for an INVERSE relation field (one carrying no `fields: [...]`),
|
|
236
|
+
* derived by @relation("Name") pairing: the opposing model's same-named field
|
|
237
|
+
* that owns the FK pins the columns. Returns undefined when there is no
|
|
238
|
+
* relation name or no named counterpart. This is how Prisma disambiguates two
|
|
239
|
+
* or more relations to the same target model.
|
|
240
|
+
*/
|
|
241
|
+
function pairedInverseFkColumns(model, field, targetModelName, modelsByName) {
|
|
242
|
+
const relName = relationNameOf(field);
|
|
243
|
+
const targetModel = modelsByName.get(targetModelName);
|
|
244
|
+
if (!relName || !targetModel)
|
|
245
|
+
return null;
|
|
246
|
+
const opposing = targetModel.fields.find((f) => f.type === model.name && relationNameOf(f) === relName && (relationFkColumns(targetModel, f)?.length ?? 0) > 0);
|
|
247
|
+
return opposing ? relationFkColumns(targetModel, opposing) : null;
|
|
248
|
+
}
|
|
234
249
|
function resolveRelation(model, fieldName, targetModelName, isList, modelTable, modelsByName, schema, tableMeta, noDb) {
|
|
235
250
|
const cardinality = isList ? 'many' : 'one';
|
|
236
251
|
const base = {
|
|
@@ -256,15 +271,7 @@ function resolveRelation(model, fieldName, targetModelName, isList, modelTable,
|
|
|
256
271
|
// fall back to the ambiguity handling below when there is no relation name or
|
|
257
272
|
// the named pair cannot be found.
|
|
258
273
|
if (!fkColumns || fkColumns.length === 0) {
|
|
259
|
-
|
|
260
|
-
const targetModel = modelsByName.get(targetModelName);
|
|
261
|
-
if (relName && targetModel) {
|
|
262
|
-
const opposing = targetModel.fields.find((f) => f.type === model.name &&
|
|
263
|
-
relationNameOf(f) === relName &&
|
|
264
|
-
(relationFkColumns(targetModel, f)?.length ?? 0) > 0);
|
|
265
|
-
if (opposing)
|
|
266
|
-
fkColumns = relationFkColumns(targetModel, opposing);
|
|
267
|
-
}
|
|
274
|
+
fkColumns = pairedInverseFkColumns(model, field, targetModelName, modelsByName);
|
|
268
275
|
}
|
|
269
276
|
const candidates = Object.values(tableMeta.relations).filter((def) => {
|
|
270
277
|
if (targetTable && def.to !== targetTable)
|
|
@@ -294,6 +301,36 @@ function resolveRelation(model, fieldName, targetModelName, isList, modelTable,
|
|
|
294
301
|
};
|
|
295
302
|
}
|
|
296
303
|
if (picked.length > 1) {
|
|
304
|
+
// Elimination pass: an UNNAMED pair can still resolve when every sibling
|
|
305
|
+
// relation field to the same target pins a different candidate (via its own
|
|
306
|
+
// `fields: [...]` or @relation("Name") pairing). Subtract those consumed
|
|
307
|
+
// candidates; exactly one survivor means the unnamed pair is unambiguous by
|
|
308
|
+
// elimination, which is how Prisma itself resolves it.
|
|
309
|
+
const consumed = new Set();
|
|
310
|
+
for (const sibling of model.fields) {
|
|
311
|
+
if (sibling.name === fieldName || sibling.type !== targetModelName)
|
|
312
|
+
continue;
|
|
313
|
+
const sibFks = relationFkColumns(model, sibling) ?? pairedInverseFkColumns(model, sibling, targetModelName, modelsByName);
|
|
314
|
+
if (!sibFks || sibFks.length === 0)
|
|
315
|
+
continue;
|
|
316
|
+
const sibWant = [...sibFks].sort().join(',');
|
|
317
|
+
for (const def of candidates) {
|
|
318
|
+
const fk = Array.isArray(def.foreignKey) ? def.foreignKey : [def.foreignKey];
|
|
319
|
+
if ([...fk].sort().join(',') === sibWant)
|
|
320
|
+
consumed.add(def.name);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
const surviving = picked.filter((def) => !consumed.has(def.name));
|
|
324
|
+
if (surviving.length === 1) {
|
|
325
|
+
const def = surviving[0];
|
|
326
|
+
return {
|
|
327
|
+
...base,
|
|
328
|
+
turbineName: def.name,
|
|
329
|
+
cardinality,
|
|
330
|
+
junction: def.type === 'manyToMany' ? def.through?.table : undefined,
|
|
331
|
+
status: 'resolved',
|
|
332
|
+
};
|
|
333
|
+
}
|
|
297
334
|
return {
|
|
298
335
|
...base,
|
|
299
336
|
reason: `ambiguous - ${picked.length} candidate relations match (${picked.map((d) => d.name).join(', ')})`,
|
package/dist/client.d.ts
CHANGED
|
@@ -158,6 +158,12 @@ export interface TurbineConfig {
|
|
|
158
158
|
idleTimeoutMs?: number;
|
|
159
159
|
/** Connection timeout in ms (default: 5000) */
|
|
160
160
|
connectionTimeoutMs?: number;
|
|
161
|
+
/** pg-style alias for {@link poolSize}; the explicit field wins when both are set. */
|
|
162
|
+
max?: number;
|
|
163
|
+
/** pg-style alias for {@link idleTimeoutMs}; the explicit field wins when both are set. */
|
|
164
|
+
idleTimeoutMillis?: number;
|
|
165
|
+
/** pg-style alias for {@link connectionTimeoutMs}; the explicit field wins when both are set. */
|
|
166
|
+
connectionTimeoutMillis?: number;
|
|
161
167
|
/** Enable query logging to console (default: false) */
|
|
162
168
|
logging?: boolean;
|
|
163
169
|
/** Default LIMIT applied to findMany() when no limit is specified (opt-in, default: undefined) */
|
package/dist/client.js
CHANGED
|
@@ -404,9 +404,9 @@ export class TurbineClient {
|
|
|
404
404
|
}
|
|
405
405
|
else {
|
|
406
406
|
const poolConfig = {
|
|
407
|
-
max: config.poolSize ?? 10,
|
|
408
|
-
idleTimeoutMillis: config.idleTimeoutMs ?? 30_000,
|
|
409
|
-
connectionTimeoutMillis: config.connectionTimeoutMs ?? 5_000,
|
|
407
|
+
max: config.poolSize ?? config.max ?? 10,
|
|
408
|
+
idleTimeoutMillis: config.idleTimeoutMs ?? config.idleTimeoutMillis ?? 30_000,
|
|
409
|
+
connectionTimeoutMillis: config.connectionTimeoutMs ?? config.connectionTimeoutMillis ?? 5_000,
|
|
410
410
|
};
|
|
411
411
|
// Did the caller supply ANY explicit connection target? If not, and a
|
|
412
412
|
// DATABASE_URL is present in the environment, fall back to it so
|
|
@@ -455,9 +455,9 @@ export class TurbineClient {
|
|
|
455
455
|
if (typeof replica === 'string') {
|
|
456
456
|
const replicaPool = new pg.Pool({
|
|
457
457
|
connectionString: replica,
|
|
458
|
-
max: config.poolSize ?? 10,
|
|
459
|
-
idleTimeoutMillis: config.idleTimeoutMs ?? 30_000,
|
|
460
|
-
connectionTimeoutMillis: config.connectionTimeoutMs ?? 5_000,
|
|
458
|
+
max: config.poolSize ?? config.max ?? 10,
|
|
459
|
+
idleTimeoutMillis: config.idleTimeoutMs ?? config.idleTimeoutMillis ?? 30_000,
|
|
460
|
+
connectionTimeoutMillis: config.connectionTimeoutMs ?? config.connectionTimeoutMillis ?? 5_000,
|
|
461
461
|
...(config.ssl !== undefined ? { ssl: config.ssl } : {}),
|
|
462
462
|
});
|
|
463
463
|
replicaPool.on('error', (err) => {
|
package/dist/prisma-compat.js
CHANGED
|
@@ -147,7 +147,16 @@ function decorate(err, prismaErrorCodes) {
|
|
|
147
147
|
}
|
|
148
148
|
return err;
|
|
149
149
|
}
|
|
150
|
-
|
|
150
|
+
const TIME_PG_TYPES = new Set(['time', 'time without time zone', 'timetz', 'time with time zone']);
|
|
151
|
+
/** `HH:MM:SS(.fff)?` (a pg `time` wire value) → Prisma's epoch-day `Date`. */
|
|
152
|
+
function timeStringToDate(v) {
|
|
153
|
+
const m = v.match(/^(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,6}))?$/);
|
|
154
|
+
if (!m)
|
|
155
|
+
return null;
|
|
156
|
+
const ms = m[4] ? Math.round(Number(`0.${m[4]}`) * 1000) : 0;
|
|
157
|
+
return new Date(Date.UTC(1970, 0, 1, Number(m[1]), Number(m[2]), Number(m[3]), ms));
|
|
158
|
+
}
|
|
159
|
+
function buildLookups(ctx, mm) {
|
|
151
160
|
const reverseFields = {};
|
|
152
161
|
let identityFields = true;
|
|
153
162
|
for (const [prismaField, turbineField] of Object.entries(mm.fields)) {
|
|
@@ -159,12 +168,17 @@ function buildLookups(mm) {
|
|
|
159
168
|
for (const [prismaRel, rel] of Object.entries(mm.relations)) {
|
|
160
169
|
reverseRelations[rel.name] = { prismaName: prismaRel, cardinality: rel.cardinality };
|
|
161
170
|
}
|
|
162
|
-
|
|
171
|
+
const timeFields = new Set();
|
|
172
|
+
for (const col of ctx.schema.tables[mm.table]?.columns ?? []) {
|
|
173
|
+
if (TIME_PG_TYPES.has(col.pgType))
|
|
174
|
+
timeFields.add(col.field);
|
|
175
|
+
}
|
|
176
|
+
return { reverseFields, identityFields, reverseRelations, timeFields };
|
|
163
177
|
}
|
|
164
178
|
function lookupsFor(ctx, mm) {
|
|
165
179
|
let l = ctx.lookups.get(mm.table);
|
|
166
180
|
if (!l) {
|
|
167
|
-
l = buildLookups(mm);
|
|
181
|
+
l = buildLookups(ctx, mm);
|
|
168
182
|
ctx.lookups.set(mm.table, l);
|
|
169
183
|
}
|
|
170
184
|
return l;
|
|
@@ -720,7 +734,11 @@ function reshapeRow(ctx, mm, row) {
|
|
|
720
734
|
out[rel.prismaName] = rv;
|
|
721
735
|
continue;
|
|
722
736
|
}
|
|
723
|
-
|
|
737
|
+
let sv = val;
|
|
738
|
+
if (typeof sv === 'string' && l.timeFields.has(key)) {
|
|
739
|
+
sv = timeStringToDate(sv) ?? sv;
|
|
740
|
+
}
|
|
741
|
+
out[l.identityFields ? key : (l.reverseFields[key] ?? key)] = sv;
|
|
724
742
|
}
|
|
725
743
|
return out;
|
|
726
744
|
}
|
|
@@ -752,7 +770,11 @@ function reshapeAggregate(ctx, mm, res) {
|
|
|
752
770
|
out[key] = reshapeAggFieldBlock(l, val, false);
|
|
753
771
|
continue;
|
|
754
772
|
}
|
|
755
|
-
|
|
773
|
+
let sv = val;
|
|
774
|
+
if (typeof sv === 'string' && l.timeFields.has(key)) {
|
|
775
|
+
sv = timeStringToDate(sv) ?? sv;
|
|
776
|
+
}
|
|
777
|
+
out[l.identityFields ? key : (l.reverseFields[key] ?? key)] = sv;
|
|
756
778
|
}
|
|
757
779
|
return out;
|
|
758
780
|
}
|
|
@@ -784,7 +806,11 @@ function reshapeGroupRow(ctx, mm, row) {
|
|
|
784
806
|
out[key] = reshapeAggFieldBlock(l, val, false);
|
|
785
807
|
continue;
|
|
786
808
|
}
|
|
787
|
-
|
|
809
|
+
let sv = val;
|
|
810
|
+
if (typeof sv === 'string' && l.timeFields.has(key)) {
|
|
811
|
+
sv = timeStringToDate(sv) ?? sv;
|
|
812
|
+
}
|
|
813
|
+
out[l.identityFields ? key : (l.reverseFields[key] ?? key)] = sv;
|
|
788
814
|
}
|
|
789
815
|
return out;
|
|
790
816
|
}
|
package/dist/query/where.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* primitives this module needs. See builder.ts for the thin delegating methods.
|
|
12
12
|
*/
|
|
13
13
|
import { UnsupportedFeatureError, ValidationError } from '../errors.js';
|
|
14
|
-
import { camelToSnake } from '../schema.js';
|
|
14
|
+
import { camelToSnake, normalizeKeyColumns } from '../schema.js';
|
|
15
15
|
import { assertBindableEqualsOperand, findArrayUniqueKey, findJsonUniqueKey, isArrayFilter, isColumnRef, isJsonFilter, isUnmatchedPlainObject, isWhereOperator, JSON_RANGE_OPERATORS, VECTOR_DISTANCE_COMPARATORS, VECTOR_METRIC_OPERATORS, validateTextSearchConfig, } from './filters.js';
|
|
16
16
|
import { escapeLike, OPERATOR_KEYS, ownLookup } from './utils.js';
|
|
17
17
|
import { classifyScalarForSql, fingerprintScalarToken, walkWhere, } from './where-compile.js';
|
|
@@ -808,7 +808,44 @@ export function buildRelationFilter(qi, _relName, relDef, filterObj, params, par
|
|
|
808
808
|
const clauses = [];
|
|
809
809
|
// Correlation: link child table to parent table (supports composite FKs)
|
|
810
810
|
let correlation;
|
|
811
|
-
if (relDef.type === '
|
|
811
|
+
if (relDef.type === 'manyToMany') {
|
|
812
|
+
// The target row is related iff a junction row links it to the parent.
|
|
813
|
+
// Direct FK correlation (the other branches) would compile target.pk =
|
|
814
|
+
// parent.pk and silently match nothing, so route through the junction:
|
|
815
|
+
// EXISTS (SELECT 1 FROM junction
|
|
816
|
+
// WHERE junction.targetKey = target.pk AND junction.sourceKey = parent.ref)
|
|
817
|
+
// All bare table names (no aliases), so the scoped sub-where machinery and
|
|
818
|
+
// nested relation filters inside the branch keep their qualification. The
|
|
819
|
+
// fragment binds no params, so collectRelationFilterParams needs no mirror.
|
|
820
|
+
if (!relDef.through) {
|
|
821
|
+
throw new ValidationError(`[turbine] manyToMany relation "${relDef.name}" is missing a \`through\` junction descriptor.`);
|
|
822
|
+
}
|
|
823
|
+
const qJunction = qi.q(relDef.through.table);
|
|
824
|
+
const targetKeys = normalizeKeyColumns(relDef.through.targetKey);
|
|
825
|
+
const targetPk = targetMeta.primaryKey;
|
|
826
|
+
if (targetPk.length === 0) {
|
|
827
|
+
throw new ValidationError(`[turbine] manyToMany relation "${relDef.name}" targets table "${targetTable}" which has no primary key; ` +
|
|
828
|
+
`cannot correlate the relation filter through the junction.`);
|
|
829
|
+
}
|
|
830
|
+
if (targetKeys.length !== targetPk.length) {
|
|
831
|
+
throw new ValidationError(`[turbine] manyToMany relation "${relDef.name}": through.targetKey has ${targetKeys.length} column(s) ` +
|
|
832
|
+
`but target "${targetTable}" primary key has ${targetPk.length}. Composite keys must pair positionally.`);
|
|
833
|
+
}
|
|
834
|
+
const sourceKeys = normalizeKeyColumns(relDef.through.sourceKey);
|
|
835
|
+
const refKeys = normalizeKeyColumns(relDef.referenceKey);
|
|
836
|
+
if (sourceKeys.length !== refKeys.length) {
|
|
837
|
+
throw new ValidationError(`[turbine] manyToMany relation "${relDef.name}": through.sourceKey has ${sourceKeys.length} column(s) ` +
|
|
838
|
+
`but referenceKey has ${refKeys.length}. Composite keys must pair positionally.`);
|
|
839
|
+
}
|
|
840
|
+
const targetLink = targetKeys
|
|
841
|
+
.map((jcol, i) => `${qJunction}.${qi.q(jcol)} = ${qt}.${qi.q(targetPk[i])}`)
|
|
842
|
+
.join(' AND ');
|
|
843
|
+
const parentLink = sourceKeys
|
|
844
|
+
.map((jcol, i) => `${qJunction}.${qi.q(jcol)} = ${qSelf}.${qi.q(refKeys[i])}`)
|
|
845
|
+
.join(' AND ');
|
|
846
|
+
correlation = `EXISTS (SELECT 1 FROM ${qJunction} WHERE ${targetLink} AND ${parentLink})`;
|
|
847
|
+
}
|
|
848
|
+
else if (relDef.type === 'hasMany' || relDef.type === 'hasOne') {
|
|
812
849
|
// parent.pk = child.fk
|
|
813
850
|
correlation = qi.dialect.buildCorrelation(qt, relDef.foreignKey, qSelf, relDef.referenceKey);
|
|
814
851
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "Postgres-native TypeScript ORM — runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|