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.
Files changed (42) hide show
  1. package/README.md +22 -1
  2. package/dist/adapters/TablesDBAdapter.js +7 -4
  3. package/dist/collections/attributes.d.ts +1 -1
  4. package/dist/collections/attributes.js +42 -7
  5. package/dist/collections/indexes.js +13 -3
  6. package/dist/collections/methods.d.ts +9 -0
  7. package/dist/collections/methods.js +268 -0
  8. package/dist/databases/setup.js +6 -2
  9. package/dist/interactiveCLI.js +2 -1
  10. package/dist/migrations/appwriteToX.d.ts +2 -2
  11. package/dist/migrations/comprehensiveTransfer.js +12 -0
  12. package/dist/migrations/dataLoader.d.ts +5 -5
  13. package/dist/migrations/relationships.d.ts +2 -2
  14. package/dist/shared/jsonSchemaGenerator.d.ts +1 -0
  15. package/dist/shared/jsonSchemaGenerator.js +6 -2
  16. package/dist/shared/operationQueue.js +14 -1
  17. package/dist/shared/schemaGenerator.d.ts +2 -1
  18. package/dist/shared/schemaGenerator.js +61 -78
  19. package/dist/storage/schemas.d.ts +8 -8
  20. package/dist/utils/loadConfigs.js +44 -19
  21. package/dist/utils/schemaStrings.d.ts +2 -1
  22. package/dist/utils/schemaStrings.js +61 -78
  23. package/dist/utils/setupFiles.js +19 -1
  24. package/dist/utils/versionDetection.d.ts +6 -0
  25. package/dist/utils/versionDetection.js +30 -0
  26. package/dist/utilsController.js +32 -5
  27. package/package.json +1 -1
  28. package/src/adapters/TablesDBAdapter.ts +20 -17
  29. package/src/collections/attributes.ts +198 -156
  30. package/src/collections/indexes.ts +36 -28
  31. package/src/collections/methods.ts +292 -19
  32. package/src/databases/setup.ts +11 -7
  33. package/src/interactiveCLI.ts +8 -7
  34. package/src/migrations/comprehensiveTransfer.ts +22 -8
  35. package/src/shared/jsonSchemaGenerator.ts +36 -29
  36. package/src/shared/operationQueue.ts +48 -33
  37. package/src/shared/schemaGenerator.ts +128 -134
  38. package/src/utils/loadConfigs.ts +48 -29
  39. package/src/utils/schemaStrings.ts +124 -130
  40. package/src/utils/setupFiles.ts +21 -5
  41. package/src/utils/versionDetection.ts +48 -21
  42. package/src/utilsController.ts +59 -32
@@ -22,16 +22,23 @@ interface RelationshipDetail {
22
22
  isChild: boolean;
23
23
  }
24
24
 
