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 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
+ };
@@ -21,4 +21,5 @@ export declare class InteractiveCLI {
21
21
  private getLocalCollections;
22
22
  private getLocalDatabases;
23
23
  private reloadConfig;
24
+ private updateFunctionSpec;
24
25
  }
@@ -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: false,
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: false,
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 (options.wipeDatabase && options.databases) {
175
- for (const db of options.databases) {
176
- await controller.wipeDatabase(db, options.wipeDocumentStorage);
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 (options.wipeDocumentStorage && parsedArgv.bucketIds) {
180
- for (const bucketId of parsedArgv.bucketIds.split(",")) {
181
- await controller.wipeDocumentStorage(bucketId);
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 (options.wipeUsers) {
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
- await tryAwaitWithRetry(async () => await storage.createFile(toBucketId, file.$id, fileToCreate, file.$permissions));
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
  }
@@ -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.990",
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.95",
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
+ };
@@ -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: false,
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: false,
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: parsedArgv.wipe === "all" || parsedArgv.wipe === "storage",
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 (options.wipeDatabase && options.databases) {
222
- for (const db of options.databases) {
223
- await controller.wipeDatabase(db, options.wipeDocumentStorage);
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 (options.wipeDocumentStorage && parsedArgv.bucketIds) {
227
- for (const bucketId of parsedArgv.bucketIds.split(",")) {
228
- await controller.wipeDocumentStorage(bucketId);
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
- if (options.wipeUsers) {
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
- await tryAwaitWithRetry(
113
- async () =>
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
  }
@@ -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
  }