appwrite-utils-cli 0.9.77 → 0.9.78

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.
@@ -7,6 +7,7 @@ export interface SetupOptions {
7
7
  collections?: string[];
8
8
  doBackup?: boolean;
9
9
  wipeDatabase?: boolean;
10
+ wipeCollections?: boolean;
10
11
  wipeDocumentStorage?: boolean;
11
12
  wipeUsers?: boolean;
12
13
  generateSchemas?: boolean;
@@ -26,9 +27,11 @@ export declare class UtilsController {
26
27
  afterImportActionsDefinitions: AfterImportActions;
27
28
  constructor(currentUserDir: string);
28
29
  init(): Promise<void>;
30
+ reloadConfig(): Promise<void>;
29
31
  setupMigrationDatabase(): Promise<void>;
30
32
  ensureDatabaseConfigBucketsExist(databases?: Models.Database[]): Promise<void>;
31
33
  ensureDatabasesExist(databases?: Models.Database[]): Promise<void>;
34
+ ensureCollectionsExist(database: Models.Database, collections?: Models.Collection[]): Promise<void>;
32
35
  getDatabasesByIds(ids: string[]): Promise<Models.Database[]>;
33
36
  wipeOtherDatabases(databasesToKeep: Models.Database[]): Promise<void>;
34
37
  wipeUsers(): Promise<void>;
@@ -37,16 +40,17 @@ export declare class UtilsController {
37
40
  collectionId: string;
38
41
  collectionName: string;
39
42
  }[]>;
43
+ wipeCollection(database: Models.Database, collection: Models.Collection): Promise<void>;
40
44
  wipeDocumentStorage(bucketId: string): Promise<void>;
41
- createOrUpdateCollectionsForDatabases(databases: Models.Database[]): Promise<void>;
45
+ createOrUpdateCollectionsForDatabases(databases: Models.Database[], collections?: Models.Collection[]): Promise<void>;
42
46
  createOrUpdateCollections(database: Models.Database, deletedCollections?: {
43
47
  collectionId: string;
44
48
  collectionName: string;
45
- }[]): Promise<void>;
49
+ }[], collections?: Models.Collection[]): Promise<void>;
46
50
  generateSchemas(): Promise<void>;
47
- importData(options: SetupOptions): Promise<void>;
51
+ importData(options?: SetupOptions): Promise<void>;
48
52
  synchronizeConfigurations(databases?: Models.Database[], config?: AppwriteConfig): Promise<void>;
49
- syncDb(): Promise<void>;
53
+ syncDb(databases?: Models.Database[], collections?: Models.Collection[]): Promise<void>;
50
54
  getAppwriteFolderPath(): string;
51
55
  transferData(options: TransferOptions): Promise<void>;
52
56
  }
@@ -5,8 +5,8 @@ import { UsersController } from "./migrations/users.js";
5
5
  import { AppwriteToX } from "./migrations/appwriteToX.js";
6
6
  import { ImportController } from "./migrations/importController.js";
7
7
  import { ImportDataActions } from "./migrations/importDataActions.js";
8
- import { setupMigrationDatabase, ensureDatabasesExist, wipeOtherDatabases, } from "./migrations/setupDatabase.js";
9
- import { createOrUpdateCollections, wipeDatabase, generateSchemas, fetchAllCollections, } from "./collections/methods.js";
8
+ import { setupMigrationDatabase, ensureDatabasesExist, wipeOtherDatabases, ensureCollectionsExist, } from "./migrations/setupDatabase.js";
9
+ import { createOrUpdateCollections, wipeDatabase, generateSchemas, fetchAllCollections, wipeCollection, } from "./collections/methods.js";
10
10
  import { backupDatabase, ensureDatabaseConfigBucketsExist, initOrGetBackupStorage, wipeDocumentStorage, } from "./storage/methods.js";
11
11
  import path from "path";
12
12
  import { converterFunctions, validationRules, } from "appwrite-utils";
@@ -53,6 +53,17 @@ export class UtilsController {
53
53
  this.config.appwriteClient = this.appwriteServer;
54
54
  }
55
55
  }
