turbine-orm 0.43.0 → 0.44.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 +45 -1
- package/dist/cjs/generate.js +3 -0
- package/dist/cjs/prisma-compat.js +193 -13
- package/dist/cli/prisma-resolve.js +45 -1
- package/dist/generate.js +3 -0
- package/dist/prisma-compat.js +193 -13
- package/dist/schema.d.ts +11 -0
- package/package.json +1 -1
|
@@ -153,7 +153,15 @@ function resolvePrismaSchema(ast, schema, options = {}) {
|
|
|
153
153
|
if (c.status === 'resolved' && c.turbineFields)
|
|
154
154
|
compoundUniques[c.selector] = c.turbineFields;
|
|
155
155
|
}
|
|
156
|
-
|
|
156
|
+
const clientDefaults = collectClientDefaults(model, fields, resolvedSchema?.tables[table]);
|
|
157
|
+
result.map.models[model.name] = {
|
|
158
|
+
table,
|
|
159
|
+
accessor,
|
|
160
|
+
fields,
|
|
161
|
+
relations,
|
|
162
|
+
compoundUniques,
|
|
163
|
+
...(clientDefaults ? { clientDefaults } : {}),
|
|
164
|
+
};
|
|
157
165
|
}
|
|
158
166
|
}
|
|
159
167
|
// Enums.
|
|
@@ -236,6 +244,42 @@ function relationFkColumns(model, field) {
|
|
|
236
244
|
const fieldsArg = relAttr?.args.find((a) => a.key === 'fields' && a.kind === 'array');
|
|
237
245
|
return fieldsArg?.items?.map((pf) => fieldColumn(model, pf)) ?? null;
|
|
238
246
|
}
|
|
247
|
+
/**
|
|
248
|
+
* Prisma CLIENT-side defaults for a resolved model: `@default(uuid())` /
|
|
249
|
+
* `@default(cuid())` / `@updatedAt` are filled by the Prisma client (the
|
|
250
|
+
* database column typically has NO default), so migrated call sites omit those
|
|
251
|
+
* fields and a plain insert violates NOT NULL. `@default(now())` is normally a
|
|
252
|
+
* database default, so it is carried only when the introspected column has no
|
|
253
|
+
* default. `@updatedAt` is always carried (Prisma touches it on every update
|
|
254
|
+
* regardless of any database default). Returns undefined when the model has
|
|
255
|
+
* none, keeping the emitted map byte-identical for unaffected schemas.
|
|
256
|
+
*/
|
|
257
|
+
function collectClientDefaults(model, resolvedFields, tableMeta) {
|
|
258
|
+
if (!tableMeta)
|
|
259
|
+
return undefined;
|
|
260
|
+
const out = {};
|
|
261
|
+
for (const field of model.fields) {
|
|
262
|
+
const turbineField = resolvedFields[field.name];
|
|
263
|
+
if (!turbineField)
|
|
264
|
+
continue;
|
|
265
|
+
const col = tableMeta.columns.find((c) => c.field === turbineField || c.name === turbineField);
|
|
266
|
+
if (field.attrs.some((a) => a.name === 'updatedAt')) {
|
|
267
|
+
out[field.name] = 'updatedAt';
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
if (col?.hasDefault)
|
|
271
|
+
continue; // the database fills it; nothing to emulate
|
|
272
|
+
const def = field.attrs.find((a) => a.name === 'default');
|
|
273
|
+
const raw = def?.args.find((a) => a.key === undefined && a.kind === 'raw')?.value ?? '';
|
|
274
|
+
if (/^uuid\s*\(/.test(raw))
|
|
275
|
+
out[field.name] = 'uuid';
|
|
276
|
+
else if (/^cuid\s*\(/.test(raw))
|
|
277
|
+
out[field.name] = 'cuid';
|
|
278
|
+
else if (/^now\s*\(\s*\)$/.test(raw))
|
|
279
|
+
out[field.name] = 'now';
|
|
280
|
+
}
|
|
281
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
282
|
+
}
|
|
239
283
|
/**
|
|
240
284
|
* FK columns for an INVERSE relation field (one carrying no `fields: [...]`),
|
|
241
285
|
* derived by @relation("Name") pairing: the opposing model's same-named field
|
package/dist/cjs/generate.js
CHANGED
|
@@ -956,6 +956,9 @@ function generatePrismaMap(map, options) {
|
|
|
956
956
|
}
|
|
957
957
|
lines.push(' },');
|
|
958
958
|
lines.push(` compoundUniques: ${serializeStringArrayRecord(model.compoundUniques)},`);
|
|
959
|
+
if (model.clientDefaults && Object.keys(model.clientDefaults).length > 0) {
|
|
960
|
+
lines.push(` clientDefaults: ${serializeStringRecord(model.clientDefaults)},`);
|
|
961
|
+
}
|
|
959
962
|
lines.push(' },');
|
|
960
963
|
}
|
|
961
964
|
lines.push(' },');
|
|
@@ -872,7 +872,118 @@ function prismaPropertyAlias(model) {
|
|
|
872
872
|
* QueryInterface for this model's table on the active connection (the base pool,
|
|
873
873
|
* or a transaction's connection inside `$transaction(callback)`).
|
|
874
874
|
*/
|
|
875
|
-
|
|
875
|
+
// ---------------------------------------------------------------------------
|
|
876
|
+
// Prisma client-side defaults (@default(uuid()/cuid()) / @updatedAt / now())
|
|
877
|
+
// ---------------------------------------------------------------------------
|
|
878
|
+
let cuidCounter = Math.floor(Math.random() * 1296);
|
|
879
|
+
/**
|
|
880
|
+
* A cuid-shaped id ('c' + timestamp + counter + fingerprint + random, 25
|
|
881
|
+
* chars): collision-resistant and format-compatible with Prisma's
|
|
882
|
+
* `@default(cuid())` call sites. Not byte-identical to any specific cuid
|
|
883
|
+
* library; Prisma treats these as opaque unique strings.
|
|
884
|
+
*/
|
|
885
|
+
function makeCuid() {
|
|
886
|
+
const ts = Date.now().toString(36);
|
|
887
|
+
const count = (cuidCounter++ % 1296).toString(36).padStart(2, '0');
|
|
888
|
+
const rand = () => Math.floor(Math.random() * 36 ** 4)
|
|
889
|
+
.toString(36)
|
|
890
|
+
.padStart(4, '0');
|
|
891
|
+
return `c${ts}${count}${rand()}${rand()}${rand()}`.slice(0, 25);
|
|
892
|
+
}
|
|
893
|
+
function clientDefaultValue(kind) {
|
|
894
|
+
if (kind === 'uuid')
|
|
895
|
+
return globalThis.crypto.randomUUID();
|
|
896
|
+
if (kind === 'cuid')
|
|
897
|
+
return makeCuid();
|
|
898
|
+
return new Date();
|
|
899
|
+
}
|
|
900
|
+
/** Fill missing create-side client defaults (uuid / cuid / now / updatedAt), by Prisma field name. */
|
|
901
|
+
function applyCreateDefaults(mm, data) {
|
|
902
|
+
const cd = mm.clientDefaults;
|
|
903
|
+
if (!cd || !isPlainObject(data))
|
|
904
|
+
return data;
|
|
905
|
+
let out = data;
|
|
906
|
+
for (const [field, kind] of Object.entries(cd)) {
|
|
907
|
+
if (out[field] === undefined) {
|
|
908
|
+
if (out === data)
|
|
909
|
+
out = { ...data };
|
|
910
|
+
out[field] = clientDefaultValue(kind);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
return out;
|
|
914
|
+
}
|
|
915
|
+
/** Touch @updatedAt fields on the update side (Prisma sets them on every update). */
|
|
916
|
+
function applyUpdateTouch(mm, data) {
|
|
917
|
+
const cd = mm.clientDefaults;
|
|
918
|
+
if (!cd || !isPlainObject(data))
|
|
919
|
+
return data;
|
|
920
|
+
let out = data;
|
|
921
|
+
for (const [field, kind] of Object.entries(cd)) {
|
|
922
|
+
if (kind === 'updatedAt' && out[field] === undefined) {
|
|
923
|
+
if (out === data)
|
|
924
|
+
out = { ...data };
|
|
925
|
+
out[field] = new Date();
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return out;
|
|
929
|
+
}
|
|
930
|
+
/** Whether TRANSLATED write data carries relation keys (nested-write shapes). */
|
|
931
|
+
function hasNestedKeys(ctx, mm, data) {
|
|
932
|
+
const rels = ctx.schema.tables[mm.table]?.relations;
|
|
933
|
+
if (!rels || !isPlainObject(data))
|
|
934
|
+
return false;
|
|
935
|
+
return Object.keys(data).some((k) => {
|
|
936
|
+
if (!Object.hasOwn(rels, k))
|
|
937
|
+
return false;
|
|
938
|
+
const v = data[k];
|
|
939
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date);
|
|
940
|
+
});
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Whether an upsert's translated `where` key values all equal the
|
|
944
|
+
* corresponding `create` values. When they do (the common Prisma idiom), the
|
|
945
|
+
* native single-statement ON CONFLICT upsert is semantically identical to
|
|
946
|
+
* Prisma's lookup-first and stays atomic. When they differ, native upsert
|
|
947
|
+
* would insert the `create` row even though the `where` row exists, so the
|
|
948
|
+
* adapter must emulate lookup-first instead.
|
|
949
|
+
*/
|
|
950
|
+
function upsertKeysMatch(t) {
|
|
951
|
+
const where = t.where;
|
|
952
|
+
const create = t.create;
|
|
953
|
+
if (!isPlainObject(where) || !isPlainObject(create))
|
|
954
|
+
return false;
|
|
955
|
+
const scalarEq = (a, b) => {
|
|
956
|
+
if (a instanceof Date || b instanceof Date) {
|
|
957
|
+
return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
|
|
958
|
+
}
|
|
959
|
+
return a === b;
|
|
960
|
+
};
|
|
961
|
+
for (const [k, v] of Object.entries(where)) {
|
|
962
|
+
if (v !== null && typeof v === 'object' && !(v instanceof Date)) {
|
|
963
|
+
// Compound-unique selector object: every member must scalar-match create.
|
|
964
|
+
if (Array.isArray(v))
|
|
965
|
+
return false;
|
|
966
|
+
for (const [mk, mv] of Object.entries(v)) {
|
|
967
|
+
if (mv !== null && typeof mv === 'object' && !(mv instanceof Date))
|
|
968
|
+
return false;
|
|
969
|
+
if (!scalarEq(mv, create[mk]))
|
|
970
|
+
return false;
|
|
971
|
+
}
|
|
972
|
+
continue;
|
|
973
|
+
}
|
|
974
|
+
if (!scalarEq(v, create[k]))
|
|
975
|
+
return false;
|
|
976
|
+
}
|
|
977
|
+
return true;
|
|
978
|
+
}
|
|
979
|
+
/** Prisma upsert semantics: look up by where; update the found row, else insert create. */
|
|
980
|
+
async function upsertLookupFirst(qi, t) {
|
|
981
|
+
const existing = await qi.findUnique({ where: t.where });
|
|
982
|
+
if (existing)
|
|
983
|
+
return qi.update({ where: t.where, data: t.update });
|
|
984
|
+
return qi.create({ data: t.create });
|
|
985
|
+
}
|
|
986
|
+
function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
876
987
|
const pe = ctx.options.prismaErrorCodes;
|
|
877
988
|
// Build a lazy Prisma-style promise for one delegate call. Crucially, the
|
|
878
989
|
// Prisma-arg `translate` step runs INSIDE the deferred paths (the run closure
|
|
@@ -885,7 +996,27 @@ function makeDelegate(ctx, mm, getQI) {
|
|
|
885
996
|
// catches the same throw from `batch.build`.
|
|
886
997
|
const defer = (translate, run, batch) => {
|
|
887
998
|
const batchable = batch
|
|
888
|
-
? {
|
|
999
|
+
? {
|
|
1000
|
+
build: () => batch.build(getQI(), translate()),
|
|
1001
|
+
reshape: batch.reshape,
|
|
1002
|
+
nested: () => {
|
|
1003
|
+
try {
|
|
1004
|
+
return batch.nested?.(translate()) ?? false;
|
|
1005
|
+
}
|
|
1006
|
+
catch {
|
|
1007
|
+
return false; // let the build path surface the translation error consistently
|
|
1008
|
+
}
|
|
1009
|
+
},
|
|
1010
|
+
execInTx: async (table) => {
|
|
1011
|
+
try {
|
|
1012
|
+
const t = translate();
|
|
1013
|
+
return batch.execInTx ? await batch.execInTx(table, t) : await run(table(mm.table), t);
|
|
1014
|
+
}
|
|
1015
|
+
catch (err) {
|
|
1016
|
+
throw decorate(err, pe);
|
|
1017
|
+
}
|
|
1018
|
+
},
|
|
1019
|
+
}
|
|
889
1020
|
: undefined;
|
|
890
1021
|
return new CompatPromise(async () => {
|
|
891
1022
|
try {
|
|
@@ -909,14 +1040,20 @@ function makeDelegate(ctx, mm, getQI) {
|
|
|
909
1040
|
findFirstOrThrow: (args = {}) => defer(() => translateReadArgs(ctx, mm, args), (qi, t) => qi.findFirstOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindFirstOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
|
|
910
1041
|
findUniqueOrThrow: (args) => defer(() => translateReadArgs(ctx, mm, requireWhere(args, 'findUniqueOrThrow')), (qi, t) => qi.findUniqueOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindUniqueOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
|
|
911
1042
|
create: (args) => defer(() => {
|
|
912
|
-
const t = { data: translateWriteData(ctx, mm, args.data) };
|
|
1043
|
+
const t = { data: translateWriteData(ctx, mm, applyCreateDefaults(mm, args.data)) };
|
|
913
1044
|
if (typeof args.timeout === 'number')
|
|
914
1045
|
t.timeout = args.timeout;
|
|
915
1046
|
return t;
|
|
916
|
-
}, (qi, t) => qi.create(t).then((r) => reshapeRow(ctx, mm, r)), {
|
|
1047
|
+
}, (qi, t) => qi.create(t).then((r) => reshapeRow(ctx, mm, r)), {
|
|
1048
|
+
build: (qi, t) => qi.buildCreate(t),
|
|
1049
|
+
reshape: (raw) => reshapeRow(ctx, mm, raw),
|
|
1050
|
+
nested: (t) => hasNestedKeys(ctx, mm, t.data),
|
|
1051
|
+
}),
|
|
917
1052
|
createMany: (args) => defer(() => {
|
|
918
1053
|
const data = args.data;
|
|
919
|
-
const rows = Array.isArray(data)
|
|
1054
|
+
const rows = Array.isArray(data)
|
|
1055
|
+
? data.map((d) => translateWriteData(ctx, mm, applyCreateDefaults(mm, d)))
|
|
1056
|
+
: [];
|
|
920
1057
|
const t = { data: rows };
|
|
921
1058
|
if (args.skipDuplicates)
|
|
922
1059
|
t.skipDuplicates = true;
|
|
@@ -924,11 +1061,21 @@ function makeDelegate(ctx, mm, getQI) {
|
|
|
924
1061
|
}, (qi, t) => qi.createMany(t).then((r) => ({ count: r.length })), { build: (qi, t) => qi.buildCreateMany(t), reshape: (raw) => ({ count: raw.length }) }),
|
|
925
1062
|
update: (args) => defer(() => {
|
|
926
1063
|
const a = requireWhere(args, 'update');
|
|
927
|
-
return {
|
|
928
|
-
|
|
1064
|
+
return {
|
|
1065
|
+
where: translateWhere(ctx, mm, a.where),
|
|
1066
|
+
data: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.data)),
|
|
1067
|
+
};
|
|
1068
|
+
}, (qi, t) => qi.update(t).then((r) => reshapeRow(ctx, mm, r)), {
|
|
1069
|
+
build: (qi, t) => qi.buildUpdate(t),
|
|
1070
|
+
reshape: (raw) => reshapeRow(ctx, mm, raw),
|
|
1071
|
+
nested: (t) => hasNestedKeys(ctx, mm, t.data),
|
|
1072
|
+
}),
|
|
929
1073
|
updateMany: (args) => defer(() => {
|
|
930
1074
|
const a = args;
|
|
931
|
-
const t = {
|
|
1075
|
+
const t = {
|
|
1076
|
+
where: translateWhere(ctx, mm, a.where ?? {}),
|
|
1077
|
+
data: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.data)),
|
|
1078
|
+
};
|
|
932
1079
|
if (a.where === undefined)
|
|
933
1080
|
t.allowFullTableScan = true;
|
|
934
1081
|
return t;
|
|
@@ -948,10 +1095,31 @@ function makeDelegate(ctx, mm, getQI) {
|
|
|
948
1095
|
const a = requireWhere(args, 'upsert');
|
|
949
1096
|
return {
|
|
950
1097
|
where: translateWhere(ctx, mm, a.where),
|
|
951
|
-
create: translateWriteData(ctx, mm, a.create),
|
|
952
|
-
update: translateWriteData(ctx, mm, a.update),
|
|
1098
|
+
create: translateWriteData(ctx, mm, applyCreateDefaults(mm, a.create)),
|
|
1099
|
+
update: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.update)),
|
|
953
1100
|
};
|
|
954
|
-
}, (qi, t) =>
|
|
1101
|
+
}, (qi, t) => {
|
|
1102
|
+
// Native ON CONFLICT upsert is only Prisma-equivalent when the where
|
|
1103
|
+
// key values equal the create values AND no nested write data is
|
|
1104
|
+
// present; otherwise emulate Prisma's lookup-first atomically.
|
|
1105
|
+
if (upsertKeysMatch(t) && !hasNestedKeys(ctx, mm, t.create) && !hasNestedKeys(ctx, mm, t.update)) {
|
|
1106
|
+
return qi.upsert(t).then((r) => reshapeRow(ctx, mm, r));
|
|
1107
|
+
}
|
|
1108
|
+
return runInTx(async (table) => {
|
|
1109
|
+
const row = await upsertLookupFirst(table(mm.table), t);
|
|
1110
|
+
return reshapeRow(ctx, mm, row);
|
|
1111
|
+
});
|
|
1112
|
+
}, {
|
|
1113
|
+
build: (qi, t) => qi.buildUpsert(t),
|
|
1114
|
+
reshape: (raw) => reshapeRow(ctx, mm, raw),
|
|
1115
|
+
nested: (t) => !upsertKeysMatch(t) || hasNestedKeys(ctx, mm, t.create) || hasNestedKeys(ctx, mm, t.update),
|
|
1116
|
+
execInTx: async (table, t) => {
|
|
1117
|
+
if (upsertKeysMatch(t) && !hasNestedKeys(ctx, mm, t.create) && !hasNestedKeys(ctx, mm, t.update)) {
|
|
1118
|
+
return reshapeRow(ctx, mm, await table(mm.table).upsert(t));
|
|
1119
|
+
}
|
|
1120
|
+
return reshapeRow(ctx, mm, await upsertLookupFirst(table(mm.table), t));
|
|
1121
|
+
},
|
|
1122
|
+
}),
|
|
955
1123
|
count: (args = {}) => defer(() => {
|
|
956
1124
|
const t = {};
|
|
957
1125
|
if (args.where !== undefined)
|
|
@@ -1046,7 +1214,7 @@ function createPrismaCompatClient(client, map, options = {}) {
|
|
|
1046
1214
|
// Delegates bound to the base client (each call reads db.table(...) lazily).
|
|
1047
1215
|
const delegates = new Map();
|
|
1048
1216
|
for (const [prismaModel, mm] of Object.entries(map.models)) {
|
|
1049
|
-
delegates.set(prismaModel, makeDelegate(ctx, mm, () => db.table(mm.table)));
|
|
1217
|
+
delegates.set(prismaModel, makeDelegate(ctx, mm, () => db.table(mm.table), (fn) => db.$transaction((tx) => fn((n) => tx.table(n)))));
|
|
1050
1218
|
}
|
|
1051
1219
|
const ph = placeholderOf(db);
|
|
1052
1220
|
const runRaw = async (text, params) => {
|
|
@@ -1073,6 +1241,18 @@ function createPrismaCompatClient(client, map, options = {}) {
|
|
|
1073
1241
|
}
|
|
1074
1242
|
return b;
|
|
1075
1243
|
});
|
|
1244
|
+
// Nested write data (or a lookup-first upsert) cannot run as a
|
|
1245
|
+
// single deferred statement. Prisma's array form still supports
|
|
1246
|
+
// those, so fall back to running the WHOLE array sequentially
|
|
1247
|
+
// inside one transaction; ordering and atomicity are preserved.
|
|
1248
|
+
if (batchables.some((b) => b.nested())) {
|
|
1249
|
+
return await db.$transaction(async (tx) => {
|
|
1250
|
+
const out = [];
|
|
1251
|
+
for (const b of batchables)
|
|
1252
|
+
out.push(await b.execInTx((n) => tx.table(n)));
|
|
1253
|
+
return out;
|
|
1254
|
+
}, txOptions);
|
|
1255
|
+
}
|
|
1076
1256
|
const deferreds = batchables.map((b) => b.build());
|
|
1077
1257
|
const results = (await db.$transaction(deferreds));
|
|
1078
1258
|
return results.map((raw, i) => batchables[i].reshape(raw));
|
|
@@ -1087,7 +1267,7 @@ function createPrismaCompatClient(client, map, options = {}) {
|
|
|
1087
1267
|
return db.$transaction((tx) => {
|
|
1088
1268
|
const txDelegates = {};
|
|
1089
1269
|
for (const [prismaModel, mm] of Object.entries(map.models)) {
|
|
1090
|
-
txDelegates[prismaModel] = makeDelegate(ctx, mm, () => tx.table(mm.table));
|
|
1270
|
+
txDelegates[prismaModel] = makeDelegate(ctx, mm, () => tx.table(mm.table), (fn) => fn((n) => tx.table(n)));
|
|
1091
1271
|
const alias = prismaPropertyAlias(prismaModel);
|
|
1092
1272
|
if (alias && !(alias in map.models) && !(alias in txDelegates)) {
|
|
1093
1273
|
txDelegates[alias] = txDelegates[prismaModel];
|
|
@@ -148,7 +148,15 @@ export function resolvePrismaSchema(ast, schema, options = {}) {
|
|
|
148
148
|
if (c.status === 'resolved' && c.turbineFields)
|
|
149
149
|
compoundUniques[c.selector] = c.turbineFields;
|
|
150
150
|
}
|
|
151
|
-
|
|
151
|
+
const clientDefaults = collectClientDefaults(model, fields, resolvedSchema?.tables[table]);
|
|
152
|
+
result.map.models[model.name] = {
|
|
153
|
+
table,
|
|
154
|
+
accessor,
|
|
155
|
+
fields,
|
|
156
|
+
relations,
|
|
157
|
+
compoundUniques,
|
|
158
|
+
...(clientDefaults ? { clientDefaults } : {}),
|
|
159
|
+
};
|
|
152
160
|
}
|
|
153
161
|
}
|
|
154
162
|
// Enums.
|
|
@@ -231,6 +239,42 @@ function relationFkColumns(model, field) {
|
|
|
231
239
|
const fieldsArg = relAttr?.args.find((a) => a.key === 'fields' && a.kind === 'array');
|
|
232
240
|
return fieldsArg?.items?.map((pf) => fieldColumn(model, pf)) ?? null;
|
|
233
241
|
}
|
|
242
|
+
/**
|
|
243
|
+
* Prisma CLIENT-side defaults for a resolved model: `@default(uuid())` /
|
|
244
|
+
* `@default(cuid())` / `@updatedAt` are filled by the Prisma client (the
|
|
245
|
+
* database column typically has NO default), so migrated call sites omit those
|
|
246
|
+
* fields and a plain insert violates NOT NULL. `@default(now())` is normally a
|
|
247
|
+
* database default, so it is carried only when the introspected column has no
|
|
248
|
+
* default. `@updatedAt` is always carried (Prisma touches it on every update
|
|
249
|
+
* regardless of any database default). Returns undefined when the model has
|
|
250
|
+
* none, keeping the emitted map byte-identical for unaffected schemas.
|
|
251
|
+
*/
|
|
252
|
+
function collectClientDefaults(model, resolvedFields, tableMeta) {
|
|
253
|
+
if (!tableMeta)
|
|
254
|
+
return undefined;
|
|
255
|
+
const out = {};
|
|
256
|
+
for (const field of model.fields) {
|
|
257
|
+
const turbineField = resolvedFields[field.name];
|
|
258
|
+
if (!turbineField)
|
|
259
|
+
continue;
|
|
260
|
+
const col = tableMeta.columns.find((c) => c.field === turbineField || c.name === turbineField);
|
|
261
|
+
if (field.attrs.some((a) => a.name === 'updatedAt')) {
|
|
262
|
+
out[field.name] = 'updatedAt';
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
265
|
+
if (col?.hasDefault)
|
|
266
|
+
continue; // the database fills it; nothing to emulate
|
|
267
|
+
const def = field.attrs.find((a) => a.name === 'default');
|
|
268
|
+
const raw = def?.args.find((a) => a.key === undefined && a.kind === 'raw')?.value ?? '';
|
|
269
|
+
if (/^uuid\s*\(/.test(raw))
|
|
270
|
+
out[field.name] = 'uuid';
|
|
271
|
+
else if (/^cuid\s*\(/.test(raw))
|
|
272
|
+
out[field.name] = 'cuid';
|
|
273
|
+
else if (/^now\s*\(\s*\)$/.test(raw))
|
|
274
|
+
out[field.name] = 'now';
|
|
275
|
+
}
|
|
276
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
277
|
+
}
|
|
234
278
|
/**
|
|
235
279
|
* FK columns for an INVERSE relation field (one carrying no `fields: [...]`),
|
|
236
280
|
* derived by @relation("Name") pairing: the opposing model's same-named field
|
package/dist/generate.js
CHANGED
|
@@ -944,6 +944,9 @@ export function generatePrismaMap(map, options) {
|
|
|
944
944
|
}
|
|
945
945
|
lines.push(' },');
|
|
946
946
|
lines.push(` compoundUniques: ${serializeStringArrayRecord(model.compoundUniques)},`);
|
|
947
|
+
if (model.clientDefaults && Object.keys(model.clientDefaults).length > 0) {
|
|
948
|
+
lines.push(` clientDefaults: ${serializeStringRecord(model.clientDefaults)},`);
|
|
949
|
+
}
|
|
947
950
|
lines.push(' },');
|
|
948
951
|
}
|
|
949
952
|
lines.push(' },');
|
package/dist/prisma-compat.js
CHANGED
|
@@ -868,7 +868,118 @@ function prismaPropertyAlias(model) {
|
|
|
868
868
|
* QueryInterface for this model's table on the active connection (the base pool,
|
|
869
869
|
* or a transaction's connection inside `$transaction(callback)`).
|
|
870
870
|
*/
|
|
871
|
-
|
|
871
|
+
// ---------------------------------------------------------------------------
|
|
872
|
+
// Prisma client-side defaults (@default(uuid()/cuid()) / @updatedAt / now())
|
|
873
|
+
// ---------------------------------------------------------------------------
|
|
874
|
+
let cuidCounter = Math.floor(Math.random() * 1296);
|
|
875
|
+
/**
|
|
876
|
+
* A cuid-shaped id ('c' + timestamp + counter + fingerprint + random, 25
|
|
877
|
+
* chars): collision-resistant and format-compatible with Prisma's
|
|
878
|
+
* `@default(cuid())` call sites. Not byte-identical to any specific cuid
|
|
879
|
+
* library; Prisma treats these as opaque unique strings.
|
|
880
|
+
*/
|
|
881
|
+
function makeCuid() {
|
|
882
|
+
const ts = Date.now().toString(36);
|
|
883
|
+
const count = (cuidCounter++ % 1296).toString(36).padStart(2, '0');
|
|
884
|
+
const rand = () => Math.floor(Math.random() * 36 ** 4)
|
|
885
|
+
.toString(36)
|
|
886
|
+
.padStart(4, '0');
|
|
887
|
+
return `c${ts}${count}${rand()}${rand()}${rand()}`.slice(0, 25);
|
|
888
|
+
}
|
|
889
|
+
function clientDefaultValue(kind) {
|
|
890
|
+
if (kind === 'uuid')
|
|
891
|
+
return globalThis.crypto.randomUUID();
|
|
892
|
+
if (kind === 'cuid')
|
|
893
|
+
return makeCuid();
|
|
894
|
+
return new Date();
|
|
895
|
+
}
|
|
896
|
+
/** Fill missing create-side client defaults (uuid / cuid / now / updatedAt), by Prisma field name. */
|
|
897
|
+
function applyCreateDefaults(mm, data) {
|
|
898
|
+
const cd = mm.clientDefaults;
|
|
899
|
+
if (!cd || !isPlainObject(data))
|
|
900
|
+
return data;
|
|
901
|
+
let out = data;
|
|
902
|
+
for (const [field, kind] of Object.entries(cd)) {
|
|
903
|
+
if (out[field] === undefined) {
|
|
904
|
+
if (out === data)
|
|
905
|
+
out = { ...data };
|
|
906
|
+
out[field] = clientDefaultValue(kind);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
return out;
|
|
910
|
+
}
|
|
911
|
+
/** Touch @updatedAt fields on the update side (Prisma sets them on every update). */
|
|
912
|
+
function applyUpdateTouch(mm, data) {
|
|
913
|
+
const cd = mm.clientDefaults;
|
|
914
|
+
if (!cd || !isPlainObject(data))
|
|
915
|
+
return data;
|
|
916
|
+
let out = data;
|
|
917
|
+
for (const [field, kind] of Object.entries(cd)) {
|
|
918
|
+
if (kind === 'updatedAt' && out[field] === undefined) {
|
|
919
|
+
if (out === data)
|
|
920
|
+
out = { ...data };
|
|
921
|
+
out[field] = new Date();
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return out;
|
|
925
|
+
}
|
|
926
|
+
/** Whether TRANSLATED write data carries relation keys (nested-write shapes). */
|
|
927
|
+
function hasNestedKeys(ctx, mm, data) {
|
|
928
|
+
const rels = ctx.schema.tables[mm.table]?.relations;
|
|
929
|
+
if (!rels || !isPlainObject(data))
|
|
930
|
+
return false;
|
|
931
|
+
return Object.keys(data).some((k) => {
|
|
932
|
+
if (!Object.hasOwn(rels, k))
|
|
933
|
+
return false;
|
|
934
|
+
const v = data[k];
|
|
935
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v) && !(v instanceof Date);
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Whether an upsert's translated `where` key values all equal the
|
|
940
|
+
* corresponding `create` values. When they do (the common Prisma idiom), the
|
|
941
|
+
* native single-statement ON CONFLICT upsert is semantically identical to
|
|
942
|
+
* Prisma's lookup-first and stays atomic. When they differ, native upsert
|
|
943
|
+
* would insert the `create` row even though the `where` row exists, so the
|
|
944
|
+
* adapter must emulate lookup-first instead.
|
|
945
|
+
*/
|
|
946
|
+
function upsertKeysMatch(t) {
|
|
947
|
+
const where = t.where;
|
|
948
|
+
const create = t.create;
|
|
949
|
+
if (!isPlainObject(where) || !isPlainObject(create))
|
|
950
|
+
return false;
|
|
951
|
+
const scalarEq = (a, b) => {
|
|
952
|
+
if (a instanceof Date || b instanceof Date) {
|
|
953
|
+
return a instanceof Date && b instanceof Date && a.getTime() === b.getTime();
|
|
954
|
+
}
|
|
955
|
+
return a === b;
|
|
956
|
+
};
|
|
957
|
+
for (const [k, v] of Object.entries(where)) {
|
|
958
|
+
if (v !== null && typeof v === 'object' && !(v instanceof Date)) {
|
|
959
|
+
// Compound-unique selector object: every member must scalar-match create.
|
|
960
|
+
if (Array.isArray(v))
|
|
961
|
+
return false;
|
|
962
|
+
for (const [mk, mv] of Object.entries(v)) {
|
|
963
|
+
if (mv !== null && typeof mv === 'object' && !(mv instanceof Date))
|
|
964
|
+
return false;
|
|
965
|
+
if (!scalarEq(mv, create[mk]))
|
|
966
|
+
return false;
|
|
967
|
+
}
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
if (!scalarEq(v, create[k]))
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
return true;
|
|
974
|
+
}
|
|
975
|
+
/** Prisma upsert semantics: look up by where; update the found row, else insert create. */
|
|
976
|
+
async function upsertLookupFirst(qi, t) {
|
|
977
|
+
const existing = await qi.findUnique({ where: t.where });
|
|
978
|
+
if (existing)
|
|
979
|
+
return qi.update({ where: t.where, data: t.update });
|
|
980
|
+
return qi.create({ data: t.create });
|
|
981
|
+
}
|
|
982
|
+
function makeDelegate(ctx, mm, getQI, runInTx) {
|
|
872
983
|
const pe = ctx.options.prismaErrorCodes;
|
|
873
984
|
// Build a lazy Prisma-style promise for one delegate call. Crucially, the
|
|
874
985
|
// Prisma-arg `translate` step runs INSIDE the deferred paths (the run closure
|
|
@@ -881,7 +992,27 @@ function makeDelegate(ctx, mm, getQI) {
|
|
|
881
992
|
// catches the same throw from `batch.build`.
|
|
882
993
|
const defer = (translate, run, batch) => {
|
|
883
994
|
const batchable = batch
|
|
884
|
-
? {
|
|
995
|
+
? {
|
|
996
|
+
build: () => batch.build(getQI(), translate()),
|
|
997
|
+
reshape: batch.reshape,
|
|
998
|
+
nested: () => {
|
|
999
|
+
try {
|
|
1000
|
+
return batch.nested?.(translate()) ?? false;
|
|
1001
|
+
}
|
|
1002
|
+
catch {
|
|
1003
|
+
return false; // let the build path surface the translation error consistently
|
|
1004
|
+
}
|
|
1005
|
+
},
|
|
1006
|
+
execInTx: async (table) => {
|
|
1007
|
+
try {
|
|
1008
|
+
const t = translate();
|
|
1009
|
+
return batch.execInTx ? await batch.execInTx(table, t) : await run(table(mm.table), t);
|
|
1010
|
+
}
|
|
1011
|
+
catch (err) {
|
|
1012
|
+
throw decorate(err, pe);
|
|
1013
|
+
}
|
|
1014
|
+
},
|
|
1015
|
+
}
|
|
885
1016
|
: undefined;
|
|
886
1017
|
return new CompatPromise(async () => {
|
|
887
1018
|
try {
|
|
@@ -905,14 +1036,20 @@ function makeDelegate(ctx, mm, getQI) {
|
|
|
905
1036
|
findFirstOrThrow: (args = {}) => defer(() => translateReadArgs(ctx, mm, args), (qi, t) => qi.findFirstOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindFirstOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
|
|
906
1037
|
findUniqueOrThrow: (args) => defer(() => translateReadArgs(ctx, mm, requireWhere(args, 'findUniqueOrThrow')), (qi, t) => qi.findUniqueOrThrow(t).then((r) => reshapeRow(ctx, mm, r)), { build: (qi, t) => qi.buildFindUniqueOrThrow(t), reshape: (raw) => reshapeRow(ctx, mm, raw) }),
|
|
907
1038
|
create: (args) => defer(() => {
|
|
908
|
-
const t = { data: translateWriteData(ctx, mm, args.data) };
|
|
1039
|
+
const t = { data: translateWriteData(ctx, mm, applyCreateDefaults(mm, args.data)) };
|
|
909
1040
|
if (typeof args.timeout === 'number')
|
|
910
1041
|
t.timeout = args.timeout;
|
|
911
1042
|
return t;
|
|
912
|
-
}, (qi, t) => qi.create(t).then((r) => reshapeRow(ctx, mm, r)), {
|
|
1043
|
+
}, (qi, t) => qi.create(t).then((r) => reshapeRow(ctx, mm, r)), {
|
|
1044
|
+
build: (qi, t) => qi.buildCreate(t),
|
|
1045
|
+
reshape: (raw) => reshapeRow(ctx, mm, raw),
|
|
1046
|
+
nested: (t) => hasNestedKeys(ctx, mm, t.data),
|
|
1047
|
+
}),
|
|
913
1048
|
createMany: (args) => defer(() => {
|
|
914
1049
|
const data = args.data;
|
|
915
|
-
const rows = Array.isArray(data)
|
|
1050
|
+
const rows = Array.isArray(data)
|
|
1051
|
+
? data.map((d) => translateWriteData(ctx, mm, applyCreateDefaults(mm, d)))
|
|
1052
|
+
: [];
|
|
916
1053
|
const t = { data: rows };
|
|
917
1054
|
if (args.skipDuplicates)
|
|
918
1055
|
t.skipDuplicates = true;
|
|
@@ -920,11 +1057,21 @@ function makeDelegate(ctx, mm, getQI) {
|
|
|
920
1057
|
}, (qi, t) => qi.createMany(t).then((r) => ({ count: r.length })), { build: (qi, t) => qi.buildCreateMany(t), reshape: (raw) => ({ count: raw.length }) }),
|
|
921
1058
|
update: (args) => defer(() => {
|
|
922
1059
|
const a = requireWhere(args, 'update');
|
|
923
|
-
return {
|
|
924
|
-
|
|
1060
|
+
return {
|
|
1061
|
+
where: translateWhere(ctx, mm, a.where),
|
|
1062
|
+
data: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.data)),
|
|
1063
|
+
};
|
|
1064
|
+
}, (qi, t) => qi.update(t).then((r) => reshapeRow(ctx, mm, r)), {
|
|
1065
|
+
build: (qi, t) => qi.buildUpdate(t),
|
|
1066
|
+
reshape: (raw) => reshapeRow(ctx, mm, raw),
|
|
1067
|
+
nested: (t) => hasNestedKeys(ctx, mm, t.data),
|
|
1068
|
+
}),
|
|
925
1069
|
updateMany: (args) => defer(() => {
|
|
926
1070
|
const a = args;
|
|
927
|
-
const t = {
|
|
1071
|
+
const t = {
|
|
1072
|
+
where: translateWhere(ctx, mm, a.where ?? {}),
|
|
1073
|
+
data: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.data)),
|
|
1074
|
+
};
|
|
928
1075
|
if (a.where === undefined)
|
|
929
1076
|
t.allowFullTableScan = true;
|
|
930
1077
|
return t;
|
|
@@ -944,10 +1091,31 @@ function makeDelegate(ctx, mm, getQI) {
|
|
|
944
1091
|
const a = requireWhere(args, 'upsert');
|
|
945
1092
|
return {
|
|
946
1093
|
where: translateWhere(ctx, mm, a.where),
|
|
947
|
-
create: translateWriteData(ctx, mm, a.create),
|
|
948
|
-
update: translateWriteData(ctx, mm, a.update),
|
|
1094
|
+
create: translateWriteData(ctx, mm, applyCreateDefaults(mm, a.create)),
|
|
1095
|
+
update: translateWriteData(ctx, mm, applyUpdateTouch(mm, a.update)),
|
|
949
1096
|
};
|
|
950
|
-
}, (qi, t) =>
|
|
1097
|
+
}, (qi, t) => {
|
|
1098
|
+
// Native ON CONFLICT upsert is only Prisma-equivalent when the where
|
|
1099
|
+
// key values equal the create values AND no nested write data is
|
|
1100
|
+
// present; otherwise emulate Prisma's lookup-first atomically.
|
|
1101
|
+
if (upsertKeysMatch(t) && !hasNestedKeys(ctx, mm, t.create) && !hasNestedKeys(ctx, mm, t.update)) {
|
|
1102
|
+
return qi.upsert(t).then((r) => reshapeRow(ctx, mm, r));
|
|
1103
|
+
}
|
|
1104
|
+
return runInTx(async (table) => {
|
|
1105
|
+
const row = await upsertLookupFirst(table(mm.table), t);
|
|
1106
|
+
return reshapeRow(ctx, mm, row);
|
|
1107
|
+
});
|
|
1108
|
+
}, {
|
|
1109
|
+
build: (qi, t) => qi.buildUpsert(t),
|
|
1110
|
+
reshape: (raw) => reshapeRow(ctx, mm, raw),
|
|
1111
|
+
nested: (t) => !upsertKeysMatch(t) || hasNestedKeys(ctx, mm, t.create) || hasNestedKeys(ctx, mm, t.update),
|
|
1112
|
+
execInTx: async (table, t) => {
|
|
1113
|
+
if (upsertKeysMatch(t) && !hasNestedKeys(ctx, mm, t.create) && !hasNestedKeys(ctx, mm, t.update)) {
|
|
1114
|
+
return reshapeRow(ctx, mm, await table(mm.table).upsert(t));
|
|
1115
|
+
}
|
|
1116
|
+
return reshapeRow(ctx, mm, await upsertLookupFirst(table(mm.table), t));
|
|
1117
|
+
},
|
|
1118
|
+
}),
|
|
951
1119
|
count: (args = {}) => defer(() => {
|
|
952
1120
|
const t = {};
|
|
953
1121
|
if (args.where !== undefined)
|
|
@@ -1042,7 +1210,7 @@ export function createPrismaCompatClient(client, map, options = {}) {
|
|
|
1042
1210
|
// Delegates bound to the base client (each call reads db.table(...) lazily).
|
|
1043
1211
|
const delegates = new Map();
|
|
1044
1212
|
for (const [prismaModel, mm] of Object.entries(map.models)) {
|
|
1045
|
-
delegates.set(prismaModel, makeDelegate(ctx, mm, () => db.table(mm.table)));
|
|
1213
|
+
delegates.set(prismaModel, makeDelegate(ctx, mm, () => db.table(mm.table), (fn) => db.$transaction((tx) => fn((n) => tx.table(n)))));
|
|
1046
1214
|
}
|
|
1047
1215
|
const ph = placeholderOf(db);
|
|
1048
1216
|
const runRaw = async (text, params) => {
|
|
@@ -1069,6 +1237,18 @@ export function createPrismaCompatClient(client, map, options = {}) {
|
|
|
1069
1237
|
}
|
|
1070
1238
|
return b;
|
|
1071
1239
|
});
|
|
1240
|
+
// Nested write data (or a lookup-first upsert) cannot run as a
|
|
1241
|
+
// single deferred statement. Prisma's array form still supports
|
|
1242
|
+
// those, so fall back to running the WHOLE array sequentially
|
|
1243
|
+
// inside one transaction; ordering and atomicity are preserved.
|
|
1244
|
+
if (batchables.some((b) => b.nested())) {
|
|
1245
|
+
return await db.$transaction(async (tx) => {
|
|
1246
|
+
const out = [];
|
|
1247
|
+
for (const b of batchables)
|
|
1248
|
+
out.push(await b.execInTx((n) => tx.table(n)));
|
|
1249
|
+
return out;
|
|
1250
|
+
}, txOptions);
|
|
1251
|
+
}
|
|
1072
1252
|
const deferreds = batchables.map((b) => b.build());
|
|
1073
1253
|
const results = (await db.$transaction(deferreds));
|
|
1074
1254
|
return results.map((raw, i) => batchables[i].reshape(raw));
|
|
@@ -1083,7 +1263,7 @@ export function createPrismaCompatClient(client, map, options = {}) {
|
|
|
1083
1263
|
return db.$transaction((tx) => {
|
|
1084
1264
|
const txDelegates = {};
|
|
1085
1265
|
for (const [prismaModel, mm] of Object.entries(map.models)) {
|
|
1086
|
-
txDelegates[prismaModel] = makeDelegate(ctx, mm, () => tx.table(mm.table));
|
|
1266
|
+
txDelegates[prismaModel] = makeDelegate(ctx, mm, () => tx.table(mm.table), (fn) => fn((n) => tx.table(n)));
|
|
1087
1267
|
const alias = prismaPropertyAlias(prismaModel);
|
|
1088
1268
|
if (alias && !(alias in map.models) && !(alias in txDelegates)) {
|
|
1089
1269
|
txDelegates[alias] = txDelegates[prismaModel];
|
package/dist/schema.d.ts
CHANGED
|
@@ -297,6 +297,17 @@ export interface PrismaModelMap {
|
|
|
297
297
|
* the core `findUnique`-family derivation cannot know.
|
|
298
298
|
*/
|
|
299
299
|
compoundUniques: Record<string, string[]>;
|
|
300
|
+
/**
|
|
301
|
+
* Prisma CLIENT-side default per Prisma field name. Prisma's
|
|
302
|
+
* `@default(uuid())` / `@default(cuid())` / `@updatedAt` (and `@default(now())`
|
|
303
|
+
* when the migration left no database default) are filled by the Prisma
|
|
304
|
+
* client, not the database, so the columns commonly have NO db default and a
|
|
305
|
+
* Prisma call site omits them. The compat adapter fills these on create
|
|
306
|
+
* (`uuid` / `cuid` / `now`) and touches `updatedAt` fields on
|
|
307
|
+
* update/updateMany/upsert, exactly like Prisma. Emitted by
|
|
308
|
+
* `migrate-from-prisma` only for columns the database does not default.
|
|
309
|
+
*/
|
|
310
|
+
clientDefaults?: Record<string, 'uuid' | 'cuid' | 'now' | 'updatedAt'>;
|
|
300
311
|
}
|
|
301
312
|
/** A resolved Prisma relation field → Turbine relation. */
|
|
302
313
|
export interface PrismaRelationMap {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "turbine-orm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.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": {
|