appwrite-utils-cli 0.9.990 → 0.9.992
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -0
- package/dist/functions/methods.d.ts +10 -0
- package/dist/functions/methods.js +79 -0
- package/dist/interactiveCLI.d.ts +1 -0
- package/dist/interactiveCLI.js +46 -2
- package/dist/main.js +54 -7
- package/dist/migrations/transfer.js +7 -1
- package/dist/utilsController.d.ts +2 -1
- package/dist/utilsController.js +11 -1
- package/package.json +3 -2
- package/src/functions/methods.ts +235 -0
- package/src/interactiveCLI.ts +49 -2
- package/src/main.ts +62 -10
- package/src/migrations/transfer.ts +8 -3
- package/src/utilsController.ts +12 -2
package/README.md
CHANGED
@@ -82,6 +82,11 @@ Available options:
|
|
82
82
|
- `--remoteProjectId`: Set the remote Appwrite project ID for transfers
|
83
83
|
- `--remoteApiKey`: Set the remote Appwrite API key for transfers
|
84
84
|
- `--setup`: Create setup files
|
85
|
+
- `--updateFunctionSpec`: Update function specifications
|
86
|
+
- `--functionId`: Function ID to update
|
87
|
+
- `--specification`: New function specification (one of: s-0.5vcpu-512mb, s-1vcpu-1gb, s-2vcpu-2gb, s-2vcpu-4gb, s-4vcpu-4gb, s-4vcpu-8gb, s-8vcpu-4gb, s-8vcpu-8gb)
|
88
|
+
|
89
|
+
|
85
90
|
|
86
91
|
## Examples
|
87
92
|
|
@@ -109,6 +114,25 @@ Transfer files between buckets:
|
|
109
114
|
npx appwrite-utils-cli appwrite-migrate --transfer --fromBucketId sourceBucketId --toBucketId targetBucketId --remoteEndpoint https://appwrite.otherserver.com --remoteProjectId yourProjectId --remoteApiKey yourApiKey
|
110
115
|
```
|
111
116
|
|
117
|
+
### Update Function Specifications
|
118
|
+
|
119
|
+
Update the CPU and RAM specifications for a function:
|
120
|
+
|
121
|
+
```bash
|
122
|
+
npx appwrite-utils-cli appwrite-migrate --updateFunctionSpec --functionId yourFunctionId --specification s-1vcpu-1gb
|
123
|
+
```
|
124
|
+
|
125
|
+
Available specifications:
|
126
|
+
|
127
|
+
- s-0.5vcpu-512mb: 0.5 vCPU, 512MB RAM
|
128
|
+
- s-1vcpu-1gb: 1 vCPU, 1GB RAM
|
129
|
+
- s-2vcpu-2gb: 2 vCPU, 2GB RAM
|
130
|
+
- s-2vcpu-4gb: 2 vCPU, 4GB RAM
|
131
|
+
- s-4vcpu-4gb: 4 vCPU, 4GB RAM
|
132
|
+
- s-4vcpu-8gb: 4 vCPU, 8GB RAM
|
133
|
+
- s-8vcpu-4gb: 8 vCPU, 4GB RAM
|
134
|
+
- s-8vcpu-8gb: 8 vCPU, 8GB RAM
|
135
|
+
|
112
136
|
## Additional Notes
|
113
137
|
|
114
138
|
- If you run out of RAM during large data imports, you can increase Node's memory allocation:
|
@@ -125,6 +149,7 @@ This updated CLI ensures that developers have robust tools at their fingertips t
|
|
125
149
|
|
126
150
|
## Changelog
|
127
151
|
|
152
|
+
- 0.9.992: Added `updateFunctionSpecifications` which lists functions and specifications to allow you to update your functions max CPU and RAM usage per-function
|
128
153
|
- 0.9.990: Fixed `transferFilesLocalToLocal` and `remote` if a document exists with that `$id`, also fixed wipe `"all"` option also wiping the associated buckets
|
129
154
|
- 0.9.983: Fixed `afterImportActions` not resolving
|
130
155
|
- 0.9.981: Try fixing `tryAwaitWithRetry` to catch `522` errors from Cloudflare, they were appearing for some users, also added a 1000ms delay to `tryAwaitWithRetry`
|
@@ -0,0 +1,10 @@
|
|
1
|
+
import { Client, Runtime, type Models } from "node-appwrite";
|
2
|
+
import { type Specification } from "appwrite-utils";
|
3
|
+
export declare const listFunctions: (client: Client, queries?: string[], search?: string) => Promise<Models.FunctionList>;
|
4
|
+
export declare const getFunction: (client: Client, functionId: string) => Promise<Models.Function>;
|
5
|
+
export declare const deleteFunction: (client: Client, functionId: string) => Promise<{}>;
|
6
|
+
export declare const createFunction: (client: Client, functionId: string, name: string, runtime: Runtime, execute?: string[], events?: string[], schedule?: string, timeout?: number, enabled?: boolean, logging?: boolean, entrypoint?: string, commands?: string, scopes?: string[], installationId?: string, providerRepositoryId?: string, providerBranch?: string, providerSilentMode?: boolean, providerRootDirectory?: string, templateRepository?: string, templateOwner?: string, templateRootDirectory?: string, templateVersion?: string, specification?: string) => Promise<Models.Function>;
|
7
|
+
export declare const updateFunctionSpecifications: (client: Client, functionId: string, specification: Specification) => Promise<Models.Function | undefined>;
|
8
|
+
export declare const listSpecifications: (client: Client) => Promise<Models.SpecificationList>;
|
9
|
+
export declare const updateFunction: (client: Client, functionId: string, name: string, runtime?: Runtime, execute?: string[], events?: string[], schedule?: string, timeout?: number, enabled?: boolean, logging?: boolean, entrypoint?: string, commands?: string, scopes?: string[], installationId?: string, providerRepositoryId?: string, providerBranch?: string, providerSilentMode?: boolean, providerRootDirectory?: string, specification?: Specification) => Promise<Models.Function>;
|
10
|
+
export declare const deployFunction: (client: Client, functionId: string, codePath: string, activate?: boolean, entrypoint?: string, commands?: string) => Promise<Models.Deployment>;
|
@@ -0,0 +1,79 @@
|
|
1
|
+
import { AppwriteException, Client, Functions, Query, Runtime, } from "node-appwrite";
|
2
|
+
import { InputFile } from "node-appwrite/file";
|
3
|
+
import { create as createTarball } from "tar";
|
4
|
+
import { join } from "node:path";
|
5
|
+
import fs from "node:fs";
|
6
|
+
import {} from "appwrite-utils";
|
7
|
+
import chalk from "chalk";
|
8
|
+
export const listFunctions = async (client, queries, search) => {
|
9
|
+
const functions = new Functions(client);
|
10
|
+
const functionsList = await functions.list(queries, search);
|
11
|
+
return functionsList;
|
12
|
+
};
|
13
|
+
export const getFunction = async (client, functionId) => {
|
14
|
+
const functions = new Functions(client);
|
15
|
+
const functionResponse = await functions.get(functionId);
|
16
|
+
return functionResponse;
|
17
|
+
};
|
18
|
+
export const deleteFunction = async (client, functionId) => {
|
19
|
+
const functions = new Functions(client);
|
20
|
+
const functionResponse = await functions.delete(functionId);
|
21
|
+
return functionResponse;
|
22
|
+
};
|
23
|
+
export const createFunction = async (client, functionId, name, runtime, execute, events, schedule, timeout, enabled, logging, entrypoint, commands, scopes, installationId, providerRepositoryId, providerBranch, providerSilentMode, providerRootDirectory, templateRepository, templateOwner, templateRootDirectory, templateVersion, specification) => {
|
24
|
+
const functions = new Functions(client);
|
25
|
+
const functionResponse = await functions.create(functionId, name, runtime, execute, events, schedule, timeout, enabled, logging, entrypoint, commands, scopes, installationId, providerRepositoryId, providerBranch, providerSilentMode, providerRootDirectory, templateRepository, templateOwner, templateRootDirectory, templateVersion, specification);
|
26
|
+
return functionResponse;
|
27
|
+
};
|
28
|
+
export const updateFunctionSpecifications = async (client, functionId, specification) => {
|
29
|
+
const curFunction = await listFunctions(client, [
|
30
|
+
Query.equal("$id", functionId),
|
31
|
+
]);
|
32
|
+
if (curFunction.functions.length === 0) {
|
33
|
+
throw new Error("Function not found");
|
34
|
+
}
|
35
|
+
const functionFound = curFunction.functions[0];
|
36
|
+
try {
|
37
|
+
const functionResponse = await updateFunction(client, functionId, functionFound.name, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, specification);
|
38
|
+
return functionResponse;
|
39
|
+
}
|
40
|
+
catch (error) {
|
41
|
+
if (error instanceof AppwriteException &&
|
42
|
+
error.message.includes("Invalid `specification`")) {
|
43
|
+
console.error(chalk.red("Error updating function specifications, please try setting the env variable `_FUNCTIONS_CPUS` and `_FUNCTIONS_RAM` to non-zero values"));
|
44
|
+
}
|
45
|
+
else {
|
46
|
+
console.error(chalk.red("Error updating function specifications."));
|
47
|
+
throw error;
|
48
|
+
}
|
49
|
+
}
|
50
|
+
};
|
51
|
+
export const listSpecifications = async (client) => {
|
52
|
+
const functions = new Functions(client);
|
53
|
+
const specifications = await functions.listSpecifications();
|
54
|
+
return specifications;
|
55
|
+
};
|
56
|
+
export const updateFunction = async (client, functionId, name, runtime, execute, events, schedule, timeout, enabled, logging, entrypoint, commands, scopes, installationId, providerRepositoryId, providerBranch, providerSilentMode, providerRootDirectory, specification) => {
|
57
|
+
const functions = new Functions(client);
|
58
|
+
const functionResponse = await functions.update(functionId, name, runtime, execute, events, schedule, timeout, enabled, logging, entrypoint, commands, scopes, installationId, providerRepositoryId, providerBranch, providerSilentMode, providerRootDirectory, specification);
|
59
|
+
return functionResponse;
|
60
|
+
};
|
61
|
+
export const deployFunction = async (client, functionId, codePath, activate = true, entrypoint = "index.js", commands = "npm install") => {
|
62
|
+
const functions = new Functions(client);
|
63
|
+
// Create temporary tar.gz file
|
64
|
+
const tarPath = join(process.cwd(), `function-${functionId}.tar.gz`);
|
65
|
+
await createTarball({
|
66
|
+
gzip: true,
|
67
|
+
file: tarPath,
|
68
|
+
cwd: codePath,
|
69
|
+
}, ["."] // Include all files from codePath
|
70
|
+
);
|
71
|
+
// Read the tar.gz file and create a File object
|
72
|
+
const fileBuffer = await fs.promises.readFile(tarPath);
|
73
|
+
const fileObject = InputFile.fromBuffer(new Uint8Array(fileBuffer), `function-${functionId}.tar.gz`);
|
74
|
+
// Create deployment with the File object
|
75
|
+
const functionResponse = await functions.createDeployment(functionId, fileObject, activate, entrypoint, commands);
|
76
|
+
// Clean up the temporary tar file
|
77
|
+
await fs.promises.unlink(tarPath);
|
78
|
+
return functionResponse;
|
79
|
+
};
|
package/dist/interactiveCLI.d.ts
CHANGED
package/dist/interactiveCLI.js
CHANGED
@@ -10,6 +10,7 @@ import { parseAttribute, PermissionToAppwritePermission, } from "appwrite-utils"
|
|
10
10
|
import { ulid } from "ulidx";
|
11
11
|
import chalk from "chalk";
|
12
12
|
import { DateTime } from "luxon";
|
13
|
+
import { listFunctions, listSpecifications } from "./functions/methods.js";
|
13
14
|
var CHOICES;
|
14
15
|
(function (CHOICES) {
|
15
16
|
CHOICES["CREATE_COLLECTION_CONFIG"] = "Create collection config file";
|
@@ -24,6 +25,7 @@ var CHOICES;
|
|
24
25
|
CHOICES["GENERATE_SCHEMAS"] = "Generate schemas";
|
25
26
|
CHOICES["IMPORT_DATA"] = "Import data";
|
26
27
|
CHOICES["RELOAD_CONFIG"] = "Reload configuration files";
|
28
|
+
CHOICES["UPDATE_FUNCTION_SPEC"] = "Update function specifications";
|
27
29
|
CHOICES["EXIT"] = "Exit";
|
28
30
|
})(CHOICES || (CHOICES = {}));
|
29
31
|
export class InteractiveCLI {
|
@@ -91,6 +93,10 @@ export class InteractiveCLI {
|
|
91
93
|
await this.initControllerIfNeeded();
|
92
94
|
await this.reloadConfig();
|
93
95
|
break;
|
96
|
+
case CHOICES.UPDATE_FUNCTION_SPEC:
|
97
|
+
await this.initControllerIfNeeded();
|
98
|
+
await this.updateFunctionSpec();
|
99
|
+
break;
|
94
100
|
case CHOICES.EXIT:
|
95
101
|
console.log(chalk.green("Goodbye!"));
|
96
102
|
return;
|
@@ -141,7 +147,7 @@ export class InteractiveCLI {
|
|
141
147
|
name: "selectedDatabases",
|
142
148
|
message: chalk.blue(message),
|
143
149
|
choices,
|
144
|
-
loop:
|
150
|
+
loop: true,
|
145
151
|
pageSize: 10,
|
146
152
|
},
|
147
153
|
]);
|
@@ -190,7 +196,7 @@ export class InteractiveCLI {
|
|
190
196
|
name: "selectedCollections",
|
191
197
|
message: chalk.blue(message),
|
192
198
|
choices,
|
193
|
-
loop:
|
199
|
+
loop: true,
|
194
200
|
pageSize: 10,
|
195
201
|
},
|
196
202
|
]);
|
@@ -689,4 +695,42 @@ export class InteractiveCLI {
|
|
689
695
|
console.error(chalk.red("Error reloading configuration files:"), error);
|
690
696
|
}
|
691
697
|
}
|
698
|
+
async updateFunctionSpec() {
|
699
|
+
const functions = await listFunctions(this.controller.appwriteServer, [
|
700
|
+
Query.limit(1000),
|
701
|
+
]);
|
702
|
+
const functionsToUpdate = await inquirer.prompt([
|
703
|
+
{
|
704
|
+
type: 'checkbox',
|
705
|
+
name: 'functionId',
|
706
|
+
message: 'Select functions to update:',
|
707
|
+
choices: functions.functions.map(f => ({
|
708
|
+
name: `${f.name} (${f.$id})`,
|
709
|
+
value: f.$id
|
710
|
+
})),
|
711
|
+
loop: true,
|
712
|
+
}
|
713
|
+
]);
|
714
|
+
const specifications = await listSpecifications(this.controller.appwriteServer);
|
715
|
+
const { specification } = await inquirer.prompt([
|
716
|
+
{
|
717
|
+
type: 'list',
|
718
|
+
name: 'specification',
|
719
|
+
message: 'Select new specification:',
|
720
|
+
choices: specifications.specifications.map((s) => ({
|
721
|
+
name: `${s.slug}`,
|
722
|
+
value: s.slug
|
723
|
+
})),
|
724
|
+
}
|
725
|
+
]);
|
726
|
+
try {
|
727
|
+
for (const functionId of functionsToUpdate.functionId) {
|
728
|
+
await this.controller.updateFunctionSpecifications(functionId, specification);
|
729
|
+
console.log(chalk.green(`Successfully updated function specification to ${specification}`));
|
730
|
+
}
|
731
|
+
}
|
732
|
+
catch (error) {
|
733
|
+
console.error(chalk.red('Error updating function specification:'), error);
|
734
|
+
}
|
735
|
+
}
|
692
736
|
}
|
package/dist/main.js
CHANGED
@@ -9,6 +9,8 @@ import { getClient } from "./utils/getClientFromConfig.js";
|
|
9
9
|
import { fetchAllDatabases } from "./migrations/databases.js";
|
10
10
|
import { setupDirsFiles } from "./utils/setupFiles.js";
|
11
11
|
import { fetchAllCollections } from "./collections/methods.js";
|
12
|
+
import chalk from "chalk";
|
13
|
+
import { listSpecifications } from "./functions/methods.js";
|
12
14
|
const argv = yargs(hideBin(process.argv))
|
13
15
|
.option("it", {
|
14
16
|
alias: ["interactive", "i"],
|
@@ -119,6 +121,28 @@ const argv = yargs(hideBin(process.argv))
|
|
119
121
|
.option("setup", {
|
120
122
|
type: "boolean",
|
121
123
|
description: "Setup directories and files",
|
124
|
+
})
|
125
|
+
.option("updateFunctionSpec", {
|
126
|
+
type: "boolean",
|
127
|
+
description: "Update function specifications",
|
128
|
+
})
|
129
|
+
.option("functionId", {
|
130
|
+
type: "string",
|
131
|
+
description: "Function ID to update",
|
132
|
+
})
|
133
|
+
.option("specification", {
|
134
|
+
type: "string",
|
135
|
+
description: "New function specification (e.g., 's-1vcpu-1gb')",
|
136
|
+
choices: [
|
137
|
+
"s-0.5vcpu-512mb",
|
138
|
+
"s-1vcpu-1gb",
|
139
|
+
"s-2vcpu-2gb",
|
140
|
+
"s-2vcpu-4gb",
|
141
|
+
"s-4vcpu-4gb",
|
142
|
+
"s-4vcpu-8gb",
|
143
|
+
"s-8vcpu-4gb",
|
144
|
+
"s-8vcpu-8gb"
|
145
|
+
],
|
122
146
|
})
|
123
147
|
.parse();
|
124
148
|
async function main() {
|
@@ -148,6 +172,18 @@ async function main() {
|
|
148
172
|
shouldWriteFile: parsedArgv.writeData,
|
149
173
|
wipeCollections: parsedArgv.wipeCollections,
|
150
174
|
};
|
175
|
+
if (parsedArgv.updateFunctionSpec) {
|
176
|
+
if (!parsedArgv.functionId || !parsedArgv.specification) {
|
177
|
+
throw new Error("Function ID and specification are required for updating function specs");
|
178
|
+
}
|
179
|
+
console.log(chalk.yellow(`Updating function specification for ${parsedArgv.functionId} to ${parsedArgv.specification}, checking if specification exists...`));
|
180
|
+
const specifications = await listSpecifications(controller.appwriteServer);
|
181
|
+
if (!specifications.specifications.some((s) => s.slug === parsedArgv.specification)) {
|
182
|
+
console.log(chalk.red(`Specification ${parsedArgv.specification} not found`));
|
183
|
+
return;
|
184
|
+
}
|
185
|
+
await controller.updateFunctionSpecifications(parsedArgv.functionId, parsedArgv.specification);
|
186
|
+
}
|
151
187
|
// Add default databases if not specified
|
152
188
|
if (!options.databases || options.databases.length === 0) {
|
153
189
|
const allDatabases = await fetchAllDatabases(controller.database);
|
@@ -171,19 +207,30 @@ async function main() {
|
|
171
207
|
options.wipeDocumentStorage ||
|
172
208
|
options.wipeUsers ||
|
173
209
|
options.wipeCollections) {
|
174
|
-
if (
|
175
|
-
|
176
|
-
|
210
|
+
if (parsedArgv.wipe === "all") {
|
211
|
+
if (options.databases) {
|
212
|
+
for (const db of options.databases) {
|
213
|
+
await controller.wipeDatabase(db, true); // true to wipe associated buckets
|
214
|
+
}
|
177
215
|
}
|
216
|
+
await controller.wipeUsers();
|
178
217
|
}
|
179
|
-
if (
|
180
|
-
|
181
|
-
|
218
|
+
else if (parsedArgv.wipe === "docs") {
|
219
|
+
if (options.databases) {
|
220
|
+
for (const db of options.databases) {
|
221
|
+
await controller.wipeBucketFromDatabase(db);
|
222
|
+
}
|
223
|
+
}
|
224
|
+
if (parsedArgv.bucketIds) {
|
225
|
+
for (const bucketId of parsedArgv.bucketIds.split(",")) {
|
226
|
+
await controller.wipeDocumentStorage(bucketId);
|
227
|
+
}
|
182
228
|
}
|
183
229
|
}
|
184
|
-
if (
|
230
|
+
else if (parsedArgv.wipe === "users") {
|
185
231
|
await controller.wipeUsers();
|
186
232
|
}
|
233
|
+
// Handle specific collection wipes
|
187
234
|
if (options.wipeCollections && options.databases) {
|
188
235
|
for (const db of options.databases) {
|
189
236
|
const dbCollections = await fetchAllCollections(db.$id, controller.database);
|
@@ -65,7 +65,13 @@ export const transferStorageLocalToLocal = async (storage, fromBucketId, toBucke
|
|
65
65
|
continue;
|
66
66
|
}
|
67
67
|
const fileToCreate = InputFile.fromBuffer(new Uint8Array(fileData), file.name);
|
68
|
-
|
68
|
+
try {
|
69
|
+
await tryAwaitWithRetry(async () => await storage.createFile(toBucketId, file.$id, fileToCreate, file.$permissions));
|
70
|
+
}
|
71
|
+
catch (error) {
|
72
|
+
// File already exists, so we can skip it
|
73
|
+
continue;
|
74
|
+
}
|
69
75
|
numberOfFiles++;
|
70
76
|
}
|
71
77
|
}
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { Client, Databases, Storage, type Models } from "node-appwrite";
|
2
|
-
import { type AppwriteConfig } from "appwrite-utils";
|
2
|
+
import { type AppwriteConfig, type Specification } from "appwrite-utils";
|
3
3
|
import { type AfterImportActions, type ConverterFunctions, type ValidationRules } from "appwrite-utils";
|
4
4
|
import { type TransferOptions } from "./migrations/transfer.js";
|
5
5
|
export interface SetupOptions {
|
@@ -51,4 +51,5 @@ export declare class UtilsController {
|
|
51
51
|
syncDb(databases?: Models.Database[], collections?: Models.Collection[]): Promise<void>;
|
52
52
|
getAppwriteFolderPath(): string;
|
53
53
|
transferData(options: TransferOptions): Promise<void>;
|
54
|
+
updateFunctionSpecifications(functionId: string, specification: Specification): Promise<void>;
|
54
55
|
}
|
package/dist/utilsController.js
CHANGED
@@ -14,6 +14,8 @@ import { afterImportActions } from "./migrations/afterImportActions.js";
|
|
14
14
|
import { transferDatabaseLocalToLocal, transferDatabaseLocalToRemote, transferStorageLocalToLocal, transferStorageLocalToRemote, } from "./migrations/transfer.js";
|
15
15
|
import { getClient } from "./utils/getClientFromConfig.js";
|
16
16
|
import { fetchAllDatabases } from "./migrations/databases.js";
|
17
|
+
import { updateFunctionSpecifications } from "./functions/methods.js";
|
18
|
+
import chalk from "chalk";
|
17
19
|
export class UtilsController {
|
18
20
|
appwriteFolderPath;
|
19
21
|
appwriteConfigPath;
|
@@ -277,6 +279,14 @@ export class UtilsController {
|
|
277
279
|
}
|
278
280
|
}
|
279
281
|
}
|
280
|
-
console.log("Transfer completed");
|
282
|
+
console.log(chalk.green("Transfer completed"));
|
283
|
+
}
|
284
|
+
async updateFunctionSpecifications(functionId, specification) {
|
285
|
+
await this.init();
|
286
|
+
if (!this.appwriteServer)
|
287
|
+
throw new Error("Appwrite server not initialized");
|
288
|
+
console.log(chalk.green(`Updating function specifications for ${functionId} to ${specification}`));
|
289
|
+
await updateFunctionSpecifications(this.appwriteServer, functionId, specification);
|
290
|
+
console.log(chalk.green(`Successfully updated function specifications for ${functionId} to ${specification}`));
|
281
291
|
}
|
282
292
|
}
|
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.
|
4
|
+
"version": "0.9.992",
|
5
5
|
"main": "src/main.ts",
|
6
6
|
"type": "module",
|
7
7
|
"repository": {
|
@@ -31,7 +31,7 @@
|
|
31
31
|
},
|
32
32
|
"dependencies": {
|
33
33
|
"@types/inquirer": "^9.0.7",
|
34
|
-
"appwrite-utils": "^0.3.
|
34
|
+
"appwrite-utils": "^0.3.96",
|
35
35
|
"chalk": "^5.3.0",
|
36
36
|
"commander": "^12.1.0",
|
37
37
|
"inquirer": "^9.3.6",
|
@@ -40,6 +40,7 @@
|
|
40
40
|
"luxon": "^3.5.0",
|
41
41
|
"nanostores": "^0.10.3",
|
42
42
|
"node-appwrite": "^14.1.0",
|
43
|
+
"tar": "^7.4.3",
|
43
44
|
"tsx": "^4.17.0",
|
44
45
|
"ulidx": "^2.4.0",
|
45
46
|
"winston": "^3.14.2",
|
@@ -0,0 +1,235 @@
|
|
1
|
+
import {
|
2
|
+
AppwriteException,
|
3
|
+
Client,
|
4
|
+
Functions,
|
5
|
+
Query,
|
6
|
+
Runtime,
|
7
|
+
type Models,
|
8
|
+
} from "node-appwrite";
|
9
|
+
import { InputFile } from "node-appwrite/file";
|
10
|
+
import { create as createTarball } from "tar";
|
11
|
+
import { join } from "node:path";
|
12
|
+
import fs from "node:fs";
|
13
|
+
import { type Specification } from "appwrite-utils";
|
14
|
+
import chalk from "chalk";
|
15
|
+
|
16
|
+
export const listFunctions = async (
|
17
|
+
client: Client,
|
18
|
+
queries?: string[],
|
19
|
+
search?: string
|
20
|
+
) => {
|
21
|
+
const functions = new Functions(client);
|
22
|
+
const functionsList = await functions.list(queries, search);
|
23
|
+
return functionsList;
|
24
|
+
};
|
25
|
+
|
26
|
+
export const getFunction = async (client: Client, functionId: string) => {
|
27
|
+
const functions = new Functions(client);
|
28
|
+
const functionResponse = await functions.get(functionId);
|
29
|
+
return functionResponse;
|
30
|
+
};
|
31
|
+
|
32
|
+
export const deleteFunction = async (client: Client, functionId: string) => {
|
33
|
+
const functions = new Functions(client);
|
34
|
+
const functionResponse = await functions.delete(functionId);
|
35
|
+
return functionResponse;
|
36
|
+
};
|
37
|
+
|
38
|
+
export const createFunction = async (
|
39
|
+
client: Client,
|
40
|
+
functionId: string,
|
41
|
+
name: string,
|
42
|
+
runtime: Runtime,
|
43
|
+
execute?: string[],
|
44
|
+
events?: string[],
|
45
|
+
schedule?: string,
|
46
|
+
timeout?: number,
|
47
|
+
enabled?: boolean,
|
48
|
+
logging?: boolean,
|
49
|
+
entrypoint?: string,
|
50
|
+
commands?: string,
|
51
|
+
scopes?: string[],
|
52
|
+
installationId?: string,
|
53
|
+
providerRepositoryId?: string,
|
54
|
+
providerBranch?: string,
|
55
|
+
providerSilentMode?: boolean,
|
56
|
+
providerRootDirectory?: string,
|
57
|
+
templateRepository?: string,
|
58
|
+
templateOwner?: string,
|
59
|
+
templateRootDirectory?: string,
|
60
|
+
templateVersion?: string,
|
61
|
+
specification?: string
|
62
|
+
) => {
|
63
|
+
const functions = new Functions(client);
|
64
|
+
const functionResponse = await functions.create(
|
65
|
+
functionId,
|
66
|
+
name,
|
67
|
+
runtime,
|
68
|
+
execute,
|
69
|
+
events,
|
70
|
+
schedule,
|
71
|
+
timeout,
|
72
|
+
enabled,
|
73
|
+
logging,
|
74
|
+
entrypoint,
|
75
|
+
commands,
|
76
|
+
scopes,
|
77
|
+
installationId,
|
78
|
+
providerRepositoryId,
|
79
|
+
providerBranch,
|
80
|
+
providerSilentMode,
|
81
|
+
providerRootDirectory,
|
82
|
+
templateRepository,
|
83
|
+
templateOwner,
|
84
|
+
templateRootDirectory,
|
85
|
+
templateVersion,
|
86
|
+
specification
|
87
|
+
);
|
88
|
+
return functionResponse;
|
89
|
+
};
|
90
|
+
|
91
|
+
export const updateFunctionSpecifications = async (
|
92
|
+
client: Client,
|
93
|
+
functionId: string,
|
94
|
+
specification: Specification
|
95
|
+
) => {
|
96
|
+
const curFunction = await listFunctions(client, [
|
97
|
+
Query.equal("$id", functionId),
|
98
|
+
]);
|
99
|
+
if (curFunction.functions.length === 0) {
|
100
|
+
throw new Error("Function not found");
|
101
|
+
}
|
102
|
+
const functionFound = curFunction.functions[0];
|
103
|
+
try {
|
104
|
+
const functionResponse = await updateFunction(
|
105
|
+
client,
|
106
|
+
functionId,
|
107
|
+
functionFound.name,
|
108
|
+
undefined,
|
109
|
+
undefined,
|
110
|
+
undefined,
|
111
|
+
undefined,
|
112
|
+
undefined,
|
113
|
+
undefined,
|
114
|
+
undefined,
|
115
|
+
undefined,
|
116
|
+
undefined,
|
117
|
+
undefined,
|
118
|
+
undefined,
|
119
|
+
undefined,
|
120
|
+
undefined,
|
121
|
+
undefined,
|
122
|
+
undefined,
|
123
|
+
specification
|
124
|
+
);
|
125
|
+
return functionResponse;
|
126
|
+
} catch (error) {
|
127
|
+
if (
|
128
|
+
error instanceof AppwriteException &&
|
129
|
+
error.message.includes("Invalid `specification`")
|
130
|
+
) {
|
131
|
+
console.error(
|
132
|
+
chalk.red(
|
133
|
+
"Error updating function specifications, please try setting the env variable `_FUNCTIONS_CPUS` and `_FUNCTIONS_RAM` to non-zero values"
|
134
|
+
)
|
135
|
+
);
|
136
|
+
} else {
|
137
|
+
console.error(chalk.red("Error updating function specifications."));
|
138
|
+
throw error;
|
139
|
+
}
|
140
|
+
}
|
141
|
+
};
|
142
|
+
|
143
|
+
export const listSpecifications = async (client: Client) => {
|
144
|
+
const functions = new Functions(client);
|
145
|
+
const specifications = await functions.listSpecifications();
|
146
|
+
return specifications;
|
147
|
+
};
|
148
|
+
|
149
|
+
export const updateFunction = async (
|
150
|
+
client: Client,
|
151
|
+
functionId: string,
|
152
|
+
name: string,
|
153
|
+
runtime?: Runtime,
|
154
|
+
execute?: string[],
|
155
|
+
events?: string[],
|
156
|
+
schedule?: string,
|
157
|
+
timeout?: number,
|
158
|
+
enabled?: boolean,
|
159
|
+
logging?: boolean,
|
160
|
+
entrypoint?: string,
|
161
|
+
commands?: string,
|
162
|
+
scopes?: string[],
|
163
|
+
installationId?: string,
|
164
|
+
providerRepositoryId?: string,
|
165
|
+
providerBranch?: string,
|
166
|
+
providerSilentMode?: boolean,
|
167
|
+
providerRootDirectory?: string,
|
168
|
+
specification?: Specification
|
169
|
+
) => {
|
170
|
+
const functions = new Functions(client);
|
171
|
+
const functionResponse = await functions.update(
|
172
|
+
functionId,
|
173
|
+
name,
|
174
|
+
runtime,
|
175
|
+
execute,
|
176
|
+
events,
|
177
|
+
schedule,
|
178
|
+
timeout,
|
179
|
+
enabled,
|
180
|
+
logging,
|
181
|
+
entrypoint,
|
182
|
+
commands,
|
183
|
+
scopes,
|
184
|
+
installationId,
|
185
|
+
providerRepositoryId,
|
186
|
+
providerBranch,
|
187
|
+
providerSilentMode,
|
188
|
+
providerRootDirectory,
|
189
|
+
specification
|
190
|
+
);
|
191
|
+
return functionResponse;
|
192
|
+
};
|
193
|
+
|
194
|
+
export const deployFunction = async (
|
195
|
+
client: Client,
|
196
|
+
functionId: string,
|
197
|
+
codePath: string,
|
198
|
+
activate: boolean = true,
|
199
|
+
entrypoint: string = "index.js",
|
200
|
+
commands: string = "npm install"
|
201
|
+
) => {
|
202
|
+
const functions = new Functions(client);
|
203
|
+
|
204
|
+
// Create temporary tar.gz file
|
205
|
+
const tarPath = join(process.cwd(), `function-${functionId}.tar.gz`);
|
206
|
+
await createTarball(
|
207
|
+
{
|
208
|
+
gzip: true,
|
209
|
+
file: tarPath,
|
210
|
+
cwd: codePath,
|
211
|
+
},
|
212
|
+
["."] // Include all files from codePath
|
213
|
+
);
|
214
|
+
|
215
|
+
// Read the tar.gz file and create a File object
|
216
|
+
const fileBuffer = await fs.promises.readFile(tarPath);
|
217
|
+
const fileObject = InputFile.fromBuffer(
|
218
|
+
new Uint8Array(fileBuffer),
|
219
|
+
`function-${functionId}.tar.gz`
|
220
|
+
);
|
221
|
+
|
222
|
+
// Create deployment with the File object
|
223
|
+
const functionResponse = await functions.createDeployment(
|
224
|
+
functionId,
|
225
|
+
fileObject,
|
226
|
+
activate,
|
227
|
+
entrypoint,
|
228
|
+
commands
|
229
|
+
);
|
230
|
+
|
231
|
+
// Clean up the temporary tar file
|
232
|
+
await fs.promises.unlink(tarPath);
|
233
|
+
|
234
|
+
return functionResponse;
|
235
|
+
};
|
package/src/interactiveCLI.ts
CHANGED
@@ -23,6 +23,7 @@ import {
|
|
23
23
|
import { ulid } from "ulidx";
|
24
24
|
import chalk from "chalk";
|
25
25
|
import { DateTime } from "luxon";
|
26
|
+
import { listFunctions, listSpecifications } from "./functions/methods.js";
|
26
27
|
|
27
28
|
enum CHOICES {
|
28
29
|
CREATE_COLLECTION_CONFIG = "Create collection config file",
|
@@ -37,6 +38,7 @@ enum CHOICES {
|
|
37
38
|
GENERATE_SCHEMAS = "Generate schemas",
|
38
39
|
IMPORT_DATA = "Import data",
|
39
40
|
RELOAD_CONFIG = "Reload configuration files",
|
41
|
+
UPDATE_FUNCTION_SPEC = "Update function specifications",
|
40
42
|
EXIT = "Exit",
|
41
43
|
}
|
42
44
|
|
@@ -112,6 +114,10 @@ export class InteractiveCLI {
|
|
112
114
|
await this.initControllerIfNeeded();
|
113
115
|
await this.reloadConfig();
|
114
116
|
break;
|
117
|
+
case CHOICES.UPDATE_FUNCTION_SPEC:
|
118
|
+
await this.initControllerIfNeeded();
|
119
|
+
await this.updateFunctionSpec();
|
120
|
+
break;
|
115
121
|
case CHOICES.EXIT:
|
116
122
|
console.log(chalk.green("Goodbye!"));
|
117
123
|
return;
|
@@ -176,7 +182,7 @@ export class InteractiveCLI {
|
|
176
182
|
name: "selectedDatabases",
|
177
183
|
message: chalk.blue(message),
|
178
184
|
choices,
|
179
|
-
loop:
|
185
|
+
loop: true,
|
180
186
|
pageSize: 10,
|
181
187
|
},
|
182
188
|
]);
|
@@ -256,7 +262,7 @@ export class InteractiveCLI {
|
|
256
262
|
name: "selectedCollections",
|
257
263
|
message: chalk.blue(message),
|
258
264
|
choices,
|
259
|
-
loop:
|
265
|
+
loop: true,
|
260
266
|
pageSize: 10,
|
261
267
|
},
|
262
268
|
]);
|
@@ -962,4 +968,45 @@ export class InteractiveCLI {
|
|
962
968
|
console.error(chalk.red("Error reloading configuration files:"), error);
|
963
969
|
}
|
964
970
|
}
|
971
|
+
|
972
|
+
private async updateFunctionSpec(): Promise<void> {
|
973
|
+
const functions = await listFunctions(this.controller!.appwriteServer!, [
|
974
|
+
Query.limit(1000),
|
975
|
+
]);
|
976
|
+
|
977
|
+
const functionsToUpdate = await inquirer.prompt([
|
978
|
+
{
|
979
|
+
type: 'checkbox',
|
980
|
+
name: 'functionId',
|
981
|
+
message: 'Select functions to update:',
|
982
|
+
choices: functions.functions.map(f => ({
|
983
|
+
name: `${f.name} (${f.$id})`,
|
984
|
+
value: f.$id
|
985
|
+
})),
|
986
|
+
loop: true,
|
987
|
+
}
|
988
|
+
]);
|
989
|
+
|
990
|
+
const specifications = await listSpecifications(this.controller!.appwriteServer!);
|
991
|
+
const { specification } = await inquirer.prompt([
|
992
|
+
{
|
993
|
+
type: 'list',
|
994
|
+
name: 'specification',
|
995
|
+
message: 'Select new specification:',
|
996
|
+
choices: specifications.specifications.map((s) => ({
|
997
|
+
name: `${s.slug}`,
|
998
|
+
value: s.slug
|
999
|
+
})),
|
1000
|
+
}
|
1001
|
+
]);
|
1002
|
+
|
1003
|
+
try {
|
1004
|
+
for (const functionId of functionsToUpdate.functionId) {
|
1005
|
+
await this.controller!.updateFunctionSpecifications(functionId, specification);
|
1006
|
+
console.log(chalk.green(`Successfully updated function specification to ${specification}`));
|
1007
|
+
}
|
1008
|
+
} catch (error) {
|
1009
|
+
console.error(chalk.red('Error updating function specification:'), error);
|
1010
|
+
}
|
1011
|
+
}
|
965
1012
|
}
|
package/src/main.ts
CHANGED
@@ -10,6 +10,9 @@ import { getClient } from "./utils/getClientFromConfig.js";
|
|
10
10
|
import { fetchAllDatabases } from "./migrations/databases.js";
|
11
11
|
import { setupDirsFiles } from "./utils/setupFiles.js";
|
12
12
|
import { fetchAllCollections } from "./collections/methods.js";
|
13
|
+
import type { Specification } from "appwrite-utils";
|
14
|
+
import chalk from "chalk";
|
15
|
+
import { listSpecifications } from "./functions/methods.js";
|
13
16
|
|
14
17
|
interface CliOptions {
|
15
18
|
it?: boolean;
|
@@ -38,6 +41,9 @@ interface CliOptions {
|
|
38
41
|
remoteProjectId?: string;
|
39
42
|
remoteApiKey?: string;
|
40
43
|
setup?: boolean;
|
44
|
+
updateFunctionSpec?: boolean;
|
45
|
+
functionId?: string;
|
46
|
+
specification?: string;
|
41
47
|
}
|
42
48
|
|
43
49
|
type ParsedArgv = ArgumentsCamelCase<CliOptions>;
|
@@ -157,6 +163,28 @@ const argv = yargs(hideBin(process.argv))
|
|
157
163
|
type: "boolean",
|
158
164
|
description: "Setup directories and files",
|
159
165
|
})
|
166
|
+
.option("updateFunctionSpec", {
|
167
|
+
type: "boolean",
|
168
|
+
description: "Update function specifications",
|
169
|
+
})
|
170
|
+
.option("functionId", {
|
171
|
+
type: "string",
|
172
|
+
description: "Function ID to update",
|
173
|
+
})
|
174
|
+
.option("specification", {
|
175
|
+
type: "string",
|
176
|
+
description: "New function specification (e.g., 's-1vcpu-1gb')",
|
177
|
+
choices: [
|
178
|
+
"s-0.5vcpu-512mb",
|
179
|
+
"s-1vcpu-1gb",
|
180
|
+
"s-2vcpu-2gb",
|
181
|
+
"s-2vcpu-4gb",
|
182
|
+
"s-4vcpu-4gb",
|
183
|
+
"s-4vcpu-8gb",
|
184
|
+
"s-8vcpu-4gb",
|
185
|
+
"s-8vcpu-8gb"
|
186
|
+
],
|
187
|
+
})
|
160
188
|
.parse() as ParsedArgv;
|
161
189
|
|
162
190
|
async function main() {
|
@@ -181,7 +209,8 @@ async function main() {
|
|
181
209
|
collections: parsedArgv.collectionIds?.split(","),
|
182
210
|
doBackup: parsedArgv.backup,
|
183
211
|
wipeDatabase: parsedArgv.wipe === "all" || parsedArgv.wipe === "docs",
|
184
|
-
wipeDocumentStorage:
|
212
|
+
wipeDocumentStorage:
|
213
|
+
parsedArgv.wipe === "all" || parsedArgv.wipe === "storage",
|
185
214
|
wipeUsers: parsedArgv.wipe === "all" || parsedArgv.wipe === "users",
|
186
215
|
generateSchemas: parsedArgv.generate,
|
187
216
|
importData: parsedArgv.import,
|
@@ -189,6 +218,19 @@ async function main() {
|
|
189
218
|
wipeCollections: parsedArgv.wipeCollections,
|
190
219
|
};
|
191
220
|
|
221
|
+
if (parsedArgv.updateFunctionSpec) {
|
222
|
+
if (!parsedArgv.functionId || !parsedArgv.specification) {
|
223
|
+
throw new Error("Function ID and specification are required for updating function specs");
|
224
|
+
}
|
225
|
+
console.log(chalk.yellow(`Updating function specification for ${parsedArgv.functionId} to ${parsedArgv.specification}, checking if specification exists...`));
|
226
|
+
const specifications = await listSpecifications(controller.appwriteServer!);
|
227
|
+
if (!specifications.specifications.some((s: { slug: string }) => s.slug === parsedArgv.specification)) {
|
228
|
+
console.log(chalk.red(`Specification ${parsedArgv.specification} not found`));
|
229
|
+
return;
|
230
|
+
}
|
231
|
+
await controller.updateFunctionSpecifications(parsedArgv.functionId, parsedArgv.specification as Specification);
|
232
|
+
}
|
233
|
+
|
192
234
|
// Add default databases if not specified
|
193
235
|
if (!options.databases || options.databases.length === 0) {
|
194
236
|
const allDatabases = await fetchAllDatabases(controller.database!);
|
@@ -218,19 +260,29 @@ async function main() {
|
|
218
260
|
options.wipeUsers ||
|
219
261
|
options.wipeCollections
|
220
262
|
) {
|
221
|
-
if (
|
222
|
-
|
223
|
-
|
263
|
+
if (parsedArgv.wipe === "all") {
|
264
|
+
if (options.databases) {
|
265
|
+
for (const db of options.databases) {
|
266
|
+
await controller.wipeDatabase(db, true); // true to wipe associated buckets
|
267
|
+
}
|
224
268
|
}
|
225
|
-
|
226
|
-
if (
|
227
|
-
|
228
|
-
|
269
|
+
await controller.wipeUsers();
|
270
|
+
} else if (parsedArgv.wipe === "docs") {
|
271
|
+
if (options.databases) {
|
272
|
+
for (const db of options.databases) {
|
273
|
+
await controller.wipeBucketFromDatabase(db);
|
274
|
+
}
|
229
275
|
}
|
230
|
-
|
231
|
-
|
276
|
+
if (parsedArgv.bucketIds) {
|
277
|
+
for (const bucketId of parsedArgv.bucketIds.split(",")) {
|
278
|
+
await controller.wipeDocumentStorage(bucketId);
|
279
|
+
}
|
280
|
+
}
|
281
|
+
} else if (parsedArgv.wipe === "users") {
|
232
282
|
await controller.wipeUsers();
|
233
283
|
}
|
284
|
+
|
285
|
+
// Handle specific collection wipes
|
234
286
|
if (options.wipeCollections && options.databases) {
|
235
287
|
for (const db of options.databases) {
|
236
288
|
const dbCollections = await fetchAllCollections(
|
@@ -109,15 +109,20 @@ export const transferStorageLocalToLocal = async (
|
|
109
109
|
new Uint8Array(fileData),
|
110
110
|
file.name
|
111
111
|
);
|
112
|
-
|
113
|
-
|
112
|
+
try {
|
113
|
+
await tryAwaitWithRetry(
|
114
|
+
async () =>
|
114
115
|
await storage.createFile(
|
115
116
|
toBucketId,
|
116
117
|
file.$id,
|
117
118
|
fileToCreate,
|
118
119
|
file.$permissions
|
119
120
|
)
|
120
|
-
|
121
|
+
);
|
122
|
+
} catch (error: any) {
|
123
|
+
// File already exists, so we can skip it
|
124
|
+
continue;
|
125
|
+
}
|
121
126
|
numberOfFiles++;
|
122
127
|
}
|
123
128
|
}
|
package/src/utilsController.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import { Client, Databases, Query, Storage, type Models } from "node-appwrite";
|
2
|
-
import { type AppwriteConfig } from "appwrite-utils";
|
2
|
+
import { type AppwriteConfig, type Specification } from "appwrite-utils";
|
3
3
|
import { loadConfig, findAppwriteConfig } from "./utils/loadConfigs.js";
|
4
4
|
import { UsersController } from "./migrations/users.js";
|
5
5
|
import { AppwriteToX } from "./migrations/appwriteToX.js";
|
@@ -42,6 +42,8 @@ import {
|
|
42
42
|
} from "./migrations/transfer.js";
|
43
43
|
import { getClient } from "./utils/getClientFromConfig.js";
|
44
44
|
import { fetchAllDatabases } from "./migrations/databases.js";
|
45
|
+
import { updateFunctionSpecifications } from "./functions/methods.js";
|
46
|
+
import chalk from "chalk";
|
45
47
|
|
46
48
|
export interface SetupOptions {
|
47
49
|
databases?: Models.Database[];
|
@@ -404,6 +406,14 @@ export class UtilsController {
|
|
404
406
|
}
|
405
407
|
}
|
406
408
|
|
407
|
-
console.log("Transfer completed");
|
409
|
+
console.log(chalk.green("Transfer completed"));
|
410
|
+
}
|
411
|
+
|
412
|
+
async updateFunctionSpecifications(functionId: string, specification: Specification) {
|
413
|
+
await this.init();
|
414
|
+
if (!this.appwriteServer) throw new Error("Appwrite server not initialized");
|
415
|
+
console.log(chalk.green(`Updating function specifications for ${functionId} to ${specification}`));
|
416
|
+
await updateFunctionSpecifications(this.appwriteServer, functionId, specification);
|
417
|
+
console.log(chalk.green(`Successfully updated function specifications for ${functionId} to ${specification}`));
|
408
418
|
}
|
409
419
|
}
|