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
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ "allowSyntheticDefaultImports": true,
7
+ "esModuleInterop": true,
8
+ "allowJs": true,
9
+ "checkJs": false,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src",
12
+ "strict": true,
13
+ "noImplicitAny": true,
14
+ "strictNullChecks": true,
15
+ "strictFunctionTypes": true,
16
+ "noImplicitThis": true,
17
+ "noImplicitReturns": true,
18
+ "noFallthroughCasesInSwitch": true,
19
+ "moduleDetection": "force",
20
+ "resolveJsonModule": true,
21
+ "isolatedModules": true,
22
+ "verbatimModuleSyntax": false,
23
+ "skipLibCheck": true,
24
+ "forceConsistentCasingInFileNames": true
25
+ },
26
+ "include": ["src/**/*"],
27
+ "exclude": ["node_modules", "dist"]
28
+ }
@@ -0,0 +1,31 @@
1
+ # Python UV Function Template
2
+
3
+ This is a Python template for Appwrite Functions using UV for fast dependency management.
4
+
5
+ ## Structure
6
+ - `src/index.py`: Main function entry point
7
+ - `pyproject.toml`: UV/PyPI project configuration and dependencies
8
+
9
+ ## Usage
10
+ Your function will receive a context object with:
11
+ - `req`: Request object containing request data, headers, and environment variables
12
+ - `res`: Response object for sending responses
13
+ - `log`: Function for logging (shows in your function logs)
14
+ - `error`: Function for error logging
15
+
16
+ ## Example Request
17
+ ```json
18
+ {
19
+ "key": "value"
20
+ }
21
+ ```
22
+
23
+ ## Development
24
+ 1. Install UV: `curl -LsSf https://astral.sh/uv/install.sh | sh`
25
+ 2. Install dependencies: `uv sync`
26
+ 3. Run locally: `uv run python src/index.py`
27
+ 4. Deploy: Dependencies will be installed during deployment
28
+
29
+ ## Deployment
30
+ Make sure it's inside `appwriteConfig.yaml` functions array, and if you want to install dependencies FIRST, before Appwrite (using your system), you can
31
+ add the `predeployCommands` to the function in `appwriteConfig.yaml`.
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "{{functionName}}"
3
+ version = "0.1.0"
4
+ description = "Appwrite Python function using UV"
5
+ authors = [{name = "Your Name", email = "you@example.com"}]
6
+ readme = "README.md"
7
+ requires-python = ">=3.9"
8
+ dependencies = [
9
+ "appwrite>=7.0.1",
10
+ "pydantic>=2.0.0",
11
+ ]
12
+
13
+ [build-system]
14
+ requires = ["hatchling"]
15
+ build-backend = "hatchling.build"
16
+
17
+ [tool.uv]
18
+ dev-dependencies = [
19
+ "pytest>=7.0.0",
20
+ "black>=23.0.0",
21
+ "ruff>=0.1.0",
22
+ ]
23
+
24
+ [tool.ruff]
25
+ line-length = 88
26
+ target-version = "py39"
27
+
28
+ [tool.ruff.lint]
29
+ select = ["E", "F", "W", "I"]
30
+ ignore = ["E501"]
File without changes
@@ -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
@@ -76,13 +76,13 @@ async function performEnhancedSync(controller, parsedArgv) {
76
76
  isNew: false
77
77
  }));
78
78
  const selectionSummary = SelectionDialogs.createSyncSelectionSummary(databaseSelections, bucketSelections);
79
- const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary);
79
+ const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary, 'pull');
80
80
  if (!confirmed) {
81
- MessageFormatter.info("Sync operation cancelled by user", { prefix: "Sync" });
81
+ MessageFormatter.info("Pull operation cancelled by user", { prefix: "Sync" });
82
82
  return null;
83
83
  }