56
+ async reloadConfig() {
57
+ this.config = await loadConfig(this.appwriteFolderPath);
58
+ this.appwriteServer = new Client();
59
+ this.appwriteServer
60
+ .setEndpoint(this.config.appwriteEndpoint)
61
+ .setProject(this.config.appwriteProject)
62
+ .setKey(this.config.appwriteKey);
63
+ this.database = new Databases(this.appwriteServer);
64
+ this.storage = new Storage(this.appwriteServer);
65
+ this.config.appwriteClient = this.appwriteServer;
66
+ }
56
67
  async setupMigrationDatabase() {
57
68
  await this.init();
58
69
  if (!this.config)
@@ -73,7 +84,13 @@ export class UtilsController {
73
84
  throw new Error("Config not initialized");
74
85
  await this.setupMigrationDatabase();
75
86
  await this.ensureDatabaseConfigBucketsExist(databases);
76
- await ensureDatabasesExist(this.config);
87
+ await ensureDatabasesExist(this.config, databases);
88
+ }
89
+ async ensureCollectionsExist(database, collections) {
90
+ await this.init();
91
+ if (!this.config)
92
+ throw new Error("Config not initialized");
93
+ await ensureCollectionsExist(this.config, database, collections);
77
94
  }
78
95
  async getDatabasesByIds(ids) {
79
96
  await this.init();
@@ -110,27 +127,33 @@ export class UtilsController {
110
127
  throw new Error("Database not initialized");
111
128
  return await wipeDatabase(this.database, database.$id);
112
129
  }
130
+ async wipeCollection(database, collection) {
131
+ await this.init();
132
+ if (!this.database)
133
+ throw new Error("Database not initialized");
134
+ await wipeCollection(this.database, database.$id, collection.$id);
135
+ }
113
136
  async wipeDocumentStorage(bucketId) {
114
137
  await this.init();
115
138
  if (!this.storage)
116
139
  throw new Error("Storage not initialized");
117
140
  await wipeDocumentStorage(this.storage, bucketId);
118
141
  }
119
- async createOrUpdateCollectionsForDatabases(databases) {
142
+ async createOrUpdateCollectionsForDatabases(databases, collections = []) {
120
143
  await this.init();
121
144
  if (!this.database || !this.config)
122
145
  throw new Error("Database or config not initialized");
123
146
  for (const database of databases) {
124
147
  if (database.$id === "migrations")
125
148
  continue;
126
- await this.createOrUpdateCollections(database);
149
+ await this.createOrUpdateCollections(database, undefined, collections);
127
150
  }
128
151
  }
129
- async createOrUpdateCollections(database, deletedCollections) {
152
+ async createOrUpdateCollections(database, deletedCollections, collections = []) {
130
153
  await this.init();
131
154
  if (!this.database || !this.config)
132
155
  throw new Error("Database or config not initialized");
133
- await createOrUpdateCollections(this.database, database.$id, this.config, deletedCollections);
156
+ await createOrUpdateCollections(this.database, database.$id, this.config, deletedCollections, collections);
134
157
  }
135
158
  async generateSchemas() {
136
159
  await this.init();
@@ -138,13 +161,17 @@ export class UtilsController {
138
161
  throw new Error("Config not initialized");
139
162
  await generateSchemas(this.config, this.appwriteFolderPath);
140
163
  }
141
- async importData(options) {
164
+ async importData(options = {}) {
142
165
  await this.init();
143
- if (!this.config || !this.database || !this.storage)
144
- throw new Error("Config, database, or storage not initialized");
166
+ if (!this.database)
167
+ throw new Error("Database not initialized");
168
+ if (!this.storage)
169
+ throw new Error("Storage not initialized");
170
+ if (!this.config)
171
+ throw new Error("Config not initialized");
145
172
  const importDataActions = new ImportDataActions(this.database, this.storage, this.config, this.converterDefinitions, this.validityRuleDefinitions, this.afterImportActionsDefinitions);
146
- const importController = new ImportController(this.config, this.database, this.storage, this.appwriteFolderPath, importDataActions, options);
147
- await importController.run();
173
+ const importController = new ImportController(this.config, this.database, this.storage, this.appwriteFolderPath, importDataActions, options, options.databases);
174
+ await importController.run(options.collections);
148
175
  }
149
176
  async synchronizeConfigurations(databases, config) {
150
177
  await this.init();
@@ -156,14 +183,17 @@ export class UtilsController {
156
183
  const appwriteToX = new AppwriteToX(configToUse, this.appwriteFolderPath, this.storage);
157
184
  await appwriteToX.toSchemas(databases);
158
185
  }
159
- async syncDb() {
186
+ async syncDb(databases = [], collections = []) {
160
187
  await this.init();
161
188
  if (!this.database)
162
189
  throw new Error("Database not initialized");
163
- const databases = await fetchAllDatabases(this.database);
190
+ if (databases.length === 0) {
191
+ const allDatabases = await fetchAllDatabases(this.database);
192
+ databases = allDatabases;
193
+ }
164
194
  await this.ensureDatabasesExist(databases);
165
195
  await this.ensureDatabaseConfigBucketsExist(databases);
166
- await this.createOrUpdateCollectionsForDatabases(databases);
196
+ await this.createOrUpdateCollectionsForDatabases(databases, collections);
167
197
  }
168
198
  getAppwriteFolderPath() {
169
199
  return this.appwriteFolderPath;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "appwrite-utils-cli",
3
3
  "description": "Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.",
4
- "version": "0.9.77",
4
+ "version": "0.9.78",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -31,14 +31,15 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@types/inquirer": "^9.0.7",
34
- "appwrite-utils": "^0.3.8",
34
+ "appwrite-utils": "^0.3.9",
35
+ "chalk": "^5.3.0",
35
36
  "commander": "^12.1.0",
36
37
  "inquirer": "^9.3.6",
37
38
  "js-yaml": "^4.1.0",
38
39
  "lodash": "^4.17.21",
39
40
  "luxon": "^3.5.0",
40
41
  "nanostores": "^0.10.3",
41
- "node-appwrite": "14.0.0",
42
+ "node-appwrite": "^14.1.0",
42
43
  "tsx": "^4.17.0",
43
44
  "ulidx": "^2.4.0",
44
45
  "winston": "^3.14.2",
@@ -7,6 +7,7 @@ import {
7
7
  import { nameToIdMapping, enqueueOperation } from "../migrations/queue.js";
8
8
  import _ from "lodash";
9
9
  import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
10
+ import chalk from "chalk";
10
11
 
11
12
  const attributesSame = (
12
13
  databaseAttribute: Attribute,
@@ -85,6 +86,7 @@ export const createOrUpdateAttribute = async (
85
86
  (attr) => attr.key === attribute.key
86
87
  ) as unknown as any;
87
88
  foundAttribute = parseAttribute(collectionAttr);
89
+ // console.log(`Found attribute: ${JSON.stringify(foundAttribute)}`);
88
90
  } catch (error) {
89
91
  foundAttribute = undefined;
90
92
  }
@@ -96,7 +98,10 @@ export const createOrUpdateAttribute = async (
96
98
  // console.log(
97
99
  // `Updating attribute with same key ${attribute.key} but different values`
98
100
  // );
99
- finalAttribute = attribute;
101
+ finalAttribute = {
102
+ ...foundAttribute,
103
+ ...attribute,
104
+ };
100
105
  action = "update";
101
106
  } else if (!updateEnabled && foundAttribute && !attributesSame(foundAttribute, attribute)) {
102
107
  await db.deleteAttribute(dbId, collection.$id, attribute.key);
@@ -120,9 +125,9 @@ export const createOrUpdateAttribute = async (
120
125
  relatedCollectionId!
121
126
  );
122
127
  } catch (e) {
123
- console.log(
124
- `Collection not found: ${finalAttribute.relatedCollection} when nameToIdMapping was set`
125
- );
128
+ // console.log(
129
+ // `Collection not found: ${finalAttribute.relatedCollection} when nameToIdMapping was set`
130
+ // );
126
131
  collectionFoundViaRelatedCollection = undefined;
127
132
  }
128
133
  } else {
@@ -139,7 +144,7 @@ export const createOrUpdateAttribute = async (
139
144
  }
140
145
  }
141
146
  if (!(relatedCollectionId && collectionFoundViaRelatedCollection)) {
142
- console.log(`Enqueueing operation for attribute: ${finalAttribute.key}`);
147
+ // console.log(`Enqueueing operation for attribute: ${finalAttribute.key}`);
143
148
  enqueueOperation({
144
149
  type: "attribute",
145
150
  collectionId: collection.$id,
@@ -150,7 +155,7 @@ export const createOrUpdateAttribute = async (
150
155
  return;
151
156
  }
152
157
  }
153
- finalAttribute = attributeSchema.parse(finalAttribute);
158
+ finalAttribute = parseAttribute(finalAttribute);
154
159
  // console.log(`Final Attribute: ${JSON.stringify(finalAttribute)}`);
155
160
  switch (finalAttribute.type) {
156
161
  case "string":
@@ -463,7 +468,7 @@ export const createUpdateCollectionAttributes = async (
463
468
  attributes: Attribute[]
464
469
  ): Promise<void> => {
465
470
  console.log(
466
- `Creating/Updating attributes for collection: ${collection.name}`
471
+ chalk.green(`Creating/Updating attributes for collection: ${collection.name}`)
467
472
  );
468
473
 
469
474
  const batchSize = 3;
@@ -6,7 +6,7 @@ import {
6
6
  Query,
7
7
  type Models,
8
8
  } from "node-appwrite";
9
- import type { AppwriteConfig, CollectionCreate } from "appwrite-utils";
9
+ import type { AppwriteConfig, CollectionCreate, Indexes } from "appwrite-utils";
10
10
  import { nameToIdMapping, processQueue } from "../migrations/queue.js";
11
11
  import { createUpdateCollectionAttributes } from "./attributes.js";
12
12
  import { createOrUpdateIndexes } from "./indexes.js";
@@ -124,6 +124,22 @@ export const fetchAndCacheCollectionByName = async (
124
124
  }
125
125
  };
126
126
 
127
+ async function wipeDocumentsFromCollection(database: Databases, databaseId: string, collectionId: string) {
128
+ const initialDocuments = await database.listDocuments(databaseId, collectionId, [Query.limit(1000)]);
129
+ let documents = initialDocuments.documents;
130
+ while (documents.length === 1000) {
131
+ const docsResponse = await database.listDocuments(databaseId, collectionId, [Query.limit(1000)]);
132
+ documents = documents.concat(docsResponse.documents);
133
+ }
134
+ const batchDeletePromises = documents.map(doc => database.deleteDocument(databaseId, collectionId, doc.$id));
135
+ const maxStackSize = 100;
136
+ for (let i = 0; i < batchDeletePromises.length; i += maxStackSize) {
137
+ await Promise.all(batchDeletePromises.slice(i, i + maxStackSize));
138
+ await delay(100);
139
+ }
140
+ console.log(`Deleted ${documents.length} documents from collection ${collectionId}`);
141
+ }
142
+
127
143
  export const wipeDatabase = async (
128
144
  database: Databases,
129
145
  databaseId: string
@@ -146,6 +162,20 @@ export const wipeDatabase = async (
146
162
  return collectionsDeleted;
147
163
  };
148
164
 
165
+ export const wipeCollection = async (
166
+ database: Databases,
167
+ databaseId: string,
168
+ collectionId: string
169
+ ): Promise<void> => {
170
+ const collections = await database.listCollections(databaseId, [Query.equal("$id", collectionId)]);
171
+ if (collections.total === 0) {
172
+ console.log(`Collection ${collectionId} not found`);
173
+ return;
174
+ }
175
+ const collection = collections.collections[0];
176
+ await wipeDocumentsFromCollection(database, databaseId, collection.$id);
177
+ };
178
+
149
179
  export const generateSchemas = async (
150
180
  config: AppwriteConfig,
151
181
  appwriteFolderPath: string
@@ -158,38 +188,45 @@ export const createOrUpdateCollections = async (
158
188
  database: Databases,
159
189
  databaseId: string,
160
190
  config: AppwriteConfig,
161
- deletedCollections?: { collectionId: string; collectionName: string }[]
191
+ deletedCollections?: { collectionId: string; collectionName: string }[],
192
+ selectedCollections: Models.Collection[] = []
162
193
  ): Promise<void> => {
163
- const configCollections = config.collections;
164
- if (!configCollections) {
194
+ const collectionsToProcess = selectedCollections.length > 0 ? selectedCollections : config.collections;
195
+ if (!collectionsToProcess) {
165
196
  return;
166
197
  }
167
198
  const usedIds = new Set();
168
199
 
169
- for (const { attributes, indexes, ...collection } of configCollections) {
200
+ for (const collection of collectionsToProcess) {
201
+ const { attributes, indexes, ...collectionData } = collection;
202
+
170
203
  // Prepare permissions for the collection
171
204
  const permissions: string[] = [];
172
205
  if (collection.$permissions && collection.$permissions.length > 0) {
173
206
  for (const permission of collection.$permissions) {
174
- switch (permission.permission) {
175
- case "read":
176
- permissions.push(Permission.read(permission.target));
177
- break;
178
- case "create":
179
- permissions.push(Permission.create(permission.target));
180
- break;
181
- case "update":
182
- permissions.push(Permission.update(permission.target));
183
- break;
184
- case "delete":
185
- permissions.push(Permission.delete(permission.target));
186
- break;
187
- case "write":
188
- permissions.push(Permission.write(permission.target));
189
- break;
190
- default:
191
- console.log(`Unknown permission: ${permission.permission}`);
192
- break;
207
+ if (typeof permission === 'string') {
208
+ permissions.push(permission);
209
+ } else {
210
+ switch (permission.permission) {
211
+ case "read":
212
+ permissions.push(Permission.read(permission.target));
213
+ break;
214
+ case "create":
215
+ permissions.push(Permission.create(permission.target));
216
+ break;
217
+ case "update":
218
+ permissions.push(Permission.update(permission.target));
219
+ break;
220
+ case "delete":
221
+ permissions.push(Permission.delete(permission.target));
222
+ break;
223
+ case "write":
224
+ permissions.push(Permission.write(permission.target));
225
+ break;
226
+ default:
227
+ console.log(`Unknown permission: ${permission.permission}`);
228
+ break;
229
+ }
193
230
  }
194
231
  }
195
232
  }
@@ -198,7 +235,7 @@ export const createOrUpdateCollections = async (
198
235
  let collectionsFound = await tryAwaitWithRetry(
199
236
  async () =>
200
237
  await database.listCollections(databaseId, [
201
- Query.equal("name", collection.name),
238
+ Query.equal("name", collectionData.name),
202
239
  ])
203
240
  );
204
241
 
@@ -208,15 +245,15 @@ export const createOrUpdateCollections = async (
208
245
  // Determine the correct ID for the collection
209
246
  let collectionId: string;
210
247
  if (!collectionToUse) {
211
- console.log(`Creating collection: ${collection.name}`);
248
+ console.log(`Creating collection: ${collectionData.name}`);
212
249
  let foundColl = deletedCollections?.find(
213
250
  (coll) =>
214
251
  coll.collectionName.toLowerCase().trim().replace(" ", "") ===
215
- collection.name.toLowerCase().trim().replace(" ", "")
252
+ collectionData.name.toLowerCase().trim().replace(" ", "")
216
253
  );
217
254
 
218
- if (collection.$id) {
219
- collectionId = collection.$id;
255
+ if (collectionData.$id) {
256
+ collectionId = collectionData.$id;
220
257
  } else if (foundColl && !usedIds.has(foundColl.collectionId)) {
221
258
  collectionId = foundColl.collectionId;
222
259
  } else {
@@ -232,31 +269,31 @@ export const createOrUpdateCollections = async (
232
269
  await database.createCollection(
233
270
  databaseId,
234
271
  collectionId,
235
- collection.name,
272
+ collectionData.name,
236
273
  permissions,
237
- collection.documentSecurity ?? false,
238
- collection.enabled ?? true
274
+ collectionData.documentSecurity ?? false,
275
+ collectionData.enabled ?? true
239
276
  )
240
277
  );
241
- collection.$id = collectionToUse!.$id;
242
- nameToIdMapping.set(collection.name, collectionToUse!.$id);
278
+ collectionData.$id = collectionToUse!.$id;
279
+ nameToIdMapping.set(collectionData.name, collectionToUse!.$id);
243
280
  } catch (error) {
244
281
  console.error(
245
- `Failed to create collection ${collection.name} with ID ${collectionId}: ${error}`
282
+ `Failed to create collection ${collectionData.name} with ID ${collectionId}: ${error}`
246
283
  );
247
284
  continue;
248
285
  }
249
286
  } else {
250
- console.log(`Collection ${collection.name} exists, updating it`);
287
+ console.log(`Collection ${collectionData.name} exists, updating it`);
251
288
  await tryAwaitWithRetry(
252
289
  async () =>
253
290
  await database.updateCollection(
254
291
  databaseId,
255
292
  collectionToUse!.$id,
256
- collection.name,
293
+ collectionData.name,
257
294
  permissions,
258
- collection.documentSecurity ?? false,
259
- collection.enabled ?? true
295
+ collectionData.documentSecurity ?? false,
296
+ collectionData.enabled ?? true
260
297
  )
261
298
  );
262
299
  }
@@ -270,6 +307,7 @@ export const createOrUpdateCollections = async (
270
307
  database,
271
308
  databaseId,
272
309
  collectionToUse!,
310
+ // @ts-expect-error
273
311
  attributes
274
312
  );
275
313
 
@@ -281,7 +319,7 @@ export const createOrUpdateCollections = async (
281
319
  databaseId,
282
320
  database,
283
321
  collectionToUse!.$id,
284
- indexes ?? []
322
+ (indexes ?? []) as Indexes,
285
323
  );
286
324
 
287
325
  // Add delay after creating indexes