appwrite-utils-cli 1.7.8 → 1.7.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/dist/cli/commands/databaseCommands.js +7 -8
  2. package/dist/config/services/ConfigLoaderService.d.ts +7 -0
  3. package/dist/config/services/ConfigLoaderService.js +47 -1
  4. package/dist/functions/deployments.js +5 -23
  5. package/dist/functions/methods.js +4 -2
  6. package/dist/functions/pathResolution.d.ts +37 -0
  7. package/dist/functions/pathResolution.js +185 -0
  8. package/dist/functions/templates/count-docs-in-collection/README.md +54 -0
  9. package/dist/functions/templates/count-docs-in-collection/package.json +25 -0
  10. package/dist/functions/templates/count-docs-in-collection/src/main.ts +159 -0
  11. package/dist/functions/templates/count-docs-in-collection/src/request.ts +9 -0
  12. package/dist/functions/templates/count-docs-in-collection/tsconfig.json +28 -0
  13. package/dist/functions/templates/hono-typescript/README.md +286 -0
  14. package/dist/functions/templates/hono-typescript/package.json +26 -0
  15. package/dist/functions/templates/hono-typescript/src/adapters/request.ts +74 -0
  16. package/dist/functions/templates/hono-typescript/src/adapters/response.ts +106 -0
  17. package/dist/functions/templates/hono-typescript/src/app.ts +180 -0
  18. package/dist/functions/templates/hono-typescript/src/context.ts +103 -0
  19. package/dist/functions/templates/hono-typescript/src/index.ts +54 -0
  20. package/dist/functions/templates/hono-typescript/src/middleware/appwrite.ts +119 -0
  21. package/dist/functions/templates/hono-typescript/tsconfig.json +20 -0
  22. package/dist/functions/templates/typescript-node/README.md +32 -0
  23. package/dist/functions/templates/typescript-node/package.json +25 -0
  24. package/dist/functions/templates/typescript-node/src/context.ts +103 -0
  25. package/dist/functions/templates/typescript-node/src/index.ts +29 -0
  26. package/dist/functions/templates/typescript-node/tsconfig.json +28 -0
  27. package/dist/functions/templates/uv/README.md +31 -0
  28. package/dist/functions/templates/uv/pyproject.toml +30 -0
  29. package/dist/functions/templates/uv/src/__init__.py +0 -0
  30. package/dist/functions/templates/uv/src/context.py +125 -0
  31. package/dist/functions/templates/uv/src/index.py +46 -0
  32. package/dist/main.js +8 -8
  33. package/dist/shared/selectionDialogs.d.ts +1 -1
  34. package/dist/shared/selectionDialogs.js +31 -7
  35. package/dist/utilsController.d.ts +2 -1
  36. package/dist/utilsController.js +111 -19
  37. package/package.json +4 -2
  38. package/scripts/copy-templates.ts +23 -0
  39. package/src/cli/commands/databaseCommands.ts +7 -8
  40. package/src/config/services/ConfigLoaderService.ts +62 -1
  41. package/src/functions/deployments.ts +10 -35
  42. package/src/functions/methods.ts +4 -2
  43. package/src/functions/pathResolution.ts +227 -0
  44. package/src/main.ts +8 -8
  45. package/src/shared/selectionDialogs.ts +36 -7
  46. package/src/utilsController.ts +138 -22
  47. package/dist/utils/schemaStrings.d.ts +0 -14
  48. package/dist/utils/schemaStrings.js +0 -428
  49. package/dist/utils/sessionPreservationExample.d.ts +0 -1666
  50. package/dist/utils/sessionPreservationExample.js +0 -101
@@ -12,11 +12,10 @@ import {
12
12
  type Specification,
13
13
  } from "appwrite-utils";
14
14
  import {
15
- loadConfig,
16
- loadConfigWithPath,
17
15
  findAppwriteConfig,
18
16
  findFunctionsDir,
19
17
  } from "./utils/loadConfigs.js";
18
+ import { normalizeFunctionName, validateFunctionDirectory } from './functions/pathResolution.js';
20
19
  import { UsersController } from "./users/methods.js";
21
20
  import { AppwriteToX } from "./migrations/appwriteToX.js";
22
21
  import { ImportController } from "./migrations/importController.js";
