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
|
@@ -135,6 +135,45 @@ const YamlCollectionSchema = z.object({
|
|
|
135
135
|
})).optional().default([]),
|
|
136
136
|
importDefs: z.array(z.any()).optional().default([])
|
|
137
137
|
});
|
|
138
|
+
// YAML Table Schema - Supports table-specific terminology
|
|
139
|
+
const YamlTableSchema = z.object({
|
|
140
|
+
name: z.string(),
|
|
141
|
+
id: z.string().optional(),
|
|
142
|
+
rowSecurity: z.boolean().default(false), // Tables use rowSecurity
|
|
143
|
+
enabled: z.boolean().default(true),
|
|
144
|
+
permissions: z.array(z.object({
|
|
145
|
+
permission: z.string(),
|
|
146
|
+
target: z.string()
|
|
147
|
+
})).optional().default([]),
|
|
148
|
+
columns: z.array(// Tables use columns terminology
|
|
149
|
+
z.object({
|
|
150
|
+
key: z.string(),
|
|
151
|
+
type: z.string(),
|
|
152
|
+
size: z.number().optional(),
|
|
153
|
+
required: z.boolean().default(false),
|
|
154
|
+
array: z.boolean().optional(),
|
|
155
|
+
encrypted: z.boolean().optional(), // Tables support encrypted property
|
|
156
|
+
default: z.any().optional(),
|
|
157
|
+
min: z.number().optional(),
|
|
158
|
+
max: z.number().optional(),
|
|
159
|
+
elements: z.array(z.string()).optional(),
|
|
160
|
+
relatedTable: z.string().optional(), // Tables use relatedTable
|
|
161
|
+
relationType: z.string().optional(),
|
|
162
|
+
twoWay: z.boolean().optional(),
|
|
163
|
+
twoWayKey: z.string().optional(),
|
|
164
|
+
onDelete: z.string().optional(),
|
|
165
|
+
side: z.string().optional(),
|
|
166
|
+
encrypt: z.boolean().optional(),
|
|
167
|
+
format: z.string().optional()
|
|
168
|
+
})).optional().default([]),
|
|
169
|
+
indexes: z.array(z.object({
|
|
170
|
+
key: z.string(),
|
|
171
|
+
type: z.string(),
|
|
172
|
+
columns: z.array(z.string()), // Tables use columns in indexes
|
|
173
|
+
orders: z.array(z.string()).optional()
|
|
174
|
+
})).optional().default([]),
|
|
175
|
+
importDefs: z.array(z.any()).optional().default([])
|
|
176
|
+
});
|
|
138
177
|
/**
|
|
139
178
|
* Loads a YAML collection file and converts it to CollectionCreate format
|
|
140
179
|
* @param filePath Path to the YAML collection file
|
|
@@ -190,55 +229,52 @@ export const loadYamlCollection = (filePath) => {
|
|
|
190
229
|
}
|
|
191
230
|
};
|
|
192
231
|
/**
|
|
193
|
-
* Loads a YAML table file and converts it to
|
|
232
|
+
* Loads a YAML table file and converts it to CollectionCreate format
|
|
194
233
|
* @param filePath Path to the YAML table file
|
|
195
|
-
* @returns
|
|
234
|
+
* @returns CollectionCreate object or null if loading fails
|
|
196
235
|
*/
|
|
197
236
|
export const loadYamlTable = (filePath) => {
|
|
198
237
|
try {
|
|
199
238
|
const fileContent = fs.readFileSync(filePath, "utf8");
|
|
200
239
|
const yamlData = yaml.load(fileContent);
|
|
201
|
-
//
|
|
202
|
-
const parsedTable =
|
|
203
|
-
// Convert YAML table to
|
|
240
|
+
// Use the new table-specific schema
|
|
241
|
+
const parsedTable = YamlTableSchema.parse(yamlData);
|
|
242
|
+
// Convert YAML table to CollectionCreate format (internal representation)
|
|
204
243
|
const table = {
|
|
205
244
|
name: parsedTable.name,
|
|
206
|
-
|
|
207
|
-
documentSecurity: parsedTable.documentSecurity
|
|
245
|
+
$id: yamlData.tableId || parsedTable.id || parsedTable.name.toLowerCase().replace(/\s+/g, '_'),
|
|
246
|
+
documentSecurity: parsedTable.rowSecurity, // Convert rowSecurity to documentSecurity
|
|
208
247
|
enabled: parsedTable.enabled,
|
|
209
248
|
$permissions: parsedTable.permissions.map(p => ({
|
|
210
249
|
permission: p.permission,
|
|
211
250
|
target: p.target
|
|
212
251
|
})),
|
|
213
|
-
attributes: parsedTable.
|
|
214
|
-
key:
|
|
215
|
-
type:
|
|
216
|
-
size:
|
|
217
|
-
required:
|
|
218
|
-
array:
|
|
219
|
-
xdefault:
|
|
220
|
-
min:
|
|
221
|
-
max:
|
|
222
|
-
elements:
|
|
223
|
-
relatedCollection:
|
|
224
|
-
relationType:
|
|
225
|
-
twoWay:
|
|
226
|
-
twoWayKey:
|
|
227
|
-
onDelete:
|
|
228
|
-
side:
|
|
229
|
-
encrypted:
|
|
230
|
-
format:
|
|
252
|
+
attributes: parsedTable.columns.map(col => ({
|
|
253
|
+
key: col.key,
|
|
254
|
+
type: col.type,
|
|
255
|
+
size: col.size,
|
|
256
|
+
required: col.required,
|
|
257
|
+
array: col.array,
|
|
258
|
+
xdefault: col.default,
|
|
259
|
+
min: col.min,
|
|
260
|
+
max: col.max,
|
|
261
|
+
elements: col.elements,
|
|
262
|
+
relatedCollection: col.relatedTable, // Convert relatedTable to relatedCollection
|
|
263
|
+
relationType: col.relationType,
|
|
264
|
+
twoWay: col.twoWay,
|
|
265
|
+
twoWayKey: col.twoWayKey,
|
|
266
|
+
onDelete: col.onDelete,
|
|
267
|
+
side: col.side,
|
|
268
|
+
encrypted: col.encrypted || col.encrypt, // Support both encrypted and encrypt
|
|
269
|
+
format: col.format
|
|
231
270
|
})),
|
|
232
271
|
indexes: parsedTable.indexes.map(idx => ({
|
|
233
272
|
key: idx.key,
|
|
234
273
|
type: idx.type,
|
|
235
|
-
attributes: idx.attributes
|
|
274
|
+
attributes: idx.columns, // Convert columns to attributes
|
|
236
275
|
orders: idx.orders
|
|
237
276
|
})),
|
|
238
|
-
importDefs: parsedTable.importDefs
|
|
239
|
-
databaseId: yamlData.databaseId,
|
|
240
|
-
// Add backward compatibility field
|
|
241
|
-
$id: yamlData.$id || parsedTable.id
|
|
277
|
+
importDefs: parsedTable.importDefs || []
|
|
242
278
|
};
|
|
243
279
|
return table;
|
|
244
280
|
}
|
|
@@ -15,9 +15,15 @@ export function collectionToYaml(collection, config = {
|
|
|
15
15
|
const yamlData = {
|
|
16
16
|
name: collection.name,
|
|
17
17
|
id: collection.$id,
|
|
18
|
-
documentSecurity: collection.documentSecurity,
|
|
19
18
|
enabled: collection.enabled,
|
|
20
19
|
};
|
|
20
|
+
// Use appropriate security field based on terminology
|
|
21
|
+
if (config.useTableTerminology) {
|
|
22
|
+
yamlData.rowSecurity = collection.documentSecurity;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
yamlData.documentSecurity = collection.documentSecurity;
|
|
26
|
+
}
|
|
21
27
|
// Convert permissions
|
|
22
28
|
if (collection.$permissions && collection.$permissions.length > 0) {
|
|
23
29
|
yamlData.permissions = collection.$permissions.map(p => ({
|
|
@@ -158,6 +164,11 @@ export function normalizeYamlData(yamlData) {
|
|
|
158
164
|
columns: undefined
|
|
159
165
|
}));
|
|
160
166
|
}
|
|
167
|
+
// Normalize security fields - prefer documentSecurity for consistency
|
|
168
|
+
if (yamlData.rowSecurity !== undefined && yamlData.documentSecurity === undefined) {
|
|
169
|
+
normalized.documentSecurity = yamlData.rowSecurity;
|
|
170
|
+
delete normalized.rowSecurity;
|
|
171
|
+
}
|
|
161
172
|
return normalized;
|
|
162
173
|
}
|
|
163
174
|
/**
|
|
@@ -165,7 +176,8 @@ export function normalizeYamlData(yamlData) {
|
|
|
165
176
|
*/
|
|
166
177
|
export function usesTableTerminology(yamlData) {
|
|
167
178
|
return !!(yamlData.columns && yamlData.columns.length > 0) ||
|
|
168
|
-
!!(yamlData.indexes?.some(idx => !!idx.columns))
|
|
179
|
+
!!(yamlData.indexes?.some(idx => !!idx.columns)) ||
|
|
180
|
+
yamlData.rowSecurity !== undefined;
|
|
169
181
|
}
|
|
170
182
|
/**
|
|
171
183
|
* Converts between attribute and column terminology
|
|
@@ -190,6 +202,11 @@ export function convertTerminology(yamlData, toTableTerminology) {
|
|
|
190
202
|
attributes: idx.attributes // Keep both for compatibility
|
|
191
203
|
}));
|
|
192
204
|
}
|
|
205
|
+
// Convert security field
|
|
206
|
+
if (yamlData.documentSecurity !== undefined && yamlData.rowSecurity === undefined) {
|
|
207
|
+
converted.rowSecurity = yamlData.documentSecurity;
|
|
208
|
+
delete converted.documentSecurity;
|
|
209
|
+
}
|
|
193
210
|
return converted;
|
|
194
211
|
}
|
|
195
212
|
else {
|
|
@@ -272,7 +289,6 @@ export function generateYamlTemplate(entityName, config) {
|
|
|
272
289
|
const template = {
|
|
273
290
|
name: entityName,
|
|
274
291
|
id: entityName.toLowerCase().replace(/\s+/g, '_'),
|
|
275
|
-
documentSecurity: false,
|
|
276
292
|
enabled: true,
|
|
277
293
|
permissions: [
|
|
278
294
|
{
|
|
@@ -294,6 +310,13 @@ export function generateYamlTemplate(entityName, config) {
|
|
|
294
310
|
],
|
|
295
311
|
importDefs: []
|
|
296
312
|
};
|
|
313
|
+
// Use appropriate security field based on terminology
|
|
314
|
+
if (config.useTableTerminology) {
|
|
315
|
+
template.rowSecurity = false;
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
template.documentSecurity = false;
|
|
319
|
+
}
|
|
297
320
|
// Assign fields with correct property name
|
|
298
321
|
template[fieldsKey] = fieldsArray;
|
|
299
322
|
template.indexes = indexesArray;
|
|
@@ -4,6 +4,7 @@ import { type AfterImportActions, type ConverterFunctions, type ValidationRules
|
|
|
4
4
|
import { type TransferOptions } from "./migrations/transfer.js";
|
|
5
5
|
import type { DatabaseAdapter } from './adapters/DatabaseAdapter.js';
|
|
6
6
|
import { type ValidationResult } from "./config/configValidation.js";
|
|
7
|
+
import type { DatabaseSelection, BucketSelection } from "./shared/selectionDialogs.js";
|
|
7
8
|
export interface SetupOptions {
|
|
8
9
|
databases?: Models.Database[];
|
|
9
10
|
collections?: string[];
|
|
@@ -60,6 +61,9 @@ export declare class UtilsController {
|
|
|
60
61
|
ensureDatabasesExist(databases?: Models.Database[]): Promise<void>;
|
|
61
62
|
ensureCollectionsExist(database: Models.Database, collections?: Models.Collection[]): Promise<void>;
|
|
62
63
|
getDatabasesByIds(ids: string[]): Promise<Models.Database[] | undefined>;
|
|
64
|
+
fetchAllBuckets(): Promise<{
|
|
65
|
+
buckets: Models.Bucket[];
|
|
66
|
+
}>;
|
|
63
67
|
wipeOtherDatabases(databasesToKeep: Models.Database[]): Promise<void>;
|
|
64
68
|
wipeUsers(): Promise<void>;
|
|
65
69
|
backupDatabase(database: Models.Database, format?: 'json' | 'zip'): Promise<void>;
|
|
@@ -78,7 +82,9 @@ export declare class UtilsController {
|
|
|
78
82
|
}[], collections?: Models.Collection[]): Promise<void>;
|
|
79
83
|
generateSchemas(): Promise<void>;
|
|
80
84
|
importData(options?: SetupOptions): Promise<void>;
|
|
81
|
-
synchronizeConfigurations(databases?: Models.Database[], config?: AppwriteConfig): Promise<void>;
|
|
85
|
+
synchronizeConfigurations(databases?: Models.Database[], config?: AppwriteConfig, databaseSelections?: DatabaseSelection[], bucketSelections?: BucketSelection[]): Promise<void>;
|
|
86
|
+
selectivePull(databaseSelections: DatabaseSelection[], bucketSelections: BucketSelection[]): Promise<void>;
|
|
87
|
+
selectivePush(databaseSelections: DatabaseSelection[], bucketSelections: BucketSelection[]): Promise<void>;
|
|
82
88
|
syncDb(databases?: Models.Database[], collections?: Models.Collection[]): Promise<void>;
|
|
83
89
|
getAppwriteFolderPath(): string | undefined;
|
|
84
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";
|
|
@@ -25,6 +26,7 @@ import { configureLogging, updateLogger, logger } from "./shared/logging.js";
|
|
|
25
26
|
import { MessageFormatter, Messages } from "./shared/messageFormatter.js";
|
|
26
27
|
import { SchemaGenerator } from "./shared/schemaGenerator.js";
|
|
27
28
|
import { findYamlConfig } from "./config/yamlConfig.js";
|
|
29
|
+
import { createImportSchemas } from "./migrations/yaml/generateImportSchemas.js";
|
|
28
30
|
import { validateCollectionsTablesConfig, reportValidationResults, validateWithStrictMode } from "./config/configValidation.js";
|
|
29
31
|
import { ConfigManager } from "./config/ConfigManager.js";
|
|
30
32
|
import { ClientFactory } from "./utils/ClientFactory.js";
|
|
@@ -38,6 +40,26 @@ export class UtilsController {
|
|
|
38
40
|
* Get the UtilsController singleton instance
|
|
39
41
|
*/
|
|
40
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
|
+
}
|
|
41
63
|
if (!UtilsController.instance) {
|
|
42
64
|
UtilsController.instance = new UtilsController(currentUserDir, directConfig);
|
|
43
65
|
}
|
|
@@ -225,6 +247,24 @@ export class UtilsController {
|
|
|
225
247
|
]);
|
|
226
248
|
return dbs.databases;
|
|
227
249
|
}
|
|
250
|
+
async fetchAllBuckets() {
|
|
251
|
+
await this.init();
|
|
252
|
+
if (!this.storage) {
|
|
253
|
+
MessageFormatter.warning("Storage not initialized - buckets will be empty", { prefix: "Controller" });
|
|
254
|
+
return { buckets: [] };
|
|
255
|
+
}
|
|
256
|
+
try {
|
|
257
|
+
const result = await this.storage.listBuckets([
|
|
258
|
+
Query.limit(1000) // Increase limit to get all buckets
|
|
259
|
+
]);
|
|
260
|
+
MessageFormatter.success(`Found ${result.buckets.length} buckets`, { prefix: "Controller" });
|
|
261
|
+
return result;
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
MessageFormatter.error(`Failed to fetch buckets: ${error.message || error}`, error instanceof Error ? error : undefined, { prefix: "Controller" });
|
|
265
|
+
return { buckets: [] };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
228
268
|
async wipeOtherDatabases(databasesToKeep) {
|
|
229
269
|
await this.init();
|
|
230
270
|
if (!this.database) {
|
|
@@ -276,9 +316,14 @@ export class UtilsController {
|
|
|
276
316
|
for (const entry of entries) {
|
|
277
317
|
if (entry.isDirectory()) {
|
|
278
318
|
const functionPath = path.join(functionsDir, entry.name);
|
|
279
|
-
//
|
|
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
|
|
280
324
|
if (this.config?.functions) {
|
|
281
|
-
const
|
|
325
|
+
const normalizedEntryName = normalizeFunctionName(entry.name);
|
|
326
|
+
const matchingFunc = this.config.functions.find((f) => normalizeFunctionName(f.name) === normalizedEntryName);
|
|
282
327
|
if (matchingFunc) {
|
|
283
328
|
functionDirMap.set(matchingFunc.name, functionPath);
|
|
284
329
|
}
|
|
@@ -403,20 +448,22 @@ export class UtilsController {
|
|
|
403
448
|
async generateSchemas() {
|
|
404
449
|
// Schema generation doesn't need Appwrite connection, just config
|
|
405
450
|
if (!this.config) {
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
|
+
});
|
|
416
461
|
}
|
|
462
|
+
this.config = configManager.getConfig();
|
|
463
|
+
MessageFormatter.info("Config loaded successfully from ConfigManager", { prefix: "Config" });
|
|
417
464
|
}
|
|
418
|
-
|
|
419
|
-
MessageFormatter.error("
|
|
465
|
+
catch (error) {
|
|
466
|
+
MessageFormatter.error("Failed to load config", error instanceof Error ? error : undefined, { prefix: "Config" });
|
|
420
467
|
return;
|
|
421
468
|
}
|
|
422
469
|
}
|
|
@@ -448,7 +495,7 @@ export class UtilsController {
|
|
|
448
495
|
const importController = new ImportController(this.config, this.database, this.storage, this.appwriteFolderPath, importDataActions, options, options.databases);
|
|
449
496
|
await importController.run(options.collections);
|
|
450
497
|
}
|
|
451
|
-
async synchronizeConfigurations(databases, config) {
|
|
498
|
+
async synchronizeConfigurations(databases, config, databaseSelections, bucketSelections) {
|
|
452
499
|
await this.init();
|
|
453
500
|
if (!this.storage) {
|
|
454
501
|
MessageFormatter.error("Storage not initialized", undefined, { prefix: "Controller" });
|
|
@@ -463,8 +510,25 @@ export class UtilsController {
|
|
|
463
510
|
MessageFormatter.error("Failed to get appwriteFolderPath", undefined, { prefix: "Controller" });
|
|
464
511
|
return;
|
|
465
512
|
}
|
|
513
|
+
// If selections are provided, filter the databases accordingly
|
|
514
|
+
let filteredDatabases = databases;
|
|
515
|
+
if (databaseSelections && databaseSelections.length > 0) {
|
|
516
|
+
// Convert selections to Models.Database format
|
|
517
|
+
filteredDatabases = [];
|
|
518
|
+
const allDatabases = databases ? databases : await fetchAllDatabases(this.database);
|
|
519
|
+
for (const selection of databaseSelections) {
|
|
520
|
+
const database = allDatabases.find(db => db.$id === selection.databaseId);
|
|
521
|
+
if (database) {
|
|
522
|
+
filteredDatabases.push(database);
|
|
523
|
+
}
|
|
524
|
+
else {
|
|
525
|
+
MessageFormatter.warning(`Database with ID ${selection.databaseId} not found`, { prefix: "Controller" });
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
MessageFormatter.info(`Syncing ${filteredDatabases.length} selected databases out of ${allDatabases.length} available`, { prefix: "Controller" });
|
|
529
|
+
}
|
|
466
530
|
const appwriteToX = new AppwriteToX(configToUse, this.appwriteFolderPath, this.storage);
|
|
467
|
-
await appwriteToX.toSchemas(
|
|
531
|
+
await appwriteToX.toSchemas(filteredDatabases);
|
|
468
532
|
// Update the controller's config with the synchronized collections
|
|
469
533
|
this.config = appwriteToX.updatedConfig;
|
|
470
534
|
// Write the updated config back to disk
|
|
@@ -472,6 +536,123 @@ export class UtilsController {
|
|
|
472
536
|
const yamlConfigPath = findYamlConfig(this.appwriteFolderPath);
|
|
473
537
|
const isYamlProject = !!yamlConfigPath;
|
|
474
538
|
await generator.updateConfig(this.config, isYamlProject);
|
|
539
|
+
// Regenerate JSON schemas to reflect any table terminology fixes
|
|
540
|
+
try {
|
|
541
|
+
MessageFormatter.progress("Regenerating JSON schemas...", { prefix: "Sync" });
|
|
542
|
+
await createImportSchemas(this.appwriteFolderPath);
|
|
543
|
+
MessageFormatter.success("JSON schemas regenerated successfully", { prefix: "Sync" });
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
// Log error but don't fail the sync process
|
|
547
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
548
|
+
MessageFormatter.warning(`Failed to regenerate JSON schemas, but sync completed: ${errorMessage}`, { prefix: "Sync" });
|
|
549
|
+
logger.warn("Schema regeneration failed during sync:", error);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
async selectivePull(databaseSelections, bucketSelections) {
|
|
553
|
+
await this.init();
|
|
554
|
+
if (!this.database) {
|
|
555
|
+
MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
MessageFormatter.progress("Starting selective pull (Appwrite → local config)...", { prefix: "Controller" });
|
|
559
|
+
// Convert database selections to Models.Database format
|
|
560
|
+
const selectedDatabases = [];
|
|
561
|
+
for (const dbSelection of databaseSelections) {
|
|
562
|
+
// Get the full database object from the controller
|
|
563
|
+
const databases = await fetchAllDatabases(this.database);
|
|
564
|
+
const database = databases.find(db => db.$id === dbSelection.databaseId);
|
|
565
|
+
if (database) {
|
|
566
|
+
selectedDatabases.push(database);
|
|
567
|
+
MessageFormatter.info(`Selected database: ${database.name} (${database.$id})`, { prefix: "Controller" });
|
|
568
|
+
// Log selected tables for this database
|
|
569
|
+
if (dbSelection.tableIds && dbSelection.tableIds.length > 0) {
|
|
570
|
+
MessageFormatter.info(` Tables: ${dbSelection.tableIds.join(', ')}`, { prefix: "Controller" });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
MessageFormatter.warning(`Database with ID ${dbSelection.databaseId} not found`, { prefix: "Controller" });
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (selectedDatabases.length === 0) {
|
|
578
|
+
MessageFormatter.warning("No valid databases selected for pull", { prefix: "Controller" });
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
// Log bucket selections if provided
|
|
582
|
+
if (bucketSelections && bucketSelections.length > 0) {
|
|
583
|
+
MessageFormatter.info(`Selected ${bucketSelections.length} buckets:`, { prefix: "Controller" });
|
|
584
|
+
for (const bucketSelection of bucketSelections) {
|
|
585
|
+
const dbInfo = bucketSelection.databaseId ? ` (DB: ${bucketSelection.databaseId})` : '';
|
|
586
|
+
MessageFormatter.info(` - ${bucketSelection.bucketName} (${bucketSelection.bucketId})${dbInfo}`, { prefix: "Controller" });
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
// Perform selective sync using the enhanced synchronizeConfigurations method
|
|
590
|
+
await this.synchronizeConfigurations(selectedDatabases, this.config, databaseSelections, bucketSelections);
|
|
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" });
|
|
475
656
|
}
|
|
476
657
|
async syncDb(databases = [], collections = []) {
|
|
477
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
|
+
}
|