appwrite-utils-cli 1.7.7 → 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 (65) hide show
  1. package/SELECTION_DIALOGS.md +146 -0
  2. package/dist/cli/commands/databaseCommands.js +89 -23
  3. package/dist/config/services/ConfigLoaderService.d.ts +7 -0
  4. package/dist/config/services/ConfigLoaderService.js +47 -1
  5. package/dist/functions/deployments.js +5 -23
  6. package/dist/functions/methods.js +4 -2
  7. package/dist/functions/pathResolution.d.ts +37 -0
  8. package/dist/functions/pathResolution.js +185 -0
  9. package/dist/functions/templates/count-docs-in-collection/README.md +54 -0
  10. package/dist/functions/templates/count-docs-in-collection/package.json +25 -0
  11. package/dist/functions/templates/count-docs-in-collection/src/main.ts +159 -0
  12. package/dist/functions/templates/count-docs-in-collection/src/request.ts +9 -0
  13. package/dist/functions/templates/count-docs-in-collection/tsconfig.json +28 -0
  14. package/dist/functions/templates/hono-typescript/README.md +286 -0
  15. package/dist/functions/templates/hono-typescript/package.json +26 -0
  16. package/dist/functions/templates/hono-typescript/src/adapters/request.ts +74 -0
  17. package/dist/functions/templates/hono-typescript/src/adapters/response.ts +106 -0
  18. package/dist/functions/templates/hono-typescript/src/app.ts +180 -0
  19. package/dist/functions/templates/hono-typescript/src/context.ts +103 -0
  20. package/dist/functions/templates/hono-typescript/src/index.ts +54 -0
  21. package/dist/functions/templates/hono-typescript/src/middleware/appwrite.ts +119 -0
  22. package/dist/functions/templates/hono-typescript/tsconfig.json +20 -0
  23. package/dist/functions/templates/typescript-node/README.md +32 -0
  24. package/dist/functions/templates/typescript-node/package.json +25 -0
  25. package/dist/functions/templates/typescript-node/src/context.ts +103 -0
  26. package/dist/functions/templates/typescript-node/src/index.ts +29 -0
  27. package/dist/functions/templates/typescript-node/tsconfig.json +28 -0
  28. package/dist/functions/templates/uv/README.md +31 -0
  29. package/dist/functions/templates/uv/pyproject.toml +30 -0
  30. package/dist/functions/templates/uv/src/__init__.py +0 -0
  31. package/dist/functions/templates/uv/src/context.py +125 -0
  32. package/dist/functions/templates/uv/src/index.py +46 -0
  33. package/dist/main.js +175 -4
  34. package/dist/migrations/appwriteToX.d.ts +27 -2
  35. package/dist/migrations/appwriteToX.js +293 -69
  36. package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +1 -1
  37. package/dist/migrations/yaml/generateImportSchemas.js +23 -8
  38. package/dist/shared/schemaGenerator.js +25 -12
  39. package/dist/shared/selectionDialogs.d.ts +214 -0
  40. package/dist/shared/selectionDialogs.js +540 -0
  41. package/dist/utils/configDiscovery.d.ts +4 -4
  42. package/dist/utils/configDiscovery.js +66 -30
  43. package/dist/utils/yamlConverter.d.ts +1 -0
  44. package/dist/utils/yamlConverter.js +26 -3
  45. package/dist/utilsController.d.ts +7 -1
  46. package/dist/utilsController.js +198 -17
  47. package/package.json +4 -2
  48. package/scripts/copy-templates.ts +23 -0
  49. package/src/cli/commands/databaseCommands.ts +133 -34
  50. package/src/config/services/ConfigLoaderService.ts +62 -1
  51. package/src/functions/deployments.ts +10 -35
  52. package/src/functions/methods.ts +4 -2
  53. package/src/functions/pathResolution.ts +227 -0
  54. package/src/main.ts +276 -34
  55. package/src/migrations/appwriteToX.ts +385 -90
  56. package/src/migrations/yaml/generateImportSchemas.ts +26 -8
  57. package/src/shared/schemaGenerator.ts +29 -12
  58. package/src/shared/selectionDialogs.ts +745 -0
  59. package/src/utils/configDiscovery.ts +83 -39
  60. package/src/utils/yamlConverter.ts +29 -3
  61. package/src/utilsController.ts +250 -22
  62. package/dist/utils/schemaStrings.d.ts +0 -14
  63. package/dist/utils/schemaStrings.js +0 -428
  64. package/dist/utils/sessionPreservationExample.d.ts +0 -1666
  65. package/dist/utils/sessionPreservationExample.js +0 -101