@@ -116,9 +115,33 @@ export class UtilsController {
116
115
  appwriteKey?: string;
117
116
  }
118
117
  ): UtilsController {
118
+ // Clear instance if currentUserDir has changed
119
+ if (UtilsController.instance &&
120
+ UtilsController.instance.currentUserDir !== currentUserDir) {
121
+ logger.debug(`Clearing singleton: currentUserDir changed from ${UtilsController.instance.currentUserDir} to ${currentUserDir}`, { prefix: "UtilsController" });
122
+ UtilsController.clearInstance();
123
+ }
124
+
125
+ // Clear instance if directConfig endpoint or project has changed
126
+ if (UtilsController.instance && directConfig) {
127
+ const existingConfig = UtilsController.instance.config;
128
+ if (existingConfig) {
129
+ const endpointChanged = directConfig.appwriteEndpoint &&
130
+ existingConfig.appwriteEndpoint !== directConfig.appwriteEndpoint;
131
+ const projectChanged = directConfig.appwriteProject &&
132
+ existingConfig.appwriteProject !== directConfig.appwriteProject;
133
+
134
+ if (endpointChanged || projectChanged) {
135
+ logger.debug("Clearing singleton: endpoint or project changed", { prefix: "UtilsController" });
136
+ UtilsController.clearInstance();
137
+ }
138
+ }
139
+ }
140
+
119
141
  if (!UtilsController.instance) {
120
142
  UtilsController.instance = new UtilsController(currentUserDir, directConfig);
121
143
  }
144
+
122
145
  return UtilsController.instance;
123
146
  }
124
147
 
