appwrite-utils-cli 1.7.9 → 1.8.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 (70) hide show
  1. package/CHANGELOG.md +14 -199
  2. package/README.md +87 -30
  3. package/dist/adapters/AdapterFactory.js +5 -25
  4. package/dist/adapters/DatabaseAdapter.d.ts +17 -2
  5. package/dist/adapters/LegacyAdapter.d.ts +2 -1
  6. package/dist/adapters/LegacyAdapter.js +212 -16
  7. package/dist/adapters/TablesDBAdapter.d.ts +2 -12
  8. package/dist/adapters/TablesDBAdapter.js +261 -57
  9. package/dist/cli/commands/databaseCommands.js +4 -3
  10. package/dist/cli/commands/functionCommands.js +17 -8
  11. package/dist/collections/attributes.js +447 -125
  12. package/dist/collections/methods.js +197 -186
  13. package/dist/collections/tableOperations.d.ts +86 -0
  14. package/dist/collections/tableOperations.js +434 -0
  15. package/dist/collections/transferOperations.d.ts +3 -2
  16. package/dist/collections/transferOperations.js +93 -12
  17. package/dist/config/yamlConfig.d.ts +221 -88
  18. package/dist/examples/yamlTerminologyExample.d.ts +1 -1
  19. package/dist/examples/yamlTerminologyExample.js +6 -3
  20. package/dist/functions/fnConfigDiscovery.d.ts +3 -0
  21. package/dist/functions/fnConfigDiscovery.js +108 -0
  22. package/dist/interactiveCLI.js +18 -15
  23. package/dist/main.js +211 -73
  24. package/dist/migrations/appwriteToX.d.ts +88 -23
  25. package/dist/migrations/comprehensiveTransfer.d.ts +2 -0
  26. package/dist/migrations/comprehensiveTransfer.js +83 -6
  27. package/dist/migrations/dataLoader.d.ts +227 -69
  28. package/dist/migrations/dataLoader.js +3 -3
  29. package/dist/migrations/importController.js +3 -3
  30. package/dist/migrations/relationships.d.ts +8 -2
  31. package/dist/migrations/services/ImportOrchestrator.js +3 -3
  32. package/dist/migrations/transfer.js +159 -37
  33. package/dist/shared/attributeMapper.d.ts +20 -0
  34. package/dist/shared/attributeMapper.js +203 -0
  35. package/dist/shared/selectionDialogs.js +8 -4
  36. package/dist/storage/schemas.d.ts +354 -92
  37. package/dist/utils/configDiscovery.js +4 -3
  38. package/dist/utils/versionDetection.d.ts +0 -4
  39. package/dist/utils/versionDetection.js +41 -173
  40. package/dist/utils/yamlConverter.js +89 -16
  41. package/dist/utils/yamlLoader.d.ts +1 -1
  42. package/dist/utils/yamlLoader.js +6 -2
  43. package/dist/utilsController.js +56 -19
  44. package/package.json +4 -4
  45. package/src/adapters/AdapterFactory.ts +119 -143
  46. package/src/adapters/DatabaseAdapter.ts +18 -3
  47. package/src/adapters/LegacyAdapter.ts +236 -105
  48. package/src/adapters/TablesDBAdapter.ts +773 -643
  49. package/src/cli/commands/databaseCommands.ts +13 -12
  50. package/src/cli/commands/functionCommands.ts +23 -14
  51. package/src/collections/attributes.ts +2054 -1611
  52. package/src/collections/methods.ts +208 -293
  53. package/src/collections/tableOperations.ts +506 -0
  54. package/src/collections/transferOperations.ts +218 -144
  55. package/src/examples/yamlTerminologyExample.ts +10 -5
  56. package/src/functions/fnConfigDiscovery.ts +103 -0
  57. package/src/interactiveCLI.ts +25 -20
  58. package/src/main.ts +549 -194
  59. package/src/migrations/comprehensiveTransfer.ts +126 -50
  60. package/src/migrations/dataLoader.ts +3 -3
  61. package/src/migrations/importController.ts +3 -3
  62. package/src/migrations/services/ImportOrchestrator.ts +3 -3
  63. package/src/migrations/transfer.ts +148 -131
  64. package/src/shared/attributeMapper.ts +229 -0
  65. package/src/shared/selectionDialogs.ts +29 -25
  66. package/src/utils/configDiscovery.ts +9 -3
  67. package/src/utils/versionDetection.ts +74 -228
  68. package/src/utils/yamlConverter.ts +94 -17
  69. package/src/utils/yamlLoader.ts +11 -4
  70. package/src/utilsController.ts +80 -30
