appwrite-utils-cli 1.7.7 → 1.7.8

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.
@@ -26,9 +26,52 @@ import {
26
26
  type Specification,
27
27
  } from "appwrite-utils";
28
28
  import { getDatabaseFromConfig } from "./afterImportActions.js";
29
+ import { getAdapterFromConfig } from "../utils/getClientFromConfig.js";
29
30
  import { listBuckets } from "../storage/methods.js";
30
31
  import { listFunctions, listFunctionDeployments } from "../functions/methods.js";
31
32
  import { MessageFormatter } from "../shared/messageFormatter.js";
33
+ import { isLegacyDatabases } from "../utils/typeGuards.js";
34
+ import type { DatabaseAdapter } from "../adapters/DatabaseAdapter.js";
35
+ import type { DatabaseSelection, BucketSelection } from "../shared/selectionDialogs.js";
36
+
37
+ /**
38
+ * Convert between collection and table terminology based on data structure
39
+ */
40
+ function normalizeCollectionOrTable(collection: any): {
41
+ attributes: any[];
42
+ permissions: any[];
43
+ name: string;
44
+ $id: string;
45
+ enabled: boolean;
46
+ indexes?: any[];
47
+ } {
48
+ // Check if this is a table (has columns) or collection (has attributes)
49
+ const isTable = collection.columns && Array.isArray(collection.columns);
50
+
51
+ if (isTable) {
52
+ // Table structure - convert columns to attributes
53
+ MessageFormatter.debug(`Detected table structure: ${collection.name || collection.tableName}`, { prefix: "Migration" });
54
+ return {
55
+ ...collection,
56
+ attributes: collection.columns || [],
57
+ permissions: collection.$permissions || collection.permissions || [],
58
+ name: collection.name || collection.tableName,
59
+ $id: collection.$id || collection.tableId,
60
+ enabled: collection.enabled ?? true
61
+ };
62
+ } else {
63
+ // Collection structure - use as-is with fallbacks
64
+ MessageFormatter.debug(`Detected collection structure: ${collection.name}`, { prefix: "Migration" });
65
+ return {
66
+ ...collection,
67
+ attributes: collection.attributes || [],
68
+ permissions: collection.$permissions || collection.permissions || [],
69
+ name: collection.name,
70
+ $id: collection.$id,
71
+ enabled: collection.enabled ?? true
72
+ };
73
+ }
74
+ }
32
75
 