84
- // Perform sync with existing configuration
85
- await controller.selectiveSync(databaseSelections, bucketSelections);
84
+ // Perform sync with existing configuration (pull from remote)
85
+ await controller.selectivePull(databaseSelections, bucketSelections);
86
86
  return selectionSummary;
87
87
  }
88
88
  }
@@ -154,13 +154,13 @@ async function performEnhancedSync(controller, parsedArgv) {
154
154
  configuredBuckets, availableDatabases);
155
155
  // Show final confirmation
156
156
  const selectionSummary = SelectionDialogs.createSyncSelectionSummary(databaseSelections, bucketSelections);
157
- const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary);
157
+ const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary, 'pull');
158
158
  if (!confirmed) {
159
- MessageFormatter.info("Sync operation cancelled by user", { prefix: "Sync" });
159
+ MessageFormatter.info("Pull operation cancelled by user", { prefix: "Sync" });
160
160
  return null;
161
161
  }
162
- // Perform the selective sync
163
- await controller.selectiveSync(databaseSelections, bucketSelections);
162
+ // Perform the selective sync (pull from remote)
163
+ await controller.selectivePull(databaseSelections, bucketSelections);
164
164
  MessageFormatter.success("Enhanced sync completed successfully", { prefix: "Sync" });
165
165
  return selectionSummary;
166
166
  }
