@eide/foir-cli 0.1.42 → 0.1.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,8 +1,12 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ findConfigFile,
4
+ loadConfigFile
5
+ } from "./chunk-L642MYIL.js";
2
6
 
3
7
  // src/cli.ts
4
8
  import { config } from "dotenv";
5
- import { resolve as resolve7, dirname as dirname6 } from "path";
9
+ import { resolve as resolve6, dirname as dirname6 } from "path";
6
10
  import { fileURLToPath as fileURLToPath2 } from "url";
7
11
  import { createRequire } from "module";
8
12
  import { Command } from "commander";
@@ -44,8 +48,8 @@ async function getCredentials() {
44
48
  }
45
49
  async function writeCredentials(credentials) {
46
50
  await ensureDir(getCredentialsDir());
47
- const path4 = getCredentialsPath();
48
- await fs.writeFile(path4, JSON.stringify(credentials, null, 2), {
51
+ const path3 = getCredentialsPath();
52
+ await fs.writeFile(path3, JSON.stringify(credentials, null, 2), {
49
53
  mode: 384
50
54
  });
51
55
  }
@@ -95,9 +99,19 @@ function getProjectContextDir() {
95
99
  function getProjectContextPath() {
96
100
  return join(getProjectContextDir(), "project.json");
97
101
  }
98
- async function getProjectContext() {
102
+ function getProfilesDir() {
103
+ return join(getProjectContextDir(), "profiles");
104
+ }
105
+ function getProfilePath(name) {
106
+ return join(getProfilesDir(), `${name}.json`);
107
+ }
108
+ function getDefaultProfilePath() {
109
+ return join(getProjectContextDir(), "default-profile");
110
+ }
111
+ async function getProjectContext(profileName) {
112
+ const filePath = profileName ? getProfilePath(profileName) : getProjectContextPath();
99
113
  try {
100
- const content = await fs.readFile(getProjectContextPath(), "utf-8");
114
+ const content = await fs.readFile(filePath, "utf-8");
101
115
  return JSON.parse(content);
102
116
  } catch (error) {
103
117
  if (error.code === "ENOENT") {
@@ -106,13 +120,135 @@ async function getProjectContext() {
106
120
  throw error;
107
121
  }
108
122
  }
109
- async function writeProjectContext(project) {
110
- const dir = getProjectContextDir();
111
- await ensureDir(dir);
112
- const filePath = getProjectContextPath();
113
- await fs.writeFile(filePath, JSON.stringify(project, null, 2), {
114
- mode: 384
115
- });
123
+ async function writeProjectContext(project, profileName) {
124
+ if (profileName) {
125
+ await ensureDir(getProfilesDir());
126
+ const filePath = getProfilePath(profileName);
127
+ await fs.writeFile(filePath, JSON.stringify(project, null, 2), {
128
+ mode: 384
129
+ });
130
+ } else {
131
+ const dir = getProjectContextDir();
132
+ await ensureDir(dir);
133
+ const filePath = getProjectContextPath();
134
+ await fs.writeFile(filePath, JSON.stringify(project, null, 2), {
135
+ mode: 384
136
+ });
137
+ }
138
+ }
139
+ async function listProfiles() {
140
+ const dir = getProfilesDir();
141
+ let files;
142
+ try {
143
+ files = await fs.readdir(dir);
144
+ } catch (error) {
145
+ if (error.code === "ENOENT") {
146
+ return [];
147
+ }
148
+ throw error;
149
+ }
150
+ const profiles = [];
151
+ for (const file of files) {
152
+ if (!file.endsWith(".json")) continue;
153
+ const name = file.replace(/\.json$/, "");
154
+ try {
155
+ const content = await fs.readFile(join(dir, file), "utf-8");
156
+ profiles.push({ name, project: JSON.parse(content) });
157
+ } catch {
158
+ }
159
+ }
160
+ return profiles;
161
+ }
162
+ async function getDefaultProfile() {
163
+ try {
164
+ const content = await fs.readFile(getDefaultProfilePath(), "utf-8");
165
+ return content.trim() || null;
166
+ } catch (error) {
167
+ if (error.code === "ENOENT") {
168
+ return null;
169
+ }
170
+ throw error;
171
+ }
172
+ }
173
+ async function setDefaultProfile(name) {
174
+ await ensureDir(getProjectContextDir());
175
+ await fs.writeFile(getDefaultProfilePath(), name, { mode: 384 });
176
+ }
177
+ async function deleteProfile(name) {
178
+ try {
179
+ await fs.unlink(getProfilePath(name));
180
+ } catch (error) {
181
+ if (error.code !== "ENOENT") {
182
+ throw error;
183
+ }
184
+ }
185
+ const defaultProfile = await getDefaultProfile();
186
+ if (defaultProfile === name) {
187
+ try {
188
+ await fs.unlink(getDefaultProfilePath());
189
+ } catch (error) {
190
+ if (error.code !== "ENOENT") {
191
+ throw error;
192
+ }
193
+ }
194
+ }
195
+ }
196
+ async function resolveProjectContext(options) {
197
+ if (options?.project) {
198
+ const project2 = await getProjectContext(options.project);
199
+ if (project2) {
200
+ return {
201
+ project: project2,
202
+ source: "--project flag",
203
+ profileName: options.project
204
+ };
205
+ }
206
+ throw new Error(
207
+ `Profile "${options.project}" not found. Run \`foir profiles list\` to see available profiles.`
208
+ );
209
+ }
210
+ const envProject = process.env.FOIR_PROJECT;
211
+ if (envProject) {
212
+ const project2 = await getProjectContext(envProject);
213
+ if (project2) {
214
+ return {
215
+ project: project2,
216
+ source: "FOIR_PROJECT env",
217
+ profileName: envProject
218
+ };
219
+ }
220
+ }
221
+ try {
222
+ const { loadConfigProject } = await import("./loader-7VE4OF73.js");
223
+ const configProfile = await loadConfigProject();
224
+ if (configProfile) {
225
+ const project2 = await getProjectContext(configProfile);
226
+ if (project2) {
227
+ return {
228
+ project: project2,
229
+ source: "foir.config.ts",
230
+ profileName: configProfile
231
+ };
232
+ }
233
+ }
234
+ } catch {
235
+ }
236
+ const defaultProfile = await getDefaultProfile();
237
+ if (defaultProfile) {
238
+ const project2 = await getProjectContext(defaultProfile);
239
+ if (project2) {
240
+ return {
241
+ project: project2,
242
+ source: "default profile",
243
+ profileName: defaultProfile
244
+ };
245
+ }
246
+ }
247
+ const project = await getProjectContext();
248
+ if (project) {
249
+ return { project, source: "project.json" };
250
+ }
251
+ return null;
116
252
  }
117
253
 
118
254
  // src/lib/config.ts
@@ -226,13 +362,13 @@ function withErrorHandler(optsFn, fn) {
226
362
  // src/commands/login.ts
227
363
  async function findAvailablePort(start, end) {
228
364
  for (let port = start; port <= end; port++) {
229
- const available = await new Promise((resolve8) => {
365
+ const available = await new Promise((resolve7) => {
230
366
  const server = http.createServer();
231
367
  server.listen(port, () => {
232
368
  server.close();
233
- resolve8(true);
369
+ resolve7(true);
234
370
  });
235
- server.on("error", () => resolve8(false));
371
+ server.on("error", () => resolve7(false));
236
372
  });
237
373
  if (available) return port;
238
374
  }
@@ -270,7 +406,7 @@ async function loginAction(globalOpts) {
270
406
  const state = crypto.randomBytes(16).toString("hex");
271
407
  const port = await findAvailablePort(9876, 9900);
272
408
  const redirectUri = `http://localhost:${port}/callback`;
273
- const authCode = await new Promise((resolve8, reject) => {
409
+ const authCode = await new Promise((resolve7, reject) => {
274
410
  const server = http.createServer((req, res) => {
275
411
  const url = new URL(req.url, `http://localhost:${port}`);
276
412
  if (url.pathname === "/callback") {
@@ -303,7 +439,7 @@ async function loginAction(globalOpts) {
303
439
  `<html><head><meta http-equiv="refresh" content="2;url=${mainUrl}"></head><body style="font-family:system-ui;text-align:center;padding:50px"><h1>Authentication successful!</h1><p>You can close this window.</p></body></html>`
304
440
  );
305
441
  server.close();
306
- resolve8(code);
442
+ resolve7(code);
307
443
  }
308
444
  });
309
445
  server.listen(port);
@@ -469,87 +605,99 @@ async function provisionApiKey(apiUrl, accessToken, projectId, tenantId) {
469
605
  return { apiKey: created.plainKey, apiKeyId: created.apiKey.id };
470
606
  }
471
607
  function registerSelectProjectCommand(program2, globalOpts) {
472
- program2.command("select-project").description("Choose which project to work with").option("--project-id <id>", "Project ID to select directly").action(
473
- withErrorHandler(globalOpts, async (cmdOpts) => {
474
- const opts = globalOpts();
475
- const apiUrl = getApiUrl(opts);
476
- const credentials = await getCredentials();
477
- if (!credentials) {
478
- console.log("Not logged in. Run `foir login` first.");
479
- throw new Error("Not authenticated");
480
- }
481
- console.log("Fetching your projects...\n");
482
- const sessionContext = await fetchSessionContext(
483
- apiUrl,
484
- credentials.accessToken
485
- );
486
- const { availableTenants: tenants, availableProjects: projects } = sessionContext;
487
- if (projects.length === 0) {
488
- console.log("No projects found. Create one in the platform first.");
489
- throw new Error("No projects available");
490
- }
491
- const tenantNameMap = new Map(tenants.map((t) => [t.id, t.name]));
492
- let selectedProject;
493
- if (cmdOpts.projectId) {
494
- const found = projects.find((p) => p.id === cmdOpts.projectId);
495
- if (!found) {
496
- console.log(`Project with ID "${cmdOpts.projectId}" not found.`);
497
- console.log("Available projects:");
498
- for (const p of projects) {
499
- console.log(` - ${p.name} (${p.id})`);
500
- }
501
- throw new Error("Project not found");
608
+ program2.command("select-project").description("Choose which project to work with").option("--project-id <id>", "Project ID to select directly").option("--save-as <name>", "Save as a named profile").action(
609
+ withErrorHandler(
610
+ globalOpts,
611
+ async (cmdOpts) => {
612
+ const opts = globalOpts();
613
+ const apiUrl = getApiUrl(opts);
614
+ const credentials = await getCredentials();
615
+ if (!credentials) {
616
+ console.log("Not logged in. Run `foir login` first.");
617
+ throw new Error("Not authenticated");
502
618
  }
503
- selectedProject = found;
504
- } else {
505
- const byTenant = projects.reduce(
506
- (acc, p) => {
507
- const key = tenantNameMap.get(p.tenantId) ?? "Unknown";
508
- if (!acc[key]) acc[key] = [];
509
- acc[key].push(p);
510
- return acc;
511
- },
512
- {}
619
+ console.log("Fetching your projects...\n");
620
+ const sessionContext = await fetchSessionContext(
621
+ apiUrl,
622
+ credentials.accessToken
513
623
  );
514
- const choices = Object.entries(byTenant).flatMap(
515
- ([tenantName, tenantProjects]) => [
516
- new inquirer.Separator(`\u2500\u2500 ${tenantName} \u2500\u2500`),
517
- ...tenantProjects.map((p) => ({
518
- name: ` ${p.name}`,
519
- value: p.id,
520
- short: p.name
521
- }))
522
- ]
624
+ const { availableTenants: tenants, availableProjects: projects } = sessionContext;
625
+ if (projects.length === 0) {
626
+ console.log("No projects found. Create one in the platform first.");
627
+ throw new Error("No projects available");
628
+ }
629
+ const tenantNameMap = new Map(tenants.map((t) => [t.id, t.name]));
630
+ let selectedProject;
631
+ if (cmdOpts.projectId) {
632
+ const found = projects.find((p) => p.id === cmdOpts.projectId);
633
+ if (!found) {
634
+ console.log(`Project with ID "${cmdOpts.projectId}" not found.`);
635
+ console.log("Available projects:");
636
+ for (const p of projects) {
637
+ console.log(` - ${p.name} (${p.id})`);
638
+ }
639
+ throw new Error("Project not found");
640
+ }
641
+ selectedProject = found;
642
+ } else {
643
+ const byTenant = projects.reduce(
644
+ (acc, p) => {
645
+ const key = tenantNameMap.get(p.tenantId) ?? "Unknown";
646
+ if (!acc[key]) acc[key] = [];
647
+ acc[key].push(p);
648
+ return acc;
649
+ },
650
+ {}
651
+ );
652
+ const choices = Object.entries(byTenant).flatMap(
653
+ ([tenantName, tenantProjects]) => [
654
+ new inquirer.Separator(`\u2500\u2500 ${tenantName} \u2500\u2500`),
655
+ ...tenantProjects.map((p) => ({
656
+ name: ` ${p.name}`,
657
+ value: p.id,
658
+ short: p.name
659
+ }))
660
+ ]
661
+ );
662
+ const { projectId } = await inquirer.prompt([
663
+ {
664
+ type: "list",
665
+ name: "projectId",
666
+ message: "Select a project:",
667
+ choices
668
+ }
669
+ ]);
670
+ selectedProject = projects.find((p) => p.id === projectId);
671
+ }
672
+ console.log("\nProvisioning API key for CLI access...");
673
+ const { apiKey, apiKeyId } = await provisionApiKey(
674
+ apiUrl,
675
+ credentials.accessToken,
676
+ selectedProject.id,
677
+ selectedProject.tenantId
523
678
  );
524
- const { projectId } = await inquirer.prompt([
679
+ await writeProjectContext(
525
680
  {
526
- type: "list",
527
- name: "projectId",
528
- message: "Select a project:",
529
- choices
530
- }
531
- ]);
532
- selectedProject = projects.find((p) => p.id === projectId);
533
- }
534
- console.log("\nProvisioning API key for CLI access...");
535
- const { apiKey, apiKeyId } = await provisionApiKey(
536
- apiUrl,
537
- credentials.accessToken,
538
- selectedProject.id,
539
- selectedProject.tenantId
540
- );
541
- await writeProjectContext({
542
- id: selectedProject.id,
543
- name: selectedProject.name,
544
- tenantId: selectedProject.tenantId,
545
- apiKey,
546
- apiKeyId
547
- });
548
- console.log(`
681
+ id: selectedProject.id,
682
+ name: selectedProject.name,
683
+ tenantId: selectedProject.tenantId,
684
+ apiKey,
685
+ apiKeyId
686
+ },
687
+ cmdOpts.saveAs
688
+ );
689
+ console.log(`
549
690
  \u2713 Selected project: ${selectedProject.name}`);
550
- console.log("\u2713 API key provisioned for CLI access");
551
- console.log(" (stored in .foir/project.json for this repository)");
552
- })
691
+ console.log("\u2713 API key provisioned for CLI access");
692
+ if (cmdOpts.saveAs) {
693
+ console.log(
694
+ ` Saved as profile "${cmdOpts.saveAs}". Use --project ${cmdOpts.saveAs} or set as default with \`foir profiles default ${cmdOpts.saveAs}\``
695
+ );
696
+ } else {
697
+ console.log(" (stored in .foir/project.json for this repository)");
698
+ }
699
+ }
700
+ )
553
701
  );
554
702
  }
555
703
 
@@ -653,14 +801,18 @@ function registerWhoamiCommand(program2, globalOpts) {
653
801
  throw new Error("Not authenticated");
654
802
  }
655
803
  const expired = isTokenExpired(credentials);
656
- const projectContext = await getProjectContext();
804
+ const resolved = await resolveProjectContext(opts);
657
805
  if (opts.json || opts.jsonl) {
658
806
  formatOutput(
659
807
  {
660
808
  authenticated: true,
661
809
  tokenValid: !expired,
662
810
  user: credentials.user,
663
- selectedProject: projectContext ?? null
811
+ selectedProject: resolved ? {
812
+ ...resolved.project,
813
+ profile: resolved.profileName ?? null,
814
+ source: resolved.source
815
+ } : null
664
816
  },
665
817
  opts
666
818
  );
@@ -673,13 +825,18 @@ function registerWhoamiCommand(program2, globalOpts) {
673
825
  );
674
826
  console.log(`User ID: ${credentials.user.id}`);
675
827
  console.log(`Token: ${expired ? "\u26A0 Expired" : "\u2713 Valid"}`);
676
- if (projectContext) {
828
+ if (resolved) {
677
829
  console.log("");
678
830
  console.log("Selected Project (this repo)");
679
831
  console.log("\u2500".repeat(40));
680
- console.log(`Name: ${projectContext.name}`);
681
- console.log(`ID: ${projectContext.id}`);
682
- console.log(`Tenant ID: ${projectContext.tenantId}`);
832
+ console.log(`Name: ${resolved.project.name}`);
833
+ console.log(`ID: ${resolved.project.id}`);
834
+ console.log(`Tenant ID: ${resolved.project.tenantId}`);
835
+ if (resolved.profileName) {
836
+ console.log(
837
+ `Profile: ${resolved.profileName} (from ${resolved.source})`
838
+ );
839
+ }
683
840
  } else {
684
841
  console.log("");
685
842
  console.log("No project selected for this repository.");
@@ -715,10 +872,10 @@ async function createClient(options) {
715
872
  throw new Error("Session expired. Run `foir login` to re-authenticate.");
716
873
  }
717
874
  headers["Authorization"] = `Bearer ${credentials.accessToken}`;
718
- const project = await getProjectContext();
719
- if (project) {
720
- headers["x-tenant-id"] = project.tenantId;
721
- headers["x-project-id"] = project.id;
875
+ const resolved = await resolveProjectContext(options);
876
+ if (resolved) {
877
+ headers["x-tenant-id"] = resolved.project.tenantId;
878
+ headers["x-project-id"] = resolved.project.id;
722
879
  }
723
880
  return new GraphQLClient(endpoint, { headers });
724
881
  }
@@ -738,10 +895,10 @@ async function getRestAuth(options) {
738
895
  throw new Error("Session expired. Run `foir login` to re-authenticate.");
739
896
  }
740
897
  headers["Authorization"] = `Bearer ${credentials.accessToken}`;
741
- const project = await getProjectContext();
742
- if (project) {
743
- headers["x-tenant-id"] = project.tenantId;
744
- headers["x-project-id"] = project.id;
898
+ const resolved = await resolveProjectContext(options);
899
+ if (resolved) {
900
+ headers["x-tenant-id"] = resolved.project.tenantId;
901
+ headers["x-project-id"] = resolved.project.id;
745
902
  }
746
903
  return { apiUrl, headers };
747
904
  }
@@ -784,54 +941,11 @@ function registerMediaCommands(program2, globalOpts) {
784
941
  }
785
942
 
786
943
  // src/commands/pull.ts
787
- import { resolve as resolve2 } from "path";
944
+ import { resolve } from "path";
788
945
  import chalk4 from "chalk";
789
946
 
790
947
  // src/config/pull-config.ts
791
- import { resolve } from "path";
792
- import { pathToFileURL } from "url";
793
- import { existsSync } from "fs";
794
- var CONFIG_FILE_NAMES = [
795
- "foir.config.ts",
796
- "foir.config.js",
797
- "foir.config.mjs",
798
- ".foirrc.ts",
799
- ".foirrc.js",
800
- ".foirrc.mjs"
801
- ];
802
- var DEFAULT_TYPES_DIR = "./src/generated/types";
803
- var DEFAULT_DOCS_DIR = "./src/generated/documents";
804
- var ALL_DOMAINS = {
805
- auth: true,
806
- authProviders: true,
807
- files: true,
808
- sync: true,
809
- notifications: true,
810
- operations: true,
811
- schedules: true,
812
- sharing: true,
813
- embeddings: true,
814
- analytics: true
815
- };
816
- function findConfigFile(explicitPath) {
817
- if (explicitPath) {
818
- const abs = resolve(explicitPath);
819
- if (existsSync(abs)) return abs;
820
- throw new Error(`Config file not found: ${explicitPath}`);
821
- }
822
- const cwd = process.cwd();
823
- for (const name of CONFIG_FILE_NAMES) {
824
- const candidate = resolve(cwd, name);
825
- if (existsSync(candidate)) return candidate;
826
- }
827
- return null;
828
- }
829
- async function loadConfigFile(filePath) {
830
- const url = pathToFileURL(filePath).href;
831
- const mod = await import(url);
832
- const config2 = mod.default ?? mod;
833
- return config2;
834
- }
948
+ var DEFAULT_OUTPUT_DIR = "./src/generated";
835
949
  async function loadPullConfig(flags) {
836
950
  let fileConfig = {};
837
951
  const configPath = findConfigFile(flags.config);
@@ -839,46 +953,28 @@ async function loadPullConfig(flags) {
839
953
  const full = await loadConfigFile(configPath);
840
954
  fileConfig = full.pull ?? {};
841
955
  }
842
- const types = flags.out ?? fileConfig.output?.types ?? DEFAULT_TYPES_DIR;
843
- const documents = fileConfig.output?.documents ?? DEFAULT_DOCS_DIR;
844
- const swift = flags.swift ?? fileConfig.output?.swift;
845
- const targets = fileConfig.targets ?? [];
846
- const typesParent = types.replace(/\/[^/]+$/, "");
847
- const operations = fileConfig.output?.operations ?? `${typesParent}/operations`;
848
- const hooks = targets.includes("react") ? fileConfig.output?.hooks ?? `${typesParent}/hooks` : void 0;
849
- const loaders = targets.includes("remix") ? fileConfig.output?.loaders ?? `${typesParent}/loaders` : void 0;
850
- const output = {
851
- types,
852
- documents,
853
- operations,
854
- ...hooks ? { hooks } : {},
855
- ...loaders ? { loaders } : {},
856
- ...swift ? { swift } : {}
857
- };
858
- let domains;
859
- if (fileConfig.domains === false) {
860
- domains = {
861
- auth: false,
862
- authProviders: false,
863
- files: false,
864
- sync: false,
865
- notifications: false,
866
- operations: false,
867
- schedules: false,
868
- sharing: false,
869
- embeddings: false,
870
- analytics: false
871
- };
872
- } else if (typeof fileConfig.domains === "object") {
873
- domains = { ...ALL_DOMAINS, ...fileConfig.domains };
956
+ let outputDir;
957
+ if (flags.out) {
958
+ outputDir = flags.out;
959
+ } else if (typeof fileConfig.output === "string") {
960
+ outputDir = fileConfig.output;
961
+ } else if (typeof fileConfig.output === "object" && fileConfig.output?.types) {
962
+ const legacyTypes = fileConfig.output.types;
963
+ outputDir = legacyTypes.replace(/\/types\/?$/, "") || DEFAULT_OUTPUT_DIR;
874
964
  } else {
875
- domains = { ...ALL_DOMAINS };
965
+ outputDir = DEFAULT_OUTPUT_DIR;
876
966
  }
877
967
  const only = flags.only ? flags.only.split(",").map((s) => s.trim()) : fileConfig.only ?? [];
878
968
  const includeInline = fileConfig.includeInline ?? true;
879
969
  const prettier = flags.noPrettier ? false : fileConfig.prettier ?? true;
880
970
  const dryRun = flags.dryRun ?? false;
881
- return { output, targets, domains, only, includeInline, prettier, dryRun };
971
+ return {
972
+ output: { types: outputDir },
973
+ only,
974
+ includeInline,
975
+ prettier,
976
+ dryRun
977
+ };
882
978
  }
883
979
 
884
980
  // src/graphql/generated.ts
@@ -1480,10 +1576,6 @@ function toPascalCase(str) {
1480
1576
  const camel = toCamelCase(str);
1481
1577
  return camel.charAt(0).toUpperCase() + camel.slice(1);
1482
1578
  }
1483
- function toUpperSnakeCase(str) {
1484
- if (!str) return "UNKNOWN";
1485
- return str.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toUpperCase();
1486
- }
1487
1579
  function sanitizeFieldName(key) {
1488
1580
  if (!key) return "unknown";
1489
1581
  const camel = toCamelCase(key);
@@ -1545,542 +1637,107 @@ function generateFieldDef(field) {
1545
1637
  return `{ ${parts.join(", ")} }`;
1546
1638
  }
1547
1639
 
1548
- // src/codegen/generators/field-types.ts
1549
- function generateFieldTypesFile() {
1550
- return `/**
1551
- * Field Types and Definitions
1552
- *
1553
- * Value types, filter types, and field definition types.
1554
- *
1555
- * @generated by foir \u2014 DO NOT EDIT MANUALLY
1556
- */
1557
-
1558
- // =============================================================================
1559
- // JSON-SAFE BASE TYPE
1560
- // =============================================================================
1561
-
1562
- /** Recursive JSON-serializable value type. Safe for TanStack Router, Remix, RSC, and other serialization boundaries. */
1563
- export type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue };
1564
-
1565
- // =============================================================================
1566
- // VALUE TYPES
1567
- // =============================================================================
1568
-
1569
- /** Rich text content (Lexical JSON format) */
1570
- export type RichtextValue = JsonValue;
1571
-
1572
- /** Currency value with amount and ISO 4217 code */
1573
- export interface CurrencyValue {
1574
- amount: number;
1575
- currency: string;
1576
- }
1577
-
1578
- /** Image reference with metadata */
1579
- export interface ImageValue {
1580
- id: string;
1581
- url: string;
1582
- alt?: string;
1583
- width?: number;
1584
- height?: number;
1585
- }
1586
-
1587
- /** Video reference with metadata */
1588
- export interface VideoValue {
1589
- id: string;
1590
- url: string;
1591
- thumbnail?: string;
1592
- duration?: number;
1593
- }
1594
-
1595
- /** File reference with metadata */
1596
- export interface FileValue {
1597
- id: string;
1598
- url: string;
1599
- name: string;
1600
- size: number;
1601
- mimeType: string;
1602
- }
1603
-
1604
- /** Link value (internal reference or external URL) */
1605
- export interface LinkValue {
1606
- type: 'entity' | 'url';
1607
- entity?: LinkRecordReference;
1608
- url?: string;
1609
- target?: '_self' | '_blank';
1610
- }
1611
-
1612
- /** Record reference for internal links */
1613
- export interface LinkRecordReference {
1614
- modelKey: string;
1615
- naturalKey: string;
1616
- }
1617
-
1618
- /** Record reference value (generic TPreview for typed preview data when reference target is known) */
1619
- export interface ReferenceValue<TPreview = Record<string, JsonValue>> {
1620
- _type: 'reference';
1621
- _schema: string;
1622
- naturalKey: string;
1623
- _preview?: TPreview;
1640
+ // src/codegen/generators/model-types.ts
1641
+ function isInlineOnlyModel(model) {
1642
+ return model.config.inline && !model.config.records;
1624
1643
  }
1625
-
1626
- /** Composite/inline value */
1627
- export interface CompositeValue {
1628
- _type: 'composite';
1629
- _schema: string;
1630
- fields: Record<string, JsonValue>;
1644
+ function generateModelTypes(model, allModels) {
1645
+ const typeName = toPascalCase(model.key);
1646
+ const configName = toCamelCase(model.key) + "Config";
1647
+ const fields = model.fields ?? [];
1648
+ const fieldTypeImports = getFieldTypeImportsForFields(fields);
1649
+ const inlineSchemaRefs = getInlineSchemaReferences(fields);
1650
+ const referenceModelRefs = getReferenceTypeModelRefs(fields);
1651
+ let code = buildImportStatements(
1652
+ model,
1653
+ fieldTypeImports,
1654
+ inlineSchemaRefs,
1655
+ referenceModelRefs,
1656
+ allModels
1657
+ );
1658
+ if (isInlineOnlyModel(model)) {
1659
+ code += generateDataInterface(model, fields, typeName, allModels);
1660
+ return code;
1661
+ }
1662
+ code += generateConfigObject(model, fields, configName);
1663
+ code += "\n";
1664
+ code += generateDataInterface(model, fields, typeName + "Data", allModels);
1665
+ return code;
1631
1666
  }
1632
-
1633
- /** A single item in a flexible field array */
1634
- export interface FlexibleFieldItem {
1635
- _id: string;
1636
- _key: string;
1637
- _type: string;
1638
- _label: string;
1639
- _required?: boolean;
1640
- _helpText?: string;
1641
- _config?: Record<string, JsonValue>;
1642
- value: JsonValue;
1667
+ function buildImportStatements(model, fieldTypeImports, inlineSchemaRefs, referenceModelRefs, allModels) {
1668
+ const imports = [];
1669
+ if (fieldTypeImports.size > 0) {
1670
+ const types = Array.from(fieldTypeImports).sort().join(", ");
1671
+ imports.push(`import type { ${types} } from '@eide/foir-client';`);
1672
+ }
1673
+ const allModelRefKeys = /* @__PURE__ */ new Set([
1674
+ ...inlineSchemaRefs,
1675
+ ...referenceModelRefs
1676
+ ]);
1677
+ for (const refKey of allModelRefKeys) {
1678
+ if (refKey === model.key) continue;
1679
+ const refModel = allModels.find((m) => m.key === refKey);
1680
+ if (refModel) {
1681
+ const refTypeName = isInlineOnlyModel(refModel) ? toPascalCase(refKey) : toPascalCase(refKey) + "Data";
1682
+ imports.push(`import type { ${refTypeName} } from './${refKey}.js';`);
1683
+ }
1684
+ }
1685
+ return imports.length > 0 ? imports.join("\n") + "\n\n" : "";
1643
1686
  }
1644
-
1645
- // =============================================================================
1646
- // FILTER TYPES
1647
- // =============================================================================
1648
-
1649
- export interface TextFilter {
1650
- eq?: string;
1651
- ne?: string;
1652
- contains?: string;
1653
- startsWith?: string;
1654
- endsWith?: string;
1655
- in?: string[];
1656
- notIn?: string[];
1657
- isNull?: boolean;
1658
- }
1659
-
1660
- export interface NumberFilter {
1661
- eq?: number;
1662
- ne?: number;
1663
- gt?: number;
1664
- gte?: number;
1665
- lt?: number;
1666
- lte?: number;
1667
- in?: number[];
1668
- notIn?: number[];
1669
- isNull?: boolean;
1670
- }
1671
-
1672
- export interface BooleanFilter {
1673
- eq?: boolean;
1674
- ne?: boolean;
1675
- isNull?: boolean;
1676
- }
1677
-
1678
- export interface DateFilter {
1679
- eq?: string;
1680
- ne?: string;
1681
- gt?: string;
1682
- gte?: string;
1683
- lt?: string;
1684
- lte?: string;
1685
- isNull?: boolean;
1686
- }
1687
-
1688
- export interface SelectFilter<T extends string = string> {
1689
- eq?: T;
1690
- ne?: T;
1691
- in?: T[];
1692
- notIn?: T[];
1693
- isNull?: boolean;
1694
- }
1695
-
1696
- export interface MultiselectFilter<T extends string = string> {
1697
- contains?: T;
1698
- containsAny?: T[];
1699
- containsAll?: T[];
1700
- isNull?: boolean;
1701
- }
1702
-
1703
- export interface ReferenceFilter {
1704
- eq?: string;
1705
- ne?: string;
1706
- in?: string[];
1707
- notIn?: string[];
1708
- isNull?: boolean;
1709
- }
1710
-
1711
- export interface FilterInput {
1712
- field: string;
1713
- operator: string;
1714
- value: JsonValue;
1715
- }
1716
-
1717
- export interface SortInput {
1718
- field: string;
1719
- direction: 'ASC' | 'DESC';
1720
- }
1721
-
1722
- // =============================================================================
1723
- // RESOLVE TYPES
1724
- // =============================================================================
1725
-
1726
- /** Variant context for record resolution */
1727
- export interface VariantContext {
1728
- locale?: string;
1729
- device?: string;
1730
- region?: string;
1731
- contexts?: Record<string, JsonValue>;
1732
- }
1733
-
1734
- /** Reference resolution options */
1735
- export interface ReferenceResolutionOptions {
1736
- maxDepth?: number;
1737
- resolveMedia?: boolean;
1738
- resolveReferences?: boolean;
1739
- }
1740
-
1741
- /** Resolved record metadata */
1742
- export interface ResolvedRecord {
1743
- id: string;
1744
- modelKey: string;
1745
- naturalKey: string | null;
1746
- metadata?: Record<string, JsonValue>;
1747
- }
1748
-
1749
- /** Resolved variant info */
1750
- export interface ResolvedVariant {
1751
- id: string;
1752
- variantKey: string;
1753
- }
1754
-
1755
- /** Resolved field with value */
1756
- export interface ResolvedField {
1757
- key: string;
1758
- type: string;
1759
- label?: string;
1760
- required?: boolean;
1761
- value: JsonValue;
1762
- }
1763
-
1764
- /** Resolved content */
1765
- export interface ResolvedContent {
1766
- fields: ResolvedField[];
1767
- }
1768
-
1769
- /** Resolution context output */
1770
- export interface ResolutionContext {
1771
- locale: string;
1772
- contexts: Record<string, JsonValue>;
1773
- }
1774
-
1775
- /** Base resolved record content */
1776
- export interface ResolvedRecordContentBase {
1777
- record: ResolvedRecord;
1778
- variant: ResolvedVariant;
1779
- content: ResolvedContent;
1780
- resolvedWith: ResolutionContext;
1781
- }
1782
-
1783
- // =============================================================================
1784
- // FIELD DEFINITION TYPES
1785
- // =============================================================================
1786
-
1787
- export interface BaseFieldDef {
1788
- key: string;
1789
- label: string;
1790
- required?: boolean;
1791
- helpText?: string;
1792
- defaultValue?: JsonValue;
1793
- }
1794
-
1795
- export interface TextFieldDef extends BaseFieldDef {
1796
- type: 'text';
1797
- maxLength?: number;
1798
- minLength?: number;
1799
- pattern?: string;
1800
- }
1801
-
1802
- export interface NumberFieldDef extends BaseFieldDef {
1803
- type: 'number';
1804
- min?: number;
1805
- max?: number;
1806
- step?: number;
1807
- }
1808
-
1809
- export interface BooleanFieldDef extends BaseFieldDef {
1810
- type: 'boolean';
1811
- }
1812
-
1813
- export interface DateFieldDef extends BaseFieldDef {
1814
- type: 'date';
1815
- }
1816
-
1817
- export interface RichtextFieldDef extends BaseFieldDef {
1818
- type: 'richtext';
1819
- }
1820
-
1821
- export interface ImageFieldDef extends BaseFieldDef {
1822
- type: 'image';
1823
- allowedTypes?: string[];
1824
- maxSize?: number;
1825
- }
1826
-
1827
- export interface VideoFieldDef extends BaseFieldDef {
1828
- type: 'video';
1829
- allowedTypes?: string[];
1830
- maxSize?: number;
1831
- }
1832
-
1833
- export interface FileFieldDef extends BaseFieldDef {
1834
- type: 'file';
1835
- allowedTypes?: string[];
1836
- maxSize?: number;
1837
- }
1838
-
1839
- export interface SelectFieldDef extends BaseFieldDef {
1840
- type: 'select';
1841
- options: Array<{ label: string; value: string }>;
1842
- }
1843
-
1844
- export interface MultiselectFieldDef extends BaseFieldDef {
1845
- type: 'multiselect';
1846
- options: Array<{ label: string; value: string }>;
1847
- }
1848
-
1849
- export interface LinkFieldDef extends BaseFieldDef {
1850
- type: 'link';
1851
- }
1852
-
1853
- export interface ReferenceFieldDef extends BaseFieldDef {
1854
- type: 'reference';
1855
- referenceTypes?: string[];
1856
- multiple?: boolean;
1857
- }
1858
-
1859
- export interface ListFieldDef extends BaseFieldDef {
1860
- type: 'list';
1861
- itemType?: string;
1862
- minItems?: number;
1863
- maxItems?: number;
1864
- }
1865
-
1866
- export interface JsonFieldDef extends BaseFieldDef {
1867
- type: 'json';
1868
- }
1869
-
1870
- export interface FlexibleFieldDef extends BaseFieldDef {
1871
- type: 'flexible';
1872
- }
1873
-
1874
- /** Field def for inline model types (type is the model's key, e.g. 'seo', 'hero-banner') */
1875
- export interface InlineModelFieldDef extends BaseFieldDef {
1876
- type: string;
1877
- }
1878
-
1879
- export type FieldDef =
1880
- | TextFieldDef
1881
- | NumberFieldDef
1882
- | BooleanFieldDef
1883
- | DateFieldDef
1884
- | RichtextFieldDef
1885
- | ImageFieldDef
1886
- | VideoFieldDef
1887
- | FileFieldDef
1888
- | SelectFieldDef
1889
- | MultiselectFieldDef
1890
- | LinkFieldDef
1891
- | ReferenceFieldDef
1892
- | ListFieldDef
1893
- | JsonFieldDef
1894
- | FlexibleFieldDef
1895
- | InlineModelFieldDef;
1896
- `;
1897
- }
1898
-
1899
- // src/codegen/generators/config.ts
1900
- function generateConfigFile() {
1901
- return `/**
1902
- * Model Configuration Type
1903
- *
1904
- * Strongly-typed model definitions for the unified data layer.
1905
- *
1906
- * @generated by foir \u2014 DO NOT EDIT MANUALLY
1907
- */
1908
-
1909
- import type { FieldDef, JsonValue } from './field-types.js';
1910
-
1911
- /**
1912
- * Model configuration
1913
- *
1914
- * Defines the complete configuration for a model including
1915
- * its schema, capabilities, and lifecycle hooks.
1916
- *
1917
- * @example
1918
- * export const blogPostConfig = {
1919
- * key: 'blog-post',
1920
- * name: 'Blog Post',
1921
- * records: true,
1922
- * inline: false,
1923
- * publicApi: true,
1924
- * versioning: true,
1925
- * publishing: true,
1926
- * variants: false,
1927
- * customerScoped: false,
1928
- * fieldDefs: [
1929
- * { key: 'title', type: 'text', label: 'Title', required: true },
1930
- * { key: 'content', type: 'richtext', label: 'Content' },
1931
- * ],
1932
- * } as const satisfies ModelConfig;
1933
- */
1934
- export interface ModelConfig {
1935
- /** Unique identifier (kebab-case) */
1936
- key: string;
1937
- /** Display name */
1938
- name: string;
1939
- /** Description */
1940
- description?: string;
1941
-
1942
- // Capability flags (from model config)
1943
- /** Can create standalone records */
1944
- records: boolean;
1945
- /** Available as inline field type in other models */
1946
- inline: boolean;
1947
- /** Exposed via public GraphQL API */
1948
- publicApi: boolean;
1949
- /** Version history enabled */
1950
- versioning: boolean;
1951
- /** Publishing workflow enabled */
1952
- publishing: boolean;
1953
- /** Market/device/locale variants enabled */
1954
- variants: boolean;
1955
- /** Customer-level record isolation */
1956
- customerScoped: boolean;
1957
-
1958
- /** Embedding configuration */
1959
- embeddings?: {
1960
- enabled: boolean;
1961
- fields: Array<{ fieldPath: string; weight?: number }>;
1962
- };
1963
-
1964
- /** Lifecycle hooks configuration */
1965
- hooks?: Record<string, JsonValue>;
1966
-
1967
- /** Field definitions */
1968
- fieldDefs: readonly FieldDef[];
1969
- }
1970
-
1971
- /**
1972
- * Helper to create a type-safe model config
1973
- */
1974
- export function defineModel<T extends ModelConfig>(config: T): T {
1975
- return config;
1976
- }
1977
- `;
1978
- }
1979
-
1980
- // src/codegen/generators/model-types.ts
1981
- function isInlineOnlyModel(model) {
1982
- return model.config.inline && !model.config.records;
1983
- }
1984
- function generateModelTypes(model, allModels) {
1985
- const typeName = toPascalCase(model.key);
1986
- const configName = toCamelCase(model.key) + "Config";
1987
- const fields = model.fields ?? [];
1988
- const fieldTypeImports = getFieldTypeImportsForFields(fields);
1989
- const inlineSchemaRefs = getInlineSchemaReferences(fields);
1990
- const referenceModelRefs = getReferenceTypeModelRefs(fields);
1991
- let code = buildImportStatements(
1992
- model,
1993
- fieldTypeImports,
1994
- inlineSchemaRefs,
1995
- referenceModelRefs,
1996
- allModels
1997
- );
1998
- if (isInlineOnlyModel(model)) {
1999
- code += generateDataInterface(model, fields, typeName, allModels);
2000
- return code;
2001
- }
2002
- code += generateConfigObject(model, fields, configName);
2003
- code += "\n";
2004
- code += generateDataInterface(model, fields, typeName + "Data", allModels);
2005
- return code;
2006
- }
2007
- function buildImportStatements(model, fieldTypeImports, inlineSchemaRefs, referenceModelRefs, allModels) {
2008
- const imports = [];
2009
- if (!isInlineOnlyModel(model)) {
2010
- imports.push("import type { ModelConfig } from '../config.js';");
2011
- }
2012
- if (fieldTypeImports.size > 0) {
2013
- const types = Array.from(fieldTypeImports).sort().join(", ");
2014
- imports.push(`import type { ${types} } from '../field-types.js';`);
2015
- }
2016
- const allModelRefKeys = /* @__PURE__ */ new Set([
2017
- ...inlineSchemaRefs,
2018
- ...referenceModelRefs
2019
- ]);
2020
- for (const refKey of allModelRefKeys) {
2021
- if (refKey === model.key) continue;
2022
- const refModel = allModels.find((m) => m.key === refKey);
2023
- if (refModel) {
2024
- const refTypeName = isInlineOnlyModel(refModel) ? toPascalCase(refKey) : toPascalCase(refKey) + "Data";
2025
- imports.push(`import type { ${refTypeName} } from './${refKey}.js';`);
2026
- }
2027
- }
2028
- return imports.length > 0 ? imports.join("\n") + "\n\n" : "";
2029
- }
2030
- function generateConfigObject(model, fields, configName) {
2031
- const lines = [];
2032
- lines.push("/**");
2033
- lines.push(` * ${model.name} Configuration`);
2034
- if (model.description) lines.push(` * ${model.description}`);
2035
- lines.push(` *`);
2036
- lines.push(` * @generated from model '${model.key}'`);
2037
- lines.push(" */");
2038
- const escapedName = (model.name ?? model.key).replace(/'/g, "\\'");
2039
- lines.push(`export const ${configName} = {`);
2040
- lines.push(` key: '${model.key}',`);
2041
- lines.push(` name: '${escapedName}',`);
2042
- if (model.description) {
2043
- lines.push(` description: '${model.description.replace(/'/g, "\\'")}',`);
2044
- }
2045
- lines.push("");
2046
- lines.push(" // Capability flags");
2047
- lines.push(` records: ${model.config.records},`);
2048
- lines.push(` inline: ${model.config.inline},`);
2049
- lines.push(` publicApi: ${model.config.publicApi},`);
2050
- lines.push(` versioning: ${model.config.versioning},`);
2051
- lines.push(` publishing: ${model.config.publishing},`);
2052
- lines.push(` variants: ${model.config.variants},`);
2053
- lines.push(` customerScoped: ${model.config.customerScoped},`);
2054
- if (model.config.embeddings?.enabled) {
2055
- lines.push("");
2056
- lines.push(" // Embeddings");
2057
- lines.push(
2058
- ` embeddings: ${JSON.stringify(model.config.embeddings, null, 4).replace(/\n/g, "\n ")},`
2059
- );
2060
- }
2061
- if (model.hooks && Object.keys(model.hooks).length > 0) {
2062
- lines.push("");
2063
- lines.push(" // Lifecycle hooks");
2064
- lines.push(
2065
- ` hooks: ${JSON.stringify(model.hooks, null, 4).replace(/\n/g, "\n ")},`
2066
- );
2067
- } else {
2068
- lines.push("");
2069
- lines.push(" hooks: {},");
2070
- }
2071
- lines.push("");
2072
- lines.push(" // Field definitions");
2073
- if (fields.length === 0) {
2074
- lines.push(" fieldDefs: [],");
2075
- } else {
2076
- lines.push(" fieldDefs: [");
2077
- for (const field of fields) {
2078
- lines.push(` ${generateFieldDef(field)},`);
2079
- }
2080
- lines.push(" ],");
2081
- }
2082
- lines.push("} as const satisfies ModelConfig;");
2083
- return lines.join("\n") + "\n";
1687
+ function generateConfigObject(model, fields, configName) {
1688
+ const lines = [];
1689
+ lines.push("/**");
1690
+ lines.push(` * ${model.name} Configuration`);
1691
+ if (model.description) lines.push(` * ${model.description}`);
1692
+ lines.push(` *`);
1693
+ lines.push(` * @generated from model '${model.key}'`);
1694
+ lines.push(" */");
1695
+ const escapedName = (model.name ?? model.key).replace(/'/g, "\\'");
1696
+ lines.push(`export const ${configName} = {`);
1697
+ lines.push(` key: '${model.key}',`);
1698
+ lines.push(` name: '${escapedName}',`);
1699
+ if (model.description) {
1700
+ lines.push(` description: '${model.description.replace(/'/g, "\\'")}',`);
1701
+ }
1702
+ lines.push("");
1703
+ lines.push(" // Capability flags");
1704
+ lines.push(` records: ${model.config.records},`);
1705
+ lines.push(` inline: ${model.config.inline},`);
1706
+ lines.push(` publicApi: ${model.config.publicApi},`);
1707
+ lines.push(` versioning: ${model.config.versioning},`);
1708
+ lines.push(` publishing: ${model.config.publishing},`);
1709
+ lines.push(` variants: ${model.config.variants},`);
1710
+ lines.push(` customerScoped: ${model.config.customerScoped},`);
1711
+ if (model.config.embeddings?.enabled) {
1712
+ lines.push("");
1713
+ lines.push(" // Embeddings");
1714
+ lines.push(
1715
+ ` embeddings: ${JSON.stringify(model.config.embeddings, null, 4).replace(/\n/g, "\n ")},`
1716
+ );
1717
+ }
1718
+ if (model.hooks && Object.keys(model.hooks).length > 0) {
1719
+ lines.push("");
1720
+ lines.push(" // Lifecycle hooks");
1721
+ lines.push(
1722
+ ` hooks: ${JSON.stringify(model.hooks, null, 4).replace(/\n/g, "\n ")},`
1723
+ );
1724
+ } else {
1725
+ lines.push("");
1726
+ lines.push(" hooks: {},");
1727
+ }
1728
+ lines.push("");
1729
+ lines.push(" // Field definitions");
1730
+ if (fields.length === 0) {
1731
+ lines.push(" fieldDefs: [],");
1732
+ } else {
1733
+ lines.push(" fieldDefs: [");
1734
+ for (const field of fields) {
1735
+ lines.push(` ${generateFieldDef(field)},`);
1736
+ }
1737
+ lines.push(" ],");
1738
+ }
1739
+ lines.push("} as const;");
1740
+ return lines.join("\n") + "\n";
2084
1741
  }
2085
1742
  function generateDataInterface(model, fields, interfaceName, allModels) {
2086
1743
  const lines = [];
@@ -2168,666 +1825,447 @@ function generateModelIndex(models) {
2168
1825
  `;
2169
1826
  code += `export type { ${typeName}Data } from './${model.key}.js';
2170
1827
  `;
1828
+ if (model.config.publicApi) {
1829
+ code += `export type { ${typeName}Where, ${typeName}SortField } from './${model.key}.filters.js';
1830
+ `;
1831
+ }
2171
1832
  }
2172
1833
  }
2173
1834
  return code;
2174
1835
  }
2175
1836
 
2176
- // src/codegen/generators/documents.ts
2177
- function generateModelDocuments(model) {
2178
- const typeName = toPascalCase(model.key);
2179
- const pluralName = model.pluralName ? toPascalCase(model.pluralName.replace(/\s+/g, "")) : `${typeName}s`;
2180
- const displayName = model.name ?? model.key;
2181
- return `# Generated GraphQL operations for ${displayName}
2182
- # @generated by foir \u2014 DO NOT EDIT MANUALLY
2183
-
2184
- fragment ${typeName}Fields on Record {
2185
- id
2186
- modelKey
2187
- naturalKey
2188
- data
2189
- metadata
2190
- publishedVersionNumber
2191
- publishedAt
2192
- versionNumber
2193
- changeDescription
2194
- createdAt
2195
- updatedAt
2196
- }
1837
+ // src/codegen/generators/client-factory.ts
1838
+ function generateClientFactory(models, hasCustomerProfile) {
1839
+ const publicModels = models.filter(
1840
+ (m) => m.config.publicApi && m.config.records
1841
+ );
1842
+ const lines = [];
1843
+ lines.push(`/**
1844
+ * Typed Foir client for this project.
1845
+ *
1846
+ * @generated by foir \u2014 DO NOT EDIT MANUALLY
1847
+ */
2197
1848
 
2198
- query Get${typeName}($id: ID!, $locale: String, $preview: Boolean, $fields: FieldSelectionInput) {
2199
- record(id: $id) {
2200
- ...${typeName}Fields
2201
- resolved(locale: $locale, preview: $preview, fields: $fields) {
2202
- content
2203
- record { id modelKey naturalKey }
2204
- version { id versionNumber }
2205
- }
1849
+ import {
1850
+ createBaseClient,
1851
+ createModelAccessor,
1852
+ createAuthClient,
1853
+ createFilesClient,
1854
+ createNotificationsClient,
1855
+ createSharingClient,
1856
+ createSearchClient,
1857
+ createProfileClient,
1858
+ createOperationsClient,
1859
+ type ClientConfig,
1860
+ type ModelAccessor,
1861
+ type FoirClient,
1862
+ } from '@eide/foir-client';
1863
+ `);
1864
+ for (const model of publicModels) {
1865
+ const typeName = toPascalCase(model.key);
1866
+ const dataType = `${typeName}Data`;
1867
+ const whereType = `${typeName}Where`;
1868
+ const sortType = `${typeName}SortField`;
1869
+ lines.push(
1870
+ `import type { ${dataType}, ${whereType}, ${sortType} } from './models/${model.key}.js';`
1871
+ );
2206
1872
  }
2207
- }
2208
-
2209
- query Get${typeName}ByKey($naturalKey: String!, $locale: String, $preview: Boolean, $fields: FieldSelectionInput) {
2210
- recordByKey(modelKey: "${model.key}", naturalKey: $naturalKey) {
2211
- ...${typeName}Fields
2212
- resolved(locale: $locale, preview: $preview, fields: $fields) {
2213
- content
2214
- record { id modelKey naturalKey }
2215
- version { id versionNumber }
2216
- }
1873
+ if (hasCustomerProfile) {
1874
+ lines.push(
1875
+ `import type { CustomerProfileData } from './models/customer-profile.js';`
1876
+ );
2217
1877
  }
2218
- }
2219
-
2220
- query List${pluralName}(
2221
- $limit: Int
2222
- $offset: Int
2223
- $filters: [FilterInput!]
2224
- $sort: SortInput
2225
- $locale: String
2226
- $preview: Boolean
2227
- $fields: FieldSelectionInput
2228
- ) {
2229
- records(
2230
- modelKey: "${model.key}"
2231
- limit: $limit
2232
- offset: $offset
2233
- filters: $filters
2234
- sort: $sort
2235
- ) {
2236
- items {
2237
- ...${typeName}Fields
2238
- resolved(locale: $locale, preview: $preview, fields: $fields) {
2239
- content
2240
- record { id modelKey naturalKey }
2241
- version { id versionNumber }
2242
- }
2243
- }
2244
- total
1878
+ lines.push("");
1879
+ lines.push("export interface TypedClient extends FoirClient {");
1880
+ for (const model of publicModels) {
1881
+ const typeName = toPascalCase(model.key);
1882
+ const accessorName = toCamelCase(model.key);
1883
+ const dataType = `${typeName}Data`;
1884
+ const whereType = `${typeName}Where`;
1885
+ const sortType = `${typeName}SortField`;
1886
+ lines.push(` ${accessorName}: ModelAccessor<${dataType}, ${whereType}, ${sortType}>;`);
2245
1887
  }
2246
- }
2247
-
2248
- mutation Create${typeName}($input: CreateRecordInput!) {
2249
- createRecord(input: $input) {
2250
- record {
2251
- ...${typeName}Fields
2252
- }
1888
+ if (hasCustomerProfile) {
1889
+ lines.push(
1890
+ ` profile: ReturnType<typeof createProfileClient<CustomerProfileData>>;`
1891
+ );
2253
1892
  }
2254
- }
2255
-
2256
- mutation Update${typeName}($input: UpdateRecordInput!) {
2257
- updateRecord(input: $input) {
2258
- record {
2259
- ...${typeName}Fields
2260
- }
2261
- matched
1893
+ lines.push("}");
1894
+ lines.push("");
1895
+ lines.push("export function createClient(config: ClientConfig): TypedClient {");
1896
+ lines.push(" const base = createBaseClient(config);");
1897
+ lines.push("");
1898
+ lines.push(" return {");
1899
+ lines.push(" auth: createAuthClient(base),");
1900
+ lines.push(" files: createFilesClient(base),");
1901
+ lines.push(" notifications: createNotificationsClient(base),");
1902
+ lines.push(" sharing: createSharingClient(base),");
1903
+ lines.push(" search: createSearchClient(base),");
1904
+ if (hasCustomerProfile) {
1905
+ lines.push(" profile: createProfileClient<CustomerProfileData>(base),");
1906
+ } else {
1907
+ lines.push(" profile: createProfileClient(base),");
2262
1908
  }
2263
- }
2264
-
2265
- mutation Delete${typeName}($id: ID!) {
2266
- deleteRecord(id: $id) {
2267
- id
1909
+ lines.push(" operations: createOperationsClient(base),");
1910
+ for (const model of publicModels) {
1911
+ const typeName = toPascalCase(model.key);
1912
+ const accessorName = toCamelCase(model.key);
1913
+ const dataType = `${typeName}Data`;
1914
+ const whereType = `${typeName}Where`;
1915
+ const sortType = `${typeName}SortField`;
1916
+ lines.push(
1917
+ ` ${accessorName}: createModelAccessor<${dataType}, ${whereType}, ${sortType}>(base, '${model.key}'),`
1918
+ );
1919
+ }
1920
+ lines.push(` model<TData = unknown>(modelKey: string) {`);
1921
+ lines.push(` return createModelAccessor<TData>(base, modelKey);`);
1922
+ lines.push(` },`);
1923
+ lines.push(" request: base.request,");
1924
+ lines.push(" setCustomerToken: base.setCustomerToken,");
1925
+ lines.push(" setLocale: base.setLocale,");
1926
+ lines.push(" setPreview: base.setPreview,");
1927
+ lines.push(" } as TypedClient;");
1928
+ lines.push("}");
1929
+ lines.push("");
1930
+ lines.push("// Re-export types from @eide/foir-client for convenience");
1931
+ lines.push(`export type {
1932
+ ClientConfig,
1933
+ TypedRecord,
1934
+ TypedList,
1935
+ ModelAccessor,
1936
+ JsonValue,
1937
+ ImageValue,
1938
+ VideoValue,
1939
+ FileValue,
1940
+ LinkValue,
1941
+ ReferenceValue,
1942
+ FlexibleFieldItem,
1943
+ RichtextValue,
1944
+ CurrencyValue,
1945
+ SelectMap,
1946
+ ApplySelect,
1947
+ FoirClient,
1948
+ } from '@eide/foir-client';`);
1949
+ lines.push("");
1950
+ for (const model of publicModels) {
1951
+ const typeName = toPascalCase(model.key);
1952
+ const dataType = `${typeName}Data`;
1953
+ const whereType = `${typeName}Where`;
1954
+ const sortType = `${typeName}SortField`;
1955
+ lines.push(
1956
+ `export type { ${dataType}, ${whereType}, ${sortType} } from './models/${model.key}.js';`
1957
+ );
1958
+ }
1959
+ if (hasCustomerProfile) {
1960
+ lines.push(
1961
+ `export type { CustomerProfileData } from './models/customer-profile.js';`
1962
+ );
2268
1963
  }
1964
+ return lines.join("\n") + "\n";
2269
1965
  }
2270
1966
 
2271
- mutation Publish${typeName}Version($versionId: ID!) {
2272
- publishVersion(versionId: $versionId)
2273
- }
2274
-
2275
- mutation Unpublish${typeName}($id: ID!) {
2276
- unpublishRecord(id: $id)
2277
- }
2278
- ${model.config.sharing?.enabled ? generateSharingOperations(model.key, typeName, pluralName) : ""}`;
2279
- }
2280
- function generateSharedFragments() {
2281
- return `# Shared fragments used across multiple model documents
2282
- # @generated by foir \u2014 DO NOT EDIT MANUALLY
2283
-
2284
- fragment ShareFields on Share {
2285
- id
2286
- resourceType
2287
- recordId
2288
- fileId
2289
- permission
2290
- status
2291
- sharedWithCustomerId
2292
- acceptedAt
2293
- declinedAt
2294
- expiresAt
2295
- createdAt
2296
- createdBy
2297
- revokedAt
2298
- }
2299
- `;
2300
- }
2301
- function generateSharingOperations(modelKey, typeName, pluralName) {
2302
- return `# Sharing operations
2303
-
2304
- mutation Share${typeName}($recordId: ID!, $sharedWithCustomerId: ID!, $permission: SharePermission!) {
2305
- shareRecord(recordId: $recordId, sharedWithCustomerId: $sharedWithCustomerId, permission: $permission) {
2306
- ...ShareFields
2307
- }
2308
- }
2309
-
2310
- mutation Accept${typeName}Share($shareId: ID!) {
2311
- acceptShare(shareId: $shareId) {
2312
- ...ShareFields
2313
- }
2314
- }
2315
-
2316
- mutation Decline${typeName}Share($shareId: ID!) {
2317
- declineShare(shareId: $shareId) {
2318
- ...ShareFields
1967
+ // src/codegen/generators/schema-manifest.ts
1968
+ function generateSchemaManifest(models, cpSchema) {
1969
+ const manifest = {
1970
+ $schema: "https://foir.io/schema/v1.json",
1971
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1972
+ models: {}
1973
+ };
1974
+ for (const model of models) {
1975
+ manifest.models[model.key] = {
1976
+ name: model.name,
1977
+ pluralName: model.pluralName,
1978
+ description: model.description,
1979
+ category: model.category,
1980
+ config: model.config,
1981
+ fields: model.fields.map((f) => ({
1982
+ key: f.key,
1983
+ type: f.type,
1984
+ label: f.label,
1985
+ required: f.required,
1986
+ helpText: f.helpText,
1987
+ options: f.options
1988
+ }))
1989
+ };
2319
1990
  }
2320
- }
2321
-
2322
- mutation Revoke${typeName}Share($shareId: ID!) {
2323
- revokeShare(shareId: $shareId) {
2324
- ...ShareFields
1991
+ if (cpSchema && cpSchema.fields.length > 0) {
1992
+ manifest.customerProfile = {
1993
+ fields: cpSchema.fields.map((f) => ({
1994
+ key: f.key,
1995
+ type: f.type,
1996
+ label: f.label,
1997
+ required: f.required,
1998
+ helpText: f.helpText,
1999
+ options: f.options
2000
+ }))
2001
+ };
2325
2002
  }
2003
+ return JSON.stringify(manifest, null, 2) + "\n";
2326
2004
  }
2327
2005
 
2328
- query ${typeName}Shares($resourceId: ID!, $status: ShareStatus) {
2329
- shares(resourceType: RECORD, resourceId: $resourceId, status: $status) {
2330
- ...ShareFields
2331
- }
2006
+ // src/codegen/generators/model-filters.ts
2007
+ function isInlineOnlyModel3(model) {
2008
+ return model.config.inline && !model.config.records;
2332
2009
  }
2333
-
2334
- query ${pluralName}SharedWithMe($status: ShareStatus) {
2335
- sharedWithMe(resourceType: RECORD, modelKey: "${modelKey}", status: $status) {
2336
- ...ShareFields
2337
- record {
2338
- ...${typeName}Fields
2339
- }
2010
+ function getFilterType(fieldType) {
2011
+ switch (fieldType) {
2012
+ case "text":
2013
+ case "richtext":
2014
+ case "email":
2015
+ case "phone":
2016
+ case "url":
2017
+ case "select":
2018
+ return "StringFilter";
2019
+ case "number":
2020
+ case "currency":
2021
+ return "NumberFilter";
2022
+ case "boolean":
2023
+ return "BooleanFilter";
2024
+ case "date":
2025
+ return "DateFilter";
2026
+ default:
2027
+ return null;
2340
2028
  }
2341
2029
  }
2342
- `;
2343
- }
2344
-
2345
- // src/codegen/swift-field-mapping.ts
2346
- var SWIFT_FIELD_TYPE_MAPPING = {
2347
- text: {
2348
- type: "String",
2349
- alwaysOptional: false,
2350
- defaultValue: '""',
2351
- castExpression: "as? String"
2352
- },
2353
- richtext: {
2354
- type: "String",
2355
- alwaysOptional: true,
2356
- defaultValue: '""',
2357
- castExpression: "as? String"
2358
- },
2359
- number: {
2360
- type: "Double",
2361
- alwaysOptional: true,
2362
- defaultValue: "0",
2363
- castExpression: "as? Double"
2364
- },
2365
- boolean: {
2366
- type: "Bool",
2367
- alwaysOptional: true,
2368
- defaultValue: "false",
2369
- castExpression: "as? Bool"
2370
- },
2371
- email: {
2372
- type: "String",
2373
- alwaysOptional: true,
2374
- defaultValue: '""',
2375
- castExpression: "as? String"
2376
- },
2377
- phone: {
2378
- type: "String",
2379
- alwaysOptional: true,
2380
- defaultValue: '""',
2381
- castExpression: "as? String"
2382
- },
2383
- url: {
2384
- type: "String",
2385
- alwaysOptional: true,
2386
- defaultValue: '""',
2387
- castExpression: "as? String"
2388
- },
2389
- date: {
2390
- type: "String",
2391
- alwaysOptional: true,
2392
- defaultValue: '""',
2393
- castExpression: "as? String"
2394
- },
2395
- image: {
2396
- type: "ImageValue",
2397
- alwaysOptional: true,
2398
- defaultValue: 'ImageValue(id: "", url: "")',
2399
- castExpression: "as? [String: Any]",
2400
- needsSharedType: true
2401
- },
2402
- video: {
2403
- type: "VideoValue",
2404
- alwaysOptional: true,
2405
- defaultValue: 'VideoValue(id: "", url: "")',
2406
- castExpression: "as? [String: Any]",
2407
- needsSharedType: true
2408
- },
2409
- file: {
2410
- type: "FileValue",
2411
- alwaysOptional: true,
2412
- defaultValue: 'FileValue(id: "", url: "", name: "", size: 0, mimeType: "")',
2413
- // fromSyncData handles fileId→id mapping
2414
- castExpression: "as? [String: Any]",
2415
- needsSharedType: true
2416
- },
2417
- currency: {
2418
- type: "CurrencyValue",
2419
- alwaysOptional: true,
2420
- defaultValue: 'CurrencyValue(amount: 0, currency: "")',
2421
- castExpression: "as? [String: Any]",
2422
- needsSharedType: true
2423
- },
2424
- select: {
2425
- type: "String",
2426
- alwaysOptional: true,
2427
- defaultValue: '""',
2428
- castExpression: "as? String"
2429
- },
2430
- multiselect: {
2431
- type: "[String]",
2432
- alwaysOptional: true,
2433
- defaultValue: "[]",
2434
- castExpression: "as? [String]"
2435
- },
2436
- json: {
2437
- type: "Any",
2438
- alwaysOptional: true,
2439
- defaultValue: "nil",
2440
- castExpression: ""
2441
- },
2442
- list: {
2443
- type: "[Any]",
2444
- alwaysOptional: true,
2445
- defaultValue: "[]",
2446
- castExpression: "as? [Any]"
2447
- },
2448
- flexible: {
2449
- type: "[[String: Any]]",
2450
- alwaysOptional: true,
2451
- defaultValue: "[]",
2452
- castExpression: "as? [[String: Any]]"
2453
- },
2454
- reference: {
2455
- type: "String",
2456
- alwaysOptional: true,
2457
- defaultValue: '""',
2458
- castExpression: "as? String"
2459
- },
2460
- link: {
2461
- type: "LinkValue",
2462
- alwaysOptional: true,
2463
- defaultValue: 'LinkValue(type: "")',
2464
- castExpression: "as? [String: Any]",
2465
- needsSharedType: true
2466
- },
2467
- model: {
2468
- type: "String",
2469
- alwaysOptional: true,
2470
- defaultValue: '""',
2471
- castExpression: "as? String"
2472
- }
2473
- };
2474
- function getSwiftFieldType(field) {
2475
- const mapping = SWIFT_FIELD_TYPE_MAPPING[field.type];
2476
- if (!mapping) {
2477
- return {
2478
- type: "Any",
2479
- isOptional: true,
2480
- mapping: void 0
2481
- };
2482
- }
2483
- const isOptional = mapping.alwaysOptional || !field.required;
2484
- return { type: mapping.type, isOptional, mapping };
2030
+ function isSortable(fieldType) {
2031
+ return ["text", "number", "date", "boolean", "email", "url", "select"].includes(fieldType);
2485
2032
  }
2486
-
2487
- // src/codegen/generators/swift-types.ts
2488
- function generateSwiftModelFile(model) {
2033
+ function generateModelWhere(model) {
2034
+ if (isInlineOnlyModel3(model)) return "";
2489
2035
  const typeName = toPascalCase(model.key);
2036
+ const whereName = `${typeName}Where`;
2490
2037
  const fields = model.fields ?? [];
2491
- const lines = [];
2492
- lines.push("//");
2493
- lines.push(`// ${typeName}.swift`);
2494
- lines.push("//");
2495
- lines.push(`// Generated from model '${model.key}'`);
2496
- lines.push("//");
2497
- lines.push("// @generated by foir \u2014 DO NOT EDIT MANUALLY");
2498
- lines.push("//");
2499
- lines.push("");
2500
- lines.push("import Foundation");
2501
- lines.push("");
2502
- lines.push(generateFieldsEnum(typeName, fields));
2503
- lines.push("");
2504
- lines.push(generateDataStruct(typeName, fields));
2505
- lines.push("");
2506
- lines.push(generateSerializationExtension(typeName, fields));
2507
- lines.push("");
2508
- lines.push(generateConfigEnum(typeName, model));
2509
- return lines.join("\n");
2510
- }
2511
- function generateFieldsEnum(typeName, fields) {
2512
- const lines = [];
2513
- lines.push(`// MARK: - ${typeName} Field Keys`);
2514
- lines.push("");
2515
- lines.push(`enum ${typeName}Fields {`);
2516
- for (const field of fields) {
2517
- lines.push(` static let ${field.key} = "${field.key}"`);
2518
- }
2519
- lines.push("}");
2520
- return lines.join("\n");
2521
- }
2522
- function generateDataStruct(typeName, fields) {
2523
- const lines = [];
2524
- lines.push(`// MARK: - ${typeName} Data`);
2525
- lines.push("");
2526
- lines.push(`struct ${typeName}Data {`);
2038
+ const filterImports = /* @__PURE__ */ new Set();
2039
+ const filterFields = [];
2527
2040
  for (const field of fields) {
2528
- const { type, isOptional } = getSwiftFieldType(field);
2529
- const optionalSuffix = isOptional ? "?" : "";
2530
- lines.push(` var ${field.key}: ${type}${optionalSuffix}`);
2531
- }
2532
- lines.push("}");
2533
- return lines.join("\n");
2534
- }
2535
- function generateSerializationExtension(typeName, fields) {
2536
- const lines = [];
2537
- lines.push(`// MARK: - ${typeName} Serialization`);
2538
- lines.push("");
2539
- lines.push(`extension ${typeName}Data {`);
2540
- lines.push(" func toSyncData() -> [String: Any] {");
2541
- const requiredFields = fields.filter((f) => {
2542
- const { isOptional } = getSwiftFieldType(f);
2543
- return !isOptional;
2544
- });
2545
- const optionalFields = fields.filter((f) => {
2546
- const { isOptional } = getSwiftFieldType(f);
2547
- return isOptional;
2548
- });
2549
- if (requiredFields.length > 0) {
2550
- if (optionalFields.length === 0) {
2551
- lines.push(` return [`);
2552
- requiredFields.forEach((f, i) => {
2553
- const comma = i < requiredFields.length - 1 ? "," : "";
2554
- lines.push(
2555
- ` ${typeName}Fields.${f.key}: ${toSyncValueExpr(f)}${comma}`
2556
- );
2557
- });
2558
- lines.push(" ]");
2559
- } else {
2560
- lines.push(` var data: [String: Any] = [`);
2561
- requiredFields.forEach((f, i) => {
2562
- const comma = i < requiredFields.length - 1 ? "," : "";
2563
- lines.push(
2564
- ` ${typeName}Fields.${f.key}: ${toSyncValueExpr(f)}${comma}`
2565
- );
2566
- });
2567
- lines.push(" ]");
2568
- for (const f of optionalFields) {
2569
- lines.push(
2570
- ` if let ${f.key} { data[${typeName}Fields.${f.key}] = ${toSyncValueExprForOptional(f)} }`
2571
- );
2572
- }
2573
- lines.push(" return data");
2574
- }
2575
- } else {
2576
- lines.push(" var data: [String: Any] = [:]");
2577
- for (const f of optionalFields) {
2578
- lines.push(
2579
- ` if let ${f.key} { data[${typeName}Fields.${f.key}] = ${toSyncValueExprForOptional(f)} }`
2580
- );
2041
+ const filterType = getFilterType(field.type);
2042
+ if (filterType) {
2043
+ filterImports.add(filterType);
2044
+ const fieldName = sanitizeFieldName(field.key);
2045
+ filterFields.push(` ${fieldName}?: ${filterType};`);
2581
2046
  }
2582
- lines.push(" return data");
2583
2047
  }
2584
- lines.push(" }");
2048
+ filterImports.add("DateFilter");
2049
+ filterFields.push(" createdAt?: DateFilter;");
2050
+ filterFields.push(" updatedAt?: DateFilter;");
2051
+ const imports = Array.from(filterImports).sort().join(", ");
2052
+ const lines = [];
2053
+ lines.push(`import type { ${imports} } from '@eide/foir-client';`);
2585
2054
  lines.push("");
2586
- lines.push(
2587
- " static func fromSyncData(_ data: [String: Any]) -> " + typeName + "Data {"
2588
- );
2589
- lines.push(` ${typeName}Data(`);
2590
- fields.forEach((field, i) => {
2591
- const comma = i < fields.length - 1 ? "," : "";
2592
- const { isOptional, mapping } = getSwiftFieldType(field);
2593
- lines.push(
2594
- ` ${field.key}: ${fromSyncDataExpr(field, typeName, isOptional, mapping)}${comma}`
2595
- );
2596
- });
2597
- lines.push(" )");
2598
- lines.push(" }");
2055
+ lines.push(`export interface ${whereName} {`);
2056
+ lines.push(...filterFields);
2057
+ lines.push(` AND?: ${whereName}[];`);
2058
+ lines.push(` OR?: ${whereName}[];`);
2059
+ lines.push(` NOT?: ${whereName};`);
2599
2060
  lines.push("}");
2600
- return lines.join("\n");
2601
- }
2602
- function toSyncValueExpr(field) {
2603
- const mapping = SWIFT_FIELD_TYPE_MAPPING[field.type];
2604
- if (mapping?.needsSharedType) {
2605
- return `${field.key}.toSyncData()`;
2606
- }
2607
- return field.key;
2608
- }
2609
- function toSyncValueExprForOptional(field) {
2610
- const mapping = SWIFT_FIELD_TYPE_MAPPING[field.type];
2611
- if (mapping?.needsSharedType) {
2612
- return `${field.key}.toSyncData()`;
2613
- }
2614
- return field.key;
2061
+ return lines.join("\n") + "\n";
2615
2062
  }
2616
- function fromSyncDataExpr(field, typeName, isOptional, mapping) {
2617
- const accessor = `data[${typeName}Fields.${field.key}]`;
2618
- if (!mapping) {
2619
- return isOptional ? `${accessor}` : `${accessor} ?? nil`;
2620
- }
2621
- if (mapping.needsSharedType) {
2622
- const dictCast = `${accessor} as? [String: Any]`;
2623
- if (isOptional) {
2624
- return `(${dictCast}).map { ${mapping.type}.fromSyncData($0) }`;
2063
+ function generateModelSortField(model) {
2064
+ if (isInlineOnlyModel3(model)) return "";
2065
+ const typeName = toPascalCase(model.key);
2066
+ const sortName = `${typeName}SortField`;
2067
+ const fields = model.fields ?? [];
2068
+ const sortableFields = [];
2069
+ for (const field of fields) {
2070
+ if (isSortable(field.type)) {
2071
+ sortableFields.push(`'${field.key}'`);
2625
2072
  }
2626
- return `${mapping.type}.fromSyncData(${dictCast} ?? [:])`;
2627
- }
2628
- if (field.type === "json") {
2629
- return isOptional ? accessor : `${accessor}`;
2630
2073
  }
2631
- if (isOptional) {
2632
- return `${accessor} ${mapping.castExpression}`;
2633
- }
2634
- return `${accessor} ${mapping.castExpression} ?? ${mapping.defaultValue}`;
2635
- }
2636
- function generateConfigEnum(typeName, model) {
2637
- const lines = [];
2638
- lines.push(`// MARK: - ${typeName} Config`);
2639
- lines.push("");
2640
- lines.push(`enum ${typeName}Config {`);
2641
- const escapedName = (model.name ?? model.key).replace(/"/g, '\\"');
2642
- lines.push(` static let key = "${model.key}"`);
2643
- lines.push(` static let name = "${escapedName}"`);
2644
- lines.push(` static let customerScoped = ${model.config.customerScoped}`);
2645
- lines.push(` static let publicApi = ${model.config.publicApi}`);
2646
- lines.push(` static let versioning = ${model.config.versioning}`);
2647
- lines.push(` static let publishing = ${model.config.publishing}`);
2648
- lines.push(` static let variants = ${model.config.variants}`);
2649
- lines.push(
2650
- ` static let sharingEnabled = ${model.config.sharing?.enabled ?? false}`
2651
- );
2652
- lines.push(
2653
- ` static let sharingRequireAcceptance = ${model.config.sharing?.requireAcceptance ?? true}`
2654
- );
2655
- lines.push("}");
2656
- lines.push("");
2657
- return lines.join("\n");
2074
+ sortableFields.push(`'createdAt'`);
2075
+ sortableFields.push(`'updatedAt'`);
2076
+ const unique = [...new Set(sortableFields)];
2077
+ return `export type ${sortName} = ${unique.join(" | ")};
2078
+ `;
2658
2079
  }
2659
2080
 
2660
- // src/codegen/generators/swift-field-types.ts
2661
- function generateSwiftFieldTypesFile() {
2662
- return `//
2663
- // FieldTypes.swift
2664
- //
2665
- // Shared value types for platform sync data.
2666
- //
2667
- // @generated by foir \u2014 DO NOT EDIT MANUALLY
2668
- //
2669
-
2670
- import Foundation
2671
-
2672
- // MARK: - Image
2673
-
2674
- struct ImageValue {
2675
- let id: String
2676
- let url: String
2677
- var alt: String?
2678
- var width: Int?
2679
- var height: Int?
2680
- var dominantColor: String?
2681
- var blurhash: String?
2682
-
2683
- func toSyncData() -> [String: Any] {
2684
- var data: [String: Any] = ["fileId": id, "source": "internal"]
2685
- if let alt { data["altText"] = alt }
2686
- if let width { data["width"] = width }
2687
- if let height { data["height"] = height }
2688
- if let dominantColor { data["dominantColor"] = dominantColor }
2689
- if let blurhash { data["blurhash"] = blurhash }
2690
- return data
2691
- }
2692
-
2693
- static func fromSyncData(_ data: [String: Any]) -> ImageValue {
2694
- ImageValue(
2695
- id: data["fileId"] as? String ?? data["id"] as? String ?? "",
2696
- url: data["url"] as? String ?? "",
2697
- alt: data["altText"] as? String ?? data["alt"] as? String,
2698
- width: data["width"] as? Int,
2699
- height: data["height"] as? Int,
2700
- dominantColor: data["dominantColor"] as? String,
2701
- blurhash: data["blurhash"] as? String
2702
- )
2703
- }
2081
+ // src/codegen/generators/model-zod.ts
2082
+ function isInlineOnlyModel4(model) {
2083
+ return model.config.inline && !model.config.records;
2704
2084
  }
2705
-
2706
- // MARK: - Video
2707
-
2708
- struct VideoValue {
2709
- let id: String
2710
- let url: String
2711
- var thumbnail: String?
2712
- var duration: Double?
2713
-
2714
- func toSyncData() -> [String: Any] {
2715
- var data: [String: Any] = ["fileId": id, "source": "internal"]
2716
- if let thumbnail { data["thumbnailUrl"] = thumbnail }
2717
- if let duration { data["duration"] = duration }
2718
- return data
2085
+ function getZodExpression(field, allModels) {
2086
+ const imports = /* @__PURE__ */ new Set();
2087
+ let expr;
2088
+ switch (field.type) {
2089
+ case "text":
2090
+ case "email":
2091
+ case "phone":
2092
+ case "url": {
2093
+ imports.add("textField");
2094
+ const opts = [];
2095
+ if (field.options?.maxLength) opts.push(`maxLength: ${field.options.maxLength}`);
2096
+ if (field.options?.minLength) opts.push(`minLength: ${field.options.minLength}`);
2097
+ if (field.options?.pattern) opts.push(`pattern: '${field.options.pattern}'`);
2098
+ expr = opts.length > 0 ? `textField({ ${opts.join(", ")} })` : `textField()`;
2099
+ break;
2719
2100
  }
2720
-
2721
- static func fromSyncData(_ data: [String: Any]) -> VideoValue {
2722
- VideoValue(
2723
- id: data["fileId"] as? String ?? data["id"] as? String ?? "",
2724
- url: data["url"] as? String ?? "",
2725
- thumbnail: data["thumbnailUrl"] as? String ?? data["thumbnail"] as? String,
2726
- duration: data["duration"] as? Double
2727
- )
2101
+ case "number": {
2102
+ imports.add("numberField");
2103
+ const opts = [];
2104
+ if (field.options?.min !== void 0) opts.push(`min: ${field.options.min}`);
2105
+ if (field.options?.max !== void 0) opts.push(`max: ${field.options.max}`);
2106
+ expr = opts.length > 0 ? `numberField({ ${opts.join(", ")} })` : `numberField()`;
2107
+ break;
2728
2108
  }
2729
- }
2730
-
2731
- // MARK: - File
2732
-
2733
- struct FileValue {
2734
- let id: String
2735
- let url: String
2736
- let name: String
2737
- let size: Int
2738
- let mimeType: String
2739
-
2740
- func toSyncData() -> [String: Any] {
2741
- [
2742
- "fileId": id,
2743
- "source": "internal",
2744
- "filename": name,
2745
- "fileSize": size,
2746
- "mimeType": mimeType,
2747
- ]
2109
+ case "boolean":
2110
+ imports.add("booleanField");
2111
+ expr = "booleanField()";
2112
+ break;
2113
+ case "date":
2114
+ imports.add("dateField");
2115
+ expr = "dateField()";
2116
+ break;
2117
+ case "richtext":
2118
+ imports.add("richtextValueSchema");
2119
+ expr = "richtextValueSchema";
2120
+ break;
2121
+ case "image":
2122
+ imports.add("imageValueSchema");
2123
+ expr = "imageValueSchema";
2124
+ break;
2125
+ case "video":
2126
+ imports.add("videoValueSchema");
2127
+ expr = "videoValueSchema";
2128
+ break;
2129
+ case "file":
2130
+ imports.add("fileValueSchema");
2131
+ expr = "fileValueSchema";
2132
+ break;
2133
+ case "currency":
2134
+ imports.add("currencyValueSchema");
2135
+ expr = "currencyValueSchema";
2136
+ break;
2137
+ case "link":
2138
+ imports.add("linkValueSchema");
2139
+ expr = "linkValueSchema";
2140
+ break;
2141
+ case "reference":
2142
+ imports.add("referenceValueSchema");
2143
+ expr = "referenceValueSchema";
2144
+ break;
2145
+ case "select": {
2146
+ if (field.options?.options) {
2147
+ imports.add("selectField");
2148
+ const options = field.options.options;
2149
+ const values = options.map((o) => `'${o.value}'`).join(", ");
2150
+ expr = `selectField([${values}])`;
2151
+ } else {
2152
+ expr = "z.string()";
2153
+ }
2154
+ break;
2748
2155
  }
2749
-
2750
- static func fromSyncData(_ data: [String: Any]) -> FileValue {
2751
- FileValue(
2752
- id: data["fileId"] as? String ?? data["id"] as? String ?? "",
2753
- url: data["url"] as? String ?? "",
2754
- name: data["filename"] as? String ?? data["name"] as? String ?? "",
2755
- size: data["fileSize"] as? Int ?? data["size"] as? Int ?? 0,
2756
- mimeType: data["mimeType"] as? String ?? ""
2757
- )
2156
+ case "multiselect": {
2157
+ if (field.options?.options) {
2158
+ imports.add("multiselectField");
2159
+ const options = field.options.options;
2160
+ const values = options.map((o) => `'${o.value}'`).join(", ");
2161
+ expr = `multiselectField([${values}])`;
2162
+ } else {
2163
+ expr = "z.array(z.string())";
2164
+ }
2165
+ break;
2758
2166
  }
2759
- }
2760
-
2761
- // MARK: - Currency
2762
-
2763
- struct CurrencyValue {
2764
- let amount: Double
2765
- let currency: String
2766
-
2767
- func toSyncData() -> [String: Any] {
2768
- ["amount": amount, "currency": currency]
2167
+ case "flexible":
2168
+ imports.add("flexibleFieldItemSchema");
2169
+ expr = "z.array(flexibleFieldItemSchema)";
2170
+ break;
2171
+ case "json":
2172
+ imports.add("jsonValueSchema");
2173
+ expr = "jsonValueSchema";
2174
+ break;
2175
+ case "list": {
2176
+ if (field.options?.itemType) {
2177
+ const itemType = field.options.itemType;
2178
+ const refModel = allModels.find((m) => m.key === itemType);
2179
+ if (refModel) {
2180
+ const refSchemaName = `${toPascalCase(itemType)}${isInlineOnlyModel4(refModel) ? "" : "Data"}Schema`;
2181
+ expr = `z.array(${refSchemaName})`;
2182
+ } else {
2183
+ imports.add("jsonValueSchema");
2184
+ expr = "z.array(jsonValueSchema)";
2185
+ }
2186
+ } else {
2187
+ imports.add("jsonValueSchema");
2188
+ expr = "z.array(jsonValueSchema)";
2189
+ }
2190
+ break;
2769
2191
  }
2770
-
2771
- static func fromSyncData(_ data: [String: Any]) -> CurrencyValue {
2772
- CurrencyValue(
2773
- amount: data["amount"] as? Double ?? 0,
2774
- currency: data["currency"] as? String ?? ""
2775
- )
2192
+ default: {
2193
+ const refModel = allModels.find((m) => m.key === field.type);
2194
+ if (refModel) {
2195
+ const refSchemaName = `${toPascalCase(field.type)}${isInlineOnlyModel4(refModel) ? "" : "Data"}Schema`;
2196
+ expr = refSchemaName;
2197
+ } else {
2198
+ imports.add("jsonValueSchema");
2199
+ expr = "jsonValueSchema";
2200
+ }
2776
2201
  }
2202
+ }
2203
+ if (!field.required) {
2204
+ expr = `${expr}.optional()`;
2205
+ }
2206
+ return { expression: expr, imports };
2777
2207
  }
2208
+ function generateModelZodSchema(model, allModels) {
2209
+ const typeName = toPascalCase(model.key);
2210
+ const schemaName = isInlineOnlyModel4(model) ? `${typeName}Schema` : `${typeName}DataSchema`;
2211
+ const fields = model.fields ?? [];
2212
+ if (fields.length === 0) {
2213
+ return `import { z } from '@eide/foir-client/validation';
2778
2214
 
2779
- // MARK: - Link
2780
-
2781
- struct LinkValue {
2782
- let type: String
2783
- var entityModelKey: String?
2784
- var entityNaturalKey: String?
2785
- var url: String?
2786
- var target: String?
2787
-
2788
- func toSyncData() -> [String: Any] {
2789
- var data: [String: Any] = ["type": type]
2790
- if let entityModelKey { data["entityModelKey"] = entityModelKey }
2791
- if let entityNaturalKey { data["entityNaturalKey"] = entityNaturalKey }
2792
- if let url { data["url"] = url }
2793
- if let target { data["target"] = target }
2794
- return data
2215
+ export const ${schemaName} = z.object({});
2216
+ `;
2217
+ }
2218
+ const allImports = /* @__PURE__ */ new Set();
2219
+ const fieldLines = [];
2220
+ const inlineSchemaImports = [];
2221
+ for (const field of fields) {
2222
+ const { expression, imports } = getZodExpression(field, allModels);
2223
+ for (const imp of imports) allImports.add(imp);
2224
+ const fieldName = sanitizeFieldName(field.key);
2225
+ fieldLines.push(` ${fieldName}: ${expression},`);
2226
+ if (!isPrimitiveFieldType(field.type) && field.type !== "list") {
2227
+ const refModel = allModels.find((m) => m.key === field.type);
2228
+ if (refModel && refModel.key !== model.key) {
2229
+ const refSchemaName = `${toPascalCase(field.type)}${isInlineOnlyModel4(refModel) ? "" : "Data"}Schema`;
2230
+ inlineSchemaImports.push(
2231
+ `import { ${refSchemaName} } from './${field.type}.zod.js';`
2232
+ );
2233
+ }
2795
2234
  }
2796
-
2797
- static func fromSyncData(_ data: [String: Any]) -> LinkValue {
2798
- LinkValue(
2799
- type: data["type"] as? String ?? "",
2800
- entityModelKey: data["entityModelKey"] as? String,
2801
- entityNaturalKey: data["entityNaturalKey"] as? String,
2802
- url: data["url"] as? String,
2803
- target: data["target"] as? String
2804
- )
2235
+ if (field.type === "list" && field.options?.itemType) {
2236
+ const itemType = field.options.itemType;
2237
+ const refModel = allModels.find((m) => m.key === itemType);
2238
+ if (refModel && refModel.key !== model.key) {
2239
+ const refSchemaName = `${toPascalCase(itemType)}${isInlineOnlyModel4(refModel) ? "" : "Data"}Schema`;
2240
+ inlineSchemaImports.push(
2241
+ `import { ${refSchemaName} } from './${itemType}.zod.js';`
2242
+ );
2243
+ }
2805
2244
  }
2806
- }
2807
- `;
2808
- }
2809
-
2810
- // src/codegen/generators/swift-model-keys.ts
2811
- function generateSwiftModelKeys(models) {
2245
+ }
2812
2246
  const lines = [];
2813
- lines.push("//");
2814
- lines.push("// ModelKeys.swift");
2815
- lines.push("//");
2816
- lines.push("// All model key constants.");
2817
- lines.push("//");
2818
- lines.push("// @generated by foir \u2014 DO NOT EDIT MANUALLY");
2819
- lines.push("//");
2820
- lines.push("");
2821
- lines.push("import Foundation");
2247
+ lines.push(`/**`);
2248
+ lines.push(` * Zod validation schema for ${model.name}`);
2249
+ lines.push(` *`);
2250
+ lines.push(` * @generated by foir \u2014 DO NOT EDIT MANUALLY`);
2251
+ lines.push(` */`);
2822
2252
  lines.push("");
2823
- lines.push("enum ModelKeys {");
2824
- for (const model of models) {
2825
- const propName = toCamelCase(model.key);
2826
- lines.push(` static let ${propName} = "${model.key}"`);
2253
+ lines.push(`import { z } from '@eide/foir-client/validation';`);
2254
+ if (allImports.size > 0) {
2255
+ const sorted = Array.from(allImports).sort();
2256
+ lines.push(`import { ${sorted.join(", ")} } from '@eide/foir-client/validation';`);
2827
2257
  }
2828
- lines.push("}");
2258
+ const uniqueInlineImports = [...new Set(inlineSchemaImports)];
2259
+ for (const imp of uniqueInlineImports) {
2260
+ lines.push(imp);
2261
+ }
2262
+ lines.push("");
2263
+ lines.push(`export const ${schemaName} = z.object({`);
2264
+ lines.push(...fieldLines);
2265
+ lines.push("});");
2829
2266
  lines.push("");
2830
- return lines.join("\n");
2267
+ lines.push(`export type ${isInlineOnlyModel4(model) ? typeName : typeName + "Data"}Validated = z.infer<typeof ${schemaName}>;`);
2268
+ return lines.join("\n") + "\n";
2831
2269
  }
2832
2270
 
2833
2271
  // src/codegen/generators/customer-profile-types.ts
@@ -2882,1887 +2320,42 @@ function getFieldTypeImportsForFields2(fields) {
2882
2320
  return imports;
2883
2321
  }
2884
2322
 
2885
- // src/codegen/generators/swift-customer-profile.ts
2886
- function generateSwiftCustomerProfileFile(schema) {
2887
- const typeName = "CustomerProfile";
2888
- const fields = schema.fields ?? [];
2889
- const lines = [];
2890
- lines.push("//");
2891
- lines.push("// CustomerProfile.swift");
2892
- lines.push("//");
2893
- lines.push(
2894
- `// Generated from customer profile schema (version ${schema.version})`
2895
- );
2896
- lines.push("//");
2897
- lines.push("// @generated by foir \u2014 DO NOT EDIT MANUALLY");
2898
- lines.push("//");
2899
- lines.push("");
2900
- lines.push("import Foundation");
2901
- lines.push("");
2902
- lines.push(generateFieldsEnum2(typeName, fields));
2903
- lines.push("");
2904
- lines.push(generateDataStruct2(typeName, fields));
2905
- lines.push("");
2906
- lines.push(generateSerializationExtension2(typeName, fields));
2907
- return lines.join("\n");
2908
- }
2909
- function generateFieldsEnum2(typeName, fields) {
2910
- const lines = [];
2911
- lines.push(`// MARK: - ${typeName} Field Keys`);
2912
- lines.push("");
2913
- lines.push(`enum ${typeName}Fields {`);
2914
- for (const field of fields) {
2915
- lines.push(` static let ${field.key} = "${field.key}"`);
2916
- }
2917
- lines.push("}");
2918
- return lines.join("\n");
2919
- }
2920
- function generateDataStruct2(typeName, fields) {
2921
- const lines = [];
2922
- lines.push(`// MARK: - ${typeName} Data`);
2923
- lines.push("");
2924
- lines.push(`struct ${typeName}Data {`);
2925
- for (const field of fields) {
2926
- const { type, isOptional } = getSwiftFieldType(field);
2927
- const optionalSuffix = isOptional ? "?" : "";
2928
- lines.push(` var ${field.key}: ${type}${optionalSuffix}`);
2929
- }
2930
- lines.push("}");
2931
- return lines.join("\n");
2932
- }
2933
- function generateSerializationExtension2(typeName, fields) {
2934
- const lines = [];
2935
- lines.push(`// MARK: - ${typeName} Serialization`);
2936
- lines.push("");
2937
- lines.push(`extension ${typeName}Data {`);
2938
- lines.push(" func toSyncData() -> [String: Any] {");
2939
- const requiredFields = fields.filter((f) => {
2940
- const { isOptional } = getSwiftFieldType(f);
2941
- return !isOptional;
2942
- });
2943
- const optionalFields = fields.filter((f) => {
2944
- const { isOptional } = getSwiftFieldType(f);
2945
- return isOptional;
2946
- });
2947
- if (requiredFields.length > 0) {
2948
- if (optionalFields.length === 0) {
2949
- lines.push(" return [");
2950
- requiredFields.forEach((f, i) => {
2951
- const comma = i < requiredFields.length - 1 ? "," : "";
2952
- lines.push(
2953
- ` ${typeName}Fields.${f.key}: ${toSyncValueExpr2(f)}${comma}`
2954
- );
2955
- });
2956
- lines.push(" ]");
2957
- } else {
2958
- lines.push(" var data: [String: Any] = [");
2959
- requiredFields.forEach((f, i) => {
2960
- const comma = i < requiredFields.length - 1 ? "," : "";
2961
- lines.push(
2962
- ` ${typeName}Fields.${f.key}: ${toSyncValueExpr2(f)}${comma}`
2963
- );
2323
+ // src/codegen/write-files.ts
2324
+ import { mkdir, writeFile } from "fs/promises";
2325
+ import { dirname as dirname2 } from "path";
2326
+ async function writeGeneratedFile(filePath, content, usePrettier = true) {
2327
+ await mkdir(dirname2(filePath), { recursive: true });
2328
+ let formattedContent = content;
2329
+ const isSwift = filePath.endsWith(".swift");
2330
+ if (usePrettier && !isSwift) {
2331
+ try {
2332
+ const prettier = await import("prettier");
2333
+ const parser = filePath.endsWith(".graphql") ? "graphql" : "typescript";
2334
+ formattedContent = await prettier.format(content, {
2335
+ parser,
2336
+ semi: true,
2337
+ singleQuote: true,
2338
+ trailingComma: "es5",
2339
+ printWidth: 100
2964
2340
  });
2965
- lines.push(" ]");
2966
- for (const f of optionalFields) {
2967
- lines.push(
2968
- ` if let ${f.key} { data[${typeName}Fields.${f.key}] = ${toSyncValueExprForOptional2(f)} }`
2969
- );
2970
- }
2971
- lines.push(" return data");
2972
- }
2973
- } else {
2974
- lines.push(" var data: [String: Any] = [:]");
2975
- for (const f of optionalFields) {
2976
- lines.push(
2977
- ` if let ${f.key} { data[${typeName}Fields.${f.key}] = ${toSyncValueExprForOptional2(f)} }`
2978
- );
2341
+ } catch {
2979
2342
  }
2980
- lines.push(" return data");
2981
2343
  }
2982
- lines.push(" }");
2983
- lines.push("");
2984
- lines.push(
2985
- " static func fromSyncData(_ data: [String: Any]) -> " + typeName + "Data {"
2986
- );
2987
- lines.push(` ${typeName}Data(`);
2988
- fields.forEach((field, i) => {
2989
- const comma = i < fields.length - 1 ? "," : "";
2990
- const { isOptional, mapping } = getSwiftFieldType(field);
2991
- lines.push(
2992
- ` ${field.key}: ${fromSyncDataExpr2(field, typeName, isOptional, mapping)}${comma}`
2993
- );
2994
- });
2995
- lines.push(" )");
2996
- lines.push(" }");
2997
- lines.push("}");
2998
- lines.push("");
2999
- return lines.join("\n");
2344
+ await writeFile(filePath, formattedContent, "utf-8");
3000
2345
  }
3001
- function toSyncValueExpr2(field) {
3002
- const mapping = SWIFT_FIELD_TYPE_MAPPING[field.type];
3003
- if (mapping?.needsSharedType) {
3004
- return `${field.key}.toSyncData()`;
3005
- }
3006
- return field.key;
3007
- }
3008
- function toSyncValueExprForOptional2(field) {
3009
- const mapping = SWIFT_FIELD_TYPE_MAPPING[field.type];
3010
- if (mapping?.needsSharedType) {
3011
- return `${field.key}.toSyncData()`;
3012
- }
3013
- return field.key;
3014
- }
3015
- function fromSyncDataExpr2(field, typeName, isOptional, mapping) {
3016
- const accessor = `data[${typeName}Fields.${field.key}]`;
3017
- if (!mapping) {
3018
- return isOptional ? `${accessor}` : `${accessor} ?? nil`;
3019
- }
3020
- if (mapping.needsSharedType) {
3021
- const dictCast = `${accessor} as? [String: Any]`;
3022
- if (isOptional) {
3023
- return `(${dictCast}).map { ${mapping.type}.fromSyncData($0) }`;
3024
- }
3025
- return `${mapping.type}.fromSyncData(${dictCast} ?? [:])`;
3026
- }
3027
- if (field.type === "json") {
3028
- return isOptional ? accessor : `${accessor}`;
3029
- }
3030
- if (isOptional) {
3031
- return `${accessor} ${mapping.castExpression}`;
3032
- }
3033
- return `${accessor} ${mapping.castExpression} ?? ${mapping.defaultValue}`;
3034
- }
3035
-
3036
- // src/codegen/generators/customer-profile-documents.ts
3037
- function generateCustomerProfileDocuments() {
3038
- return `# Generated GraphQL operations for Customer Profiles
3039
- # @generated by foir \u2014 DO NOT EDIT MANUALLY
3040
-
3041
- fragment CustomerProfileFields on CustomerProfile {
3042
- id
3043
- customerId
3044
- data
3045
- createdAt
3046
- updatedAt
3047
- }
3048
-
3049
- query GetMyProfile {
3050
- myProfile {
3051
- ...CustomerProfileFields
3052
- resolved
3053
- }
3054
- }
3055
-
3056
- query GetCustomerProfile($customerId: ID!) {
3057
- customerProfile(customerId: $customerId) {
3058
- ...CustomerProfileFields
3059
- resolved
3060
- }
3061
- }
3062
-
3063
- mutation SetMyProfile($data: JSON!) {
3064
- setMyProfile(data: $data) {
3065
- ...CustomerProfileFields
3066
- }
3067
- }
3068
-
3069
- mutation UpdateCustomerProfile($customerId: ID, $data: JSON!) {
3070
- updateCustomerProfile(customerId: $customerId, input: { data: $data }) {
3071
- ...CustomerProfileFields
3072
- }
3073
- }
3074
- `;
3075
- }
3076
-
3077
- // src/codegen/generators/static-documents.ts
3078
- var HEADER = "# @generated by foir \u2014 DO NOT EDIT MANUALLY";
3079
- function authDocument() {
3080
- return `# Customer authentication operations
3081
- ${HEADER}
3082
-
3083
- mutation CustomerLogin($email: String!, $password: String!) {
3084
- customerLogin(email: $email, password: $password) {
3085
- success
3086
- accessToken
3087
- refreshToken
3088
- user { id email status }
3089
- }
3090
- }
3091
-
3092
- mutation CustomerRegister($email: String!, $password: String!) {
3093
- customerRegister(email: $email, password: $password) {
3094
- success
3095
- accessToken
3096
- refreshToken
3097
- user { id email status }
3098
- emailVerificationRequired
3099
- }
3100
- }
3101
-
3102
- mutation CustomerRequestOTP($email: String!) {
3103
- customerRequestOTP(email: $email) {
3104
- success
3105
- expiresAt
3106
- message
3107
- }
3108
- }
3109
-
3110
- mutation CustomerLoginOTP($email: String!, $otp: String!) {
3111
- customerLoginOTP(email: $email, otp: $otp) {
3112
- success
3113
- accessToken
3114
- refreshToken
3115
- user { id email status }
3116
- }
3117
- }
3118
-
3119
- mutation CustomerRefreshToken($refreshToken: String!) {
3120
- customerRefreshToken(refreshToken: $refreshToken) {
3121
- success
3122
- accessToken
3123
- refreshToken
3124
- }
3125
- }
3126
-
3127
- mutation CustomerRequestPasswordReset($email: String!) {
3128
- customerRequestPasswordReset(email: $email) {
3129
- success
3130
- message
3131
- }
3132
- }
3133
-
3134
- mutation CustomerResetPassword($token: String!, $newPassword: String!) {
3135
- customerResetPassword(token: $token, newPassword: $newPassword) {
3136
- success
3137
- message
3138
- }
3139
- }
3140
-
3141
- mutation CustomerUpdatePassword($currentPassword: String!, $newPassword: String!) {
3142
- customerUpdatePassword(currentPassword: $currentPassword, newPassword: $newPassword) {
3143
- success
3144
- message
3145
- }
3146
- }
3147
-
3148
- mutation CustomerVerifyEmail($token: String!) {
3149
- customerVerifyEmail(token: $token) {
3150
- success
3151
- user { id email }
3152
- message
3153
- }
3154
- }
3155
-
3156
- mutation CustomerResendVerificationEmail {
3157
- customerResendVerificationEmail {
3158
- success
3159
- message
3160
- }
3161
- }
3162
-
3163
- mutation CustomerLogout {
3164
- customerLogout {
3165
- success
3166
- message
3167
- }
3168
- }
3169
-
3170
- query AuthConfig($tenantId: ID) {
3171
- authConfig(tenantId: $tenantId) {
3172
- authMethods
3173
- passwordPolicy {
3174
- minLength
3175
- requireUppercase
3176
- requireLowercase
3177
- requireNumbers
3178
- requireSpecialChars
3179
- requireSpecial
3180
- }
3181
- publicRegistrationEnabled
3182
- }
3183
- }
3184
-
3185
- query CurrentUser {
3186
- currentUser {
3187
- id
3188
- email
3189
- emailVerified
3190
- status
3191
- userType
3192
- }
3193
- }
3194
- `;
3195
- }
3196
- function authProvidersDocument() {
3197
- return `# Auth provider operations
3198
- ${HEADER}
3199
-
3200
- query AuthProviders {
3201
- authProviders {
3202
- id
3203
- key
3204
- name
3205
- type
3206
- enabled
3207
- isDefault
3208
- priority
3209
- }
3210
- }
3211
-
3212
- query DefaultAuthProvider {
3213
- defaultAuthProvider {
3214
- id
3215
- key
3216
- name
3217
- type
3218
- enabled
3219
- isDefault
3220
- priority
3221
- }
3222
- }
3223
-
3224
- mutation CustomerLoginWithProvider($input: ProviderLoginInput!) {
3225
- customerLoginWithProvider(input: $input) {
3226
- method
3227
- providerId
3228
- providerKey
3229
- redirectUrl
3230
- accessToken
3231
- refreshToken
3232
- user { id email userType }
3233
- otpSent
3234
- email
3235
- expiresAt
3236
- state
3237
- }
3238
- }
3239
-
3240
- mutation CustomerProviderCallback($input: ProviderCallbackInput!) {
3241
- customerProviderCallback(input: $input) {
3242
- accessToken
3243
- refreshToken
3244
- user { id email userType }
3245
- isNewCustomer
3246
- providerAccessToken
3247
- providerAccessTokenExpiresIn
3248
- }
3249
- }
3250
-
3251
- mutation CustomerProviderVerifyOTP($input: ProviderOTPVerifyInput!) {
3252
- customerProviderVerifyOTP(input: $input) {
3253
- accessToken
3254
- refreshToken
3255
- user { id email userType }
3256
- isNewCustomer
3257
- providerAccessToken
3258
- providerAccessTokenExpiresIn
3259
- }
3260
- }
3261
- `;
3262
- }
3263
- function filesDocument() {
3264
- return `# File management operations
3265
- ${HEADER}
3266
-
3267
- query GetFile($id: ID!) {
3268
- file(id: $id) {
3269
- id
3270
- filename
3271
- mimeType
3272
- size
3273
- url
3274
- source
3275
- status
3276
- metadata
3277
- width
3278
- height
3279
- blurhash
3280
- dominantColor
3281
- duration
3282
- thumbnailUrl
3283
- previewUrl
3284
- altText
3285
- caption
3286
- description
3287
- isImage
3288
- isVideo
3289
- createdAt
3290
- updatedAt
3291
- }
3292
- }
3293
-
3294
- mutation CreateFileUpload(
3295
- $filename: String!
3296
- $mimeType: String!
3297
- $size: Int!
3298
- $folder: String
3299
- $metadata: JSON
3300
- ) {
3301
- createFileUpload(
3302
- filename: $filename
3303
- mimeType: $mimeType
3304
- size: $size
3305
- folder: $folder
3306
- metadata: $metadata
3307
- ) {
3308
- uploadId
3309
- uploadUrl
3310
- expiresAt
3311
- }
3312
- }
3313
-
3314
- mutation ConfirmFileUpload($uploadId: ID!) {
3315
- confirmFileUpload(uploadId: $uploadId) {
3316
- id
3317
- filename
3318
- mimeType
3319
- size
3320
- url
3321
- source
3322
- status
3323
- createdAt
3324
- }
3325
- }
3326
- `;
3327
- }
3328
- function syncDocument() {
3329
- return `# Sync engine operations (Layer 1: delta sync protocol)
3330
- ${HEADER}
3331
-
3332
- query SyncPull($modelKey: String!, $since: String!, $limit: Int) {
3333
- syncPull(modelKey: $modelKey, since: $since, limit: $limit) {
3334
- items {
3335
- id
3336
- modelKey
3337
- naturalKey
3338
- data
3339
- metadata
3340
- syncVersion
3341
- updatedAt
3342
- deleted
3343
- }
3344
- cursor
3345
- hasMore
3346
- }
3347
- }
3348
-
3349
- mutation SyncPush($items: [SyncPushItemInput!]!) {
3350
- syncPush(items: $items) {
3351
- items {
3352
- clientId
3353
- serverId
3354
- syncVersion
3355
- status
3356
- serverData
3357
- serverSyncVersion
3358
- error
3359
- }
3360
- }
3361
- }
3362
-
3363
- subscription RecordChanged($modelKey: String!) {
3364
- recordChanged(modelKey: $modelKey) {
3365
- type
3366
- recordId
3367
- modelKey
3368
- naturalKey
3369
- syncVersion
3370
- data
3371
- updatedBy
3372
- timestamp
3373
- }
3374
- }
3375
- `;
3376
- }
3377
- function notificationsDocument() {
3378
- return `# Customer notification operations
3379
- ${HEADER}
3380
-
3381
- query CustomerNotifications(
3382
- $unreadOnly: Boolean
3383
- $category: String
3384
- $limit: Int
3385
- $offset: Int
3386
- ) {
3387
- customerNotifications(
3388
- unreadOnly: $unreadOnly
3389
- category: $category
3390
- limit: $limit
3391
- offset: $offset
3392
- ) {
3393
- items {
3394
- id
3395
- type
3396
- category
3397
- title
3398
- message
3399
- actionUrl
3400
- imageUrl
3401
- metadata
3402
- alertChannels
3403
- isRead
3404
- readAt
3405
- createdAt
3406
- }
3407
- total
3408
- unreadCount
3409
- hasMore
3410
- }
3411
- }
3412
-
3413
- query CustomerUnreadCount($category: String) {
3414
- customerUnreadCount(category: $category)
3415
- }
3416
-
3417
- query NotificationPreferences {
3418
- notificationPreferences {
3419
- id
3420
- category
3421
- channel
3422
- enabled
3423
- }
3424
- }
3425
-
3426
- mutation SendNotification($input: SendNotificationInput!) {
3427
- sendNotification(input: $input) {
3428
- id
3429
- type
3430
- category
3431
- title
3432
- message
3433
- actionUrl
3434
- imageUrl
3435
- metadata
3436
- alertChannels
3437
- isRead
3438
- readAt
3439
- createdAt
3440
- }
3441
- }
3442
-
3443
- mutation SendBulkNotifications($input: SendBulkNotificationsInput!) {
3444
- sendBulkNotifications(input: $input) {
3445
- sent
3446
- failed
3447
- }
3448
- }
3449
-
3450
- mutation MarkCustomerNotificationRead($id: ID!) {
3451
- markCustomerNotificationRead(id: $id) {
3452
- id
3453
- isRead
3454
- readAt
3455
- }
3456
- }
3457
-
3458
- mutation MarkAllCustomerNotificationsRead($category: String) {
3459
- markAllCustomerNotificationsRead(category: $category)
3460
- }
3461
-
3462
- mutation RegisterDeviceToken($input: RegisterDeviceTokenInput!) {
3463
- registerDeviceToken(input: $input) {
3464
- id
3465
- platform
3466
- token
3467
- deviceName
3468
- isActive
3469
- createdAt
3470
- }
3471
- }
3472
-
3473
- mutation UnregisterDeviceToken($token: String!) {
3474
- unregisterDeviceToken(token: $token)
3475
- }
3476
-
3477
- mutation UpdateNotificationPreference($input: UpdateNotificationPreferenceInput!) {
3478
- updateNotificationPreference(input: $input) {
3479
- id
3480
- category
3481
- channel
3482
- enabled
3483
- }
3484
- }
3485
- `;
3486
- }
3487
- function operationsDocument() {
3488
- return `# Operation execution operations
3489
- ${HEADER}
3490
-
3491
- query GetOperationExecution($id: ID!) {
3492
- operationExecution(id: $id) {
3493
- id
3494
- operationKey
3495
- status
3496
- result
3497
- error
3498
- startedAt
3499
- completedAt
3500
- durationMs
3501
- metadata
3502
- createdAt
3503
- }
3504
- }
3505
-
3506
- query ListOperationExecutions(
3507
- $operationKey: String
3508
- $status: OperationExecutionStatus
3509
- $limit: Int
3510
- $offset: Int
3511
- ) {
3512
- operationExecutions(
3513
- operationKey: $operationKey
3514
- status: $status
3515
- limit: $limit
3516
- offset: $offset
3517
- ) {
3518
- items {
3519
- id
3520
- operationKey
3521
- status
3522
- durationMs
3523
- startedAt
3524
- completedAt
3525
- metadata
3526
- createdAt
3527
- }
3528
- total
3529
- }
3530
- }
3531
-
3532
- mutation ExecuteOperation($input: ExecuteOperationInput!) {
3533
- executeOperation(input: $input) {
3534
- success
3535
- result
3536
- error {
3537
- code
3538
- message
3539
- }
3540
- executionId
3541
- durationMs
3542
- metadata
3543
- }
3544
- }
3545
-
3546
- mutation CancelOperationExecution($id: ID!) {
3547
- cancelOperationExecution(id: $id) {
3548
- id
3549
- status
3550
- }
3551
- }
3552
- `;
3553
- }
3554
- function schedulesDocument() {
3555
- return `# Schedule management operations
3556
- ${HEADER}
3557
-
3558
- query GetSchedule($key: String!) {
3559
- schedule(key: $key) {
3560
- id
3561
- key
3562
- name
3563
- description
3564
- cron
3565
- cronDescription
3566
- timezone
3567
- targetType
3568
- isActive
3569
- lastRunAt
3570
- lastRunStatus
3571
- nextRunAt
3572
- runCount
3573
- failureCount
3574
- createdAt
3575
- updatedAt
3576
- }
3577
- }
3578
-
3579
- query ListSchedules(
3580
- $targetType: ScheduleTargetType
3581
- $isActive: Boolean
3582
- $limit: Int
3583
- $offset: Int
3584
- ) {
3585
- schedules(
3586
- targetType: $targetType
3587
- isActive: $isActive
3588
- limit: $limit
3589
- offset: $offset
3590
- ) {
3591
- items {
3592
- id
3593
- key
3594
- name
3595
- cron
3596
- cronDescription
3597
- timezone
3598
- isActive
3599
- lastRunAt
3600
- lastRunStatus
3601
- nextRunAt
3602
- runCount
3603
- failureCount
3604
- createdAt
3605
- }
3606
- total
3607
- }
3608
- }
3609
-
3610
- mutation CreateSchedule($input: CreateScheduleInput!) {
3611
- createSchedule(input: $input) {
3612
- id
3613
- key
3614
- name
3615
- cron
3616
- isActive
3617
- createdAt
3618
- }
3619
- }
3620
-
3621
- mutation UpdateSchedule($key: String!, $input: UpdateScheduleInput!) {
3622
- updateSchedule(key: $key, input: $input) {
3623
- id
3624
- key
3625
- name
3626
- cron
3627
- isActive
3628
- updatedAt
3629
- }
3630
- }
3631
-
3632
- mutation DeleteSchedule($key: String!) {
3633
- deleteSchedule(key: $key)
3634
- }
3635
-
3636
- mutation PauseSchedule($key: String!) {
3637
- pauseSchedule(key: $key) {
3638
- id
3639
- key
3640
- isActive
3641
- }
3642
- }
3643
-
3644
- mutation ResumeSchedule($key: String!) {
3645
- resumeSchedule(key: $key) {
3646
- id
3647
- key
3648
- isActive
3649
- }
3650
- }
3651
-
3652
- mutation TriggerSchedule($key: String!) {
3653
- triggerSchedule(key: $key) {
3654
- success
3655
- jobId
3656
- error
3657
- }
3658
- }
3659
- `;
3660
- }
3661
- function sharingDocument() {
3662
- return `# Sharing operations
3663
- ${HEADER}
3664
-
3665
- fragment ShareFields on Share {
3666
- id
3667
- resourceType
3668
- recordId
3669
- fileId
3670
- permission
3671
- status
3672
- sharedWithCustomerId
3673
- acceptedAt
3674
- declinedAt
3675
- expiresAt
3676
- createdAt
3677
- createdBy
3678
- revokedAt
3679
- }
3680
-
3681
- query GetShares($resourceType: ShareResourceType!, $resourceId: ID!, $status: ShareStatus) {
3682
- shares(resourceType: $resourceType, resourceId: $resourceId, status: $status) {
3683
- ...ShareFields
3684
- }
3685
- }
3686
-
3687
- query SharedWithMe(
3688
- $resourceType: ShareResourceType
3689
- $modelKey: String
3690
- $status: ShareStatus
3691
- $limit: Int
3692
- $offset: Int
3693
- ) {
3694
- sharedWithMe(
3695
- resourceType: $resourceType
3696
- modelKey: $modelKey
3697
- status: $status
3698
- limit: $limit
3699
- offset: $offset
3700
- ) {
3701
- ...ShareFields
3702
- }
3703
- }
3704
-
3705
- mutation ShareRecord(
3706
- $recordId: ID!
3707
- $sharedWithCustomerId: ID!
3708
- $permission: SharePermission!
3709
- $expiresAt: DateTime
3710
- ) {
3711
- shareRecord(
3712
- recordId: $recordId
3713
- sharedWithCustomerId: $sharedWithCustomerId
3714
- permission: $permission
3715
- expiresAt: $expiresAt
3716
- ) {
3717
- ...ShareFields
3718
- }
3719
- }
3720
-
3721
- mutation ShareFile(
3722
- $fileId: ID!
3723
- $sharedWithCustomerId: ID!
3724
- $permission: SharePermission!
3725
- $expiresAt: DateTime
3726
- ) {
3727
- shareFile(
3728
- fileId: $fileId
3729
- sharedWithCustomerId: $sharedWithCustomerId
3730
- permission: $permission
3731
- expiresAt: $expiresAt
3732
- ) {
3733
- ...ShareFields
3734
- }
3735
- }
3736
-
3737
- mutation AcceptShare($shareId: ID!) {
3738
- acceptShare(shareId: $shareId) {
3739
- ...ShareFields
3740
- }
3741
- }
3742
-
3743
- mutation DeclineShare($shareId: ID!) {
3744
- declineShare(shareId: $shareId) {
3745
- ...ShareFields
3746
- }
3747
- }
3748
-
3749
- mutation RevokeShare($shareId: ID!) {
3750
- revokeShare(shareId: $shareId) {
3751
- ...ShareFields
3752
- }
3753
- }
3754
-
3755
- mutation UpdateSharePermission($shareId: ID!, $permission: SharePermission!) {
3756
- updateSharePermission(shareId: $shareId, permission: $permission) {
3757
- ...ShareFields
3758
- }
3759
- }
3760
- `;
3761
- }
3762
- function embeddingsDocument() {
3763
- return `# Vector embedding operations (search, write, delete)
3764
- ${HEADER}
3765
-
3766
- query SearchEmbeddings($input: SearchEmbeddingsInput!) {
3767
- searchEmbeddings(input: $input) {
3768
- recordId
3769
- modelKey
3770
- naturalKey
3771
- key
3772
- similarity
3773
- metadata
3774
- }
3775
- }
3776
-
3777
- mutation WriteEmbeddings($input: WriteEmbeddingsInput!) {
3778
- writeEmbeddings(input: $input) {
3779
- written
3780
- errors {
3781
- recordId
3782
- key
3783
- message
3784
- }
3785
- }
3786
- }
3787
-
3788
- mutation DeleteEmbeddings($input: DeleteEmbeddingsInput!) {
3789
- deleteEmbeddings(input: $input) {
3790
- deleted
3791
- }
3792
- }
3793
- `;
3794
- }
3795
- function analyticsDocument() {
3796
- return `# Analytics & conversion tracking operations
3797
- ${HEADER}
3798
-
3799
- mutation RecordConversion($input: RecordConversionInput!) {
3800
- recordConversion(input: $input) {
3801
- success
3802
- }
3803
- }
3804
- `;
3805
- }
3806
- function generateStaticDocuments(domains) {
3807
- const files = [];
3808
- if (domains.auth)
3809
- files.push({ filename: "auth.graphql", content: authDocument() });
3810
- if (domains.authProviders)
3811
- files.push({
3812
- filename: "auth-providers.graphql",
3813
- content: authProvidersDocument()
3814
- });
3815
- if (domains.files)
3816
- files.push({ filename: "files.graphql", content: filesDocument() });
3817
- if (domains.sync)
3818
- files.push({ filename: "sync.graphql", content: syncDocument() });
3819
- if (domains.notifications)
3820
- files.push({
3821
- filename: "notifications.graphql",
3822
- content: notificationsDocument()
3823
- });
3824
- if (domains.operations)
3825
- files.push({
3826
- filename: "operations.graphql",
3827
- content: operationsDocument()
3828
- });
3829
- if (domains.schedules)
3830
- files.push({ filename: "schedules.graphql", content: schedulesDocument() });
3831
- if (domains.sharing)
3832
- files.push({ filename: "sharing.graphql", content: sharingDocument() });
3833
- if (domains.embeddings)
3834
- files.push({
3835
- filename: "embeddings.graphql",
3836
- content: embeddingsDocument()
3837
- });
3838
- if (domains.analytics)
3839
- files.push({ filename: "analytics.graphql", content: analyticsDocument() });
3840
- return files;
3841
- }
3842
-
3843
- // src/codegen/generators/typed-operations-common.ts
3844
- function generateTypedOperationsCommon(typesRelPath) {
3845
- return `/**
3846
- * Shared types for typed GraphQL operations.
3847
- *
3848
- * @generated by foir \u2014 DO NOT EDIT MANUALLY
3849
- */
3850
-
3851
- import type { JsonValue } from '${typesRelPath}/field-types.js';
3852
-
3853
- /** A record with strongly-typed data. */
3854
- export interface BaseRecord<T> {
3855
- id: string;
3856
- modelKey: string;
3857
- naturalKey: string | null;
3858
- data: T;
3859
- metadata: Record<string, JsonValue> | null;
3860
- publishedVersionNumber: number | null;
3861
- publishedAt: string | null;
3862
- versionNumber: number | null;
3863
- changeDescription: string | null;
3864
- createdAt: string;
3865
- updatedAt: string;
3866
- }
3867
-
3868
- /** Resolved content wrapping strongly-typed data. */
3869
- export interface ResolvedContent<T> {
3870
- content: T;
3871
- record: { id: string; modelKey: string; naturalKey: string | null };
3872
- version: { id: string; versionNumber: number } | null;
3873
- }
3874
-
3875
- /** Paginated list result. */
3876
- export interface PaginatedResult<T> {
3877
- items: (BaseRecord<T> & { resolved: ResolvedContent<T> | null })[];
3878
- total: number;
3879
- }
3880
-
3881
- /** Result of a createRecord mutation. */
3882
- export interface CreateRecordResult<T> {
3883
- record: BaseRecord<T>;
3884
- }
3885
-
3886
- /** Result of an updateRecord mutation. */
3887
- export interface UpdateRecordResult<T> {
3888
- record: BaseRecord<T>;
3889
- matched: boolean;
3890
- }
3891
-
3892
- /** Result of a deleteRecord mutation. */
3893
- export interface DeleteRecordResult {
3894
- id: string;
3895
- }
3896
-
3897
- /** Share resource type. */
3898
- export interface ShareResult {
3899
- id: string;
3900
- resourceType: string;
3901
- permission: string;
3902
- status: string;
3903
- acceptedAt: string | null;
3904
- declinedAt: string | null;
3905
- expiresAt: string | null;
3906
- createdAt: string;
3907
- revokedAt: string | null;
3908
- }
3909
-
3910
- /** A share that includes the shared record. */
3911
- export interface ShareWithRecord<T> extends ShareResult {
3912
- record: BaseRecord<T>;
3913
- }
3914
-
3915
- /** Field selection for resolved content \u2014 pick or omit specific fields. */
3916
- export interface FieldSelection<T = Record<string, unknown>> {
3917
- /** Include only these field keys (mutually exclusive with omit) */
3918
- pick?: (keyof T & string)[];
3919
- /** Exclude these field keys (mutually exclusive with omit) */
3920
- omit?: (keyof T & string)[];
3921
- }
3922
- `;
3923
- }
3924
-
3925
- // src/codegen/generators/typed-operations.ts
3926
- import path from "path";
3927
- function generateTypedOperations(model, typesRelPath) {
3928
- const typeName = toPascalCase(model.key);
3929
- const upperSnake = toUpperSnakeCase(model.key);
3930
- const pluralName = model.pluralName ? toPascalCase(model.pluralName.replace(/\s+/g, "")) : `${typeName}s`;
3931
- const pluralUpperSnake = model.pluralName ? toUpperSnakeCase(model.pluralName.replace(/\s+/g, "")) : `${upperSnake}S`;
3932
- const dataType = `${typeName}Data`;
3933
- const lines = [];
3934
- lines.push(`/**
3935
- * Typed operations for ${model.name ?? model.key}
3936
- *
3937
- * @generated by foir \u2014 DO NOT EDIT MANUALLY
3938
- */
3939
-
3940
- import type { JsonValue } from '${typesRelPath}/field-types.js';
3941
- import type { ${dataType} } from '${typesRelPath}/models/${model.key}.js';
3942
- import type {
3943
- BaseRecord,
3944
- ResolvedContent,
3945
- PaginatedResult,
3946
- CreateRecordResult,
3947
- UpdateRecordResult,
3948
- DeleteRecordResult,
3949
- FieldSelection,${model.config.sharing?.enabled ? "\n ShareResult,\n ShareWithRecord," : ""}
3950
- } from './_common.js';
3951
- `);
3952
- lines.push(`export type ${typeName}Record = BaseRecord<${dataType}>;`);
3953
- lines.push("");
3954
- lines.push(`export const GET_${upperSnake} = \`
3955
- query Get${typeName}($id: ID!, $locale: String, $preview: Boolean, $fields: FieldSelectionInput) {
3956
- record(id: $id) {
3957
- id modelKey naturalKey data metadata
3958
- publishedVersionNumber publishedAt versionNumber changeDescription
3959
- createdAt updatedAt
3960
- resolved(locale: $locale, preview: $preview, fields: $fields) {
3961
- content
3962
- record { id modelKey naturalKey }
3963
- version { id versionNumber }
3964
- }
3965
- }
3966
- }
3967
- \`;`);
3968
- lines.push("");
3969
- lines.push(`export interface Get${typeName}Variables {
3970
- id: string;
3971
- locale?: string;
3972
- preview?: boolean;
3973
- fields?: FieldSelection<${dataType}>;
3974
- }`);
3975
- lines.push("");
3976
- lines.push(`export interface Get${typeName}Result {
3977
- record: ${typeName}Record & {
3978
- resolved: ResolvedContent<${dataType}> | null;
3979
- } | null;
3980
- }`);
3981
- lines.push("");
3982
- lines.push(`export const GET_${upperSnake}_BY_KEY = \`
3983
- query Get${typeName}ByKey($naturalKey: String!, $locale: String, $preview: Boolean, $fields: FieldSelectionInput) {
3984
- recordByKey(modelKey: "${model.key}", naturalKey: $naturalKey) {
3985
- id modelKey naturalKey data metadata
3986
- publishedVersionNumber publishedAt versionNumber changeDescription
3987
- createdAt updatedAt
3988
- resolved(locale: $locale, preview: $preview, fields: $fields) {
3989
- content
3990
- record { id modelKey naturalKey }
3991
- version { id versionNumber }
3992
- }
3993
- }
3994
- }
3995
- \`;`);
3996
- lines.push("");
3997
- lines.push(`export interface Get${typeName}ByKeyVariables {
3998
- naturalKey: string;
3999
- locale?: string;
4000
- preview?: boolean;
4001
- fields?: FieldSelection<${dataType}>;
4002
- }`);
4003
- lines.push("");
4004
- lines.push(`export interface Get${typeName}ByKeyResult {
4005
- recordByKey: ${typeName}Record & {
4006
- resolved: ResolvedContent<${dataType}> | null;
4007
- } | null;
4008
- }`);
4009
- lines.push("");
4010
- lines.push(`export const LIST_${pluralUpperSnake} = \`
4011
- query List${pluralName}($limit: Int, $offset: Int, $filters: [FilterInput!], $sort: SortInput, $locale: String, $preview: Boolean, $fields: FieldSelectionInput) {
4012
- records(modelKey: "${model.key}", limit: $limit, offset: $offset, filters: $filters, sort: $sort) {
4013
- items {
4014
- id modelKey naturalKey data metadata
4015
- publishedVersionNumber publishedAt versionNumber changeDescription
4016
- createdAt updatedAt
4017
- resolved(locale: $locale, preview: $preview, fields: $fields) {
4018
- content
4019
- record { id modelKey naturalKey }
4020
- version { id versionNumber }
4021
- }
4022
- }
4023
- total
4024
- }
4025
- }
4026
- \`;`);
4027
- lines.push("");
4028
- lines.push(`export interface List${pluralName}Variables {
4029
- limit?: number;
4030
- offset?: number;
4031
- filters?: Array<{ field: string; operator: string; value: JsonValue }>;
4032
- sort?: { field: string; direction: 'ASC' | 'DESC' };
4033
- locale?: string;
4034
- preview?: boolean;
4035
- fields?: FieldSelection<${dataType}>;
4036
- }`);
4037
- lines.push("");
4038
- lines.push(`export interface List${pluralName}Result {
4039
- records: PaginatedResult<${dataType}>;
4040
- }`);
4041
- lines.push("");
4042
- lines.push(`export const CREATE_${upperSnake} = \`
4043
- mutation Create${typeName}($input: CreateRecordInput!) {
4044
- createRecord(input: $input) {
4045
- record {
4046
- id modelKey naturalKey data metadata createdAt updatedAt
4047
- }
4048
- }
4049
- }
4050
- \`;`);
4051
- lines.push("");
4052
- lines.push(`export interface Create${typeName}Variables {
4053
- input: {
4054
- modelKey: string;
4055
- naturalKey?: string;
4056
- data: Partial<${dataType}>;
4057
- metadata?: Record<string, JsonValue>;
4058
- };
4059
- }`);
4060
- lines.push("");
4061
- lines.push(`export interface Create${typeName}Result {
4062
- createRecord: CreateRecordResult<${dataType}>;
4063
- }`);
4064
- lines.push("");
4065
- lines.push(`export const UPDATE_${upperSnake} = \`
4066
- mutation Update${typeName}($input: UpdateRecordInput!) {
4067
- updateRecord(input: $input) {
4068
- record {
4069
- id modelKey naturalKey data metadata createdAt updatedAt
4070
- }
4071
- matched
4072
- }
4073
- }
4074
- \`;`);
4075
- lines.push("");
4076
- lines.push(`export interface Update${typeName}Variables {
4077
- input: {
4078
- id: string;
4079
- data?: Partial<${dataType}>;
4080
- metadata?: Record<string, JsonValue>;
4081
- changeDescription?: string;
4082
- };
4083
- }`);
4084
- lines.push("");
4085
- lines.push(`export interface Update${typeName}Result {
4086
- updateRecord: UpdateRecordResult<${dataType}>;
4087
- }`);
4088
- lines.push("");
4089
- lines.push(`export const DELETE_${upperSnake} = \`
4090
- mutation Delete${typeName}($id: ID!) {
4091
- deleteRecord(id: $id) { id }
4092
- }
4093
- \`;`);
4094
- lines.push("");
4095
- lines.push(`export interface Delete${typeName}Variables {
4096
- id: string;
4097
- }`);
4098
- lines.push("");
4099
- lines.push(`export interface Delete${typeName}Result {
4100
- deleteRecord: DeleteRecordResult;
4101
- }`);
4102
- lines.push("");
4103
- lines.push(`export const PUBLISH_${upperSnake}_VERSION = \`
4104
- mutation Publish${typeName}Version($versionId: ID!) {
4105
- publishVersion(versionId: $versionId)
4106
- }
4107
- \`;`);
4108
- lines.push("");
4109
- lines.push(`export const UNPUBLISH_${upperSnake} = \`
4110
- mutation Unpublish${typeName}($id: ID!) {
4111
- unpublishRecord(id: $id)
4112
- }
4113
- \`;`);
4114
- lines.push("");
4115
- if (model.config.sharing?.enabled) {
4116
- lines.push(`// --- Sharing operations ---`);
4117
- lines.push("");
4118
- lines.push(`export const SHARE_${upperSnake} = \`
4119
- mutation Share${typeName}($recordId: ID!, $sharedWithCustomerId: ID!, $permission: SharePermission!) {
4120
- shareRecord(recordId: $recordId, sharedWithCustomerId: $sharedWithCustomerId, permission: $permission) {
4121
- id resourceType permission status acceptedAt declinedAt expiresAt createdAt revokedAt
4122
- }
4123
- }
4124
- \`;`);
4125
- lines.push("");
4126
- lines.push(`export interface Share${typeName}Variables {
4127
- recordId: string;
4128
- sharedWithCustomerId: string;
4129
- permission: 'VIEW' | 'EDIT' | 'ADMIN';
4130
- }`);
4131
- lines.push("");
4132
- lines.push(`export interface Share${typeName}Result {
4133
- shareRecord: ShareResult;
4134
- }`);
4135
- lines.push("");
4136
- lines.push(`export const ${upperSnake}_SHARES = \`
4137
- query ${typeName}Shares($resourceId: ID!, $status: ShareStatus) {
4138
- shares(resourceType: RECORD, resourceId: $resourceId, status: $status) {
4139
- id resourceType permission status acceptedAt declinedAt expiresAt createdAt revokedAt
4140
- }
4141
- }
4142
- \`;`);
4143
- lines.push("");
4144
- lines.push(`export const ${pluralUpperSnake}_SHARED_WITH_ME = \`
4145
- query ${pluralName}SharedWithMe($status: ShareStatus) {
4146
- sharedWithMe(resourceType: RECORD, modelKey: "${model.key}", status: $status) {
4147
- id resourceType permission status acceptedAt declinedAt expiresAt createdAt revokedAt
4148
- record {
4149
- id modelKey naturalKey data metadata
4150
- publishedVersionNumber publishedAt versionNumber changeDescription
4151
- createdAt updatedAt
4152
- }
4153
- }
4154
- }
4155
- \`;`);
4156
- lines.push("");
4157
- lines.push(`export interface ${pluralName}SharedWithMeResult {
4158
- sharedWithMe: ShareWithRecord<${dataType}>[];
4159
- }`);
4160
- lines.push("");
4161
- }
4162
- return lines.join("\n");
4163
- }
4164
- function computeTypesRelPath(opsDir, typesDir) {
4165
- const rel = path.relative(opsDir, typesDir).replace(/\\/g, "/");
4166
- return rel.startsWith(".") ? rel : `./${rel}`;
4167
- }
4168
-
4169
- // src/codegen/generators/typed-operations-index.ts
4170
- function generateTypedOperationsIndex(models, hasCustomerProfile) {
4171
- const lines = [];
4172
- lines.push(`/**
4173
- * Typed GraphQL operations.
4174
- *
4175
- * @generated by foir \u2014 DO NOT EDIT MANUALLY
4176
- */
4177
-
4178
- export * from './_common.js';
4179
- `);
4180
- for (const model of models) {
4181
- lines.push(`export * from './${model.key}.js';`);
4182
- }
4183
- if (hasCustomerProfile) {
4184
- lines.push(`export * from './customer-profile.js';`);
4185
- }
4186
- lines.push("");
4187
- return lines.join("\n");
4188
- }
4189
-
4190
- // src/codegen/generators/customer-profile-operations.ts
4191
- function generateCustomerProfileOperations(typesRelPath) {
4192
- return `/**
4193
- * Typed operations for Customer Profiles
4194
- *
4195
- * @generated by foir \u2014 DO NOT EDIT MANUALLY
4196
- */
4197
-
4198
- import type { CustomerProfileData } from '${typesRelPath}/customer-profile.js';
4199
-
4200
- export const GET_MY_PROFILE = \`
4201
- query GetMyProfile {
4202
- myProfile {
4203
- id
4204
- customerId
4205
- data
4206
- createdAt
4207
- updatedAt
4208
- }
4209
- }
4210
- \`;
4211
-
4212
- export interface GetMyProfileResult {
4213
- myProfile: {
4214
- id: string;
4215
- customerId: string;
4216
- data: CustomerProfileData;
4217
- createdAt: string;
4218
- updatedAt: string;
4219
- } | null;
4220
- }
4221
-
4222
- export const GET_CUSTOMER_PROFILE = \`
4223
- query GetCustomerProfile($customerId: ID!) {
4224
- customerProfile(customerId: $customerId) {
4225
- id
4226
- customerId
4227
- data
4228
- createdAt
4229
- updatedAt
4230
- }
4231
- }
4232
- \`;
4233
-
4234
- export interface GetCustomerProfileVariables {
4235
- customerId: string;
4236
- }
4237
-
4238
- export interface GetCustomerProfileResult {
4239
- customerProfile: {
4240
- id: string;
4241
- customerId: string;
4242
- data: CustomerProfileData;
4243
- createdAt: string;
4244
- updatedAt: string;
4245
- } | null;
4246
- }
4247
-
4248
- export const SET_MY_PROFILE = \`
4249
- mutation SetMyProfile($data: JSON!) {
4250
- setMyProfile(data: $data) {
4251
- id
4252
- customerId
4253
- data
4254
- createdAt
4255
- updatedAt
4256
- }
4257
- }
4258
- \`;
4259
-
4260
- export interface SetMyProfileVariables {
4261
- data: Partial<CustomerProfileData>;
4262
- }
4263
-
4264
- export interface SetMyProfileResult {
4265
- setMyProfile: {
4266
- id: string;
4267
- customerId: string;
4268
- data: CustomerProfileData;
4269
- createdAt: string;
4270
- updatedAt: string;
4271
- };
4272
- }
4273
-
4274
- export const UPDATE_CUSTOMER_PROFILE = \`
4275
- mutation UpdateCustomerProfile($customerId: ID, $input: CustomerProfileInput!) {
4276
- updateCustomerProfile(customerId: $customerId, input: $input) {
4277
- id
4278
- customerId
4279
- data
4280
- createdAt
4281
- updatedAt
4282
- }
4283
- }
4284
- \`;
4285
-
4286
- export interface UpdateCustomerProfileVariables {
4287
- customerId?: string;
4288
- input: {
4289
- data: Partial<CustomerProfileData>;
4290
- };
4291
- }
4292
-
4293
- export interface UpdateCustomerProfileResult {
4294
- updateCustomerProfile: {
4295
- id: string;
4296
- customerId: string;
4297
- data: CustomerProfileData;
4298
- createdAt: string;
4299
- updatedAt: string;
4300
- };
4301
- }
4302
-
4303
- export const DELETE_MY_PROFILE = \`
4304
- mutation DeleteMyProfile {
4305
- deleteMyProfile
4306
- }
4307
- \`;
4308
-
4309
- export interface DeleteMyProfileResult {
4310
- deleteMyProfile: boolean;
4311
- }
4312
- `;
4313
- }
4314
-
4315
- // src/codegen/generators/react-hooks.ts
4316
- function generateReactHooks(model) {
4317
- const typeName = toPascalCase(model.key);
4318
- const upperSnake = toUpperSnakeCase(model.key);
4319
- const pluralName = model.pluralName ? toPascalCase(model.pluralName.replace(/\s+/g, "")) : `${typeName}s`;
4320
- const pluralUpperSnake = model.pluralName ? toUpperSnakeCase(model.pluralName.replace(/\s+/g, "")) : `${upperSnake}S`;
4321
- const lines = [];
4322
- lines.push(`/**
4323
- * React Apollo hooks for ${model.name ?? model.key}
4324
- *
4325
- * @generated by foir \u2014 DO NOT EDIT MANUALLY
4326
- */
4327
-
4328
- import { useQuery, useMutation } from '@apollo/client';
4329
- import type { QueryHookOptions, MutationHookOptions } from '@apollo/client';
4330
- import { gql } from '@apollo/client';
4331
- import {
4332
- GET_${upperSnake},
4333
- GET_${upperSnake}_BY_KEY,
4334
- LIST_${pluralUpperSnake},
4335
- CREATE_${upperSnake},
4336
- UPDATE_${upperSnake},
4337
- DELETE_${upperSnake},
4338
- PUBLISH_${upperSnake}_VERSION,
4339
- UNPUBLISH_${upperSnake},
4340
- } from '../operations/${model.key}.js';
4341
- import type {
4342
- ${typeName}Record,
4343
- Get${typeName}Variables,
4344
- Get${typeName}Result,
4345
- Get${typeName}ByKeyVariables,
4346
- Get${typeName}ByKeyResult,
4347
- List${pluralName}Variables,
4348
- List${pluralName}Result,
4349
- Create${typeName}Variables,
4350
- Create${typeName}Result,
4351
- Update${typeName}Variables,
4352
- Update${typeName}Result,
4353
- Delete${typeName}Variables,
4354
- Delete${typeName}Result,
4355
- } from '../operations/${model.key}.js';
4356
- `);
4357
- lines.push(`export type { ${typeName}Record };`);
4358
- lines.push("");
4359
- lines.push(`export function useGet${typeName}(
4360
- id: string | null | undefined,
4361
- options?: Omit<QueryHookOptions<Get${typeName}Result, Get${typeName}Variables>, 'variables' | 'skip'>
4362
- ) {
4363
- return useQuery<Get${typeName}Result, Get${typeName}Variables>(
4364
- gql\`\${GET_${upperSnake}}\`,
4365
- { ...options, variables: { id: id! }, skip: !id }
4366
- );
4367
- }`);
4368
- lines.push("");
4369
- lines.push(`export function useGet${typeName}ByKey(
4370
- naturalKey: string | null | undefined,
4371
- options?: Omit<QueryHookOptions<Get${typeName}ByKeyResult, Get${typeName}ByKeyVariables>, 'variables' | 'skip'>
4372
- ) {
4373
- return useQuery<Get${typeName}ByKeyResult, Get${typeName}ByKeyVariables>(
4374
- gql\`\${GET_${upperSnake}_BY_KEY}\`,
4375
- { ...options, variables: { naturalKey: naturalKey! }, skip: !naturalKey }
4376
- );
4377
- }`);
4378
- lines.push("");
4379
- lines.push(`export function useList${pluralName}(
4380
- variables?: List${pluralName}Variables,
4381
- options?: Omit<QueryHookOptions<List${pluralName}Result, List${pluralName}Variables>, 'variables'>
4382
- ) {
4383
- return useQuery<List${pluralName}Result, List${pluralName}Variables>(
4384
- gql\`\${LIST_${pluralUpperSnake}}\`,
4385
- { ...options, variables }
4386
- );
4387
- }`);
4388
- lines.push("");
4389
- lines.push(`export function useCreate${typeName}(
4390
- options?: MutationHookOptions<Create${typeName}Result, Create${typeName}Variables>
4391
- ) {
4392
- return useMutation<Create${typeName}Result, Create${typeName}Variables>(
4393
- gql\`\${CREATE_${upperSnake}}\`,
4394
- options
4395
- );
4396
- }`);
4397
- lines.push("");
4398
- lines.push(`export function useUpdate${typeName}(
4399
- options?: MutationHookOptions<Update${typeName}Result, Update${typeName}Variables>
4400
- ) {
4401
- return useMutation<Update${typeName}Result, Update${typeName}Variables>(
4402
- gql\`\${UPDATE_${upperSnake}}\`,
4403
- options
4404
- );
4405
- }`);
4406
- lines.push("");
4407
- lines.push(`export function useDelete${typeName}(
4408
- options?: MutationHookOptions<Delete${typeName}Result, Delete${typeName}Variables>
4409
- ) {
4410
- return useMutation<Delete${typeName}Result, Delete${typeName}Variables>(
4411
- gql\`\${DELETE_${upperSnake}}\`,
4412
- options
4413
- );
4414
- }`);
4415
- lines.push("");
4416
- lines.push(`export function usePublish${typeName}Version(
4417
- options?: MutationHookOptions<{ publishVersion: boolean }, { versionId: string }>
4418
- ) {
4419
- return useMutation<{ publishVersion: boolean }, { versionId: string }>(
4420
- gql\`\${PUBLISH_${upperSnake}_VERSION}\`,
4421
- options
4422
- );
4423
- }`);
4424
- lines.push("");
4425
- lines.push(`export function useUnpublish${typeName}(
4426
- options?: MutationHookOptions<{ unpublishRecord: boolean }, { id: string }>
4427
- ) {
4428
- return useMutation<{ unpublishRecord: boolean }, { id: string }>(
4429
- gql\`\${UNPUBLISH_${upperSnake}}\`,
4430
- options
4431
- );
4432
- }`);
4433
- lines.push("");
4434
- return lines.join("\n");
4435
- }
4436
-
4437
- // src/codegen/generators/react-hooks-index.ts
4438
- function generateReactHooksIndex(models, hasCustomerProfile) {
4439
- const lines = [];
4440
- lines.push(`/**
4441
- * React Apollo hooks for all models.
4442
- *
4443
- * @generated by foir \u2014 DO NOT EDIT MANUALLY
4444
- */
4445
- `);
4446
- for (const model of models) {
4447
- lines.push(`export * from './${model.key}.js';`);
4448
- }
4449
- if (hasCustomerProfile) {
4450
- lines.push(`export * from './customer-profile.js';`);
4451
- }
4452
- lines.push("");
4453
- return lines.join("\n");
4454
- }
4455
-
4456
- // src/codegen/generators/customer-profile-hooks.ts
4457
- function generateCustomerProfileHooks() {
4458
- return `/**
4459
- * React Apollo hooks for Customer Profiles
4460
- *
4461
- * @generated by foir \u2014 DO NOT EDIT MANUALLY
4462
- */
4463
-
4464
- import { useQuery, useMutation } from '@apollo/client';
4465
- import type { QueryHookOptions, MutationHookOptions } from '@apollo/client';
4466
- import { gql } from '@apollo/client';
4467
- import {
4468
- GET_MY_PROFILE,
4469
- GET_CUSTOMER_PROFILE,
4470
- SET_MY_PROFILE,
4471
- UPDATE_CUSTOMER_PROFILE,
4472
- DELETE_MY_PROFILE,
4473
- } from '../operations/customer-profile.js';
4474
- import type {
4475
- GetMyProfileResult,
4476
- GetCustomerProfileVariables,
4477
- GetCustomerProfileResult,
4478
- SetMyProfileVariables,
4479
- SetMyProfileResult,
4480
- UpdateCustomerProfileVariables,
4481
- UpdateCustomerProfileResult,
4482
- DeleteMyProfileResult,
4483
- } from '../operations/customer-profile.js';
4484
-
4485
- export function useMyProfile(
4486
- options?: QueryHookOptions<GetMyProfileResult>
4487
- ) {
4488
- return useQuery<GetMyProfileResult>(
4489
- gql\`\${GET_MY_PROFILE}\`,
4490
- options
4491
- );
4492
- }
4493
-
4494
- export function useCustomerProfile(
4495
- customerId: string | null | undefined,
4496
- options?: Omit<QueryHookOptions<GetCustomerProfileResult, GetCustomerProfileVariables>, 'variables' | 'skip'>
4497
- ) {
4498
- return useQuery<GetCustomerProfileResult, GetCustomerProfileVariables>(
4499
- gql\`\${GET_CUSTOMER_PROFILE}\`,
4500
- { ...options, variables: { customerId: customerId! }, skip: !customerId }
4501
- );
4502
- }
4503
-
4504
- export function useSetMyProfile(
4505
- options?: MutationHookOptions<SetMyProfileResult, SetMyProfileVariables>
4506
- ) {
4507
- return useMutation<SetMyProfileResult, SetMyProfileVariables>(
4508
- gql\`\${SET_MY_PROFILE}\`,
4509
- options
4510
- );
4511
- }
4512
-
4513
- export function useUpdateCustomerProfile(
4514
- options?: MutationHookOptions<UpdateCustomerProfileResult, UpdateCustomerProfileVariables>
4515
- ) {
4516
- return useMutation<UpdateCustomerProfileResult, UpdateCustomerProfileVariables>(
4517
- gql\`\${UPDATE_CUSTOMER_PROFILE}\`,
4518
- options
4519
- );
4520
- }
4521
-
4522
- export function useDeleteMyProfile(
4523
- options?: MutationHookOptions<DeleteMyProfileResult>
4524
- ) {
4525
- return useMutation<DeleteMyProfileResult>(
4526
- gql\`\${DELETE_MY_PROFILE}\`,
4527
- options
4528
- );
4529
- }
4530
- `;
4531
- }
4532
-
4533
- // src/codegen/generators/remix-loaders.ts
4534
- function generateRemixLoaders(model) {
4535
- const typeName = toPascalCase(model.key);
4536
- const upperSnake = toUpperSnakeCase(model.key);
4537
- const pluralName = model.pluralName ? toPascalCase(model.pluralName.replace(/\s+/g, "")) : `${typeName}s`;
4538
- const pluralUpperSnake = model.pluralName ? toUpperSnakeCase(model.pluralName.replace(/\s+/g, "")) : `${upperSnake}S`;
4539
- return `/**
4540
- * Remix / server-side loader functions for ${model.name ?? model.key}
4541
- *
4542
- * @generated by foir \u2014 DO NOT EDIT MANUALLY
4543
- */
4544
-
4545
- import {
4546
- GET_${upperSnake},
4547
- GET_${upperSnake}_BY_KEY,
4548
- LIST_${pluralUpperSnake},
4549
- CREATE_${upperSnake},
4550
- UPDATE_${upperSnake},
4551
- DELETE_${upperSnake},
4552
- PUBLISH_${upperSnake}_VERSION,
4553
- UNPUBLISH_${upperSnake},
4554
- } from '../operations/${model.key}.js';
4555
- import type {
4556
- Get${typeName}Variables,
4557
- Get${typeName}Result,
4558
- Get${typeName}ByKeyVariables,
4559
- Get${typeName}ByKeyResult,
4560
- List${pluralName}Variables,
4561
- List${pluralName}Result,
4562
- Create${typeName}Variables,
4563
- Create${typeName}Result,
4564
- Update${typeName}Variables,
4565
- Update${typeName}Result,
4566
- Delete${typeName}Variables,
4567
- Delete${typeName}Result,
4568
- } from '../operations/${model.key}.js';
4569
-
4570
- /** A minimal GraphQL client interface (works with graphql-request, urql, or custom). */
4571
- export interface GraphQLClient {
4572
- request<T>(query: string, variables?: Record<string, unknown>): Promise<T>;
4573
- }
4574
-
4575
- export async function get${typeName}(
4576
- client: GraphQLClient,
4577
- variables: Get${typeName}Variables
4578
- ): Promise<Get${typeName}Result> {
4579
- return client.request<Get${typeName}Result>(GET_${upperSnake}, variables as Record<string, unknown>);
4580
- }
4581
-
4582
- export async function get${typeName}ByKey(
4583
- client: GraphQLClient,
4584
- variables: Get${typeName}ByKeyVariables
4585
- ): Promise<Get${typeName}ByKeyResult> {
4586
- return client.request<Get${typeName}ByKeyResult>(GET_${upperSnake}_BY_KEY, variables as Record<string, unknown>);
4587
- }
4588
-
4589
- export async function list${pluralName}(
4590
- client: GraphQLClient,
4591
- variables?: List${pluralName}Variables
4592
- ): Promise<List${pluralName}Result> {
4593
- return client.request<List${pluralName}Result>(LIST_${pluralUpperSnake}, variables as Record<string, unknown>);
4594
- }
4595
-
4596
- export async function create${typeName}(
4597
- client: GraphQLClient,
4598
- variables: Create${typeName}Variables
4599
- ): Promise<Create${typeName}Result> {
4600
- return client.request<Create${typeName}Result>(CREATE_${upperSnake}, variables as Record<string, unknown>);
4601
- }
4602
-
4603
- export async function update${typeName}(
4604
- client: GraphQLClient,
4605
- variables: Update${typeName}Variables
4606
- ): Promise<Update${typeName}Result> {
4607
- return client.request<Update${typeName}Result>(UPDATE_${upperSnake}, variables as Record<string, unknown>);
4608
- }
4609
-
4610
- export async function delete${typeName}(
4611
- client: GraphQLClient,
4612
- variables: Delete${typeName}Variables
4613
- ): Promise<Delete${typeName}Result> {
4614
- return client.request<Delete${typeName}Result>(DELETE_${upperSnake}, variables as Record<string, unknown>);
4615
- }
4616
-
4617
- export async function publish${typeName}Version(
4618
- client: GraphQLClient,
4619
- versionId: string
4620
- ): Promise<{ publishVersion: boolean }> {
4621
- return client.request<{ publishVersion: boolean }>(PUBLISH_${upperSnake}_VERSION, { versionId });
4622
- }
4623
-
4624
- export async function unpublish${typeName}(
4625
- client: GraphQLClient,
4626
- id: string
4627
- ): Promise<{ unpublishRecord: boolean }> {
4628
- return client.request<{ unpublishRecord: boolean }>(UNPUBLISH_${upperSnake}, { id });
4629
- }
4630
- `;
4631
- }
4632
-
4633
- // src/codegen/generators/remix-loaders-index.ts
4634
- function generateRemixLoadersIndex(models, hasCustomerProfile) {
4635
- const lines = [];
4636
- lines.push(`/**
4637
- * Remix / server-side loader functions for all models.
4638
- *
4639
- * @generated by foir \u2014 DO NOT EDIT MANUALLY
4640
- */
4641
- `);
4642
- for (const model of models) {
4643
- lines.push(`export * from './${model.key}.js';`);
4644
- }
4645
- if (hasCustomerProfile) {
4646
- lines.push(`export * from './customer-profile.js';`);
4647
- }
4648
- lines.push("");
4649
- return lines.join("\n");
4650
- }
4651
-
4652
- // src/codegen/generators/customer-profile-loaders.ts
4653
- function generateCustomerProfileLoaders() {
4654
- return `/**
4655
- * Remix / server-side loader functions for Customer Profiles
4656
- *
4657
- * @generated by foir \u2014 DO NOT EDIT MANUALLY
4658
- */
4659
-
4660
- import {
4661
- GET_MY_PROFILE,
4662
- GET_CUSTOMER_PROFILE,
4663
- SET_MY_PROFILE,
4664
- UPDATE_CUSTOMER_PROFILE,
4665
- DELETE_MY_PROFILE,
4666
- } from '../operations/customer-profile.js';
4667
- import type {
4668
- GetMyProfileResult,
4669
- GetCustomerProfileVariables,
4670
- GetCustomerProfileResult,
4671
- SetMyProfileVariables,
4672
- SetMyProfileResult,
4673
- UpdateCustomerProfileVariables,
4674
- UpdateCustomerProfileResult,
4675
- DeleteMyProfileResult,
4676
- } from '../operations/customer-profile.js';
4677
-
4678
- /** A minimal GraphQL client interface (works with graphql-request, urql, or custom). */
4679
- export interface GraphQLClient {
4680
- request<T>(query: string, variables?: Record<string, unknown>): Promise<T>;
4681
- }
4682
-
4683
- export async function getMyProfile(
4684
- client: GraphQLClient
4685
- ): Promise<GetMyProfileResult> {
4686
- return client.request<GetMyProfileResult>(GET_MY_PROFILE);
4687
- }
4688
-
4689
- export async function getCustomerProfile(
4690
- client: GraphQLClient,
4691
- variables: GetCustomerProfileVariables
4692
- ): Promise<GetCustomerProfileResult> {
4693
- return client.request<GetCustomerProfileResult>(GET_CUSTOMER_PROFILE, variables as Record<string, unknown>);
4694
- }
4695
-
4696
- export async function setMyProfile(
4697
- client: GraphQLClient,
4698
- variables: SetMyProfileVariables
4699
- ): Promise<SetMyProfileResult> {
4700
- return client.request<SetMyProfileResult>(SET_MY_PROFILE, variables as Record<string, unknown>);
4701
- }
4702
-
4703
- export async function updateCustomerProfile(
4704
- client: GraphQLClient,
4705
- variables: UpdateCustomerProfileVariables
4706
- ): Promise<UpdateCustomerProfileResult> {
4707
- return client.request<UpdateCustomerProfileResult>(UPDATE_CUSTOMER_PROFILE, variables as Record<string, unknown>);
4708
- }
4709
-
4710
- export async function deleteMyProfile(
4711
- client: GraphQLClient
4712
- ): Promise<DeleteMyProfileResult> {
4713
- return client.request<DeleteMyProfileResult>(DELETE_MY_PROFILE);
4714
- }
4715
- `;
4716
- }
4717
-
4718
- // src/codegen/generators/public-schema-content.ts
4719
- async function fetchPublicSchema(client) {
4720
- try {
4721
- const res = await client.request(
4722
- `query { _service { sdl } }`
4723
- );
4724
- return res._service?.sdl ?? null;
4725
- } catch {
4726
- return null;
4727
- }
4728
- }
4729
-
4730
- // src/codegen/write-files.ts
4731
- import { mkdir, writeFile } from "fs/promises";
4732
- import { dirname as dirname2 } from "path";
4733
- async function writeGeneratedFile(filePath, content, usePrettier = true) {
4734
- await mkdir(dirname2(filePath), { recursive: true });
4735
- let formattedContent = content;
4736
- const isSwift = filePath.endsWith(".swift");
4737
- if (usePrettier && !isSwift) {
4738
- try {
4739
- const prettier = await import("prettier");
4740
- const parser = filePath.endsWith(".graphql") ? "graphql" : "typescript";
4741
- formattedContent = await prettier.format(content, {
4742
- parser,
4743
- semi: true,
4744
- singleQuote: true,
4745
- trailingComma: "es5",
4746
- printWidth: 100
4747
- });
4748
- } catch {
4749
- }
4750
- }
4751
- await writeFile(filePath, formattedContent, "utf-8");
4752
- }
4753
- async function writeFiles(files, usePrettier = true) {
4754
- await Promise.all(
4755
- files.map(
4756
- (file) => writeGeneratedFile(file.path, file.content, usePrettier)
4757
- )
4758
- );
2346
+ async function writeFiles(files, usePrettier = true) {
2347
+ await Promise.all(
2348
+ files.map(
2349
+ (file) => writeGeneratedFile(file.path, file.content, usePrettier)
2350
+ )
2351
+ );
4759
2352
  }
4760
2353
 
4761
2354
  // src/commands/pull.ts
4762
2355
  function registerPullCommand(program2, globalOpts) {
4763
2356
  program2.command("pull").description(
4764
- "Generate TypeScript types, GraphQL documents, and Swift types from platform models"
4765
- ).option("--config <path>", "Path to config file").option("--only <models>", "Comma-separated model keys to generate").option("--no-prettier", "Skip Prettier formatting").option("--dry-run", "Show what would be generated without writing").option("--out <dir>", "Override output directory for types").option("--swift <dir>", "Generate Swift files to directory").action(
2357
+ "Generate typed client, model types, and validation schemas from platform models"
2358
+ ).option("--config <path>", "Path to config file").option("--only <models>", "Comma-separated model keys to generate").option("--no-prettier", "Skip Prettier formatting").option("--dry-run", "Show what would be generated without writing").option("--out <dir>", "Override output directory").action(
4766
2359
  withErrorHandler(
4767
2360
  globalOpts,
4768
2361
  async (cmdOpts) => {
@@ -4772,16 +2365,14 @@ function registerPullCommand(program2, globalOpts) {
4772
2365
  only: cmdOpts.only,
4773
2366
  noPrettier: cmdOpts.prettier === false,
4774
2367
  dryRun: !!cmdOpts.dryRun,
4775
- out: cmdOpts.out,
4776
- swift: cmdOpts.swift
2368
+ out: cmdOpts.out
4777
2369
  };
4778
2370
  const config2 = await loadPullConfig(flags);
4779
2371
  const client = await createClient(opts);
4780
2372
  console.log(chalk4.dim("Fetching models\u2026"));
4781
- const [allModels, cpSchema, publicSchema] = await Promise.all([
2373
+ const [allModels, cpSchema] = await Promise.all([
4782
2374
  fetchModelsForCodegen(client),
4783
- fetchCustomerProfileSchema(client),
4784
- fetchPublicSchema(client)
2375
+ fetchCustomerProfileSchema(client)
4785
2376
  ]);
4786
2377
  if (allModels.length === 0 && !cpSchema) {
4787
2378
  console.log(chalk4.yellow("No models found. Nothing to generate."));
@@ -4794,169 +2385,65 @@ function registerPullCommand(program2, globalOpts) {
4794
2385
  console.log(
4795
2386
  chalk4.dim(
4796
2387
  `Found ${allModels.length} model(s), generating for ${models.length}.`
4797
- )
4798
- );
4799
- const cwd = process.cwd();
4800
- const typesDir = resolve2(cwd, config2.output.types);
4801
- const docsDir = resolve2(cwd, config2.output.documents);
4802
- const opsDir = resolve2(cwd, config2.output.operations);
4803
- const hooksDir = config2.output.hooks ? resolve2(cwd, config2.output.hooks) : null;
4804
- const loadersDir = config2.output.loaders ? resolve2(cwd, config2.output.loaders) : null;
4805
- const files = [];
4806
- const hasCustomerProfile = !!(cpSchema && cpSchema.fields.length > 0);
4807
- const publicModels = models.filter(
4808
- (m) => m.config.publicApi && m.config.records
4809
- );
4810
- files.push({
4811
- path: resolve2(typesDir, "field-types.ts"),
4812
- content: generateFieldTypesFile()
4813
- });
4814
- files.push({
4815
- path: resolve2(typesDir, "config.ts"),
4816
- content: generateConfigFile()
4817
- });
4818
- for (const model of models) {
4819
- files.push({
4820
- path: resolve2(typesDir, "models", `${model.key}.ts`),
4821
- content: generateModelTypes(model, models)
4822
- });
4823
- }
4824
- files.push({
4825
- path: resolve2(typesDir, "models", "index.ts"),
4826
- content: generateModelIndex(models)
4827
- });
4828
- if (hasCustomerProfile) {
4829
- files.push({
4830
- path: resolve2(typesDir, "customer-profile.ts"),
4831
- content: generateCustomerProfileTypes(cpSchema)
4832
- });
4833
- }
4834
- files.push({
4835
- path: resolve2(typesDir, "index.ts"),
4836
- content: generateMainIndex(hasCustomerProfile)
4837
- });
4838
- for (const model of publicModels) {
4839
- files.push({
4840
- path: resolve2(docsDir, `${model.key}.graphql`),
4841
- content: generateModelDocuments(model)
4842
- });
4843
- }
4844
- const hasSharingModels = publicModels.some(
4845
- (m) => m.config.sharing?.enabled
4846
- );
4847
- if (hasSharingModels) {
4848
- files.push({
4849
- path: resolve2(docsDir, "_shared.graphql"),
4850
- content: generateSharedFragments()
4851
- });
4852
- }
4853
- files.push({
4854
- path: resolve2(docsDir, "customer-profile.graphql"),
4855
- content: generateCustomerProfileDocuments()
4856
- });
4857
- const staticDocs = generateStaticDocuments(config2.domains);
4858
- for (const doc of staticDocs) {
4859
- files.push({
4860
- path: resolve2(docsDir, doc.filename),
4861
- content: doc.content
4862
- });
4863
- }
4864
- if (publicSchema) {
4865
- files.push({
4866
- path: resolve2(docsDir, "public-schema.graphql"),
4867
- content: publicSchema
4868
- });
4869
- }
4870
- const typesRelPath = computeTypesRelPath(opsDir, typesDir);
4871
- files.push({
4872
- path: resolve2(opsDir, "_common.ts"),
4873
- content: generateTypedOperationsCommon(typesRelPath)
4874
- });
4875
- for (const model of publicModels) {
4876
- files.push({
4877
- path: resolve2(opsDir, `${model.key}.ts`),
4878
- content: generateTypedOperations(model, typesRelPath)
4879
- });
4880
- }
4881
- if (hasCustomerProfile) {
2388
+ )
2389
+ );
2390
+ const cwd = process.cwd();
2391
+ const outDir = resolve(cwd, config2.output.types);
2392
+ const modelsDir = resolve(outDir, "models");
2393
+ const files = [];
2394
+ const hasCustomerProfile = !!(cpSchema && cpSchema.fields.length > 0);
2395
+ const publicModels = models.filter(
2396
+ (m) => m.config.publicApi && m.config.records
2397
+ );
2398
+ for (const model of models) {
4882
2399
  files.push({
4883
- path: resolve2(opsDir, "customer-profile.ts"),
4884
- content: generateCustomerProfileOperations(typesRelPath)
2400
+ path: resolve(modelsDir, `${model.key}.ts`),
2401
+ content: generateModelTypes(model, models)
4885
2402
  });
4886
- }
4887
- files.push({
4888
- path: resolve2(opsDir, "index.ts"),
4889
- content: generateTypedOperationsIndex(
4890
- publicModels,
4891
- hasCustomerProfile
4892
- )
4893
- });
4894
- if (hooksDir) {
4895
- for (const model of publicModels) {
4896
- files.push({
4897
- path: resolve2(hooksDir, `${model.key}.ts`),
4898
- content: generateReactHooks(model)
4899
- });
4900
- }
4901
- if (hasCustomerProfile) {
4902
- files.push({
4903
- path: resolve2(hooksDir, "customer-profile.ts"),
4904
- content: generateCustomerProfileHooks()
4905
- });
4906
- }
4907
2403
  files.push({
4908
- path: resolve2(hooksDir, "index.ts"),
4909
- content: generateReactHooksIndex(
4910
- publicModels,
4911
- hasCustomerProfile
4912
- )
2404
+ path: resolve(modelsDir, `${model.key}.zod.ts`),
2405
+ content: generateModelZodSchema(model, models)
4913
2406
  });
4914
2407
  }
4915
- if (loadersDir) {
4916
- for (const model of publicModels) {
4917
- files.push({
4918
- path: resolve2(loadersDir, `${model.key}.ts`),
4919
- content: generateRemixLoaders(model)
4920
- });
4921
- }
4922
- if (hasCustomerProfile) {
2408
+ for (const model of publicModels) {
2409
+ const whereCode = generateModelWhere(model);
2410
+ const sortCode = generateModelSortField(model);
2411
+ if (whereCode || sortCode) {
4923
2412
  files.push({
4924
- path: resolve2(loadersDir, "customer-profile.ts"),
4925
- content: generateCustomerProfileLoaders()
2413
+ path: resolve(modelsDir, `${model.key}.filters.ts`),
2414
+ content: `/**
2415
+ * Filter & sort types for ${model.name}
2416
+ *
2417
+ * @generated by foir \u2014 DO NOT EDIT MANUALLY
2418
+ */
2419
+
2420
+ ${whereCode}
2421
+ ${sortCode}`
4926
2422
  });
4927
2423
  }
4928
- files.push({
4929
- path: resolve2(loadersDir, "index.ts"),
4930
- content: generateRemixLoadersIndex(
4931
- publicModels,
4932
- hasCustomerProfile
4933
- )
4934
- });
4935
2424
  }
4936
- if (config2.output.swift) {
4937
- const swiftDir = resolve2(cwd, config2.output.swift);
4938
- files.push({
4939
- path: resolve2(swiftDir, "FieldTypes.swift"),
4940
- content: generateSwiftFieldTypesFile()
4941
- });
2425
+ if (hasCustomerProfile) {
4942
2426
  files.push({
4943
- path: resolve2(swiftDir, "ModelKeys.swift"),
4944
- content: generateSwiftModelKeys(models)
2427
+ path: resolve(modelsDir, "customer-profile.ts"),
2428
+ content: generateCustomerProfileTypes(cpSchema)
4945
2429
  });
4946
- for (const model of models) {
4947
- const swiftTypeName = toPascalCase(model.key);
4948
- files.push({
4949
- path: resolve2(swiftDir, `${swiftTypeName}.swift`),
4950
- content: generateSwiftModelFile(model)
4951
- });
4952
- }
4953
- if (hasCustomerProfile) {
4954
- files.push({
4955
- path: resolve2(swiftDir, "CustomerProfile.swift"),
4956
- content: generateSwiftCustomerProfileFile(cpSchema)
4957
- });
4958
- }
4959
2430
  }
2431
+ files.push({
2432
+ path: resolve(modelsDir, "index.ts"),
2433
+ content: generateModelIndex(models)
2434
+ });
2435
+ files.push({
2436
+ path: resolve(outDir, "client.ts"),
2437
+ content: generateClientFactory(publicModels, hasCustomerProfile)
2438
+ });
2439
+ files.push({
2440
+ path: resolve(outDir, "schema.json"),
2441
+ content: generateSchemaManifest(models, cpSchema)
2442
+ });
2443
+ files.push({
2444
+ path: resolve(outDir, "index.ts"),
2445
+ content: generateRootIndex(hasCustomerProfile)
2446
+ });
4960
2447
  if (config2.dryRun) {
4961
2448
  console.log(
4962
2449
  chalk4.bold("\nDry run \u2014 files that would be generated:\n")
@@ -4970,50 +2457,31 @@ ${chalk4.dim(`${files.length} file(s) total`)}`);
4970
2457
  return;
4971
2458
  }
4972
2459
  await writeFiles(files, config2.prettier);
4973
- const modelCount = models.length;
4974
- const docCount = publicModels.length + staticDocs.length;
4975
- const opsCount = publicModels.length + (hasCustomerProfile ? 1 : 0) + 2;
4976
- const hookCount = hooksDir ? publicModels.length + (hasCustomerProfile ? 1 : 0) + 1 : 0;
4977
- const loaderCount = loadersDir ? publicModels.length + (hasCustomerProfile ? 1 : 0) + 1 : 0;
4978
- const swiftCount = config2.output.swift ? models.length + 2 : 0;
4979
- const cpSuffix = hasCustomerProfile ? ", customer profile" : "";
2460
+ const cpSuffix = hasCustomerProfile ? " + customer profile" : "";
4980
2461
  console.log(
4981
2462
  chalk4.green(`
4982
- Generated ${files.length} file(s)`) + chalk4.dim(
4983
- ` (${modelCount} type(s), ${docCount} document(s), ${opsCount} operation(s)${cpSuffix}${hookCount > 0 ? `, ${hookCount} hook(s)` : ""}${loaderCount > 0 ? `, ${loaderCount} loader(s)` : ""}${swiftCount > 0 ? `, ${swiftCount} Swift file(s)` : ""})`
2463
+ \u2713 Generated ${files.length} file(s)`) + chalk4.dim(
2464
+ ` (${models.length} model(s), ${publicModels.length} typed accessor(s)${cpSuffix})`
4984
2465
  )
4985
2466
  );
4986
- console.log(chalk4.dim(` Types: ${typesDir}`));
4987
- console.log(chalk4.dim(` Documents: ${docsDir}`));
4988
- console.log(chalk4.dim(` Operations: ${opsDir}`));
4989
- if (hooksDir) {
4990
- console.log(chalk4.dim(` Hooks: ${hooksDir}`));
4991
- }
4992
- if (loadersDir) {
4993
- console.log(chalk4.dim(` Loaders: ${loadersDir}`));
4994
- }
4995
- if (config2.output.swift) {
4996
- console.log(
4997
- chalk4.dim(` Swift: ${resolve2(cwd, config2.output.swift)}`)
4998
- );
4999
- }
2467
+ console.log(chalk4.dim(` Output: ${outDir}`));
5000
2468
  }
5001
2469
  )
5002
2470
  );
5003
2471
  }
5004
- function generateMainIndex(includeCustomerProfile) {
2472
+ function generateRootIndex(includeCustomerProfile) {
5005
2473
  let code = `/**
5006
- * Generated types and configs
2474
+ * Generated Foir client and model types.
5007
2475
  *
5008
2476
  * @generated by foir \u2014 DO NOT EDIT MANUALLY
5009
2477
  */
5010
2478
 
5011
- export * from './field-types.js';
5012
- export * from './config.js';
2479
+ export { createClient } from './client.js';
2480
+ export type { TypedClient } from './client.js';
5013
2481
  export * from './models/index.js';
5014
2482
  `;
5015
2483
  if (includeCustomerProfile) {
5016
- code += `export * from './customer-profile.js';
2484
+ code += `export type { CustomerProfileData } from './models/customer-profile.js';
5017
2485
  `;
5018
2486
  }
5019
2487
  return code;
@@ -5025,11 +2493,11 @@ import inquirer2 from "inquirer";
5025
2493
 
5026
2494
  // src/scaffold/scaffold.ts
5027
2495
  import * as fs4 from "fs";
5028
- import * as path3 from "path";
2496
+ import * as path2 from "path";
5029
2497
 
5030
2498
  // src/scaffold/package-manager.ts
5031
2499
  import * as fs3 from "fs";
5032
- import * as path2 from "path";
2500
+ import * as path from "path";
5033
2501
  function detectPackageManager() {
5034
2502
  const lockFiles = [
5035
2503
  { file: "pnpm-lock.yaml", manager: "pnpm" },
@@ -5039,11 +2507,11 @@ function detectPackageManager() {
5039
2507
  let dir = process.cwd();
5040
2508
  while (true) {
5041
2509
  for (const { file, manager } of lockFiles) {
5042
- if (fs3.existsSync(path2.join(dir, file))) {
2510
+ if (fs3.existsSync(path.join(dir, file))) {
5043
2511
  return getManagerInfo(manager);
5044
2512
  }
5045
2513
  }
5046
- const parentDir = path2.dirname(dir);
2514
+ const parentDir = path.dirname(dir);
5047
2515
  if (parentDir === dir) {
5048
2516
  break;
5049
2517
  }
@@ -5076,7 +2544,7 @@ function getManagerInfo(manager) {
5076
2544
 
5077
2545
  // src/scaffold/scaffold.ts
5078
2546
  async function scaffold(projectName, extensionType, apiUrl) {
5079
- const projectDir = path3.resolve(process.cwd(), projectName);
2547
+ const projectDir = path2.resolve(process.cwd(), projectName);
5080
2548
  if (fs4.existsSync(projectDir)) {
5081
2549
  throw new Error(
5082
2550
  `Directory "${projectName}" already exists. Choose a different name or remove the existing directory.`
@@ -5084,8 +2552,8 @@ async function scaffold(projectName, extensionType, apiUrl) {
5084
2552
  }
5085
2553
  const files = getFiles(projectName, extensionType, apiUrl);
5086
2554
  for (const [filePath, content] of Object.entries(files)) {
5087
- const fullPath = path3.join(projectDir, filePath);
5088
- const dir = path3.dirname(fullPath);
2555
+ const fullPath = path2.join(projectDir, filePath);
2556
+ const dir = path2.dirname(fullPath);
5089
2557
  if (!fs4.existsSync(dir)) {
5090
2558
  fs4.mkdirSync(dir, { recursive: true });
5091
2559
  }
@@ -5653,9 +3121,9 @@ Media (${data.globalSearch.media.length}):`);
5653
3121
  }
5654
3122
 
5655
3123
  // src/commands/init.ts
5656
- import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
3124
+ import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
5657
3125
  import { writeFile as writeFile2 } from "fs/promises";
5658
- import { resolve as resolve4, join as join4 } from "path";
3126
+ import { resolve as resolve3, join as join4 } from "path";
5659
3127
  import chalk6 from "chalk";
5660
3128
  import inquirer3 from "inquirer";
5661
3129
  var FIELD_DEFAULTS = {
@@ -5746,8 +3214,8 @@ function registerInitCommands(program2, globalOpts) {
5746
3214
  async (key, opts) => {
5747
3215
  const globalFlags = globalOpts();
5748
3216
  const template = generateModelTemplate(key);
5749
- const outDir = resolve4(opts.output);
5750
- if (!existsSync4(outDir)) {
3217
+ const outDir = resolve3(opts.output);
3218
+ if (!existsSync3(outDir)) {
5751
3219
  mkdirSync2(outDir, { recursive: true });
5752
3220
  }
5753
3221
  const ext = opts.ts ? "ts" : "json";
@@ -5824,8 +3292,8 @@ Edit the file, then run:
5824
3292
  console.log("No models selected.");
5825
3293
  return;
5826
3294
  }
5827
- const outDir = resolve4(opts.output);
5828
- if (!existsSync4(outDir)) {
3295
+ const outDir = resolve3(opts.output);
3296
+ if (!existsSync3(outDir)) {
5829
3297
  mkdirSync2(outDir, { recursive: true });
5830
3298
  }
5831
3299
  const createdFiles = [];
@@ -5856,11 +3324,247 @@ Edit the files, then run:
5856
3324
  );
5857
3325
  }
5858
3326
 
3327
+ // src/commands/profiles.ts
3328
+ import chalk7 from "chalk";
3329
+
3330
+ // src/lib/input.ts
3331
+ import inquirer4 from "inquirer";
3332
+
3333
+ // src/lib/config-loader.ts
3334
+ import { readFile } from "fs/promises";
3335
+ import { pathToFileURL } from "url";
3336
+ import { resolve as resolve4 } from "path";
3337
+ async function loadConfig(filePath) {
3338
+ const absPath = resolve4(filePath);
3339
+ if (filePath.endsWith(".ts")) {
3340
+ const configModule = await import(pathToFileURL(absPath).href);
3341
+ return configModule.default;
3342
+ }
3343
+ if (filePath.endsWith(".js") || filePath.endsWith(".mjs")) {
3344
+ const configModule = await import(pathToFileURL(absPath).href);
3345
+ return configModule.default;
3346
+ }
3347
+ if (filePath.endsWith(".json")) {
3348
+ const content = await readFile(absPath, "utf-8");
3349
+ return JSON.parse(content);
3350
+ }
3351
+ throw new Error(
3352
+ `Unsupported file extension for "${filePath}". Supported: .ts, .js, .mjs, .json`
3353
+ );
3354
+ }
3355
+
3356
+ // src/lib/input.ts
3357
+ async function parseInputData(opts) {
3358
+ if (opts.data) {
3359
+ return JSON.parse(opts.data);
3360
+ }
3361
+ if (opts.file) {
3362
+ return await loadConfig(opts.file);
3363
+ }
3364
+ if (!process.stdin.isTTY) {
3365
+ const chunks = [];
3366
+ for await (const chunk of process.stdin) {
3367
+ chunks.push(chunk);
3368
+ }
3369
+ const stdinContent = Buffer.concat(chunks).toString("utf-8").trim();
3370
+ if (stdinContent) {
3371
+ return JSON.parse(stdinContent);
3372
+ }
3373
+ }
3374
+ throw new Error(
3375
+ "No input data provided. Use --data, --file, or pipe via stdin."
3376
+ );
3377
+ }
3378
+ function isUUID(value) {
3379
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
3380
+ value
3381
+ ) || /^c[a-z0-9]{24,}$/.test(value);
3382
+ }
3383
+ async function confirmAction(message, opts) {
3384
+ if (opts?.confirm) return true;
3385
+ const { confirmed } = await inquirer4.prompt([
3386
+ {
3387
+ type: "confirm",
3388
+ name: "confirmed",
3389
+ message,
3390
+ default: false
3391
+ }
3392
+ ]);
3393
+ return confirmed;
3394
+ }
3395
+
3396
+ // src/commands/profiles.ts
3397
+ function registerProfilesCommand(program2, globalOpts) {
3398
+ const profiles = program2.command("profiles").description("Manage named project profiles");
3399
+ profiles.command("list").description("List all saved project profiles").action(
3400
+ withErrorHandler(globalOpts, async () => {
3401
+ const opts = globalOpts();
3402
+ const allProfiles = await listProfiles();
3403
+ const defaultName = await getDefaultProfile();
3404
+ if (allProfiles.length === 0) {
3405
+ if (opts.json || opts.jsonl) {
3406
+ formatOutput([], opts);
3407
+ } else {
3408
+ console.log("No profiles saved.");
3409
+ console.log(
3410
+ "Use `foir select-project --save-as <name>` to create one."
3411
+ );
3412
+ }
3413
+ return;
3414
+ }
3415
+ if (opts.json || opts.jsonl) {
3416
+ formatOutput(
3417
+ allProfiles.map((p) => ({
3418
+ name: p.name,
3419
+ default: p.name === defaultName,
3420
+ projectName: p.project.name,
3421
+ projectId: p.project.id,
3422
+ tenantId: p.project.tenantId
3423
+ })),
3424
+ opts
3425
+ );
3426
+ return;
3427
+ }
3428
+ const rows = allProfiles.map((p) => ({
3429
+ name: p.name === defaultName ? `${p.name} (default)` : p.name,
3430
+ project: p.project.name,
3431
+ projectId: p.project.id,
3432
+ tenantId: p.project.tenantId
3433
+ }));
3434
+ formatList(rows, opts, {
3435
+ columns: [
3436
+ { key: "name", header: "Profile", width: 20 },
3437
+ { key: "project", header: "Project", width: 30 },
3438
+ { key: "projectId", header: "Project ID", width: 28 },
3439
+ { key: "tenantId", header: "Tenant ID", width: 28 }
3440
+ ]
3441
+ });
3442
+ })
3443
+ );
3444
+ profiles.command("default [name]").description("Show or set the default profile").action(
3445
+ withErrorHandler(globalOpts, async (name) => {
3446
+ const opts = globalOpts();
3447
+ if (name) {
3448
+ const project = await getProjectContext(name);
3449
+ if (!project) {
3450
+ throw new Error(
3451
+ `Profile "${name}" not found. Run \`foir profiles list\` to see available profiles.`
3452
+ );
3453
+ }
3454
+ await setDefaultProfile(name);
3455
+ if (opts.json || opts.jsonl) {
3456
+ formatOutput({ default: name }, opts);
3457
+ } else {
3458
+ console.log(`Default profile set to "${name}".`);
3459
+ }
3460
+ } else {
3461
+ const current = await getDefaultProfile();
3462
+ if (opts.json || opts.jsonl) {
3463
+ formatOutput({ default: current }, opts);
3464
+ } else if (current) {
3465
+ console.log(`Default profile: ${current}`);
3466
+ } else {
3467
+ console.log("No default profile set.");
3468
+ console.log("Use `foir profiles default <name>` to set one.");
3469
+ }
3470
+ }
3471
+ })
3472
+ );
3473
+ profiles.command("show [name]").description("Show details of a profile (or active profile if no name)").action(
3474
+ withErrorHandler(globalOpts, async (name) => {
3475
+ const opts = globalOpts();
3476
+ if (name) {
3477
+ const project = await getProjectContext(name);
3478
+ if (!project) {
3479
+ throw new Error(
3480
+ `Profile "${name}" not found. Run \`foir profiles list\` to see available profiles.`
3481
+ );
3482
+ }
3483
+ const defaultName = await getDefaultProfile();
3484
+ if (opts.json || opts.jsonl) {
3485
+ formatOutput(
3486
+ {
3487
+ profileName: name,
3488
+ default: name === defaultName,
3489
+ projectName: project.name,
3490
+ projectId: project.id,
3491
+ tenantId: project.tenantId
3492
+ },
3493
+ opts
3494
+ );
3495
+ } else {
3496
+ console.log(
3497
+ `Profile: ${name}${name === defaultName ? " (default)" : ""}`
3498
+ );
3499
+ console.log("\u2500".repeat(40));
3500
+ console.log(`Name: ${project.name}`);
3501
+ console.log(`ID: ${project.id}`);
3502
+ console.log(`Tenant ID: ${project.tenantId}`);
3503
+ }
3504
+ } else {
3505
+ const resolved = await resolveProjectContext(opts);
3506
+ if (!resolved) {
3507
+ console.log("No active project context.");
3508
+ console.log("Run `foir select-project` to choose a project.");
3509
+ return;
3510
+ }
3511
+ if (opts.json || opts.jsonl) {
3512
+ formatOutput(
3513
+ {
3514
+ profileName: resolved.profileName ?? null,
3515
+ source: resolved.source,
3516
+ ...resolved.project
3517
+ },
3518
+ opts
3519
+ );
3520
+ } else {
3521
+ console.log(
3522
+ `Active Project${resolved.profileName ? ` (profile: ${resolved.profileName})` : ""}`
3523
+ );
3524
+ console.log("\u2500".repeat(40));
3525
+ console.log(`Name: ${resolved.project.name}`);
3526
+ console.log(`ID: ${resolved.project.id}`);
3527
+ console.log(`Tenant ID: ${resolved.project.tenantId}`);
3528
+ console.log(`Source: ${resolved.source}`);
3529
+ }
3530
+ }
3531
+ })
3532
+ );
3533
+ profiles.command("delete <name>").description("Delete a named profile").option("--confirm", "Skip confirmation prompt").action(
3534
+ withErrorHandler(
3535
+ globalOpts,
3536
+ async (name, cmdOpts) => {
3537
+ const opts = globalOpts();
3538
+ const project = await getProjectContext(name);
3539
+ if (!project) {
3540
+ throw new Error(
3541
+ `Profile "${name}" not found. Run \`foir profiles list\` to see available profiles.`
3542
+ );
3543
+ }
3544
+ const confirmed = await confirmAction(
3545
+ `Delete profile "${name}" (${project.name})?`,
3546
+ { confirm: !!cmdOpts.confirm }
3547
+ );
3548
+ if (!confirmed) {
3549
+ console.log("Aborted.");
3550
+ return;
3551
+ }
3552
+ await deleteProfile(name);
3553
+ if (opts.json || opts.jsonl) {
3554
+ formatOutput({ deleted: name }, opts);
3555
+ } else {
3556
+ console.log(chalk7.green(`Deleted profile "${name}".`));
3557
+ }
3558
+ }
3559
+ )
3560
+ );
3561
+ }
3562
+
5859
3563
  // src/commands/register-commands.ts
5860
3564
  import { readFileSync, readdirSync } from "fs";
5861
- import { resolve as resolve6, dirname as dirname5 } from "path";
3565
+ import { resolve as resolve5, dirname as dirname5 } from "path";
5862
3566
  import { fileURLToPath } from "url";
5863
- import chalk7 from "chalk";
3567
+ import chalk8 from "chalk";
5864
3568
 
5865
3569
  // ../command-registry/src/command-map.ts
5866
3570
  var COMMANDS = [
@@ -7457,81 +5161,15 @@ function createSchemaEngine(sdl) {
7457
5161
  };
7458
5162
  }
7459
5163
 
7460
- // src/lib/input.ts
7461
- import inquirer4 from "inquirer";
7462
-
7463
- // src/lib/config-loader.ts
7464
- import { readFile } from "fs/promises";
7465
- import { pathToFileURL as pathToFileURL2 } from "url";
7466
- import { resolve as resolve5 } from "path";
7467
- async function loadConfig(filePath) {
7468
- const absPath = resolve5(filePath);
7469
- if (filePath.endsWith(".ts")) {
7470
- const configModule = await import(pathToFileURL2(absPath).href);
7471
- return configModule.default;
7472
- }
7473
- if (filePath.endsWith(".js") || filePath.endsWith(".mjs")) {
7474
- const configModule = await import(pathToFileURL2(absPath).href);
7475
- return configModule.default;
7476
- }
7477
- if (filePath.endsWith(".json")) {
7478
- const content = await readFile(absPath, "utf-8");
7479
- return JSON.parse(content);
7480
- }
7481
- throw new Error(
7482
- `Unsupported file extension for "${filePath}". Supported: .ts, .js, .mjs, .json`
7483
- );
7484
- }
7485
-
7486
- // src/lib/input.ts
7487
- async function parseInputData(opts) {
7488
- if (opts.data) {
7489
- return JSON.parse(opts.data);
7490
- }
7491
- if (opts.file) {
7492
- return await loadConfig(opts.file);
7493
- }
7494
- if (!process.stdin.isTTY) {
7495
- const chunks = [];
7496
- for await (const chunk of process.stdin) {
7497
- chunks.push(chunk);
7498
- }
7499
- const stdinContent = Buffer.concat(chunks).toString("utf-8").trim();
7500
- if (stdinContent) {
7501
- return JSON.parse(stdinContent);
7502
- }
7503
- }
7504
- throw new Error(
7505
- "No input data provided. Use --data, --file, or pipe via stdin."
7506
- );
7507
- }
7508
- function isUUID(value) {
7509
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
7510
- value
7511
- ) || /^c[a-z0-9]{24,}$/.test(value);
7512
- }
7513
- async function confirmAction(message, opts) {
7514
- if (opts?.confirm) return true;
7515
- const { confirmed } = await inquirer4.prompt([
7516
- {
7517
- type: "confirm",
7518
- name: "confirmed",
7519
- message,
7520
- default: false
7521
- }
7522
- ]);
7523
- return confirmed;
7524
- }
7525
-
7526
5164
  // src/commands/register-commands.ts
7527
5165
  var __filename = fileURLToPath(import.meta.url);
7528
5166
  var __dirname = dirname5(__filename);
7529
5167
  function loadSchemaSDL() {
7530
- const bundledPath = resolve6(__dirname, "schema.graphql");
5168
+ const bundledPath = resolve5(__dirname, "schema.graphql");
7531
5169
  try {
7532
5170
  return readFileSync(bundledPath, "utf-8");
7533
5171
  } catch {
7534
- const monorepoPath = resolve6(
5172
+ const monorepoPath = resolve5(
7535
5173
  __dirname,
7536
5174
  "../../../graphql-core/schema.graphql"
7537
5175
  );
@@ -7687,11 +5325,11 @@ function registerDynamicCommands(program2, globalOpts) {
7687
5325
  );
7688
5326
  Object.assign(variables, coerced);
7689
5327
  if (flags.dir && entry.acceptsInput) {
7690
- const dirPath = resolve6(String(flags.dir));
5328
+ const dirPath = resolve5(String(flags.dir));
7691
5329
  const files = readdirSync(dirPath).filter((f) => /\.(json|ts|js|mjs)$/.test(f)).sort();
7692
5330
  if (files.length === 0) {
7693
5331
  console.error(
7694
- chalk7.yellow(`\u26A0 No .json/.ts/.js files found in ${dirPath}`)
5332
+ chalk8.yellow(`\u26A0 No .json/.ts/.js files found in ${dirPath}`)
7695
5333
  );
7696
5334
  return;
7697
5335
  }
@@ -7699,7 +5337,7 @@ function registerDynamicCommands(program2, globalOpts) {
7699
5337
  let updated = 0;
7700
5338
  let failed = 0;
7701
5339
  for (const file of files) {
7702
- const filePath = resolve6(dirPath, file);
5340
+ const filePath = resolve5(dirPath, file);
7703
5341
  const fileData = await parseInputData({ file: filePath });
7704
5342
  const argName = entry.inputArgName ?? "input";
7705
5343
  const fileVars = { ...variables, [argName]: fileData };
@@ -7736,19 +5374,19 @@ function registerDynamicCommands(program2, globalOpts) {
7736
5374
  } catch (updateErr) {
7737
5375
  failed++;
7738
5376
  const msg2 = updateErr instanceof Error ? updateErr.message : String(updateErr);
7739
- console.error(chalk7.red(`\u2717 ${label}:`), msg2);
5377
+ console.error(chalk8.red(`\u2717 ${label}:`), msg2);
7740
5378
  continue;
7741
5379
  }
7742
5380
  }
7743
5381
  failed++;
7744
5382
  const msg = err instanceof Error ? err.message : String(err);
7745
- console.error(chalk7.red(`\u2717 ${label}:`), msg);
5383
+ console.error(chalk8.red(`\u2717 ${label}:`), msg);
7746
5384
  }
7747
5385
  }
7748
5386
  if (!(opts.json || opts.jsonl || opts.quiet)) {
7749
5387
  console.log("");
7750
5388
  console.log(
7751
- chalk7.bold(
5389
+ chalk8.bold(
7752
5390
  `Done: ${created} created${updated ? `, ${updated} updated` : ""}${failed ? `, ${failed} failed` : ""}`
7753
5391
  )
7754
5392
  );
@@ -7767,13 +5405,13 @@ function registerDynamicCommands(program2, globalOpts) {
7767
5405
  );
7768
5406
  const fieldNames = new Set(inputFields.map((f) => f.name));
7769
5407
  if (fieldNames.has("projectId") && !inputData.projectId || fieldNames.has("tenantId") && !inputData.tenantId) {
7770
- const project = await getProjectContext();
7771
- if (project) {
5408
+ const resolved = await resolveProjectContext(opts);
5409
+ if (resolved) {
7772
5410
  if (fieldNames.has("projectId") && !inputData.projectId) {
7773
- inputData.projectId = project.id;
5411
+ inputData.projectId = resolved.project.id;
7774
5412
  }
7775
5413
  if (fieldNames.has("tenantId") && !inputData.tenantId) {
7776
- inputData.tenantId = project.tenantId;
5414
+ inputData.tenantId = resolved.project.tenantId;
7777
5415
  }
7778
5416
  }
7779
5417
  }
@@ -7924,7 +5562,7 @@ function registerDynamicCommands(program2, globalOpts) {
7924
5562
  }
7925
5563
  } else if (!(opts.json || opts.jsonl || opts.quiet)) {
7926
5564
  console.error(
7927
- chalk7.yellow(
5565
+ chalk8.yellow(
7928
5566
  "\u26A0 Could not auto-publish: no version found in response"
7929
5567
  )
7930
5568
  );
@@ -7948,24 +5586,26 @@ function autoColumns(items) {
7948
5586
  // src/cli.ts
7949
5587
  var __filename2 = fileURLToPath2(import.meta.url);
7950
5588
  var __dirname2 = dirname6(__filename2);
7951
- config({ path: resolve7(__dirname2, "../.env.local") });
5589
+ config({ path: resolve6(__dirname2, "../.env.local") });
7952
5590
  var require2 = createRequire(import.meta.url);
7953
5591
  var { version } = require2("../package.json");
7954
5592
  var program = new Command();
7955
- program.name("foir").description("CLI for Foir platform").version(version).option("--api-url <url>", "API endpoint URL").option("--json", "Output as JSON").option("--jsonl", "Output as JSON Lines").option("--quiet", "Minimal output (IDs only)");
5593
+ program.name("foir").description("CLI for Foir platform").version(version).option("--api-url <url>", "API endpoint URL").option("--json", "Output as JSON").option("--jsonl", "Output as JSON Lines").option("--quiet", "Minimal output (IDs only)").option("--project <name>", "Use a named project profile");
7956
5594
  function getGlobalOpts() {
7957
5595
  const opts = program.opts();
7958
5596
  return {
7959
5597
  apiUrl: opts.apiUrl,
7960
5598
  json: !!opts.json,
7961
5599
  jsonl: !!opts.jsonl,
7962
- quiet: !!opts.quiet
5600
+ quiet: !!opts.quiet,
5601
+ project: opts.project
7963
5602
  };
7964
5603
  }
7965
5604
  registerLoginCommand(program, getGlobalOpts);
7966
5605
  registerLogoutCommand(program, getGlobalOpts);
7967
5606
  registerSelectProjectCommand(program, getGlobalOpts);
7968
5607
  registerWhoamiCommand(program, getGlobalOpts);
5608
+ registerProfilesCommand(program, getGlobalOpts);
7969
5609
  registerMediaCommands(program, getGlobalOpts);
7970
5610
  registerSearchCommands(program, getGlobalOpts);
7971
5611
  registerPullCommand(program, getGlobalOpts);