33
76
  export class AppwriteToX {
34
77
  config: AppwriteConfig;
@@ -36,6 +79,9 @@ export class AppwriteToX {
36
79
  updatedConfig: AppwriteConfig;
37
80
  collToAttributeMap = new Map<string, Attribute[]>();
38
81
  appwriteFolderPath: string;
82
+ adapter?: DatabaseAdapter;
83
+ apiMode?: 'legacy' | 'tablesdb';
84
+ databaseApiModes = new Map<string, 'legacy' | 'tablesdb'>();
39
85
 
40
86
  constructor(
41
87
  config: AppwriteConfig,
@@ -49,6 +95,27 @@ export class AppwriteToX {
49
95
  this.ensureClientInitialized();
50
96
  }
51
97
 
98
+ /**
99
+ * Initialize adapter for database operations with API mode detection
100
+ */
101
+ private async initializeAdapter(): Promise<void> {
102
+ if (!this.adapter) {
103
+ try {
104
+ const { adapter, apiMode } = await getAdapterFromConfig(this.config);
105
+ this.adapter = adapter;
106
+ this.apiMode = apiMode;
107
+ MessageFormatter.info(`Initialized database adapter with API mode: ${apiMode}`, { prefix: "Migration" });
108
+ } catch (error) {
109
+ MessageFormatter.warning(
110
+ `Failed to initialize adapter, falling back to legacy client: ${error instanceof Error ? error.message : 'Unknown error'}`,
111
+ { prefix: "Migration" }
112
+ );
113
+ // Fallback to legacy client initialization
114
+ this.ensureClientInitialized();
115
+ }
116
+ }
117
+ }
118
+
52
119
  private ensureClientInitialized() {
53
120
  if (!this.config.appwriteClient) {
54
121
  const client = new Client();
@@ -83,18 +150,128 @@ export class AppwriteToX {
83
150
  };
84
151
 
85
152
  updateCollectionConfigAttributes = (collection: Models.Collection) => {
86
- for (const attribute of collection.attributes) {
87
- const attributeMap = this.collToAttributeMap.get(
88
- collection.name as string
89
- );
153
+ // Normalize collection/table structure to handle both TablesDB and Legacy formats
154
+ const normalizedCollection = normalizeCollectionOrTable(collection);
155
+
156
+ for (const attribute of normalizedCollection.attributes) {
157
+ if (!attribute) {
158
+ MessageFormatter.warning("Skipping null/undefined attribute in updateCollectionConfigAttributes", { prefix: "Migration" });
159
+ continue;
160
+ }
90
161
  const attributeParsed = attributeSchema.parse(attribute);
91
162
  this.collToAttributeMap
92
- .get(collection.name as string)
163
+ .get(normalizedCollection.name)
93
164
  ?.push(attributeParsed);
94
165
  }
95
166
  };
96
167
 
97
- async appwriteSync(config: AppwriteConfig, databases?: Models.Database[]) {
168
+ /**
169
+ * Fetch collections/tables using the appropriate adapter or legacy client
170
+ */
171
+ private async fetchCollectionsOrTables(databaseId: string, db: any): Promise<Models.Collection[]> {
172
+ // Try to use adapter first
173
+ if (this.adapter) {
174
+ try {
175
+ const result = await this.adapter.listTables({ databaseId });
176
+ const items = (result as any).tables || result.collections || [];
177
+ MessageFormatter.info(`Fetched ${items.length} items using ${this.apiMode} adapter`, { prefix: "Migration" });
178
+ return items as Models.Collection[];
179
+ } catch (error) {
180
+ MessageFormatter.warning(
181
+ `Adapter fetch failed, falling back to legacy: ${error instanceof Error ? error.message : 'Unknown error'}`,
182
+ { prefix: "Migration" }
183
+ );
184
+ }
185
+ }
186
+
187
+ // Fallback to legacy method
188
+ try {
189
+ const collections = await fetchAllCollections(databaseId, db);
190
+ MessageFormatter.info(`Fetched ${collections.length} collections using legacy client`, { prefix: "Migration" });
191
+ return collections;
192
+ } catch (error) {
193
+ MessageFormatter.error(
194
+ "Failed to fetch collections with both adapter and legacy methods",
195
+ error instanceof Error ? error : new Error(String(error)),
196
+ { prefix: "Migration" }
197
+ );
198
+ throw error;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Get collection/table using the appropriate adapter or legacy client
204
+ */
205
+ private async getCollectionOrTable(databaseId: string, collectionId: string): Promise<Models.Collection> {
206
+ // Try to use adapter first
207
+ if (this.adapter) {
208
+ try {
209
+ const result = await this.adapter.getTable({ databaseId, tableId: collectionId });
210
+ return result as Models.Collection;
211
+ } catch (error) {
212
+ MessageFormatter.warning(
213
+ `Adapter get failed, falling back to legacy: ${error instanceof Error ? error.message : 'Unknown error'}`,
214
+ { prefix: "Migration" }
215
+ );
216
+ }
217
+ }
218
+
219
+ // Fallback to legacy method
220
+ const db = getDatabaseFromConfig(this.config);
221
+ return await db.getCollection(databaseId, collectionId);
222
+ }
223
+
224
+ /**
225
+ * Detect API mode for a specific database by testing adapter capabilities
226
+ */
227
+ private async detectDatabaseApiMode(databaseId: string): Promise<'legacy' | 'tablesdb'> {
228
+ // If we already detected this database, return cached result
229
+ if (this.databaseApiModes.has(databaseId)) {
230
+ return this.databaseApiModes.get(databaseId)!;
231
+ }
232
+
233
+ // If we have a global adapter, use its API mode as default
234
+ if (this.apiMode) {
235
+ this.databaseApiModes.set(databaseId, this.apiMode);
236
+ MessageFormatter.debug(`Using global API mode for database ${databaseId}: ${this.apiMode}`, { prefix: "Migration" });
237
+ return this.apiMode;
238
+ }
239
+
240
+ // Default to legacy if no adapter available
241
+ const defaultMode = 'legacy';
242
+ this.databaseApiModes.set(databaseId, defaultMode);
243
+ MessageFormatter.debug(`Defaulting to legacy mode for database ${databaseId}`, { prefix: "Migration" });
244
+ return defaultMode;
245
+ }
246
+
247
+ /**
248
+ * Get API mode context for schema generation
249
+ */
250
+ private getSchemaGeneratorApiContext(): any {
251
+ const databaseModes: Record<string, 'legacy' | 'tablesdb'> = {};
252
+
253
+ // Get API mode for each database
254
+ for (const db of this.updatedConfig.databases || []) {
255
+ const apiMode = this.databaseApiModes.get(db.$id) || this.apiMode || 'legacy';
256
+ databaseModes[db.$id] = apiMode;
257
+ }
258
+
259
+ return {
260
+ apiMode: this.apiMode || 'legacy',
261
+ databaseApiModes: databaseModes,
262
+ adapterMetadata: this.adapter?.getMetadata()
263
+ };
264
+ }
265
+
266
+ async appwriteSync(
267
+ config: AppwriteConfig,
268
+ databases?: Models.Database[],
269
+ databaseSelections?: DatabaseSelection[],
270
+ bucketSelections?: BucketSelection[]
271
+ ) {
272
+ // Initialize adapter for proper API mode detection and usage
273
+ await this.initializeAdapter();
274
+
98
275
  const db = getDatabaseFromConfig(config);
99
276
  if (!databases) {
100
277
  try {
@@ -110,6 +287,16 @@ export class AppwriteToX {
110
287
  throw new Error(`Database fetch failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
111
288
  }
112
289
  }
290
+
291
+ // Filter databases based on selection if provided
292
+ let databasesToProcess = databases;
293
+ if (databaseSelections && databaseSelections.length > 0) {
294
+ databasesToProcess = databases?.filter(db =>
295
+ databaseSelections.some(selection => selection.databaseId === db.$id)
296
+ ) || [];
297
+ MessageFormatter.info(`Filtered to ${databasesToProcess.length} selected databases`, { prefix: "Migration" });
298
+ }
299
+
113
300
  let updatedConfig: AppwriteConfig = { ...config };
114
301
 
115
302
  // Initialize databases array if it doesn't exist
@@ -118,11 +305,11 @@ export class AppwriteToX {
118
305
  }
119
306
 
120
307
  // Sync remote databases to local config - add missing ones
121
- MessageFormatter.info(`Syncing ${databases.length} remote databases with local config...`, { prefix: "Migration" });
308
+ MessageFormatter.info(`Syncing ${databasesToProcess.length} remote databases with local config...`, { prefix: "Migration" });
122
309
  let addedCount = 0;
123
310
  let updatedCount = 0;
124
311
 
125
- for (const remoteDb of databases) {
312
+ for (const remoteDb of databasesToProcess) {
126
313
  // Check if this database already exists in the config
127
314
  const existingDbIndex = updatedConfig.databases.findIndex(
128
315
  (localDb) => localDb.$id === remoteDb.$id
@@ -151,10 +338,23 @@ export class AppwriteToX {
151
338
  // Fetch all buckets
152
339
  const allBuckets = await listBuckets(this.storage);
153
340
 
341
+ // Filter buckets based on selection if provided
342
+ let matchedBuckets = allBuckets.buckets;
343
+ if (bucketSelections && bucketSelections.length > 0) {
344
+ matchedBuckets = allBuckets.buckets.filter(bucket =>
345
+ bucketSelections.some(selection => selection.bucketId === bucket.$id)
346
+ );
347
+ MessageFormatter.info(`Filtered to ${matchedBuckets.length} selected buckets`, { prefix: "Migration" });
348
+ }
349
+
154
350
  // Loop through each database
155
- for (const database of databases) {
156
- // Match bucket to database
157
- const matchedBucket = allBuckets.buckets.find((bucket) =>
351
+ for (const database of databasesToProcess) {
352
+ // Detect API mode for this specific database
353
+ const dbApiMode = await this.detectDatabaseApiMode(database.$id);
354
+ MessageFormatter.info(`Processing database '${database.name}' with API mode: ${dbApiMode}`, { prefix: "Migration" });
355
+
356
+ // Match bucket to database (from filtered buckets if selections provided)
357
+ const matchedBucket = matchedBuckets.find((bucket) =>
158
358
  bucket.$id.toLowerCase().includes(database.$id.toLowerCase())
159
359
  );
160
360
 
@@ -176,103 +376,183 @@ export class AppwriteToX {
176
376
  }
177
377
  }
178
378
 
179
- const collections = await fetchAllCollections(database.$id, db);
379
+ // Use adapter-aware collection/table fetching with proper API mode detection
380
+ const collections = await this.fetchCollectionsOrTables(database.$id, db);
381
+
382
+ // Filter collections based on table selection if provided
383
+ let collectionsToProcess = collections;
384
+ if (databaseSelections && databaseSelections.length > 0) {
385
+ const dbSelection = databaseSelections.find(selection => selection.databaseId === database.$id);
386
+ if (dbSelection && dbSelection.tableIds.length > 0) {
387
+ collectionsToProcess = collections.filter(collection =>
388
+ dbSelection.tableIds.includes(collection.$id)
389
+ );
390
+ MessageFormatter.info(`Filtered to ${collectionsToProcess.length} selected tables for database '${database.name}'`, { prefix: "Migration" });
391
+ }
392
+ }
180
393
 
181
394
  // Loop through each collection in the current database
182
395
  if (!updatedConfig.collections) {
183
396
  updatedConfig.collections = [];
184
397
  }
185
- for (const collection of collections) {
186
- MessageFormatter.processing(`Processing collection: ${collection.name}`, { prefix: "Migration" });
187
- const existingCollectionIndex = updatedConfig.collections.findIndex(
188
- (c) => c.name === collection.name
189
- );
190
- // Parse the collection permissions and attributes
191
- const collPermissions = this.parsePermissionsArray(
192
- collection.$permissions
193
- );
194
- const collAttributes = collection.attributes
195
- .map((attr: any) => {
196
- return parseAttribute(attr);
197
- })
198
- .filter((attribute: Attribute) =>
199
- attribute.type === "relationship"
200
- ? attribute.side !== "child"
201
- : true
398
+
399
+ MessageFormatter.info(`Processing ${collectionsToProcess.length} collections/tables in database '${database.name}'`, { prefix: "Migration" });
400
+ let processedCount = 0;
401
+ let errorCount = 0;
402
+
403
+ for (const collection of collectionsToProcess) {
404
+ try {
405
+ if (!collection) {
406
+ MessageFormatter.warning("Skipping null/undefined collection", { prefix: "Migration" });
407
+ errorCount++;
408
+ continue;
409
+ }
410
+
411
+ // Normalize collection/table structure to handle both TablesDB and Legacy formats
412
+ const normalizedCollection = normalizeCollectionOrTable(collection);
413
+
414
+ MessageFormatter.processing(`Processing ${normalizedCollection.name} (${normalizedCollection.$id})`, { prefix: "Migration" });
415
+ const existingCollectionIndex = updatedConfig.collections.findIndex(
416
+ (c) => c.name === normalizedCollection.name
417
+ );
418
+
419
+ // Parse the collection permissions and attributes using normalized structure
420
+ const collPermissions = this.parsePermissionsArray(
421
+ normalizedCollection.permissions
202
422
  );
203
- for (const attribute of collAttributes) {
204
- if (
205
- attribute.type === "relationship" &&
206
- attribute.relatedCollection
207
- ) {
208
- MessageFormatter.info(
209
- `Fetching related collection for ID: ${attribute.relatedCollection}`,
423
+
424
+ // Process attributes with proper error handling
425
+ let collAttributes: Attribute[] = [];
426
+ try {
427
+ collAttributes = normalizedCollection.attributes
428
+ .map((attr: any) => {
429
+ if (!attr) {
430
+ MessageFormatter.warning("Skipping null/undefined attribute", { prefix: "Migration" });
431
+ return null;
432
+ }
433
+ return parseAttribute(attr);
434
+ })
435
+ .filter((attribute: Attribute | null): attribute is Attribute =>
436
+ attribute !== null &&
437
+ (attribute.type !== "relationship" ? true : attribute.side !== "child")
438
+ );
439
+ } catch (error) {
440
+ MessageFormatter.error(
441
+ `Error processing attributes for ${normalizedCollection.name}`,
442
+ error instanceof Error ? error : new Error(String(error)),
210
443
  { prefix: "Migration" }
211
444
  );
212
- try {
213
- const relatedCollectionPulled = await db.getCollection(
214
- database.$id,
215
- attribute.relatedCollection
216
- );
217
- MessageFormatter.info(
218
- `Fetched Collection Name: ${relatedCollectionPulled.name}`,
219
- { prefix: "Migration" }
220
- );
221
- attribute.relatedCollection = relatedCollectionPulled.name;
445
+ // Continue with empty attributes array
446
+ collAttributes = [];
447
+ }
448
+
449
+ for (const attribute of collAttributes) {
450
+ if (
451
+ attribute.type === "relationship" &&
452
+ attribute.relatedCollection
453
+ ) {
222
454
  MessageFormatter.info(
223
- `Updated attribute.relatedCollection to: ${attribute.relatedCollection}`,
224
- { prefix: "Migration" }
225
- );
226
- } catch (error) {
227
- MessageFormatter.error(
228
- "Error fetching related collection",
229
- error instanceof Error ? error : new Error(String(error)),
455
+ `Fetching related collection for ID: ${attribute.relatedCollection}`,
230
456
  { prefix: "Migration" }
231
457
  );
458
+ try {
459
+ const relatedCollectionPulled = await this.getCollectionOrTable(
460
+ database.$id,
461
+ attribute.relatedCollection
462
+ );
463
+ MessageFormatter.info(
464
+ `Fetched Collection Name: ${relatedCollectionPulled.name}`,
465
+ { prefix: "Migration" }
466
+ );
467
+ attribute.relatedCollection = relatedCollectionPulled.name;
468
+ MessageFormatter.info(
469
+ `Updated attribute.relatedCollection to: ${attribute.relatedCollection}`,
470
+ { prefix: "Migration" }
471
+ );
472
+ } catch (error) {
473
+ MessageFormatter.error(
474
+ "Error fetching related collection",
475
+ error instanceof Error ? error : new Error(String(error)),
476
+ { prefix: "Migration" }
477
+ );
478
+ }
232
479
  }
233
480
  }
234
- }
235
- this.collToAttributeMap.set(collection.name, collAttributes);
236
- const finalIndexes = collection.indexes.map((index: Models.Index) => {
237
- return {
238
- ...index,
239
- orders: index.orders?.filter((order: string) => {
240
- return order !== null && order;
241
- }),
242
- };
243
- });
244
- const collIndexes = indexesSchema.parse(finalIndexes) ?? [];
245
-
246
- // Prepare the collection object to be added or updated
247
- const collToPush = CollectionSchema.parse({
248
- $id: collection.$id,
249
- name: collection.name,
250
- enabled: collection.enabled,
251
- documentSecurity: collection.documentSecurity,
252
- $createdAt: collection.$createdAt,
253
- $updatedAt: collection.$updatedAt,
254
- $permissions:
255
- collPermissions.length > 0 ? collPermissions : undefined,
256
- indexes: collIndexes.length > 0 ? collIndexes : undefined,
257
- attributes: collAttributes.length > 0 ? collAttributes : undefined,
258
- });
481
+ this.collToAttributeMap.set(normalizedCollection.name, collAttributes);
482
+
483
+ // Process indexes with proper error handling using normalized collection
484
+ let collIndexes: any[] = [];
485
+ try {
486
+ const finalIndexes = (normalizedCollection.indexes || collection.indexes || []).map((index: any) => {
487
+ if (!index) {
488
+ MessageFormatter.warning("Skipping null/undefined index", { prefix: "Migration" });
489
+ return null;
490
+ }
491
+ return {
492
+ ...index,
493
+ // Convert TablesDB 'columns' to expected 'attributes' for schema validation
494
+ attributes: index.attributes || index.columns || [],
495
+ orders: index.orders?.filter((order: string) => {
496
+ return order !== null && order;
497
+ }),
498
+ };
499
+ }).filter((index: any): index is any => index !== null);
500
+
501
+ collIndexes = indexesSchema.parse(finalIndexes) ?? [];
502
+ } catch (error) {
503
+ MessageFormatter.error(
504
+ `Error processing indexes for ${normalizedCollection.name}`,
505
+ error instanceof Error ? error : new Error(String(error)),
506
+ { prefix: "Migration" }
507
+ );
508
+ // Continue with empty indexes array
509
+ collIndexes = [];
510
+ }
511
+
512
+ // Prepare the collection object to be added or updated using normalized data
513
+ const collToPush = CollectionSchema.parse({
514
+ $id: normalizedCollection.$id,
515
+ name: normalizedCollection.name,
516
+ enabled: normalizedCollection.enabled,
517
+ documentSecurity: collection.documentSecurity, // Use original collection for this field
518
+ $createdAt: collection.$createdAt, // Use original collection for timestamps
519
+ $updatedAt: collection.$updatedAt,
520
+ $permissions:
521
+ collPermissions.length > 0 ? collPermissions : undefined,
522
+ indexes: collIndexes.length > 0 ? collIndexes : undefined,
523
+ attributes: collAttributes.length > 0 ? collAttributes : undefined,
524
+ });
259
525
 
260
- if (existingCollectionIndex !== -1) {
261
- // Update existing collection
262
- updatedConfig.collections[existingCollectionIndex] = collToPush;
263
- } else {
264
- // Add new collection
265
- updatedConfig.collections.push(collToPush);
526
+ if (existingCollectionIndex !== -1) {
527
+ // Update existing collection
528
+ updatedConfig.collections[existingCollectionIndex] = collToPush;
529
+ MessageFormatter.debug(`Updated existing collection: ${normalizedCollection.name}`, { prefix: "Migration" });
530
+ } else {
531
+ // Add new collection
532
+ updatedConfig.collections.push(collToPush);
533
+ MessageFormatter.debug(`Added new collection: ${normalizedCollection.name}`, { prefix: "Migration" });
534
+ }
535
+
536
+ processedCount++;
537
+ } catch (error) {
538
+ MessageFormatter.error(
539
+ `Error processing collection: ${collection?.name || 'unknown'}`,
540
+ error instanceof Error ? error : new Error(String(error)),
541
+ { prefix: "Migration" }
542
+ );
543
+ errorCount++;
266
544
  }
267
545
  }
268
546
 
269
547
  MessageFormatter.success(
270
- `Processed ${collections.length} collections in ${database.name}`,
548
+ `Database '${database.name}' processing complete: ${processedCount} collections processed, ${errorCount} errors`,
271
549
  { prefix: "Migration" }
272
550
  );
273
551
  }
274
552
  // Add unmatched buckets as global buckets
275
- const globalBuckets = allBuckets.buckets.filter(
553
+ // Use filtered buckets if selections provided, otherwise use all buckets
554
+ const sourceBuckets = bucketSelections && bucketSelections.length > 0 ? matchedBuckets : allBuckets.buckets;
555
+ const globalBuckets = sourceBuckets.filter(
276
556
  (bucket) =>
277
557
  !updatedConfig.databases.some(
278
558
  (db) => db.bucket && db.bucket.$id === bucket.$id
@@ -318,16 +598,31 @@ export class AppwriteToX {
318
598
  MessageFormatter.success(`Sync completed - ${updatedConfig.databases.length} databases, ${updatedConfig.collections?.length || 0} collections, ${updatedConfig.buckets?.length || 0} buckets, ${updatedConfig.functions?.length || 0} functions`, { prefix: "Migration" });
319
599
  }
320
600
 
321
- async toSchemas(databases?: Models.Database[]) {
601
+ async toSchemas(
602
+ databases?: Models.Database[],
603
+ databaseSelections?: DatabaseSelection[],
604
+ bucketSelections?: BucketSelection[]
605
+ ) {
322
606
  try {
323
607
  MessageFormatter.info("Starting sync-from-Appwrite process...", { prefix: "Migration" });
324
- await this.appwriteSync(this.config, databases);
608
+ await this.appwriteSync(this.config, databases, databaseSelections, bucketSelections);
325
609
 
326
610
  const generator = new SchemaGenerator(
327
611
  this.updatedConfig,
328
612
  this.appwriteFolderPath
329
613
  );
330
614
 
615
+ // Pass API mode context to the schema generator
616
+ const apiContext = this.getSchemaGeneratorApiContext();
617
+
618
+ // Extend the config with API mode information for schema generation
619
+ const configWithApiContext = {
620
+ ...this.updatedConfig,
621
+ apiMode: apiContext.apiMode,
622
+ databaseApiModes: apiContext.databaseApiModes,
623
+ adapterMetadata: apiContext.adapterMetadata
624
+ };
625
+
331
626
  // Check if this is a YAML-based project
332
627
  const yamlConfigPath = findYamlConfig(this.appwriteFolderPath);
333
628
  const isYamlProject = !!yamlConfigPath;
@@ -335,11 +630,11 @@ export class AppwriteToX {
335
630
  if (isYamlProject) {
336
631
  MessageFormatter.info("Detected YAML configuration - generating YAML collection definitions", { prefix: "Migration" });
337
632
  generator.updateYamlCollections();
338
- await generator.updateConfig(this.updatedConfig, true);
633
+ await generator.updateConfig(configWithApiContext, true);
339
634
  } else {
340
635
  MessageFormatter.info("Generating TypeScript collection definitions", { prefix: "Migration" });
341
636
  generator.updateTsSchemas();
342
- await generator.updateConfig(this.updatedConfig, false);
637
+ await generator.updateConfig(configWithApiContext, false);
343
638
  }
344
639
 
345
640
  MessageFormatter.info("Generating Zod schemas from synced collections...", { prefix: "Migration" });
@@ -524,6 +524,14 @@ export function generateTableSchema(): any {
524
524
  tableSchema.title = "Appwrite Table Definition";
525
525
  tableSchema.description = "YAML configuration for Appwrite table definitions (new TablesDB API)";
526
526
 
527
+ // Replace 'documentSecurity' with 'rowSecurity'
528
+ delete tableSchema.properties.documentSecurity;
529
+ tableSchema.properties.rowSecurity = {
530
+ "type": "boolean",
531
+ "description": "Enable row-level security",
532
+ "default": false
533
+ };
534
+
527
535
  // Replace 'attributes' with 'columns'
528
536
  delete tableSchema.properties.attributes;
529
537
  tableSchema.properties.columns = {
@@ -535,7 +543,8 @@ export function generateTableSchema(): any {
535
543
  "default": []
536
544
  };
537
545
 
538
- // Update index definition to support both attributes and columns
546
+ // Update index definition to use columns instead of attributes
547
+ delete tableSchema.$defs.index.properties.attributes;
539
548
  tableSchema.$defs.index.properties.columns = {
540
549
  "type": "array",
541
550
  "items": { "type": "string" },
@@ -543,20 +552,29 @@ export function generateTableSchema(): any {
543
552
  "minItems": 1
544
553
  };
545
554
 
546
- // Make index support either attributes or columns
547
- tableSchema.$defs.index.oneOf = [
548
- { "required": ["key", "type", "attributes"] },
549
- { "required": ["key", "type", "columns"] }
550
- ];
551
- delete tableSchema.$defs.index.required;
555
+ // Update index required fields to use columns
556
+ const requiredIndex = tableSchema.$defs.index.required;
557
+ if (requiredIndex && requiredIndex.includes("attributes")) {
558
+ const attributesIndex = requiredIndex.indexOf("attributes");
559
+ requiredIndex[attributesIndex] = "columns";
560
+ }
552
561
 
553
562
  // Add column definition (similar to attribute but with table terminology)
554
563
  tableSchema.$defs.column = JSON.parse(JSON.stringify(tableSchema.$defs.attribute));
564
+
565
+ // Add encrypted property (table-specific feature)
566
+ tableSchema.$defs.column.properties.encrypted = {
567
+ "type": "boolean",
568
+ "description": "Whether the column should be encrypted",
569
+ "default": false
570
+ };
571
+
572
+ // Replace relatedCollection with relatedTable for table terminology
573
+ delete tableSchema.$defs.column.properties.relatedCollection;
555
574
  tableSchema.$defs.column.properties.relatedTable = {
556
575
  "type": "string",
557
576
  "description": "Related table for relationship columns"
558
577
  };
559
- delete tableSchema.$defs.column.properties.relatedCollection;
560
578
 
561
579
  return tableSchema;
562
580
  }