@cedarjs/internal 4.0.0-canary.13813 → 4.0.0-canary.13814

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.
@@ -22,6 +22,13 @@ export interface BackendModelInfo {
22
22
  fields: BackendFieldInfo[];
23
23
  idField: BackendFieldInfo | undefined;
24
24
  }
25
+ export interface GqlormBackendConfig {
26
+ membershipModel: string;
27
+ membershipModelCamel: string;
28
+ membershipUserField: string;
29
+ membershipOrganizationField: string;
30
+ membershipModelExists: boolean;
31
+ }
25
32
  /**
26
33
  * Map a DMMF field type to its GraphQL SDL equivalent.
27
34
  *
@@ -76,7 +83,7 @@ export declare function getExistingSdlTypeNames(graphqlDir: string): Set<string>
76
83
  * and fields — no hidden/sensitive fields, no @gqlorm hide models, no
77
84
  * dependency on the generated Prisma client path or @prisma/client.
78
85
  */
79
- export declare function generateGqlormBackendContent(models: BackendModelInfo[]): string;
86
+ export declare function generateGqlormBackendContent(models: BackendModelInfo[], config?: GqlormBackendConfig): string;
80
87
  /**
81
88
  * Generate gqlorm artifacts from the Prisma schema.
82
89
  *
@@ -1 +1 @@
1
- {"version":3,"file":"gqlormSchema.d.ts","sourceRoot":"","sources":["../../../src/generate/gqlormSchema.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,IAAI,MAAM,cAAc,CAAA;AA8BzC,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;AAE3C,UAAU,iBAAiB;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;CACf;AAED,UAAU,iBAAiB;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,iBAAiB,EAAE,CAAA;CAC5B;AAMD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,OAAO,CAAA;IACnB,IAAI,EAAE,OAAO,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,gBAAgB,EAAE,CAAA;IAC1B,OAAO,EAAE,gBAAgB,GAAG,SAAS,CAAA;CACtC;AAqDD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMvE;AAoGD,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,IAAI,CAAC,QAAQ,GAClB,iBAAiB,EAAE,CAiDrB;AAED,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,iBAAiB,EAAE,GAC1B,MAAM,CA2CR;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,GAAG,WAAW,CAkDjE;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,GAAG,gBAAgB,EAAE,CA2D7E;AAkBD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CA6BvE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,gBAAgB,EAAE,GACzB,MAAM,CA0IR;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC;IACvD,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;CAC9C,CAAC,CAgFD"}
1
+ {"version":3,"file":"gqlormSchema.d.ts","sourceRoot":"","sources":["../../../src/generate/gqlormSchema.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,IAAI,MAAM,cAAc,CAAA;AA8BzC,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;AAE3C,UAAU,iBAAiB;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;CACf;AAED,UAAU,iBAAiB;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,iBAAiB,EAAE,CAAA;CAC5B;AAMD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,OAAO,CAAA;IACnB,IAAI,EAAE,OAAO,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,gBAAgB,EAAE,CAAA;IAC1B,OAAO,EAAE,gBAAgB,GAAG,SAAS,CAAA;CACtC;AAED,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE,MAAM,CAAA;IACvB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,2BAA2B,EAAE,MAAM,CAAA;IACnC,qBAAqB,EAAE,OAAO,CAAA;CAC/B;AA6DD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMvE;AAoGD,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,IAAI,CAAC,QAAQ,GAClB,iBAAiB,EAAE,CAiDrB;AAED,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,iBAAiB,EAAE,GAC1B,MAAM,CA2CR;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,GAAG,WAAW,CAkDjE;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,GAAG,gBAAgB,EAAE,CA2D7E;AAkBD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CA6BvE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,gBAAgB,EAAE,EAC1B,MAAM,GAAE,mBAAmD,GAC1D,MAAM,CA0VR;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC;IACvD,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;CAC9C,CAAC,CAwHD"}
@@ -57,6 +57,13 @@ const SENSITIVE_PATTERNS = [
57
57
  "encryptionkey",
58
58
  "privatekey"
59
59
  ];
60
+ const DEFAULT_GQLORM_BACKEND_CONFIG = {
61
+ membershipModel: "Membership",
62
+ membershipModelCamel: "membership",
63
+ membershipUserField: "userId",
64
+ membershipOrganizationField: "organizationId",
65
+ membershipModelExists: false
66
+ };
60
67
  function hasDirective(doc, directive) {
61
68
  if (!doc) {
62
69
  return false;
@@ -341,16 +348,37 @@ function getExistingSdlTypeNames(graphqlDir) {
341
348
  }
342
349
  return typeNames;
343
350
  }
344
- function generateGqlormBackendContent(models) {
351
+ function generateGqlormBackendContent(models, config = DEFAULT_GQLORM_BACKEND_CONFIG) {
345
352
  if (models.length === 0) {
346
353
  return "";
347
354
  }
355
+ const anyModelNeedsOrgScoping = config.membershipModelExists && models.some(
356
+ (m) => m.camelName !== config.membershipModelCamel && m.fields.some((f) => f.name === config.membershipOrganizationField)
357
+ );
358
+ const anyModelNeedsAuth = models.some((m) => {
359
+ const hasUserField = m.fields.some(
360
+ (f) => f.name === config.membershipUserField
361
+ );
362
+ const hasOrgField = m.fields.some(
363
+ (f) => f.name === config.membershipOrganizationField
364
+ );
365
+ const isMembership = m.camelName === config.membershipModelCamel;
366
+ return hasUserField || hasOrgField && config.membershipModelExists && !isMembership;
367
+ });
348
368
  const lines = [
349
369
  "// This file is auto-generated by Cedar gqlorm codegen.",
350
370
  "// Do not edit \u2014 it will be overwritten on every codegen run.",
351
371
  "// To hide a model from gqlorm, add /// @gqlorm hide in schema.prisma.",
352
372
  "",
353
373
  "import gql from 'graphql-tag'",
374
+ ...anyModelNeedsAuth ? [
375
+ "import { AuthenticationError, ForbiddenError } from '@cedarjs/graphql-server'"
376
+ ] : [],
377
+ "",
378
+ "// Minimal context type used in auth checks",
379
+ "interface GqlormContext {",
380
+ " currentUser: Record<string, unknown> | null | undefined",
381
+ "}",
354
382
  "",
355
383
  "// Generated minimal interface \u2014 only visible models and fields, only the",
356
384
  "// operations used by this file. No @gqlorm hide models, no sensitive fields.",
@@ -362,7 +390,8 @@ function generateGqlormBackendContent(models) {
362
390
  const selectType = model.fields.map((f) => `${f.name}: true`).join("; ");
363
391
  lines.push(` ${model.camelName}: {`);
364
392
  lines.push(" findMany(args: {");
365
- lines.push(` select: { ${selectType} }`);
393
+ lines.push(" where?: Record<string, unknown>");
394
+ lines.push(` select: Partial<{ ${selectType} }>`);
366
395
  lines.push(" }): Promise<");
367
396
  lines.push(" Array<{");
368
397
  for (const field of model.fields) {
@@ -389,6 +418,29 @@ function generateGqlormBackendContent(models) {
389
418
  }
390
419
  lines.push(" } | null>");
391
420
  }
421
+ if (anyModelNeedsOrgScoping && model.camelName === config.membershipModelCamel) {
422
+ lines.push(" findFirst(args: {");
423
+ lines.push(" where: Record<string, unknown>");
424
+ lines.push(" }): Promise<Record<string, unknown> | null>");
425
+ }
426
+ lines.push(" }");
427
+ }
428
+ const membershipAlreadyInModels = models.some(
429
+ (m) => m.camelName === config.membershipModelCamel
430
+ );
431
+ if (anyModelNeedsOrgScoping && !membershipAlreadyInModels) {
432
+ lines.push(` ${config.membershipModelCamel}: {`);
433
+ lines.push(" findMany(args: {");
434
+ lines.push(" where: Record<string, unknown>");
435
+ lines.push(` select: { ${config.membershipOrganizationField}: true }`);
436
+ lines.push(" }): Promise<");
437
+ lines.push(
438
+ ` Array<{ ${config.membershipOrganizationField}: unknown }>`
439
+ );
440
+ lines.push(" >");
441
+ lines.push(" findFirst(args: {");
442
+ lines.push(" where: Record<string, unknown>");
443
+ lines.push(" }): Promise<Record<string, unknown> | null>");
392
444
  lines.push(" }");
393
445
  }
394
446
  lines.push("}");
@@ -405,11 +457,22 @@ function generateGqlormBackendContent(models) {
405
457
  }
406
458
  lines.push(" type Query {");
407
459
  for (const model of models) {
408
- lines.push(` ${model.pluralName}: [${model.modelName}!]! @skipAuth`);
460
+ const hasUserField = model.fields.some(
461
+ (f) => f.name === config.membershipUserField
462
+ );
463
+ const hasOrgField = model.fields.some(
464
+ (f) => f.name === config.membershipOrganizationField
465
+ );
466
+ const isMembershipModel = model.camelName === config.membershipModelCamel;
467
+ const needsAuth = hasUserField || hasOrgField && config.membershipModelExists && !isMembershipModel;
468
+ const authDirective = needsAuth ? "@requireAuth" : "@skipAuth";
469
+ lines.push(
470
+ ` ${model.pluralName}: [${model.modelName}!]! ${authDirective}`
471
+ );
409
472
  if (model.idField) {
410
473
  const idNullMark = model.idField.isRequired ? "!" : "";
411
474
  lines.push(
412
- ` ${model.camelName}(${model.idField.name}: ${model.idField.graphqlType}${idNullMark}): ${model.modelName} @skipAuth`
475
+ ` ${model.camelName}(${model.idField.name}: ${model.idField.graphqlType}${idNullMark}): ${model.modelName} ${authDirective}`
413
476
  );
414
477
  }
415
478
  }
@@ -428,21 +491,133 @@ function generateGqlormBackendContent(models) {
428
491
  for (let i = 0; i < models.length; i++) {
429
492
  const model = models[i];
430
493
  const selectObj = model.fields.map((f) => `${f.name}: true`).join(", ");
431
- lines.push(` ${model.pluralName}: () => {`);
432
- lines.push(` return db.${model.camelName}.findMany({`);
433
- lines.push(` select: { ${selectObj} },`);
434
- lines.push(" })");
494
+ const hasUserField = model.fields.some(
495
+ (f) => f.name === config.membershipUserField
496
+ );
497
+ const hasOrgField = model.fields.some(
498
+ (f) => f.name === config.membershipOrganizationField
499
+ );
500
+ const isMembershipModel = model.camelName === config.membershipModelCamel;
501
+ const useOrgScoping = hasOrgField && config.membershipModelExists && !isMembershipModel;
502
+ lines.push(
503
+ ` ${model.pluralName}: async (_root: unknown, _args: unknown, ${hasUserField || useOrgScoping ? "context" : "_context"}: GqlormContext) => {`
504
+ );
505
+ if (hasUserField || useOrgScoping) {
506
+ lines.push(" if (!context.currentUser) {");
507
+ lines.push(
508
+ ` throw new AuthenticationError("You don't have permission to do that.")`
509
+ );
510
+ lines.push(" }");
511
+ lines.push(" const currentUserId = context.currentUser['id']");
512
+ lines.push(
513
+ " if (currentUserId === undefined || currentUserId === null) {"
514
+ );
515
+ lines.push(
516
+ ` throw new AuthenticationError("Could not determine the current user's ID.")`
517
+ );
518
+ lines.push(" }");
519
+ lines.push(" const where: Record<string, unknown> = {}");
520
+ if (hasUserField) {
521
+ lines.push(" // Scope to the current user");
522
+ lines.push(
523
+ ` where['${config.membershipUserField}'] = currentUserId`
524
+ );
525
+ }
526
+ if (useOrgScoping) {
527
+ lines.push(" // Scope to the current user's organizations");
528
+ lines.push(
529
+ ` const memberships = await db.${config.membershipModelCamel}.findMany({`
530
+ );
531
+ lines.push(
532
+ ` where: { ${config.membershipUserField}: currentUserId },`
533
+ );
534
+ lines.push(
535
+ ` select: { ${config.membershipOrganizationField}: true },`
536
+ );
537
+ lines.push(" })");
538
+ lines.push(
539
+ ` const organizationIds = memberships.map((m) => m.${config.membershipOrganizationField})`
540
+ );
541
+ lines.push(
542
+ ` where['${config.membershipOrganizationField}'] = { in: organizationIds }`
543
+ );
544
+ }
545
+ lines.push(` return db.${model.camelName}.findMany({`);
546
+ lines.push(" where,");
547
+ lines.push(` select: { ${selectObj} },`);
548
+ lines.push(" })");
549
+ } else {
550
+ lines.push(` return db.${model.camelName}.findMany({`);
551
+ lines.push(` select: { ${selectObj} },`);
552
+ lines.push(" })");
553
+ }
435
554
  lines.push(" },");
436
555
  if (model.idField) {
437
556
  const idFieldName = model.idField.name;
438
557
  const tsType = graphqlTypeToTsType(model.idField.graphqlType);
439
558
  lines.push(
440
- ` ${model.camelName}: (_root: unknown, { ${idFieldName} }: { ${idFieldName}: ${tsType} }) => {`
559
+ ` ${model.camelName}: async (_root: unknown, { ${idFieldName} }: { ${idFieldName}: ${tsType} }, ${hasUserField || useOrgScoping ? "context" : "_context"}: GqlormContext) => {`
560
+ );
561
+ if (hasUserField || useOrgScoping) {
562
+ lines.push(" if (!context.currentUser) {");
563
+ lines.push(
564
+ ` throw new AuthenticationError("You don't have permission to do that.")`
565
+ );
566
+ lines.push(" }");
567
+ lines.push(" const currentUserId = context.currentUser['id']");
568
+ lines.push(
569
+ " if (currentUserId === undefined || currentUserId === null) {"
570
+ );
571
+ lines.push(
572
+ ` throw new AuthenticationError("Could not determine the current user's ID.")`
573
+ );
574
+ lines.push(" }");
575
+ }
576
+ lines.push("");
577
+ lines.push(
578
+ ` const record = await db.${model.camelName}.findUnique({`
441
579
  );
442
- lines.push(` return db.${model.camelName}.findUnique({`);
443
580
  lines.push(` where: { ${idFieldName} },`);
444
581
  lines.push(` select: { ${selectObj} },`);
445
582
  lines.push(" })");
583
+ lines.push("");
584
+ lines.push(" if (!record) {");
585
+ lines.push(" return null");
586
+ lines.push(" }");
587
+ if (hasUserField) {
588
+ lines.push("");
589
+ lines.push(" // Verify the current user owns this record");
590
+ lines.push(
591
+ ` if (record.${config.membershipUserField} !== currentUserId) {`
592
+ );
593
+ lines.push(
594
+ ` throw new ForbiddenError('Not authorized to access this resource')`
595
+ );
596
+ lines.push(" }");
597
+ }
598
+ if (useOrgScoping) {
599
+ lines.push("");
600
+ lines.push(
601
+ " // Verify the current user belongs to the record's organization"
602
+ );
603
+ lines.push(
604
+ ` const membership = await db.${config.membershipModelCamel}.findFirst({`
605
+ );
606
+ lines.push(" where: {");
607
+ lines.push(` ${config.membershipUserField}: currentUserId,`);
608
+ lines.push(
609
+ ` ${config.membershipOrganizationField}: record.${config.membershipOrganizationField},`
610
+ );
611
+ lines.push(" },");
612
+ lines.push(" })");
613
+ lines.push(" if (!membership) {");
614
+ lines.push(
615
+ ` throw new ForbiddenError('Not authorized to access this resource')`
616
+ );
617
+ lines.push(" }");
618
+ }
619
+ lines.push("");
620
+ lines.push(" return record");
446
621
  lines.push(" },");
447
622
  }
448
623
  if (i < models.length - 1) {
@@ -489,11 +664,37 @@ async function generateGqlormArtifacts() {
489
664
  const graphqlDir = paths.api.graphql;
490
665
  const existingTypes = getExistingSdlTypeNames(graphqlDir);
491
666
  const allModels = buildBackendModelInfo(dmmf);
667
+ const gqlormConfig = (0, import_project_config.getConfig)().experimental.gqlorm;
668
+ const membershipModel = gqlormConfig.membershipModel ?? "Membership";
669
+ const membershipModelCamel = membershipModel.charAt(0).toLowerCase() + membershipModel.slice(1);
670
+ const membershipUserField = gqlormConfig.membershipUserField ?? "userId";
671
+ const membershipOrganizationField = gqlormConfig.membershipOrganizationField ?? "organizationId";
672
+ const membershipModelExists = dmmf.datamodel.models.some(
673
+ (m) => m.name === membershipModel
674
+ );
675
+ const backendConfig = {
676
+ membershipModel,
677
+ membershipModelCamel,
678
+ membershipUserField,
679
+ membershipOrganizationField,
680
+ membershipModelExists
681
+ };
492
682
  const gqlormModels = allModels.filter(
493
683
  (m) => !existingTypes.has(m.modelName)
494
684
  );
685
+ const anyModelHasOrgField = gqlormModels.some(
686
+ (m) => m.fields.some((f) => f.name === membershipOrganizationField)
687
+ );
688
+ if (anyModelHasOrgField && !membershipModelExists) {
689
+ console.warn(
690
+ `[gqlorm] One or more models have a \`${membershipOrganizationField}\` field, but the membership model "${membershipModel}" was not found in the schema. Organization-based access scoping will not be applied for these models. Add a \`${membershipModel}\` model to your schema.prisma or configure \`experimental.gqlorm.membershipModel\` in cedar.toml.`
691
+ );
692
+ }
495
693
  if (gqlormModels.length > 0) {
496
- const backendContent = generateGqlormBackendContent(gqlormModels);
694
+ const backendContent = generateGqlormBackendContent(
695
+ gqlormModels,
696
+ backendConfig
697
+ );
497
698
  import_node_fs.default.mkdirSync(backendOutputDir, { recursive: true });
498
699
  import_node_fs.default.writeFileSync(backendOutputPath, backendContent);
499
700
  files.push(backendOutputPath);
@@ -22,6 +22,13 @@ export interface BackendModelInfo {
22
22
  fields: BackendFieldInfo[];
23
23
  idField: BackendFieldInfo | undefined;
24
24
  }
25
+ export interface GqlormBackendConfig {
26
+ membershipModel: string;
27
+ membershipModelCamel: string;
28
+ membershipUserField: string;
29
+ membershipOrganizationField: string;
30
+ membershipModelExists: boolean;
31
+ }
25
32
  /**
26
33
  * Map a DMMF field type to its GraphQL SDL equivalent.
27
34
  *
@@ -76,7 +83,7 @@ export declare function getExistingSdlTypeNames(graphqlDir: string): Set<string>
76
83
  * and fields — no hidden/sensitive fields, no @gqlorm hide models, no
77
84
  * dependency on the generated Prisma client path or @prisma/client.
78
85
  */
