appwrite-utils-cli 1.2.5 → 1.2.6

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.
@@ -1660,6 +1660,7 @@ export class InteractiveCLI {
1660
1660
  message: "Select what to transfer:",
1661
1661
  choices: [
1662
1662
  { name: "👥 Users", value: "users", checked: true },
1663
+ { name: "👥 Teams", value: "teams", checked: true },
1663
1664
  { name: "🗄️ Databases", value: "databases", checked: true },
1664
1665
  { name: "📦 Storage Buckets", value: "buckets", checked: true },
1665
1666
  { name: "⚡ Functions", value: "functions", checked: true },
@@ -1746,6 +1747,7 @@ export class InteractiveCLI {
1746
1747
  targetProject: targetConfig.targetProject,
1747
1748
  targetKey: targetConfig.targetKey,
1748
1749
  transferUsers: transferOptions.transferTypes.includes("users"),
1750
+ transferTeams: transferOptions.transferTypes.includes("teams"),
1749
1751
  transferDatabases: transferOptions.transferTypes.includes("databases"),
1750
1752
  transferBuckets: transferOptions.transferTypes.includes("buckets"),
1751
1753
  transferFunctions: transferOptions.transferTypes.includes("functions"),
@@ -1764,6 +1766,9 @@ export class InteractiveCLI {
1764
1766
  MessageFormatter.info("Users with preserved password hashes can log in with their original passwords", { prefix: "Transfer" });
1765
1767
  MessageFormatter.info("Users with temporary passwords will need to reset their passwords", { prefix: "Transfer" });
1766
1768
  }
1769
+ if (transferOptions.transferTypes.includes("teams") && results.teams.transferred > 0) {
1770
+ MessageFormatter.info("Team memberships have been transferred and may require user acceptance of invitations", { prefix: "Transfer" });
1771
+ }
1767
1772
  }
1768
1773
  }
1769
1774
  catch (error) {
@@ -6,6 +6,7 @@ export interface ComprehensiveTransferOptions {
6
6
  targetProject: string;
7
7
  targetKey: string;
8
8
  transferUsers?: boolean;
9
+ transferTeams?: boolean;
9
10
  transferDatabases?: boolean;
10
11
  transferBuckets?: boolean;
11
12
  transferFunctions?: boolean;
@@ -18,6 +19,11 @@ export interface TransferResults {
18
19
  skipped: number;
19
20
  failed: number;
20
21
  };
22
+ teams: {
23
+ transferred: number;
24
+ skipped: number;
25
+ failed: number;
26
+ };
21
27
  databases: {
22
28
  transferred: number;
23
29
  skipped: number;
@@ -41,6 +47,8 @@ export declare class ComprehensiveTransfer {
41
47
  private targetClient;
42
48
  private sourceUsers;
43
49
  private targetUsers;
50
+ private sourceTeams;
51
+ private targetTeams;
44
52
  private sourceDatabases;
45
53
  private targetDatabases;
46
54
  private sourceStorage;
@@ -57,6 +65,7 @@ export declare class ComprehensiveTransfer {
57
65
  constructor(options: ComprehensiveTransferOptions);
58
66
  execute(): Promise<TransferResults>;
59
67
  private transferAllUsers;
68
+ private transferAllTeams;
60
69
  private transferAllDatabases;
61
70
  /**
62
71
  * Phase 1: Create database structure (collections, attributes, indexes) without transferring documents
@@ -96,5 +105,17 @@ export declare class ComprehensiveTransfer {
96
105
  * Helper method to transfer documents between databases
97
106
  */
98
107
  private transferDocumentsBetweenDatabases;
108
+ /**
109
+ * Helper method to fetch all teams with pagination
110
+ */
111
+ private fetchAllTeams;
112
+ /**
113
+ * Helper method to fetch all memberships for a team with pagination
114
+ */
115
+ private fetchAllMemberships;
116
+ /**
117
+ * Helper method to transfer team memberships
118
+ */
119
+ private transferTeamMemberships;
99
120
  private printSummary;
100
121
  }
@@ -1,5 +1,5 @@
1
1
  import { converterFunctions, tryAwaitWithRetry, parseAttribute } from "appwrite-utils";
2
- import { Client, Databases, Storage, Users, Functions, Query, } from "node-appwrite";
2
+ import { Client, Databases, Storage, Users, Functions, Teams, Query, } from "node-appwrite";
3
3
  import { InputFile } from "node-appwrite/file";
4
4
  import { MessageFormatter } from "../shared/messageFormatter.js";
5
5
  import { ProgressManager } from "../shared/progressManager.js";
@@ -17,6 +17,8 @@ export class ComprehensiveTransfer {
17
17
  targetClient;
18
18
  sourceUsers;
19
19
  targetUsers;
20
+ sourceTeams;
21
+ targetTeams;
20
22
  sourceDatabases;
21
23
  targetDatabases;
22
24
  sourceStorage;
@@ -36,6 +38,8 @@ export class ComprehensiveTransfer {
36
38
  this.targetClient = getClient(options.targetEndpoint, options.targetProject, options.targetKey);
37
39
  this.sourceUsers = new Users(this.sourceClient);
38
40
  this.targetUsers = new Users(this.targetClient);
41
+ this.sourceTeams = new Teams(this.sourceClient);
42
+ this.targetTeams = new Teams(this.targetClient);
39
43
  this.sourceDatabases = new Databases(this.sourceClient);
40
44
  this.targetDatabases = new Databases(this.targetClient);
41
45
  this.sourceStorage = new Storage(this.sourceClient);
@@ -51,6 +55,7 @@ export class ComprehensiveTransfer {
51
55
  this.fileLimit = pLimit(Math.max(1, Math.floor(baseLimit / 4)));
52
56
  this.results = {
53
57
  users: { transferred: 0, skipped: 0, failed: 0 },
58
+ teams: { transferred: 0, skipped: 0, failed: 0 },
54
59
  databases: { transferred: 0, skipped: 0, failed: 0 },
55
60
  buckets: { transferred: 0, skipped: 0, failed: 0 },
56
61
  functions: { transferred: 0, skipped: 0, failed: 0 },
@@ -78,6 +83,9 @@ export class ComprehensiveTransfer {
78
83
  if (this.options.transferUsers !== false) {
79
84
  await this.transferAllUsers();
80
85
  }
86
+ if (this.options.transferTeams !== false) {
87
+ await this.transferAllTeams();
88
+ }
81
89
  if (this.options.transferDatabases !== false) {
82
90
  await this.transferAllDatabases();
83
91
  }
@@ -124,6 +132,65 @@ export class ComprehensiveTransfer {
124
132
  this.results.users.failed = 1;
125
133
  }
126
134
  }
135
+ async transferAllTeams() {
136
+ MessageFormatter.info("Starting team transfer phase", { prefix: "Transfer" });
137
+ try {
138
+ // Fetch all teams from source with pagination
139
+ const allSourceTeams = await this.fetchAllTeams(this.sourceTeams);
140
+ const allTargetTeams = await this.fetchAllTeams(this.targetTeams);
141
+ if (this.options.dryRun) {
142
+ let totalMemberships = 0;
143
+ for (const team of allSourceTeams) {
144
+ const memberships = await this.sourceTeams.listMemberships(team.$id, [Query.limit(1)]);
145
+ totalMemberships += memberships.total;
146
+ }
147
+ MessageFormatter.info(`DRY RUN: Would transfer ${allSourceTeams.length} teams with ${totalMemberships} memberships`, { prefix: "Transfer" });
148
+ return;
149
+ }
150
+ const transferTasks = allSourceTeams.map(team => this.limit(async () => {
151
+ try {
152
+ // Check if team exists in target
153
+ const existingTeam = allTargetTeams.find(tt => tt.$id === team.$id);
154
+ if (!existingTeam) {
155
+ // Fetch all memberships to extract unique roles before creating team
156
+ MessageFormatter.info(`Fetching memberships for team ${team.name} to extract roles`, { prefix: "Transfer" });
157
+ const memberships = await this.fetchAllMemberships(team.$id);
158
+ // Extract unique roles from all memberships
159
+ const allRoles = new Set();
160
+ memberships.forEach(membership => {
161
+ membership.roles.forEach(role => allRoles.add(role));
162
+ });
163
+ const uniqueRoles = Array.from(allRoles);
164
+ MessageFormatter.info(`Found ${uniqueRoles.length} unique roles for team ${team.name}: ${uniqueRoles.join(', ')}`, { prefix: "Transfer" });
165
+ // Create team in target with the collected roles
166
+ await this.targetTeams.create(team.$id, team.name, uniqueRoles);
167
+ MessageFormatter.success(`Created team: ${team.name} with roles: ${uniqueRoles.join(', ')}`, { prefix: "Transfer" });
168
+ }
169
+ else {
170
+ MessageFormatter.info(`Team ${team.name} already exists, updating if needed`, { prefix: "Transfer" });
171
+ // Update team if needed
172
+ if (existingTeam.name !== team.name) {
173
+ await this.targetTeams.updateName(team.$id, team.name);
174
+ MessageFormatter.success(`Updated team name: ${team.name}`, { prefix: "Transfer" });
175
+ }
176
+ }
177
+ // Transfer team memberships
178
+ await this.transferTeamMemberships(team.$id);
179
+ this.results.teams.transferred++;
180
+ MessageFormatter.success(`Team ${team.name} transferred successfully`, { prefix: "Transfer" });
181
+ }
182
+ catch (error) {
183
+ MessageFormatter.error(`Team ${team.name} transfer failed`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
184
+ this.results.teams.failed++;
185
+ }
186
+ }));
187
+ await Promise.all(transferTasks);
188
+ MessageFormatter.success("Team transfer phase completed", { prefix: "Transfer" });
189
+ }
190
+ catch (error) {
191
+ MessageFormatter.error("Team transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
192
+ }
193
+ }
127
194
  async transferAllDatabases() {
128
195
  MessageFormatter.info("Starting database transfer phase", { prefix: "Transfer" });
129
196
  try {
@@ -680,16 +747,118 @@ export class ComprehensiveTransfer {
680
747
  }
681
748
  MessageFormatter.info(`Transferred ${totalTransferred} documents from ${sourceCollectionId} to ${targetCollectionId}`, { prefix: "Transfer" });
682
749
  }
750
+ /**
751
+ * Helper method to fetch all teams with pagination
752
+ */
753
+ async fetchAllTeams(teams) {
754
+ const teamsList = [];
755
+ let lastId;
756
+ while (true) {
757
+ const queries = [Query.limit(100)];
758
+ if (lastId) {
759
+ queries.push(Query.cursorAfter(lastId));
760
+ }
761
+ const result = await tryAwaitWithRetry(async () => teams.list(queries));
762
+ if (result.teams.length === 0) {
763
+ break;
764
+ }
765
+ teamsList.push(...result.teams);
766
+ if (result.teams.length < 100) {
767
+ break;
768
+ }
769
+ lastId = result.teams[result.teams.length - 1].$id;
770
+ }
771
+ return teamsList;
772
+ }
773
+ /**
774
+ * Helper method to fetch all memberships for a team with pagination
775
+ */
776
+ async fetchAllMemberships(teamId) {
777
+ const membershipsList = [];
778
+ let lastId;
779
+ while (true) {
780
+ const queries = [Query.limit(100)];
781
+ if (lastId) {
782
+ queries.push(Query.cursorAfter(lastId));
783
+ }
784
+ const result = await tryAwaitWithRetry(async () => this.sourceTeams.listMemberships(teamId, queries));
785
+ if (result.memberships.length === 0) {
786
+ break;
787
+ }
788
+ membershipsList.push(...result.memberships);
789
+ if (result.memberships.length < 100) {
790
+ break;
791
+ }
792
+ lastId = result.memberships[result.memberships.length - 1].$id;
793
+ }
794
+ return membershipsList;
795
+ }
796
+ /**
797
+ * Helper method to transfer team memberships
798
+ */
799
+ async transferTeamMemberships(teamId) {
800
+ MessageFormatter.info(`Transferring memberships for team ${teamId}`, { prefix: "Transfer" });
801
+ try {
802
+ // Fetch all memberships for this team
803
+ const memberships = await this.fetchAllMemberships(teamId);
804
+ if (memberships.length === 0) {
805
+ MessageFormatter.info(`No memberships found for team ${teamId}`, { prefix: "Transfer" });
806
+ return;
807
+ }
808
+ MessageFormatter.info(`Found ${memberships.length} memberships for team ${teamId}`, { prefix: "Transfer" });
809
+ let totalTransferred = 0;
810
+ // Transfer memberships with rate limiting
811
+ const transferTasks = memberships.map(membership => this.userLimit(async () => {
812
+ try {
813
+ // Check if membership already exists
814
+ try {
815
+ await this.targetTeams.getMembership(teamId, membership.$id);
816
+ MessageFormatter.info(`Membership ${membership.$id} already exists, skipping`, { prefix: "Transfer" });
817
+ return;
818
+ }
819
+ catch (error) {
820
+ // Membership doesn't exist, proceed with creation
821
+ }
822
+ // Get user data from target (users should already be transferred)
823
+ let userData = null;
824
+ try {
825
+ userData = await this.targetUsers.get(membership.userId);
826
+ }
827
+ catch (error) {
828
+ MessageFormatter.warning(`User ${membership.userId} not found in target, membership ${membership.$id} may fail`, { prefix: "Transfer" });
829
+ }
830
+ // Create membership using the comprehensive user data
831
+ await tryAwaitWithRetry(async () => this.targetTeams.createMembership(teamId, membership.roles, userData?.email || membership.userEmail, // Use target user email if available, fallback to membership email
832
+ membership.userId, // User ID
833
+ userData?.phone || undefined, // Use target user phone if available
834
+ undefined, // Invitation URL placeholder
835
+ userData?.name || membership.userName // Use target user name if available, fallback to membership name
836
+ ));
837
+ totalTransferred++;
838
+ MessageFormatter.success(`Transferred membership ${membership.$id} for user ${userData?.name || membership.userName}`, { prefix: "Transfer" });
839
+ }
840
+ catch (error) {
841
+ MessageFormatter.error(`Failed to transfer membership ${membership.$id}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
842
+ }
843
+ }));
844
+ await Promise.all(transferTasks);
845
+ MessageFormatter.info(`Transferred ${totalTransferred} memberships for team ${teamId}`, { prefix: "Transfer" });
846
+ }
847
+ catch (error) {
848
+ MessageFormatter.error(`Failed to transfer memberships for team ${teamId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
849
+ }
850
+ }
683
851
  printSummary() {
684
852
  const duration = Math.round((Date.now() - this.startTime) / 1000);
685
853
  MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", { prefix: "Transfer" });
686
854
  MessageFormatter.info(`Total Time: ${duration}s`, { prefix: "Transfer" });
687
855
  MessageFormatter.info(`Users: ${this.results.users.transferred} transferred, ${this.results.users.skipped} skipped, ${this.results.users.failed} failed`, { prefix: "Transfer" });
856
+ MessageFormatter.info(`Teams: ${this.results.teams.transferred} transferred, ${this.results.teams.skipped} skipped, ${this.results.teams.failed} failed`, { prefix: "Transfer" });
688
857
  MessageFormatter.info(`Databases: ${this.results.databases.transferred} transferred, ${this.results.databases.skipped} skipped, ${this.results.databases.failed} failed`, { prefix: "Transfer" });
689
858
  MessageFormatter.info(`Buckets: ${this.results.buckets.transferred} transferred, ${this.results.buckets.skipped} skipped, ${this.results.buckets.failed} failed`, { prefix: "Transfer" });
690
859
  MessageFormatter.info(`Functions: ${this.results.functions.transferred} transferred, ${this.results.functions.skipped} skipped, ${this.results.functions.failed} failed`, { prefix: "Transfer" });
691
- const totalTransferred = this.results.users.transferred + this.results.databases.transferred + this.results.buckets.transferred + this.results.functions.transferred;
692
- const totalFailed = this.results.users.failed + this.results.databases.failed + this.results.buckets.failed + this.results.functions.failed;
860
+ const totalTransferred = this.results.users.transferred + this.results.teams.transferred + this.results.databases.transferred + this.results.buckets.transferred + this.results.functions.transferred;
861
+ const totalFailed = this.results.users.failed + this.results.teams.failed + this.results.databases.failed + this.results.buckets.failed + this.results.functions.failed;
693
862
  if (totalFailed === 0) {
694
863
  MessageFormatter.success(`All ${totalTransferred} items transferred successfully!`, { prefix: "Transfer" });
695
864
  }
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": "1.2.5",
4
+ "version": "1.2.6",
5
5
  "main": "src/main.ts",
6
6
  "type": "module",
7
7
  "repository": {
@@ -2202,6 +2202,7 @@ export class InteractiveCLI {
2202
2202
  message: "Select what to transfer:",
2203
2203
  choices: [
2204
2204
  { name: "👥 Users", value: "users", checked: true },
2205
+ { name: "👥 Teams", value: "teams", checked: true },
2205
2206
  { name: "🗄️ Databases", value: "databases", checked: true },
2206
2207
  { name: "📦 Storage Buckets", value: "buckets", checked: true },
2207
2208
  { name: "⚡ Functions", value: "functions", checked: true },
@@ -2294,6 +2295,7 @@ export class InteractiveCLI {
2294
2295
  targetProject: targetConfig.targetProject,
2295
2296
  targetKey: targetConfig.targetKey,
2296
2297
  transferUsers: transferOptions.transferTypes.includes("users"),
2298
+ transferTeams: transferOptions.transferTypes.includes("teams"),
2297
2299
  transferDatabases: transferOptions.transferTypes.includes("databases"),
2298
2300
  transferBuckets: transferOptions.transferTypes.includes("buckets"),
2299
2301
  transferFunctions: transferOptions.transferTypes.includes("functions"),
@@ -2313,6 +2315,9 @@ export class InteractiveCLI {
2313
2315
  MessageFormatter.info("Users with preserved password hashes can log in with their original passwords", { prefix: "Transfer" });
2314
2316
  MessageFormatter.info("Users with temporary passwords will need to reset their passwords", { prefix: "Transfer" });
2315
2317
  }
2318
+ if (transferOptions.transferTypes.includes("teams") && results.teams.transferred > 0) {
2319
+ MessageFormatter.info("Team memberships have been transferred and may require user acceptance of invitations", { prefix: "Transfer" });
2320
+ }
2316
2321
  }
2317
2322
 
2318
2323
  } catch (error) {
@@ -5,6 +5,7 @@ import {
5
5
  Storage,
6
6
  Users,
7
7
  Functions,
8
+ Teams,
8
9
  type Models,
9
10
  Query,
10
11
  } from "node-appwrite";
@@ -36,6 +37,7 @@ export interface ComprehensiveTransferOptions {
36
37
  targetProject: string;
37
38
  targetKey: string;
38
39
  transferUsers?: boolean;
40
+ transferTeams?: boolean;
39
41
  transferDatabases?: boolean;
40
42
  transferBuckets?: boolean;
41
43
  transferFunctions?: boolean;
@@ -45,6 +47,7 @@ export interface ComprehensiveTransferOptions {
45
47
 
46
48
  export interface TransferResults {
47
49
  users: { transferred: number; skipped: number; failed: number };
50
+ teams: { transferred: number; skipped: number; failed: number };
48
51
  databases: { transferred: number; skipped: number; failed: number };
49
52
  buckets: { transferred: number; skipped: number; failed: number };
50
53
  functions: { transferred: number; skipped: number; failed: number };
@@ -56,6 +59,8 @@ export class ComprehensiveTransfer {
56
59
  private targetClient: Client;
57
60
  private sourceUsers: Users;
58
61
  private targetUsers: Users;
62
+ private sourceTeams: Teams;
63
+ private targetTeams: Teams;
59
64
  private sourceDatabases: Databases;
60
65
  private targetDatabases: Databases;
61
66
  private sourceStorage: Storage;
@@ -84,6 +89,8 @@ export class ComprehensiveTransfer {
84
89
 
85
90
  this.sourceUsers = new Users(this.sourceClient);
86
91
  this.targetUsers = new Users(this.targetClient);
92
+ this.sourceTeams = new Teams(this.sourceClient);
93
+ this.targetTeams = new Teams(this.targetClient);
87
94
  this.sourceDatabases = new Databases(this.sourceClient);
88
95
  this.targetDatabases = new Databases(this.targetClient);
89
96
  this.sourceStorage = new Storage(this.sourceClient);
@@ -101,6 +108,7 @@ export class ComprehensiveTransfer {
101
108
  this.fileLimit = pLimit(Math.max(1, Math.floor(baseLimit / 4)));
102
109
  this.results = {
103
110
  users: { transferred: 0, skipped: 0, failed: 0 },
111
+ teams: { transferred: 0, skipped: 0, failed: 0 },
104
112
  databases: { transferred: 0, skipped: 0, failed: 0 },
105
113
  buckets: { transferred: 0, skipped: 0, failed: 0 },
106
114
  functions: { transferred: 0, skipped: 0, failed: 0 },
@@ -135,6 +143,10 @@ export class ComprehensiveTransfer {
135
143
  await this.transferAllUsers();
136
144
  }
137
145
 
146
+ if (this.options.transferTeams !== false) {
147
+ await this.transferAllTeams();
148
+ }
149
+
138
150
  if (this.options.transferDatabases !== false) {
139
151
  await this.transferAllDatabases();
140
152
  }
@@ -193,6 +205,80 @@ export class ComprehensiveTransfer {
193
205
  }
194
206
  }
195
207
 
208
+ private async transferAllTeams(): Promise<void> {
209
+ MessageFormatter.info("Starting team transfer phase", { prefix: "Transfer" });
210
+
211
+ try {
212
+ // Fetch all teams from source with pagination
213
+ const allSourceTeams = await this.fetchAllTeams(this.sourceTeams);
214
+ const allTargetTeams = await this.fetchAllTeams(this.targetTeams);
215
+
216
+ if (this.options.dryRun) {
217
+ let totalMemberships = 0;
218
+ for (const team of allSourceTeams) {
219
+ const memberships = await this.sourceTeams.listMemberships(team.$id, [Query.limit(1)]);
220
+ totalMemberships += memberships.total;
221
+ }
222
+ MessageFormatter.info(`DRY RUN: Would transfer ${allSourceTeams.length} teams with ${totalMemberships} memberships`, { prefix: "Transfer" });
223
+ return;
224
+ }
225
+
226
+ const transferTasks = allSourceTeams.map(team =>
227
+ this.limit(async () => {
228
+ try {
229
+ // Check if team exists in target
230
+ const existingTeam = allTargetTeams.find(tt => tt.$id === team.$id);
231
+
232
+ if (!existingTeam) {
233
+ // Fetch all memberships to extract unique roles before creating team
234
+ MessageFormatter.info(`Fetching memberships for team ${team.name} to extract roles`, { prefix: "Transfer" });
235
+ const memberships = await this.fetchAllMemberships(team.$id);
236
+
237
+ // Extract unique roles from all memberships
238
+ const allRoles = new Set<string>();
239
+ memberships.forEach(membership => {
240
+ membership.roles.forEach(role => allRoles.add(role));
241
+ });
242
+ const uniqueRoles = Array.from(allRoles);
243
+
244
+ MessageFormatter.info(`Found ${uniqueRoles.length} unique roles for team ${team.name}: ${uniqueRoles.join(', ')}`, { prefix: "Transfer" });
245
+
246
+ // Create team in target with the collected roles
247
+ await this.targetTeams.create(
248
+ team.$id,
249
+ team.name,
250
+ uniqueRoles
251
+ );
252
+ MessageFormatter.success(`Created team: ${team.name} with roles: ${uniqueRoles.join(', ')}`, { prefix: "Transfer" });
253
+ } else {
254
+ MessageFormatter.info(`Team ${team.name} already exists, updating if needed`, { prefix: "Transfer" });
255
+
256
+ // Update team if needed
257
+ if (existingTeam.name !== team.name) {
258
+ await this.targetTeams.updateName(team.$id, team.name);
259
+ MessageFormatter.success(`Updated team name: ${team.name}`, { prefix: "Transfer" });
260
+ }
261
+ }
262
+
263
+ // Transfer team memberships
264
+ await this.transferTeamMemberships(team.$id);
265
+
266
+ this.results.teams.transferred++;
267
+ MessageFormatter.success(`Team ${team.name} transferred successfully`, { prefix: "Transfer" });
268
+ } catch (error) {
269
+ MessageFormatter.error(`Team ${team.name} transfer failed`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
270
+ this.results.teams.failed++;
271
+ }
272
+ })
273
+ );
274
+
275
+ await Promise.all(transferTasks);
276
+ MessageFormatter.success("Team transfer phase completed", { prefix: "Transfer" });
277
+ } catch (error) {
278
+ MessageFormatter.error("Team transfer phase failed", error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
279
+ }
280
+ }
281
+
196
282
  private async transferAllDatabases(): Promise<void> {
197
283
  MessageFormatter.info("Starting database transfer phase", { prefix: "Transfer" });
198
284
 
@@ -987,18 +1073,151 @@ export class ComprehensiveTransfer {
987
1073
  MessageFormatter.info(`Transferred ${totalTransferred} documents from ${sourceCollectionId} to ${targetCollectionId}`, { prefix: "Transfer" });
988
1074
  }
989
1075
 
1076
+ /**
1077
+ * Helper method to fetch all teams with pagination
1078
+ */
1079
+ private async fetchAllTeams(teams: Teams): Promise<Models.Team<Models.Preferences>[]> {
1080
+ const teamsList: Models.Team<Models.Preferences>[] = [];
1081
+ let lastId: string | undefined;
1082
+
1083
+ while (true) {
1084
+ const queries = [Query.limit(100)];
1085
+ if (lastId) {
1086
+ queries.push(Query.cursorAfter(lastId));
1087
+ }
1088
+
1089
+ const result = await tryAwaitWithRetry(async () => teams.list(queries));
1090
+
1091
+ if (result.teams.length === 0) {
1092
+ break;
1093
+ }
1094
+
1095
+ teamsList.push(...result.teams);
1096
+
1097
+ if (result.teams.length < 100) {
1098
+ break;
1099
+ }
1100
+
1101
+ lastId = result.teams[result.teams.length - 1].$id;
1102
+ }
1103
+
1104
+ return teamsList;
1105
+ }
1106
+
1107
+ /**
1108
+ * Helper method to fetch all memberships for a team with pagination
1109
+ */
1110
+ private async fetchAllMemberships(teamId: string): Promise<Models.Membership[]> {
1111
+ const membershipsList: Models.Membership[] = [];
1112
+ let lastId: string | undefined;
1113
+
1114
+ while (true) {
1115
+ const queries = [Query.limit(100)];
1116
+ if (lastId) {
1117
+ queries.push(Query.cursorAfter(lastId));
1118
+ }
1119
+
1120
+ const result = await tryAwaitWithRetry(async () =>
1121
+ this.sourceTeams.listMemberships(teamId, queries)
1122
+ );
1123
+
1124
+ if (result.memberships.length === 0) {
1125
+ break;
1126
+ }
1127
+
1128
+ membershipsList.push(...result.memberships);
1129
+
1130
+ if (result.memberships.length < 100) {
1131
+ break;
1132
+ }
1133
+
1134
+ lastId = result.memberships[result.memberships.length - 1].$id;
1135
+ }
1136
+
1137
+ return membershipsList;
1138
+ }
1139
+
1140
+ /**
1141
+ * Helper method to transfer team memberships
1142
+ */
1143
+ private async transferTeamMemberships(teamId: string): Promise<void> {
1144
+ MessageFormatter.info(`Transferring memberships for team ${teamId}`, { prefix: "Transfer" });
1145
+
1146
+ try {
1147
+ // Fetch all memberships for this team
1148
+ const memberships = await this.fetchAllMemberships(teamId);
1149
+
1150
+ if (memberships.length === 0) {
1151
+ MessageFormatter.info(`No memberships found for team ${teamId}`, { prefix: "Transfer" });
1152
+ return;
1153
+ }
1154
+
1155
+ MessageFormatter.info(`Found ${memberships.length} memberships for team ${teamId}`, { prefix: "Transfer" });
1156
+
1157
+ let totalTransferred = 0;
1158
+
1159
+ // Transfer memberships with rate limiting
1160
+ const transferTasks = memberships.map(membership =>
1161
+ this.userLimit(async () => { // Use userLimit for team operations (more sensitive)
1162
+ try {
1163
+ // Check if membership already exists
1164
+ try {
1165
+ await this.targetTeams.getMembership(teamId, membership.$id);
1166
+ MessageFormatter.info(`Membership ${membership.$id} already exists, skipping`, { prefix: "Transfer" });
1167
+ return;
1168
+ } catch (error) {
1169
+ // Membership doesn't exist, proceed with creation
1170
+ }
1171
+
1172
+ // Get user data from target (users should already be transferred)
1173
+ let userData: Models.User<Record<string, any>> | null = null;
1174
+ try {
1175
+ userData = await this.targetUsers.get(membership.userId);
1176
+ } catch (error) {
1177
+ MessageFormatter.warning(`User ${membership.userId} not found in target, membership ${membership.$id} may fail`, { prefix: "Transfer" });
1178
+ }
1179
+
1180
+ // Create membership using the comprehensive user data
1181
+ await tryAwaitWithRetry(async () =>
1182
+ this.targetTeams.createMembership(
1183
+ teamId,
1184
+ membership.roles,
1185
+ userData?.email || membership.userEmail, // Use target user email if available, fallback to membership email
1186
+ membership.userId, // User ID
1187
+ userData?.phone || undefined, // Use target user phone if available
1188
+ undefined, // Invitation URL placeholder
1189
+ userData?.name || membership.userName // Use target user name if available, fallback to membership name
1190
+ )
1191
+ );
1192
+
1193
+ totalTransferred++;
1194
+ MessageFormatter.success(`Transferred membership ${membership.$id} for user ${userData?.name || membership.userName}`, { prefix: "Transfer" });
1195
+ } catch (error) {
1196
+ MessageFormatter.error(`Failed to transfer membership ${membership.$id}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
1197
+ }
1198
+ })
1199
+ );
1200
+
1201
+ await Promise.all(transferTasks);
1202
+ MessageFormatter.info(`Transferred ${totalTransferred} memberships for team ${teamId}`, { prefix: "Transfer" });
1203
+ } catch (error) {
1204
+ MessageFormatter.error(`Failed to transfer memberships for team ${teamId}`, error instanceof Error ? error : new Error(String(error)), { prefix: "Transfer" });
1205
+ }
1206
+ }
1207
+
990
1208
  private printSummary(): void {
991
1209
  const duration = Math.round((Date.now() - this.startTime) / 1000);
992
1210
 
993
1211
  MessageFormatter.info("=== COMPREHENSIVE TRANSFER SUMMARY ===", { prefix: "Transfer" });
994
1212
  MessageFormatter.info(`Total Time: ${duration}s`, { prefix: "Transfer" });
995
1213
  MessageFormatter.info(`Users: ${this.results.users.transferred} transferred, ${this.results.users.skipped} skipped, ${this.results.users.failed} failed`, { prefix: "Transfer" });
1214
+ MessageFormatter.info(`Teams: ${this.results.teams.transferred} transferred, ${this.results.teams.skipped} skipped, ${this.results.teams.failed} failed`, { prefix: "Transfer" });
996
1215
  MessageFormatter.info(`Databases: ${this.results.databases.transferred} transferred, ${this.results.databases.skipped} skipped, ${this.results.databases.failed} failed`, { prefix: "Transfer" });
997
1216
  MessageFormatter.info(`Buckets: ${this.results.buckets.transferred} transferred, ${this.results.buckets.skipped} skipped, ${this.results.buckets.failed} failed`, { prefix: "Transfer" });
998
1217
  MessageFormatter.info(`Functions: ${this.results.functions.transferred} transferred, ${this.results.functions.skipped} skipped, ${this.results.functions.failed} failed`, { prefix: "Transfer" });
999
1218
 
1000
- const totalTransferred = this.results.users.transferred + this.results.databases.transferred + this.results.buckets.transferred + this.results.functions.transferred;
1001
- const totalFailed = this.results.users.failed + this.results.databases.failed + this.results.buckets.failed + this.results.functions.failed;
1219
+ const totalTransferred = this.results.users.transferred + this.results.teams.transferred + this.results.databases.transferred + this.results.buckets.transferred + this.results.functions.transferred;
1220
+ const totalFailed = this.results.users.failed + this.results.teams.failed + this.results.databases.failed + this.results.buckets.failed + this.results.functions.failed;
1002
1221
 
1003
1222
  if (totalFailed === 0) {
1004
1223
  MessageFormatter.success(`All ${totalTransferred} items transferred successfully!`, { prefix: "Transfer" });