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.
- package/SELECTION_DIALOGS.md +146 -0
- package/dist/cli/commands/databaseCommands.js +90 -23
- package/dist/main.js +175 -4
- package/dist/migrations/appwriteToX.d.ts +27 -2
- package/dist/migrations/appwriteToX.js +293 -69
- package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +1 -1
- package/dist/migrations/yaml/generateImportSchemas.js +23 -8
- package/dist/shared/schemaGenerator.js +25 -12
- package/dist/shared/selectionDialogs.d.ts +214 -0
- package/dist/shared/selectionDialogs.js +516 -0
- package/dist/utils/configDiscovery.d.ts +4 -4
- package/dist/utils/configDiscovery.js +66 -30
- package/dist/utils/yamlConverter.d.ts +1 -0
- package/dist/utils/yamlConverter.js +26 -3
- package/dist/utilsController.d.ts +6 -1
- package/dist/utilsController.js +91 -2
- package/package.json +1 -1
- package/src/cli/commands/databaseCommands.ts +134 -34
- package/src/main.ts +276 -34
- package/src/migrations/appwriteToX.ts +385 -90
- package/src/migrations/yaml/generateImportSchemas.ts +26 -8
- package/src/shared/schemaGenerator.ts +29 -12
- package/src/shared/selectionDialogs.ts +716 -0
- package/src/utils/configDiscovery.ts +83 -39
- package/src/utils/yamlConverter.ts +29 -3
- package/src/utilsController.ts +116 -4
|
@@ -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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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(
|
|
163
|
+
.get(normalizedCollection.name)
|
|
93
164
|
?.push(attributeParsed);
|
|
94
165
|
}
|
|
95
166
|
};
|
|
96
167
|
|
|
97
|
-
|
|
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 ${
|
|
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
|
|
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
|
|
156
|
-
//
|
|
157
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
collection
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
`
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
547
|
-
tableSchema.$defs.index.
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
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
|
}
|