@@ -182,7 +182,7 @@ export declare class SelectionDialogs {
182
182
  /**
183
183
  * Shows final confirmation dialog with sync selection summary
184
184
  */
185
- static confirmSyncSelection(selectionSummary: SyncSelectionSummary): Promise<boolean>;
185
+ static confirmSyncSelection(selectionSummary: SyncSelectionSummary, operationType?: 'push' | 'pull' | 'sync'): Promise<boolean>;
186
186
  /**
187
187
  * Creates a sync selection summary from selected items
188
188
  */
@@ -359,8 +359,32 @@ export class SelectionDialogs {
359
359
  /**
360
360
  * Shows final confirmation dialog with sync selection summary
361
361
  */
362
- static async confirmSyncSelection(selectionSummary) {
363
- MessageFormatter.banner("Sync Selection Summary", "Review your selections before proceeding");
362
+ static async confirmSyncSelection(selectionSummary, operationType = 'sync') {
363
+ const labels = {
364
+ push: {
365
+ banner: "Push Selection Summary",
366
+ subtitle: "Review selections before pushing to Appwrite",
367
+ confirm: "Proceed with push operation?",
368
+ success: "Push operation confirmed.",
369
+ cancel: "Push operation cancelled."
370
+ },
371
+ pull: {
372
+ banner: "Pull Selection Summary",
373
+ subtitle: "Review selections before pulling from Appwrite",
374
+ confirm: "Proceed with pull operation?",
375
+ success: "Pull operation confirmed.",
376
+ cancel: "Pull operation cancelled."
377
+ },
378
+ sync: {
379
+ banner: "Sync Selection Summary",
380
+ subtitle: "Review your selections before proceeding",
381
+ confirm: "Proceed with sync operation?",
382
+ success: "Sync operation confirmed.",
383
+ cancel: "Sync operation cancelled."
384
+ }
385
+ };
386
+ const label = labels[operationType];
387
+ MessageFormatter.banner(label.banner, label.subtitle);
364
388
  // Database summary
365
389
  console.log(chalk.bold.cyan("\n📊 Databases:"));
366
390
  console.log(` Total: ${selectionSummary.totalDatabases}`);
@@ -395,20 +419,20 @@ export class SelectionDialogs {
395
419
  const { confirmed } = await inquirer.prompt([{
396
420
  type: 'confirm',
397
421
  name: 'confirmed',
398
- message: chalk.green.bold('Proceed with sync operation?'),
422
+ message: chalk.green.bold(label.confirm),
399
423
  default: true
400
424
  }]);
401
425
  if (confirmed) {
402
- MessageFormatter.success("Sync operation confirmed.", { skipLogging: true });
403
- logger.info("Sync selection confirmed", {
426
+ MessageFormatter.success(label.success, { skipLogging: true });
427
+ logger.info(`${operationType} selection confirmed`, {
404
428
  databases: selectionSummary.totalDatabases,
405
429
  tables: selectionSummary.totalTables,
406
430
  buckets: selectionSummary.totalBuckets
407
431
  });
408
432
  }
409
433
  else {
410
- MessageFormatter.warning("Sync operation cancelled.", { skipLogging: true });
411
- logger.info("Sync selection cancelled by user");
434
+ MessageFormatter.warning(label.cancel, { skipLogging: true });
435
+ logger.info(`${operationType} selection cancelled by user`);
412
436
  }
413
437
  return confirmed;
414
438
  }
@@ -83,7 +83,8 @@ export declare class UtilsController {
83
83
  generateSchemas(): Promise<void>;
84
84
  importData(options?: SetupOptions): Promise<void>;
85
85
  synchronizeConfigurations(databases?: Models.Database[], config?: AppwriteConfig, databaseSelections?: DatabaseSelection[], bucketSelections?: BucketSelection[]): Promise<void>;
86
- selectiveSync(databaseSelections: DatabaseSelection[], bucketSelections: BucketSelection[]): Promise<void>;
86
+ selectivePull(databaseSelections: DatabaseSelection[], bucketSelections: BucketSelection[]): Promise<void>;
87
+ selectivePush(databaseSelections: DatabaseSelection[], bucketSelections: BucketSelection[]): Promise<void>;
87
88
  syncDb(databases?: Models.Database[], collections?: Models.Collection[]): Promise<void>;
88
89
  getAppwriteFolderPath(): string | undefined;
89
90
  transferData(options: TransferOptions): Promise<void>;
@@ -1,6 +1,7 @@
1
1
  import { Client, Databases, Query, Storage, Users, } from "node-appwrite";
2
2
  import {} from "appwrite-utils";
3
- import { loadConfig, loadConfigWithPath, findAppwriteConfig, findFunctionsDir, } from "./utils/loadConfigs.js";
3
+ import { findAppwriteConfig, findFunctionsDir, } from "./utils/loadConfigs.js";
4
+ import { normalizeFunctionName, validateFunctionDirectory } from './functions/pathResolution.js';
4
5
  import { UsersController } from "./users/methods.js";
5
6
  import { AppwriteToX } from "./migrations/appwriteToX.js";
6
7
  import { ImportController } from "./migrations/importController.js";
@@ -39,6 +40,26 @@ export class UtilsController {
39
40
  * Get the UtilsController singleton instance
40
41
  */
41
42
  static getInstance(currentUserDir, directConfig) {
43
+ // Clear instance if currentUserDir has changed
44
+ if (UtilsController.instance &&
45
+ UtilsController.instance.currentUserDir !== currentUserDir) {
46
+ logger.debug(`Clearing singleton: currentUserDir changed from ${UtilsController.instance.currentUserDir} to ${currentUserDir}`, { prefix: "UtilsController" });
47
+ UtilsController.clearInstance();
48
+ }
49
+ // Clear instance if directConfig endpoint or project has changed
50
+ if (UtilsController.instance && directConfig) {
51
+ const existingConfig = UtilsController.instance.config;
52
+ if (existingConfig) {
53
+ const endpointChanged = directConfig.appwriteEndpoint &&
54
+ existingConfig.appwriteEndpoint !== directConfig.appwriteEndpoint;
55
+ const projectChanged = directConfig.appwriteProject &&
56
+ existingConfig.appwriteProject !== directConfig.appwriteProject;
57
+ if (endpointChanged || projectChanged) {
58
+ logger.debug("Clearing singleton: endpoint or project changed", { prefix: "UtilsController" });
59
+ UtilsController.clearInstance();
60
+ }
61
+ }
62
+ }
42
63
  if (!UtilsController.instance) {
43
64
  UtilsController.instance = new UtilsController(currentUserDir, directConfig);
44
65
  }
@@ -295,9 +316,14 @@ export class UtilsController {
295
316
  for (const entry of entries) {
296
317
  if (entry.isDirectory()) {
297
318
  const functionPath = path.join(functionsDir, entry.name);
298
- // Match with config functions by name
319
+ // Validate it's a function directory
320
+ if (!validateFunctionDirectory(functionPath)) {
321
+ continue; // Skip invalid directories
322
+ }
323
+ // Match with config functions using normalized names
299
324
  if (this.config?.functions) {
300
- const matchingFunc = this.config.functions.find((f) => f.name.toLowerCase() === entry.name.toLowerCase());
325
+ const normalizedEntryName = normalizeFunctionName(entry.name);
326
+ const matchingFunc = this.config.functions.find((f) => normalizeFunctionName(f.name) === normalizedEntryName);
301
327
  if (matchingFunc) {
302
328
  functionDirMap.set(matchingFunc.name, functionPath);
303
329
  }
@@ -422,20 +448,22 @@ export class UtilsController {
422
448
  async generateSchemas() {
423
449
  // Schema generation doesn't need Appwrite connection, just config
424
450
  if (!this.config) {
425
- if (this.appwriteFolderPath && this.appwriteConfigPath) {
426
- MessageFormatter.progress("Loading config from file...", { prefix: "Config" });
427
- try {
428
- const { config, actualConfigPath } = await loadConfigWithPath(this.appwriteFolderPath, { validate: false, strictMode: false, reportValidation: false });
429
- this.config = config;
430
- MessageFormatter.info(`Loaded config from: ${actualConfigPath}`, { prefix: "Config" });
431
- }
432
- catch (error) {
433
- MessageFormatter.error("Failed to load config from file", error instanceof Error ? error : undefined, { prefix: "Config" });
434
- return;
451
+ MessageFormatter.progress("Loading config from ConfigManager...", { prefix: "Config" });
452
+ try {
453
+ const configManager = ConfigManager.getInstance();
454
+ // Load config if not already loaded
455
+ if (!configManager.hasConfig()) {
456
+ await configManager.loadConfig({
457
+ configDir: this.currentUserDir,
458
+ validate: false,
459
+ strictMode: false,
460
+ });
435
461
  }
462
+ this.config = configManager.getConfig();
463
+ MessageFormatter.info("Config loaded successfully from ConfigManager", { prefix: "Config" });
436
464
  }
437
- else {
438
- MessageFormatter.error("No configuration available", undefined, { prefix: "Controller" });
465
+ catch (error) {
466
+ MessageFormatter.error("Failed to load config", error instanceof Error ? error : undefined, { prefix: "Config" });
439
467
  return;
440
468
  }
441
469
  }
@@ -521,13 +549,13 @@ export class UtilsController {
521
549
  logger.warn("Schema regeneration failed during sync:", error);
522
550
  }
523
551
  }
524
- async selectiveSync(databaseSelections, bucketSelections) {
552
+ async selectivePull(databaseSelections, bucketSelections) {
525
553
  await this.init();
526
554
  if (!this.database) {
527
555
  MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
528
556
  return;
529
557
  }
530
- MessageFormatter.progress("Starting selective sync...", { prefix: "Controller" });
558
+ MessageFormatter.progress("Starting selective pull (Appwrite → local config)...", { prefix: "Controller" });
531
559
  // Convert database selections to Models.Database format
532
560
  const selectedDatabases = [];
533
561
  for (const dbSelection of databaseSelections) {
@@ -547,7 +575,7 @@ export class UtilsController {
547
575
  }
548
576
  }
549
577
  if (selectedDatabases.length === 0) {
550
- MessageFormatter.warning("No valid databases selected for sync", { prefix: "Controller" });
578
+ MessageFormatter.warning("No valid databases selected for pull", { prefix: "Controller" });
551
579
  return;
552
580
  }
553
581
  // Log bucket selections if provided
@@ -560,7 +588,71 @@ export class UtilsController {
560
588
  }
561
589
  // Perform selective sync using the enhanced synchronizeConfigurations method
562
590
  await this.synchronizeConfigurations(selectedDatabases, this.config, databaseSelections, bucketSelections);
563
- MessageFormatter.success("Selective sync completed successfully!", { prefix: "Controller" });
591
+ MessageFormatter.success("Selective pull completed successfully! Remote config pulled to local.", { prefix: "Controller" });
592
+ }
593
+ async selectivePush(databaseSelections, bucketSelections) {
594
+ await this.init();
595
+ if (!this.database) {
596
+ MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
597
+ return;
598
+ }
599
+ MessageFormatter.progress("Starting selective push (local config → Appwrite)...", { prefix: "Controller" });
600
+ // Convert database selections to Models.Database format
601
+ const selectedDatabases = [];
602
+ for (const dbSelection of databaseSelections) {
603
+ // Get the full database object from the controller
604
+ const databases = await fetchAllDatabases(this.database);
605
+ const database = databases.find(db => db.$id === dbSelection.databaseId);
606
+ if (database) {
607
+ selectedDatabases.push(database);
608
+ MessageFormatter.info(`Selected database: ${database.name} (${database.$id})`, { prefix: "Controller" });
609
+ // Log selected tables for this database
610
+ if (dbSelection.tableIds && dbSelection.tableIds.length > 0) {
611
+ MessageFormatter.info(` Tables: ${dbSelection.tableIds.join(', ')}`, { prefix: "Controller" });
612
+ }
613
+ }
614
+ else {
615
+ MessageFormatter.warning(`Database with ID ${dbSelection.databaseId} not found`, { prefix: "Controller" });
616
+ }
617
+ }
618
+ if (selectedDatabases.length === 0) {
619
+ MessageFormatter.warning("No valid databases selected for push", { prefix: "Controller" });
620
+ return;
621
+ }
622
+ // Log bucket selections if provided
623
+ if (bucketSelections && bucketSelections.length > 0) {
624
+ MessageFormatter.info(`Selected ${bucketSelections.length} buckets:`, { prefix: "Controller" });
625
+ for (const bucketSelection of bucketSelections) {
626
+ const dbInfo = bucketSelection.databaseId ? ` (DB: ${bucketSelection.databaseId})` : '';
627
+ MessageFormatter.info(` - ${bucketSelection.bucketName} (${bucketSelection.bucketId})${dbInfo}`, { prefix: "Controller" });
628
+ }
629
+ }
630
+ // PUSH OPERATION: Push local configuration to Appwrite
631
+ // Build selected collections/tables from databaseSelections
632
+ const selectedCollections = [];
633
+ // Get all collections/tables from config (they're at the root level, not nested in databases)
634
+ const allCollections = this.config?.collections || this.config?.tables || [];
635
+ // Collect all selected table IDs from all database selections
636
+ const selectedTableIds = new Set();
637
+ for (const dbSelection of databaseSelections) {
638
+ for (const tableId of dbSelection.tableIds) {
639
+ selectedTableIds.add(tableId);
640
+ }
641
+ }
642
+ // Filter to only the selected table IDs
643
+ for (const collection of allCollections) {
644
+ const collectionId = collection.$id || collection.id;
645
+ if (selectedTableIds.has(collectionId)) {
646
+ selectedCollections.push(collection);
647
+ }
648
+ }
649
+ MessageFormatter.info(`Pushing ${selectedCollections.length} selected tables/collections to Appwrite`, { prefix: "Controller" });
650
+ // Ensure databases exist
651
+ await this.ensureDatabasesExist(selectedDatabases);
652
+ await this.ensureDatabaseConfigBucketsExist(selectedDatabases);
653
+ // Create/update ONLY the selected collections/tables
654
+ await this.createOrUpdateCollectionsForDatabases(selectedDatabases, selectedCollections);
655
+ MessageFormatter.success("Selective push completed successfully! Local config pushed to Appwrite.", { prefix: "Controller" });
564
656
  }
565
657
  async syncDb(databases = [], collections = []) {
566
658
  await this.init();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "appwrite-utils-cli",
3
3
  "description": "Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.",
4
- "version": "1.7.8",
4
+ "version": "1.7.9",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -24,7 +24,9 @@
24
24
  "appwrite-migrate": "./dist/main.js"
25
25
  },
26
26
  "scripts": {
27
- "build": "bun run tsc",
27
+ "build": "bun run tsc && bun run copy-templates",
28
+ "prebuild": "rm -rf dist",
29
+ "copy-templates": "tsx scripts/copy-templates.ts",
28
30
  "start": "tsx --no-cache src/main.ts",
29
31
  "deploy": "bun run build && npm publish --access public",
30
32
  "test": "jest",
@@ -0,0 +1,23 @@
1
+ import { cpSync, mkdirSync, existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ const src = join(process.cwd(), 'src', 'functions', 'templates');
5
+ const dest = join(process.cwd(), 'dist', 'functions', 'templates');
6
+
7
+ // Verify source exists
8
+ if (!existsSync(src)) {
9
+ console.error('❌ Error: Template source directory not found:', src);
10
+ process.exit(1);
11
+ }
12
+
13
+ // Create destination directory
14
+ mkdirSync(dest, { recursive: true });
15
+
16
+ // Copy templates recursively
17
+ try {
18
+ cpSync(src, dest, { recursive: true });
19
+ console.log('✓ Templates copied to dist/functions/templates/');
20
+ } catch (error) {
21
+ console.error('❌ Failed to copy templates:', error);
22
+ process.exit(1);
23
+ }
@@ -26,14 +26,13 @@ export const databaseCommands = {
26
26
  // Get local collections for selection
27
27
  const localCollections = (cli as any).getLocalCollections();
28
28
 
29
- // Prompt about existing configuration
30
- const { syncExisting, modifyConfiguration } = await SelectionDialogs.promptForExistingConfig(configuredDatabases);
29
+ // Push operations always use local configuration as source of truth
31
30
 
32
31
  // Select databases
33
32
  const selectedDatabaseIds = await SelectionDialogs.selectDatabases(
34
33
  availableDatabases,
35
34
  configuredDatabases,
36
- { showSelectAll: true, allowNewOnly: !syncExisting }
35
+ { showSelectAll: true, allowNewOnly: false }
37
36
  );
38
37
 
39
38
  if (selectedDatabaseIds.length === 0) {
@@ -133,16 +132,16 @@ export const databaseCommands = {
133
132
  bucketSelections
134
133
  );
135
134
 
136
- const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary);
135
+ const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary, 'push');
137
136
 
138
137
  if (!confirmed) {
139
- MessageFormatter.info("Sync operation cancelled by user", { prefix: "Database" });
138
+ MessageFormatter.info("Push operation cancelled by user", { prefix: "Database" });
140
139
  return;
141
140
  }
142
141
 
143
- // Perform selective sync using the controller
144
- MessageFormatter.progress("Starting selective sync...", { prefix: "Database" });
145
- await (cli as any).controller!.selectiveSync(databaseSelections, bucketSelections);
142
+ // Perform selective push using the controller
143
+ MessageFormatter.progress("Starting selective push...", { prefix: "Database" });
144
+ await (cli as any).controller!.selectivePush(databaseSelections, bucketSelections);
146
145
 
147
146
  MessageFormatter.success("\n✅ All database configurations pushed successfully!", { prefix: "Database" });
148
147