appwrite-utils-cli 1.7.7 → 1.7.9

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.
Files changed (65) hide show
  1. package/SELECTION_DIALOGS.md +146 -0
  2. package/dist/cli/commands/databaseCommands.js +89 -23
  3. package/dist/config/services/ConfigLoaderService.d.ts +7 -0
  4. package/dist/config/services/ConfigLoaderService.js +47 -1
  5. package/dist/functions/deployments.js +5 -23
  6. package/dist/functions/methods.js +4 -2
  7. package/dist/functions/pathResolution.d.ts +37 -0
  8. package/dist/functions/pathResolution.js +185 -0
  9. package/dist/functions/templates/count-docs-in-collection/README.md +54 -0
  10. package/dist/functions/templates/count-docs-in-collection/package.json +25 -0
  11. package/dist/functions/templates/count-docs-in-collection/src/main.ts +159 -0
  12. package/dist/functions/templates/count-docs-in-collection/src/request.ts +9 -0
  13. package/dist/functions/templates/count-docs-in-collection/tsconfig.json +28 -0
  14. package/dist/functions/templates/hono-typescript/README.md +286 -0
  15. package/dist/functions/templates/hono-typescript/package.json +26 -0
  16. package/dist/functions/templates/hono-typescript/src/adapters/request.ts +74 -0
  17. package/dist/functions/templates/hono-typescript/src/adapters/response.ts +106 -0
  18. package/dist/functions/templates/hono-typescript/src/app.ts +180 -0
  19. package/dist/functions/templates/hono-typescript/src/context.ts +103 -0
  20. package/dist/functions/templates/hono-typescript/src/index.ts +54 -0
  21. package/dist/functions/templates/hono-typescript/src/middleware/appwrite.ts +119 -0
  22. package/dist/functions/templates/hono-typescript/tsconfig.json +20 -0
  23. package/dist/functions/templates/typescript-node/README.md +32 -0
  24. package/dist/functions/templates/typescript-node/package.json +25 -0
  25. package/dist/functions/templates/typescript-node/src/context.ts +103 -0
  26. package/dist/functions/templates/typescript-node/src/index.ts +29 -0
  27. package/dist/functions/templates/typescript-node/tsconfig.json +28 -0
  28. package/dist/functions/templates/uv/README.md +31 -0
  29. package/dist/functions/templates/uv/pyproject.toml +30 -0
  30. package/dist/functions/templates/uv/src/__init__.py +0 -0
  31. package/dist/functions/templates/uv/src/context.py +125 -0
  32. package/dist/functions/templates/uv/src/index.py +46 -0
  33. package/dist/main.js +175 -4
  34. package/dist/migrations/appwriteToX.d.ts +27 -2
  35. package/dist/migrations/appwriteToX.js +293 -69
  36. package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +1 -1
  37. package/dist/migrations/yaml/generateImportSchemas.js +23 -8
  38. package/dist/shared/schemaGenerator.js +25 -12
  39. package/dist/shared/selectionDialogs.d.ts +214 -0
  40. package/dist/shared/selectionDialogs.js +540 -0
  41. package/dist/utils/configDiscovery.d.ts +4 -4
  42. package/dist/utils/configDiscovery.js +66 -30
  43. package/dist/utils/yamlConverter.d.ts +1 -0
  44. package/dist/utils/yamlConverter.js +26 -3
  45. package/dist/utilsController.d.ts +7 -1
  46. package/dist/utilsController.js +198 -17
  47. package/package.json +4 -2
  48. package/scripts/copy-templates.ts +23 -0
  49. package/src/cli/commands/databaseCommands.ts +133 -34
  50. package/src/config/services/ConfigLoaderService.ts +62 -1
  51. package/src/functions/deployments.ts +10 -35
  52. package/src/functions/methods.ts +4 -2
  53. package/src/functions/pathResolution.ts +227 -0
  54. package/src/main.ts +276 -34
  55. package/src/migrations/appwriteToX.ts +385 -90
  56. package/src/migrations/yaml/generateImportSchemas.ts +26 -8
  57. package/src/shared/schemaGenerator.ts +29 -12
  58. package/src/shared/selectionDialogs.ts +745 -0
  59. package/src/utils/configDiscovery.ts +83 -39
  60. package/src/utils/yamlConverter.ts +29 -3
  61. package/src/utilsController.ts +250 -22
  62. package/dist/utils/schemaStrings.d.ts +0 -14
  63. package/dist/utils/schemaStrings.js +0 -428
  64. package/dist/utils/sessionPreservationExample.d.ts +0 -1666
  65. package/dist/utils/sessionPreservationExample.js +0 -101
