@prisma-idb/idb-client-generator 0.34.1 → 0.35.1

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.
Files changed (29) hide show
  1. package/dist/fileCreators/batch-processor/create.js +289 -120
  2. package/dist/fileCreators/batch-processor/create.js.map +1 -1
  3. package/dist/fileCreators/idb-interface/create.js +12 -4
  4. package/dist/fileCreators/idb-interface/create.js.map +1 -1
  5. package/dist/fileCreators/idb-utils/create.js +3 -0
  6. package/dist/fileCreators/idb-utils/create.js.map +1 -1
  7. package/dist/fileCreators/idb-utils/helpers/extractEqualityValue.js +23 -0
  8. package/dist/fileCreators/idb-utils/helpers/extractEqualityValue.js.map +1 -0
  9. package/dist/fileCreators/prisma-idb-client/classes/PrismaIDBClient.js +30 -10
  10. package/dist/fileCreators/prisma-idb-client/classes/PrismaIDBClient.js.map +1 -1
  11. package/dist/fileCreators/prisma-idb-client/classes/models/IDBModelClass.js +3 -1
  12. package/dist/fileCreators/prisma-idb-client/classes/models/IDBModelClass.js.map +1 -1
  13. package/dist/fileCreators/prisma-idb-client/classes/models/api/findMany.js +93 -11
  14. package/dist/fileCreators/prisma-idb-client/classes/models/api/findMany.js.map +1 -1
  15. package/dist/fileCreators/prisma-idb-client/classes/models/api/findUnique.js +13 -4
  16. package/dist/fileCreators/prisma-idb-client/classes/models/api/findUnique.js.map +1 -1
  17. package/dist/fileCreators/prisma-idb-client/classes/models/utils/_getRecords.js +65 -0
  18. package/dist/fileCreators/prisma-idb-client/classes/models/utils/_getRecords.js.map +1 -0
  19. package/dist/fileCreators/prisma-idb-client/create.js +4 -4
  20. package/dist/fileCreators/prisma-idb-client/create.js.map +1 -1
  21. package/dist/fileCreators/scoped-schema/create.js +10 -5
  22. package/dist/fileCreators/scoped-schema/create.js.map +1 -1
  23. package/dist/generator.js +4 -2
  24. package/dist/generator.js.map +1 -1
  25. package/dist/helpers/parseGeneratorConfig.js +49 -1
  26. package/dist/helpers/parseGeneratorConfig.js.map +1 -1
  27. package/dist/helpers/utils.js +57 -0
  28. package/dist/helpers/utils.js.map +1 -1
  29. package/package.json +3 -3
@@ -15,31 +15,48 @@ exports.pushErrorTypes = {
15
15
  MAX_RETRIES: "MAX_RETRIES",
16
16
  CUSTOM_VALIDATION_FAILED: "CUSTOM_VALIDATION_FAILED",
17
17
  };
18
- function buildAuthorizationPath(targetModel, rootModel, models, dag) {
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 dfs = (current) => {
26
- if (current === rootModel)
27
- return true;
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 false;
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) && dfs(field.type)) {
35
- path.unshift(field.name);
36
- return true;
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
- return path;
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 authPath = buildAuthorizationPath(model.name, rootModel.name, models, dag);
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 = buildFlatWhereClause(pk.name, pkFields, authPath, rootModel);
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 authPath = buildAuthorizationPath(model.name, rootModel.name, allModels, dag);
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 && authPath.length > 0) {
325
- const firstRelationFieldName = authPath[0];
326
- const relationField = model.fields.find((f) => f.name === firstRelationFieldName);
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
- const foreignKeyFields = relationField.relationFromFields;
334
- const parentModel = allModels.find((m) => m.name === relationField.type);
335
- if (!parentModel || foreignKeyFields.length === 0) {
336
- throw new Error(`Failed to find parent model for ${model.name} via relation field ${firstRelationFieldName}`);
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
- else {
354
- const compositeKey = parentPkFields.map((field, i) => `${field}: data.${foreignKeyFields[i]}`).join(", ");
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
- writer.writeLine(` select: ${parentSelectObj},`);
358
- writer.writeLine(` });`);
359
- writer.blankLine();
360
- writer.writeLine(` if (!parentRecord || parentRecord${accessChain} !== scopeKey) {`);
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
- writer.writeLine(` const parentRecord = await tx.${parentModelLower}.findUnique({`);
372
- if (parentPkFields.length === 1) {
373
- writer.writeLine(` where: { ${parentPkFields[0]}: data.${foreignKeyFields[0]} },`);
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 compositeKey = parentPkFields.map((field, i) => `${field}: data.${foreignKeyFields[i]}`).join(", ");
377
- writer.writeLine(` where: { ${parentPk.name}: { ${compositeKey} } },`);
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 = buildSelectObject(authPath, rootModel);
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 accessChain = buildAccessChain(authPath, rootPkFieldName, model, allModels);
444
- writer.writeLine(` if (record${accessChain} !== scopeKey) {`);
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 (authPath.length > 0) {
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
- const firstRelationFieldName = authPath[0];
493
- const relationField = model.fields.find((f) => f.name === firstRelationFieldName);
494
- if (relationField && relationField.relationFromFields && relationField.relationFromFields.length > 0) {
495
- const foreignKeyFields = relationField.relationFromFields;
496
- const parentModel = allModels.find((m) => m.name === relationField.type);
497
- if (parentModel) {
498
- const parentModelLower = parentModel.name.charAt(0).toLowerCase() + parentModel.name.slice(1);
499
- const remainingPath = authPath.slice(1);
500
- if (remainingPath.length > 0) {
501
- const parentSelectObj = buildSelectObject(remainingPath, rootModel);
502
- const parentPk = (0, utils_1.getUniqueIdentifiers)(parentModel)[0];
503
- const parentPkFields = JSON.parse(parentPk.keyPath);
504
- const rootPkFieldName2 = (0, utils_1.getUniqueIdentifiers)(rootModel)[0].name;
505
- const accessChainRemaining = buildAccessChain(remainingPath, rootPkFieldName2, parentModel, allModels);
506
- if (!relationField.isRequired) {
507
- emitOptionalFkGuard(writer, " ", foreignKeyFields, model.name);
508
- }
509
- writer.writeLine(` const parent = await tx.${parentModelLower}.findUnique({`);
510
- if (parentPkFields.length === 1) {
511
- writer.writeLine(` where: { ${parentPkFields[0]}: data.${foreignKeyFields[0]} },`);
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
- const compositeKey = parentPkFields.map((field, i) => `${field}: data.${foreignKeyFields[i]}`).join(", ");
515
- writer.writeLine(` where: { ${parentPk.name}: { ${compositeKey} } },`);
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 = buildSelectObject(authPath, rootModel);
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 accessChain = buildAccessChain(authPath, rootPkFieldName, model, allModels);
586
- writer.writeLine(` if (record${accessChain} !== scopeKey) {`);
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] }`;