appwrite-utils-cli 1.2.4 → 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.
- package/dist/config/yamlConfig.d.ts +5 -5
- package/dist/interactiveCLI.js +5 -0
- package/dist/migrations/comprehensiveTransfer.d.ts +21 -0
- package/dist/migrations/comprehensiveTransfer.js +172 -3
- package/package.json +2 -2
- package/src/interactiveCLI.ts +5 -0
- package/src/migrations/comprehensiveTransfer.ts +221 -2
@@ -235,7 +235,7 @@ declare const YamlConfigSchema: z.ZodObject<{
|
|
235
235
|
functions: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodObject<{
|
236
236
|
id: z.ZodString;
|
237
237
|
name: z.ZodString;
|
238
|
-
runtime: z.ZodEnum<["node-14.5", "node-16.0", "node-18.0", "node-19.0", "node-20.0", "node-21.0", "
|
238
|
+
runtime: z.ZodEnum<["node-14.5", "node-16.0", "node-18.0", "node-19.0", "node-20.0", "node-21.0", "node-22", "bun-1.0", "bun-1.1", "deno-1.21", "deno-1.24", "deno-1.35", "deno-1.40", "deno-1.46", "deno-2.0", "go-1.23", "python-3.8", "python-3.9", "python-3.10", "python-3.11", "python-3.12", "python-ml-3.11", "dart-2.15", "dart-2.16", "dart-2.17", "dart-2.18", "dart-3.0", "dart-3.1", "dart-3.3", "dart-3.5", "php-8.0", "php-8.1", "php-8.2", "php-8.3", "ruby-3.0", "ruby-3.1", "ruby-3.2", "ruby-3.3", "dotnet-6.0", "dotnet-7.0", "dotnet-8.0", "java-8.0", "java-11.0", "java-17.0", "java-18.0", "java-21.0", "java-22", "swift-5.5", "swift-5.8", "swift-5.9", "swift-5.10", "kotlin-1.6", "kotlin-1.8", "kotlin-1.9", "kotlin-2.0", "cpp-17", "cpp-20"]>;
|
239
239
|
execute: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
240
240
|
events: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
241
241
|
schedule: z.ZodOptional<z.ZodString>;
|
@@ -258,7 +258,7 @@ declare const YamlConfigSchema: z.ZodObject<{
|
|
258
258
|
}, "strip", z.ZodTypeAny, {
|
259
259
|
name: string;
|
260
260
|
id: string;
|
261
|
-
runtime: "node-14.5" | "node-16.0" | "node-18.0" | "node-19.0" | "node-20.0" | "node-21.0" | "
|
261
|
+
runtime: "node-14.5" | "node-16.0" | "node-18.0" | "node-19.0" | "node-20.0" | "node-21.0" | "node-22" | "bun-1.0" | "bun-1.1" | "deno-1.21" | "deno-1.24" | "deno-1.35" | "deno-1.40" | "deno-1.46" | "deno-2.0" | "go-1.23" | "python-3.8" | "python-3.9" | "python-3.10" | "python-3.11" | "python-3.12" | "python-ml-3.11" | "dart-2.15" | "dart-2.16" | "dart-2.17" | "dart-2.18" | "dart-3.0" | "dart-3.1" | "dart-3.3" | "dart-3.5" | "php-8.0" | "php-8.1" | "php-8.2" | "php-8.3" | "ruby-3.0" | "ruby-3.1" | "ruby-3.2" | "ruby-3.3" | "dotnet-6.0" | "dotnet-7.0" | "dotnet-8.0" | "java-8.0" | "java-11.0" | "java-17.0" | "java-18.0" | "java-21.0" | "java-22" | "swift-5.5" | "swift-5.8" | "swift-5.9" | "swift-5.10" | "kotlin-1.6" | "kotlin-1.8" | "kotlin-1.9" | "kotlin-2.0" | "cpp-17" | "cpp-20";
|
262
262
|
enabled?: boolean | undefined;
|
263
263
|
logging?: boolean | undefined;
|
264
264
|
execute?: string[] | undefined;
|
@@ -281,7 +281,7 @@ declare const YamlConfigSchema: z.ZodObject<{
|
|
281
281
|
}, {
|
282
282
|
name: string;
|
283
283
|
id: string;
|
284
|
-
runtime: "node-14.5" | "node-16.0" | "node-18.0" | "node-19.0" | "node-20.0" | "node-21.0" | "
|
284
|
+
runtime: "node-14.5" | "node-16.0" | "node-18.0" | "node-19.0" | "node-20.0" | "node-21.0" | "node-22" | "bun-1.0" | "bun-1.1" | "deno-1.21" | "deno-1.24" | "deno-1.35" | "deno-1.40" | "deno-1.46" | "deno-2.0" | "go-1.23" | "python-3.8" | "python-3.9" | "python-3.10" | "python-3.11" | "python-3.12" | "python-ml-3.11" | "dart-2.15" | "dart-2.16" | "dart-2.17" | "dart-2.18" | "dart-3.0" | "dart-3.1" | "dart-3.3" | "dart-3.5" | "php-8.0" | "php-8.1" | "php-8.2" | "php-8.3" | "ruby-3.0" | "ruby-3.1" | "ruby-3.2" | "ruby-3.3" | "dotnet-6.0" | "dotnet-7.0" | "dotnet-8.0" | "java-8.0" | "java-11.0" | "java-17.0" | "java-18.0" | "java-21.0" | "java-22" | "swift-5.5" | "swift-5.8" | "swift-5.9" | "swift-5.10" | "kotlin-1.6" | "kotlin-1.8" | "kotlin-1.9" | "kotlin-2.0" | "cpp-17" | "cpp-20";
|
285
285
|
enabled?: boolean | undefined;
|
286
286
|
logging?: boolean | undefined;
|
287
287
|
execute?: string[] | undefined;
|
@@ -376,7 +376,7 @@ declare const YamlConfigSchema: z.ZodObject<{
|
|
376
376
|
functions: {
|
377
377
|
name: string;
|
378
378
|
id: string;
|
379
|
-
runtime: "node-14.5" | "node-16.0" | "node-18.0" | "node-19.0" | "node-20.0" | "node-21.0" | "
|
379
|
+
runtime: "node-14.5" | "node-16.0" | "node-18.0" | "node-19.0" | "node-20.0" | "node-21.0" | "node-22" | "bun-1.0" | "bun-1.1" | "deno-1.21" | "deno-1.24" | "deno-1.35" | "deno-1.40" | "deno-1.46" | "deno-2.0" | "go-1.23" | "python-3.8" | "python-3.9" | "python-3.10" | "python-3.11" | "python-3.12" | "python-ml-3.11" | "dart-2.15" | "dart-2.16" | "dart-2.17" | "dart-2.18" | "dart-3.0" | "dart-3.1" | "dart-3.3" | "dart-3.5" | "php-8.0" | "php-8.1" | "php-8.2" | "php-8.3" | "ruby-3.0" | "ruby-3.1" | "ruby-3.2" | "ruby-3.3" | "dotnet-6.0" | "dotnet-7.0" | "dotnet-8.0" | "java-8.0" | "java-11.0" | "java-17.0" | "java-18.0" | "java-21.0" | "java-22" | "swift-5.5" | "swift-5.8" | "swift-5.9" | "swift-5.10" | "kotlin-1.6" | "kotlin-1.8" | "kotlin-1.9" | "kotlin-2.0" | "cpp-17" | "cpp-20";
|
380
380
|
enabled?: boolean | undefined;
|
381
381
|
logging?: boolean | undefined;
|
382
382
|
execute?: string[] | undefined;
|
@@ -465,7 +465,7 @@ declare const YamlConfigSchema: z.ZodObject<{
|
|
465
465
|
functions?: {
|
466
466
|
name: string;
|
467
467
|
id: string;
|
468
|
-
runtime: "node-14.5" | "node-16.0" | "node-18.0" | "node-19.0" | "node-20.0" | "node-21.0" | "
|
468
|
+
runtime: "node-14.5" | "node-16.0" | "node-18.0" | "node-19.0" | "node-20.0" | "node-21.0" | "node-22" | "bun-1.0" | "bun-1.1" | "deno-1.21" | "deno-1.24" | "deno-1.35" | "deno-1.40" | "deno-1.46" | "deno-2.0" | "go-1.23" | "python-3.8" | "python-3.9" | "python-3.10" | "python-3.11" | "python-3.12" | "python-ml-3.11" | "dart-2.15" | "dart-2.16" | "dart-2.17" | "dart-2.18" | "dart-3.0" | "dart-3.1" | "dart-3.3" | "dart-3.5" | "php-8.0" | "php-8.1" | "php-8.2" | "php-8.3" | "ruby-3.0" | "ruby-3.1" | "ruby-3.2" | "ruby-3.3" | "dotnet-6.0" | "dotnet-7.0" | "dotnet-8.0" | "java-8.0" | "java-11.0" | "java-17.0" | "java-18.0" | "java-21.0" | "java-22" | "swift-5.5" | "swift-5.8" | "swift-5.9" | "swift-5.10" | "kotlin-1.6" | "kotlin-1.8" | "kotlin-1.9" | "kotlin-2.0" | "cpp-17" | "cpp-20";
|
469
469
|
enabled?: boolean | undefined;
|
470
470
|
logging?: boolean | undefined;
|
471
471
|
execute?: string[] | undefined;
|
package/dist/interactiveCLI.js
CHANGED
@@ -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.
|
4
|
+
"version": "1.2.6",
|
5
5
|
"main": "src/main.ts",
|
6
6
|
"type": "module",
|
7
7
|
"repository": {
|
@@ -33,7 +33,7 @@
|
|
33
33
|
"@types/inquirer": "^9.0.8",
|
34
34
|
"@types/json-schema": "^7.0.15",
|
35
35
|
"@types/yargs": "^17.0.33",
|
36
|
-
"appwrite-utils": "^1.
|
36
|
+
"appwrite-utils": "^1.2.0",
|
37
37
|
"chalk": "^5.4.1",
|
38
38
|
"cli-progress": "^3.12.0",
|
39
39
|
"commander": "^12.1.0",
|
package/src/interactiveCLI.ts
CHANGED
@@ -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" });
|