@@ -0,0 +1,185 @@
1
+ import { existsSync, statSync, readdirSync } from 'node:fs';
2
+ import { join, resolve, isAbsolute } from 'node:path';
3
+ import { homedir } from 'node:os';
4
+ import { MessageFormatter } from '../shared/messageFormatter.js';
5
+ import { logger } from '../shared/logging.js';
6
+ /**
7
+ * Expands tilde (~) in paths to the user's home directory
8
+ * @param pathStr - Path string that may contain ~
9
+ * @returns Expanded path with home directory
10
+ */
11
+ export function expandTildePath(pathStr) {
12
+ if (!pathStr)
13
+ return pathStr;
14
+ if (pathStr.startsWith('~/') || pathStr === '~') {
15
+ const expandedPath = pathStr.replace(/^~(?=$|\/|\\)/, homedir());
16
+ logger.debug('Expanded tilde path', { original: pathStr, expanded: expandedPath });
17
+ return expandedPath;
18
+ }
19
+ return pathStr;
20
+ }
21
+ /**
22
+ * Normalizes function name to standard format (lowercase, dashes instead of spaces)
23
+ * @param name - Function name to normalize
24
+ * @returns Normalized function name
25
+ */
26
+ export function normalizeFunctionName(name) {
27
+ if (!name)
28
+ return name;
29
+ const normalized = name.toLowerCase().replace(/\s+/g, '-');
30
+ if (normalized !== name) {
31
+ logger.debug('Normalized function name', { original: name, normalized });
32
+ }
33
+ return normalized;
34
+ }
35
+ /**
36
+ * Validates that a directory exists and contains function markers
37
+ * @param dirPath - Directory path to validate
38
+ * @returns True if directory is a valid function directory
39
+ */
40
+ export function validateFunctionDirectory(dirPath) {
41
+ try {
42
+ // Check if directory exists
43
+ if (!existsSync(dirPath)) {
44
+ logger.debug('Directory does not exist', { dirPath });
45
+ return false;
46
+ }
47
+ // Check if it's actually a directory
48
+ const stats = statSync(dirPath);
49
+ if (!stats.isDirectory()) {
50
+ logger.debug('Path is not a directory', { dirPath });
51
+ return false;
52
+ }
53
+ // Check for function markers
54
+ const contents = readdirSync(dirPath);
55
+ const hasPackageJson = contents.includes('package.json');
56
+ const hasPyprojectToml = contents.includes('pyproject.toml');
57
+ const hasSrcDir = contents.includes('src');
58
+ const isValid = hasPackageJson || hasPyprojectToml || hasSrcDir;
59
+ logger.debug('Function directory validation', {
60
+ dirPath,
61
+ isValid,
62
+ markers: {
63
+ hasPackageJson,
64
+ hasPyprojectToml,
65
+ hasSrcDir
66
+ }
67
+ });
68
+ return isValid;
69
+ }
70
+ catch (error) {
71
+ logger.debug('Error validating function directory', {
72
+ dirPath,
73
+ error: error instanceof Error ? error.message : String(error)
74
+ });
75
+ return false;
76
+ }
77
+ }
78
+ /**
79
+ * Helper function to search for function in standard locations
80
+ * @param configDirPath - Directory where config file is located
81
+ * @param normalizedName - Normalized function name
82
+ * @returns First valid function directory path or undefined
83
+ */
84
+ export function findFunctionInStandardLocations(configDirPath, normalizedName) {
85
+ const searchPaths = [
86
+ // Same directory as config
87
+ join(configDirPath, 'functions', normalizedName),
88
+ // Parent directory of config
89
+ join(configDirPath, '..', 'functions', normalizedName),
90
+ // Current working directory
91
+ join(process.cwd(), 'functions', normalizedName),
92
+ ];
93
+ logger.debug('Searching for function in standard locations', {
94
+ normalizedName,
95
+ configDirPath,
96
+ searchPaths
97
+ });
98
+ for (const searchPath of searchPaths) {
99
+ const resolvedPath = resolve(searchPath);
100
+ logger.debug('Checking search path', { searchPath, resolvedPath });
101
+ if (validateFunctionDirectory(resolvedPath)) {
102
+ logger.debug('Found function in standard location', { resolvedPath });
103
+ return resolvedPath;
104
+ }
105
+ }
106
+ logger.debug('Function not found in any standard location', { normalizedName });
107
+ return undefined;
108
+ }
109
+ /**
110
+ * Resolves the absolute path to a function directory
111
+ * Handles multiple resolution strategies with proper priority
112
+ *
113
+ * @param functionName - Name of the function
114
+ * @param configDirPath - Directory where config file is located
115
+ * @param dirPath - Optional explicit dirPath from config
116
+ * @param explicitPath - Optional path passed as parameter (highest priority)
117
+ * @returns Absolute path to the function directory
118
+ * @throws Error if function directory cannot be found or is invalid
119
+ */
120
+ export function resolveFunctionDirectory(functionName, configDirPath, dirPath, explicitPath) {
121
+ logger.debug('Resolving function directory', {
122
+ functionName,
123
+ configDirPath,
124
+ dirPath,
125
+ explicitPath
126
+ });
127
+ const normalizedName = normalizeFunctionName(functionName);
128
+ // Priority 1: Explicit path parameter (highest priority)
129
+ if (explicitPath) {
130
+ logger.debug('Using explicit path parameter');
131
+ const expandedPath = expandTildePath(explicitPath);
132
+ const resolvedPath = isAbsolute(expandedPath)
133
+ ? expandedPath
134
+ : resolve(process.cwd(), expandedPath);
135
+ if (!validateFunctionDirectory(resolvedPath)) {
136
+ const errorMsg = `Explicit path is not a valid function directory: ${resolvedPath}`;
137
+ logger.error(errorMsg);
138
+ MessageFormatter.error('Invalid function directory', errorMsg, { prefix: 'Path Resolution' });
139
+ throw new Error(errorMsg);
140
+ }
141
+ logger.debug('Resolved using explicit path', { resolvedPath });
142
+ MessageFormatter.debug(`Resolved function directory using explicit path: ${resolvedPath}`, undefined, { prefix: 'Path Resolution' });
143
+ return resolvedPath;
144
+ }
145
+ // Priority 2: dirPath from config (relative to config location)
146
+ if (dirPath) {
147
+ logger.debug('Using dirPath from config');
148
+ const expandedPath = expandTildePath(dirPath);
149
+ const resolvedPath = isAbsolute(expandedPath)
150
+ ? expandedPath
151
+ : resolve(configDirPath, expandedPath);
152
+ if (!validateFunctionDirectory(resolvedPath)) {
153
+ const errorMsg = `Config dirPath is not a valid function directory: ${resolvedPath}`;
154
+ logger.error(errorMsg);
155
+ MessageFormatter.error('Invalid function directory', errorMsg, { prefix: 'Path Resolution' });
156
+ throw new Error(errorMsg);
157
+ }
158
+ logger.debug('Resolved using config dirPath', { resolvedPath });
159
+ MessageFormatter.debug(`Resolved function directory using config dirPath: ${resolvedPath}`, undefined, { prefix: 'Path Resolution' });
160
+ return resolvedPath;
161
+ }
162
+ // Priority 3: Search standard locations
163
+ logger.debug('Searching standard locations for function');
164
+ const foundPath = findFunctionInStandardLocations(configDirPath, normalizedName);
165
+ if (foundPath) {
166
+ logger.debug('Resolved using standard location search', { foundPath });
167
+ MessageFormatter.debug(`Found function directory in standard location: ${foundPath}`, undefined, { prefix: 'Path Resolution' });
168
+ return foundPath;
169
+ }
170
+ // Priority 4: Not found - throw error
171
+ const searchedLocations = [
172
+ join(configDirPath, 'functions', normalizedName),
173
+ join(configDirPath, '..', 'functions', normalizedName),
174
+ join(process.cwd(), 'functions', normalizedName),
175
+ ];
176
+ const errorMsg = `Function directory not found for '${functionName}' (normalized: '${normalizedName}'). ` +
177
+ `Searched locations:\n${searchedLocations.map(p => ` - ${p}`).join('\n')}`;
178
+ logger.error('Function directory not found', {
179
+ functionName,
180
+ normalizedName,
181
+ searchedLocations
182
+ });
183
+ MessageFormatter.error('Function directory not found', errorMsg, { prefix: 'Path Resolution' });
184
+ throw new Error(errorMsg);
185
+ }
@@ -0,0 +1,54 @@
1
+ # Count Documents in Collection Function
2
+
3
+ A utility function that accurately counts documents in an Appwrite collection, even when there are more than 5,000 documents.
4
+
5
+ ## Features
6
+ - Handles collections with any number of documents
7
+ - Supports filtering using Appwrite queries
8
+ - Uses efficient binary search algorithm for large collections
9
+ - Provides detailed logging during the counting process
10
+
11
+ ## Structure
12
+ - `src/main.ts`: Main function implementation with counting logic
13
+ - `src/request.ts`: Request validation schema using Zod
14
+
15
+ ## Usage
16
+ Send a POST request with:
17
+ ```json
18
+ {
19
+ "databaseId": "your-database-id",
20
+ "collectionId": "your-collection-id",
21
+ "queries": [Query.orderDesc("$createdAt"), Query.contains("name", "John")] // Or put the string array from after this, they are the same
22
+ }
23
+ ```
24
+
25
+ ## Response
26
+ ```json
27
+ {
28
+ "success": true,
29
+ "count": 12345
30
+ }
31
+ ```
32
+
33
+ ## Development
34
+ 1. Install dependencies: `npm|yarn|bun install`
35
+ 2. Build: `npm|yarn|bun run build`
36
+ 3. Deploy: Function will be built automatically during deployment
37
+
38
+ ## Deployment
39
+ Make sure it's inside `appwriteConfig.ts` functions array, and if you want to build it FIRST, before Appwrite (using your system), you can
40
+ add the `predeployCommands` to the function in `appwriteConfig.ts`.
41
+
42
+ ## Example Config
43
+ ```typescript
44
+ {
45
+ $id: 'count-docs',
46
+ name: 'Count Documents',
47
+ runtime: 'node-18.0',
48
+ path: 'functions/count-docs',
49
+ entrypoint: './main.js',
50
+ execute: ['any'],
51
+ predeployCommands: ['npm install', 'npm run build'],
52
+ deployDir: './dist'
53
+ }
54
+ ```
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "{{functionName}}",
3
+ "version": "1.0.0",
4
+ "description": "Appwrite function to count documents in a collection",
5
+ "main": "src/main.ts",
6
+ "type": "module",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "start": "node dist/main.js",
10
+ "dev": "tsx src/main.ts"
11
+ },
12
+ "dependencies": {
13
+ "node-appwrite": "^13.0.0",
14
+ "appwrite-utils": "latest",
15
+ "zod": "^3.23.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^20.0.0",
19
+ "typescript": "^5.0.0",
20
+ "tsx": "^4.0.0"
21
+ },
22
+ "engines": {
23
+ "node": ">=18.0.0"
24
+ }
25
+ }
@@ -0,0 +1,159 @@
1
+ import { Client, Databases, Query } from "node-appwrite";
2
+ import { AppwriteRequest, type AppwriteResponse } from "appwrite-utils";
3
+ import { requestSchema } from "./request.js";
4
+
5
+ /**
6
+ * Main function to handle document counting requests.
7
+ * @param {Object} params - The function parameters.
8
+ * @param {Object} params.req - The request object.
9
+ * @param {Object} params.res - The response object.
10
+ * @param {Function} params.log - Logging function.
11
+ * @param {Function} params.error - Error logging function.
12
+ * @returns {Promise<Object>} JSON response with count or error message.
13
+ */
14
+ export default async ({
15
+ req,
16
+ res,
17
+ log,
18
+ error,
19
+ }: {
20
+ req: AppwriteRequest;
21
+ res: AppwriteResponse;
22
+ log: (message: string) => void;
23
+ error: (message: string) => void;
24
+ }) => {
25
+ // Initialize Appwrite client
26
+ const client = new Client()
27
+ .setEndpoint(process.env["APPWRITE_FUNCTION_ENDPOINT"]!)
28
+ .setProject(process.env["APPWRITE_FUNCTION_PROJECT_ID"]!)
29
+ .setKey(req.headers["x-appwrite-key"] || "");
30
+
31
+ const databases = new Databases(client);
32
+
33
+ try {
34
+ if (req.method === "POST") {
35
+ // Parse request body
36
+ const body = requestSchema.safeParse(
37
+ typeof req.body === "string" ? JSON.parse(req.body) : req.body
38
+ );
39
+
40
+ if (!body.success) {
41
+ return res.json({ success: false, error: body.error }, 400);
42
+ }
43
+
44
+ const { databaseId, collectionId, queries = [] } = body.data;
45
+
46
+ log(`Queries: ${JSON.stringify(queries)}`);
47
+
48
+ // Count documents in the specified collection
49
+ const count = await countAllDocuments(
50
+ log,
51
+ databases,
52
+ databaseId,
53
+ collectionId,
54
+ queries
55
+ );
56
+
57
+ // Return successful response with document count
58
+ return res.json({
59
+ success: true,
60
+ count: count,
61
+ });
62
+ } else {
63
+ // Return error for non-POST requests
64
+ return res.json({ success: false, error: "Method not allowed" }, 405);
65
+ }
66
+ } catch (err) {
67
+ // Log and return any errors
68
+ error(`Error processing request: ${err}`);
69
+ return res.json({ success: false, error: (err as Error).message }, 500);
70
+ }
71
+ };
72
+
73
+ /**
74
+ * Counts all documents in a collection, handling large collections efficiently.
75
+ * @param {Function} log - Logging function.
76
+ * @param {Databases} databases - Appwrite Databases instance.
77
+ * @param {string} databaseId - ID of the database.
78
+ * @param {string} collectionId - ID of the collection.
79
+ * @param {string[]} queries - Array of query strings to filter documents.
80
+ * @param {number} batchSize - Size of batches for processing (default: 1000).
81
+ * @returns {Promise<number>} Total count of documents.
82
+ */
83
+ async function countAllDocuments(
84
+ log: any,
85
+ databases: Databases,
86
+ databaseId: string,
87
+ collectionId: string,
88
+ queries: string[] = [],
89
+ batchSize: number = 1000
90
+ ): Promise<number> {
91
+ // Filter out limit and offset queries
92
+ const initialQueries = queries.filter(
93
+ (q) => !(q.includes("limit") || q.includes("offset"))
94
+ );
95
+
96
+ // Initial query to check if total is less than 5000
97
+ const initialResponse = await databases.listDocuments(
98
+ databaseId,
99
+ collectionId,
100
+ [...initialQueries, Query.limit(1)]
101
+ );
102
+
103
+ if (initialResponse.total < 5000) {
104
+ log(`Total documents (from initial response): ${initialResponse.total}`);
105
+ return initialResponse.total;
106
+ }
107
+
108
+ // If total is 5000 or more, we need to count manually
109
+ let bound = 5000;
110
+
111
+ // Exponential search to find an upper bound
112
+ while (true) {
113
+ log(`Querying for offset ${bound}`);
114
+ try {
115
+ const response = await databases.listDocuments(databaseId, collectionId, [
116
+ ...initialQueries,
117
+ Query.limit(1),
118
+ Query.offset(bound),
119
+ ]);
120
+
121
+ if (response.documents.length === 0) {
122
+ break;
123
+ }
124
+ bound *= 2;
125
+ } catch (error) {
126
+ break;
127
+ }
128
+ }
129
+
130
+ // Binary search to find the exact count
131
+ let low = Math.floor(bound / 2);
132
+ let high = bound;
133
+ let lastValidCount = low;
134
+
135
+ while (low <= high) {
136
+ const mid = Math.floor((low + high) / 2);
137
+ log(`Binary search: Querying for offset ${mid}`);
138
+
139
+ try {
140
+ const response = await databases.listDocuments(databaseId, collectionId, [
141
+ ...initialQueries,
142
+ Query.limit(1),
143
+ Query.offset(mid),
144
+ ]);
145
+
146
+ if (response.documents.length > 0) {
147
+ lastValidCount = mid + 1; // +1 because offset is 0-based
148
+ low = mid + 1;
149
+ } else {
150
+ high = mid - 1;
151
+ }
152
+ } catch (error) {
153
+ high = mid - 1;
154
+ }
155
+ }
156
+
157
+ log(`Total documents: ${lastValidCount}`);
158
+ return lastValidCount;
159
+ }
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+
3
+ export const requestSchema = z.object({
4
+ databaseId: z.string(),
5
+ collectionId: z.string(),
6
+ queries: z.array(z.string()).optional(),
7
+ });
8
+
9
+ export type Request = z.infer<typeof requestSchema>;
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ "allowSyntheticDefaultImports": true,
7
+ "esModuleInterop": true,
8
+ "allowJs": true,
9
+ "checkJs": false,
10
+ "outDir": "./dist",
11
+ "rootDir": "./src",
12
+ "strict": true,
13
+ "noImplicitAny": true,
14
+ "strictNullChecks": true,
15
+ "strictFunctionTypes": true,
16
+ "noImplicitThis": true,
17
+ "noImplicitReturns": true,
18
+ "noFallthroughCasesInSwitch": true,
19
+ "moduleDetection": "force",
20
+ "resolveJsonModule": true,
21
+ "isolatedModules": true,
22
+ "verbatimModuleSyntax": false,
23
+ "skipLibCheck": true,
24
+ "forceConsistentCasingInFileNames": true
25
+ },
26
+ "include": ["src/**/*"],
27
+ "exclude": ["node_modules", "dist"]
28
+ }