25
- export class SchemaGenerator {
26
- private relationshipMap = new Map<string, RelationshipDetail[]>();
27
- private config: AppwriteConfig;
28
- private appwriteFolderPath: string;
29
-
30
- constructor(config: AppwriteConfig, appwriteFolderPath: string) {
31
- this.config = config;
32
- this.appwriteFolderPath = appwriteFolderPath;
33
- this.extractRelationships();
34
- }
25
+ export class SchemaGenerator {
26
+ private relationshipMap = new Map<string, RelationshipDetail[]>();
27
+ private config: AppwriteConfig;
28
+ private appwriteFolderPath: string;
29
+
30
+ constructor(config: AppwriteConfig, appwriteFolderPath: string) {
31
+ this.config = config;
32
+ this.appwriteFolderPath = appwriteFolderPath;
33
+ this.extractRelationships();
34
+ }
35
+
36
+ private resolveCollectionName = (idOrName: string): string => {
37
+ const col = this.config.collections?.find(
38
+ (c) => c.$id === (idOrName as any) || c.name === idOrName
39
+ );
40
+ return col?.name ?? idOrName;
41
+ };
35
42
 
36
43
  public updateTsSchemas(): void {
37
44
  const collections = this.config.collections;
@@ -182,12 +189,12 @@ export class SchemaGenerator {
182
189
  if (!collection.attributes) {
183
190
  return;
184
191
  }
185
- collection.attributes.forEach((attr) => {
186
- if (attr.type === "relationship" && attr.twoWay && attr.twoWayKey) {
187
- const relationshipAttr = attr as RelationshipAttribute;
188
- let isArrayParent = false;
189
- let isArrayChild = false;
190
- switch (relationshipAttr.relationType) {
192
+ collection.attributes.forEach((attr) => {
193
+ if (attr.type === "relationship" && attr.twoWay && attr.twoWayKey) {
194
+ const relationshipAttr = attr as RelationshipAttribute;
195
+ let isArrayParent = false;
196
+ let isArrayChild = false;
197
+ switch (relationshipAttr.relationType) {
191
198
  case "oneToMany":
192
199
  isArrayParent = true;
193
200
  isArrayChild = false;
@@ -207,14 +214,14 @@ export class SchemaGenerator {
207
214
  default:
208
215
  break;
209
216
  }
210
- this.addRelationship(
211
- collection.name,
212
- relationshipAttr.relatedCollection,
213
- attr.key,
214
- relationshipAttr.twoWayKey,
215
- isArrayParent,
216
- isArrayChild
217
- );
217
+ this.addRelationship(
218
+ collection.name,
219
+ this.resolveCollectionName(relationshipAttr.relatedCollection),
220
+ attr.key,
221
+ relationshipAttr.twoWayKey!,
222
+ isArrayParent,
223
+ isArrayChild
224
+ );
218
225
  console.log(
219
226
  `Extracted relationship: ${attr.key}\n\t${collection.name} -> ${relationshipAttr.relatedCollection}, databaseId: ${collection.databaseId}`
220
227
  );
@@ -259,10 +266,10 @@ export class SchemaGenerator {
259
266
  return;
260
267
  }
261
268
  this.config.collections.forEach((collection) => {
262
- const schemaString = this.createSchemaString(
263
- collection.name,
264
- collection.attributes
265
- );
269
+ const schemaString = this.createSchemaStringV4(
270
+ collection.name,
271
+ collection.attributes
272
+ );
266
273
  const camelCaseName = toCamelCase(collection.name);
267
274
  const schemaPath = path.join(
268
275
  this.appwriteFolderPath,
@@ -274,117 +281,104 @@ export class SchemaGenerator {
274
281
  });
275
282
  }
276
283
 
277
- createSchemaString = (name: string, attributes: Attribute[]): string => {
278
- const pascalName = toPascalCase(name);
279
- let imports = `import { z } from "zod";\n`;
284
+ // Zod v4 recursive getter-based schemas
285
+ createSchemaStringV4 = (name: string, attributes: Attribute[]): string => {
286
+ const pascalName = toPascalCase(name);
287
+ let imports = `import { z } from "zod";\n`;
280
288
 
281
289
  // Use the relationshipMap to find related collections
282
- const relationshipDetails = this.relationshipMap.get(name) || [];
283
- const relatedCollections = relationshipDetails
284
- .filter((detail, index, self) => {
285
- const uniqueKey = `${detail.parentCollection}-${detail.childCollection}-${detail.parentKey}-${detail.childKey}`;
286
- return (
287
- index ===
288
- self.findIndex(
290
+ const relationshipDetails = this.relationshipMap.get(name) || [];
291
+ let relatedCollections = relationshipDetails
292
+ .filter((detail, index, self) => {
293
+ const uniqueKey = `${detail.parentCollection}-${detail.childCollection}-${detail.parentKey}-${detail.childKey}`;
294
+ return (
295
+ index ===
296
+ self.findIndex(
289
297
  (obj) =>
290
298
  `${obj.parentCollection}-${obj.childCollection}-${obj.parentKey}-${obj.childKey}` ===
291
299
  uniqueKey
292
300
  )
293
301
  );
294
302
  })
295
- .map((detail) => {
296
- const relatedCollectionName = detail.isChild
297
- ? detail.parentCollection
298
- : detail.childCollection;
299
- const key = detail.isChild ? detail.childKey : detail.parentKey;
300
- const isArray = detail.isArray ? "array" : "";
301
- return [relatedCollectionName, key, isArray];
302
- });
303
-
304
- // Check if we have any relationships - if not, generate simple schema
305
- const hasRelationships = relatedCollections.length > 0;
306
-
307
- let schemaString = `${imports}\n\n`;
308
-
309
- if (!hasRelationships) {
310
- // Simple case: no relationships, generate single schema directly
311
- schemaString += `export const ${pascalName}Schema = z.object({\n`;
312
- schemaString += ` $id: z.string(),\n`;
313
- schemaString += ` $createdAt: z.string(),\n`;
314
- schemaString += ` $updatedAt: z.string(),\n`;
315
- schemaString += ` $permissions: z.array(z.string()),\n`;
316
- for (const attribute of attributes) {
317
- if (attribute.type === "relationship") {
318
- continue;
319
- }
320
- schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
321
- }
322
- schemaString += `});\n\n`;
323
- schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
324
- } else {
325
- // Complex case: has relationships, generate BaseSchema + extended schema pattern
326
- let relatedTypes = "";
327
- let relatedTypesLazy = "";
328
- let curNum = 0;
329
- let maxNum = relatedCollections.length;
330
-
331
- relatedCollections.forEach((relatedCollection) => {
332
- console.log(relatedCollection);
333
- let relatedPascalName = toPascalCase(relatedCollection[0]);
334
- let relatedCamelName = toCamelCase(relatedCollection[0]);
335
- curNum++;
336
- let endNameTypes = relatedPascalName;
337
- let endNameLazy = `${relatedPascalName}Schema`;
338
- if (relatedCollection[2] === "array") {
339
- endNameTypes += "[]";
340
- endNameLazy += ".array().default([])";
341
- } else if (!(relatedCollection[2] === "array")) {
342
- endNameTypes += " | null";
343
- endNameLazy += ".nullish()";
344
- }
345
- imports += `import { ${relatedPascalName}Schema, type ${relatedPascalName} } from "./${relatedCamelName}";\n`;
346
- relatedTypes += `${relatedCollection[1]}?: ${endNameTypes};\n`;
347
- if (relatedTypes.length > 0 && curNum !== maxNum) {
348
- relatedTypes += " ";
349
- }
350
- relatedTypesLazy += `${relatedCollection[1]}: z.lazy(() => ${endNameLazy}),\n`;
351
- if (relatedTypesLazy.length > 0 && curNum !== maxNum) {
352
- relatedTypesLazy += " ";
353
- }
354
- });
355
-
356
- // Re-add imports after processing relationships
357
- schemaString = `${imports}\n\n`;
358
-
359
- schemaString += `export const ${pascalName}SchemaBase = z.object({\n`;
360
- schemaString += ` $id: z.string(),\n`;
361
- schemaString += ` $createdAt: z.string(),\n`;
362
- schemaString += ` $updatedAt: z.string(),\n`;
363
- schemaString += ` $permissions: z.array(z.string()),\n`;
364
- for (const attribute of attributes) {
365
- if (attribute.type === "relationship") {
366
- continue;
367
- }
368
- schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
369
- }
370
- schemaString += `});\n\n`;
371
- schemaString += `export type ${pascalName}Base = z.infer<typeof ${pascalName}SchemaBase>`;
372
- if (relatedTypes.length > 0) {
373
- schemaString += ` & {\n ${relatedTypes}};\n\n`;
374
- } else {
375
- schemaString += `;\n\n`;
376
- }
377
- schemaString += `export const ${pascalName}Schema: z.ZodType<${pascalName}Base> = ${pascalName}SchemaBase`;
378
- if (relatedTypes.length > 0) {
379
- schemaString += `.extend({\n ${relatedTypesLazy}});\n\n`;
380
- } else {
381
- schemaString += `;\n`;
382
- }
383
- schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
384
- }
303
+ .map((detail) => {
304
+ const relatedCollectionName = detail.isChild
305
+ ? detail.parentCollection
306
+ : detail.childCollection;
307
+ const key = detail.isChild ? detail.childKey : detail.parentKey;
308
+ const isArray = detail.isArray ? "array" : "";
309
+ return [relatedCollectionName, key, isArray];
310
+ });
311
+
312
+ // Include one-way relationship attributes directly (no twoWayKey)
313
+ const oneWayRels: Array<[string, string, string]> = [];
314
+ for (const attr of attributes) {
315
+ if (attr.type === "relationship" && attr.relatedCollection) {
316
+ const relatedName = this.resolveCollectionName(attr.relatedCollection);
317
+ const isArray =
318
+ attr.relationType === "oneToMany" || attr.relationType === "manyToMany"
319
+ ? "array"
320
+ : "";
321
+ oneWayRels.push([relatedName, attr.key, isArray]);
322
+ }
323
+ }
324
+
325
+ // Merge and dedupe (by relatedName+key)
326
+ relatedCollections = [...relatedCollections, ...oneWayRels].filter(
327
+ (item, idx, self) =>
328
+ idx === self.findIndex((o) => `${o[0]}::${o[1]}` === `${item[0]}::${item[1]}`)
329
+ );
385
330
 
386
- return schemaString;
387
- };
331
+ const hasRelationships = relatedCollections.length > 0;
332
+
333
+ // Build imports for related collections
334
+ if (hasRelationships) {
335
+ const importLines = relatedCollections.map((rel) => {
336
+ const relatedPascalName = toPascalCase(rel[0]);
337
+ const relatedCamelName = toCamelCase(rel[0]);
338
+ return `import { ${relatedPascalName}Schema } from "./${relatedCamelName}";`;
339
+ });
340
+ const unique = Array.from(new Set(importLines));
341
+ imports += unique.join("\n") + (unique.length ? "\n" : "");
342
+ }
343
+
344
+ let schemaString = `${imports}\n`;
345
+
346
+ // Single object schema with recursive getters (Zod v4)
347
+ schemaString += `export const ${pascalName}Schema = z.object({\n`;
348
+ schemaString += ` $id: z.string(),\n`;
349
+ schemaString += ` $createdAt: z.string(),\n`;
350
+ schemaString += ` $updatedAt: z.string(),\n`;
351
+ schemaString += ` $permissions: z.array(z.string()),\n`;
352
+ for (const attribute of attributes) {
353
+ if (attribute.type === "relationship") continue;
354
+ schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
355
+ }
356
+
357
+ // Add recursive getters for relationships (respect required flag)
358
+ relatedCollections.forEach((rel) => {
359
+ const relatedPascalName = toPascalCase(rel[0]);
360
+ const isArray = rel[2] === "array";
361
+ const key = String(rel[1]);
362
+ const attrMeta = attributes.find(a => a.key === key && a.type === "relationship");
363
+ const isRequired = !!attrMeta?.required;
364
+ let getterBody = "";
365
+ if (isArray) {
366
+ getterBody = isRequired
367
+ ? `${relatedPascalName}Schema.array()`
368
+ : `${relatedPascalName}Schema.array().nullish()`;
369
+ } else {
370
+ getterBody = isRequired
371
+ ? `${relatedPascalName}Schema`
372
+ : `${relatedPascalName}Schema.nullish()`;
373
+ }
374
+ schemaString += ` get ${key}(){\n return ${getterBody}\n },\n`;
375
+ });
376
+
377
+ schemaString += `});\n\n`;
378
+ schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
379
+
380
+ return schemaString;
381
+ };
388
382
 
389
383
  typeToZod = (attribute: Attribute) => {
390
384
  let baseSchemaCode = "";
@@ -1,7 +1,9 @@
1
1
  import { mkdirSync, writeFileSync, existsSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import type { AppwriteConfig } from "appwrite-utils";
4
- import { findAppwriteConfig } from "./loadConfigs.js";
4
+ import { findAppwriteConfig } from "./loadConfigs.js";
5
+ import { loadYamlConfig } from "../config/yamlConfig.js";
6
+ import { fetchServerVersion, isVersionAtLeast } from "./versionDetection.js";
5
7
  import { findYamlConfig } from "../config/yamlConfig.js";
6
8
  import { ID } from "node-appwrite";
7
9
  import { ulid } from "ulidx";
@@ -265,16 +267,30 @@ export const setupDirsFiles = async (
265
267
  const appwriteSchemaFolder = path.join(appwriteFolder, "schemas");
266
268
  const appwriteYamlSchemaFolder = path.join(appwriteFolder, ".yaml_schemas");
267
269
  const appwriteDataFolder = path.join(appwriteFolder, "importData");
268
- const collectionsFolder = path.join(appwriteFolder, "collections");
270
+ // Decide between collections or tables folder
271
+ let useTables = false;
272
+ try {
273
+ // Try reading YAML config if present to detect version
274
+ const yamlPath = findYamlConfig(basePath);
275
+ if (yamlPath) {
276
+ const cfg = await loadYamlConfig(yamlPath);
277
+ if (cfg) {
278
+ const ver = await fetchServerVersion(cfg.appwriteEndpoint);
279
+ if (isVersionAtLeast(ver || undefined, '1.8.0')) useTables = true;
280
+ }
281
+ }
282
+ } catch {}
283
+ const targetFolderName = useTables ? "tables" : "collections";
284
+ const collectionsFolder = path.join(appwriteFolder, targetFolderName);
269
285
 
270
286
  // Create directory structure
271
287
  if (!existsSync(appwriteFolder)) {
272
288
  mkdirSync(appwriteFolder, { recursive: true });
273
289
  }
274
290
 
275
- if (!existsSync(collectionsFolder)) {
276
- mkdirSync(collectionsFolder, { recursive: true });
277
- }
291
+ if (!existsSync(collectionsFolder)) {
292
+ mkdirSync(collectionsFolder, { recursive: true });
293
+ }
278
294
 
279
295
  // Handle configuration file creation - YAML is now default
280
296
  if (useYaml) {
@@ -12,12 +12,12 @@
12
12
 
13
13
  export type ApiMode = 'legacy' | 'tablesdb';
14
14
 
15
- export interface VersionDetectionResult {
16
- apiMode: ApiMode;
17
- detectionMethod: 'endpoint_probe' | 'health_check' | 'fallback';
18
- serverVersion?: string;
19
- confidence: 'high' | 'medium' | 'low';
20
- }
15
+ export interface VersionDetectionResult {
16
+ apiMode: ApiMode;
17
+ detectionMethod: 'endpoint_probe' | 'health_check' | 'fallback';
18
+ serverVersion?: string;
19
+ confidence: 'high' | 'medium' | 'low';
20
+ }
21
21
 
22
22
  /**
23
23
  * Detects Appwrite API version and TablesDB support
@@ -55,13 +55,13 @@ export async function detectAppwriteVersion(
55
55
  console.warn('SDK capability probe failed:', error instanceof Error ? error.message : 'Unknown error');
56
56
  }
57
57
 
58
- // Fallback to legacy mode
59
- return {
60
- apiMode: 'legacy',
61
- detectionMethod: 'fallback',
62
- confidence: 'low'
63
- };
64
- }
58
+ // Fallback to legacy mode
59
+ return {
60
+ apiMode: 'legacy',
61
+ detectionMethod: 'fallback',
62
+ confidence: 'low'
63
+ };
64
+ }
65
65
 
66
66
  /**
67
67
  * Test TablesDB endpoint availability - most reliable detection method
@@ -82,15 +82,15 @@ async function probeTablesDbEndpoint(
82
82
  signal: AbortSignal.timeout(5000)
83
83
  });
84
84
 
85
- if (response.ok || response.status === 404) {
85
+ if (response.ok || response.status === 404) {
86
86
  // 200 = TablesDB available, 404 = endpoint exists but no tables
87
87
  // Both indicate TablesDB support
88
- return {
89
- apiMode: 'tablesdb',
90
- detectionMethod: 'endpoint_probe',
91
- confidence: 'high'
92
- };
93
- }
88
+ return {
89
+ apiMode: 'tablesdb',
90
+ detectionMethod: 'endpoint_probe',
91
+ confidence: 'high'
92
+ };
93
+ }
94
94
 
95
95
  // 501 Not Implemented or other errors = no TablesDB support
96
96
  throw new Error(`TablesDB endpoint returned ${response.status}: ${response.statusText}`);
@@ -262,4 +262,31 @@ export async function detectSdkSupport(): Promise<{
262
262
  */
263
263
  export function clearVersionDetectionCache(): void {
264
264
  detectionCache.clear();
265
- }
265
+ }
266
+
267
+ /**
268
+ * Fetch server version from /health/version (no auth required)
269
+ */
270
+ export async function fetchServerVersion(endpoint: string): Promise<string | null> {
271
+ try {
272
+ const clean = endpoint.replace(/\/$/, '');
273
+ const res = await fetch(`${clean}/health/version`, { method: 'GET', signal: AbortSignal.timeout(5000) });
274
+ if (!res.ok) return null;
275
+ const data = await res.json().catch(() => null) as any;
276
+ const version = (data && (data.version || data.build || data.release)) ?? null;
277
+ return typeof version === 'string' ? version : null;
278
+ } catch {
279
+ return null;
280
+ }
281
+ }
282
+
283
+ /** Compare semantic versions (basic) */
284
+ export function isVersionAtLeast(current: string | undefined, target: string): boolean {
285
+ if (!current) return false;
286
+ const toNums = (v: string) => v.split('.').map(n => parseInt(n, 10));
287
+ const [a1=0,a2=0,a3=0] = toNums(current);
288
+ const [b1,b2,b3] = toNums(target);
289
+ if (a1 !== b1) return a1 > b1;
290
+ if (a2 !== b2) return a2 > b2;
291
+ return a3 >= b3;
292
+ }
@@ -27,13 +27,14 @@ import {
27
27
  wipeOtherDatabases,
28
28
  ensureCollectionsExist,
29
29
  } from "./databases/setup.js";
30
- import {
31
- createOrUpdateCollections,
32
- wipeDatabase,
33
- generateSchemas,
34
- fetchAllCollections,
35
- wipeCollection,
36
- } from "./collections/methods.js";
30
+ import {
31
+ createOrUpdateCollections,
32
+ wipeDatabase,
33
+ generateSchemas,
34
+ fetchAllCollections,
35
+ wipeCollection,
36
+ } from "./collections/methods.js";
37
+ import { wipeAllTables, wipeTableRows } from "./collections/methods.js";
37
38
  import {
38
39
  backupDatabase,
39
40
  ensureDatabaseConfigBucketsExist,
@@ -57,7 +58,8 @@ import {
57
58
  transferUsersLocalToRemote,
58
59
  type TransferOptions,
59
60
  } from "./migrations/transfer.js";
60
- import { getClient } from "./utils/getClientFromConfig.js";
61
+ import { getClient } from "./utils/getClientFromConfig.js";
62
+ import { getAdapterFromConfig } from "./utils/getClientFromConfig.js";
61
63
  import { fetchAllDatabases } from "./databases/methods.js";
62
64
  import {
63
65
  listFunctions,
@@ -413,14 +415,23 @@ export class UtilsController {
413
415
  MessageFormatter.success("All functions synchronized successfully!", { prefix: "Functions" });
414
416
  }
415
417
 
416
- async wipeDatabase(database: Models.Database, wipeBucket: boolean = false) {
417
- await this.init();
418
- if (!this.database) throw new Error("Database not initialized");
419
- await wipeDatabase(this.database, database.$id);
420
- if (wipeBucket) {
421
- await this.wipeBucketFromDatabase(database);
422
- }
423
- }
418
+ async wipeDatabase(database: Models.Database, wipeBucket: boolean = false) {
419
+ await this.init();
420
+ if (!this.database || !this.config) throw new Error("Database not initialized");
421
+ try {
422
+ const { adapter, apiMode } = await getAdapterFromConfig(this.config);
423
+ if (apiMode === 'tablesdb') {
424
+ await wipeAllTables(adapter, database.$id);
425
+ } else {
426
+ await wipeDatabase(this.database, database.$id);
427
+ }
428
+ } catch {
429
+ await wipeDatabase(this.database, database.$id);
430
+ }
431
+ if (wipeBucket) {
432
+ await this.wipeBucketFromDatabase(database);
433
+ }
434
+ }
424
435
 
425
436
  async wipeBucketFromDatabase(database: Models.Database) {
426
437
  // Check configured bucket in database config
@@ -448,14 +459,23 @@ export class UtilsController {
448
459
  }
449
460
  }
450
461
 
451
- async wipeCollection(
452
- database: Models.Database,
453
- collection: Models.Collection
454
- ) {
455
- await this.init();
456
- if (!this.database) throw new Error("Database not initialized");
457
- await wipeCollection(this.database, database.$id, collection.$id);
458
- }
462
+ async wipeCollection(
463
+ database: Models.Database,
464
+ collection: Models.Collection
465
+ ) {
466
+ await this.init();
467
+ if (!this.database || !this.config) throw new Error("Database not initialized");
468
+ try {
469
+ const { adapter, apiMode } = await getAdapterFromConfig(this.config);
470
+ if (apiMode === 'tablesdb') {
471
+ await wipeTableRows(adapter, database.$id, collection.$id);
472
+ } else {
473
+ await wipeCollection(this.database, database.$id, collection.$id);
474
+ }
475
+ } catch {
476
+ await wipeCollection(this.database, database.$id, collection.$id);
477
+ }
478
+ }
459
479
 
460
480
  async wipeDocumentStorage(bucketId: string) {
461
481
  await this.init();
@@ -581,10 +601,10 @@ export class UtilsController {
581
601
  await generator.updateConfig(this.config, isYamlProject);
582
602
  }
583
603
 
584
- async syncDb(
585
- databases: Models.Database[] = [],
586
- collections: Models.Collection[] = []
587
- ) {
604
+ async syncDb(
605
+ databases: Models.Database[] = [],
606
+ collections: Models.Collection[] = []
607
+ ) {
588
608
  await this.init();
589
609
  if (!this.database) {
590
610
  MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
@@ -594,10 +614,17 @@ export class UtilsController {
594
614
  const allDatabases = await fetchAllDatabases(this.database);
595
615
  databases = allDatabases;
596
616
  }
597
- await this.ensureDatabasesExist(databases);
598
- await this.ensureDatabaseConfigBucketsExist(databases);
599
- await this.createOrUpdateCollectionsForDatabases(databases, collections);
600
- }
617
+ // Ensure DBs exist (this may internally ensure migrations exists)
618
+ await this.ensureDatabasesExist(databases);
619
+ await this.ensureDatabaseConfigBucketsExist(databases);
620
+
621
+ // Do not push collections to the migrations database (prevents duplicate runs)
622
+ const dbsForCollections = databases.filter(
623
+ (db) => (this.config?.useMigrations ?? true) ? db.name.toLowerCase() !== "migrations" : true
624
+ );
625
+
626
+ await this.createOrUpdateCollectionsForDatabases(dbsForCollections, collections);
627
+ }
601
628
 
602
629
  getAppwriteFolderPath() {
603
630
  return this.appwriteFolderPath;