@@ -5,9 +5,10 @@
5
5
  * without any translation layer. It uses object notation parameters
6
6
  * and returns Models.Row instead of Models.Document.
7
7
  */
8
- import { Query } from "node-appwrite";
8
+ import { IndexType, Query, RelationMutate, RelationshipType } from "node-appwrite";
9
9
  import { chunk } from "es-toolkit";
10
10
  import { BaseAdapter, AdapterError } from './DatabaseAdapter.js';
11
+ import { TablesDB, Client } from "node-appwrite";
11
12
  /**
12
13
  * TablesDBAdapter implementation for native TablesDB API
13
14
  */
@@ -16,12 +17,16 @@ export class TablesDBAdapter extends BaseAdapter {
16
17
  constructor(client) {
17
18
  super(client, 'tablesdb');
18
19
  // Assuming TablesDB service is available on the client
19
- this.tablesDB = client.tablesDB || client;
20
+ this.tablesDB = new TablesDB(client);
20
21
  }
21
22
  // Row (Document) Operations
22
23
  async listRows(params) {
23
24
  try {
24
- const result = await this.tablesDB.listRows(params);
25
+ const result = await this.tablesDB.listRows({
26
+ tableId: params.tableId,
27
+ databaseId: params.databaseId,
28
+ queries: params.queries || [],
29
+ });
25
30
  return {
26
31
  data: result.rows,
27
32
  rows: result.rows,
@@ -100,8 +105,9 @@ export class TablesDBAdapter extends BaseAdapter {
100
105
  }
101
106
  async createTable(params) {
102
107
  try {
108
+ const rowSecurity = params.rowSecurity ?? params.documentSecurity ?? false;
103
109
  const result = await this.tablesDB.createTable(params.databaseId, params.id, // tableId
104
- params.name, params.permissions || [], params.documentSecurity ?? false, params.enabled ?? true);
110
+ params.name, params.permissions || [], rowSecurity, params.enabled ?? true);
105
111
  return {
106
112
  data: result,
107
113
  tables: [result]
@@ -113,8 +119,9 @@ export class TablesDBAdapter extends BaseAdapter {
113
119
  }
114
120
  async updateTable(params) {
115
121
  try {
122
+ const rowSecurity = params.rowSecurity ?? params.documentSecurity;
116
123
  const result = await this.tablesDB.updateTable(params.databaseId, params.id, // tableId
117
- params.name, params.permissions, params.documentSecurity, params.enabled);
124
+ params.name, params.permissions, rowSecurity, params.enabled);
118
125
  return {
119
126
  data: result,
120
127
  tables: [result]
@@ -179,39 +186,133 @@ export class TablesDBAdapter extends BaseAdapter {
179
186
  // Attribute Operations
180
187
  async createAttribute(params) {
181
188
  try {
182
- // TablesDB uses type-specific attribute methods like the legacy SDK
189
+ // TablesDB exposes type-specific column methods
190
+ // TablesDB uses type-specific column methods
183
191
  let result;
184
- switch (params.type.toLowerCase()) {
192
+ const type = (params.type || "").toLowerCase();
193
+ const required = params.required ?? false;
194
+ const array = params.array ?? false;
195
+ const encrypt = params.encrypt ?? params.encrypted ?? false;
196
+ const normalizedDefault = params.default === null || params.default === undefined
197
+ ? undefined
198
+ : params.default;
199
+ const numberDefault = typeof normalizedDefault === "number" ? normalizedDefault : undefined;
200
+ const stringDefault = typeof normalizedDefault === "string" ? normalizedDefault : undefined;
201
+ const booleanDefault = typeof normalizedDefault === "boolean" ? normalizedDefault : undefined;
202
+ switch (type) {
185
203
  case 'string':
186
- result = await this.tablesDB.createStringAttribute(params.databaseId, params.tableId, params.key, params.size || 255, params.required ?? false, params.default, params.array ?? false, params.encrypt ?? false);
204
+ result = await this.tablesDB.createStringColumn({
205
+ databaseId: params.databaseId,
206
+ tableId: params.tableId,
207
+ key: params.key,
208
+ size: params.size || 255,
209
+ required: params.required ?? false,
210
+ xdefault: params.default,
211
+ array: params.array ?? false,
212
+ encrypt: params.encrypt ?? false
213
+ });
187
214
  break;
188
215
  case 'integer':
189
- result = await this.tablesDB.createIntegerAttribute(params.databaseId, params.tableId, params.key, params.required ?? false, params.min, params.max, params.default, params.array ?? false);
216
+ result = await this.tablesDB.createIntegerColumn({
217
+ databaseId: params.databaseId,
218
+ tableId: params.tableId,
219
+ key: params.key,
220
+ required: params.required ?? false,
221
+ min: params.min,
222
+ max: params.max,
223
+ xdefault: params.default,
224
+ array: params.array ?? false
225
+ });
190
226
  break;
191
227
  case 'float':
192
228
  case 'double':
193
- result = await this.tablesDB.createFloatAttribute(params.databaseId, params.tableId, params.key, params.required ?? false, params.min, params.max, params.default, params.array ?? false);
229
+ result = await this.tablesDB.createFloatColumn({
230
+ databaseId: params.databaseId,
231
+ tableId: params.tableId,
232
+ key: params.key,
233
+ required: params.required ?? false,
234
+ min: params.min,
235
+ max: params.max,
236
+ xdefault: params.default,
237
+ array: params.array ?? false
238
+ });
194
239
  break;
195
240
  case 'boolean':
196
- result = await this.tablesDB.createBooleanAttribute(params.databaseId, params.tableId, params.key, params.required ?? false, params.default, params.array ?? false);
241
+ result = await this.tablesDB.createBooleanColumn({
242
+ databaseId: params.databaseId,
243
+ tableId: params.tableId,
244
+ key: params.key,
245
+ required: params.required ?? false,
246
+ xdefault: params.default,
247
+ array: params.array ?? false
248
+ });
197
249
  break;
198
250
  case 'datetime':
199
- result = await this.tablesDB.createDatetimeAttribute(params.databaseId, params.tableId, params.key, params.required ?? false, params.default, params.array ?? false);
251
+ result = await this.tablesDB.createDatetimeColumn({
252
+ databaseId: params.databaseId,
253
+ tableId: params.tableId,
254
+ key: params.key,
255
+ required: params.required ?? false,
256
+ xdefault: params.default,
257
+ array: params.array ?? false
258
+ });
200
259
  break;
201
260
  case 'email':
202
- result = await this.tablesDB.createEmailAttribute(params.databaseId, params.tableId, params.key, params.required ?? false, params.default, params.array ?? false);
261
+ result = await this.tablesDB.createEmailColumn({
262
+ databaseId: params.databaseId,
263
+ tableId: params.tableId,
264
+ key: params.key,
265
+ required: params.required ?? false,
266
+ xdefault: params.default,
267
+ array: params.array ?? false
268
+ });
203
269
  break;
204
270
  case 'enum':
205
- result = await this.tablesDB.createEnumAttribute(params.databaseId, params.tableId, params.key, params.elements || [], params.required ?? false, params.default, params.array ?? false);
271
+ // Defensive: require non-empty elements
272
+ if (!Array.isArray(params.elements) || params.elements.length === 0) {
273
+ throw new AdapterError(`Enum '${params.key}' requires a non-empty 'elements' array`, 'CREATE_ENUM_ELEMENTS_REQUIRED');
274
+ }
275
+ result = await this.tablesDB.createEnumColumn({
276
+ databaseId: params.databaseId,
277
+ tableId: params.tableId,
278
+ key: params.key,
279
+ elements: params.elements,
280
+ required: params.required ?? false,
281
+ xdefault: params.default,
282
+ array: params.array ?? false
283
+ });
206
284
  break;
207
285
  case 'ip':
208
- result = await this.tablesDB.createIpAttribute(params.databaseId, params.tableId, params.key, params.required ?? false, params.default, params.array ?? false);
286
+ result = await this.tablesDB.createIpColumn({
287
+ databaseId: params.databaseId,
288
+ tableId: params.tableId,
289
+ key: params.key,
290
+ required: params.required ?? false,
291
+ xdefault: params.default,
292
+ array: params.array ?? false
293
+ });
209
294
  break;
210
295
  case 'url':
211
- result = await this.tablesDB.createUrlAttribute(params.databaseId, params.tableId, params.key, params.required ?? false, params.default, params.array ?? false);
296
+ result = await this.tablesDB.createUrlColumn({
297
+ databaseId: params.databaseId,
298
+ tableId: params.tableId,
299
+ key: params.key,
300
+ required: params.required ?? false,
301
+ xdefault: params.default,
302
+ array: params.array ?? false
303
+ });
212
304
  break;
213
305
  case 'relationship':
214
- result = await this.tablesDB.createRelationshipAttribute(params.databaseId, params.tableId, params.key, params.relatedCollection || '', params.type || 'oneToOne', params.twoWay ?? false, params.onDelete || 'restrict');
306
+ result = await this.tablesDB.createRelationshipColumn({
307
+ databaseId: params.databaseId,
308
+ tableId: params.tableId,
309
+ key: params.key,
310
+ relatedTableId: params.relatedCollection || '',
311
+ type: (params.type || 'oneToOne'),
312
+ twoWay: params.twoWay ?? false,
313
+ twoWayKey: params.twoWayKey,
314
+ onDelete: params.onDelete || 'restrict'
315
+ });
215
316
  break;
216
317
  default:
217
318
  throw new AdapterError(`Unsupported attribute type: ${params.type}`, 'UNSUPPORTED_ATTRIBUTE_TYPE');
@@ -224,9 +325,135 @@ export class TablesDBAdapter extends BaseAdapter {
224
325
  }
225
326
  async updateAttribute(params) {
226
327
  try {
227
- // TablesDB uses type-specific update methods or generic updateAttribute with positional params
228
- // Try type-specific first, fallback to generic
229
- const result = await this.tablesDB.updateStringAttribute(params.databaseId, params.tableId, params.key, params.required ?? false, params.default);
328
+ // TablesDB uses type-specific update methods with object notation
329
+ // We need to detect the existing column type to use the correct update method
330
+ // For now, we'll need to get the column info first, then use the appropriate update method
331
+ // Get the current table schema to determine the column type
332
+ const tableInfo = await this.tablesDB.getTable(params.databaseId, params.tableId);
333
+ // Find the column to determine its type
334
+ const column = tableInfo.columns?.find(col => col.key === params.key);
335
+ if (!column) {
336
+ throw new AdapterError(`Column '${params.key}' not found in table`, 'COLUMN_NOT_FOUND');
337
+ }
338
+ let result;
339
+ // Use the appropriate updateXColumn method based on the column type
340
+ // Cast column to proper Models type to access its specific properties
341
+ const columnType = column.type;
342
+ switch (columnType) {
343
+ case 'string':
344
+ const stringColumn = column;
345
+ result = await this.tablesDB.updateStringColumn({
346
+ databaseId: params.databaseId,
347
+ tableId: params.tableId,
348
+ key: params.key,
349
+ required: params.required !== undefined ? params.required : stringColumn.required,
350
+ xdefault: params.default !== undefined ? params.default : stringColumn.default,
351
+ size: params.size !== undefined ? params.size : stringColumn.size,
352
+ });
353
+ break;
354
+ case 'integer':
355
+ const integerColumn = column;
356
+ result = await this.tablesDB.updateIntegerColumn({
357
+ databaseId: params.databaseId,
358
+ tableId: params.tableId,
359
+ key: params.key,
360
+ required: params.required !== undefined ? params.required : integerColumn.required,
361
+ xdefault: params.default !== undefined ? params.default : integerColumn.default,
362
+ // Only send min/max when explicitly provided to avoid resubmitting extreme values
363
+ ...(params.min !== undefined ? { min: params.min } : {}),
364
+ ...(params.max !== undefined ? { max: params.max } : {}),
365
+ });
366
+ break;
367
+ case 'float':
368
+ case 'double':
369
+ const floatColumn = column;
370
+ result = await this.tablesDB.updateFloatColumn({
371
+ databaseId: params.databaseId,
372
+ tableId: params.tableId,
373
+ key: params.key,
374
+ required: params.required !== undefined ? params.required : floatColumn.required,
375
+ xdefault: params.default !== undefined ? params.default : floatColumn.default,
376
+ ...(params.min !== undefined ? { min: params.min } : {}),
377
+ ...(params.max !== undefined ? { max: params.max } : {}),
378
+ });
379
+ break;
380
+ case 'boolean':
381
+ const booleanColumn = column;
382
+ result = await this.tablesDB.updateBooleanColumn({
383
+ databaseId: params.databaseId,
384
+ tableId: params.tableId,
385
+ key: params.key,
386
+ required: params.required !== undefined ? params.required : booleanColumn.required,
387
+ xdefault: params.default !== undefined ? params.default : booleanColumn.default
388
+ });
389
+ break;
390
+ case 'datetime':
391
+ const datetimeColumn = column;
392
+ result = await this.tablesDB.updateDatetimeColumn({
393
+ databaseId: params.databaseId,
394
+ tableId: params.tableId,
395
+ key: params.key,
396
+ required: params.required !== undefined ? params.required : datetimeColumn.required,
397
+ xdefault: params.default !== undefined ? params.default : datetimeColumn.default
398
+ });
399
+ break;
400
+ case 'email':
401
+ const emailColumn = column;
402
+ result = await this.tablesDB.updateEmailColumn({
403
+ databaseId: params.databaseId,
404
+ tableId: params.tableId,
405
+ key: params.key,
406
+ required: params.required !== undefined ? params.required : emailColumn.required,
407
+ xdefault: params.default !== undefined ? params.default : emailColumn.default
408
+ });
409
+ break;
410
+ case 'enum':
411
+ const enumColumn = column;
412
+ // Choose elements to send only when provided, otherwise preserve existing
413
+ const provided = params.elements;
414
+ const existing = enumColumn?.elements;
415
+ const nextElements = (Array.isArray(provided) && provided.length > 0) ? provided : existing;
416
+ result = await this.tablesDB.updateEnumColumn({
417
+ databaseId: params.databaseId,
418
+ tableId: params.tableId,
419
+ key: params.key,
420
+ required: params.required !== undefined ? params.required : enumColumn.required,
421
+ xdefault: params.default !== undefined ? params.default : enumColumn.default,
422
+ elements: nextElements
423
+ });
424
+ break;
425
+ case 'ip':
426
+ const ipColumn = column;
427
+ result = await this.tablesDB.updateIpColumn({
428
+ databaseId: params.databaseId,
429
+ tableId: params.tableId,
430
+ key: params.key,
431
+ required: params.required !== undefined ? params.required : ipColumn.required,
432
+ xdefault: params.default !== undefined ? params.default : ipColumn.default
433
+ });
434
+ break;
435
+ case 'url':
436
+ const urlColumn = column;
437
+ result = await this.tablesDB.updateUrlColumn({
438
+ databaseId: params.databaseId,
439
+ tableId: params.tableId,
440
+ key: params.key,
441
+ required: params.required !== undefined ? params.required : urlColumn.required,
442
+ xdefault: params.default !== undefined ? params.default : urlColumn.default
443
+ });
444
+ break;
445
+ case 'relationship':
446
+ const relationshipColumn = column;
447
+ result = await this.tablesDB.updateRelationshipColumn({
448
+ databaseId: params.databaseId,
449
+ tableId: params.tableId,
450
+ key: params.key,
451
+ onDelete: (params.onDelete !== undefined ? params.onDelete : relationshipColumn.onDelete),
452
+ });
453
+ break;
454
+ default:
455
+ throw new AdapterError(`Unsupported column type for update: ${columnType}`, 'UNSUPPORTED_COLUMN_TYPE');
456
+ }
230
457
  return { data: result };
231
458
  }
232
459
  catch (error) {
@@ -235,7 +462,11 @@ export class TablesDBAdapter extends BaseAdapter {
235
462
  }
236
463
  async deleteAttribute(params) {
237
464
  try {
238
- const result = await this.tablesDB.deleteAttribute(params.databaseId, params.tableId, params.key);
465
+ const result = await this.tablesDB.deleteColumn({
466
+ databaseId: params.databaseId,
467
+ tableId: params.tableId,
468
+ key: params.key
469
+ });
239
470
  return { data: result };
240
471
  }
241
472
  catch (error) {
@@ -245,7 +476,7 @@ export class TablesDBAdapter extends BaseAdapter {
245
476
  // Bulk Operations (Native TablesDB Support)
246
477
  async bulkCreateRows(params) {
247
478
  try {
248
- const result = await this.tablesDB.bulkCreateRows(params);
479
+ const result = await this.tablesDB.createRows(params);
249
480
  return {
250
481
  data: result.rows,
251
482
  rows: result.rows,
@@ -258,7 +489,7 @@ export class TablesDBAdapter extends BaseAdapter {
258
489
  }
259
490
  async bulkUpsertRows(params) {
260
491
  try {
261
- const result = await this.tablesDB.bulkUpsertRows(params);
492
+ const result = await this.tablesDB.upsertRows(params);
262
493
  return {
263
494
  data: result.rows,
264
495
  rows: result.rows,
@@ -323,45 +554,18 @@ export class TablesDBAdapter extends BaseAdapter {
323
554
  * Execute a transaction (if supported by TablesDB)
324
555
  */
325
556
  async executeTransaction(operations) {
326
- if (!this.tablesDB.transaction) {
327
- throw new AdapterError('Transactions are not supported in this TablesDB version', 'TRANSACTIONS_NOT_SUPPORTED');
328
- }
329
557
  try {
330
- const result = await this.tablesDB.transaction(operations);
558
+ // Create a new transaction first
559
+ const transaction = await this.tablesDB.createTransaction();
560
+ // Add operations to the transaction
561
+ const result = await this.tablesDB.createOperations({
562
+ transactionId: transaction.$id,
563
+ operations: operations.map(op => ({ operation: 'execute', fn: op }))
564
+ });
331
565
  return { data: result };
332
566
  }
333
567
  catch (error) {
334
568
  throw new AdapterError(`Transaction failed: ${error instanceof Error ? error.message : 'Unknown error'}`, 'TRANSACTION_FAILED', error instanceof Error ? error : undefined);
335
569
  }
336
570
  }
337
- /**
338
- * Subscribe to real-time updates (if supported)
339
- */
340
- subscribeToTable(params, callback) {
341
- if (!this.tablesDB.subscribe) {
342
- throw new AdapterError('Real-time subscriptions are not supported', 'REALTIME_NOT_SUPPORTED');
343
- }
344
- try {
345
- return this.tablesDB.subscribe(`databases.${params.databaseId}.tables.${params.tableId}.rows`, callback);
346
- }
347
- catch (error) {
348
- throw new AdapterError(`Failed to subscribe to table: ${error instanceof Error ? error.message : 'Unknown error'}`, 'SUBSCRIPTION_FAILED', error instanceof Error ? error : undefined);
349
- }
350
- }
351
- /**
352
- * Get table statistics (if available in TablesDB)
353
- */
354
- async getTableStats(params) {
355
- try {
356
- if (!this.tablesDB.getTableStats) {
357
- // Fallback to basic table info
358
- return this.getTable(params);
359
- }
360
- const result = await this.tablesDB.getTableStats(params);
361
- return { data: result };
362
- }
363
- catch (error) {
364
- throw new AdapterError(`Failed to get table stats: ${error instanceof Error ? error.message : 'Unknown error'}`, 'GET_TABLE_STATS_FAILED', error instanceof Error ? error : undefined);
365
- }
366
- }
367
571
  }
@@ -21,7 +21,7 @@ export const databaseCommands = {
21
21
  const localCollections = cli.getLocalCollections();
22
22
  // Push operations always use local configuration as source of truth
23
23
  // Select databases
24
- const selectedDatabaseIds = await SelectionDialogs.selectDatabases(availableDatabases, configuredDatabases, { showSelectAll: true, allowNewOnly: false });
24
+ const selectedDatabaseIds = await SelectionDialogs.selectDatabases(availableDatabases, configuredDatabases, { showSelectAll: false, allowNewOnly: false, defaultSelected: [] });
25
25
  if (selectedDatabaseIds.length === 0) {
26
26
  MessageFormatter.warning("No databases selected. Skipping database sync.", { prefix: "Database" });
27
27
  return;
@@ -33,7 +33,8 @@ export const databaseCommands = {
33
33
  const database = availableDatabases.find(db => db.$id === databaseId);
34
34
  // Use the existing selectCollectionsAndTables method
35
35
  const selectedCollections = await cli.selectCollectionsAndTables(database, cli.controller.database, chalk.blue(`Select collections/tables to push to "${database.name}":`), true, // multiSelect
36
- true // prefer local
36
+ true, // prefer local
37
+ true // shouldFilterByDatabase
37
38
  );
38
39
  // Map selected collections to table IDs
39
40
  const selectedTableIds = selectedCollections.map((c) => c.$id || c.id);
@@ -66,7 +67,7 @@ export const databaseCommands = {
66
67
  }
67
68
  else {
68
69
  // Select buckets using SelectionDialogs
69
- const selectedBucketIds = await SelectionDialogs.selectBucketsForDatabases(selectedDatabaseIds, availableBuckets, configuredBuckets, { showSelectAll: true, groupByDatabase: true });
70
+ const selectedBucketIds = await SelectionDialogs.selectBucketsForDatabases(selectedDatabaseIds, availableBuckets, configuredBuckets, { showSelectAll: false, groupByDatabase: true, defaultSelected: [] });
70
71
  if (selectedBucketIds.length > 0) {
71
72
  // Create BucketSelection objects
72
73
  bucketSelections = SelectionDialogs.createBucketSelection(selectedBucketIds, availableBuckets, configuredBuckets, availableDatabases);
@@ -8,6 +8,7 @@ import { Query } from "node-appwrite";
8
8
  import { MessageFormatter } from "../../shared/messageFormatter.js";
9
9
  import { createFunctionTemplate, deleteFunction, downloadLatestFunctionDeployment, listFunctions, listSpecifications, } from "../../functions/methods.js";
10
10
  import { deployLocalFunction } from "../../functions/deployments.js";
11
+ import { discoverFnConfigs, mergeDiscoveredFunctions } from "../../functions/fnConfigDiscovery.js";
11
12
  import { addFunctionToYamlConfig, findYamlConfig } from "../../config/yamlConfig.js";
12
13
  import { RuntimeSchema } from "appwrite-utils";
13
14
  export const functionCommands = {
@@ -110,6 +111,13 @@ export const functionCommands = {
110
111
  MessageFormatter.error("Failed to initialize controller or load config", undefined, { prefix: "Functions" });
111
112
  return;
112
113
  }
114
+ // Discover local .fnconfig.yaml functions and merge into controller config
115
+ try {
116
+ const discovered = discoverFnConfigs(cli.currentDir);
117
+ const merged = mergeDiscoveredFunctions(cli.controller.config.functions || [], discovered);
118
+ cli.controller.config.functions = merged;
119
+ }
120
+ catch { }
113
121
  const functions = await cli.selectFunctions("Select function(s) to deploy:", true, true);
114
122
  if (!functions?.length) {
115
123
  MessageFormatter.error("No function selected", undefined, { prefix: "Functions" });
@@ -140,17 +148,18 @@ export const functionCommands = {
140
148
  }
141
149
  MessageFormatter.info(` Appwrite folder: ${cli.controller.getAppwriteFolderPath()}`, { prefix: "Functions" });
142
150
  MessageFormatter.info(` Current working dir: ${process.cwd()}`, { prefix: "Functions" });
143
- // Helper function to expand tilde in paths
144
- const expandTildePath = (path) => {
145
- if (path.startsWith('~/')) {
146
- return path.replace('~', os.homedir());
147
- }
148
- return path;
149
- };
151
+ // Resolve config dirPath relative to central YAML if it's relative
152
+ const yamlConfigPath = findYamlConfig(cli.currentDir);
153
+ const yamlBaseDir = yamlConfigPath ? require('node:path').dirname(yamlConfigPath) : process.cwd();
154
+ const expandTildePath = (p) => (p?.startsWith('~/') ? p.replace('~', os.homedir()) : p);
150
155
  // Check locations in priority order:
151
156
  const priorityLocations = [
152
157
  // 1. Config dirPath if specified (with tilde expansion)
153
- functionConfig.dirPath ? expandTildePath(functionConfig.dirPath) : undefined,
158
+ functionConfig.dirPath
159
+ ? (require('node:path').isAbsolute(expandTildePath(functionConfig.dirPath))
160
+ ? expandTildePath(functionConfig.dirPath)
161
+ : require('node:path').resolve(yamlBaseDir, expandTildePath(functionConfig.dirPath)))
162
+ : undefined,
154
163
  // 2. Appwrite config folder/functions/name
155
164
  join(cli.controller.getAppwriteFolderPath(), "functions", functionNameLower),
156
165
  // 3. Current working directory/functions/name