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.
- package/dist/cli/commands/databaseCommands.js +7 -8
- package/dist/config/services/ConfigLoaderService.d.ts +7 -0
- package/dist/config/services/ConfigLoaderService.js +47 -1
- package/dist/functions/deployments.js +5 -23
- package/dist/functions/methods.js +4 -2
- package/dist/functions/pathResolution.d.ts +37 -0
- package/dist/functions/pathResolution.js +185 -0
- package/dist/functions/templates/count-docs-in-collection/README.md +54 -0
- package/dist/functions/templates/count-docs-in-collection/package.json +25 -0
- package/dist/functions/templates/count-docs-in-collection/src/main.ts +159 -0
- package/dist/functions/templates/count-docs-in-collection/src/request.ts +9 -0
- package/dist/functions/templates/count-docs-in-collection/tsconfig.json +28 -0
- package/dist/functions/templates/hono-typescript/README.md +286 -0
- package/dist/functions/templates/hono-typescript/package.json +26 -0
- package/dist/functions/templates/hono-typescript/src/adapters/request.ts +74 -0
- package/dist/functions/templates/hono-typescript/src/adapters/response.ts +106 -0
- package/dist/functions/templates/hono-typescript/src/app.ts +180 -0
- package/dist/functions/templates/hono-typescript/src/context.ts +103 -0
- package/dist/functions/templates/hono-typescript/src/index.ts +54 -0
- package/dist/functions/templates/hono-typescript/src/middleware/appwrite.ts +119 -0
- package/dist/functions/templates/hono-typescript/tsconfig.json +20 -0
- package/dist/functions/templates/typescript-node/README.md +32 -0
- package/dist/functions/templates/typescript-node/package.json +25 -0
- package/dist/functions/templates/typescript-node/src/context.ts +103 -0
- package/dist/functions/templates/typescript-node/src/index.ts +29 -0
- package/dist/functions/templates/typescript-node/tsconfig.json +28 -0
- package/dist/functions/templates/uv/README.md +31 -0
- package/dist/functions/templates/uv/pyproject.toml +30 -0
- package/dist/functions/templates/uv/src/__init__.py +0 -0
- package/dist/functions/templates/uv/src/context.py +125 -0
- package/dist/functions/templates/uv/src/index.py +46 -0
- package/dist/main.js +8 -8
- package/dist/shared/selectionDialogs.d.ts +1 -1
- package/dist/shared/selectionDialogs.js +31 -7
- package/dist/utilsController.d.ts +2 -1
- package/dist/utilsController.js +111 -19
- package/package.json +4 -2
- package/scripts/copy-templates.ts +23 -0
- package/src/cli/commands/databaseCommands.ts +7 -8
- package/src/config/services/ConfigLoaderService.ts +62 -1
- package/src/functions/deployments.ts +10 -35
- package/src/functions/methods.ts +4 -2
- package/src/functions/pathResolution.ts +227 -0
- package/src/main.ts +8 -8
- package/src/shared/selectionDialogs.ts +36 -7
- package/src/utilsController.ts +138 -22
- package/dist/utils/schemaStrings.d.ts +0 -14
- package/dist/utils/schemaStrings.js +0 -428
- package/dist/utils/sessionPreservationExample.d.ts +0 -1666
- package/dist/utils/sessionPreservationExample.js +0 -101
package/src/utilsController.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
608
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
}
|