@prisma-idb/idb-client-generator 0.35.0 → 0.36.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/LICENSE +21 -661
- package/README.md +1 -1
- package/dist/fileCreators/batch-processor/create.js +289 -120
- package/dist/fileCreators/batch-processor/create.js.map +1 -1
- package/dist/fileCreators/prisma-idb-client/classes/PrismaIDBClient.js +1 -1
- package/dist/fileCreators/prisma-idb-client/classes/PrismaIDBClient.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -151,7 +151,7 @@ If you discover a security vulnerability, please follow our SECURITY.md guidelin
|
|
|
151
151
|
|
|
152
152
|
## License
|
|
153
153
|
|
|
154
|
-
This project is licensed under the
|
|
154
|
+
This project is licensed under the MIT License. See the LICENSE file for more details.
|
|
155
155
|
|
|
156
156
|
## Acknowledgements
|
|
157
157
|
|
|
@@ -15,31 +15,48 @@ exports.pushErrorTypes = {
|
|
|
15
15
|
MAX_RETRIES: "MAX_RETRIES",
|
|
16
16
|
CUSTOM_VALIDATION_FAILED: "CUSTOM_VALIDATION_FAILED",
|
|
17
17
|
};
|
|
18
|
-
function
|
|
18
|
+
function buildAllAuthorizationPaths(targetModel, rootModel, models, dag) {
|
|
19
19
|
if (targetModel === rootModel) {
|
|
20
20
|
return [];
|
|
21
21
|
}
|
|
22
|
-
const path = [];
|
|
23
|
-
const visited = new Set();
|
|
24
22
|
const modelMap = new Map(models.map((m) => [m.name, m]));
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
const allPaths = [];
|
|
24
|
+
const dfs = (current, path, visited) => {
|
|
25
|
+
if (current === rootModel) {
|
|
26
|
+
allPaths.push([...path]);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
28
29
|
visited.add(current);
|
|
29
30
|
const model = modelMap.get(current);
|
|
30
31
|
if (!model)
|
|
31
|
-
return
|
|
32
|
+
return;
|
|
32
33
|
const relationFields = model.fields.filter((f) => f.kind === "object" && !f.isList && dag[f.type]);
|
|
33
34
|
for (const field of relationFields) {
|
|
34
|
-
if (!visited.has(field.type)
|
|
35
|
-
path.
|
|
36
|
-
|
|
35
|
+
if (!visited.has(field.type)) {
|
|
36
|
+
path.push(field.name);
|
|
37
|
+
dfs(field.type, path, new Set(visited));
|
|
38
|
+
path.pop();
|
|
37
39
|
}
|
|
38
40
|
}
|
|
39
|
-
return false;
|
|
40
41
|
};
|
|
41
|
-
dfs(targetModel);
|
|
42
|
-
|
|
42
|
+
dfs(targetModel, [], new Set());
|
|
43
|
+
allPaths.sort((a, b) => {
|
|
44
|
+
if (a.length !== b.length)
|
|
45
|
+
return a.length - b.length;
|
|
46
|
+
const targetModelObj = modelMap.get(targetModel);
|
|
47
|
+
if (targetModelObj) {
|
|
48
|
+
const fieldA = targetModelObj.fields.find((f) => f.name === a[0]);
|
|
49
|
+
const fieldB = targetModelObj.fields.find((f) => f.name === b[0]);
|
|
50
|
+
if (fieldA && fieldB) {
|
|
51
|
+
if (fieldA.isRequired && !fieldB.isRequired)
|
|
52
|
+
return -1;
|
|
53
|
+
if (!fieldA.isRequired && fieldB.isRequired)
|
|
54
|
+
return 1;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return 0;
|
|
58
|
+
});
|
|
59
|
+
return allPaths;
|
|
43
60
|
}
|
|
44
61
|
function createBatchProcessorFile(writer, options) {
|
|
45
62
|
const { models, prismaClientImport, rootModel } = options;
|
|
@@ -220,7 +237,7 @@ function createBatchProcessorFile(writer, options) {
|
|
|
220
237
|
const pk = (0, utils_1.getUniqueIdentifiers)(model)[0];
|
|
221
238
|
const pkFields = JSON.parse(pk.keyPath);
|
|
222
239
|
const isRootModel = model.name === rootModel.name;
|
|
223
|
-
const
|
|
240
|
+
const allAuthPaths = buildAllAuthorizationPaths(model.name, rootModel.name, models, dag);
|
|
224
241
|
writer.writeLine(` case "${model.name}": {`);
|
|
225
242
|
writer.writeLine(` const keyPathValidation = keyPathValidators.${model.name}.safeParse(log.keyPath);`);
|
|
226
243
|
writer.writeLine(` if (!keyPathValidation.success) {`);
|
|
@@ -242,7 +259,7 @@ function createBatchProcessorFile(writer, options) {
|
|
|
242
259
|
writer.writeLine(` });`);
|
|
243
260
|
}
|
|
244
261
|
else {
|
|
245
|
-
const flatWhere =
|
|
262
|
+
const flatWhere = buildMultiPathFlatWhereClause(pk.name, pkFields, allAuthPaths, rootModel);
|
|
246
263
|
writer.writeLine(` const record = await prisma.${modelNameLower}.findFirst({`);
|
|
247
264
|
writer.writeLine(` where: ${flatWhere},`);
|
|
248
265
|
writer.writeLine(` select: { ${selectFields} },`);
|
|
@@ -293,8 +310,10 @@ function generateModelSyncHandler(writer, model, allModels, rootModel, dag) {
|
|
|
293
310
|
const modelNameLower = model.name.charAt(0).toLowerCase() + model.name.slice(1);
|
|
294
311
|
const pk = (0, utils_1.getUniqueIdentifiers)(model)[0];
|
|
295
312
|
const pkFields = JSON.parse(pk.keyPath);
|
|
296
|
-
const
|
|
313
|
+
const allAuthPaths = buildAllAuthorizationPaths(model.name, rootModel.name, allModels, dag);
|
|
314
|
+
const authPath = allAuthPaths[0] || [];
|
|
297
315
|
const isRootModel = model.name === rootModel.name;
|
|
316
|
+
const hasMultiplePaths = allAuthPaths.length > 1;
|
|
298
317
|
writer.writeLine(`async function sync${model.name}(event: OutboxEventRecord, data: z.infer<typeof validators.${model.name}>, scopeKey: string, prisma: PrismaClient): Promise<PushResult>`);
|
|
299
318
|
writer.block(() => {
|
|
300
319
|
writer.writeLine(`const { id, operation } = event;`);
|
|
@@ -321,67 +340,72 @@ function generateModelSyncHandler(writer, model, allModels, rootModel, dag) {
|
|
|
321
340
|
writer.writeLine(` return { id, error: null, appliedChangelogId: existingLog.id };`);
|
|
322
341
|
writer.writeLine(` }`);
|
|
323
342
|
writer.blankLine();
|
|
324
|
-
if (!isRootModel &&
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
if (!relationField) {
|
|
328
|
-
throw new Error(`Failed to find relation field ${firstRelationFieldName} on model ${model.name}`);
|
|
329
|
-
}
|
|
330
|
-
if (!relationField.relationFromFields || relationField.relationFromFields.length === 0) {
|
|
331
|
-
throw new Error(`Relation field ${firstRelationFieldName} on model ${model.name} does not have foreign key fields`);
|
|
343
|
+
if (!isRootModel && allAuthPaths.length > 0) {
|
|
344
|
+
if (hasMultiplePaths) {
|
|
345
|
+
emitMultiPathPayloadOwnershipCheck(writer, " ", allAuthPaths, model, allModels, rootModel, `Unauthorized: ${model.name} parent is not owned by authenticated scope`);
|
|
332
346
|
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const parentModelLower = parentModel.name.charAt(0).toLowerCase() + parentModel.name.slice(1);
|
|
339
|
-
const remainingPath = authPath.slice(1);
|
|
340
|
-
if (remainingPath.length > 0) {
|
|
341
|
-
const parentSelectObj = buildSelectObject(remainingPath, rootModel);
|
|
342
|
-
const parentPk = (0, utils_1.getUniqueIdentifiers)(parentModel)[0];
|
|
343
|
-
const parentPkFields = JSON.parse(parentPk.keyPath);
|
|
344
|
-
const rootPkFieldName = (0, utils_1.getUniqueIdentifiers)(rootModel)[0].name;
|
|
345
|
-
const accessChain = buildAccessChain(remainingPath, rootPkFieldName, parentModel, allModels);
|
|
346
|
-
if (!relationField.isRequired) {
|
|
347
|
-
emitOptionalFkGuard(writer, " ", foreignKeyFields, model.name);
|
|
348
|
-
}
|
|
349
|
-
writer.writeLine(` const parentRecord = await tx.${parentModelLower}.findUnique({`);
|
|
350
|
-
if (parentPkFields.length === 1) {
|
|
351
|
-
writer.writeLine(` where: { ${parentPkFields[0]}: data.${foreignKeyFields[0]} },`);
|
|
347
|
+
else {
|
|
348
|
+
const firstRelationFieldName = authPath[0];
|
|
349
|
+
const relationField = model.fields.find((f) => f.name === firstRelationFieldName);
|
|
350
|
+
if (!relationField) {
|
|
351
|
+
throw new Error(`Failed to find relation field ${firstRelationFieldName} on model ${model.name}`);
|
|
352
352
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
writer.writeLine(` where: { ${parentPk.name}: { ${compositeKey} } },`);
|
|
353
|
+
if (!relationField.relationFromFields || relationField.relationFromFields.length === 0) {
|
|
354
|
+
throw new Error(`Relation field ${firstRelationFieldName} on model ${model.name} does not have foreign key fields`);
|
|
356
355
|
}
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
writer.writeLine(` throw new PermanentSyncError("${exports.pushErrorTypes.SCOPE_VIOLATION}", \`Unauthorized: ${model.name} parent is not owned by authenticated scope\`);`);
|
|
362
|
-
writer.writeLine(` }`);
|
|
363
|
-
}
|
|
364
|
-
else {
|
|
365
|
-
const parentPk = (0, utils_1.getUniqueIdentifiers)(parentModel)[0];
|
|
366
|
-
const parentPkFields = JSON.parse(parentPk.keyPath);
|
|
367
|
-
const parentPkFieldName = (0, utils_1.getUniqueIdentifiers)(parentModel)[0].name;
|
|
368
|
-
if (!relationField.isRequired) {
|
|
369
|
-
emitOptionalFkGuard(writer, " ", foreignKeyFields, model.name);
|
|
356
|
+
const foreignKeyFields = relationField.relationFromFields;
|
|
357
|
+
const parentModel = allModels.find((m) => m.name === relationField.type);
|
|
358
|
+
if (!parentModel || foreignKeyFields.length === 0) {
|
|
359
|
+
throw new Error(`Failed to find parent model for ${model.name} via relation field ${firstRelationFieldName}`);
|
|
370
360
|
}
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
361
|
+
const parentModelLower = parentModel.name.charAt(0).toLowerCase() + parentModel.name.slice(1);
|
|
362
|
+
const remainingPath = authPath.slice(1);
|
|
363
|
+
if (remainingPath.length > 0) {
|
|
364
|
+
const parentSelectObj = buildSelectObject(remainingPath, rootModel);
|
|
365
|
+
const parentPk = (0, utils_1.getUniqueIdentifiers)(parentModel)[0];
|
|
366
|
+
const parentPkFields = JSON.parse(parentPk.keyPath);
|
|
367
|
+
const rootPkFieldName = (0, utils_1.getUniqueIdentifiers)(rootModel)[0].name;
|
|
368
|
+
const accessChain = buildAccessChain(remainingPath, rootPkFieldName, parentModel, allModels);
|
|
369
|
+
if (!relationField.isRequired) {
|
|
370
|
+
emitOptionalFkGuard(writer, " ", foreignKeyFields, model.name);
|
|
371
|
+
}
|
|
372
|
+
writer.writeLine(` const parentRecord = await tx.${parentModelLower}.findUnique({`);
|
|
373
|
+
if (parentPkFields.length === 1) {
|
|
374
|
+
writer.writeLine(` where: { ${parentPkFields[0]}: data.${foreignKeyFields[0]} },`);
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
const compositeKey = parentPkFields.map((field, i) => `${field}: data.${foreignKeyFields[i]}`).join(", ");
|
|
378
|
+
writer.writeLine(` where: { ${parentPk.name}: { ${compositeKey} } },`);
|
|
379
|
+
}
|
|
380
|
+
writer.writeLine(` select: ${parentSelectObj},`);
|
|
381
|
+
writer.writeLine(` });`);
|
|
382
|
+
writer.blankLine();
|
|
383
|
+
writer.writeLine(` if (!parentRecord || parentRecord${accessChain} !== scopeKey) {`);
|
|
384
|
+
writer.writeLine(` throw new PermanentSyncError("${exports.pushErrorTypes.SCOPE_VIOLATION}", \`Unauthorized: ${model.name} parent is not owned by authenticated scope\`);`);
|
|
385
|
+
writer.writeLine(` }`);
|
|
374
386
|
}
|
|
375
387
|
else {
|
|
376
|
-
const
|
|
377
|
-
|
|
388
|
+
const parentPk = (0, utils_1.getUniqueIdentifiers)(parentModel)[0];
|
|
389
|
+
const parentPkFields = JSON.parse(parentPk.keyPath);
|
|
390
|
+
const parentPkFieldName = (0, utils_1.getUniqueIdentifiers)(parentModel)[0].name;
|
|
391
|
+
if (!relationField.isRequired) {
|
|
392
|
+
emitOptionalFkGuard(writer, " ", foreignKeyFields, model.name);
|
|
393
|
+
}
|
|
394
|
+
writer.writeLine(` const parentRecord = await tx.${parentModelLower}.findUnique({`);
|
|
395
|
+
if (parentPkFields.length === 1) {
|
|
396
|
+
writer.writeLine(` where: { ${parentPkFields[0]}: data.${foreignKeyFields[0]} },`);
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
const compositeKey = parentPkFields.map((field, i) => `${field}: data.${foreignKeyFields[i]}`).join(", ");
|
|
400
|
+
writer.writeLine(` where: { ${parentPk.name}: { ${compositeKey} } },`);
|
|
401
|
+
}
|
|
402
|
+
writer.writeLine(` select: { ${parentPkFieldName}: true },`);
|
|
403
|
+
writer.writeLine(` });`);
|
|
404
|
+
writer.blankLine();
|
|
405
|
+
writer.writeLine(` if (!parentRecord || parentRecord.${parentPkFieldName} !== scopeKey) {`);
|
|
406
|
+
writer.writeLine(` throw new PermanentSyncError("${exports.pushErrorTypes.SCOPE_VIOLATION}", \`Unauthorized: ${model.name} parent is not owned by authenticated scope\`);`);
|
|
407
|
+
writer.writeLine(` }`);
|
|
378
408
|
}
|
|
379
|
-
writer.writeLine(` select: { ${parentPkFieldName}: true },`);
|
|
380
|
-
writer.writeLine(` });`);
|
|
381
|
-
writer.blankLine();
|
|
382
|
-
writer.writeLine(` if (!parentRecord || parentRecord.${parentPkFieldName} !== scopeKey) {`);
|
|
383
|
-
writer.writeLine(` throw new PermanentSyncError("${exports.pushErrorTypes.SCOPE_VIOLATION}", \`Unauthorized: ${model.name} parent is not owned by authenticated scope\`);`);
|
|
384
|
-
writer.writeLine(` }`);
|
|
385
409
|
}
|
|
386
410
|
writer.blankLine();
|
|
387
411
|
}
|
|
@@ -422,7 +446,7 @@ function generateModelSyncHandler(writer, model, allModels, rootModel, dag) {
|
|
|
422
446
|
writer.writeLine(` }`);
|
|
423
447
|
}
|
|
424
448
|
else {
|
|
425
|
-
const selectObj =
|
|
449
|
+
const selectObj = buildMultiPathSelectObject(allAuthPaths, rootModel);
|
|
426
450
|
if (pkFields.length === 1) {
|
|
427
451
|
writer.writeLine(` const record = await tx.${modelNameLower}.findUnique({`);
|
|
428
452
|
writer.writeLine(` where: { ${pkFields[0]}: validKeyPath[0] },`);
|
|
@@ -440,11 +464,14 @@ function generateModelSyncHandler(writer, model, allModels, rootModel, dag) {
|
|
|
440
464
|
writer.writeLine(` if (record) {`);
|
|
441
465
|
writer.writeLine(` // Case A: Record exists - verify ownership from DB`);
|
|
442
466
|
const rootPkFieldName = (0, utils_1.getUniqueIdentifiers)(rootModel)[0].name;
|
|
443
|
-
const
|
|
444
|
-
writer.writeLine(` if (
|
|
467
|
+
const ownershipCondition = buildMultiPathOwnershipCondition(allAuthPaths, rootPkFieldName, model, allModels);
|
|
468
|
+
writer.writeLine(` if (${ownershipCondition}) {`);
|
|
445
469
|
writer.writeLine(` throw new PermanentSyncError("${exports.pushErrorTypes.SCOPE_VIOLATION}", \`Unauthorized: ${model.name} is not owned by the authenticated scope\`);`);
|
|
446
470
|
writer.writeLine(` }`);
|
|
447
|
-
if (
|
|
471
|
+
if (hasMultiplePaths) {
|
|
472
|
+
emitMultiPathPayloadOwnershipCheck(writer, " ", allAuthPaths, model, allModels, rootModel, `Cannot reassign ${model.name} to parent outside scope`);
|
|
473
|
+
}
|
|
474
|
+
else if (authPath.length > 0) {
|
|
448
475
|
const firstRelationFieldName = authPath[0];
|
|
449
476
|
const relationField = model.fields.find((f) => f.name === firstRelationFieldName);
|
|
450
477
|
if (relationField && relationField.relationFromFields && relationField.relationFromFields.length > 0) {
|
|
@@ -489,42 +516,49 @@ function generateModelSyncHandler(writer, model, allModels, rootModel, dag) {
|
|
|
489
516
|
}
|
|
490
517
|
writer.writeLine(` } else {`);
|
|
491
518
|
writer.writeLine(` // Case B: Record doesn't exist (resurrection) - verify ownership from payload`);
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
const
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
519
|
+
if (hasMultiplePaths) {
|
|
520
|
+
emitMultiPathPayloadOwnershipCheck(writer, " ", allAuthPaths, model, allModels, rootModel, `Cannot resurrect ${model.name} into unauthorized scope`);
|
|
521
|
+
}
|
|
522
|
+
else {
|
|
523
|
+
const firstRelationFieldName = authPath[0];
|
|
524
|
+
const relationField = model.fields.find((f) => f.name === firstRelationFieldName);
|
|
525
|
+
if (relationField && relationField.relationFromFields && relationField.relationFromFields.length > 0) {
|
|
526
|
+
const foreignKeyFields = relationField.relationFromFields;
|
|
527
|
+
const parentModel = allModels.find((m) => m.name === relationField.type);
|
|
528
|
+
if (parentModel) {
|
|
529
|
+
const parentModelLower = parentModel.name.charAt(0).toLowerCase() + parentModel.name.slice(1);
|
|
530
|
+
const remainingPath = authPath.slice(1);
|
|
531
|
+
if (remainingPath.length > 0) {
|
|
532
|
+
const parentSelectObj = buildSelectObject(remainingPath, rootModel);
|
|
533
|
+
const parentPk = (0, utils_1.getUniqueIdentifiers)(parentModel)[0];
|
|
534
|
+
const parentPkFields = JSON.parse(parentPk.keyPath);
|
|
535
|
+
const rootPkFieldName2 = (0, utils_1.getUniqueIdentifiers)(rootModel)[0].name;
|
|
536
|
+
const accessChainRemaining = buildAccessChain(remainingPath, rootPkFieldName2, parentModel, allModels);
|
|
537
|
+
if (!relationField.isRequired) {
|
|
538
|
+
emitOptionalFkGuard(writer, " ", foreignKeyFields, model.name);
|
|
539
|
+
}
|
|
540
|
+
writer.writeLine(` const parent = await tx.${parentModelLower}.findUnique({`);
|
|
541
|
+
if (parentPkFields.length === 1) {
|
|
542
|
+
writer.writeLine(` where: { ${parentPkFields[0]}: data.${foreignKeyFields[0]} },`);
|
|
543
|
+
}
|
|
544
|
+
else {
|
|
545
|
+
const compositeKey = parentPkFields
|
|
546
|
+
.map((field, i) => `${field}: data.${foreignKeyFields[i]}`)
|
|
547
|
+
.join(", ");
|
|
548
|
+
writer.writeLine(` where: { ${parentPk.name}: { ${compositeKey} } },`);
|
|
549
|
+
}
|
|
550
|
+
writer.writeLine(` select: ${parentSelectObj},`);
|
|
551
|
+
writer.writeLine(` });`);
|
|
552
|
+
writer.blankLine();
|
|
553
|
+
writer.writeLine(` if (!parent || parent${accessChainRemaining} !== scopeKey) {`);
|
|
554
|
+
writer.writeLine(` throw new PermanentSyncError("${exports.pushErrorTypes.SCOPE_VIOLATION}", \`Cannot resurrect ${model.name} into unauthorized scope\`);`);
|
|
555
|
+
writer.writeLine(` }`);
|
|
512
556
|
}
|
|
513
557
|
else {
|
|
514
|
-
|
|
515
|
-
writer.writeLine(`
|
|
558
|
+
writer.writeLine(` if (data.${foreignKeyFields[0]} !== scopeKey) {`);
|
|
559
|
+
writer.writeLine(` throw new PermanentSyncError("${exports.pushErrorTypes.SCOPE_VIOLATION}", \`Cannot resurrect ${model.name} into different ${parentModel.name}\`);`);
|
|
560
|
+
writer.writeLine(` }`);
|
|
516
561
|
}
|
|
517
|
-
writer.writeLine(` select: ${parentSelectObj},`);
|
|
518
|
-
writer.writeLine(` });`);
|
|
519
|
-
writer.blankLine();
|
|
520
|
-
writer.writeLine(` if (!parent || parent${accessChainRemaining} !== scopeKey) {`);
|
|
521
|
-
writer.writeLine(` throw new PermanentSyncError("${exports.pushErrorTypes.SCOPE_VIOLATION}", \`Cannot resurrect ${model.name} into unauthorized scope\`);`);
|
|
522
|
-
writer.writeLine(` }`);
|
|
523
|
-
}
|
|
524
|
-
else {
|
|
525
|
-
writer.writeLine(` if (data.${foreignKeyFields[0]} !== scopeKey) {`);
|
|
526
|
-
writer.writeLine(` throw new PermanentSyncError("${exports.pushErrorTypes.SCOPE_VIOLATION}", \`Cannot resurrect ${model.name} into different ${parentModel.name}\`);`);
|
|
527
|
-
writer.writeLine(` }`);
|
|
528
562
|
}
|
|
529
563
|
}
|
|
530
564
|
}
|
|
@@ -565,7 +599,7 @@ function generateModelSyncHandler(writer, model, allModels, rootModel, dag) {
|
|
|
565
599
|
writer.writeLine(` }`);
|
|
566
600
|
}
|
|
567
601
|
else {
|
|
568
|
-
const selectObj =
|
|
602
|
+
const selectObj = buildMultiPathSelectObject(allAuthPaths, rootModel);
|
|
569
603
|
if (pkFields.length === 1) {
|
|
570
604
|
writer.writeLine(` const record = await tx.${modelNameLower}.findUnique({`);
|
|
571
605
|
writer.writeLine(` where: { ${pkFields[0]}: validKeyPath[0] },`);
|
|
@@ -582,8 +616,8 @@ function generateModelSyncHandler(writer, model, allModels, rootModel, dag) {
|
|
|
582
616
|
writer.blankLine();
|
|
583
617
|
writer.writeLine(` if (record) {`);
|
|
584
618
|
const rootPkFieldName = (0, utils_1.getUniqueIdentifiers)(rootModel)[0].name;
|
|
585
|
-
const
|
|
586
|
-
writer.writeLine(` if (
|
|
619
|
+
const ownershipCondition = buildMultiPathOwnershipCondition(allAuthPaths, rootPkFieldName, model, allModels);
|
|
620
|
+
writer.writeLine(` if (${ownershipCondition}) {`);
|
|
587
621
|
writer.writeLine(` throw new PermanentSyncError("${exports.pushErrorTypes.SCOPE_VIOLATION}", \`Unauthorized: ${model.name} is not owned by the authenticated scope\`);`);
|
|
588
622
|
writer.writeLine(` }`);
|
|
589
623
|
writer.writeLine(` }`);
|
|
@@ -612,16 +646,6 @@ function generateModelSyncHandler(writer, model, allModels, rootModel, dag) {
|
|
|
612
646
|
});
|
|
613
647
|
writer.blankLine();
|
|
614
648
|
}
|
|
615
|
-
function buildFlatWhereClause(pkName, pkFields, authPath, rootModel) {
|
|
616
|
-
const whereWithScope = buildWhereWithScopeCondition(authPath, rootModel);
|
|
617
|
-
if (pkFields.length === 1) {
|
|
618
|
-
return `{ ${pkFields[0]}: validKeyPath[0], ${whereWithScope} }`;
|
|
619
|
-
}
|
|
620
|
-
else {
|
|
621
|
-
const compositePkFields = pkFields.map((field, i) => `${field}: validKeyPath[${i}]`).join(", ");
|
|
622
|
-
return `{ ${compositePkFields}, ${whereWithScope} }`;
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
649
|
function buildSelectObject(authPath, rootModel) {
|
|
626
650
|
if (authPath.length === 0)
|
|
627
651
|
return "{}";
|
|
@@ -679,6 +703,151 @@ function emitOptionalFkGuard(writer, indent, foreignKeyFields, modelName) {
|
|
|
679
703
|
writer.writeLine(`${indent} throw new PermanentSyncError("${exports.pushErrorTypes.SCOPE_VIOLATION}", \`Unauthorized: ${modelName} has null foreign key(s) in ownership path\`);`);
|
|
680
704
|
writer.writeLine(`${indent}}`);
|
|
681
705
|
}
|
|
706
|
+
function buildMultiPathSelectObject(allPaths, rootModel) {
|
|
707
|
+
if (allPaths.length === 0)
|
|
708
|
+
return "{}";
|
|
709
|
+
if (allPaths.length === 1)
|
|
710
|
+
return buildSelectObject(allPaths[0], rootModel);
|
|
711
|
+
const rootPkFieldName = (0, utils_1.getUniqueIdentifiers)(rootModel)[0].name;
|
|
712
|
+
function mergePathIntoTree(tree, path) {
|
|
713
|
+
if (path.length === 0)
|
|
714
|
+
return;
|
|
715
|
+
const [first, ...rest] = path;
|
|
716
|
+
if (rest.length === 0) {
|
|
717
|
+
if (!tree[first] || typeof tree[first] === "boolean") {
|
|
718
|
+
tree[first] = { [rootPkFieldName]: true };
|
|
719
|
+
}
|
|
720
|
+
else {
|
|
721
|
+
tree[first][rootPkFieldName] = true;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
if (!tree[first] || typeof tree[first] === "boolean") {
|
|
726
|
+
tree[first] = {};
|
|
727
|
+
}
|
|
728
|
+
mergePathIntoTree(tree[first], rest);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
function serializeTree(tree) {
|
|
732
|
+
const parts = [];
|
|
733
|
+
for (const [key, value] of Object.entries(tree)) {
|
|
734
|
+
if (typeof value === "boolean") {
|
|
735
|
+
parts.push(`${key}: true`);
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
parts.push(`${key}: { select: ${serializeTree(value)} }`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
return `{ ${parts.join(", ")} }`;
|
|
742
|
+
}
|
|
743
|
+
const tree = {};
|
|
744
|
+
for (const path of allPaths) {
|
|
745
|
+
mergePathIntoTree(tree, path);
|
|
746
|
+
}
|
|
747
|
+
return serializeTree(tree);
|
|
748
|
+
}
|
|
749
|
+
function buildMultiPathOwnershipCondition(allPaths, rootPkFieldName, model, allModels, varName = "record") {
|
|
750
|
+
const chains = allPaths.map((path) => {
|
|
751
|
+
const accessChain = buildAccessChain(path, rootPkFieldName, model, allModels);
|
|
752
|
+
return `${varName}${accessChain} !== scopeKey`;
|
|
753
|
+
});
|
|
754
|
+
return chains.join(" && ");
|
|
755
|
+
}
|
|
756
|
+
function buildMultiPathFlatWhereClause(pkName, pkFields, allPaths, rootModel) {
|
|
757
|
+
let pkCondition;
|
|
758
|
+
if (pkFields.length === 1) {
|
|
759
|
+
pkCondition = `${pkFields[0]}: validKeyPath[0]`;
|
|
760
|
+
}
|
|
761
|
+
else {
|
|
762
|
+
pkCondition = pkFields.map((field, i) => `${field}: validKeyPath[${i}]`).join(", ");
|
|
763
|
+
}
|
|
764
|
+
if (allPaths.length === 1) {
|
|
765
|
+
return `{ ${pkCondition}, ${buildWhereWithScopeCondition(allPaths[0], rootModel)} }`;
|
|
766
|
+
}
|
|
767
|
+
const scopeConditions = allPaths.map((path) => {
|
|
768
|
+
const whereWithScope = buildWhereWithScopeCondition(path, rootModel);
|
|
769
|
+
return `{ ${whereWithScope} }`;
|
|
770
|
+
});
|
|
771
|
+
return `{ ${pkCondition}, OR: [${scopeConditions.join(", ")}] }`;
|
|
772
|
+
}
|
|
773
|
+
function emitMultiPathPayloadOwnershipCheck(writer, indent, allPaths, model, allModels, rootModel, errorMessage) {
|
|
774
|
+
writer.setIndentationLevel(indent);
|
|
775
|
+
writer.writeLine("let ownershipVerified = false;");
|
|
776
|
+
allPaths.forEach((path, pathIndex) => {
|
|
777
|
+
const firstRelationFieldName = path[0];
|
|
778
|
+
const relationField = model.fields.find((f) => f.name === firstRelationFieldName);
|
|
779
|
+
if (!relationField || !relationField.relationFromFields?.length)
|
|
780
|
+
return;
|
|
781
|
+
const foreignKeyFields = relationField.relationFromFields;
|
|
782
|
+
const parentModel = allModels.find((m) => m.name === relationField.type);
|
|
783
|
+
if (!parentModel)
|
|
784
|
+
return;
|
|
785
|
+
const parentModelLower = parentModel.name.charAt(0).toLowerCase() + parentModel.name.slice(1);
|
|
786
|
+
const remainingPath = path.slice(1);
|
|
787
|
+
const varName = `p${pathIndex}`;
|
|
788
|
+
const emitLookupAndCheck = () => {
|
|
789
|
+
if (remainingPath.length > 0) {
|
|
790
|
+
const parentSelectObj = buildSelectObject(remainingPath, rootModel);
|
|
791
|
+
const parentPk = (0, utils_1.getUniqueIdentifiers)(parentModel)[0];
|
|
792
|
+
const parentPkFields = JSON.parse(parentPk.keyPath);
|
|
793
|
+
const rootPkFieldName = (0, utils_1.getUniqueIdentifiers)(rootModel)[0].name;
|
|
794
|
+
const accessChain = buildAccessChain(remainingPath, rootPkFieldName, parentModel, allModels);
|
|
795
|
+
writer.writeLine(`const ${varName} = await tx.${parentModelLower}.findUnique({`);
|
|
796
|
+
writer.indent(() => {
|
|
797
|
+
if (parentPkFields.length === 1) {
|
|
798
|
+
writer.writeLine(`where: { ${parentPkFields[0]}: data.${foreignKeyFields[0]} },`);
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
const compositeKey = parentPkFields.map((field, i) => `${field}: data.${foreignKeyFields[i]}`).join(", ");
|
|
802
|
+
writer.writeLine(`where: { ${parentPk.name}: { ${compositeKey} } },`);
|
|
803
|
+
}
|
|
804
|
+
writer.writeLine(`select: ${parentSelectObj},`);
|
|
805
|
+
});
|
|
806
|
+
writer.writeLine("});");
|
|
807
|
+
writer.write(`if (!${varName} || ${varName}${accessChain} !== scopeKey) `).block(() => {
|
|
808
|
+
writer.writeLine(`throw new PermanentSyncError("${exports.pushErrorTypes.SCOPE_VIOLATION}", \`${errorMessage}\`);`);
|
|
809
|
+
});
|
|
810
|
+
writer.writeLine("ownershipVerified = true;");
|
|
811
|
+
}
|
|
812
|
+
else {
|
|
813
|
+
const parentPk = (0, utils_1.getUniqueIdentifiers)(parentModel)[0];
|
|
814
|
+
const parentPkFields = JSON.parse(parentPk.keyPath);
|
|
815
|
+
const parentPkFieldName = (0, utils_1.getUniqueIdentifiers)(parentModel)[0].name;
|
|
816
|
+
writer.writeLine(`const ${varName} = await tx.${parentModelLower}.findUnique({`);
|
|
817
|
+
writer.indent(() => {
|
|
818
|
+
if (parentPkFields.length === 1) {
|
|
819
|
+
writer.writeLine(`where: { ${parentPkFields[0]}: data.${foreignKeyFields[0]} },`);
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
const compositeKey = parentPkFields.map((field, i) => `${field}: data.${foreignKeyFields[i]}`).join(", ");
|
|
823
|
+
writer.writeLine(`where: { ${parentPk.name}: { ${compositeKey} } },`);
|
|
824
|
+
}
|
|
825
|
+
writer.writeLine(`select: { ${parentPkFieldName}: true },`);
|
|
826
|
+
});
|
|
827
|
+
writer.writeLine("});");
|
|
828
|
+
writer.write(`if (!${varName} || ${varName}.${parentPkFieldName} !== scopeKey) `).block(() => {
|
|
829
|
+
writer.writeLine(`throw new PermanentSyncError("${exports.pushErrorTypes.SCOPE_VIOLATION}", \`${errorMessage}\`);`);
|
|
830
|
+
});
|
|
831
|
+
writer.writeLine("ownershipVerified = true;");
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
if (!relationField.isRequired) {
|
|
835
|
+
const nullCheck = foreignKeyFields.length === 1
|
|
836
|
+
? `data.${foreignKeyFields[0]} !== null`
|
|
837
|
+
: foreignKeyFields.map((f) => `data.${f} !== null`).join(" && ");
|
|
838
|
+
writer.write(`if (${nullCheck}) `).block(() => {
|
|
839
|
+
emitLookupAndCheck();
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
else {
|
|
843
|
+
emitLookupAndCheck();
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
writer.write("if (!ownershipVerified) ").block(() => {
|
|
847
|
+
writer.writeLine(`throw new PermanentSyncError("${exports.pushErrorTypes.SCOPE_VIOLATION}", \`${errorMessage}\`);`);
|
|
848
|
+
});
|
|
849
|
+
writer.setIndentationLevel(0);
|
|
850
|
+
}
|
|
682
851
|
function generateWhereClause(pkName, pkFields) {
|
|
683
852
|
if (pkFields.length === 1) {
|
|
684
853
|
return `{ ${pkFields[0]}: validKeyPath[0] }`;
|