appwrite-utils-cli 1.7.8 → 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/dist/cli/commands/databaseCommands.js +7 -8
- 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 +8 -8
- package/dist/shared/selectionDialogs.d.ts +1 -1
- package/dist/shared/selectionDialogs.js +31 -7
- package/dist/utilsController.d.ts +2 -1
- package/dist/utilsController.js +111 -19
- package/package.json +4 -2
- package/scripts/copy-templates.ts +23 -0
- package/src/cli/commands/databaseCommands.ts +7 -8
- 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 +8 -8
- package/src/shared/selectionDialogs.ts +36 -7
- package/src/utilsController.ts +138 -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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Python UV Function Template
|
|
2
|
+
|
|
3
|
+
This is a Python template for Appwrite Functions using UV for fast dependency management.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
- `src/index.py`: Main function entry point
|
|
7
|
+
- `pyproject.toml`: UV/PyPI project configuration and dependencies
|
|
8
|
+
|
|
9
|
+
## Usage
|
|
10
|
+
Your function will receive a context object with:
|
|
11
|
+
- `req`: Request object containing request data, headers, and environment variables
|
|
12
|
+
- `res`: Response object for sending responses
|
|
13
|
+
- `log`: Function for logging (shows in your function logs)
|
|
14
|
+
- `error`: Function for error logging
|
|
15
|
+
|
|
16
|
+
## Example Request
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"key": "value"
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Development
|
|
24
|
+
1. Install UV: `curl -LsSf https://astral.sh/uv/install.sh | sh`
|
|
25
|
+
2. Install dependencies: `uv sync`
|
|
26
|
+
3. Run locally: `uv run python src/index.py`
|
|
27
|
+
4. Deploy: Dependencies will be installed during deployment
|
|
28
|
+
|
|
29
|
+
## Deployment
|
|
30
|
+
Make sure it's inside `appwriteConfig.yaml` functions array, and if you want to install dependencies FIRST, before Appwrite (using your system), you can
|
|
31
|
+
add the `predeployCommands` to the function in `appwriteConfig.yaml`.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "{{functionName}}"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Appwrite Python function using UV"
|
|
5
|
+
authors = [{name = "Your Name", email = "you@example.com"}]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.9"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"appwrite>=7.0.1",
|
|
10
|
+
"pydantic>=2.0.0",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
[build-system]
|
|
14
|
+
requires = ["hatchling"]
|
|
15
|
+
build-backend = "hatchling.build"
|
|
16
|
+
|
|
17
|
+
[tool.uv]
|
|
18
|
+
dev-dependencies = [
|
|
19
|
+
"pytest>=7.0.0",
|
|
20
|
+
"black>=23.0.0",
|
|
21
|
+
"ruff>=0.1.0",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.ruff]
|
|
25
|
+
line-length = 88
|
|
26
|
+
target-version = "py39"
|
|
27
|
+
|
|
28
|
+
[tool.ruff.lint]
|
|
29
|
+
select = ["E", "F", "W", "I"]
|
|
30
|
+
ignore = ["E501"]
|
|
File without changes
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import Any, Literal, Protocol
|
|
3
|
+
|
|
4
|
+
from pydantic import BaseModel, Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AppwriteHeaders(BaseModel):
|
|
8
|
+
"""Appwrite request headers"""
|
|
9
|
+
|
|
10
|
+
x_appwrite_trigger: Literal["http", "schedule", "event"] | None = Field(
|
|
11
|
+
None, alias="x-appwrite-trigger"
|
|
12
|
+
)
|
|
13
|
+
x_appwrite_event: str | None = Field(None, alias="x-appwrite-event")
|
|
14
|
+
x_appwrite_key: str | None = Field(None, alias="x-appwrite-key")
|
|
15
|
+
x_appwrite_user_id: str | None = Field(None, alias="x-appwrite-user-id")
|
|
16
|
+
x_appwrite_user_jwt: str | None = Field(None, alias="x-appwrite-user-jwt")
|
|
17
|
+
x_appwrite_country_code: str | None = Field(None, alias="x-appwrite-country-code")
|
|
18
|
+
x_appwrite_continent_code: str | None = Field(
|
|
19
|
+
None, alias="x-appwrite-continent-code"
|
|
20
|
+
)
|
|
21
|
+
x_appwrite_continent_eu: str | None = Field(None, alias="x-appwrite-continent-eu")
|
|
22
|
+
|
|
23
|
+
# Allow additional headers
|
|
24
|
+
model_config = {"extra": "allow", "populate_by_name": True}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class AppwriteEnv(BaseModel):
|
|
28
|
+
"""Appwrite environment variables"""
|
|
29
|
+
|
|
30
|
+
APPWRITE_FUNCTION_API_ENDPOINT: str
|
|
31
|
+
APPWRITE_VERSION: str
|
|
32
|
+
APPWRITE_REGION: str
|
|
33
|
+
APPWRITE_FUNCTION_API_KEY: str | None = None
|
|
34
|
+
APPWRITE_FUNCTION_ID: str
|
|
35
|
+
APPWRITE_FUNCTION_NAME: str
|
|
36
|
+
APPWRITE_FUNCTION_DEPLOYMENT: str
|
|
37
|
+
APPWRITE_FUNCTION_PROJECT_ID: str
|
|
38
|
+
APPWRITE_FUNCTION_RUNTIME_NAME: str
|
|
39
|
+
APPWRITE_FUNCTION_RUNTIME_VERSION: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class AppwriteRequest(Protocol):
|
|
43
|
+
"""Appwrite function request object with proper callable typing"""
|
|
44
|
+
|
|
45
|
+
bodyText: str | None
|
|
46
|
+
bodyJson: dict[str, Any] | str | None
|
|
47
|
+
body: Any
|
|
48
|
+
headers: dict[str, str]
|
|
49
|
+
scheme: Literal["http", "https"] | None
|
|
50
|
+
method: Literal["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]
|
|
51
|
+
url: str | None
|
|
52
|
+
host: str | None
|
|
53
|
+
port: int | str | None
|
|
54
|
+
path: str | None
|
|
55
|
+
queryString: str | None
|
|
56
|
+
query: dict[str, str | list[str]] | None
|
|
57
|
+
variables: dict[str, str] | None
|
|
58
|
+
payload: str | None
|
|
59
|
+
|
|
60
|
+
async def text(self) -> str: ...
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AppwriteResponse(Protocol):
|
|
64
|
+
"""Appwrite function response object with proper callable typing"""
|
|
65
|
+
|
|
66
|
+
def json(
|
|
67
|
+
self,
|
|
68
|
+
data: Any,
|
|
69
|
+
status: int | None = None,
|
|
70
|
+
headers: dict[str, str] | None = None,
|
|
71
|
+
) -> None: ...
|
|
72
|
+
|
|
73
|
+
def text(
|
|
74
|
+
self,
|
|
75
|
+
body: str,
|
|
76
|
+
status: int | None = None,
|
|
77
|
+
headers: dict[str, str] | None = None,
|
|
78
|
+
) -> None: ...
|
|
79
|
+
|
|
80
|
+
def empty(self) -> None: ...
|
|
81
|
+
|
|
82
|
+
def binary(self, data: bytes) -> None: ...
|
|
83
|
+
|
|
84
|
+
def redirect(self, url: str, status: int | None = None) -> None: ...
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AppwriteContext(Protocol):
|
|
88
|
+
"""Complete Appwrite function context with proper typing"""
|
|
89
|
+
|
|
90
|
+
req: AppwriteRequest
|
|
91
|
+
res: AppwriteResponse
|
|
92
|
+
|
|
93
|
+
def log(self, message: str) -> None: ...
|
|
94
|
+
|
|
95
|
+
def error(self, message: str) -> None: ...
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# Alternative: BaseModel version for cases where you need Pydantic features
|
|
99
|
+
# but want proper typing hints
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class AppwriteRequestModel(BaseModel):
|
|
103
|
+
"""Pydantic model for request validation (if needed)"""
|
|
104
|
+
|
|
105
|
+
bodyText: str | None = None
|
|
106
|
+
bodyJson: dict[str, Any] | str | None = None
|
|
107
|
+
body: Any = None
|
|
108
|
+
headers: dict[str, str] = Field(default_factory=dict)
|
|
109
|
+
scheme: Literal["http", "https"] | None = None
|
|
110
|
+
method: Literal["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]
|
|
111
|
+
url: str | None = None
|
|
112
|
+
host: str | None = None
|
|
113
|
+
port: int | str | None = None
|
|
114
|
+
path: str | None = None
|
|
115
|
+
queryString: str | None = None
|
|
116
|
+
query: dict[str, str | list[str]] | None = None
|
|
117
|
+
variables: dict[str, str] | None = None
|
|
118
|
+
payload: str | None = None
|
|
119
|
+
|
|
120
|
+
model_config = {"arbitrary_types_allowed": True}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# Type alias for the log/error functions
|
|
124
|
+
LogFunction = Callable[[str], None]
|
|
125
|
+
ErrorFunction = Callable[[str], None]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from appwrite.client import Client
|
|
3
|
+
from context import AppwriteContext, AppwriteHeaders, AppwriteRequestModel
|
|
4
|
+
|
|
5
|
+
def main(context: AppwriteContext):
|
|
6
|
+
req = context.req
|
|
7
|
+
res = context.res
|
|
8
|
+
log = context.log
|
|
9
|
+
error = context.error
|
|
10
|
+
|
|
11
|
+
# Optional: Validate request headers using Pydantic
|
|
12
|
+
try:
|
|
13
|
+
headers = AppwriteHeaders.model_validate(req.headers)
|
|
14
|
+
log("Headers validation successful")
|
|
15
|
+
except Exception as validation_error:
|
|
16
|
+
error(f"Headers validation failed: {validation_error}")
|
|
17
|
+
|
|
18
|
+
# Optional: Validate full request using Pydantic model
|
|
19
|
+
try:
|
|
20
|
+
request_model = AppwriteRequestModel.model_validate({
|
|
21
|
+
'method': req.method,
|
|
22
|
+
'headers': req.headers,
|
|
23
|
+
'path': req.path,
|
|
24
|
+
'url': req.url,
|
|
25
|
+
'body': req.body,
|
|
26
|
+
'bodyText': req.bodyText,
|
|
27
|
+
'bodyJson': req.bodyJson
|
|
28
|
+
})
|
|
29
|
+
log("Request validation successful")
|
|
30
|
+
except Exception as validation_error:
|
|
31
|
+
error(f"Request validation failed: {validation_error}")
|
|
32
|
+
|
|
33
|
+
client = Client()
|
|
34
|
+
client.set_endpoint(os.getenv('APPWRITE_FUNCTION_ENDPOINT'))
|
|
35
|
+
client.set_project(os.getenv('APPWRITE_FUNCTION_PROJECT_ID'))
|
|
36
|
+
client.set_key(os.getenv('APPWRITE_FUNCTION_API_KEY'))
|
|
37
|
+
|
|
38
|
+
log(f"Processing {req.method} request to {req.path}")
|
|
39
|
+
|
|
40
|
+
return res.json({
|
|
41
|
+
'message': 'Hello from Python function!',
|
|
42
|
+
'functionName': '{{functionName}}',
|
|
43
|
+
'method': req.method,
|
|
44
|
+
'path': req.path,
|
|
45
|
+
'headers': req.headers
|
|
46
|
+
})
|
package/dist/main.js
CHANGED
|
@@ -76,13 +76,13 @@ async function performEnhancedSync(controller, parsedArgv) {
|
|
|
76
76
|
isNew: false
|
|
77
77
|
}));
|
|
78
78
|
const selectionSummary = SelectionDialogs.createSyncSelectionSummary(databaseSelections, bucketSelections);
|
|
79
|
-
const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary);
|
|
79
|
+
const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary, 'pull');
|
|
80
80
|
if (!confirmed) {
|
|
81
|
-
MessageFormatter.info("
|
|
81
|
+
MessageFormatter.info("Pull operation cancelled by user", { prefix: "Sync" });
|
|
82
82
|
return null;
|
|
83
83
|
}
|
|
84
|
-
// Perform sync with existing configuration
|
|
85
|
-
await controller.
|
|
84
|
+
// Perform sync with existing configuration (pull from remote)
|
|
85
|
+
await controller.selectivePull(databaseSelections, bucketSelections);
|
|
86
86
|
return selectionSummary;
|
|
87
87
|
}
|
|
88
88
|
}
|
|
@@ -154,13 +154,13 @@ async function performEnhancedSync(controller, parsedArgv) {
|
|
|
154
154
|
configuredBuckets, availableDatabases);
|
|
155
155
|
// Show final confirmation
|
|
156
156
|
const selectionSummary = SelectionDialogs.createSyncSelectionSummary(databaseSelections, bucketSelections);
|
|
157
|
-
const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary);
|
|
157
|
+
const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary, 'pull');
|
|
158
158
|
if (!confirmed) {
|
|
159
|
-
MessageFormatter.info("
|
|
159
|
+
MessageFormatter.info("Pull operation cancelled by user", { prefix: "Sync" });
|
|
160
160
|
return null;
|
|
161
161
|
}
|
|
162
|
-
// Perform the selective sync
|
|
163
|
-
await controller.
|
|
162
|
+
// Perform the selective sync (pull from remote)
|
|
163
|
+
await controller.selectivePull(databaseSelections, bucketSelections);
|
|
164
164
|
MessageFormatter.success("Enhanced sync completed successfully", { prefix: "Sync" });
|
|
165
165
|
return selectionSummary;
|
|
166
166
|
}
|
|
@@ -182,7 +182,7 @@ export declare class SelectionDialogs {
|
|
|
182
182
|
/**
|
|
183
183
|
* Shows final confirmation dialog with sync selection summary
|
|
184
184
|
*/
|
|
185
|
-
static confirmSyncSelection(selectionSummary: SyncSelectionSummary): Promise<boolean>;
|
|
185
|
+
static confirmSyncSelection(selectionSummary: SyncSelectionSummary, operationType?: 'push' | 'pull' | 'sync'): Promise<boolean>;
|
|
186
186
|
/**
|
|
187
187
|
* Creates a sync selection summary from selected items
|
|
188
188
|
*/
|
|
@@ -359,8 +359,32 @@ export class SelectionDialogs {
|
|
|
359
359
|
/**
|
|
360
360
|
* Shows final confirmation dialog with sync selection summary
|
|
361
361
|
*/
|
|
362
|
-
static async confirmSyncSelection(selectionSummary) {
|
|
363
|
-
|
|
362
|
+
static async confirmSyncSelection(selectionSummary, operationType = 'sync') {
|
|
363
|
+
const labels = {
|
|
364
|
+
push: {
|
|
365
|
+
banner: "Push Selection Summary",
|
|
366
|
+
subtitle: "Review selections before pushing to Appwrite",
|
|
367
|
+
confirm: "Proceed with push operation?",
|
|
368
|
+
success: "Push operation confirmed.",
|
|
369
|
+
cancel: "Push operation cancelled."
|
|
370
|
+
},
|
|
371
|
+
pull: {
|
|
372
|
+
banner: "Pull Selection Summary",
|
|
373
|
+
subtitle: "Review selections before pulling from Appwrite",
|
|
374
|
+
confirm: "Proceed with pull operation?",
|
|
375
|
+
success: "Pull operation confirmed.",
|
|
376
|
+
cancel: "Pull operation cancelled."
|
|
377
|
+
},
|
|
378
|
+
sync: {
|
|
379
|
+
banner: "Sync Selection Summary",
|
|
380
|
+
subtitle: "Review your selections before proceeding",
|
|
381
|
+
confirm: "Proceed with sync operation?",
|
|
382
|
+
success: "Sync operation confirmed.",
|
|
383
|
+
cancel: "Sync operation cancelled."
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
const label = labels[operationType];
|
|
387
|
+
MessageFormatter.banner(label.banner, label.subtitle);
|
|
364
388
|
// Database summary
|
|
365
389
|
console.log(chalk.bold.cyan("\n📊 Databases:"));
|
|
366
390
|
console.log(` Total: ${selectionSummary.totalDatabases}`);
|
|
@@ -395,20 +419,20 @@ export class SelectionDialogs {
|
|
|
395
419
|
const { confirmed } = await inquirer.prompt([{
|
|
396
420
|
type: 'confirm',
|
|
397
421
|
name: 'confirmed',
|
|
398
|
-
message: chalk.green.bold(
|
|
422
|
+
message: chalk.green.bold(label.confirm),
|
|
399
423
|
default: true
|
|
400
424
|
}]);
|
|
401
425
|
if (confirmed) {
|
|
402
|
-
MessageFormatter.success(
|
|
403
|
-
logger.info(
|
|
426
|
+
MessageFormatter.success(label.success, { skipLogging: true });
|
|
427
|
+
logger.info(`${operationType} selection confirmed`, {
|
|
404
428
|
databases: selectionSummary.totalDatabases,
|
|
405
429
|
tables: selectionSummary.totalTables,
|
|
406
430
|
buckets: selectionSummary.totalBuckets
|
|
407
431
|
});
|
|
408
432
|
}
|
|
409
433
|
else {
|
|
410
|
-
MessageFormatter.warning(
|
|
411
|
-
logger.info(
|
|
434
|
+
MessageFormatter.warning(label.cancel, { skipLogging: true });
|
|
435
|
+
logger.info(`${operationType} selection cancelled by user`);
|
|
412
436
|
}
|
|
413
437
|
return confirmed;
|
|
414
438
|
}
|
|
@@ -83,7 +83,8 @@ export declare class UtilsController {
|
|
|
83
83
|
generateSchemas(): Promise<void>;
|
|
84
84
|
importData(options?: SetupOptions): Promise<void>;
|
|
85
85
|
synchronizeConfigurations(databases?: Models.Database[], config?: AppwriteConfig, databaseSelections?: DatabaseSelection[], bucketSelections?: BucketSelection[]): Promise<void>;
|
|
86
|
-
|
|
86
|
+
selectivePull(databaseSelections: DatabaseSelection[], bucketSelections: BucketSelection[]): Promise<void>;
|
|
87
|
+
selectivePush(databaseSelections: DatabaseSelection[], bucketSelections: BucketSelection[]): Promise<void>;
|
|
87
88
|
syncDb(databases?: Models.Database[], collections?: Models.Collection[]): Promise<void>;
|
|
88
89
|
getAppwriteFolderPath(): string | undefined;
|
|
89
90
|
transferData(options: TransferOptions): Promise<void>;
|
package/dist/utilsController.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Client, Databases, Query, Storage, Users, } from "node-appwrite";
|
|
2
2
|
import {} from "appwrite-utils";
|
|
3
|
-
import {
|
|
3
|
+
import { findAppwriteConfig, findFunctionsDir, } from "./utils/loadConfigs.js";
|
|
4
|
+
import { normalizeFunctionName, validateFunctionDirectory } from './functions/pathResolution.js';
|
|
4
5
|
import { UsersController } from "./users/methods.js";
|
|
5
6
|
import { AppwriteToX } from "./migrations/appwriteToX.js";
|
|
6
7
|
import { ImportController } from "./migrations/importController.js";
|
|
@@ -39,6 +40,26 @@ export class UtilsController {
|
|
|
39
40
|
* Get the UtilsController singleton instance
|
|
40
41
|
*/
|
|
41
42
|
static getInstance(currentUserDir, directConfig) {
|
|
43
|
+
// Clear instance if currentUserDir has changed
|
|
44
|
+
if (UtilsController.instance &&
|
|
45
|
+
UtilsController.instance.currentUserDir !== currentUserDir) {
|
|
46
|
+
logger.debug(`Clearing singleton: currentUserDir changed from ${UtilsController.instance.currentUserDir} to ${currentUserDir}`, { prefix: "UtilsController" });
|
|
47
|
+
UtilsController.clearInstance();
|
|
48
|
+
}
|
|
49
|
+
// Clear instance if directConfig endpoint or project has changed
|
|
50
|
+
if (UtilsController.instance && directConfig) {
|
|
51
|
+
const existingConfig = UtilsController.instance.config;
|
|
52
|
+
if (existingConfig) {
|
|
53
|
+
const endpointChanged = directConfig.appwriteEndpoint &&
|
|
54
|
+
existingConfig.appwriteEndpoint !== directConfig.appwriteEndpoint;
|
|
55
|
+
const projectChanged = directConfig.appwriteProject &&
|
|
56
|
+
existingConfig.appwriteProject !== directConfig.appwriteProject;
|
|
57
|
+
if (endpointChanged || projectChanged) {
|
|
58
|
+
logger.debug("Clearing singleton: endpoint or project changed", { prefix: "UtilsController" });
|
|
59
|
+
UtilsController.clearInstance();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
42
63
|
if (!UtilsController.instance) {
|
|
43
64
|
UtilsController.instance = new UtilsController(currentUserDir, directConfig);
|
|
44
65
|
}
|
|
@@ -295,9 +316,14 @@ export class UtilsController {
|
|
|
295
316
|
for (const entry of entries) {
|
|
296
317
|
if (entry.isDirectory()) {
|
|
297
318
|
const functionPath = path.join(functionsDir, entry.name);
|
|
298
|
-
//
|
|
319
|
+
// Validate it's a function directory
|
|
320
|
+
if (!validateFunctionDirectory(functionPath)) {
|
|
321
|
+
continue; // Skip invalid directories
|
|
322
|
+
}
|
|
323
|
+
// Match with config functions using normalized names
|
|
299
324
|
if (this.config?.functions) {
|
|
300
|
-
const
|
|
325
|
+
const normalizedEntryName = normalizeFunctionName(entry.name);
|
|
326
|
+
const matchingFunc = this.config.functions.find((f) => normalizeFunctionName(f.name) === normalizedEntryName);
|
|
301
327
|
if (matchingFunc) {
|
|
302
328
|
functionDirMap.set(matchingFunc.name, functionPath);
|
|
303
329
|
}
|
|
@@ -422,20 +448,22 @@ export class UtilsController {
|
|
|
422
448
|
async generateSchemas() {
|
|
423
449
|
// Schema generation doesn't need Appwrite connection, just config
|
|
424
450
|
if (!this.config) {
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
451
|
+
MessageFormatter.progress("Loading config from ConfigManager...", { prefix: "Config" });
|
|
452
|
+
try {
|
|
453
|
+
const configManager = ConfigManager.getInstance();
|
|
454
|
+
// Load config if not already loaded
|
|
455
|
+
if (!configManager.hasConfig()) {
|
|
456
|
+
await configManager.loadConfig({
|
|
457
|
+
configDir: this.currentUserDir,
|
|
458
|
+
validate: false,
|
|
459
|
+
strictMode: false,
|
|
460
|
+
});
|
|
435
461
|
}
|
|
462
|
+
this.config = configManager.getConfig();
|
|
463
|
+
MessageFormatter.info("Config loaded successfully from ConfigManager", { prefix: "Config" });
|
|
436
464
|
}
|
|
437
|
-
|
|
438
|
-
MessageFormatter.error("
|
|
465
|
+
catch (error) {
|
|
466
|
+
MessageFormatter.error("Failed to load config", error instanceof Error ? error : undefined, { prefix: "Config" });
|
|
439
467
|
return;
|
|
440
468
|
}
|
|
441
469
|
}
|
|
@@ -521,13 +549,13 @@ export class UtilsController {
|
|
|
521
549
|
logger.warn("Schema regeneration failed during sync:", error);
|
|
522
550
|
}
|
|
523
551
|
}
|
|
524
|
-
async
|
|
552
|
+
async selectivePull(databaseSelections, bucketSelections) {
|
|
525
553
|
await this.init();
|
|
526
554
|
if (!this.database) {
|
|
527
555
|
MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
|
|
528
556
|
return;
|
|
529
557
|
}
|
|
530
|
-
MessageFormatter.progress("Starting selective
|
|
558
|
+
MessageFormatter.progress("Starting selective pull (Appwrite → local config)...", { prefix: "Controller" });
|
|
531
559
|
// Convert database selections to Models.Database format
|
|
532
560
|
const selectedDatabases = [];
|
|
533
561
|
for (const dbSelection of databaseSelections) {
|
|
@@ -547,7 +575,7 @@ export class UtilsController {
|
|
|
547
575
|
}
|
|
548
576
|
}
|
|
549
577
|
if (selectedDatabases.length === 0) {
|
|
550
|
-
MessageFormatter.warning("No valid databases selected for
|
|
578
|
+
MessageFormatter.warning("No valid databases selected for pull", { prefix: "Controller" });
|
|
551
579
|
return;
|
|
552
580
|
}
|
|
553
581
|
// Log bucket selections if provided
|
|
@@ -560,7 +588,71 @@ export class UtilsController {
|
|
|
560
588
|
}
|
|
561
589
|
// Perform selective sync using the enhanced synchronizeConfigurations method
|
|
562
590
|
await this.synchronizeConfigurations(selectedDatabases, this.config, databaseSelections, bucketSelections);
|
|
563
|
-
MessageFormatter.success("Selective
|
|
591
|
+
MessageFormatter.success("Selective pull completed successfully! Remote config pulled to local.", { prefix: "Controller" });
|
|
592
|
+
}
|
|
593
|
+
async selectivePush(databaseSelections, bucketSelections) {
|
|
594
|
+
await this.init();
|
|
595
|
+
if (!this.database) {
|
|
596
|
+
MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
MessageFormatter.progress("Starting selective push (local config → Appwrite)...", { prefix: "Controller" });
|
|
600
|
+
// Convert database selections to Models.Database format
|
|
601
|
+
const selectedDatabases = [];
|
|
602
|
+
for (const dbSelection of databaseSelections) {
|
|
603
|
+
// Get the full database object from the controller
|
|
604
|
+
const databases = await fetchAllDatabases(this.database);
|
|
605
|
+
const database = databases.find(db => db.$id === dbSelection.databaseId);
|
|
606
|
+
if (database) {
|
|
607
|
+
selectedDatabases.push(database);
|
|
608
|
+
MessageFormatter.info(`Selected database: ${database.name} (${database.$id})`, { prefix: "Controller" });
|
|
609
|
+
// Log selected tables for this database
|
|
610
|
+
if (dbSelection.tableIds && dbSelection.tableIds.length > 0) {
|
|
611
|
+
MessageFormatter.info(` Tables: ${dbSelection.tableIds.join(', ')}`, { prefix: "Controller" });
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
else {
|
|
615
|
+
MessageFormatter.warning(`Database with ID ${dbSelection.databaseId} not found`, { prefix: "Controller" });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (selectedDatabases.length === 0) {
|
|
619
|
+
MessageFormatter.warning("No valid databases selected for push", { prefix: "Controller" });
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
// Log bucket selections if provided
|
|
623
|
+
if (bucketSelections && bucketSelections.length > 0) {
|
|
624
|
+
MessageFormatter.info(`Selected ${bucketSelections.length} buckets:`, { prefix: "Controller" });
|
|
625
|
+
for (const bucketSelection of bucketSelections) {
|
|
626
|
+
const dbInfo = bucketSelection.databaseId ? ` (DB: ${bucketSelection.databaseId})` : '';
|
|
627
|
+
MessageFormatter.info(` - ${bucketSelection.bucketName} (${bucketSelection.bucketId})${dbInfo}`, { prefix: "Controller" });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// PUSH OPERATION: Push local configuration to Appwrite
|
|
631
|
+
// Build selected collections/tables from databaseSelections
|
|
632
|
+
const selectedCollections = [];
|
|
633
|
+
// Get all collections/tables from config (they're at the root level, not nested in databases)
|
|
634
|
+
const allCollections = this.config?.collections || this.config?.tables || [];
|
|
635
|
+
// Collect all selected table IDs from all database selections
|
|
636
|
+
const selectedTableIds = new Set();
|
|
637
|
+
for (const dbSelection of databaseSelections) {
|
|
638
|
+
for (const tableId of dbSelection.tableIds) {
|
|
639
|
+
selectedTableIds.add(tableId);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
// Filter to only the selected table IDs
|
|
643
|
+
for (const collection of allCollections) {
|
|
644
|
+
const collectionId = collection.$id || collection.id;
|
|
645
|
+
if (selectedTableIds.has(collectionId)) {
|
|
646
|
+
selectedCollections.push(collection);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
MessageFormatter.info(`Pushing ${selectedCollections.length} selected tables/collections to Appwrite`, { prefix: "Controller" });
|
|
650
|
+
// Ensure databases exist
|
|
651
|
+
await this.ensureDatabasesExist(selectedDatabases);
|
|
652
|
+
await this.ensureDatabaseConfigBucketsExist(selectedDatabases);
|
|
653
|
+
// Create/update ONLY the selected collections/tables
|
|
654
|
+
await this.createOrUpdateCollectionsForDatabases(selectedDatabases, selectedCollections);
|
|
655
|
+
MessageFormatter.success("Selective push completed successfully! Local config pushed to Appwrite.", { prefix: "Controller" });
|
|
564
656
|
}
|
|
565
657
|
async syncDb(databases = [], collections = []) {
|
|
566
658
|
await this.init();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appwrite-utils-cli",
|
|
3
3
|
"description": "Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.",
|
|
4
|
-
"version": "1.7.
|
|
4
|
+
"version": "1.7.9",
|
|
5
5
|
"main": "src/main.ts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -24,7 +24,9 @@
|
|
|
24
24
|
"appwrite-migrate": "./dist/main.js"
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
|
-
"build": "bun run tsc",
|
|
27
|
+
"build": "bun run tsc && bun run copy-templates",
|
|
28
|
+
"prebuild": "rm -rf dist",
|
|
29
|
+
"copy-templates": "tsx scripts/copy-templates.ts",
|
|
28
30
|
"start": "tsx --no-cache src/main.ts",
|
|
29
31
|
"deploy": "bun run build && npm publish --access public",
|
|
30
32
|
"test": "jest",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { cpSync, mkdirSync, existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const src = join(process.cwd(), 'src', 'functions', 'templates');
|
|
5
|
+
const dest = join(process.cwd(), 'dist', 'functions', 'templates');
|
|
6
|
+
|
|
7
|
+
// Verify source exists
|
|
8
|
+
if (!existsSync(src)) {
|
|
9
|
+
console.error('❌ Error: Template source directory not found:', src);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Create destination directory
|
|
14
|
+
mkdirSync(dest, { recursive: true });
|
|
15
|
+
|
|
16
|
+
// Copy templates recursively
|
|
17
|
+
try {
|
|
18
|
+
cpSync(src, dest, { recursive: true });
|
|
19
|
+
console.log('✓ Templates copied to dist/functions/templates/');
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error('❌ Failed to copy templates:', error);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
@@ -26,14 +26,13 @@ export const databaseCommands = {
|
|
|
26
26
|
// Get local collections for selection
|
|
27
27
|
const localCollections = (cli as any).getLocalCollections();
|
|
28
28
|
|
|
29
|
-
//
|
|
30
|
-
const { syncExisting, modifyConfiguration } = await SelectionDialogs.promptForExistingConfig(configuredDatabases);
|
|
29
|
+
// Push operations always use local configuration as source of truth
|
|
31
30
|
|
|
32
31
|
// Select databases
|
|
33
32
|
const selectedDatabaseIds = await SelectionDialogs.selectDatabases(
|
|
34
33
|
availableDatabases,
|
|
35
34
|
configuredDatabases,
|
|
36
|
-
{ showSelectAll: true, allowNewOnly:
|
|
35
|
+
{ showSelectAll: true, allowNewOnly: false }
|
|
37
36
|
);
|
|
38
37
|
|
|
39
38
|
if (selectedDatabaseIds.length === 0) {
|
|
@@ -133,16 +132,16 @@ export const databaseCommands = {
|
|
|
133
132
|
bucketSelections
|
|
134
133
|
);
|
|
135
134
|
|
|
136
|
-
const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary);
|
|
135
|
+
const confirmed = await SelectionDialogs.confirmSyncSelection(selectionSummary, 'push');
|
|
137
136
|
|
|
138
137
|
if (!confirmed) {
|
|
139
|
-
MessageFormatter.info("
|
|
138
|
+
MessageFormatter.info("Push operation cancelled by user", { prefix: "Database" });
|
|
140
139
|
return;
|
|
141
140
|
}
|
|
142
141
|
|
|
143
|
-
// Perform selective
|
|
144
|
-
MessageFormatter.progress("Starting selective
|
|
145
|
-
await (cli as any).controller!.
|
|
142
|
+
// Perform selective push using the controller
|
|
143
|
+
MessageFormatter.progress("Starting selective push...", { prefix: "Database" });
|
|
144
|
+
await (cli as any).controller!.selectivePush(databaseSelections, bucketSelections);
|
|
146
145
|
|
|
147
146
|
MessageFormatter.success("\n✅ All database configurations pushed successfully!", { prefix: "Database" });
|
|
148
147
|
|