@@ -426,10 +449,17 @@ export class UtilsController {
426
449
  for (const entry of entries) {
427
450
  if (entry.isDirectory()) {
428
451
  const functionPath = path.join(functionsDir, entry.name);
429
- // Match with config functions by name
452
+
453
+ // Validate it's a function directory
454
+ if (!validateFunctionDirectory(functionPath)) {
455
+ continue; // Skip invalid directories
456
+ }
457
+
458
+ // Match with config functions using normalized names
430
459
  if (this.config?.functions) {
460
+ const normalizedEntryName = normalizeFunctionName(entry.name);
431
461
  const matchingFunc = this.config.functions.find(
432
- (f) => f.name.toLowerCase() === entry.name.toLowerCase()
462
+ (f) => normalizeFunctionName(f.name) === normalizedEntryName
433
463
  );
434
464
  if (matchingFunc) {
435
465
  functionDirMap.set(matchingFunc.name, functionPath);
@@ -591,28 +621,32 @@ export class UtilsController {
591
621
  async generateSchemas() {
592
622
  // Schema generation doesn't need Appwrite connection, just config
593
623
  if (!this.config) {
594
- if (this.appwriteFolderPath && this.appwriteConfigPath) {
595
- MessageFormatter.progress("Loading config from file...", { prefix: "Config" });
596
- try {
597
- const { config, actualConfigPath } = await loadConfigWithPath(
598
- this.appwriteFolderPath,
599
- { validate: false, strictMode: false, reportValidation: false }
600
- );
601
- this.config = config;
602
- MessageFormatter.info(`Loaded config from: ${actualConfigPath}`, { prefix: "Config" });
603
- } catch (error) {
604
- MessageFormatter.error("Failed to load config from file", error instanceof Error ? error : undefined, { prefix: "Config" });
605
- return;
624
+ MessageFormatter.progress("Loading config from ConfigManager...", { prefix: "Config" });
625
+ try {
626
+ const configManager = ConfigManager.getInstance();
627
+
628
+ // Load config if not already loaded
629
+ if (!configManager.hasConfig()) {
630
+ await configManager.loadConfig({
631
+ configDir: this.currentUserDir,
632
+ validate: false,
633
+ strictMode: false,
634
+ });
606
635
  }
607
- } else {
608
- MessageFormatter.error("No configuration available", undefined, { prefix: "Controller" });
636
+
637
+ this.config = configManager.getConfig();
638
+ MessageFormatter.info("Config loaded successfully from ConfigManager", { prefix: "Config" });
639
+ } catch (error) {
640
+ MessageFormatter.error("Failed to load config", error instanceof Error ? error : undefined, { prefix: "Config" });
609
641
  return;
610
642
  }
611
643
  }
644
+
612
645
  if (!this.appwriteFolderPath) {
613
646
  MessageFormatter.error("Failed to get appwriteFolderPath", undefined, { prefix: "Controller" });
614
647
  return;
615
648
  }
649
+
616
650
  await generateSchemas(this.config, this.appwriteFolderPath);
617
651
  }
618
652
 
@@ -728,7 +762,7 @@ export class UtilsController {
728
762
  }
729
763
  }
730
764
 
731
- async selectiveSync(
765
+ async selectivePull(
732
766
  databaseSelections: DatabaseSelection[],
733
767
  bucketSelections: BucketSelection[]
734
768
  ): Promise<void> {
@@ -738,7 +772,7 @@ export class UtilsController {
738
772
  return;
739
773
  }
740
774
 
741
- MessageFormatter.progress("Starting selective sync...", { prefix: "Controller" });
775
+ MessageFormatter.progress("Starting selective pull (Appwrite → local config)...", { prefix: "Controller" });
742
776
 
743
777
  // Convert database selections to Models.Database format
744
778
  const selectedDatabases: Models.Database[] = [];
@@ -762,7 +796,7 @@ export class UtilsController {
762
796
  }
763
797
 
764
798
  if (selectedDatabases.length === 0) {
765
- MessageFormatter.warning("No valid databases selected for sync", { prefix: "Controller" });
799
+ MessageFormatter.warning("No valid databases selected for pull", { prefix: "Controller" });
766
800
  return;
767
801
  }
768
802
 
@@ -778,7 +812,89 @@ export class UtilsController {
778
812
  // Perform selective sync using the enhanced synchronizeConfigurations method
779
813
  await this.synchronizeConfigurations(selectedDatabases, this.config, databaseSelections, bucketSelections);
780
814
 
781
- MessageFormatter.success("Selective sync completed successfully!", { prefix: "Controller" });
815
+ MessageFormatter.success("Selective pull completed successfully! Remote config pulled to local.", { prefix: "Controller" });
816
+ }
817
+
818
+ async selectivePush(
819
+ databaseSelections: DatabaseSelection[],
820
+ bucketSelections: BucketSelection[]
821
+ ): Promise<void> {
822
+ await this.init();
823
+ if (!this.database) {
824
+ MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
825
+ return;
826
+ }
827
+
828
+ MessageFormatter.progress("Starting selective push (local config → Appwrite)...", { prefix: "Controller" });
829
+
830
+ // Convert database selections to Models.Database format
831
+ const selectedDatabases: Models.Database[] = [];
832
+
833
+ for (const dbSelection of databaseSelections) {
834
+ // Get the full database object from the controller
835
+ const databases = await fetchAllDatabases(this.database);
836
+ const database = databases.find(db => db.$id === dbSelection.databaseId);
837
+
838
+ if (database) {
839
+ selectedDatabases.push(database);
840
+ MessageFormatter.info(`Selected database: ${database.name} (${database.$id})`, { prefix: "Controller" });
841
+
842
+ // Log selected tables for this database
843
+ if (dbSelection.tableIds && dbSelection.tableIds.length > 0) {
844
+ MessageFormatter.info(` Tables: ${dbSelection.tableIds.join(', ')}`, { prefix: "Controller" });
845
+ }
846
+ } else {
847
+ MessageFormatter.warning(`Database with ID ${dbSelection.databaseId} not found`, { prefix: "Controller" });
848
+ }
849
+ }
850
+
851
+ if (selectedDatabases.length === 0) {
852
+ MessageFormatter.warning("No valid databases selected for push", { prefix: "Controller" });
853
+ return;
854
+ }
855
+
856
+ // Log bucket selections if provided
857
+ if (bucketSelections && bucketSelections.length > 0) {
858
+ MessageFormatter.info(`Selected ${bucketSelections.length} buckets:`, { prefix: "Controller" });
859
+ for (const bucketSelection of bucketSelections) {
860
+ const dbInfo = bucketSelection.databaseId ? ` (DB: ${bucketSelection.databaseId})` : '';
861
+ MessageFormatter.info(` - ${bucketSelection.bucketName} (${bucketSelection.bucketId})${dbInfo}`, { prefix: "Controller" });
862
+ }
863
+ }
864
+
865
+ // PUSH OPERATION: Push local configuration to Appwrite
866
+ // Build selected collections/tables from databaseSelections
867
+ const selectedCollections: any[] = [];
868
+
869
+ // Get all collections/tables from config (they're at the root level, not nested in databases)
870
+ const allCollections = this.config?.collections || this.config?.tables || [];
871
+
872
+ // Collect all selected table IDs from all database selections
873
+ const selectedTableIds = new Set<string>();
874
+ for (const dbSelection of databaseSelections) {
875
+ for (const tableId of dbSelection.tableIds) {
876
+ selectedTableIds.add(tableId);
877
+ }
878
+ }
879
+
880
+ // Filter to only the selected table IDs
881
+ for (const collection of allCollections) {
882
+ const collectionId = collection.$id || (collection as any).id;
883
+ if (selectedTableIds.has(collectionId)) {
884
+ selectedCollections.push(collection);
885
+ }
886
+ }
887
+
888
+ MessageFormatter.info(`Pushing ${selectedCollections.length} selected tables/collections to Appwrite`, { prefix: "Controller" });
889
+
890
+ // Ensure databases exist
891
+ await this.ensureDatabasesExist(selectedDatabases);
892
+ await this.ensureDatabaseConfigBucketsExist(selectedDatabases);
893
+
894
+ // Create/update ONLY the selected collections/tables
895
+ await this.createOrUpdateCollectionsForDatabases(selectedDatabases, selectedCollections);
896
+
897
+ MessageFormatter.success("Selective push completed successfully! Local config pushed to Appwrite.", { prefix: "Controller" });
782
898
  }
783
899
 
784
900
  async syncDb(
@@ -1,14 +0,0 @@
1
- import type { AppwriteConfig, Attribute } from "appwrite-utils";
2
- export declare class SchemaGenerator {
3
- private relationshipMap;
4
- private config;
5
- private appwriteFolderPath;
6
- constructor(config: AppwriteConfig, appwriteFolderPath: string);
7
- private resolveCollectionName;
8
- updateTsSchemas(): void;
9
- private extractRelationships;
10
- private addRelationship;
11
- generateSchemas(): void;
12
- createSchemaStringV4: (name: string, attributes: Attribute[]) => string;
13
- typeToZod: (attribute: Attribute) => string;
14
- }
@@ -1,428 +0,0 @@
1
- import { toCamelCase, toPascalCase } from "../utils/index.js";
2
- import { Databases } from "node-appwrite";
3
- import { z } from "zod";
4
- import fs from "fs";
5
- import path from "path";
6
- import { dump } from "js-yaml";
7
- import { findFunctionsDir } from "./loadConfigs.js";
8
- import { ulid } from "ulidx";
9
- export class SchemaGenerator {
10
- relationshipMap = new Map();
11
- config;
12
- appwriteFolderPath;
13
- constructor(config, appwriteFolderPath) {
14
- this.config = config;
15
- this.appwriteFolderPath = appwriteFolderPath;
16
- this.extractRelationships();
17
- }
18
- resolveCollectionName = (idOrName) => {
19
- const col = this.config.collections?.find((c) => c.$id === idOrName || c.name === idOrName);
20
- return col?.name ?? idOrName;
21
- };
22
- updateTsSchemas() {
23
- const collections = this.config.collections;
24
- const functions = this.config.functions || [];
25
- delete this.config.collections;
26
- delete this.config.functions;
27
- const configPath = path.join(this.appwriteFolderPath, "appwriteConfig.ts");
28
- const configContent = `import { type AppwriteConfig } from "appwrite-utils";
29
-
30
- const appwriteConfig: AppwriteConfig = {
31
- appwriteEndpoint: "${this.config.appwriteEndpoint}",
32
- appwriteProject: "${this.config.appwriteProject}",
33
- appwriteKey: "${this.config.appwriteKey}",
34
- enableBackups: ${this.config.enableBackups},
35
- backupInterval: ${this.config.backupInterval},
36
- backupRetention: ${this.config.backupRetention},
37
- enableBackupCleanup: ${this.config.enableBackupCleanup},
38
- enableMockData: ${this.config.enableMockData},
39
- documentBucketId: "${this.config.documentBucketId}",
40
- usersCollectionName: "${this.config.usersCollectionName}",
41
- databases: ${JSON.stringify(this.config.databases)},
42
- buckets: ${JSON.stringify(this.config.buckets)},
43
- functions: ${JSON.stringify(functions.map((func) => ({
44
- functionId: func.$id || ulid(),
45
- name: func.name,
46
- runtime: func.runtime,
47
- path: func.dirPath || `functions/${func.name}`,
48
- entrypoint: func.entrypoint || "src/index.ts",
49
- execute: func.execute,
50
- events: func.events || [],
51
- schedule: func.schedule || "",
52
- timeout: func.timeout || 15,
53
- enabled: func.enabled !== false,
54
- logging: func.logging !== false,
55
- commands: func.commands || "npm install",
56
- scopes: func.scopes || [],
57
- installationId: func.installationId,
58
- providerRepositoryId: func.providerRepositoryId,
59
- providerBranch: func.providerBranch,
60
- providerSilentMode: func.providerSilentMode,
61
- providerRootDirectory: func.providerRootDirectory,
62
- specification: func.specification,
63
- ...(func.predeployCommands
64
- ? { predeployCommands: func.predeployCommands }
65
- : {}),
66
- ...(func.deployDir ? { deployDir: func.deployDir } : {}),
67
- })), null, 2)}
68
- };
69
-
70
- export default appwriteConfig;
71
- `;
72
- fs.writeFileSync(configPath, configContent, { encoding: "utf-8" });
73
- const collectionsFolderPath = path.join(this.appwriteFolderPath, "collections");
74
- if (!fs.existsSync(collectionsFolderPath)) {
75
- fs.mkdirSync(collectionsFolderPath, { recursive: true });
76
- }
77
- collections?.forEach((collection) => {
78
- const { databaseId, ...collectionWithoutDbId } = collection; // Destructure to exclude databaseId
79
- const collectionFilePath = path.join(collectionsFolderPath, `${collection.name}.ts`);
80
- const collectionContent = `import { type CollectionCreate } from "appwrite-utils";
81
-
82
- const ${collection.name}Config: Partial<CollectionCreate> = {
83
- name: "${collection.name}",
84
- $id: "${collection.$id}",
85
- enabled: ${collection.enabled},
86
- documentSecurity: ${collection.documentSecurity},
87
- $permissions: [
88
- ${collection.$permissions
89
- .map((permission) => `{ permission: "${permission.permission}", target: "${permission.target}" }`)
90
- .join(",\n ")}
91
- ],
92
- attributes: [
93
- ${(collection.attributes || [])
94
- .map((attr) => {
95
- return `{ ${Object.entries(attr)
96
- .map(([key, value]) => {
97
- // Check the type of the value and format it accordingly
98
- if (typeof value === "string") {
99
- // If the value is a string, wrap it in quotes
100
- return `${key}: "${value.replace(/"/g, '\\"')}"`; // Escape existing quotes in the string
101
- }
102
- else if (Array.isArray(value)) {
103
- // If the value is an array, join it with commas
104
- if (value.length > 0) {
105
- return `${key}: [${value
106
- .map((item) => `"${item}"`)
107
- .join(", ")}]`;
108
- }
109
- else {
110
- return `${key}: []`;
111
- }
112
- }
113
- else {
114
- // If the value is not a string (e.g., boolean or number), output it directly
115
- return `${key}: ${value}`;
116
- }
117
- })
118
- .join(", ")} }`;
119
- })
120
- .join(",\n ")}
121
- ],
122
- indexes: [
123
- ${(collection.indexes?.map((index) => {
124
- // Map each attribute to ensure it is properly quoted
125
- const formattedAttributes = index.attributes.map((attr) => `"${attr}"`).join(", ") ?? "";
126
- return `{ key: "${index.key}", type: "${index.type}", attributes: [${formattedAttributes}], orders: [${index.orders
127
- ?.filter((order) => order !== null)
128
- .map((order) => `"${order}"`)
129
- .join(", ") ?? ""}] }`;
130
- }) ?? []).join(",\n ")}
131
- ]
132
- };
133
-
134
- export default ${collection.name}Config;
135
- `;
136
- fs.writeFileSync(collectionFilePath, collectionContent, {
137
- encoding: "utf-8",
138
- });
139
- console.log(`Collection schema written to ${collectionFilePath}`);
140
- });
141
- }
142
- extractRelationships() {
143
- if (!this.config.collections) {
144
- return;
145
- }
146
- this.config.collections.forEach((collection) => {
147
- if (!collection.attributes) {
148
- return;
149
- }
150
- collection.attributes.forEach((attr) => {
151
- if (attr.type === "relationship" && attr.twoWay && attr.twoWayKey) {
152
- const relationshipAttr = attr;
153
- let isArrayParent = false;
154
- let isArrayChild = false;
155
- switch (relationshipAttr.relationType) {
156
- case "oneToMany":
157
- isArrayParent = true;
158
- isArrayChild = false;
159
- break;
160
- case "manyToMany":
161
- isArrayParent = true;
162
- isArrayChild = true;
163
- break;
164
- case "oneToOne":
165
- isArrayParent = false;
166
- isArrayChild = false;
167
- break;
168
- case "manyToOne":
169
- isArrayParent = false;
170
- isArrayChild = true;
171
- break;
172
- default:
173
- break;
174
- }
175
- this.addRelationship(collection.name, this.resolveCollectionName(relationshipAttr.relatedCollection), attr.key, relationshipAttr.twoWayKey, isArrayParent, isArrayChild);
176
- console.log(`Extracted relationship: ${attr.key}\n\t${collection.name} -> ${relationshipAttr.relatedCollection}, databaseId: ${collection.databaseId}`);
177
- }
178
- });
179
- });
180
- }
181
- addRelationship(parentCollection, childCollection, parentKey, childKey, isArrayParent, isArrayChild) {
182
- const relationshipsChild = this.relationshipMap.get(childCollection) || [];
183
- const relationshipsParent = this.relationshipMap.get(parentCollection) || [];
184
- relationshipsParent.push({
185
- parentCollection,
186
- childCollection,
187
- parentKey,
188
- childKey,
189
- isArray: isArrayParent,
190
- isChild: false,
191
- });
192
- relationshipsChild.push({
193
- parentCollection,
194
- childCollection,
195
- parentKey,
196
- childKey,
197
- isArray: isArrayChild,
198
- isChild: true,
199
- });
200
- this.relationshipMap.set(childCollection, relationshipsChild);
201
- this.relationshipMap.set(parentCollection, relationshipsParent);
202
- }
203
- generateSchemas() {
204
- if (!this.config.collections) {
205
- return;
206
- }
207
- this.config.collections.forEach((collection) => {
208
- const schemaString = this.createSchemaStringV4(collection.name, collection.attributes);
209
- const camelCaseName = toCamelCase(collection.name);
210
- const schemaPath = path.join(this.appwriteFolderPath, "schemas", `${camelCaseName}.ts`);
211
- fs.writeFileSync(schemaPath, schemaString, { encoding: "utf-8" });
212
- console.log(`Schema written to ${schemaPath}`);
213
- });
214
- }
215
- // Zod v4 recursive getter-based schemas
216
- createSchemaStringV4 = (name, attributes) => {
217
- const pascalName = toPascalCase(name);
218
- let imports = `import { z } from "zod";\n`;
219
- // Use the relationshipMap to find related collections
220
- const relationshipDetails = this.relationshipMap.get(name) || [];
221
- let relatedCollections = relationshipDetails
222
- .filter((detail, index, self) => {
223
- const uniqueKey = `${detail.parentCollection}-${detail.childCollection}-${detail.parentKey}-${detail.childKey}`;
224
- return (index ===
225
- self.findIndex((obj) => `${obj.parentCollection}-${obj.childCollection}-${obj.parentKey}-${obj.childKey}` ===
226
- uniqueKey));
227
- })
228
- .map((detail) => {
229
- const relatedCollectionName = detail.isChild
230
- ? detail.parentCollection
231
- : detail.childCollection;
232
- const key = detail.isChild ? detail.childKey : detail.parentKey;
233
- const isArray = detail.isArray ? "array" : "";
234
- return [relatedCollectionName, key, isArray];
235
- });
236
- // Include one-way relationship attributes directly (no twoWayKey)
237
- const oneWayRels = [];
238
- for (const attr of attributes) {
239
- if (attr.type === "relationship" && attr.relatedCollection) {
240
- const relatedName = this.resolveCollectionName(attr.relatedCollection);
241
- const isArray = attr.relationType === "oneToMany" || attr.relationType === "manyToMany"
242
- ? "array"
243
- : "";
244
- oneWayRels.push([relatedName, attr.key, isArray]);
245
- }
246
- }
247
- // Merge and dedupe (by relatedName+key)
248
- relatedCollections = [...relatedCollections, ...oneWayRels].filter((item, idx, self) => idx === self.findIndex((o) => `${o[0]}::${o[1]}` === `${item[0]}::${item[1]}`));
249
- const hasRelationships = relatedCollections.length > 0;
250
- // Build imports for related collections
251
- if (hasRelationships) {
252
- const importLines = relatedCollections.map((rel) => {
253
- const relatedPascalName = toPascalCase(rel[0]);
254
- const relatedCamelName = toCamelCase(rel[0]);
255
- return `import { ${relatedPascalName}Schema } from "./${relatedCamelName}";`;
256
- });
257
- const unique = Array.from(new Set(importLines));
258
- imports += unique.join("\n") + (unique.length ? "\n" : "");
259
- }
260
- let schemaString = `${imports}\n`;
261
- // Single object schema with recursive getters (Zod v4)
262
- schemaString += `export const ${pascalName}Schema = z.object({\n`;
263
- schemaString += ` $id: z.string(),\n`;
264
- schemaString += ` $createdAt: z.string(),\n`;
265
- schemaString += ` $updatedAt: z.string(),\n`;
266
- schemaString += ` $permissions: z.array(z.string()),\n`;
267
- for (const attribute of attributes) {
268
- if (attribute.type === "relationship")
269
- continue;
270
- schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
271
- }
272
- // Add recursive getters for relationships (respect required flag)
273
- relatedCollections.forEach((rel) => {
274
- const relatedPascalName = toPascalCase(rel[0]);
275
- const isArray = rel[2] === "array";
276
- const key = String(rel[1]);
277
- const attrMeta = attributes.find(a => a.key === key && a.type === "relationship");
278
- const isRequired = !!attrMeta?.required;
279
- let getterBody = "";
280
- if (isArray) {
281
- getterBody = isRequired
282
- ? `${relatedPascalName}Schema.array()`
283
- : `${relatedPascalName}Schema.array().nullish()`;
284
- }
285
- else {
286
- getterBody = isRequired
287
- ? `${relatedPascalName}Schema`
288
- : `${relatedPascalName}Schema.nullish()`;
289
- }
290
- schemaString += ` get ${key}(){\n return ${getterBody}\n },\n`;
291
- });
292
- schemaString += `});\n\n`;
293
- schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
294
- return schemaString;
295
- };
296
- typeToZod = (attribute) => {
297
- let baseSchemaCode = "";
298
- const finalAttribute = (attribute.type === "string" &&
299
- attribute.format &&
300
- attribute.format === "enum" &&
301
- attribute.type === "string"
302
- ? { ...attribute, type: attribute.format }
303
- : attribute);
304
- switch (finalAttribute.type) {
305
- case "string":
306
- baseSchemaCode = "z.string()";
307
- if (finalAttribute.size) {
308
- baseSchemaCode += `.max(${finalAttribute.size}, "Maximum length of ${finalAttribute.size} characters exceeded")`;
309
- }
310
- if (finalAttribute.xdefault !== undefined) {
311
- baseSchemaCode += `.default("${finalAttribute.xdefault}")`;
312
- }
313
- if (!attribute.required && !attribute.array) {
314
- baseSchemaCode += ".nullish()";
315
- }
316
- break;
317
- case "integer":
318
- baseSchemaCode = "z.number().int()";
319
- if (finalAttribute.min !== undefined) {
320
- if (BigInt(finalAttribute.min) === BigInt(-9223372036854776000)) {
321
- finalAttribute.min = undefined;
322
- }
323
- else {
324
- baseSchemaCode += `.min(${finalAttribute.min}, "Minimum value of ${finalAttribute.min} not met")`;
325
- }
326
- }
327
- if (finalAttribute.max !== undefined) {
328
- if (BigInt(finalAttribute.max) === BigInt(9223372036854776000)) {
329
- finalAttribute.max = undefined;
330
- }
331
- else {
332
- baseSchemaCode += `.max(${finalAttribute.max}, "Maximum value of ${finalAttribute.max} exceeded")`;
333
- }
334
- }
335
- if (finalAttribute.xdefault !== undefined) {
336
- baseSchemaCode += `.default(${finalAttribute.xdefault})`;
337
- }
338
- if (!finalAttribute.required && !finalAttribute.array) {
339
- baseSchemaCode += ".nullish()";
340
- }
341
- break;
342
- case "double":
343
- case "float": // Backward compatibility
344
- baseSchemaCode = "z.number()";
345
- if (finalAttribute.min !== undefined) {
346
- baseSchemaCode += `.min(${finalAttribute.min}, "Minimum value of ${finalAttribute.min} not met")`;
347
- }
348
- if (finalAttribute.max !== undefined) {
349
- baseSchemaCode += `.max(${finalAttribute.max}, "Maximum value of ${finalAttribute.max} exceeded")`;
350
- }
351
- if (finalAttribute.xdefault !== undefined) {
352
- baseSchemaCode += `.default(${finalAttribute.xdefault})`;
353
- }
354
- if (!finalAttribute.required && !finalAttribute.array) {
355
- baseSchemaCode += ".nullish()";
356
- }
357
- break;
358
- case "boolean":
359
- baseSchemaCode = "z.boolean()";
360
- if (finalAttribute.xdefault !== undefined) {
361
- baseSchemaCode += `.default(${finalAttribute.xdefault})`;
362
- }
363
- if (!finalAttribute.required && !finalAttribute.array) {
364
- baseSchemaCode += ".nullish()";
365
- }
366
- break;
367
- case "datetime":
368
- baseSchemaCode = "z.date()";
369
- if (finalAttribute.xdefault !== undefined) {
370
- baseSchemaCode += `.default(new Date("${finalAttribute.xdefault}"))`;
371
- }
372
- if (!finalAttribute.required && !finalAttribute.array) {
373
- baseSchemaCode += ".nullish()";
374
- }
375
- break;
376
- case "email":
377
- baseSchemaCode = "z.string().email()";
378
- if (finalAttribute.xdefault !== undefined) {
379
- baseSchemaCode += `.default("${finalAttribute.xdefault}")`;
380
- }
381
- if (!finalAttribute.required && !finalAttribute.array) {
382
- baseSchemaCode += ".nullish()";
383
- }
384
- break;
385
- case "ip":
386
- baseSchemaCode = "z.string()"; // Add custom validation as needed
387
- if (finalAttribute.xdefault !== undefined) {
388
- baseSchemaCode += `.default("${finalAttribute.xdefault}")`;
389
- }
390
- if (!finalAttribute.required && !finalAttribute.array) {
391
- baseSchemaCode += ".nullish()";
392
- }
393
- break;
394
- case "url":
395
- baseSchemaCode = "z.string().url()";
396
- if (finalAttribute.xdefault !== undefined) {
397
- baseSchemaCode += `.default("${finalAttribute.xdefault}")`;
398
- }
399
- if (!finalAttribute.required && !finalAttribute.array) {
400
- baseSchemaCode += ".nullish()";
401
- }
402
- break;
403
- case "enum":
404
- baseSchemaCode = `z.enum([${finalAttribute.elements
405
- .map((element) => `"${element}"`)
406
- .join(", ")}])`;
407
- if (finalAttribute.xdefault !== undefined) {
408
- baseSchemaCode += `.default("${finalAttribute.xdefault}")`;
409
- }
410
- if (!attribute.required && !attribute.array) {
411
- baseSchemaCode += ".nullish()";
412
- }
413
- break;
414
- case "relationship":
415
- break;
416
- default:
417
- baseSchemaCode = "z.any()";
418
- }
419
- // Handle arrays
420
- if (attribute.array) {
421
- baseSchemaCode = `z.array(${baseSchemaCode})`;
422
- }
423
- if (attribute.array && !attribute.required) {
424
- baseSchemaCode += ".nullish()";
425
- }
426
- return baseSchemaCode;
427
- };
428
- }