@cedarjs/internal 4.0.0 → 4.0.1-next.67

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.
@@ -1,8 +1,29 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
- import { getPaths, getPrismaSchemas } from "@cedarjs/project-config";
4
- const INTERNAL_MODEL_NAMES = /* @__PURE__ */ new Set(["RW_DataMigration"]);
5
- const SENSITIVE_PATTERNS = ["password", "secret", "token", "hash", "salt"];
3
+ import { getConfig, getPaths, getPrismaSchemas } from "@cedarjs/project-config";
4
+ import { pluralize } from "@cedarjs/utils/cedarPluralize";
5
+ const INTERNAL_MODEL_NAMES = /* @__PURE__ */ new Set([
6
+ "RW_DataMigration",
7
+ "Cedar_DataMigration"
8
+ ]);
9
+ const SENSITIVE_PATTERNS = [
10
+ "password",
11
+ "secret",
12
+ "token",
13
+ "hash",
14
+ "salt",
15
+ "apikey",
16
+ "secretkey",
17
+ "encryptionkey",
18
+ "privatekey"
19
+ ];
20
+ const DEFAULT_GQLORM_BACKEND_CONFIG = {
21
+ membershipModel: "Membership",
22
+ membershipModelCamel: "membership",
23
+ membershipUserField: "userId",
24
+ membershipOrganizationField: "organizationId",
25
+ membershipModelExists: false
26
+ };
6
27
  function hasDirective(doc, directive) {
7
28
  if (!doc) {
8
29
  return false;
@@ -22,6 +43,172 @@ function emitSensitivityWarning(modelName, fieldName) {
22
43
  `
23
44
  );
24
45
  }
46
+ const DMMF_TYPE_TO_GRAPHQL = {
47
+ String: "String",
48
+ Int: "Int",
49
+ Float: "Float",
50
+ BigInt: "BigInt",
51
+ Boolean: "Boolean",
52
+ DateTime: "DateTime",
53
+ Json: "JSON",
54
+ Decimal: "String",
55
+ Bytes: "String"
56
+ };
57
+ function mapDmmfTypeToGraphql(type, kind) {
58
+ if (kind === "enum") {
59
+ return "String";
60
+ }
61
+ return DMMF_TYPE_TO_GRAPHQL[type] ?? "String";
62
+ }
63
+ function graphqlTypeToTsType(graphqlType) {
64
+ switch (graphqlType) {
65
+ case "Int":
66
+ case "Float":
67
+ case "BigInt":
68
+ return "number";
69
+ case "Boolean":
70
+ return "boolean";
71
+ default:
72
+ return "string";
73
+ }
74
+ }
75
+ function graphqlTypeToTsInterfaceType(graphqlType, isRequired) {
76
+ let tsType;
77
+ switch (graphqlType) {
78
+ case "Int":
79
+ case "Float":
80
+ case "BigInt":
81
+ tsType = "number";
82
+ break;
83
+ case "Boolean":
84
+ tsType = "boolean";
85
+ break;
86
+ case "DateTime":
87
+ tsType = "Date";
88
+ break;
89
+ default:
90
+ tsType = "string";
91
+ }
92
+ return isRequired ? tsType : `${tsType} | null`;
93
+ }
94
+ function dmmfTypeToFrontendTsType(type, kind, isRequired) {
95
+ let tsType;
96
+ if (kind === "enum") {
97
+ tsType = "string";
98
+ } else {
99
+ switch (type) {
100
+ case "String":
101
+ tsType = "string";
102
+ break;
103
+ case "Int":
104
+ case "Float":
105
+ tsType = "number";
106
+ break;
107
+ case "BigInt":
108
+ tsType = "bigint";
109
+ break;
110
+ case "Boolean":
111
+ tsType = "boolean";
112
+ break;
113
+ case "DateTime":
114
+ tsType = "string";
115
+ break;
116
+ case "Json":
117
+ tsType = "unknown";
118
+ break;
119
+ case "Bytes":
120
+ case "Decimal":
121
+ tsType = "string";
122
+ break;
123
+ default:
124
+ tsType = "unknown";
125
+ }
126
+ }
127
+ if (isRequired) {
128
+ return tsType;
129
+ }
130
+ return `${tsType} | null`;
131
+ }
132
+ function buildFrontendModelInfo(dmmf) {
133
+ const models = [];
134
+ for (const model of dmmf.datamodel.models) {
135
+ if (INTERNAL_MODEL_NAMES.has(model.name)) {
136
+ continue;
137
+ }
138
+ if (hasDirective(model.documentation, "hide")) {
139
+ continue;
140
+ }
141
+ const fields = [];
142
+ for (const field of model.fields) {
143
+ if (field.kind !== "scalar" && field.kind !== "enum") {
144
+ continue;
145
+ }
146
+ if (hasDirective(field.documentation, "hide")) {
147
+ continue;
148
+ }
149
+ const isShown = hasDirective(field.documentation, "show");
150
+ if (!isShown && isSensitiveField(field.name)) {
151
+ continue;
152
+ }
153
+ fields.push({
154
+ name: field.name,
155
+ tsType: dmmfTypeToFrontendTsType(
156
+ field.type,
157
+ field.kind,
158
+ field.isRequired
159
+ )
160
+ });
161
+ }
162
+ if (fields.length > 0) {
163
+ models.push({
164
+ modelName: model.name,
165
+ camelName: model.name.charAt(0).toLowerCase() + model.name.slice(1),
166
+ fields
167
+ });
168
+ }
169
+ }
170
+ return models;
171
+ }
172
+ function generateWebGqlormModelsContent(models) {
173
+ if (models.length === 0) {
174
+ return [
175
+ "// Auto-generated by Cedar \u2014 do not edit",
176
+ "// Regenerated on every codegen run. Source: api/db/schema.prisma",
177
+ "",
178
+ "declare module '@cedarjs/gqlorm/types/orm' {",
179
+ " interface GqlormTypeMap {}",
180
+ "}",
181
+ ""
182
+ ].join("\n");
183
+ }
184
+ const lines = [
185
+ "// Auto-generated by Cedar \u2014 do not edit",
186
+ "// Regenerated on every codegen run. Source: api/db/schema.prisma",
187
+ "",
188
+ "declare namespace GqlormScalar {"
189
+ ];
190
+ for (const model of models) {
191
+ lines.push(` interface ${model.modelName} {`);
192
+ for (const field of model.fields) {
193
+ lines.push(` ${field.name}: ${field.tsType}`);
194
+ }
195
+ lines.push(" }");
196
+ lines.push("");
197
+ }
198
+ lines.push("}");
199
+ lines.push("");
200
+ lines.push("declare module '@cedarjs/gqlorm/types/orm' {");
201
+ lines.push(" interface GqlormTypeMap {");
202
+ lines.push(" models: {");
203
+ for (const model of models) {
204
+ lines.push(` ${model.camelName}: GqlormScalar.${model.modelName}`);
205
+ }
206
+ lines.push(" }");
207
+ lines.push(" }");
208
+ lines.push("}");
209
+ lines.push("");
210
+ return lines.join("\n");
211
+ }
25
212
  function buildModelSchema(dmmf) {
26
213
  const schema = {};
27
214
  for (const model of dmmf.datamodel.models) {
@@ -56,7 +243,368 @@ function buildModelSchema(dmmf) {
56
243
  }
57
244
  return schema;
58
245
  }
246
+ function buildBackendModelInfo(dmmf) {
247
+ const models = [];
248
+ for (const model of dmmf.datamodel.models) {
249
+ if (INTERNAL_MODEL_NAMES.has(model.name)) {
250
+ continue;
251
+ }
252
+ if (hasDirective(model.documentation, "hide")) {
253
+ continue;
254
+ }
255
+ const fields = [];
256
+ for (const field of model.fields) {
257
+ if (field.kind !== "scalar" && field.kind !== "enum") {
258
+ continue;
259
+ }
260
+ if (hasDirective(field.documentation, "hide")) {
261
+ continue;
262
+ }
263
+ const isShown = hasDirective(field.documentation, "show");
264
+ if (!isShown && isSensitiveField(field.name)) {
265
+ continue;
266
+ }
267
+ fields.push({
268
+ name: field.name,
269
+ graphqlType: mapDmmfTypeToGraphql(field.type, field.kind),
270
+ isRequired: field.isRequired,
271
+ isId: field.isId
272
+ });
273
+ }
274
+ if (fields.length > 0) {
275
+ const camelName = model.name.charAt(0).toLowerCase() + model.name.slice(1);
276
+ const pluralName = pluralize(camelName);
277
+ models.push({
278
+ modelName: model.name,
279
+ camelName,
280
+ pluralName,
281
+ fields,
282
+ idField: fields.find((f) => f.isId)
283
+ });
284
+ }
285
+ }
286
+ return models;
287
+ }
288
+ const TYPE_DEF_REGEX = /\btype\s+([A-Z]\w*)\s*\{/g;
289
+ const STRUCTURAL_TYPE_NAMES = /* @__PURE__ */ new Set(["Query", "Mutation", "Subscription"]);
290
+ function getExistingSdlTypeNames(graphqlDir) {
291
+ const typeNames = /* @__PURE__ */ new Set();
292
+ if (!fs.existsSync(graphqlDir)) {
293
+ return typeNames;
294
+ }
295
+ const sdlFiles = fs.readdirSync(graphqlDir).filter((file) => {
296
+ return /\.sdl\.(ts|js)$/.test(file) && !file.startsWith("__gqlorm__");
297
+ });
298
+ for (const file of sdlFiles) {
299
+ const content = fs.readFileSync(path.join(graphqlDir, file), "utf-8");
300
+ let match;
301
+ TYPE_DEF_REGEX.lastIndex = 0;
302
+ while ((match = TYPE_DEF_REGEX.exec(content)) !== null) {
303
+ const name = match[1];
304
+ if (!STRUCTURAL_TYPE_NAMES.has(name)) {
305
+ typeNames.add(name);
306
+ }
307
+ }
308
+ }
309
+ return typeNames;
310
+ }
311
+ function generateGqlormBackendContent(models, config = DEFAULT_GQLORM_BACKEND_CONFIG) {
312
+ if (models.length === 0) {
313
+ return "";
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
+ });
328
+ const lines = [
329
+ "// This file is auto-generated by Cedar gqlorm codegen.",
330
+ "// Do not edit \u2014 it will be overwritten on every codegen run.",
331
+ "// To hide a model from gqlorm, add /// @gqlorm hide in schema.prisma.",
332
+ "",
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
+ "}",
342
+ "",
343
+ "// Generated minimal interface \u2014 only visible models and fields, only the",
344
+ "// operations used by this file. No @gqlorm hide models, no sensitive fields.",
345
+ "// Scoped to avoid any dependency on the generated Prisma client path or",
346
+ "// @prisma/client (which is an empty shim in Prisma v7).",
347
+ "interface GqlormDb {"
348
+ ];
349
+ for (const model of models) {
350
+ const selectType = model.fields.map((f) => `${f.name}: true`).join("; ");
351
+ lines.push(` ${model.camelName}: {`);
352
+ lines.push(" findMany(args: {");
353
+ lines.push(" where?: Record<string, unknown>");
354
+ lines.push(` select: Partial<{ ${selectType} }>`);
355
+ lines.push(" }): Promise<");
356
+ lines.push(" Array<{");
357
+ for (const field of model.fields) {
358
+ const tsType = graphqlTypeToTsInterfaceType(
359
+ field.graphqlType,
360
+ field.isRequired
361
+ );
362
+ lines.push(` ${field.name}: ${tsType}`);
363
+ }
364
+ lines.push(" }>");
365
+ lines.push(" >");
366
+ if (model.idField) {
367
+ const idTsType = graphqlTypeToTsType(model.idField.graphqlType);
368
+ lines.push(" findUnique(args: {");
369
+ lines.push(` where: { ${model.idField.name}: ${idTsType} }`);
370
+ lines.push(` select: { ${selectType} }`);
371
+ lines.push(" }): Promise<{");
372
+ for (const field of model.fields) {
373
+ const tsType = graphqlTypeToTsInterfaceType(
374
+ field.graphqlType,
375
+ field.isRequired
376
+ );
377
+ lines.push(` ${field.name}: ${tsType}`);
378
+ }
379
+ lines.push(" } | null>");
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>");
404
+ lines.push(" }");
405
+ }
406
+ lines.push("}");
407
+ lines.push("");
408
+ lines.push("export const schema = gql`");
409
+ for (const model of models) {
410
+ lines.push(` type ${model.modelName} {`);
411
+ for (const field of model.fields) {
412
+ const nullMark = field.isRequired ? "!" : "";
413
+ lines.push(` ${field.name}: ${field.graphqlType}${nullMark}`);
414
+ }
415
+ lines.push(" }");
416
+ lines.push("");
417
+ }
418
+ lines.push(" type Query {");
419
+ for (const model of models) {
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
+ );
432
+ if (model.idField) {
433
+ const idNullMark = model.idField.isRequired ? "!" : "";
434
+ lines.push(
435
+ ` ${model.camelName}(${model.idField.name}: ${model.idField.graphqlType}${idNullMark}): ${model.modelName} ${authDirective}`
436
+ );
437
+ }
438
+ }
439
+ lines.push(" }");
440
+ lines.push("`");
441
+ lines.push("");
442
+ lines.push(
443
+ "// db is passed in from graphql.ts by the Babel inject plugin, which imports it"
444
+ );
445
+ lines.push(
446
+ "// from 'src/lib/db' in a context where that alias resolves correctly."
447
+ );
448
+ lines.push("export function createGqlormResolvers(db: GqlormDb) {");
449
+ lines.push(" return {");
450
+ lines.push(" Query: {");
451
+ for (let i = 0; i < models.length; i++) {
452
+ const model = models[i];
453
+ const selectObj = model.fields.map((f) => `${f.name}: true`).join(", ");
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
+ }
514
+ lines.push(" },");
515
+ if (model.idField) {
516
+ const idFieldName = model.idField.name;
517
+ const tsType = graphqlTypeToTsType(model.idField.graphqlType);
518
+ lines.push(
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({`
539
+ );
540
+ lines.push(` where: { ${idFieldName} },`);
541
+ lines.push(` select: { ${selectObj} },`);
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");
581
+ lines.push(" },");
582
+ }
583
+ if (i < models.length - 1) {
584
+ lines.push("");
585
+ }
586
+ }
587
+ lines.push(" },");
588
+ lines.push(" }");
589
+ lines.push("}");
590
+ lines.push("");
591
+ return lines.join("\n");
592
+ }
59
593
  async function generateGqlormArtifacts() {
594
+ if (!getConfig().experimental?.gqlorm?.enabled) {
595
+ const generatedBase = getPaths().generated.base;
596
+ const staleFiles = [
597
+ path.join(generatedBase, "gqlorm", "backend.ts"),
598
+ path.join(generatedBase, "gqlorm-schema.json"),
599
+ path.join(generatedBase, "types", "includes", "web-gqlorm-models.d.ts")
600
+ ];
601
+ for (const staleFile of staleFiles) {
602
+ if (fs.existsSync(staleFile)) {
603
+ fs.unlinkSync(staleFile);
604
+ }
605
+ }
606
+ return { files: [], errors: [] };
607
+ }
60
608
  const files = [];
61
609
  const errors = [];
62
610
  try {
@@ -64,14 +612,70 @@ async function generateGqlormArtifacts() {
64
612
  const mod = await import("@prisma/internals");
65
613
  const { getDMMF } = mod.default || mod;
66
614
  const dmmf = await getDMMF({ datamodel: schemas });
615
+ const paths = getPaths();
616
+ const generatedBase = paths.generated.base;
67
617
  const modelSchema = buildModelSchema(dmmf);
68
- const outputPath = path.join(
69
- getPaths().generated.base,
70
- "gqlorm-schema.json"
618
+ const frontendModels = buildFrontendModelInfo(dmmf);
619
+ const schemaOutputPath = path.join(generatedBase, "gqlorm-schema.json");
620
+ const webTypesOutputPath = path.join(
621
+ generatedBase,
622
+ "types",
623
+ "includes",
624
+ "web-gqlorm-models.d.ts"
625
+ );
626
+ fs.mkdirSync(path.dirname(schemaOutputPath), { recursive: true });
627
+ fs.writeFileSync(schemaOutputPath, JSON.stringify(modelSchema, null, 2));
628
+ files.push(schemaOutputPath);
629
+ fs.mkdirSync(path.dirname(webTypesOutputPath), { recursive: true });
630
+ fs.writeFileSync(
631
+ webTypesOutputPath,
632
+ generateWebGqlormModelsContent(frontendModels)
71
633
  );
72
- fs.mkdirSync(path.dirname(outputPath), { recursive: true });
73
- fs.writeFileSync(outputPath, JSON.stringify(modelSchema, null, 2));
74
- files.push(outputPath);
634
+ files.push(webTypesOutputPath);
635
+ const backendOutputDir = path.join(generatedBase, "gqlorm");
636
+ const backendOutputPath = path.join(backendOutputDir, "backend.ts");
637
+ const graphqlDir = paths.api.graphql;
638
+ const existingTypes = getExistingSdlTypeNames(graphqlDir);
639
+ const allModels = buildBackendModelInfo(dmmf);
640
+ const gqlormConfig = getConfig().experimental.gqlorm;
641
+ const membershipModel = gqlormConfig.membershipModel ?? "Membership";
642
+ const membershipModelCamel = membershipModel.charAt(0).toLowerCase() + membershipModel.slice(1);
643
+ const membershipUserField = gqlormConfig.membershipUserField ?? "userId";
644
+ const membershipOrganizationField = gqlormConfig.membershipOrganizationField ?? "organizationId";
645
+ const membershipModelExists = dmmf.datamodel.models.some(
646
+ (m) => m.name === membershipModel
647
+ );
648
+ const backendConfig = {
649
+ membershipModel,
650
+ membershipModelCamel,
651
+ membershipUserField,
652
+ membershipOrganizationField,
653
+ membershipModelExists
654
+ };
655
+ const gqlormModels = allModels.filter(
656
+ (m) => !existingTypes.has(m.modelName)
657
+ );
658
+ const anyModelHasOrgField = gqlormModels.some(
659
+ (m) => m.fields.some((f) => f.name === membershipOrganizationField)
660
+ );
661
+ if (anyModelHasOrgField && !membershipModelExists) {
662
+ console.warn(
663
+ `[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.`
664
+ );
665
+ }
666
+ if (gqlormModels.length > 0) {
667
+ const backendContent = generateGqlormBackendContent(
668
+ gqlormModels,
669
+ backendConfig
670
+ );
671
+ fs.mkdirSync(backendOutputDir, { recursive: true });
672
+ fs.writeFileSync(backendOutputPath, backendContent);
673
+ files.push(backendOutputPath);
674
+ } else {
675
+ if (fs.existsSync(backendOutputPath)) {
676
+ fs.unlinkSync(backendOutputPath);
677
+ }
678
+ }
75
679
  } catch (error) {
76
680
  errors.push({
77
681
  message: "Failed to generate gqlorm schema artifacts",
@@ -81,6 +685,12 @@ async function generateGqlormArtifacts() {
81
685
  return { files, errors };
82
686
  }
83
687
  export {
688
+ buildBackendModelInfo,
689
+ buildFrontendModelInfo,
84
690
  buildModelSchema,
85
- generateGqlormArtifacts
691
+ generateGqlormArtifacts,
692
+ generateGqlormBackendContent,
693
+ generateWebGqlormModelsContent,
694
+ getExistingSdlTypeNames,
695
+ mapDmmfTypeToGraphql
86
696
  };