79
- export declare function generateGqlormBackendContent(models: BackendModelInfo[]): string;
86
+ export declare function generateGqlormBackendContent(models: BackendModelInfo[], config?: GqlormBackendConfig): string;
80
87
  /**
81
88
  * Generate gqlorm artifacts from the Prisma schema.
82
89
  *
@@ -1 +1 @@
1
- {"version":3,"file":"gqlormSchema.d.ts","sourceRoot":"","sources":["../../src/generate/gqlormSchema.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,IAAI,MAAM,cAAc,CAAA;AA8BzC,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;AAE3C,UAAU,iBAAiB;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;CACf;AAED,UAAU,iBAAiB;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,iBAAiB,EAAE,CAAA;CAC5B;AAMD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,OAAO,CAAA;IACnB,IAAI,EAAE,OAAO,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,gBAAgB,EAAE,CAAA;IAC1B,OAAO,EAAE,gBAAgB,GAAG,SAAS,CAAA;CACtC;AAqDD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMvE;AAoGD,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,IAAI,CAAC,QAAQ,GAClB,iBAAiB,EAAE,CAiDrB;AAED,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,iBAAiB,EAAE,GAC1B,MAAM,CA2CR;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,GAAG,WAAW,CAkDjE;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,GAAG,gBAAgB,EAAE,CA2D7E;AAkBD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CA6BvE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,gBAAgB,EAAE,GACzB,MAAM,CA0IR;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC;IACvD,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;CAC9C,CAAC,CAgFD"}
1
+ {"version":3,"file":"gqlormSchema.d.ts","sourceRoot":"","sources":["../../src/generate/gqlormSchema.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,KAAK,IAAI,MAAM,cAAc,CAAA;AA8BzC,KAAK,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAA;AAE3C,UAAU,iBAAiB;IACzB,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,MAAM,CAAA;CACf;AAED,UAAU,iBAAiB;IACzB,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,MAAM,EAAE,iBAAiB,EAAE,CAAA;CAC5B;AAMD,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,EAAE,MAAM,CAAA;IACnB,UAAU,EAAE,OAAO,CAAA;IACnB,IAAI,EAAE,OAAO,CAAA;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,CAAA;IACjB,SAAS,EAAE,MAAM,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,MAAM,EAAE,gBAAgB,EAAE,CAAA;IAC1B,OAAO,EAAE,gBAAgB,GAAG,SAAS,CAAA;CACtC;AAED,MAAM,WAAW,mBAAmB;IAClC,eAAe,EAAE,MAAM,CAAA;IACvB,oBAAoB,EAAE,MAAM,CAAA;IAC5B,mBAAmB,EAAE,MAAM,CAAA;IAC3B,2BAA2B,EAAE,MAAM,CAAA;IACnC,qBAAqB,EAAE,OAAO,CAAA;CAC/B;AA6DD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMvE;AAoGD,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,IAAI,CAAC,QAAQ,GAClB,iBAAiB,EAAE,CAiDrB;AAED,wBAAgB,8BAA8B,CAC5C,MAAM,EAAE,iBAAiB,EAAE,GAC1B,MAAM,CA2CR;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,GAAG,WAAW,CAkDjE;AAMD;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,IAAI,CAAC,QAAQ,GAAG,gBAAgB,EAAE,CA2D7E;AAkBD;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CA6BvE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,gBAAgB,EAAE,EAC1B,MAAM,GAAE,mBAAmD,GAC1D,MAAM,CA0VR;AAMD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,uBAAuB,IAAI,OAAO,CAAC;IACvD,KAAK,EAAE,MAAM,EAAE,CAAA;IACf,MAAM,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,OAAO,CAAA;KAAE,EAAE,CAAA;CAC9C,CAAC,CAwHD"}
@@ -17,6 +17,13 @@ const SENSITIVE_PATTERNS = [
17
17
  "encryptionkey",
18
18
  "privatekey"
19
19
  ];
20
+ const DEFAULT_GQLORM_BACKEND_CONFIG = {
21
+ membershipModel: "Membership",
22
+ membershipModelCamel: "membership",
23
+ membershipUserField: "userId",
24
+ membershipOrganizationField: "organizationId",
25
+ membershipModelExists: false
26
+ };
20
27
  function hasDirective(doc, directive) {
21
28
  if (!doc) {
22
29
  return false;
@@ -301,16 +308,37 @@ function getExistingSdlTypeNames(graphqlDir) {
301
308
  }
302
309
  return typeNames;
303
310
  }
304
- function generateGqlormBackendContent(models) {
311
+ function generateGqlormBackendContent(models, config = DEFAULT_GQLORM_BACKEND_CONFIG) {
305
312
  if (models.length === 0) {
306
313
  return "";
307
314
  }
315
+ const anyModelNeedsOrgScoping = config.membershipModelExists && models.some(
316
+ (m) => m.camelName !== config.membershipModelCamel && m.fields.some((f) => f.name === config.membershipOrganizationField)
317
+ );
318
+ const anyModelNeedsAuth = models.some((m) => {
319
+ const hasUserField = m.fields.some(
320
+ (f) => f.name === config.membershipUserField
321
+ );
322
+ const hasOrgField = m.fields.some(
323
+ (f) => f.name === config.membershipOrganizationField
324
+ );
325
+ const isMembership = m.camelName === config.membershipModelCamel;
326
+ return hasUserField || hasOrgField && config.membershipModelExists && !isMembership;
327
+ });
308
328
  const lines = [
309
329
  "// This file is auto-generated by Cedar gqlorm codegen.",
310
330
  "// Do not edit \u2014 it will be overwritten on every codegen run.",
311
331
  "// To hide a model from gqlorm, add /// @gqlorm hide in schema.prisma.",
312
332
  "",
313
333
  "import gql from 'graphql-tag'",
334
+ ...anyModelNeedsAuth ? [
335
+ "import { AuthenticationError, ForbiddenError } from '@cedarjs/graphql-server'"
336
+ ] : [],
337
+ "",
338
+ "// Minimal context type used in auth checks",
339
+ "interface GqlormContext {",
340
+ " currentUser: Record<string, unknown> | null | undefined",
341
+ "}",
314
342
  "",
315
343
  "// Generated minimal interface \u2014 only visible models and fields, only the",
316
344
  "// operations used by this file. No @gqlorm hide models, no sensitive fields.",
@@ -322,7 +350,8 @@ function generateGqlormBackendContent(models) {
322
350
  const selectType = model.fields.map((f) => `${f.name}: true`).join("; ");
323
351
  lines.push(` ${model.camelName}: {`);
324
352
  lines.push(" findMany(args: {");
325
- lines.push(` select: { ${selectType} }`);
353
+ lines.push(" where?: Record<string, unknown>");
354
+ lines.push(` select: Partial<{ ${selectType} }>`);
326
355
  lines.push(" }): Promise<");
327
356
  lines.push(" Array<{");
328
357
  for (const field of model.fields) {
@@ -349,6 +378,29 @@ function generateGqlormBackendContent(models) {
349
378
  }
350
379
  lines.push(" } | null>");
351
380
  }
381
+ if (anyModelNeedsOrgScoping && model.camelName === config.membershipModelCamel) {
382
+ lines.push(" findFirst(args: {");
383
+ lines.push(" where: Record<string, unknown>");
384
+ lines.push(" }): Promise<Record<string, unknown> | null>");
385
+ }
386
+ lines.push(" }");
387
+ }
388
+ const membershipAlreadyInModels = models.some(
389
+ (m) => m.camelName === config.membershipModelCamel
390
+ );
391
+ if (anyModelNeedsOrgScoping && !membershipAlreadyInModels) {
392
+ lines.push(` ${config.membershipModelCamel}: {`);
393
+ lines.push(" findMany(args: {");
394
+ lines.push(" where: Record<string, unknown>");
395
+ lines.push(` select: { ${config.membershipOrganizationField}: true }`);
396
+ lines.push(" }): Promise<");
397
+ lines.push(
398
+ ` Array<{ ${config.membershipOrganizationField}: unknown }>`
399
+ );
400
+ lines.push(" >");
401
+ lines.push(" findFirst(args: {");
402
+ lines.push(" where: Record<string, unknown>");
403
+ lines.push(" }): Promise<Record<string, unknown> | null>");
352
404
  lines.push(" }");
353
405
  }
354
406
  lines.push("}");
@@ -365,11 +417,22 @@ function generateGqlormBackendContent(models) {
365
417
  }
366
418
  lines.push(" type Query {");
367
419
  for (const model of models) {
368
- lines.push(` ${model.pluralName}: [${model.modelName}!]! @skipAuth`);
420
+ const hasUserField = model.fields.some(
421
+ (f) => f.name === config.membershipUserField
422
+ );
423
+ const hasOrgField = model.fields.some(
424
+ (f) => f.name === config.membershipOrganizationField
425
+ );
426
+ const isMembershipModel = model.camelName === config.membershipModelCamel;
427
+ const needsAuth = hasUserField || hasOrgField && config.membershipModelExists && !isMembershipModel;
428
+ const authDirective = needsAuth ? "@requireAuth" : "@skipAuth";
429
+ lines.push(
430
+ ` ${model.pluralName}: [${model.modelName}!]! ${authDirective}`
431
+ );
369
432
  if (model.idField) {
370
433
  const idNullMark = model.idField.isRequired ? "!" : "";
371
434
  lines.push(
372
- ` ${model.camelName}(${model.idField.name}: ${model.idField.graphqlType}${idNullMark}): ${model.modelName} @skipAuth`
435
+ ` ${model.camelName}(${model.idField.name}: ${model.idField.graphqlType}${idNullMark}): ${model.modelName} ${authDirective}`
373
436
  );
374
437
  }
375
438
  }
@@ -388,21 +451,133 @@ function generateGqlormBackendContent(models) {
388
451
  for (let i = 0; i < models.length; i++) {
389
452
  const model = models[i];
390
453
  const selectObj = model.fields.map((f) => `${f.name}: true`).join(", ");
391
- lines.push(` ${model.pluralName}: () => {`);
392
- lines.push(` return db.${model.camelName}.findMany({`);
393
- lines.push(` select: { ${selectObj} },`);
394
- lines.push(" })");
454
+ const hasUserField = model.fields.some(
455
+ (f) => f.name === config.membershipUserField
456
+ );
457
+ const hasOrgField = model.fields.some(
458
+ (f) => f.name === config.membershipOrganizationField
459
+ );
460
+ const isMembershipModel = model.camelName === config.membershipModelCamel;
461
+ const useOrgScoping = hasOrgField && config.membershipModelExists && !isMembershipModel;
462
+ lines.push(
463
+ ` ${model.pluralName}: async (_root: unknown, _args: unknown, ${hasUserField || useOrgScoping ? "context" : "_context"}: GqlormContext) => {`
464
+ );
465
+ if (hasUserField || useOrgScoping) {
466
+ lines.push(" if (!context.currentUser) {");
467
+ lines.push(
468
+ ` throw new AuthenticationError("You don't have permission to do that.")`
469
+ );
470
+ lines.push(" }");
471
+ lines.push(" const currentUserId = context.currentUser['id']");
472
+ lines.push(
473
+ " if (currentUserId === undefined || currentUserId === null) {"
474
+ );
475
+ lines.push(
476
+ ` throw new AuthenticationError("Could not determine the current user's ID.")`
477
+ );
478
+ lines.push(" }");
479
+ lines.push(" const where: Record<string, unknown> = {}");
480
+ if (hasUserField) {
481
+ lines.push(" // Scope to the current user");
482
+ lines.push(
483
+ ` where['${config.membershipUserField}'] = currentUserId`
484
+ );
485
+ }
486
+ if (useOrgScoping) {
487
+ lines.push(" // Scope to the current user's organizations");
488
+ lines.push(
489
+ ` const memberships = await db.${config.membershipModelCamel}.findMany({`
490
+ );
491
+ lines.push(
492
+ ` where: { ${config.membershipUserField}: currentUserId },`
493
+ );
494
+ lines.push(
495
+ ` select: { ${config.membershipOrganizationField}: true },`
496
+ );
497
+ lines.push(" })");
498
+ lines.push(
499
+ ` const organizationIds = memberships.map((m) => m.${config.membershipOrganizationField})`
500
+ );
501
+ lines.push(
502
+ ` where['${config.membershipOrganizationField}'] = { in: organizationIds }`
503
+ );
504
+ }
505
+ lines.push(` return db.${model.camelName}.findMany({`);
506
+ lines.push(" where,");
507
+ lines.push(` select: { ${selectObj} },`);
508
+ lines.push(" })");
509
+ } else {
510
+ lines.push(` return db.${model.camelName}.findMany({`);
511
+ lines.push(` select: { ${selectObj} },`);
512
+ lines.push(" })");
513
+ }
395
514
  lines.push(" },");
396
515
  if (model.idField) {
397
516
  const idFieldName = model.idField.name;
398
517
  const tsType = graphqlTypeToTsType(model.idField.graphqlType);
399
518
  lines.push(
400
- ` ${model.camelName}: (_root: unknown, { ${idFieldName} }: { ${idFieldName}: ${tsType} }) => {`
519
+ ` ${model.camelName}: async (_root: unknown, { ${idFieldName} }: { ${idFieldName}: ${tsType} }, ${hasUserField || useOrgScoping ? "context" : "_context"}: GqlormContext) => {`
520
+ );
521
+ if (hasUserField || useOrgScoping) {
522
+ lines.push(" if (!context.currentUser) {");
523
+ lines.push(
524
+ ` throw new AuthenticationError("You don't have permission to do that.")`
525
+ );
526
+ lines.push(" }");
527
+ lines.push(" const currentUserId = context.currentUser['id']");
528
+ lines.push(
529
+ " if (currentUserId === undefined || currentUserId === null) {"
530
+ );
531
+ lines.push(
532
+ ` throw new AuthenticationError("Could not determine the current user's ID.")`
533
+ );
534
+ lines.push(" }");
535
+ }
536
+ lines.push("");
537
+ lines.push(
538
+ ` const record = await db.${model.camelName}.findUnique({`
401
539
  );
402
- lines.push(` return db.${model.camelName}.findUnique({`);
403
540
  lines.push(` where: { ${idFieldName} },`);
404
541
  lines.push(` select: { ${selectObj} },`);
405
542
  lines.push(" })");
543
+ lines.push("");
544
+ lines.push(" if (!record) {");
545
+ lines.push(" return null");
546
+ lines.push(" }");
547
+ if (hasUserField) {
548
+ lines.push("");
549
+ lines.push(" // Verify the current user owns this record");
550
+ lines.push(
551
+ ` if (record.${config.membershipUserField} !== currentUserId) {`
552
+ );
553
+ lines.push(
554
+ ` throw new ForbiddenError('Not authorized to access this resource')`
555
+ );
556
+ lines.push(" }");
557
+ }
558
+ if (useOrgScoping) {
559
+ lines.push("");
560
+ lines.push(
561
+ " // Verify the current user belongs to the record's organization"
562
+ );
563
+ lines.push(
564
+ ` const membership = await db.${config.membershipModelCamel}.findFirst({`
565
+ );
566
+ lines.push(" where: {");
567
+ lines.push(` ${config.membershipUserField}: currentUserId,`);
568
+ lines.push(
569
+ ` ${config.membershipOrganizationField}: record.${config.membershipOrganizationField},`
570
+ );
571
+ lines.push(" },");
572
+ lines.push(" })");
573
+ lines.push(" if (!membership) {");
574
+ lines.push(
575
+ ` throw new ForbiddenError('Not authorized to access this resource')`
576
+ );
577
+ lines.push(" }");
578
+ }
579
+ lines.push("");
580
+ lines.push(" return record");
406
581
  lines.push(" },");
407
582
  }
408
583
  if (i < models.length - 1) {
@@ -449,11 +624,37 @@ async function generateGqlormArtifacts() {
449
624
  const graphqlDir = paths.api.graphql;
450
625
  const existingTypes = getExistingSdlTypeNames(graphqlDir);
451
626
  const allModels = buildBackendModelInfo(dmmf);
627
+ const gqlormConfig = getConfig().experimental.gqlorm;
628
+ const membershipModel = gqlormConfig.membershipModel ?? "Membership";
629
+ const membershipModelCamel = membershipModel.charAt(0).toLowerCase() + membershipModel.slice(1);
630
+ const membershipUserField = gqlormConfig.membershipUserField ?? "userId";
631
+ const membershipOrganizationField = gqlormConfig.membershipOrganizationField ?? "organizationId";
632
+ const membershipModelExists = dmmf.datamodel.models.some(
633
+ (m) => m.name === membershipModel
634
+ );
635
+ const backendConfig = {
636
+ membershipModel,
637
+ membershipModelCamel,
638
+ membershipUserField,
639
+ membershipOrganizationField,
640
+ membershipModelExists
641
+ };
452
642
  const gqlormModels = allModels.filter(
453
643
  (m) => !existingTypes.has(m.modelName)
454
644
  );
645
+ const anyModelHasOrgField = gqlormModels.some(
646
+ (m) => m.fields.some((f) => f.name === membershipOrganizationField)
647
+ );
648
+ if (anyModelHasOrgField && !membershipModelExists) {
649
+ console.warn(
650
+ `[gqlorm] One or more models have a \`${membershipOrganizationField}\` field, but the membership model "${membershipModel}" was not found in the schema. Organization-based access scoping will not be applied for these models. Add a \`${membershipModel}\` model to your schema.prisma or configure \`experimental.gqlorm.membershipModel\` in cedar.toml.`
651
+ );
652
+ }
455
653
  if (gqlormModels.length > 0) {
456
- const backendContent = generateGqlormBackendContent(gqlormModels);
654
+ const backendContent = generateGqlormBackendContent(
655
+ gqlormModels,
656
+ backendConfig
657
+ );
457
658
  fs.mkdirSync(backendOutputDir, { recursive: true });
458
659
  fs.writeFileSync(backendOutputPath, backendContent);
459
660
  files.push(backendOutputPath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cedarjs/internal",
3
- "version": "4.0.0-canary.13813+9c6f670ae9",
3
+ "version": "4.0.0-canary.13814+3150cd24f2",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/cedarjs/cedar.git",
@@ -159,13 +159,13 @@
159
159
  "@babel/plugin-transform-react-jsx": "7.28.6",
160
160
  "@babel/plugin-transform-typescript": "^7.26.8",
161
161
  "@babel/traverse": "7.29.0",
162
- "@cedarjs/babel-config": "4.0.0-canary.13813",
163
- "@cedarjs/cli-helpers": "4.0.0-canary.13813",
164
- "@cedarjs/graphql-server": "4.0.0-canary.13813",
165
- "@cedarjs/project-config": "4.0.0-canary.13813",
166
- "@cedarjs/router": "4.0.0-canary.13813",
167
- "@cedarjs/structure": "4.0.0-canary.13813",
168
- "@cedarjs/utils": "4.0.0-canary.13813",
162
+ "@cedarjs/babel-config": "4.0.0-canary.13814",
163
+ "@cedarjs/cli-helpers": "4.0.0-canary.13814",
164
+ "@cedarjs/graphql-server": "4.0.0-canary.13814",
165
+ "@cedarjs/project-config": "4.0.0-canary.13814",
166
+ "@cedarjs/router": "4.0.0-canary.13814",
167
+ "@cedarjs/structure": "4.0.0-canary.13814",
168
+ "@cedarjs/utils": "4.0.0-canary.13814",
169
169
  "@graphql-codegen/add": "6.0.0",
170
170
  "@graphql-codegen/cli": "6.2.1",
171
171
  "@graphql-codegen/client-preset": "5.2.4",
@@ -198,7 +198,7 @@
198
198
  },
199
199
  "devDependencies": {
200
200
  "@arethetypeswrong/cli": "0.18.2",
201
- "@cedarjs/framework-tools": "4.0.0-canary.13813",
201
+ "@cedarjs/framework-tools": "4.0.0-canary.13814",
202
202
  "concurrently": "9.2.1",
203
203
  "graphql-tag": "2.12.6",
204
204
  "publint": "0.3.18",
@@ -211,5 +211,5 @@
211
211
  "publishConfig": {
212
212
  "access": "public"
213
213
  },
214
- "gitHead": "9c6f670ae9b1c903854fc4ba8a27592f27b634d2"
214
+ "gitHead": "3150cd24f242d15164dfafe0e64928bf5e3e1aca"
215
215
  }