appwrite-utils-cli 1.8.9 → 1.9.2

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 (34) hide show
  1. package/dist/adapters/DatabaseAdapter.d.ts +9 -0
  2. package/dist/adapters/LegacyAdapter.js +1 -1
  3. package/dist/adapters/TablesDBAdapter.js +29 -4
  4. package/dist/cli/commands/databaseCommands.d.ts +1 -0
  5. package/dist/cli/commands/databaseCommands.js +90 -0
  6. package/dist/config/ConfigManager.d.ts +5 -0
  7. package/dist/config/ConfigManager.js +1 -1
  8. package/dist/config/services/ConfigDiscoveryService.d.ts +43 -47
  9. package/dist/config/services/ConfigDiscoveryService.js +155 -207
  10. package/dist/config/services/ConfigLoaderService.js +2 -7
  11. package/dist/config/yamlConfig.d.ts +2 -2
  12. package/dist/functions/methods.js +14 -2
  13. package/dist/main.js +9 -1
  14. package/dist/migrations/appwriteToX.d.ts +1 -1
  15. package/dist/migrations/dataLoader.d.ts +3 -3
  16. package/dist/shared/functionManager.js +14 -2
  17. package/dist/storage/schemas.d.ts +4 -4
  18. package/dist/utils/projectConfig.d.ts +4 -1
  19. package/dist/utils/projectConfig.js +41 -6
  20. package/dist/utilsController.d.ts +1 -0
  21. package/dist/utilsController.js +2 -1
  22. package/package.json +2 -1
  23. package/src/adapters/DatabaseAdapter.ts +12 -0
  24. package/src/adapters/LegacyAdapter.ts +28 -28
  25. package/src/adapters/TablesDBAdapter.ts +46 -4
  26. package/src/cli/commands/databaseCommands.ts +141 -11
  27. package/src/config/ConfigManager.ts +10 -1
  28. package/src/config/services/ConfigDiscoveryService.ts +180 -233
  29. package/src/config/services/ConfigLoaderService.ts +2 -10
  30. package/src/functions/methods.ts +15 -2
  31. package/src/main.ts +213 -204
  32. package/src/shared/functionManager.ts +15 -3
  33. package/src/utils/projectConfig.ts +57 -16
  34. package/src/utilsController.ts +73 -72
package/src/main.ts CHANGED
@@ -41,8 +41,9 @@ if (!(globalThis as any).require) {
41
41
  (globalThis as any).require = require;
42
42
  }
43
43
 