@@ -0,0 +1,125 @@
1
+ from collections.abc import Callable
2
+ from typing import Any, Literal, Protocol
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class AppwriteHeaders(BaseModel):
8
+ """Appwrite request headers"""
9
+
10
+ x_appwrite_trigger: Literal["http", "schedule", "event"] | None = Field(
11
+ None, alias="x-appwrite-trigger"
12
+ )
13
+ x_appwrite_event: str | None = Field(None, alias="x-appwrite-event")
14
+ x_appwrite_key: str | None = Field(None, alias="x-appwrite-key")
15
+ x_appwrite_user_id: str | None = Field(None, alias="x-appwrite-user-id")
16
+ x_appwrite_user_jwt: str | None = Field(None, alias="x-appwrite-user-jwt")
17
+ x_appwrite_country_code: str | None = Field(None, alias="x-appwrite-country-code")
18
+ x_appwrite_continent_code: str | None = Field(
19
+ None, alias="x-appwrite-continent-code"
20
+ )
21
+ x_appwrite_continent_eu: str | None = Field(None, alias="x-appwrite-continent-eu")
22
+
23
+ # Allow additional headers
24
+ model_config = {"extra": "allow", "populate_by_name": True}
25
+
26
+
27
+ class AppwriteEnv(BaseModel):
28
+ """Appwrite environment variables"""
29
+
30
+ APPWRITE_FUNCTION_API_ENDPOINT: str
31
+ APPWRITE_VERSION: str
32
+ APPWRITE_REGION: str
33
+ APPWRITE_FUNCTION_API_KEY: str | None = None
34
+ APPWRITE_FUNCTION_ID: str
35
+ APPWRITE_FUNCTION_NAME: str
36
+ APPWRITE_FUNCTION_DEPLOYMENT: str
37
+ APPWRITE_FUNCTION_PROJECT_ID: str
38
+ APPWRITE_FUNCTION_RUNTIME_NAME: str
39
+ APPWRITE_FUNCTION_RUNTIME_VERSION: str
40
+
41
+
42
+ class AppwriteRequest(Protocol):
43
+ """Appwrite function request object with proper callable typing"""
44
+
45
+ bodyText: str | None
46
+ bodyJson: dict[str, Any] | str | None
47
+ body: Any
48
+ headers: dict[str, str]
49
+ scheme: Literal["http", "https"] | None
50
+ method: Literal["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]
51
+ url: str | None
52
+ host: str | None
53
+ port: int | str | None
54
+ path: str | None
55
+ queryString: str | None
56
+ query: dict[str, str | list[str]] | None
57
+ variables: dict[str, str] | None
58
+ payload: str | None
59
+
60
+ async def text(self) -> str: ...
61
+
62
+
63
+ class AppwriteResponse(Protocol):
64
+ """Appwrite function response object with proper callable typing"""
65
+
66
+ def json(
67
+ self,
68
+ data: Any,
69
+ status: int | None = None,
70
+ headers: dict[str, str] | None = None,
71
+ ) -> None: ...
72
+
73
+ def text(
74
+ self,
75
+ body: str,
76
+ status: int | None = None,
77
+ headers: dict[str, str] | None = None,
78
+ ) -> None: ...
79
+
80
+ def empty(self) -> None: ...
81
+
82
+ def binary(self, data: bytes) -> None: ...
83
+
84
+ def redirect(self, url: str, status: int | None = None) -> None: ...
85
+
86
+
87
+ class AppwriteContext(Protocol):
88
+ """Complete Appwrite function context with proper typing"""
89
+
90
+ req: AppwriteRequest
91
+ res: AppwriteResponse
92
+
93
+ def log(self, message: str) -> None: ...
94
+
95
+ def error(self, message: str) -> None: ...
96
+
97
+
98
+ # Alternative: BaseModel version for cases where you need Pydantic features
99
+ # but want proper typing hints
100
+
101
+
102
+ class AppwriteRequestModel(BaseModel):
103
+ """Pydantic model for request validation (if needed)"""
104
+
105
+ bodyText: str | None = None
106
+ bodyJson: dict[str, Any] | str | None = None
107
+ body: Any = None
108
+ headers: dict[str, str] = Field(default_factory=dict)
109
+ scheme: Literal["http", "https"] | None = None
110
+ method: Literal["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]
111
+ url: str | None = None
112
+ host: str | None = None
113
+ port: int | str | None = None
114
+ path: str | None = None
115
+ queryString: str | None = None
116
+ query: dict[str, str | list[str]] | None = None
117
+ variables: dict[str, str] | None = None
118
+ payload: str | None = None
119
+
120
+ model_config = {"arbitrary_types_allowed": True}
121
+
122
+
123
+ # Type alias for the log/error functions
124
+ LogFunction = Callable[[str], None]
125
+ ErrorFunction = Callable[[str], None]
@@ -0,0 +1,46 @@
1
+ import os
2
+ from appwrite.client import Client
3
+ from context import AppwriteContext, AppwriteHeaders, AppwriteRequestModel
4
+
5
+ def main(context: AppwriteContext):
6
+ req = context.req
7
+ res = context.res
8
+ log = context.log
9
+ error = context.error
10
+
11
+ # Optional: Validate request headers using Pydantic
12
+ try:
13
+ headers = AppwriteHeaders.model_validate(req.headers)
14
+ log("Headers validation successful")
15
+ except Exception as validation_error:
16
+ error(f"Headers validation failed: {validation_error}")
17
+
18
+ # Optional: Validate full request using Pydantic model
19
+ try:
20
+ request_model = AppwriteRequestModel.model_validate({
21
+ 'method': req.method,
22
+ 'headers': req.headers,
23
+ 'path': req.path,
24
+ 'url': req.url,
25
+ 'body': req.body,
26
+ 'bodyText': req.bodyText,
27
+ 'bodyJson': req.bodyJson
28
+ })
29
+ log("Request validation successful")
30
+ except Exception as validation_error:
31
+ error(f"Request validation failed: {validation_error}")
32
+
33
+ client = Client()
34
+ client.set_endpoint(os.getenv('APPWRITE_FUNCTION_ENDPOINT'))
35
+ client.set_project(os.getenv('APPWRITE_FUNCTION_PROJECT_ID'))
36
+ client.set_key(os.getenv('APPWRITE_FUNCTION_API_KEY'))
37
+
38
+ log(f"Processing {req.method} request to {req.path}")
39
+
40
+ return res.json({
41
+ 'message': 'Hello from Python function!',
42
+ 'functionName': '{{functionName}}',
43
+ 'method': req.method,
44
+ 'path': req.path,
45
+ 'headers': req.headers
46
+ })
package/dist/main.js CHANGED
@@ -13,6 +13,8 @@ import chalk from "chalk";
13
13
  import { listSpecifications } from "./functions/methods.js";
14
14
  import { MessageFormatter } from "./shared/messageFormatter.js";
15
15
  import { ConfirmationDialogs } from "./shared/confirmationDialogs.js";
16
+ import { SelectionDialogs } from "./shared/selectionDialogs.js";
17
+ import { logger } from "./shared/logging.js";
16
18
  import path from "path";
17
19
  import fs from "fs";
18
20
  import { createRequire } from "node:module";
@@ -23,6 +25,153 @@ const require = createRequire(import.meta.url);
23
25
  if (!globalThis.require) {
24
26
  globalThis.require = require;
25
27
  }
28
+ /**
29
+ * Enhanced sync function with intelligent configuration detection and selection dialogs
30
+ */
31
+ async function performEnhancedSync(controller, parsedArgv) {
32
+ try {
33
+ MessageFormatter.banner("Enhanced Sync", "Intelligent configuration detection and selection");
34
+ if (!controller.config) {
35
+ MessageFormatter.error("No Appwrite configuration found", undefined, { prefix: "Sync" });
36
+ return null;
37
+ }
38
+ // Get all available databases from remote
39
+ const availableDatabases = await fetchAllDatabases(controller.database);
40
+ if (availableDatabases.length === 0) {
41
+ MessageFormatter.warning("No databases found in remote project", { prefix: "Sync" });
42
+ return null;
43
+ }
44
+ // Get existing configuration
45
+ const configuredDatabases = controller.config.databases || [];
46
+ const configuredBuckets = controller.config.buckets || [];
47
+ // Check if we have existing configuration
48
+ const hasExistingConfig = configuredDatabases.length > 0 || configuredBuckets.length > 0;
49
+ let syncExisting = false;
50
+ let modifyConfiguration = true;
51
+ if (hasExistingConfig) {
52
+ // Prompt about existing configuration
53
+ const response = await SelectionDialogs.promptForExistingConfig([
54
+ ...configuredDatabases,
55
+ ...configuredBuckets
56
+ ]);
57
+ syncExisting = response.syncExisting;
58
+ modifyConfiguration = response.modifyConfiguration;
59
+ if (syncExisting && !modifyConfiguration) {
60
+ // Just sync existing configuration without changes
61
+ MessageFormatter.info("Syncing existing configuration without modifications", { prefix: "Sync" });
62
+ // Convert configured databases to DatabaseSelection format
63
+ const databaseSelections = configuredDatabases.map(db => ({
64
+ databaseId: db.$id,
65
+ databaseName: db.name,
66
+ tableIds: [], // Tables will be populated from collections config
67
+ tableNames: [],
68
+ isNew: false
69
+ }));
70
+ // Convert configured buckets to BucketSelection format
71
+ const bucketSelections = configuredBuckets.map(bucket => ({
72
+ bucketId: bucket.$id,
73
+ bucketName: bucket.name,
74
+ databaseId: undefined,
75
+ databaseName: undefined,
76
+ isNew: false
77
+ }));
78
+ const selectionSummary = SelectionDialogs.createSyncSelectionSummary(databaseSelections, bucketSelections);
79
+ const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary, 'pull');
80
+ if (!confirmed) {
81
+ MessageFormatter.info("Pull operation cancelled by user", { prefix: "Sync" });
82
+ return null;
83
+ }
84
+ // Perform sync with existing configuration (pull from remote)
85
+ await controller.selectivePull(databaseSelections, bucketSelections);
86
+ return selectionSummary;
87
+ }
88
+ }
89
+ if (!modifyConfiguration) {
90
+ MessageFormatter.info("No configuration changes requested", { prefix: "Sync" });
91
+ return null;
92
+ }
93
+ // Allow new items selection based on user choice
94
+ const allowNewOnly = !syncExisting;
95
+ // Select databases
96
+ const selectedDatabaseIds = await SelectionDialogs.selectDatabases(availableDatabases, configuredDatabases, {
97
+ showSelectAll: true,
98
+ allowNewOnly,
99
+ defaultSelected: syncExisting ? configuredDatabases.map(db => db.$id) : []
100
+ });
101
+ if (selectedDatabaseIds.length === 0) {
102
+ MessageFormatter.warning("No databases selected for sync", { prefix: "Sync" });
103
+ return null;
104
+ }
105
+ // For each selected database, get available tables and select them
106
+ const tableSelectionsMap = new Map();
107
+ const availableTablesMap = new Map();
108
+ for (const databaseId of selectedDatabaseIds) {
109
+ const database = availableDatabases.find(db => db.$id === databaseId);
110
+ SelectionDialogs.showProgress(`Fetching tables for database: ${database.name}`);
111
+ // Get available tables from remote
112
+ const availableTables = await fetchAllCollections(databaseId, controller.database);
113
+ availableTablesMap.set(databaseId, availableTables);
114
+ // Get configured tables for this database
115
+ // Note: Collections are stored globally in the config, not per database
116
+ const configuredTables = controller.config.collections || [];
117
+ // Select tables for this database
118
+ const selectedTableIds = await SelectionDialogs.selectTablesForDatabase(databaseId, database.name, availableTables, configuredTables, {
119
+ showSelectAll: true,
120
+ allowNewOnly,
121
+ defaultSelected: syncExisting ? configuredTables.map((t) => t.$id) : []
122
+ });
123
+ tableSelectionsMap.set(databaseId, selectedTableIds);
124
+ if (selectedTableIds.length === 0) {
125
+ MessageFormatter.warning(`No tables selected for database: ${database.name}`, { prefix: "Sync" });
126
+ }
127
+ }
128
+ // Select buckets
129
+ let selectedBucketIds = [];
130
+ // Get available buckets from remote
131
+ if (controller.storage) {
132
+ try {
133
+ // Note: We need to implement fetchAllBuckets or use storage.listBuckets
134
+ // For now, we'll use configured buckets as available
135
+ SelectionDialogs.showProgress("Fetching storage buckets...");
136
+ // Create a mock availableBuckets array - in real implementation,
137
+ // you'd fetch this from the Appwrite API
138
+ const availableBuckets = configuredBuckets; // Placeholder
139
+ selectedBucketIds = await SelectionDialogs.selectBucketsForDatabases(selectedDatabaseIds, availableBuckets, configuredBuckets, {
140
+ showSelectAll: true,
141
+ allowNewOnly: parsedArgv.selectBuckets ? false : allowNewOnly,
142
+ groupByDatabase: true,
143
+ defaultSelected: syncExisting ? configuredBuckets.map(b => b.$id) : []
144
+ });
145
+ }
146
+ catch (error) {
147
+ MessageFormatter.warning("Could not fetch storage buckets", { prefix: "Sync" });
148
+ logger.warn("Failed to fetch buckets during sync", { error });
149
+ }
150
+ }
151
+ // Create selection objects
152
+ const databaseSelections = SelectionDialogs.createDatabaseSelection(selectedDatabaseIds, availableDatabases, tableSelectionsMap, configuredDatabases, availableTablesMap);
153
+ const bucketSelections = SelectionDialogs.createBucketSelection(selectedBucketIds, [], // availableBuckets - would be populated from API
154
+ configuredBuckets, availableDatabases);
155
+ // Show final confirmation
156
+ const selectionSummary = SelectionDialogs.createSyncSelectionSummary(databaseSelections, bucketSelections);
157
+ const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary, 'pull');
158
+ if (!confirmed) {
159
+ MessageFormatter.info("Pull operation cancelled by user", { prefix: "Sync" });
160
+ return null;
161
+ }
162
+ // Perform the selective sync (pull from remote)
163
+ await controller.selectivePull(databaseSelections, bucketSelections);
164
+ MessageFormatter.success("Enhanced sync completed successfully", { prefix: "Sync" });
165
+ return selectionSummary;
166
+ }
167
+ catch (error) {
168
+ SelectionDialogs.showError("Enhanced sync failed", error instanceof Error ? error : new Error(String(error)));
169
+ return null;
170
+ }
171
+ }
172
+ /**
173
+ * Performs selective sync with the given database and bucket selections
174
+ */
26
175
  /**
27
176
  * Checks if the migration from collections to tables should be allowed
28
177
  * Returns an object with:
@@ -143,6 +292,15 @@ const argv = yargs(hideBin(process.argv))
143
292
  .option("sync", {
144
293
  type: "boolean",
145
294
  description: "Pull and synchronize your local config with the remote Appwrite project schema",
295
+ })
296
+ .option("autoSync", {
297
+ alias: ["auto"],
298
+ type: "boolean",
299
+ description: "Skip prompts and sync all databases, tables, and buckets (current behavior)"
300
+ })
301
+ .option("selectBuckets", {
302
+ type: "boolean",
303
+ description: "Force bucket selection dialog even if buckets are already configured"
146
304
  })
147
305
  .option("endpoint", {
148
306
  type: "string",
@@ -711,10 +869,23 @@ async function main() {
711
869
  operationStats.pushedCollections = controller.config?.collections?.length || 0;
712
870
  }
713
871
  else if (parsedArgv.sync) {
714
- // SYNC: Pull from remote
715
- const databases = options.databases || (await fetchAllDatabases(controller.database));
716
- await controller.synchronizeConfigurations(databases);
717
- operationStats.syncedDatabases = databases.length;
872
+ // Enhanced SYNC: Pull from remote with intelligent configuration detection
873
+ if (parsedArgv.autoSync) {
874
+ // Legacy behavior: sync everything without prompts
875
+ MessageFormatter.info("Using auto-sync mode (legacy behavior)", { prefix: "Sync" });
876
+ const databases = options.databases || (await fetchAllDatabases(controller.database));
877
+ await controller.synchronizeConfigurations(databases);
878
+ operationStats.syncedDatabases = databases.length;
879
+ }
880
+ else {
881
+ // Enhanced sync flow with selection dialogs
882
+ const syncResult = await performEnhancedSync(controller, parsedArgv);
883
+ if (syncResult) {
884
+ operationStats.syncedDatabases = syncResult.databases.length;
885
+ operationStats.syncedCollections = syncResult.totalTables;
886
+ operationStats.syncedBuckets = syncResult.buckets.length;
887
+ }
888
+ }
718
889
  }
719
890
  if (options.generateSchemas) {
720
891
  await controller.generateSchemas();
@@ -1,5 +1,7 @@
1
1
  import { Storage, type Models } from "node-appwrite";
2
2
  import { type AppwriteConfig } from "appwrite-utils";
3
+ import type { DatabaseAdapter } from "../adapters/DatabaseAdapter.js";
4
+ import type { DatabaseSelection, BucketSelection } from "../shared/selectionDialogs.js";
3
5
  export declare class AppwriteToX {
4
6
  config: AppwriteConfig;
5
7
  storage: Storage;
@@ -102,7 +104,14 @@ export declare class AppwriteToX {
102
104
  } | undefined;
103
105
  })[]>;
104
106
  appwriteFolderPath: string;
107
+ adapter?: DatabaseAdapter;
108
+ apiMode?: 'legacy' | 'tablesdb';
109
+ databaseApiModes: Map<string, "legacy" | "tablesdb">;
105
110
  constructor(config: AppwriteConfig, appwriteFolderPath: string, storage: Storage);
111
+ /**
112
+ * Initialize adapter for database operations with API mode detection
113
+ */
114
+ private initializeAdapter;
106
115
  private ensureClientInitialized;
