appwrite-utils-cli 1.4.0 → 1.5.0
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/README.md +22 -1
- package/dist/adapters/TablesDBAdapter.js +7 -4
- package/dist/collections/attributes.d.ts +1 -1
- package/dist/collections/attributes.js +24 -6
- package/dist/collections/indexes.js +13 -3
- package/dist/collections/methods.d.ts +9 -0
- package/dist/collections/methods.js +268 -0
- package/dist/migrations/appwriteToX.d.ts +2 -2
- package/dist/migrations/comprehensiveTransfer.js +12 -0
- package/dist/migrations/dataLoader.d.ts +5 -5
- package/dist/migrations/relationships.d.ts +2 -2
- package/dist/shared/jsonSchemaGenerator.d.ts +1 -0
- package/dist/shared/jsonSchemaGenerator.js +6 -2
- package/dist/shared/operationQueue.js +14 -1
- package/dist/shared/schemaGenerator.d.ts +2 -1
- package/dist/shared/schemaGenerator.js +61 -78
- package/dist/storage/schemas.d.ts +8 -8
- package/dist/utils/loadConfigs.js +44 -19
- package/dist/utils/schemaStrings.d.ts +2 -1
- package/dist/utils/schemaStrings.js +61 -78
- package/dist/utils/setupFiles.js +19 -1
- package/dist/utils/versionDetection.d.ts +6 -0
- package/dist/utils/versionDetection.js +30 -0
- package/dist/utilsController.js +28 -4
- package/package.json +2 -2
- package/src/adapters/TablesDBAdapter.ts +20 -17
- package/src/collections/attributes.ts +122 -99
- package/src/collections/indexes.ts +36 -28
- package/src/collections/methods.ts +292 -19
- package/src/migrations/comprehensiveTransfer.ts +22 -8
- package/src/shared/jsonSchemaGenerator.ts +36 -29
- package/src/shared/operationQueue.ts +48 -33
- package/src/shared/schemaGenerator.ts +128 -134
- package/src/utils/loadConfigs.ts +48 -29
- package/src/utils/schemaStrings.ts +124 -130
- package/src/utils/setupFiles.ts +21 -5
- package/src/utils/versionDetection.ts +48 -21
- package/src/utilsController.ts +44 -24
package/README.md
CHANGED
@@ -325,7 +325,28 @@ Available specifications:
|
|
325
325
|
|
326
326
|
This updated CLI ensures that developers have robust tools at their fingertips to manage complex Appwrite projects effectively from the command line, with both interactive and non-interactive modes available for flexibility.
|
327
327
|
|
328
|
-
## Changelog
|
328
|
+
## Changelog
|
329
|
+
|
330
|
+
### 1.5.0 - Recursive Zod v4 Schemas + One‑Way Relationship Support
|
331
|
+
|
332
|
+
Highlights
|
333
|
+
- Recursive getters: Generated schemas now use Zod v4 recursive getters (no `z.lazy`, no `BaseSchema` layering). Cleaner types and fully inferred mutual recursion.
|
334
|
+
- One‑way relationships: Relationship attributes are now included even when `twoWay: false`. Related imports resolve collection IDs to names.
|
335
|
+
- Required semantics: Relationship getters respect `required` and `array`:
|
336
|
+
- required scalar → `RelatedSchema`
|
337
|
+
- optional scalar → `RelatedSchema.nullish()`
|
338
|
+
- required array → `RelatedSchema.array()`
|
339
|
+
- optional array → `RelatedSchema.array().nullish()`
|
340
|
+
- JSON Schemas: `$ref` definitions use resolved collection names when YAML provides IDs.
|
341
|
+
|
342
|
+
Validation changes
|
343
|
+
- Relationship schema: `twoWayKey` and `side` are now required only when `twoWay` is `true`.
|
344
|
+
- Helpful errors: Keeps strong validation but removes false negatives for one‑way relationships.
|
345
|
+
|
346
|
+
Developer notes
|
347
|
+
- Imports: Schema generators import only `...Schema` from related collections (no type imports needed).
|
348
|
+
- Example YAML: `Posts.yaml` demonstrates a required `manyToMany` (`categories`) and a one‑way `manyToOne` (`author`).
|
349
|
+
|
329
350
|
|
330
351
|
### 1.3.0 - Zod v4 Upgrade & Collection Management Fixes
|
331
352
|
|
@@ -168,8 +168,9 @@ export class TablesDBAdapter extends BaseAdapter {
|
|
168
168
|
// Attribute Operations
|
169
169
|
async createAttribute(params) {
|
170
170
|
try {
|
171
|
-
//
|
172
|
-
const
|
171
|
+
// Prefer createColumn if available, fallback to createAttribute
|
172
|
+
const fn = this.tablesDB.createColumn || this.tablesDB.createAttribute;
|
173
|
+
const result = await fn.call(this.tablesDB, params);
|
173
174
|
return { data: result };
|
174
175
|
}
|
175
176
|
catch (error) {
|
@@ -178,7 +179,8 @@ export class TablesDBAdapter extends BaseAdapter {
|
|
178
179
|
}
|
179
180
|
async updateAttribute(params) {
|
180
181
|
try {
|
181
|
-
const
|
182
|
+
const fn = this.tablesDB.updateColumn || this.tablesDB.updateAttribute;
|
183
|
+
const result = await fn.call(this.tablesDB, params);
|
182
184
|
return { data: result };
|
183
185
|
}
|
184
186
|
catch (error) {
|
@@ -187,7 +189,8 @@ export class TablesDBAdapter extends BaseAdapter {
|
|
187
189
|
}
|
188
190
|
async deleteAttribute(params) {
|
189
191
|
try {
|
190
|
-
const
|
192
|
+
const fn = this.tablesDB.deleteColumn || this.tablesDB.deleteAttribute;
|
193
|
+
const result = await fn.call(this.tablesDB, params);
|
191
194
|
return { data: result };
|
192
195
|
}
|
193
196
|
catch (error) {
|
@@ -4,7 +4,7 @@ import { type Attribute } from "appwrite-utils";
|
|
4
4
|
* Enhanced attribute creation with proper status monitoring and retry logic
|
5
5
|
*/
|
6
6
|
export declare const createOrUpdateAttributeWithStatusCheck: (db: Databases, dbId: string, collection: Models.Collection, attribute: Attribute, retryCount?: number, maxRetries?: number) => Promise<boolean>;
|
7
|
-
export declare const createOrUpdateAttribute: (db: Databases, dbId: string, collection: Models.Collection, attribute: Attribute) => Promise<
|
7
|
+
export declare const createOrUpdateAttribute: (db: Databases, dbId: string, collection: Models.Collection, attribute: Attribute) => Promise<"queued" | "processed">;
|
8
8
|
/**
|
9
9
|
* Enhanced collection attribute creation with proper status monitoring
|
10
10
|
*/
|
@@ -157,7 +157,13 @@ export const createOrUpdateAttributeWithStatusCheck = async (db, dbId, collectio
|
|
157
157
|
console.log(chalk.blue(`Creating/updating attribute '${attribute.key}' (attempt ${retryCount + 1}/${maxRetries + 1})`));
|
158
158
|
try {
|
159
159
|
// First, try to create/update the attribute using existing logic
|
160
|
-
await createOrUpdateAttribute(db, dbId, collection, attribute);
|
160
|
+
const result = await createOrUpdateAttribute(db, dbId, collection, attribute);
|
161
|
+
// If the attribute was queued (relationship dependency unresolved),
|
162
|
+
// skip status polling and retry logic — the queue will handle it later.
|
163
|
+
if (result === "queued") {
|
164
|
+
console.log(chalk.yellow(`⏭️ Deferred relationship attribute '${attribute.key}' — queued for later once dependencies are available`));
|
165
|
+
return true;
|
166
|
+
}
|
161
167
|
// Now wait for the attribute to become available
|
162
168
|
const success = await waitForAttributeAvailable(db, dbId, collection.$id, attribute.key, 60000, // 1 minute timeout
|
163
169
|
retryCount, maxRetries);
|
@@ -229,7 +235,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
229
235
|
attributesSame(foundAttribute, attribute) &&
|
230
236
|
updateEnabled) {
|
231
237
|
// No need to do anything, they are the same
|
232
|
-
return;
|
238
|
+
return "processed";
|
233
239
|
}
|
234
240
|
else if (foundAttribute &&
|
235
241
|
!attributesSame(foundAttribute, attribute) &&
|
@@ -248,7 +254,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
248
254
|
!attributesSame(foundAttribute, attribute)) {
|
249
255
|
await db.deleteAttribute(dbId, collection.$id, attribute.key);
|
250
256
|
console.log(`Deleted attribute: ${attribute.key} to recreate it because they diff (update disabled temporarily)`);
|
251
|
-
return;
|
257
|
+
return "processed";
|
252
258
|
}
|
253
259
|
// console.log(`${action}-ing attribute: ${finalAttribute.key}`);
|
254
260
|
// Relationship attribute logic with adjustments
|
@@ -256,7 +262,18 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
256
262
|
let relatedCollectionId;
|
257
263
|
if (finalAttribute.type === "relationship" &&
|
258
264
|
finalAttribute.relatedCollection) {
|
259
|
-
|
265
|
+
// First try treating relatedCollection as an ID directly
|
266
|
+
try {
|
267
|
+
const byIdCollection = await db.getCollection(dbId, finalAttribute.relatedCollection);
|
268
|
+
collectionFoundViaRelatedCollection = byIdCollection;
|
269
|
+
relatedCollectionId = byIdCollection.$id;
|
270
|
+
// Cache by name for subsequent lookups
|
271
|
+
nameToIdMapping.set(byIdCollection.name, byIdCollection.$id);
|
272
|
+
}
|
273
|
+
catch (_) {
|
274
|
+
// Not an ID or not found — fall back to name-based resolution below
|
275
|
+
}
|
276
|
+
if (!collectionFoundViaRelatedCollection && nameToIdMapping.has(finalAttribute.relatedCollection)) {
|
260
277
|
relatedCollectionId = nameToIdMapping.get(finalAttribute.relatedCollection);
|
261
278
|
try {
|
262
279
|
collectionFoundViaRelatedCollection = await db.getCollection(dbId, relatedCollectionId);
|
@@ -268,7 +285,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
268
285
|
collectionFoundViaRelatedCollection = undefined;
|
269
286
|
}
|
270
287
|
}
|
271
|
-
else {
|
288
|
+
else if (!collectionFoundViaRelatedCollection) {
|
272
289
|
const collectionsPulled = await db.listCollections(dbId, [
|
273
290
|
Query.equal("name", finalAttribute.relatedCollection),
|
274
291
|
]);
|
@@ -288,7 +305,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
288
305
|
attribute,
|
289
306
|
dependencies: [finalAttribute.relatedCollection],
|
290
307
|
});
|
291
|
-
return;
|
308
|
+
return "queued";
|
292
309
|
}
|
293
310
|
}
|
294
311
|
finalAttribute = parseAttribute(finalAttribute);
|
@@ -445,6 +462,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
445
462
|
console.error("Invalid attribute type");
|
446
463
|
break;
|
447
464
|
}
|
465
|
+
return "processed";
|
448
466
|
};
|
449
467
|
/**
|
450
468
|
* Enhanced collection attribute creation with proper status monitoring
|
@@ -20,13 +20,23 @@ retryCount = 0, maxRetries = 5) => {
|
|
20
20
|
}
|
21
21
|
while (Date.now() - startTime < maxWaitTime) {
|
22
22
|
try {
|
23
|
-
const indexList = await db
|
24
|
-
|
23
|
+
const indexList = await (db instanceof Databases
|
24
|
+
? db.listIndexes(dbId, collectionId)
|
25
|
+
: db.listIndexes({ databaseId: dbId, tableId: collectionId }));
|
26
|
+
const indexes = (db instanceof Databases)
|
27
|
+
? indexList.indexes
|
28
|
+
: (indexList.data || indexList.indexes || []);
|
29
|
+
const index = indexes.find((idx) => idx.key === indexKey);
|
25
30
|
if (!index) {
|
26
31
|
console.log(chalk.red(`Index '${indexKey}' not found`));
|
27
32
|
return false;
|
28
33
|
}
|
29
|
-
|
34
|
+
if (db instanceof Databases) {
|
35
|
+
console.log(chalk.gray(`Index '${indexKey}' status: ${index.status}`));
|
36
|
+
}
|
37
|
+
else {
|
38
|
+
console.log(chalk.gray(`Index '${indexKey}' detected (TablesDB)`));
|
39
|
+
}
|
30
40
|
switch (index.status) {
|
31
41
|
case 'available':
|
32
42
|
console.log(chalk.green(`✅ Index '${indexKey}' is now available`));
|
@@ -9,11 +9,20 @@ export declare const wipeDatabase: (database: Databases, databaseId: string) =>
|
|
9
9
|
collectionName: string;
|
10
10
|
}[]>;
|
11
11
|
export declare const wipeCollection: (database: Databases, databaseId: string, collectionId: string) => Promise<void>;
|
12
|
+
export declare const wipeAllTables: (adapter: DatabaseAdapter, databaseId: string) => Promise<{
|
13
|
+
tableId: string;
|
14
|
+
tableName: string;
|
15
|
+
}[]>;
|
16
|
+
export declare const wipeTableRows: (adapter: DatabaseAdapter, databaseId: string, tableId: string) => Promise<void>;
|
12
17
|
export declare const generateSchemas: (config: AppwriteConfig, appwriteFolderPath: string) => Promise<void>;
|
13
18
|
export declare const createOrUpdateCollections: (database: Databases, databaseId: string, config: AppwriteConfig, deletedCollections?: {
|
14
19
|
collectionId: string;
|
15
20
|
collectionName: string;
|
16
21
|
}[], selectedCollections?: Models.Collection[]) => Promise<void>;
|
22
|
+
export declare const createOrUpdateCollectionsViaAdapter: (adapter: DatabaseAdapter, databaseId: string, config: AppwriteConfig, deletedCollections?: {
|
23
|
+
collectionId: string;
|
24
|
+
collectionName: string;
|
25
|
+
}[], selectedCollections?: Models.Collection[]) => Promise<void>;
|
17
26
|
export declare const generateMockData: (database: Databases, databaseId: string, configCollections: any[]) => Promise<void>;
|
18
27
|
export declare const fetchAllCollections: (dbId: string, database: Databases) => Promise<Models.Collection[]>;
|
19
28
|
/**
|
@@ -198,11 +198,89 @@ export const wipeCollection = async (database, databaseId, collectionId) => {
|
|
198
198
|
const collection = collections.collections[0];
|
199
199
|
await wipeDocumentsFromCollection(database, databaseId, collection.$id);
|
200
200
|
};
|
201
|
+
// TablesDB helpers for wiping
|
202
|
+
export const wipeAllTables = async (adapter, databaseId) => {
|
203
|
+
MessageFormatter.info(`Wiping tables in database: ${databaseId}`, { prefix: 'Wipe' });
|
204
|
+
const res = await adapter.listTables({ databaseId, queries: [Query.limit(500)] });
|
205
|
+
const tables = res.tables || [];
|
206
|
+
const deleted = [];
|
207
|
+
const progress = ProgressManager.create(`wipe-db-${databaseId}`, tables.length, { title: 'Deleting tables' });
|
208
|
+
let processed = 0;
|
209
|
+
for (const t of tables) {
|
210
|
+
try {
|
211
|
+
await adapter.deleteTable({ databaseId, tableId: t.$id });
|
212
|
+
deleted.push({ tableId: t.$id, tableName: t.name });
|
213
|
+
}
|
214
|
+
catch (e) {
|
215
|
+
MessageFormatter.error(`Failed deleting table ${t.$id}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Wipe' });
|
216
|
+
}
|
217
|
+
processed++;
|
218
|
+
progress.update(processed);
|
219
|
+
await delay(100);
|
220
|
+
}
|
221
|
+
progress.stop();
|
222
|
+
return deleted;
|
223
|
+
};
|
224
|
+
export const wipeTableRows = async (adapter, databaseId, tableId) => {
|
225
|
+
try {
|
226
|
+
const initial = await adapter.listRows({ databaseId, tableId, queries: [Query.limit(1000)] });
|
227
|
+
let rows = initial.rows || [];
|
228
|
+
let total = rows.length;
|
229
|
+
let cursor = rows.length >= 1000 ? rows[rows.length - 1].$id : undefined;
|
230
|
+
while (cursor) {
|
231
|
+
const resp = await adapter.listRows({ databaseId, tableId, queries: [Query.limit(1000), ...(cursor ? [Query.cursorAfter(cursor)] : [])] });
|
232
|
+
const more = resp.rows || [];
|
233
|
+
rows.push(...more);
|
234
|
+
total = rows.length;
|
235
|
+
cursor = more.length >= 1000 ? more[more.length - 1].$id : undefined;
|
236
|
+
if (total % 10000 === 0) {
|
237
|
+
MessageFormatter.progress(`Found ${total} rows...`, { prefix: 'Wipe' });
|
238
|
+
}
|
239
|
+
}
|
240
|
+
MessageFormatter.info(`Found ${total} rows to delete`, { prefix: 'Wipe' });
|
241
|
+
if (total === 0)
|
242
|
+
return;
|
243
|
+
const progress = ProgressManager.create(`delete-${tableId}`, total, { title: 'Deleting rows' });
|
244
|
+
let processed = 0;
|
245
|
+
const maxStackSize = 50;
|
246
|
+
const batches = chunk(rows, maxStackSize);
|
247
|
+
for (const batch of batches) {
|
248
|
+
await Promise.all(batch.map(async (row) => {
|
249
|
+
try {
|
250
|
+
await adapter.deleteRow({ databaseId, tableId, id: row.$id });
|
251
|
+
}
|
252
|
+
catch (e) {
|
253
|
+
// ignore missing rows
|
254
|
+
}
|
255
|
+
processed++;
|
256
|
+
progress.update(processed);
|
257
|
+
}));
|
258
|
+
await delay(50);
|
259
|
+
}
|
260
|
+
progress.stop();
|
261
|
+
MessageFormatter.success(`Completed deletion of ${total} rows from table ${tableId}`, { prefix: 'Wipe' });
|
262
|
+
}
|
263
|
+
catch (error) {
|
264
|
+
MessageFormatter.error(`Error wiping rows from table ${tableId}`, error instanceof Error ? error : new Error(String(error)), { prefix: 'Wipe' });
|
265
|
+
throw error;
|
266
|
+
}
|
267
|
+
};
|
201
268
|
export const generateSchemas = async (config, appwriteFolderPath) => {
|
202
269
|
const schemaGenerator = new SchemaGenerator(config, appwriteFolderPath);
|
203
270
|
schemaGenerator.generateSchemas();
|
204
271
|
};
|
205
272
|
export const createOrUpdateCollections = async (database, databaseId, config, deletedCollections, selectedCollections = []) => {
|
273
|
+
// If API mode is tablesdb, route to adapter-based implementation
|
274
|
+
try {
|
275
|
+
const { adapter, apiMode } = await getAdapterFromConfig(config);
|
276
|
+
if (apiMode === 'tablesdb') {
|
277
|
+
await createOrUpdateCollectionsViaAdapter(adapter, databaseId, config, deletedCollections, selectedCollections);
|
278
|
+
return;
|
279
|
+
}
|
280
|
+
}
|
281
|
+
catch {
|
282
|
+
// Fallback to legacy path below
|
283
|
+
}
|
206
284
|
const collectionsToProcess = selectedCollections.length > 0 ? selectedCollections : config.collections;
|
207
285
|
if (!collectionsToProcess) {
|
208
286
|
return;
|
@@ -304,6 +382,196 @@ export const createOrUpdateCollections = async (database, databaseId, config, de
|
|
304
382
|
MessageFormatter.info("No queued operations to process", { prefix: "Collections" });
|
305
383
|
}
|
306
384
|
};
|
385
|
+
// New: Adapter-based implementation for TablesDB
|
386
|
+
export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, config, deletedCollections, selectedCollections = []) => {
|
387
|
+
const collectionsToProcess = selectedCollections.length > 0 ? selectedCollections : (config.collections || []);
|
388
|
+
if (!collectionsToProcess || collectionsToProcess.length === 0)
|
389
|
+
return;
|
390
|
+
const usedIds = new Set();
|
391
|
+
// Helper: create attributes through adapter
|
392
|
+
const createAttr = async (tableId, attr) => {
|
393
|
+
const base = {
|
394
|
+
databaseId,
|
395
|
+
tableId,
|
396
|
+
key: attr.key,
|
397
|
+
type: attr.type,
|
398
|
+
size: attr.size,
|
399
|
+
required: !!attr.required,
|
400
|
+
default: attr.xdefault,
|
401
|
+
array: !!attr.array,
|
402
|
+
min: attr.min,
|
403
|
+
max: attr.max,
|
404
|
+
elements: attr.elements,
|
405
|
+
encrypt: attr.encrypted,
|
406
|
+
relatedCollection: attr.relatedCollection,
|
407
|
+
relationType: attr.relationType,
|
408
|
+
twoWay: attr.twoWay,
|
409
|
+
twoWayKey: attr.twoWayKey,
|
410
|
+
onDelete: attr.onDelete,
|
411
|
+
side: attr.side,
|
412
|
+
};
|
413
|
+
await adapter.createAttribute(base);
|
414
|
+
await delay(150);
|
415
|
+
};
|
416
|
+
// Local queue for unresolved relationships
|
417
|
+
const relQueue = [];
|
418
|
+
for (const collection of collectionsToProcess) {
|
419
|
+
const { attributes, indexes, ...collectionData } = collection;
|
420
|
+
// Prepare permissions as strings (reuse Permission helper)
|
421
|
+
const permissions = [];
|
422
|
+
if (collection.$permissions && collection.$permissions.length > 0) {
|
423
|
+
for (const p of collection.$permissions) {
|
424
|
+
if (typeof p === 'string')
|
425
|
+
permissions.push(p);
|
426
|
+
else {
|
427
|
+
switch (p.permission) {
|
428
|
+
case 'read':
|
429
|
+
permissions.push(Permission.read(p.target));
|
430
|
+
break;
|
431
|
+
case 'create':
|
432
|
+
permissions.push(Permission.create(p.target));
|
433
|
+
break;
|
434
|
+
case 'update':
|
435
|
+
permissions.push(Permission.update(p.target));
|
436
|
+
break;
|
437
|
+
case 'delete':
|
438
|
+
permissions.push(Permission.delete(p.target));
|
439
|
+
break;
|
440
|
+
case 'write':
|
441
|
+
permissions.push(Permission.write(p.target));
|
442
|
+
break;
|
443
|
+
default: break;
|
444
|
+
}
|
445
|
+
}
|
446
|
+
}
|
447
|
+
}
|
448
|
+
// Find existing table by name
|
449
|
+
const list = await adapter.listTables({ databaseId, queries: [Query.equal('name', collectionData.name)] });
|
450
|
+
const items = list.tables || [];
|
451
|
+
let table = items[0];
|
452
|
+
let tableId;
|
453
|
+
if (!table) {
|
454
|
+
// Determine ID (prefer provided $id or re-use deleted one)
|
455
|
+
let foundColl = deletedCollections?.find((coll) => coll.collectionName.toLowerCase().trim().replace(" ", "") === collectionData.name.toLowerCase().trim().replace(" ", ""));
|
456
|
+
if (collectionData.$id)
|
457
|
+
tableId = collectionData.$id;
|
458
|
+
else if (foundColl && !usedIds.has(foundColl.collectionId))
|
459
|
+
tableId = foundColl.collectionId;
|
460
|
+
else
|
461
|
+
tableId = ID.unique();
|
462
|
+
usedIds.add(tableId);
|
463
|
+
const res = await adapter.createTable({
|
464
|
+
databaseId,
|
465
|
+
id: tableId,
|
466
|
+
name: collectionData.name,
|
467
|
+
permissions,
|
468
|
+
documentSecurity: !!collectionData.documentSecurity,
|
469
|
+
enabled: collectionData.enabled !== false
|
470
|
+
});
|
471
|
+
table = res.data || res;
|
472
|
+
nameToIdMapping.set(collectionData.name, tableId);
|
473
|
+
}
|
474
|
+
else {
|
475
|
+
tableId = table.$id;
|
476
|
+
await adapter.updateTable({
|
477
|
+
databaseId,
|
478
|
+
id: tableId,
|
479
|
+
name: collectionData.name,
|
480
|
+
permissions,
|
481
|
+
documentSecurity: !!collectionData.documentSecurity,
|
482
|
+
enabled: collectionData.enabled !== false
|
483
|
+
});
|
484
|
+
}
|
485
|
+
// Add small delay after table create/update
|
486
|
+
await delay(250);
|
487
|
+
// Create attributes: non-relationship first
|
488
|
+
const nonRel = (attributes || []).filter((a) => a.type !== 'relationship');
|
489
|
+
for (const attr of nonRel) {
|
490
|
+
await createAttr(tableId, attr);
|
491
|
+
}
|
492
|
+
// Relationship attributes — resolve relatedCollection to ID
|
493
|
+
const rels = (attributes || []).filter((a) => a.type === 'relationship');
|
494
|
+
for (const attr of rels) {
|
495
|
+
const relNameOrId = attr.relatedCollection;
|
496
|
+
if (!relNameOrId)
|
497
|
+
continue;
|
498
|
+
let relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
|
499
|
+
// If looks like a name (not ULID) and not in cache, try query by name
|
500
|
+
if (!nameToIdMapping.has(relNameOrId)) {
|
501
|
+
try {
|
502
|
+
const relList = await adapter.listTables({ databaseId, queries: [Query.equal('name', relNameOrId)] });
|
503
|
+
const relItems = relList.tables || [];
|
504
|
+
if (relItems[0]?.$id) {
|
505
|
+
relId = relItems[0].$id;
|
506
|
+
nameToIdMapping.set(relNameOrId, relId);
|
507
|
+
}
|
508
|
+
}
|
509
|
+
catch { }
|
510
|
+
}
|
511
|
+
if (relId && typeof relId === 'string') {
|
512
|
+
attr.relatedCollection = relId;
|
513
|
+
await createAttr(tableId, attr);
|
514
|
+
}
|
515
|
+
else {
|
516
|
+
// Defer if unresolved
|
517
|
+
relQueue.push({ tableId, attr: attr });
|
518
|
+
}
|
519
|
+
}
|
520
|
+
// Indexes
|
521
|
+
const idxs = (indexes || []);
|
522
|
+
for (const idx of idxs) {
|
523
|
+
try {
|
524
|
+
await adapter.createIndex({
|
525
|
+
databaseId,
|
526
|
+
tableId,
|
527
|
+
key: idx.key,
|
528
|
+
type: idx.type,
|
529
|
+
attributes: idx.attributes,
|
530
|
+
orders: idx.orders || []
|
531
|
+
});
|
532
|
+
await delay(150);
|
533
|
+
}
|
534
|
+
catch (e) {
|
535
|
+
MessageFormatter.error(`Failed to create index ${idx.key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
|
536
|
+
}
|
537
|
+
}
|
538
|
+
}
|
539
|
+
// Process queued relationships once mapping likely populated
|
540
|
+
for (const { tableId, attr } of relQueue) {
|
541
|
+
const relNameOrId = attr.relatedCollection;
|
542
|
+
if (!relNameOrId)
|
543
|
+
continue;
|
544
|
+
const relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
|
545
|
+
if (relId) {
|
546
|
+
attr.relatedCollection = relId;
|
547
|
+
try {
|
548
|
+
await adapter.createAttribute({
|
549
|
+
databaseId,
|
550
|
+
tableId,
|
551
|
+
key: attr.key,
|
552
|
+
type: attr.type,
|
553
|
+
size: attr.size,
|
554
|
+
required: !!attr.required,
|
555
|
+
default: attr.xdefault,
|
556
|
+
array: !!attr.array,
|
557
|
+
min: attr.min,
|
558
|
+
max: attr.max,
|
559
|
+
elements: attr.elements,
|
560
|
+
relatedCollection: relId,
|
561
|
+
relationType: attr.relationType,
|
562
|
+
twoWay: attr.twoWay,
|
563
|
+
twoWayKey: attr.twoWayKey,
|
564
|
+
onDelete: attr.onDelete,
|
565
|
+
side: attr.side
|
566
|
+
});
|
567
|
+
await delay(150);
|
568
|
+
}
|
569
|
+
catch (e) {
|
570
|
+
MessageFormatter.error(`Failed queued relationship ${attr.key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Attributes' });
|
571
|
+
}
|
572
|
+
}
|
573
|
+
}
|
574
|
+
};
|
307
575
|
export const generateMockData = async (database, databaseId, configCollections) => {
|
308
576
|
for (const { collection, mockFunction } of configCollections) {
|
309
577
|
if (mockFunction) {
|
@@ -90,12 +90,12 @@ export declare class AppwriteToX {
|
|
90
90
|
relatedCollection: string;
|
91
91
|
relationType: "oneToMany" | "manyToOne" | "oneToOne" | "manyToMany";
|
92
92
|
twoWay: boolean;
|
93
|
-
twoWayKey: string;
|
94
93
|
onDelete: "setNull" | "cascade" | "restrict";
|
95
|
-
side: "parent" | "child";
|
96
94
|
error?: string | undefined;
|
97
95
|
required?: boolean | undefined;
|
98
96
|
array?: boolean | undefined;
|
97
|
+
twoWayKey?: string | undefined;
|
98
|
+
side?: "parent" | "child" | undefined;
|
99
99
|
importMapping?: {
|
100
100
|
originalIdField: string;
|
101
101
|
targetField?: string | undefined;
|
@@ -2,6 +2,7 @@ import { converterFunctions, tryAwaitWithRetry, parseAttribute, objectNeedsUpdat
|
|
2
2
|
import { Client, Databases, Storage, Users, Functions, Teams, Query, AppwriteException, } from "node-appwrite";
|
3
3
|
import { InputFile } from "node-appwrite/file";
|
4
4
|
import { MessageFormatter } from "../shared/messageFormatter.js";
|
5
|
+
import { processQueue, queuedOperations } from "../shared/operationQueue.js";
|
5
6
|
import { ProgressManager } from "../shared/progressManager.js";
|
6
7
|
import { getClient } from "../utils/getClientFromConfig.js";
|
7
8
|
import { transferDatabaseLocalToLocal, transferDatabaseLocalToRemote, transferStorageLocalToLocal, transferStorageLocalToRemote, transferUsersLocalToRemote, } from "./transfer.js";
|
@@ -343,6 +344,17 @@ export class ComprehensiveTransfer {
|
|
343
344
|
MessageFormatter.error(`Error processing collection ${collection.name}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
344
345
|
}
|
345
346
|
}
|
347
|
+
// After processing all collections' attributes and indexes, process any queued
|
348
|
+
// relationship attributes so dependencies are resolved within this phase.
|
349
|
+
if (queuedOperations.length > 0) {
|
350
|
+
MessageFormatter.info(`Processing ${queuedOperations.length} queued relationship operations`, { prefix: "Transfer" });
|
351
|
+
await processQueue(this.targetDatabases, dbId);
|
352
|
+
}
|
353
|
+
else {
|
354
|
+
MessageFormatter.info("No queued relationship operations to process", {
|
355
|
+
prefix: "Transfer",
|
356
|
+
});
|
357
|
+
}
|
346
358
|
}
|
347
359
|
catch (error) {
|
348
360
|
MessageFormatter.error(`Failed to create database structure for ${dbId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
|
@@ -100,16 +100,16 @@ export declare const CollectionImportDataSchema: z.ZodObject<{
|
|
100
100
|
manyToMany: "manyToMany";
|
101
101
|
}>;
|
102
102
|
twoWay: z.ZodBoolean;
|
103
|
-
twoWayKey: z.ZodString
|
103
|
+
twoWayKey: z.ZodOptional<z.ZodString>;
|
104
104
|
onDelete: z.ZodDefault<z.ZodEnum<{
|
105
105
|
setNull: "setNull";
|
106
106
|
cascade: "cascade";
|
107
107
|
restrict: "restrict";
|
108
108
|
}>>;
|
109
|
-
side: z.ZodEnum<{
|
109
|
+
side: z.ZodOptional<z.ZodEnum<{
|
110
110
|
parent: "parent";
|
111
111
|
child: "child";
|
112
|
-
}
|
112
|
+
}>>;
|
113
113
|
importMapping: z.ZodOptional<z.ZodObject<{
|
114
114
|
originalIdField: z.ZodString;
|
115
115
|
targetField: z.ZodOptional<z.ZodString>;
|
@@ -374,12 +374,12 @@ export declare class DataLoader {
|
|
374
374
|
relatedCollection: string;
|
375
375
|
relationType: "oneToMany" | "manyToOne" | "oneToOne" | "manyToMany";
|
376
376
|
twoWay: boolean;
|
377
|
-
twoWayKey: string;
|
378
377
|
onDelete: "setNull" | "cascade" | "restrict";
|
379
|
-
side: "parent" | "child";
|
380
378
|
error?: string | undefined;
|
381
379
|
required?: boolean | undefined;
|
382
380
|
array?: boolean | undefined;
|
381
|
+
twoWayKey?: string | undefined;
|
382
|
+
side?: "parent" | "child" | undefined;
|
383
383
|
importMapping?: {
|
384
384
|
originalIdField: string;
|
385
385
|
targetField?: string | undefined;
|
@@ -9,12 +9,12 @@ export declare const findCollectionsWithRelationships: (config: AppwriteConfig)
|
|
9
9
|
relatedCollection: string;
|
10
10
|
relationType: "oneToMany" | "manyToOne" | "oneToOne" | "manyToMany";
|
11
11
|
twoWay: boolean;
|
12
|
-
twoWayKey: string;
|
13
12
|
onDelete: "setNull" | "cascade" | "restrict";
|
14
|
-
side: "parent" | "child";
|
15
13
|
error?: string | undefined;
|
16
14
|
required?: boolean | undefined;
|
17
15
|
array?: boolean | undefined;
|
16
|
+
twoWayKey?: string | undefined;
|
17
|
+
side?: "parent" | "child" | undefined;
|
18
18
|
importMapping?: {
|
19
19
|
originalIdField: string;
|
20
20
|
targetField?: string | undefined;
|
@@ -31,6 +31,7 @@ export declare class JsonSchemaGenerator {
|
|
31
31
|
private appwriteFolderPath;
|
32
32
|
private relationshipMap;
|
33
33
|
constructor(config: AppwriteConfig, appwriteFolderPath: string);
|
34
|
+
private resolveCollectionName;
|
34
35
|
private extractRelationships;
|
35
36
|
private attributeToJsonSchemaProperty;
|
36
37
|
private getBaseTypeSchema;
|
@@ -11,6 +11,10 @@ export class JsonSchemaGenerator {
|
|
11
11
|
this.appwriteFolderPath = appwriteFolderPath;
|
12
12
|
this.extractRelationships();
|
13
13
|
}
|
14
|
+
resolveCollectionName = (idOrName) => {
|
15
|
+
const col = this.config.collections?.find((c) => c.$id === idOrName || c.name === idOrName);
|
16
|
+
return col?.name ?? idOrName;
|
17
|
+
};
|
14
18
|
extractRelationships() {
|
15
19
|
if (!this.config.collections)
|
16
20
|
return;
|
@@ -22,7 +26,7 @@ export class JsonSchemaGenerator {
|
|
22
26
|
const relationships = this.relationshipMap.get(collection.name) || [];
|
23
27
|
relationships.push({
|
24
28
|
attributeKey: attr.key,
|
25
|
-
relatedCollection: attr.relatedCollection,
|
29
|
+
relatedCollection: this.resolveCollectionName(attr.relatedCollection),
|
26
30
|
relationType: attr.relationType,
|
27
31
|
isArray: attr.relationType === "oneToMany" || attr.relationType === "manyToMany"
|
28
32
|
});
|
@@ -108,7 +112,7 @@ export class JsonSchemaGenerator {
|
|
108
112
|
case "relationship":
|
109
113
|
if (attribute.relatedCollection) {
|
110
114
|
// For relationships, reference the related collection schema
|
111
|
-
schema.$ref = `#/definitions/${toPascalCase(attribute.relatedCollection)}`;
|
115
|
+
schema.$ref = `#/definitions/${toPascalCase(this.resolveCollectionName(attribute.relatedCollection))}`;
|
112
116
|
}
|
113
117
|
else {
|
114
118
|
schema.type = "string";
|
@@ -33,7 +33,20 @@ export const processQueue = async (db, dbId) => {
|
|
33
33
|
}
|
34
34
|
// Attempt to resolve related collection if specified and not already found
|
35
35
|
if (!collectionFound && operation.attribute?.relatedCollection) {
|
36
|
-
|
36
|
+
// First, try treating relatedCollection as an ID
|
37
|
+
try {
|
38
|
+
const relAttr = operation.attribute;
|
39
|
+
const byId = await tryAwaitWithRetry(async () => await db.getCollection(dbId, relAttr.relatedCollection));
|
40
|
+
// We still need the target collection (operation.collectionId) to create the attribute on,
|
41
|
+
// so only use this branch to warm caches/mappings and continue to dependency checks.
|
42
|
+
// Do not override collectionFound with the related collection.
|
43
|
+
}
|
44
|
+
catch (_) {
|
45
|
+
// Not an ID or not found; fall back to name-based cache
|
46
|
+
}
|
47
|
+
// Warm cache by name (used by attribute creation path), but do not use as target collection
|
48
|
+
const relAttr = operation.attribute;
|
49
|
+
await fetchAndCacheCollectionByName(db, dbId, relAttr.relatedCollection);
|
37
50
|
}
|
38
51
|
// Handle dependencies if collection still not found
|
39
52
|
if (!collectionFound) {
|
@@ -4,6 +4,7 @@ export declare class SchemaGenerator {
|
|
4
4
|
private config;
|
5
5
|
private appwriteFolderPath;
|
6
6
|
constructor(config: AppwriteConfig, appwriteFolderPath: string);
|
7
|
+
private resolveCollectionName;
|
7
8
|
updateYamlCollections(): void;
|
8
9
|
updateTsSchemas(): void;
|
9
10
|
updateConfig(config: AppwriteConfig, isYamlConfig?: boolean): Promise<void>;
|
@@ -15,6 +16,6 @@ export declare class SchemaGenerator {
|
|
15
16
|
format?: "zod" | "json" | "both";
|
16
17
|
verbose?: boolean;
|
17
18
|
}): void;
|
18
|
-
|
19
|
+
createSchemaStringV4: (name: string, attributes: Attribute[]) => string;
|
19
20
|
typeToZod: (attribute: Attribute) => string;
|
20
21
|
}
|