appwrite-utils-cli 1.4.1 → 1.5.1
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 +42 -7
- package/dist/collections/indexes.js +13 -3
- package/dist/collections/methods.d.ts +9 -0
- package/dist/collections/methods.js +268 -0
- package/dist/databases/setup.js +6 -2
- package/dist/interactiveCLI.js +2 -1
- 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 +32 -5
- package/package.json +1 -1
- package/src/adapters/TablesDBAdapter.ts +20 -17
- package/src/collections/attributes.ts +198 -156
- package/src/collections/indexes.ts +36 -28
- package/src/collections/methods.ts +292 -19
- package/src/databases/setup.ts +11 -7
- package/src/interactiveCLI.ts +8 -7
- 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 +59 -32
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
|
*/
|
@@ -131,6 +131,15 @@ const attributesSame = (databaseAttribute, configAttribute) => {
|
|
131
131
|
(configValue === undefined || configValue === null)) {
|
132
132
|
return true;
|
133
133
|
}
|
134
|
+
// Normalize booleans: treat undefined and false as equivalent
|
135
|
+
if (typeof dbValue === "boolean" || typeof configValue === "boolean") {
|
136
|
+
return Boolean(dbValue) === Boolean(configValue);
|
137
|
+
}
|
138
|
+
// For numeric comparisons, compare numbers if both are numeric-like
|
139
|
+
if ((typeof dbValue === "number" || (typeof dbValue === "string" && dbValue !== "" && !isNaN(Number(dbValue)))) &&
|
140
|
+
(typeof configValue === "number" || (typeof configValue === "string" && configValue !== "" && !isNaN(Number(configValue))))) {
|
141
|
+
return Number(dbValue) === Number(configValue);
|
142
|
+
}
|
134
143
|
return dbValue === configValue;
|
135
144
|
}
|
136
145
|
// If neither has the attribute, consider it the same
|
@@ -140,10 +149,18 @@ const attributesSame = (databaseAttribute, configAttribute) => {
|
|
140
149
|
// If one has the attribute and the other doesn't, check if it's undefined or null
|
141
150
|
if (dbHasAttr && !configHasAttr) {
|
142
151
|
const dbValue = databaseAttribute[attr];
|
152
|
+
// Consider default-false booleans as equal to missing in config
|
153
|
+
if (typeof dbValue === "boolean") {
|
154
|
+
return dbValue === false; // missing in config equals false in db
|
155
|
+
}
|
143
156
|
return dbValue === undefined || dbValue === null;
|
144
157
|
}
|
145
158
|
if (!dbHasAttr && configHasAttr) {
|
146
159
|
const configValue = configAttribute[attr];
|
160
|
+
// Consider default-false booleans as equal to missing in db
|
161
|
+
if (typeof configValue === "boolean") {
|
162
|
+
return configValue === false; // missing in db equals false in config
|
163
|
+
}
|
147
164
|
return configValue === undefined || configValue === null;
|
148
165
|
}
|
149
166
|
// If we reach here, the attributes are different
|
@@ -157,7 +174,13 @@ export const createOrUpdateAttributeWithStatusCheck = async (db, dbId, collectio
|
|
157
174
|
console.log(chalk.blue(`Creating/updating attribute '${attribute.key}' (attempt ${retryCount + 1}/${maxRetries + 1})`));
|
158
175
|
try {
|
159
176
|
// First, try to create/update the attribute using existing logic
|
160
|
-
await createOrUpdateAttribute(db, dbId, collection, attribute);
|
177
|
+
const result = await createOrUpdateAttribute(db, dbId, collection, attribute);
|
178
|
+
// If the attribute was queued (relationship dependency unresolved),
|
179
|
+
// skip status polling and retry logic — the queue will handle it later.
|
180
|
+
if (result === "queued") {
|
181
|
+
console.log(chalk.yellow(`⏭️ Deferred relationship attribute '${attribute.key}' — queued for later once dependencies are available`));
|
182
|
+
return true;
|
183
|
+
}
|
161
184
|
// Now wait for the attribute to become available
|
162
185
|
const success = await waitForAttributeAvailable(db, dbId, collection.$id, attribute.key, 60000, // 1 minute timeout
|
163
186
|
retryCount, maxRetries);
|
@@ -229,7 +252,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
229
252
|
attributesSame(foundAttribute, attribute) &&
|
230
253
|
updateEnabled) {
|
231
254
|
// No need to do anything, they are the same
|
232
|
-
return;
|
255
|
+
return "processed";
|
233
256
|
}
|
234
257
|
else if (foundAttribute &&
|
235
258
|
!attributesSame(foundAttribute, attribute) &&
|
@@ -248,7 +271,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
248
271
|
!attributesSame(foundAttribute, attribute)) {
|
249
272
|
await db.deleteAttribute(dbId, collection.$id, attribute.key);
|
250
273
|
console.log(`Deleted attribute: ${attribute.key} to recreate it because they diff (update disabled temporarily)`);
|
251
|
-
return;
|
274
|
+
return "processed";
|
252
275
|
}
|
253
276
|
// console.log(`${action}-ing attribute: ${finalAttribute.key}`);
|
254
277
|
// Relationship attribute logic with adjustments
|
@@ -256,7 +279,18 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
256
279
|
let relatedCollectionId;
|
257
280
|
if (finalAttribute.type === "relationship" &&
|
258
281
|
finalAttribute.relatedCollection) {
|
259
|
-
|
282
|
+
// First try treating relatedCollection as an ID directly
|
283
|
+
try {
|
284
|
+
const byIdCollection = await db.getCollection(dbId, finalAttribute.relatedCollection);
|
285
|
+
collectionFoundViaRelatedCollection = byIdCollection;
|
286
|
+
relatedCollectionId = byIdCollection.$id;
|
287
|
+
// Cache by name for subsequent lookups
|
288
|
+
nameToIdMapping.set(byIdCollection.name, byIdCollection.$id);
|
289
|
+
}
|
290
|
+
catch (_) {
|
291
|
+
// Not an ID or not found — fall back to name-based resolution below
|
292
|
+
}
|
293
|
+
if (!collectionFoundViaRelatedCollection && nameToIdMapping.has(finalAttribute.relatedCollection)) {
|
260
294
|
relatedCollectionId = nameToIdMapping.get(finalAttribute.relatedCollection);
|
261
295
|
try {
|
262
296
|
collectionFoundViaRelatedCollection = await db.getCollection(dbId, relatedCollectionId);
|
@@ -268,7 +302,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
268
302
|
collectionFoundViaRelatedCollection = undefined;
|
269
303
|
}
|
270
304
|
}
|
271
|
-
else {
|
305
|
+
else if (!collectionFoundViaRelatedCollection) {
|
272
306
|
const collectionsPulled = await db.listCollections(dbId, [
|
273
307
|
Query.equal("name", finalAttribute.relatedCollection),
|
274
308
|
]);
|
@@ -288,7 +322,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
288
322
|
attribute,
|
289
323
|
dependencies: [finalAttribute.relatedCollection],
|
290
324
|
});
|
291
|
-
return;
|
325
|
+
return "queued";
|
292
326
|
}
|
293
327
|
}
|
294
328
|
finalAttribute = parseAttribute(finalAttribute);
|
@@ -338,7 +372,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
338
372
|
}
|
339
373
|
const minValue = finalAttribute.min !== undefined && finalAttribute.min !== null
|
340
374
|
? parseInt(finalAttribute.min)
|
341
|
-
: 9007199254740991;
|
375
|
+
: -9007199254740991;
|
342
376
|
const maxValue = finalAttribute.max !== undefined && finalAttribute.max !== null
|
343
377
|
? parseInt(finalAttribute.max)
|
344
378
|
: 9007199254740991;
|
@@ -445,6 +479,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
445
479
|
console.error("Invalid attribute type");
|
446
480
|
break;
|
447
481
|
}
|
482
|
+
return "processed";
|
448
483
|
};
|
449
484
|
/**
|
450
485
|
* 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) {
|
package/dist/databases/setup.js
CHANGED
@@ -72,7 +72,8 @@ export const ensureDatabasesExist = async (config, databasesToEnsure) => {
|
|
72
72
|
throw new Error("Appwrite client is not initialized in the config");
|
73
73
|
}
|
74
74
|
const database = new Databases(config.appwriteClient);
|
75
|
-
|
75
|
+
// Work on a shallow copy so we don't mutate caller-provided arrays
|
76
|
+
const databasesToCreate = [...(databasesToEnsure || config.databases || [])];
|
76
77
|
if (!databasesToCreate.length) {
|
77
78
|
console.log("No databases to create");
|
78
79
|
return;
|
@@ -81,7 +82,10 @@ export const ensureDatabasesExist = async (config, databasesToEnsure) => {
|
|
81
82
|
const migrationsDatabase = existingDatabases.databases.find((d) => d.name.toLowerCase().trim().replace(" ", "") === "migrations");
|
82
83
|
if (config.useMigrations && existingDatabases.databases.length !== 0 && migrationsDatabase) {
|
83
84
|
console.log("Creating all databases including migrations");
|
84
|
-
|
85
|
+
// Ensure migrations exists, but do not mutate the caller's array
|
86
|
+
if (!databasesToCreate.some((d) => d.$id === migrationsDatabase.$id)) {
|
87
|
+
databasesToCreate.push(migrationsDatabase);
|
88
|
+
}
|
85
89
|
}
|
86
90
|
for (const db of databasesToCreate) {
|
87
91
|
if (!existingDatabases.databases.some((d) => d.name === db.name)) {
|
package/dist/interactiveCLI.js
CHANGED
@@ -867,7 +867,8 @@ export class InteractiveCLI {
|
|
867
867
|
console.log(chalk.yellow("No databases selected. Skipping database sync."));
|
868
868
|
return;
|
869
869
|
}
|
870
|
-
const collections = await this.selectCollections(databases[0], this.controller.database, chalk.blue("Select local collections to push:"), true, true // prefer local
|
870
|
+
const collections = await this.selectCollections(databases[0], this.controller.database, chalk.blue("Select local collections to push:"), true, true, // prefer local
|
871
|
+
true // filter by selected database
|
871
872
|
);
|
872
873
|
const { syncFunctions } = await inquirer.prompt([
|
873
874
|
{
|
@@ -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;
|