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.
- package/SELECTION_DIALOGS.md +146 -0
- package/dist/cli/commands/databaseCommands.js +89 -23
- package/dist/config/services/ConfigLoaderService.d.ts +7 -0
- package/dist/config/services/ConfigLoaderService.js +47 -1
- package/dist/functions/deployments.js +5 -23
- package/dist/functions/methods.js +4 -2
- package/dist/functions/pathResolution.d.ts +37 -0
- package/dist/functions/pathResolution.js +185 -0
- package/dist/functions/templates/count-docs-in-collection/README.md +54 -0
- package/dist/functions/templates/count-docs-in-collection/package.json +25 -0
- package/dist/functions/templates/count-docs-in-collection/src/main.ts +159 -0
- package/dist/functions/templates/count-docs-in-collection/src/request.ts +9 -0
- package/dist/functions/templates/count-docs-in-collection/tsconfig.json +28 -0
- package/dist/functions/templates/hono-typescript/README.md +286 -0
- package/dist/functions/templates/hono-typescript/package.json +26 -0
- package/dist/functions/templates/hono-typescript/src/adapters/request.ts +74 -0
- package/dist/functions/templates/hono-typescript/src/adapters/response.ts +106 -0
- package/dist/functions/templates/hono-typescript/src/app.ts +180 -0
- package/dist/functions/templates/hono-typescript/src/context.ts +103 -0
- package/dist/functions/templates/hono-typescript/src/index.ts +54 -0
- package/dist/functions/templates/hono-typescript/src/middleware/appwrite.ts +119 -0
- package/dist/functions/templates/hono-typescript/tsconfig.json +20 -0
- package/dist/functions/templates/typescript-node/README.md +32 -0
- package/dist/functions/templates/typescript-node/package.json +25 -0
- package/dist/functions/templates/typescript-node/src/context.ts +103 -0
- package/dist/functions/templates/typescript-node/src/index.ts +29 -0
- package/dist/functions/templates/typescript-node/tsconfig.json +28 -0
- package/dist/functions/templates/uv/README.md +31 -0
- package/dist/functions/templates/uv/pyproject.toml +30 -0
- package/dist/functions/templates/uv/src/__init__.py +0 -0
- package/dist/functions/templates/uv/src/context.py +125 -0
- package/dist/functions/templates/uv/src/index.py +46 -0
- package/dist/main.js +175 -4
- package/dist/migrations/appwriteToX.d.ts +27 -2
- package/dist/migrations/appwriteToX.js +293 -69
- package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +1 -1
- package/dist/migrations/yaml/generateImportSchemas.js +23 -8
- package/dist/shared/schemaGenerator.js +25 -12
- package/dist/shared/selectionDialogs.d.ts +214 -0
- package/dist/shared/selectionDialogs.js +540 -0
- package/dist/utils/configDiscovery.d.ts +4 -4
- package/dist/utils/configDiscovery.js +66 -30
- package/dist/utils/yamlConverter.d.ts +1 -0
- package/dist/utils/yamlConverter.js +26 -3
- package/dist/utilsController.d.ts +7 -1
- package/dist/utilsController.js +198 -17
- package/package.json +4 -2
- package/scripts/copy-templates.ts +23 -0
- package/src/cli/commands/databaseCommands.ts +133 -34
- package/src/config/services/ConfigLoaderService.ts +62 -1
- package/src/functions/deployments.ts +10 -35
- package/src/functions/methods.ts +4 -2
- package/src/functions/pathResolution.ts +227 -0
- package/src/main.ts +276 -34
- package/src/migrations/appwriteToX.ts +385 -90
- package/src/migrations/yaml/generateImportSchemas.ts +26 -8
- package/src/shared/schemaGenerator.ts +29 -12
- package/src/shared/selectionDialogs.ts +745 -0
- package/src/utils/configDiscovery.ts +83 -39
- package/src/utils/yamlConverter.ts +29 -3
- package/src/utilsController.ts +250 -22
- package/dist/utils/schemaStrings.d.ts +0 -14
- package/dist/utils/schemaStrings.js +0 -428
- package/dist/utils/sessionPreservationExample.d.ts +0 -1666
- package/dist/utils/sessionPreservationExample.js +0 -101
|
@@ -3,6 +3,9 @@ import chalk from "chalk";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { MessageFormatter } from "../../shared/messageFormatter.js";
|
|
5
5
|
import { ConfirmationDialogs } from "../../shared/confirmationDialogs.js";
|
|
6
|
+
import { SelectionDialogs } from "../../shared/selectionDialogs.js";
|
|
7
|
+
import type { DatabaseSelection, BucketSelection } from "../../shared/selectionDialogs.js";
|
|
8
|
+
import { logger } from "../../shared/logging.js";
|
|
6
9
|
import { fetchAllDatabases } from "../../databases/methods.js";
|
|
7
10
|
import { listBuckets } from "../../storage/methods.js";
|
|
8
11
|
import { getFunction, downloadLatestFunctionDeployment } from "../../functions/methods.js";
|
|
@@ -12,23 +15,40 @@ export const databaseCommands = {
|
|
|
12
15
|
async syncDb(cli: InteractiveCLI): Promise<void> {
|
|
13
16
|
MessageFormatter.progress("Pushing local configuration to Appwrite...", { prefix: "Database" });
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
true
|
|
19
|
-
);
|
|
18
|
+
try {
|
|
19
|
+
// Initialize controller
|
|
20
|
+
await (cli as any).controller!.init();
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
22
|
+
// Get available and configured databases
|
|
23
|
+
const availableDatabases = await fetchAllDatabases((cli as any).controller!.database!);
|
|
24
|
+
const configuredDatabases = (cli as any).controller!.config?.databases || [];
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
// Get local collections for selection
|
|
27
|
+
const localCollections = (cli as any).getLocalCollections();
|
|
28
|
+
|
|
29
|
+
// Push operations always use local configuration as source of truth
|
|
30
|
+
|
|
31
|
+
// Select databases
|
|
32
|
+
const selectedDatabaseIds = await SelectionDialogs.selectDatabases(
|
|
33
|
+
availableDatabases,
|
|
34
|
+
configuredDatabases,
|
|
35
|
+
{ showSelectAll: true, allowNewOnly: false }
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (selectedDatabaseIds.length === 0) {
|
|
39
|
+
MessageFormatter.warning("No databases selected. Skipping database sync.", { prefix: "Database" });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Select tables/collections for each database using the existing method
|
|
44
|
+
const tableSelectionsMap = new Map<string, string[]>();
|
|
45
|
+
const availableTablesMap = new Map<string, any[]>();
|
|
30
46
|
|
|
31
|
-
|
|
47
|
+
for (const databaseId of selectedDatabaseIds) {
|
|
48
|
+
const database = availableDatabases.find(db => db.$id === databaseId)!;
|
|
49
|
+
|
|
50
|
+
// Use the existing selectCollectionsAndTables method
|
|
51
|
+
const selectedCollections = await (cli as any).selectCollectionsAndTables(
|
|
32
52
|
database,
|
|
33
53
|
(cli as any).controller!.database!,
|
|
34
54
|
chalk.blue(`Select collections/tables to push to "${database.name}":`),
|
|
@@ -36,19 +56,93 @@ export const databaseCommands = {
|
|
|
36
56
|
true // prefer local
|
|
37
57
|
);
|
|
38
58
|
|
|
39
|
-
|
|
59
|
+
// Map selected collections to table IDs
|
|
60
|
+
const selectedTableIds = selectedCollections.map((c: any) => c.$id || c.id);
|
|
61
|
+
|
|
62
|
+
// Store selections
|
|
63
|
+
tableSelectionsMap.set(databaseId, selectedTableIds);
|
|
64
|
+
availableTablesMap.set(databaseId, selectedCollections);
|
|
65
|
+
|
|
66
|
+
if (selectedCollections.length === 0) {
|
|
40
67
|
MessageFormatter.warning(`No collections selected for database "${database.name}". Skipping.`, { prefix: "Database" });
|
|
41
68
|
continue;
|
|
42
69
|
}
|
|
70
|
+
}
|
|
43
71
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
72
|
+
// Ask if user wants to select buckets
|
|
73
|
+
const { selectBuckets } = await inquirer.prompt([
|
|
74
|
+
{
|
|
75
|
+
type: "confirm",
|
|
76
|
+
name: "selectBuckets",
|
|
77
|
+
message: "Do you want to select storage buckets to sync as well?",
|
|
78
|
+
default: false,
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
let bucketSelections: BucketSelection[] = [];
|
|
83
|
+
|
|
84
|
+
if (selectBuckets) {
|
|
85
|
+
// Get available and configured buckets
|
|
86
|
+
try {
|
|
87
|
+
const availableBucketsResponse = await listBuckets((cli as any).controller!.storage!);
|
|
88
|
+
const availableBuckets = availableBucketsResponse.buckets || [];
|
|
89
|
+
const configuredBuckets = (cli as any).controller!.config?.buckets || [];
|
|
90
|
+
|
|
91
|
+
if (availableBuckets.length === 0) {
|
|
92
|
+
MessageFormatter.warning("No storage buckets available in remote instance.", { prefix: "Database" });
|
|
93
|
+
} else {
|
|
94
|
+
// Select buckets using SelectionDialogs
|
|
95
|
+
const selectedBucketIds = await SelectionDialogs.selectBucketsForDatabases(
|
|
96
|
+
selectedDatabaseIds,
|
|
97
|
+
availableBuckets,
|
|
98
|
+
configuredBuckets,
|
|
99
|
+
{ showSelectAll: true, groupByDatabase: true }
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
if (selectedBucketIds.length > 0) {
|
|
103
|
+
// Create BucketSelection objects
|
|
104
|
+
bucketSelections = SelectionDialogs.createBucketSelection(
|
|
105
|
+
selectedBucketIds,
|
|
106
|
+
availableBuckets,
|
|
107
|
+
configuredBuckets,
|
|
108
|
+
availableDatabases
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
MessageFormatter.info(`Selected ${bucketSelections.length} storage bucket(s)`, { prefix: "Database" });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
} catch (error) {
|
|
115
|
+
MessageFormatter.warning("Failed to fetch storage buckets. Continuing with databases only.", { prefix: "Database" });
|
|
116
|
+
logger.warn("Storage bucket fetch failed during syncDb", { error: error instanceof Error ? error.message : String(error) });
|
|
117
|
+
}
|
|
50
118
|
}
|
|
51
119
|
|
|
120
|
+
// Create DatabaseSelection objects
|
|
121
|
+
const databaseSelections = SelectionDialogs.createDatabaseSelection(
|
|
122
|
+
selectedDatabaseIds,
|
|
123
|
+
availableDatabases,
|
|
124
|
+
tableSelectionsMap,
|
|
125
|
+
configuredDatabases,
|
|
126
|
+
availableTablesMap
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// Show confirmation summary
|
|
130
|
+
const selectionSummary = SelectionDialogs.createSyncSelectionSummary(
|
|
131
|
+
databaseSelections,
|
|
132
|
+
bucketSelections
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary, 'push');
|
|
136
|
+
|
|
137
|
+
if (!confirmed) {
|
|
138
|
+
MessageFormatter.info("Push operation cancelled by user", { prefix: "Database" });
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Perform selective push using the controller
|
|
143
|
+
MessageFormatter.progress("Starting selective push...", { prefix: "Database" });
|
|
144
|
+
await (cli as any).controller!.selectivePush(databaseSelections, bucketSelections);
|
|
145
|
+
|
|
52
146
|
MessageFormatter.success("\n✅ All database configurations pushed successfully!", { prefix: "Database" });
|
|
53
147
|
|
|
54
148
|
// Then handle functions if requested
|
|
@@ -104,23 +198,28 @@ export const databaseCommands = {
|
|
|
104
198
|
(cli as any).controller!.database!
|
|
105
199
|
);
|
|
106
200
|
|
|
107
|
-
//
|
|
108
|
-
MessageFormatter.progress("Pulling collections and generating collection files...", { prefix: "Collections" });
|
|
109
|
-
await (cli as any).controller!.synchronizeConfigurations(remoteDatabases);
|
|
110
|
-
|
|
111
|
-
// Also configure buckets for any new databases
|
|
201
|
+
// First, prepare the combined database list for bucket configuration
|
|
112
202
|
const localDatabases = (cli as any).controller!.config?.databases || [];
|
|
113
|
-
const
|
|
203
|
+
const allDatabases = [
|
|
204
|
+
...localDatabases,
|
|
205
|
+
...remoteDatabases.filter(
|
|
206
|
+
(rd: any) => !localDatabases.some((ld: any) => ld.name === rd.name)
|
|
207
|
+
),
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
// Configure buckets FIRST to get user selections before writing config
|
|
211
|
+
MessageFormatter.progress("Configuring storage buckets...", { prefix: "Buckets" });
|
|
212
|
+
const configWithBuckets = await (cli as any).configureBuckets({
|
|
114
213
|
...(cli as any).controller!.config!,
|
|
115
|
-
databases:
|
|
116
|
-
...localDatabases,
|
|
117
|
-
...remoteDatabases.filter(
|
|
118
|
-
(rd: any) => !localDatabases.some((ld: any) => ld.name === rd.name)
|
|
119
|
-
),
|
|
120
|
-
],
|
|
214
|
+
databases: allDatabases,
|
|
121
215
|
});
|
|
122
216
|
|
|
123
|
-
|
|
217
|
+
// Update controller config with bucket selections
|
|
218
|
+
(cli as any).controller!.config = configWithBuckets;
|
|
219
|
+
|
|
220
|
+
// Now synchronize configurations with the updated config that includes bucket selections
|
|
221
|
+
MessageFormatter.progress("Pulling collections and generating collection files...", { prefix: "Collections" });
|
|
222
|
+
await (cli as any).controller!.synchronizeConfigurations(remoteDatabases, configWithBuckets);
|
|
124
223
|
}
|
|
125
224
|
|
|
126
225
|
// Then sync functions
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { resolve as resolvePath, dirname, isAbsolute } from "node:path";
|
|
3
4
|
import yaml from "js-yaml";
|
|
4
5
|
import { register } from "tsx/esm/api";
|
|
5
6
|
import { pathToFileURL } from "node:url";
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
type CollectionDiscoveryResult,
|
|
20
21
|
type TableDiscoveryResult,
|
|
21
22
|
} from "../../utils/configDiscovery.js";
|
|
23
|
+
import { expandTildePath } from "../../functions/pathResolution.js";
|
|
22
24
|
|
|
23
25
|
/**
|
|
24
26
|
* Options for loading collections or tables
|
|
@@ -51,6 +53,35 @@ export interface CollectionLoadOptions {
|
|
|
51
53
|
* - Validates and normalizes configuration data
|
|
52
54
|
*/
|
|
53
55
|
export class ConfigLoaderService {
|
|
56
|
+
/**
|
|
57
|
+
* Normalizes function dirPath to absolute path
|
|
58
|
+
* @param func Function configuration object
|
|
59
|
+
* @param configDir Directory containing the config file
|
|
60
|
+
* @returns Function with normalized dirPath
|
|
61
|
+
*/
|
|
62
|
+
private normalizeFunctionPath(func: any, configDir: string): any {
|
|
63
|
+
if (!func.dirPath) {
|
|
64
|
+
return func;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Expand tilde first
|
|
68
|
+
const expandedPath = expandTildePath(func.dirPath);
|
|
69
|
+
|
|
70
|
+
// If already absolute, return as-is
|
|
71
|
+
if (isAbsolute(expandedPath)) {
|
|
72
|
+
return {
|
|
73
|
+
...func,
|
|
74
|
+
dirPath: expandedPath
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Resolve relative to config directory
|
|
79
|
+
return {
|
|
80
|
+
...func,
|
|
81
|
+
dirPath: resolvePath(configDir, expandedPath)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
54
85
|
/**
|
|
55
86
|
* Loads configuration from a discovered path, auto-detecting the type
|
|
56
87
|
* @param configPath Path to the configuration file
|
|
@@ -78,6 +109,13 @@ export class ConfigLoaderService {
|
|
|
78
109
|
);
|
|
79
110
|
}
|
|
80
111
|
|
|
112
|
+
const configDir = path.dirname(configPath);
|
|
113
|
+
|
|
114
|
+
// Normalize function paths
|
|
115
|
+
const normalizedFunctions = partialConfig.functions
|
|
116
|
+
? partialConfig.functions.map(func => this.normalizeFunctionPath(func, configDir))
|
|
117
|
+
: [];
|
|
118
|
+
|
|
81
119
|
return {
|
|
82
120
|
appwriteEndpoint: partialConfig.appwriteEndpoint,
|
|
83
121
|
appwriteProject: partialConfig.appwriteProject,
|
|
@@ -105,7 +143,7 @@ export class ConfigLoaderService {
|
|
|
105
143
|
},
|
|
106
144
|
databases: partialConfig.databases || [],
|
|
107
145
|
buckets: partialConfig.buckets || [],
|
|
108
|
-
functions:
|
|
146
|
+
functions: normalizedFunctions,
|
|
109
147
|
collections: partialConfig.collections || [],
|
|
110
148
|
sessionCookie: partialConfig.sessionCookie,
|
|
111
149
|
authMethod: partialConfig.authMethod || "auto",
|
|
@@ -155,6 +193,13 @@ export class ConfigLoaderService {
|
|
|
155
193
|
|
|
156
194
|
// Load collections and tables from their respective directories
|
|
157
195
|
const configDir = path.dirname(yamlPath);
|
|
196
|
+
|
|
197
|
+
// Normalize function paths
|
|
198
|
+
if (config.functions) {
|
|
199
|
+
config.functions = config.functions.map(func =>
|
|
200
|
+
this.normalizeFunctionPath(func, configDir)
|
|
201
|
+
);
|
|
202
|
+
}
|
|
158
203
|
const collectionsDir = path.join(configDir, config.schemaConfig?.collectionsDirectory || "collections");
|
|
159
204
|
const tablesDir = path.join(configDir, config.schemaConfig?.tablesDirectory || "tables");
|
|
160
205
|
|
|
@@ -248,6 +293,14 @@ export class ConfigLoaderService {
|
|
|
248
293
|
throw new Error(`Failed to load TypeScript config from: ${tsPath}`);
|
|
249
294
|
}
|
|
250
295
|
|
|
296
|
+
// Normalize function paths
|
|
297
|
+
const configDir = path.dirname(tsPath);
|
|
298
|
+
if (config.functions) {
|
|
299
|
+
config.functions = config.functions.map(func =>
|
|
300
|
+
this.normalizeFunctionPath(func, configDir)
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
251
304
|
MessageFormatter.success(`Loaded TypeScript config from: ${tsPath}`, {
|
|
252
305
|
prefix: "Config",
|
|
253
306
|
});
|
|
@@ -292,6 +345,14 @@ export class ConfigLoaderService {
|
|
|
292
345
|
appwriteConfig.collections = collections;
|
|
293
346
|
}
|
|
294
347
|
|
|
348
|
+
// Normalize function paths
|
|
349
|
+
const configDir = path.dirname(jsonPath);
|
|
350
|
+
if (appwriteConfig.functions) {
|
|
351
|
+
appwriteConfig.functions = appwriteConfig.functions.map(func =>
|
|
352
|
+
this.normalizeFunctionPath(func, configDir)
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
|
|
295
356
|
MessageFormatter.success(`Loaded project config from: ${jsonPath}`, {
|
|
296
357
|
prefix: "Config",
|
|
297
358
|
});
|
|
@@ -16,30 +16,7 @@ import {
|
|
|
16
16
|
} from "./methods.js";
|
|
17
17
|
import ignore from "ignore";
|
|
18
18
|
import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
19
|
-
|
|
20
|
-
const findFunctionDirectory = (
|
|
21
|
-
basePath: string,
|
|
22
|
-
functionName: string
|
|
23
|
-
): string | undefined => {
|
|
24
|
-
const normalizedName = functionName.toLowerCase().replace(/\s+/g, "-");
|
|
25
|
-
const dirs = fs.readdirSync(basePath, { withFileTypes: true });
|
|
26
|
-
|
|
27
|
-
for (const dir of dirs) {
|
|
28
|
-
if (dir.isDirectory()) {
|
|
29
|
-
const fullPath = join(basePath, dir.name);
|
|
30
|
-
if (dir.name.toLowerCase() === normalizedName) {
|
|
31
|
-
return fullPath;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const nestedResult = findFunctionDirectory(fullPath, functionName);
|
|
35
|
-
if (nestedResult) {
|
|
36
|
-
return nestedResult;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return undefined;
|
|
42
|
-
};
|
|
19
|
+
import { resolveFunctionDirectory, validateFunctionDirectory } from './pathResolution.js';
|
|
43
20
|
|
|
44
21
|
export const deployFunction = async (
|
|
45
22
|
client: Client,
|
|
@@ -183,18 +160,16 @@ export const deployLocalFunction = async (
|
|
|
183
160
|
functionExists = false;
|
|
184
161
|
}
|
|
185
162
|
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
functionName.toLowerCase().replace(/\s+/g, "-")
|
|
194
|
-
);
|
|
163
|
+
const configDirPath = process.cwd(); // TODO: This should be passed from caller
|
|
164
|
+
const resolvedPath = resolveFunctionDirectory(
|
|
165
|
+
functionName,
|
|
166
|
+
configDirPath,
|
|
167
|
+
functionConfig.dirPath,
|
|
168
|
+
functionPath
|
|
169
|
+
);
|
|
195
170
|
|
|
196
|
-
if (!
|
|
197
|
-
throw new Error(`Function directory
|
|
171
|
+
if (!validateFunctionDirectory(resolvedPath)) {
|
|
172
|
+
throw new Error(`Function directory is invalid or missing required files: ${resolvedPath}`);
|
|
198
173
|
}
|
|
199
174
|
|
|
200
175
|
if (functionConfig.predeployCommands?.length) {
|
package/src/functions/methods.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
import chalk from "chalk";
|
|
18
18
|
import { extract as extractTar } from "tar";
|
|
19
19
|
import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
20
|
+
import { expandTildePath, normalizeFunctionName } from "./pathResolution.js";
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Validates and filters events array for Appwrite functions
|
|
@@ -72,7 +73,7 @@ export const downloadLatestFunctionDeployment = async (
|
|
|
72
73
|
// Create function directory using provided basePath
|
|
73
74
|
const functionDir = join(
|
|
74
75
|
basePath,
|
|
75
|
-
functionInfo.name
|
|
76
|
+
normalizeFunctionName(functionInfo.name)
|
|
76
77
|
);
|
|
77
78
|
await fs.promises.mkdir(functionDir, { recursive: true });
|
|
78
79
|
|
|
@@ -219,7 +220,8 @@ export const createFunctionTemplate = async (
|
|
|
219
220
|
functionName: string,
|
|
220
221
|
basePath: string = "./functions"
|
|
221
222
|
) => {
|
|
222
|
-
const
|
|
223
|
+
const expandedBasePath = expandTildePath(basePath);
|
|
224
|
+
const functionPath = join(expandedBasePath, functionName);
|
|
223
225
|
const currentFileUrl = import.meta.url;
|
|
224
226
|
const currentDir = dirname(fileURLToPath(currentFileUrl));
|
|
225
227
|
const templatesPath = join(currentDir, "templates", templateType);
|
|
@@ -0,0 +1,227 @@
|
|
|
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
|
+
/**
|
|
8
|
+
* Expands tilde (~) in paths to the user's home directory
|
|
9
|
+
* @param pathStr - Path string that may contain ~
|
|
10
|
+
* @returns Expanded path with home directory
|
|
11
|
+
*/
|
|
12
|
+
export function expandTildePath(pathStr: string): string {
|
|
13
|
+
if (!pathStr) return pathStr;
|
|
14
|
+
|
|
15
|
+
if (pathStr.startsWith('~/') || pathStr === '~') {
|
|
16
|
+
const expandedPath = pathStr.replace(/^~(?=$|\/|\\)/, homedir());
|
|
17
|
+
logger.debug('Expanded tilde path', { original: pathStr, expanded: expandedPath });
|
|
18
|
+
return expandedPath;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return pathStr;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Normalizes function name to standard format (lowercase, dashes instead of spaces)
|
|
26
|
+
* @param name - Function name to normalize
|
|
27
|
+
* @returns Normalized function name
|
|
28
|
+
*/
|
|
29
|
+
export function normalizeFunctionName(name: string): string {
|
|
30
|
+
if (!name) return name;
|
|
31
|
+
|
|
32
|
+
const normalized = name.toLowerCase().replace(/\s+/g, '-');
|
|
33
|
+
|
|
34
|
+
if (normalized !== name) {
|
|
35
|
+
logger.debug('Normalized function name', { original: name, normalized });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return normalized;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validates that a directory exists and contains function markers
|
|
43
|
+
* @param dirPath - Directory path to validate
|
|
44
|
+
* @returns True if directory is a valid function directory
|
|
45
|
+
*/
|
|
46
|
+
export function validateFunctionDirectory(dirPath: string): boolean {
|
|
47
|
+
try {
|
|
48
|
+
// Check if directory exists
|
|
49
|
+
if (!existsSync(dirPath)) {
|
|
50
|
+
logger.debug('Directory does not exist', { dirPath });
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Check if it's actually a directory
|
|
55
|
+
const stats = statSync(dirPath);
|
|
56
|
+
if (!stats.isDirectory()) {
|
|
57
|
+
logger.debug('Path is not a directory', { dirPath });
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check for function markers
|
|
62
|
+
const contents = readdirSync(dirPath);
|
|
63
|
+
const hasPackageJson = contents.includes('package.json');
|
|
64
|
+
const hasPyprojectToml = contents.includes('pyproject.toml');
|
|
65
|
+
const hasSrcDir = contents.includes('src');
|
|
66
|
+
|
|
67
|
+
const isValid = hasPackageJson || hasPyprojectToml || hasSrcDir;
|
|
68
|
+
|
|
69
|
+
logger.debug('Function directory validation', {
|
|
70
|
+
dirPath,
|
|
71
|
+
isValid,
|
|
72
|
+
markers: {
|
|
73
|
+
hasPackageJson,
|
|
74
|
+
hasPyprojectToml,
|
|
75
|
+
hasSrcDir
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return isValid;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
logger.debug('Error validating function directory', {
|
|
82
|
+
dirPath,
|
|
83
|
+
error: error instanceof Error ? error.message : String(error)
|
|
84
|
+
});
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Helper function to search for function in standard locations
|
|
91
|
+
* @param configDirPath - Directory where config file is located
|
|
92
|
+
* @param normalizedName - Normalized function name
|
|
93
|
+
* @returns First valid function directory path or undefined
|
|
94
|
+
*/
|
|
95
|
+
export function findFunctionInStandardLocations(
|
|
96
|
+
configDirPath: string,
|
|
97
|
+
normalizedName: string
|
|
98
|
+
): string | undefined {
|
|
99
|
+
const searchPaths = [
|
|
100
|
+
// Same directory as config
|
|
101
|
+
join(configDirPath, 'functions', normalizedName),
|
|
102
|
+
// Parent directory of config
|
|
103
|
+
join(configDirPath, '..', 'functions', normalizedName),
|
|
104
|
+
// Current working directory
|
|
105
|
+
join(process.cwd(), 'functions', normalizedName),
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
logger.debug('Searching for function in standard locations', {
|
|
109
|
+
normalizedName,
|
|
110
|
+
configDirPath,
|
|
111
|
+
searchPaths
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
for (const searchPath of searchPaths) {
|
|
115
|
+
const resolvedPath = resolve(searchPath);
|
|
116
|
+
logger.debug('Checking search path', { searchPath, resolvedPath });
|
|
117
|
+
|
|
118
|
+
if (validateFunctionDirectory(resolvedPath)) {
|
|
119
|
+
logger.debug('Found function in standard location', { resolvedPath });
|
|
120
|
+
return resolvedPath;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
logger.debug('Function not found in any standard location', { normalizedName });
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Resolves the absolute path to a function directory
|
|
130
|
+
* Handles multiple resolution strategies with proper priority
|
|
131
|
+
*
|
|
132
|
+
* @param functionName - Name of the function
|
|
133
|
+
* @param configDirPath - Directory where config file is located
|
|
134
|
+
* @param dirPath - Optional explicit dirPath from config
|
|
135
|
+
* @param explicitPath - Optional path passed as parameter (highest priority)
|
|
136
|
+
* @returns Absolute path to the function directory
|
|
137
|
+
* @throws Error if function directory cannot be found or is invalid
|
|
138
|
+
*/
|
|
139
|
+
export function resolveFunctionDirectory(
|
|
140
|
+
functionName: string,
|
|
141
|
+
configDirPath: string,
|
|
142
|
+
dirPath?: string,
|
|
143
|
+
explicitPath?: string
|
|
144
|
+
): string {
|
|
145
|
+
logger.debug('Resolving function directory', {
|
|
146
|
+
functionName,
|
|
147
|
+
configDirPath,
|
|
148
|
+
dirPath,
|
|
149
|
+
explicitPath
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const normalizedName = normalizeFunctionName(functionName);
|
|
153
|
+
|
|
154
|
+
// Priority 1: Explicit path parameter (highest priority)
|
|
155
|
+
if (explicitPath) {
|
|
156
|
+
logger.debug('Using explicit path parameter');
|
|
157
|
+
const expandedPath = expandTildePath(explicitPath);
|
|
158
|
+
const resolvedPath = isAbsolute(expandedPath)
|
|
159
|
+
? expandedPath
|
|
160
|
+
: resolve(process.cwd(), expandedPath);
|
|
161
|
+
|
|
162
|
+
if (!validateFunctionDirectory(resolvedPath)) {
|
|
163
|
+
const errorMsg = `Explicit path is not a valid function directory: ${resolvedPath}`;
|
|
164
|
+
logger.error(errorMsg);
|
|
165
|
+
MessageFormatter.error('Invalid function directory', errorMsg, { prefix: 'Path Resolution' });
|
|
166
|
+
throw new Error(errorMsg);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
logger.debug('Resolved using explicit path', { resolvedPath });
|
|
170
|
+
MessageFormatter.debug(`Resolved function directory using explicit path: ${resolvedPath}`, undefined, { prefix: 'Path Resolution' });
|
|
171
|
+
return resolvedPath;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Priority 2: dirPath from config (relative to config location)
|
|
175
|
+
if (dirPath) {
|
|
176
|
+
logger.debug('Using dirPath from config');
|
|
177
|
+
const expandedPath = expandTildePath(dirPath);
|
|
178
|
+
const resolvedPath = isAbsolute(expandedPath)
|
|
179
|
+
? expandedPath
|
|
180
|
+
: resolve(configDirPath, expandedPath);
|
|
181
|
+
|
|
182
|
+
if (!validateFunctionDirectory(resolvedPath)) {
|
|
183
|
+
const errorMsg = `Config dirPath is not a valid function directory: ${resolvedPath}`;
|
|
184
|
+
logger.error(errorMsg);
|
|
185
|
+
MessageFormatter.error('Invalid function directory', errorMsg, { prefix: 'Path Resolution' });
|
|
186
|
+
throw new Error(errorMsg);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
logger.debug('Resolved using config dirPath', { resolvedPath });
|
|
190
|
+
MessageFormatter.debug(`Resolved function directory using config dirPath: ${resolvedPath}`, undefined, { prefix: 'Path Resolution' });
|
|
191
|
+
return resolvedPath;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Priority 3: Search standard locations
|
|
195
|
+
logger.debug('Searching standard locations for function');
|
|
196
|
+
const foundPath = findFunctionInStandardLocations(configDirPath, normalizedName);
|
|
197
|
+
|
|
198
|
+
if (foundPath) {
|
|
199
|
+
logger.debug('Resolved using standard location search', { foundPath });
|
|
200
|
+
MessageFormatter.debug(`Found function directory in standard location: ${foundPath}`, undefined, { prefix: 'Path Resolution' });
|
|
201
|
+
return foundPath;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Priority 4: Not found - throw error
|
|
205
|
+
const searchedLocations = [
|
|
206
|
+
join(configDirPath, 'functions', normalizedName),
|
|
207
|
+
join(configDirPath, '..', 'functions', normalizedName),
|
|
208
|
+
join(process.cwd(), 'functions', normalizedName),
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
const errorMsg = `Function directory not found for '${functionName}' (normalized: '${normalizedName}'). ` +
|
|
212
|
+
`Searched locations:\n${searchedLocations.map(p => ` - ${p}`).join('\n')}`;
|
|
213
|
+
|
|
214
|
+
logger.error('Function directory not found', {
|
|
215
|
+
functionName,
|
|
216
|
+
normalizedName,
|
|
217
|
+
searchedLocations
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
MessageFormatter.error(
|
|
221
|
+
'Function directory not found',
|
|
222
|
+
errorMsg,
|
|
223
|
+
{ prefix: 'Path Resolution' }
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
throw new Error(errorMsg);
|
|
227
|
+
}
|