44
- interface CliOptions {
44
+ interface CliOptions {
45
45
  config?: string;
46
+ appwriteConfig?: boolean;
46
47
  it?: boolean;
47
48
  dbIds?: string;
48
49
  collectionIds?: string;
@@ -86,13 +87,13 @@ interface CliOptions {
86
87
  session?: string;
87
88
  listBackups?: boolean;
88
89
  autoSync?: boolean;
89
- selectBuckets?: boolean;
90
- // New schema/constant CLI flags
91
- generateSchemas?: boolean;
92
- schemaFormat?: 'zod' | 'json' | 'pydantic' | 'both' | 'all';
93
- schemaOutDir?: string;
94
- constantsInclude?: string;
95
- }
90
+ selectBuckets?: boolean;
91
+ // New schema/constant CLI flags
92
+ generateSchemas?: boolean;
93
+ schemaFormat?: 'zod' | 'json' | 'pydantic' | 'both' | 'all';
94
+ schemaOutDir?: string;
95
+ constantsInclude?: string;
96
+ }
96
97
 
97
98
  type ParsedArgv = ArgumentsCamelCase<CliOptions>;
98
99
 
@@ -185,15 +186,15 @@ async function performEnhancedSync(
185
186
  const allowNewOnly = !syncExisting;
186
187
 
187
188
  // Select databases
188
- const selectedDatabaseIds = await SelectionDialogs.selectDatabases(
189
- availableDatabases,
190
- configuredDatabases,
191
- {
192
- showSelectAll: false,
193
- allowNewOnly,
194
- defaultSelected: []
195
- }
196
- );
189
+ const selectedDatabaseIds = await SelectionDialogs.selectDatabases(
190
+ availableDatabases,
191
+ configuredDatabases,
192
+ {
193
+ showSelectAll: false,
194
+ allowNewOnly,
195
+ defaultSelected: []
196
+ }
197
+ );
197
198
 
198
199
  if (selectedDatabaseIds.length === 0) {
199
200
  MessageFormatter.warning("No databases selected for sync", { prefix: "Sync" });
@@ -218,17 +219,17 @@ async function performEnhancedSync(
218
219
  const configuredTables = controller.config.collections || [];
219
220
 
220
221
  // Select tables for this database
221
- const selectedTableIds = await SelectionDialogs.selectTablesForDatabase(
222
- databaseId,
223
- database.name,
224
- availableTables,
225
- configuredTables,
226
- {
227
- showSelectAll: false,
228
- allowNewOnly,
229
- defaultSelected: []
230
- }
231
- );
222
+ const selectedTableIds = await SelectionDialogs.selectTablesForDatabase(
223
+ databaseId,
224
+ database.name,
225
+ availableTables,
226
+ configuredTables,
227
+ {
228
+ showSelectAll: false,
229
+ allowNewOnly,
230
+ defaultSelected: []
231
+ }
232
+ );
232
233
 
233
234
  tableSelectionsMap.set(databaseId, selectedTableIds);
234
235
 
@@ -251,17 +252,17 @@ async function performEnhancedSync(
251
252
  // you'd fetch this from the Appwrite API
252
253
  const availableBuckets = configuredBuckets; // Placeholder
253
254
 
254
- selectedBucketIds = await SelectionDialogs.selectBucketsForDatabases(
255
- selectedDatabaseIds,
256
- availableBuckets,
257
- configuredBuckets,
258
- {
259
- showSelectAll: false,
260
- allowNewOnly: parsedArgv.selectBuckets ? false : allowNewOnly,
261
- groupByDatabase: true,
262
- defaultSelected: []
263
- }
264
- );
255
+ selectedBucketIds = await SelectionDialogs.selectBucketsForDatabases(
256
+ selectedDatabaseIds,
257
+ availableBuckets,
258
+ configuredBuckets,
259
+ {
260
+ showSelectAll: false,
261
+ allowNewOnly: parsedArgv.selectBuckets ? false : allowNewOnly,
262
+ groupByDatabase: true,
263
+ defaultSelected: []
264
+ }
265
+ );
265
266
  } catch (error) {
266
267
  MessageFormatter.warning("Could not fetch storage buckets", { prefix: "Sync" });
267
268
  logger.warn("Failed to fetch buckets during sync", { error });
@@ -370,6 +371,11 @@ const argv = yargs(hideBin(process.argv))
370
371
  type: "string",
371
372
  description: "Path to Appwrite configuration file (appwriteConfig.ts)",
372
373
  })
374
+ .option("appwriteConfig", {
375
+ alias: ["appwrite-config", "use-appwrite-config"],
376
+ type: "boolean",
377
+ description: "Prefer loading from appwrite.config.json instead of config.yaml",
378
+ })
373
379
  .option("it", {
374
380
  alias: ["interactive", "i"],
375
381
  type: "boolean",
@@ -572,30 +578,30 @@ const argv = yargs(hideBin(process.argv))
572
578
  "Comma-separated list of languages for constants (typescript,javascript,python,php,dart,json,env)",
573
579
  default: "typescript",
574
580
  })
575
- .option("constantsOutput", {
576
- type: "string",
577
- description:
578
- "Output directory for generated constants files (default: config-folder/constants)",
579
- default: "auto",
580
- })
581
- .option("constantsInclude", {
582
- type: "string",
583
- description:
584
- "Comma-separated categories to include: databases,collections,buckets,functions",
585
- })
586
- .option("generateSchemas", {
587
- type: "boolean",
588
- description: "Generate schemas/models without interactive prompts",
589
- })
590
- .option("schemaFormat", {
591
- type: "string",
592
- choices: ["zod", "json", "pydantic", "both", "all"],
593
- description: "Schema format: zod, json, pydantic, both (zod+json), or all",
594
- })
595
- .option("schemaOutDir", {
596
- type: "string",
597
- description: "Output directory for generated schemas (absolute path respected)",
598
- })
581
+ .option("constantsOutput", {
582
+ type: "string",
583
+ description:
584
+ "Output directory for generated constants files (default: config-folder/constants)",
585
+ default: "auto",
586
+ })
587
+ .option("constantsInclude", {
588
+ type: "string",
589
+ description:
590
+ "Comma-separated categories to include: databases,collections,buckets,functions",
591
+ })
592
+ .option("generateSchemas", {
593
+ type: "boolean",
594
+ description: "Generate schemas/models without interactive prompts",
595
+ })
596
+ .option("schemaFormat", {
597
+ type: "string",
598
+ choices: ["zod", "json", "pydantic", "both", "all"],
599
+ description: "Schema format: zod, json, pydantic, both (zod+json), or all",
600
+ })
601
+ .option("schemaOutDir", {
602
+ type: "string",
603
+ description: "Output directory for generated schemas (absolute path respected)",
604
+ })
599
605
  .option("migrateCollectionsToTables", {
600
606
  alias: ["migrate-collections"],
601
607
  type: "boolean",
@@ -803,7 +809,7 @@ async function main() {
803
809
  finalDirectConfig
804
810
  );
805
811
 
806
- // Pass session authentication options to the controller
812
+ // Pass session authentication and config options to the controller
807
813
  const initOptions: any = {};
808
814
  if (argv.useSession || argv.sessionCookie) {
809
815
  initOptions.useSession = true;
@@ -811,6 +817,9 @@ async function main() {
811
817
  initOptions.sessionCookie = argv.sessionCookie;
812
818
  }
813
819
  }
820
+ if (argv.appwriteConfig) {
821
+ initOptions.preferJson = true;
822
+ }
814
823
 
815
824
  await controller.init(initOptions);
816
825
 
@@ -1364,146 +1373,146 @@ async function main() {
1364
1373
  }
1365
1374
  }
1366
1375
 
1367
- if (parsedArgv.push) {
1368
- await controller.init();
1369
- if (!controller.database || !controller.config) {
1370
- MessageFormatter.error("Database or config not initialized", undefined, { prefix: "Push" });
1371
- return;
1372
- }
1373
-
1374
- // Fetch available DBs
1375
- const availableDatabases = await fetchAllDatabases(controller.database);
1376
- if (availableDatabases.length === 0) {
1377
- MessageFormatter.warning("No databases found in remote project", { prefix: "Push" });
1378
- return;
1379
- }
1380
-
1381
- // Determine selected DBs
1382
- let selectedDbIds: string[] = [];
1383
- if (parsedArgv.dbIds) {
1384
- selectedDbIds = parsedArgv.dbIds.split(/[,\s]+/).filter(Boolean);
1385
- } else {
1386
- selectedDbIds = await SelectionDialogs.selectDatabases(
1387
- availableDatabases,
1388
- controller.config.databases || [],
1389
- { showSelectAll: false, allowNewOnly: false, defaultSelected: [] }
1390
- );
1391
- }
1392
-
1393
- if (selectedDbIds.length === 0) {
1394
- MessageFormatter.warning("No databases selected for push", { prefix: "Push" });
1395
- return;
1396
- }
1397
-
1398
- // Build DatabaseSelection[] with tableIds per DB
1399
- const databaseSelections: DatabaseSelection[] = [];
1400
- const allConfigItems = controller.config.collections || controller.config.tables || [];
1401
- let lastSelectedTableIds: string[] | null = null;
1402
-
1403
- for (const dbId of selectedDbIds) {
1404
- const db = availableDatabases.find(d => d.$id === dbId);
1405
- if (!db) continue;
1406
-
1407
- // Filter config items eligible for this DB according to databaseId/databaseIds rule
1408
- const eligibleConfigItems = (allConfigItems as any[]).filter(item => {
1409
- const one = item.databaseId as string | undefined;
1410
- const many = item.databaseIds as string[] | undefined;
1411
- if (Array.isArray(many) && many.length > 0) return many.includes(dbId);
1412
- if (one) return one === dbId;
1413
- return true; // eligible everywhere if unspecified
1414
- });
1415
-
1416
- // Fetch available tables from remote for selection context
1417
- const availableTables = await fetchAllCollections(dbId, controller.database);
1418
-
1419
- // Determine selected table IDs
1420
- let selectedTableIds: string[] = [];
1421
- if (parsedArgv.collectionIds) {
1422
- // Non-interactive: respect provided table IDs as-is (apply to each selected DB)
1423
- selectedTableIds = parsedArgv.collectionIds.split(/[\,\s]+/).filter(Boolean);
1424
- } else {
1425
- // If we have a previous selection, offer to reuse it
1426
- if (lastSelectedTableIds && lastSelectedTableIds.length > 0) {
1427
- const inquirer = (await import("inquirer")).default;
1428
- const { reuseMode } = await inquirer.prompt([
1429
- {
1430
- type: "list",
1431
- name: "reuseMode",
1432
- message: `How do you want to select tables for ${db.name}?`,
1433
- choices: [
1434
- { name: `Use same selection as previous (${lastSelectedTableIds.length} items)`, value: "same" },
1435
- { name: `Filter by this database (manual select)`, value: "filter" },
1436
- { name: `Show all available in this database (manual select)`, value: "all" }
1437
- ],
1438
- default: "same"
1439
- }
1440
- ]);
1441
-
1442
- if (reuseMode === "same") {
1443
- selectedTableIds = [...lastSelectedTableIds];
1444
- } else if (reuseMode === "all") {
1445
- selectedTableIds = await SelectionDialogs.selectTablesForDatabase(
1446
- dbId,
1447
- db.name,
1448
- availableTables,
1449
- allConfigItems as any[],
1450
- { showSelectAll: false, allowNewOnly: false, defaultSelected: lastSelectedTableIds }
1451
- );
1452
- } else {
1453
- selectedTableIds = await SelectionDialogs.selectTablesForDatabase(
1454
- dbId,
1455
- db.name,
1456
- availableTables,
1457
- eligibleConfigItems,
1458
- { showSelectAll: false, allowNewOnly: true, defaultSelected: lastSelectedTableIds }
1459
- );
1460
- }
1461
- } else {
1462
- selectedTableIds = await SelectionDialogs.selectTablesForDatabase(
1463
- dbId,
1464
- db.name,
1465
- availableTables,
1466
- eligibleConfigItems,
1467
- { showSelectAll: false, allowNewOnly: true, defaultSelected: [] }
1468
- );
1469
- }
1470
- }
1471
-
1472
- databaseSelections.push({
1473
- databaseId: db.$id,
1474
- databaseName: db.name,
1475
- tableIds: selectedTableIds,
1476
- tableNames: [],
1477
- isNew: false,
1478
- });
1479
- if (!parsedArgv.collectionIds) {
1480
- lastSelectedTableIds = selectedTableIds;
1481
- }
1482
- }
1483
-
1484
- if (databaseSelections.every(sel => sel.tableIds.length === 0)) {
1485
- MessageFormatter.warning("No tables/collections selected for push", { prefix: "Push" });
1486
- return;
1487
- }
1488
-
1489
- const pushSummary: Record<string, string | number | string[]> = {
1490
- databases: databaseSelections.length,
1491
- collections: databaseSelections.reduce((sum, s) => sum + s.tableIds.length, 0),
1492
- details: databaseSelections.map(s => `${s.databaseId}: ${s.tableIds.length} items`),
1493
- };
1494
- // Skip confirmation if both dbIds and collectionIds are provided (non-interactive)
1495
- if (!(parsedArgv.dbIds && parsedArgv.collectionIds)) {
1496
- const confirmed = await ConfirmationDialogs.showOperationSummary('Push', pushSummary, { confirmationRequired: true });
1497
- if (!confirmed) {
1498
- MessageFormatter.info("Push operation cancelled", { prefix: "Push" });
1499
- return;
1500
- }
1501
- }
1502
-
1503
- await controller.selectivePush(databaseSelections, []);
1504
- operationStats.pushedDatabases = databaseSelections.length;
1505
- operationStats.pushedCollections = databaseSelections.reduce((sum, s) => sum + s.tableIds.length, 0);
1506
- } else if (parsedArgv.sync) {
1376
+ if (parsedArgv.push) {
1377
+ await controller.init();
1378
+ if (!controller.database || !controller.config) {
1379
+ MessageFormatter.error("Database or config not initialized", undefined, { prefix: "Push" });
1380
+ return;
1381
+ }
1382
+
1383
+ // Fetch available DBs
1384
+ const availableDatabases = await fetchAllDatabases(controller.database);
1385
+ if (availableDatabases.length === 0) {
1386
+ MessageFormatter.warning("No databases found in remote project", { prefix: "Push" });
1387
+ return;
1388
+ }
1389
+
1390
+ // Determine selected DBs
1391
+ let selectedDbIds: string[] = [];
1392
+ if (parsedArgv.dbIds) {
1393
+ selectedDbIds = parsedArgv.dbIds.split(/[,\s]+/).filter(Boolean);
1394
+ } else {
1395
+ selectedDbIds = await SelectionDialogs.selectDatabases(
1396
+ availableDatabases,
1397
+ controller.config.databases || [],
1398
+ { showSelectAll: false, allowNewOnly: false, defaultSelected: [] }
1399
+ );
1400
+ }
1401
+
1402
+ if (selectedDbIds.length === 0) {
1403
+ MessageFormatter.warning("No databases selected for push", { prefix: "Push" });
1404
+ return;
1405
+ }
1406
+
1407
+ // Build DatabaseSelection[] with tableIds per DB
1408
+ const databaseSelections: DatabaseSelection[] = [];
1409
+ const allConfigItems = controller.config.collections || controller.config.tables || [];
1410
+ let lastSelectedTableIds: string[] | null = null;
1411
+
1412
+ for (const dbId of selectedDbIds) {
1413
+ const db = availableDatabases.find(d => d.$id === dbId);
1414
+ if (!db) continue;
1415
+
1416
+ // Filter config items eligible for this DB according to databaseId/databaseIds rule
1417
+ const eligibleConfigItems = (allConfigItems as any[]).filter(item => {
1418
+ const one = item.databaseId as string | undefined;
1419
+ const many = item.databaseIds as string[] | undefined;
1420
+ if (Array.isArray(many) && many.length > 0) return many.includes(dbId);
1421
+ if (one) return one === dbId;
1422
+ return true; // eligible everywhere if unspecified
1423
+ });
1424
+
1425
+ // Fetch available tables from remote for selection context
1426
+ const availableTables = await fetchAllCollections(dbId, controller.database);
1427
+
1428
+ // Determine selected table IDs
1429
+ let selectedTableIds: string[] = [];
1430
+ if (parsedArgv.collectionIds) {
1431
+ // Non-interactive: respect provided table IDs as-is (apply to each selected DB)
1432
+ selectedTableIds = parsedArgv.collectionIds.split(/[\,\s]+/).filter(Boolean);
1433
+ } else {
1434
+ // If we have a previous selection, offer to reuse it
1435
+ if (lastSelectedTableIds && lastSelectedTableIds.length > 0) {
1436
+ const inquirer = (await import("inquirer")).default;
1437
+ const { reuseMode } = await inquirer.prompt([
1438
+ {
1439
+ type: "list",
1440
+ name: "reuseMode",
1441
+ message: `How do you want to select tables for ${db.name}?`,
1442
+ choices: [
1443
+ { name: `Use same selection as previous (${lastSelectedTableIds.length} items)`, value: "same" },
1444
+ { name: `Filter by this database (manual select)`, value: "filter" },
1445
+ { name: `Show all available in this database (manual select)`, value: "all" }
1446
+ ],
1447
+ default: "same"
1448
+ }
1449
+ ]);
1450
+
1451
+ if (reuseMode === "same") {
1452
+ selectedTableIds = [...lastSelectedTableIds];
1453
+ } else if (reuseMode === "all") {
1454
+ selectedTableIds = await SelectionDialogs.selectTablesForDatabase(
1455
+ dbId,
1456
+ db.name,
1457
+ availableTables,
1458
+ allConfigItems as any[],
1459
+ { showSelectAll: false, allowNewOnly: false, defaultSelected: lastSelectedTableIds }
1460
+ );
1461
+ } else {
1462
+ selectedTableIds = await SelectionDialogs.selectTablesForDatabase(
1463
+ dbId,
1464
+ db.name,
1465
+ availableTables,
1466
+ eligibleConfigItems,
1467
+ { showSelectAll: false, allowNewOnly: true, defaultSelected: lastSelectedTableIds }
1468
+ );
1469
+ }
1470
+ } else {
1471
+ selectedTableIds = await SelectionDialogs.selectTablesForDatabase(
1472
+ dbId,
1473
+ db.name,
1474
+ availableTables,
1475
+ eligibleConfigItems,
1476
+ { showSelectAll: false, allowNewOnly: true, defaultSelected: [] }
1477
+ );
1478
+ }
1479
+ }
1480
+
1481
+ databaseSelections.push({
1482
+ databaseId: db.$id,
1483
+ databaseName: db.name,
1484
+ tableIds: selectedTableIds,
1485
+ tableNames: [],
1486
+ isNew: false,
1487
+ });
1488
+ if (!parsedArgv.collectionIds) {
1489
+ lastSelectedTableIds = selectedTableIds;
1490
+ }
1491
+ }
1492
+
1493
+ if (databaseSelections.every(sel => sel.tableIds.length === 0)) {
1494
+ MessageFormatter.warning("No tables/collections selected for push", { prefix: "Push" });
1495
+ return;
1496
+ }
1497
+
1498
+ const pushSummary: Record<string, string | number | string[]> = {
1499
+ databases: databaseSelections.length,
1500
+ collections: databaseSelections.reduce((sum, s) => sum + s.tableIds.length, 0),
1501
+ details: databaseSelections.map(s => `${s.databaseId}: ${s.tableIds.length} items`),
1502
+ };
1503
+ // Skip confirmation if both dbIds and collectionIds are provided (non-interactive)
1504
+ if (!(parsedArgv.dbIds && parsedArgv.collectionIds)) {
1505
+ const confirmed = await ConfirmationDialogs.showOperationSummary('Push', pushSummary, { confirmationRequired: true });
1506
+ if (!confirmed) {
1507
+ MessageFormatter.info("Push operation cancelled", { prefix: "Push" });
1508
+ return;
1509
+ }
1510
+ }
1511
+
1512
+ await controller.selectivePush(databaseSelections, []);
1513
+ operationStats.pushedDatabases = databaseSelections.length;
1514
+ operationStats.pushedCollections = databaseSelections.reduce((sum, s) => sum + s.tableIds.length, 0);
1515
+ } else if (parsedArgv.sync) {
1507
1516
  // Enhanced SYNC: Pull from remote with intelligent configuration detection
1508
1517
  if (parsedArgv.autoSync) {
1509
1518
  // Legacy behavior: sync everything without prompts
@@ -1,5 +1,5 @@
1
1
  import { Client, Functions, Runtime, type Models } from "node-appwrite";
2
- import { type AppwriteFunction } from "appwrite-utils";
2
+ import { type AppwriteFunction, EventTypeSchema } from "appwrite-utils";
3
3
  import { join, relative, resolve, basename } from "node:path";
4
4
  import fs from "node:fs";
5
5
  import chalk from "chalk";
@@ -10,14 +10,26 @@ import { MessageFormatter } from "./messageFormatter.js";
10
10
  /**
11
11
  * Validates and filters events array for Appwrite functions
12
12
  * - Filters out empty/invalid strings
13
+ * - Validates against EventTypeSchema
13
14
  * - Limits to 100 items maximum (Appwrite limit)
14
15
  * - Returns empty array if input is invalid
15
16
  */
16
17
  const validateEvents = (events?: string[]): string[] => {
17
18
  if (!events || !Array.isArray(events)) return [];
18
-
19
+
19
20
  return events
20
- .filter(event => event && typeof event === 'string' && event.trim().length > 0)
21
+ .filter(event => {
22
+ if (!event || typeof event !== 'string' || event.trim().length === 0) {
23
+ return false;
24
+ }
25
+ // Validate against EventTypeSchema
26
+ const result = EventTypeSchema.safeParse(event);
27
+ if (!result.success) {
28
+ MessageFormatter.warning(`Invalid event type "${event}" will be filtered out`, { prefix: "Functions" });
29
+ return false;
30
+ }
31
+ return true;
32
+ })
21
33
  .slice(0, 100);
22
34
  };
23
35
 
@@ -1,5 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
- import { join, dirname } from "node:path";
2
+ import { join, dirname, resolve, isAbsolute } from "node:path";
3
3
  import { MessageFormatter } from "../shared/messageFormatter.js";
4
4
  import type { AppwriteConfig } from "appwrite-utils";
5
5
 
@@ -185,12 +185,17 @@ export function detectApiModeFromProject(projectConfig: AppwriteProjectConfig):
185
185
 
186
186
  /**
187
187
  * Convert project config to AppwriteConfig format
188
+ * @param projectConfig The project config to convert
189
+ * @param configPath Optional path to the config file (for resolving relative paths)
190
+ * @param existingConfig Optional existing config to merge with
188
191
  */
189
192
  export function projectConfigToAppwriteConfig(
190
193
  projectConfig: AppwriteProjectConfig,
194
+ configPath?: string,
191
195
  existingConfig?: Partial<AppwriteConfig>
192
196
  ): Partial<AppwriteConfig> {
193
197
  const apiMode = detectApiModeFromProject(projectConfig);
198
+ const configDir = configPath ? dirname(configPath) : process.cwd();
194
199
 
195
200
  const baseConfig: Partial<AppwriteConfig> = {
196
201
  ...existingConfig,
@@ -203,10 +208,22 @@ export function projectConfigToAppwriteConfig(
203
208
  baseConfig.appwriteEndpoint = projectConfig.endpoint;
204
209
  }
205
210
 
206
- // Convert databases (works for both legacy and TablesDB)
207
- if (projectConfig.databases || projectConfig.tablesDB) {
208
- const databases = projectConfig.databases || projectConfig.tablesDB || [];
209
- baseConfig.databases = databases.map(db => ({
211
+ // Merge databases and tablesDB arrays (modern Appwrite may have both)
212
+ const allDatabases = [
213
+ ...(projectConfig.databases || []),
214
+ ...(projectConfig.tablesDB || [])
215
+ ];
216
+
217
+ // Remove duplicates by $id
218
+ const uniqueDatabasesMap = new Map();
219
+ for (const db of allDatabases) {
220
+ if (!uniqueDatabasesMap.has(db.$id)) {
221
+ uniqueDatabasesMap.set(db.$id, db);
222
+ }
223
+ }
224
+
225
+ if (uniqueDatabasesMap.size > 0) {
226
+ baseConfig.databases = Array.from(uniqueDatabasesMap.values()).map(db => ({
210
227
  $id: db.$id,
211
228
  name: db.name,
212
229
  // Add basic bucket configuration if not exists
@@ -235,6 +252,30 @@ export function projectConfigToAppwriteConfig(
235
252
  }));
236
253
  }
237
254
 
255
+ // Convert functions with path normalization
256
+ if (projectConfig.functions) {
257
+ baseConfig.functions = projectConfig.functions.map(func => {
258
+ const normalizedFunc: any = {
259
+ $id: func.$id,
260
+ name: func.name,
261
+ runtime: func.runtime,
262
+ entrypoint: func.entrypoint,
263
+ commands: func.commands,
264
+ events: func.events,
265
+ };
266
+
267
+ // Convert path to dirPath and make absolute
268
+ if (func.path) {
269
+ const expandedPath = func.path;
270
+ normalizedFunc.dirPath = isAbsolute(expandedPath)
271
+ ? expandedPath
272
+ : resolve(configDir, expandedPath);
273
+ }
274
+
275
+ return normalizedFunc;
276
+ });
277
+ }
278
+
238
279
  return baseConfig;
239
280
  }
240
281
 
@@ -251,16 +292,16 @@ export function getCollectionsFromProject(projectConfig: AppwriteProjectConfig):
251
292
  documentSecurity: table.rowSecurity || false,
252
293
  enabled: table.enabled !== false,
253
294
  // Convert columns to attributes for compatibility
254
- attributes: table.columns.map(col => ({
255
- key: col.key,
256
- type: col.type,
257
- required: col.required,
258
- array: col.array,
259
- size: col.size,
260
- default: col.default,
261
- encrypt: col.encrypt,
262
- unique: col.unique,
263
- })),
295
+ attributes: table.columns.map(col => ({
296
+ key: col.key,
297
+ type: col.type,
298
+ required: col.required,
299
+ array: col.array,
300
+ size: col.size,
301
+ default: col.default,
302
+ encrypt: col.encrypt,
303
+ unique: col.unique,
304
+ })),
264
305
  indexes: table.indexes || [],
265
306
  // Mark as coming from TablesDB for processing
266
307
  _isFromTablesDir: true,
@@ -296,4 +337,4 @@ export function isTablesDBProject(projectConfig: AppwriteProjectConfig): boolean
296
337
  */
297
338
  export function getProjectDirectoryName(projectConfig: AppwriteProjectConfig): "tables" | "collections" {
298
339
  return isTablesDBProject(projectConfig) ? "tables" : "collections";
299
- }
340
+ }