107
116
  parsePermissionString: (permissionString: string) => {
108
117
  permission: string;
@@ -116,6 +125,22 @@ export declare class AppwriteToX {
116
125
  target: string;
117
126
  })[];
118
127
  updateCollectionConfigAttributes: (collection: Models.Collection) => void;
119
- appwriteSync(config: AppwriteConfig, databases?: Models.Database[]): Promise<void>;
120
- toSchemas(databases?: Models.Database[]): Promise<void>;
128
+ /**
129
+ * Fetch collections/tables using the appropriate adapter or legacy client
130
+ */
131
+ private fetchCollectionsOrTables;
132
+ /**
133
+ * Get collection/table using the appropriate adapter or legacy client
134
+ */
135
+ private getCollectionOrTable;
136
+ /**
137
+ * Detect API mode for a specific database by testing adapter capabilities
138
+ */
139
+ private detectDatabaseApiMode;
140
+ /**
141
+ * Get API mode context for schema generation
142
+ */
143
+ private getSchemaGeneratorApiContext;
144
+ appwriteSync(config: AppwriteConfig, databases?: Models.Database[], databaseSelections?: DatabaseSelection[], bucketSelections?: BucketSelection[]): Promise<void>;
145
+ toSchemas(databases?: Models.Database[], databaseSelections?: DatabaseSelection[], bucketSelections?: BucketSelection[]): Promise<void>;
121
146
  }