appwrite-utils-cli 1.8.7 → 1.8.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/CHANGELOG.md +16 -0
- package/dist/cli/commands/functionCommands.js +53 -48
- package/dist/collections/attributes.js +2 -2
- package/dist/collections/methods.js +4 -4
- package/dist/interactiveCLI.d.ts +1 -0
- package/dist/interactiveCLI.js +33 -13
- package/dist/main.js +42 -9
- package/dist/migrations/appwriteToX.js +14 -2
- package/dist/shared/operationQueue.d.ts +4 -4
- package/dist/shared/operationQueue.js +21 -13
- package/dist/utilsController.js +7 -0
- package/package.json +1 -1
- package/src/cli/commands/functionCommands.ts +72 -67
- package/src/collections/attributes.ts +2 -2
- package/src/collections/methods.ts +8 -8
- package/src/interactiveCLI.ts +60 -29
- package/src/main.ts +58 -15
- package/src/migrations/appwriteToX.ts +34 -22
- package/src/shared/operationQueue.ts +50 -41
- package/src/utilsController.ts +10 -3
package/CHANGELOG.md
CHANGED
|
@@ -17,3 +17,19 @@ All notable changes to this project will be documented in this file.
|
|
|
17
17
|
- Output directory selectable during interactive flow (absolute paths respected)
|
|
18
18
|
- Constants generator now supports selecting which categories to include (databases, collections/tables, buckets, functions).
|
|
19
19
|
- Bucket management: interactive create/delete commands with YAML persistence when active; selective push now diffs and updates bucket settings (name, permissions, security, limits).
|
|
20
|
+
|
|
21
|
+
## 1.8.8 - 2025-10-29
|
|
22
|
+
|
|
23
|
+
- Fix: Selective push processed state is tracked per database. Previously, pushing the same table to multiple databases could skip the second database with “already processed”; now each DB is handled independently.
|
|
24
|
+
- Fix: Clear in-memory processing state between databases during multi-DB pushes to prevent cross-database leakage of IDs and processed flags.
|
|
25
|
+
- Enhancement: Interactive selection adds “Use same selection as before” when choosing tables per database, speeding up multi-DB workflows.
|
|
26
|
+
- Behavior: Non-interactive `--push` with both `--dbIds` and `--collectionIds` is fully non-interactive. The provided IDs are applied as-is to every selected database and the confirmation summary is skipped.
|
|
27
|
+
- Improvement: Selective push reloads local config from disk before pushing to ensure the latest YAML/TS changes are used.
|
|
28
|
+
- Fix: Sync-from-Appwrite now captures function `scopes` and writes them to config; deployments use `scopes` from either central config or `.fnconfig.yaml`.
|
|
29
|
+
- Change: Removed global prompt to choose between central `config.yaml` and per-function `.fnconfig.yaml` during function deployments. The tool now prompts per function only when both sources exist for that function (with an option to merge where `.fnconfig` overrides).
|
|
30
|
+
|
|
31
|
+
## 1.8.9 - 2025-10-29
|
|
32
|
+
|
|
33
|
+
- Fix: Functions sync now fetches full details via `getFunction` per function to reliably capture `scopes` (prevents empty scopes in some environments).
|
|
34
|
+
- Confirmed schema: YAML `appwrite-config.schema.json` supports `functions[].dirPath`; writers preserve `dirPath` when present.
|
|
35
|
+
- Polish: Per-function config source selection is now applied at deploy time when both central and `.fnconfig.yaml` are present.
|
|
@@ -111,38 +111,14 @@ export const functionCommands = {
|
|
|
111
111
|
MessageFormatter.error("Failed to initialize controller or load config", undefined, { prefix: "Functions" });
|
|
112
112
|
return;
|
|
113
113
|
}
|
|
114
|
-
//
|
|
115
|
-
|
|
114
|
+
// Discover per-function .fnconfig.yaml definitions and merge with central list for selection
|
|
115
|
+
// No global prompt; we'll handle conflicts per-function if both exist.
|
|
116
|
+
let discovered = [];
|
|
117
|
+
let central = cli.controller.config.functions || [];
|
|
116
118
|
try {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
name: 'source',
|
|
121
|
-
message: 'Select function config source:',
|
|
122
|
-
choices: [
|
|
123
|
-
{ name: 'config.yaml functions (central)', value: 'central' },
|
|
124
|
-
{ name: '.fnconfig.yaml (discovered per-function)', value: 'fnconfig' },
|
|
125
|
-
{ name: 'Both (merge; .fnconfig overrides)', value: 'both' },
|
|
126
|
-
],
|
|
127
|
-
default: 'both'
|
|
128
|
-
}
|
|
129
|
-
]);
|
|
130
|
-
sourceChoice = answer.source;
|
|
131
|
-
}
|
|
132
|
-
catch { }
|
|
133
|
-
try {
|
|
134
|
-
const discovered = discoverFnConfigs(cli.currentDir);
|
|
135
|
-
const central = cli.controller.config.functions || [];
|
|
136
|
-
if (sourceChoice === 'central') {
|
|
137
|
-
cli.controller.config.functions = central;
|
|
138
|
-
}
|
|
139
|
-
else if (sourceChoice === 'fnconfig') {
|
|
140
|
-
cli.controller.config.functions = discovered;
|
|
141
|
-
}
|
|
142
|
-
else {
|
|
143
|
-
const merged = mergeDiscoveredFunctions(central, discovered);
|
|
144
|
-
cli.controller.config.functions = merged;
|
|
145
|
-
}
|
|
119
|
+
discovered = discoverFnConfigs(cli.currentDir);
|
|
120
|
+
const merged = mergeDiscoveredFunctions(central, discovered);
|
|
121
|
+
cli.controller.config.functions = merged;
|
|
146
122
|
}
|
|
147
123
|
catch { }
|
|
148
124
|
const functions = await cli.selectFunctions("Select function(s) to deploy:", true, true);
|
|
@@ -155,22 +131,51 @@ export const functionCommands = {
|
|
|
155
131
|
MessageFormatter.error("Invalid function configuration", undefined, { prefix: "Functions" });
|
|
156
132
|
return;
|
|
157
133
|
}
|
|
134
|
+
// Resolve effective config for this function (prefer per-function choice if both sources exist)
|
|
135
|
+
const byIdOrName = (arr) => arr.find((f) => f?.$id === functionConfig.$id || f?.name === functionConfig.name);
|
|
136
|
+
const centralDef = byIdOrName(central);
|
|
137
|
+
const discoveredDef = byIdOrName(discovered);
|
|
138
|
+
let effectiveConfig = functionConfig;
|
|
139
|
+
if (centralDef && discoveredDef) {
|
|
140
|
+
try {
|
|
141
|
+
const answer = await inquirer.prompt([
|
|
142
|
+
{
|
|
143
|
+
type: 'list',
|
|
144
|
+
name: 'cfgChoice',
|
|
145
|
+
message: `Multiple configs found for '${functionConfig.name}'. Which to use?`,
|
|
146
|
+
choices: [
|
|
147
|
+
{ name: 'config.yaml (central)', value: 'central' },
|
|
148
|
+
{ name: '.fnconfig.yaml (local file)', value: 'fnconfig' },
|
|
149
|
+
{ name: 'Merge (.fnconfig overrides central)', value: 'merge' },
|
|
150
|
+
],
|
|
151
|
+
default: 'fnconfig'
|
|
152
|
+
}
|
|
153
|
+
]);
|
|
154
|
+
if (answer.cfgChoice === 'central')
|
|
155
|
+
effectiveConfig = centralDef;
|
|
156
|
+
else if (answer.cfgChoice === 'fnconfig')
|
|
157
|
+
effectiveConfig = discoveredDef;
|
|
158
|
+
else
|
|
159
|
+
effectiveConfig = { ...centralDef, ...discoveredDef };
|
|
160
|
+
}
|
|
161
|
+
catch { }
|
|
162
|
+
}
|
|
158
163
|
// Ensure functions array exists
|
|
159
164
|
if (!cli.controller.config.functions) {
|
|
160
165
|
cli.controller.config.functions = [];
|
|
161
166
|
}
|
|
162
|
-
const functionNameLower =
|
|
167
|
+
const functionNameLower = effectiveConfig.name
|
|
163
168
|
.toLowerCase()
|
|
164
169
|
.replace(/\s+/g, "-");
|
|
165
170
|
// Debug logging
|
|
166
171
|
MessageFormatter.info(`🔍 Function deployment debug:`, { prefix: "Functions" });
|
|
167
|
-
MessageFormatter.info(` Function name: ${
|
|
168
|
-
MessageFormatter.info(` Function ID: ${
|
|
169
|
-
MessageFormatter.info(` Config dirPath: ${
|
|
170
|
-
if (
|
|
171
|
-
const expandedPath =
|
|
172
|
-
?
|
|
173
|
-
:
|
|
172
|
+
MessageFormatter.info(` Function name: ${effectiveConfig.name}`, { prefix: "Functions" });
|
|
173
|
+
MessageFormatter.info(` Function ID: ${effectiveConfig.$id}`, { prefix: "Functions" });
|
|
174
|
+
MessageFormatter.info(` Config dirPath: ${effectiveConfig.dirPath || 'undefined'}`, { prefix: "Functions" });
|
|
175
|
+
if (effectiveConfig.dirPath) {
|
|
176
|
+
const expandedPath = effectiveConfig.dirPath.startsWith('~/')
|
|
177
|
+
? effectiveConfig.dirPath.replace('~', os.homedir())
|
|
178
|
+
: effectiveConfig.dirPath;
|
|
174
179
|
MessageFormatter.info(` Expanded dirPath: ${expandedPath}`, { prefix: "Functions" });
|
|
175
180
|
}
|
|
176
181
|
MessageFormatter.info(` Appwrite folder: ${cli.controller.getAppwriteFolderPath()}`, { prefix: "Functions" });
|
|
@@ -182,10 +187,10 @@ export const functionCommands = {
|
|
|
182
187
|
// Check locations in priority order:
|
|
183
188
|
const priorityLocations = [
|
|
184
189
|
// 1. Config dirPath if specified (with tilde expansion)
|
|
185
|
-
|
|
186
|
-
? (require('node:path').isAbsolute(expandTildePath(
|
|
187
|
-
? expandTildePath(
|
|
188
|
-
: require('node:path').resolve(yamlBaseDir, expandTildePath(
|
|
190
|
+
effectiveConfig.dirPath
|
|
191
|
+
? (require('node:path').isAbsolute(expandTildePath(effectiveConfig.dirPath))
|
|
192
|
+
? expandTildePath(effectiveConfig.dirPath)
|
|
193
|
+
: require('node:path').resolve(yamlBaseDir, expandTildePath(effectiveConfig.dirPath)))
|
|
189
194
|
: undefined,
|
|
190
195
|
// 2. Appwrite config folder/functions/name
|
|
191
196
|
join(cli.controller.getAppwriteFolderPath(), "functions", functionNameLower),
|
|
@@ -226,10 +231,10 @@ export const functionCommands = {
|
|
|
226
231
|
if (shouldDownload) {
|
|
227
232
|
try {
|
|
228
233
|
MessageFormatter.progress("Downloading latest deployment...", { prefix: "Functions" });
|
|
229
|
-
const { path: downloadedPath, function: remoteFunction } = await downloadLatestFunctionDeployment(cli.controller.appwriteServer,
|
|
234
|
+
const { path: downloadedPath, function: remoteFunction } = await downloadLatestFunctionDeployment(cli.controller.appwriteServer, effectiveConfig.$id, join(cli.controller.getAppwriteFolderPath(), "functions"));
|
|
230
235
|
MessageFormatter.success(`✨ Function downloaded to ${downloadedPath}`, { prefix: "Functions" });
|
|
231
236
|
functionPath = downloadedPath;
|
|
232
|
-
|
|
237
|
+
effectiveConfig.dirPath = downloadedPath;
|
|
233
238
|
const existingIndex = cli.controller.config.functions.findIndex((f) => f?.$id === remoteFunction.$id);
|
|
234
239
|
if (existingIndex >= 0) {
|
|
235
240
|
cli.controller.config.functions[existingIndex].dirPath =
|
|
@@ -243,7 +248,7 @@ export const functionCommands = {
|
|
|
243
248
|
}
|
|
244
249
|
}
|
|
245
250
|
else {
|
|
246
|
-
MessageFormatter.error(`Function ${
|
|
251
|
+
MessageFormatter.error(`Function ${effectiveConfig.name} not found locally. Cannot deploy.`, undefined, { prefix: "Functions" });
|
|
247
252
|
return;
|
|
248
253
|
}
|
|
249
254
|
}
|
|
@@ -252,8 +257,8 @@ export const functionCommands = {
|
|
|
252
257
|
return;
|
|
253
258
|
}
|
|
254
259
|
try {
|
|
255
|
-
await deployLocalFunction(cli.controller.appwriteServer,
|
|
256
|
-
...
|
|
260
|
+
await deployLocalFunction(cli.controller.appwriteServer, effectiveConfig.name, {
|
|
261
|
+
...effectiveConfig,
|
|
257
262
|
dirPath: functionPath,
|
|
258
263
|
}, functionPath);
|
|
259
264
|
MessageFormatter.success("Function deployed successfully!", { prefix: "Functions" });
|
|
@@ -1222,7 +1222,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
|
|
|
1222
1222
|
// Filter to only attributes that need processing (new, changed, or not yet processed)
|
|
1223
1223
|
const attributesToProcess = attributes.filter((attribute) => {
|
|
1224
1224
|
// Skip if already processed in this session
|
|
1225
|
-
if (isAttributeProcessed(currentCollection.$id, attribute.key)) {
|
|
1225
|
+
if (isAttributeProcessed(dbId, currentCollection.$id, attribute.key)) {
|
|
1226
1226
|
return false;
|
|
1227
1227
|
}
|
|
1228
1228
|
const existing = existingAttributesMap.get(attribute.key);
|
|
@@ -1253,7 +1253,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
|
|
|
1253
1253
|
const success = await createOrUpdateAttributeWithStatusCheck(db, dbId, currentCollection, attribute);
|
|
1254
1254
|
if (success) {
|
|
1255
1255
|
// Mark this specific attribute as processed
|
|
1256
|
-
markAttributeProcessed(currentCollection.$id, attribute.key);
|
|
1256
|
+
markAttributeProcessed(dbId, currentCollection.$id, attribute.key);
|
|
1257
1257
|
// Get updated collection data for next iteration
|
|
1258
1258
|
try {
|
|
1259
1259
|
currentCollection = isDatabaseAdapter(db)
|
|
@@ -149,8 +149,8 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
|
|
|
149
149
|
const relQueue = [];
|
|
150
150
|
for (const collection of collectionsToProcess) {
|
|
151
151
|
const { attributes, indexes, ...collectionData } = collection;
|
|
152
|
-
// Check if this table has already been processed in this session
|
|
153
|
-
if (collectionData.$id && isCollectionProcessed(collectionData.$id)) {
|
|
152
|
+
// Check if this table has already been processed in this session (per database)
|
|
153
|
+
if (collectionData.$id && isCollectionProcessed(collectionData.$id, databaseId)) {
|
|
154
154
|
MessageFormatter.info(`Table '${collectionData.name}' already processed, skipping`, { prefix: "Tables" });
|
|
155
155
|
continue;
|
|
156
156
|
}
|
|
@@ -602,8 +602,8 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
|
|
|
602
602
|
catch (e) {
|
|
603
603
|
MessageFormatter.warning(`Could not evaluate deletions: ${e?.message || e}`, { prefix: 'Attributes' });
|
|
604
604
|
}
|
|
605
|
-
// Mark this table as fully processed to prevent re-processing
|
|
606
|
-
markCollectionProcessed(tableId, collectionData.name);
|
|
605
|
+
// Mark this table as fully processed for this database to prevent re-processing in the same DB only
|
|
606
|
+
markCollectionProcessed(tableId, collectionData.name, databaseId);
|
|
607
607
|
}
|
|
608
608
|
// Process queued relationships once mapping likely populated
|
|
609
609
|
if (relQueue.length > 0) {
|
package/dist/interactiveCLI.d.ts
CHANGED
package/dist/interactiveCLI.js
CHANGED
|
@@ -52,6 +52,7 @@ export class InteractiveCLI {
|
|
|
52
52
|
currentDir;
|
|
53
53
|
controller;
|
|
54
54
|
isUsingTypeScriptConfig = false;
|
|
55
|
+
lastSelectedCollectionIds = [];
|
|
55
56
|
constructor(currentDir) {
|
|
56
57
|
this.currentDir = currentDir;
|
|
57
58
|
}
|
|
@@ -382,25 +383,41 @@ export class InteractiveCLI {
|
|
|
382
383
|
}
|
|
383
384
|
// Show current database context clearly before view mode selection
|
|
384
385
|
MessageFormatter.info(`DB: ${database.name}`, { prefix: "Collections" });
|
|
385
|
-
// Ask user if they want to filter by database or
|
|
386
|
+
// Ask user if they want to filter by database, show all, or reuse previous selection
|
|
387
|
+
const choices = [
|
|
388
|
+
{
|
|
389
|
+
name: `Show all available collections/tables (${totalCount} total) - You can push any collection to any database`,
|
|
390
|
+
value: "all"
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: `Filter by database "${database.name}" - Show only related collections/tables`,
|
|
394
|
+
value: "filter"
|
|
395
|
+
}
|
|
396
|
+
];
|
|
397
|
+
if (this.lastSelectedCollectionIds && this.lastSelectedCollectionIds.length > 0) {
|
|
398
|
+
choices.unshift({
|
|
399
|
+
name: `Use same selection as before (${this.lastSelectedCollectionIds.length} items)`,
|
|
400
|
+
value: "same"
|
|
401
|
+
});
|
|
402
|
+
}
|
|
386
403
|
const { filterChoice } = await inquirer.prompt([
|
|
387
404
|
{
|
|
388
405
|
type: "list",
|
|
389
406
|
name: "filterChoice",
|
|
390
407
|
message: chalk.blue("How would you like to view collections/tables?"),
|
|
391
|
-
choices
|
|
392
|
-
|
|
393
|
-
name: `Show all available collections/tables (${totalCount} total) - You can push any collection to any database`,
|
|
394
|
-
value: "all"
|
|
395
|
-
},
|
|
396
|
-
{
|
|
397
|
-
name: `Filter by database "${database.name}" - Show only related collections/tables`,
|
|
398
|
-
value: "filter"
|
|
399
|
-
}
|
|
400
|
-
],
|
|
401
|
-
default: "all"
|
|
408
|
+
choices,
|
|
409
|
+
default: choices[0]?.value || "all"
|
|
402
410
|
}
|
|
403
411
|
]);
|
|
412
|
+
// If user wants to reuse the previous selection, map IDs to current config and return
|
|
413
|
+
if (filterChoice === "same") {
|
|
414
|
+
const map = new Map(this.getLocalCollections().map((c) => [c.$id || c.id, c]));
|
|
415
|
+
const selected = this.lastSelectedCollectionIds
|
|
416
|
+
.map((id) => map.get(id))
|
|
417
|
+
.filter((c) => !!c);
|
|
418
|
+
MessageFormatter.info(`Using same selection as previous: ${selected.length} item(s)`, { prefix: "Collections" });
|
|
419
|
+
return selected;
|
|
420
|
+
}
|
|
404
421
|
// User's choice overrides the parameter
|
|
405
422
|
const userWantsFiltering = filterChoice === "filter";
|
|
406
423
|
// Show appropriate informational message
|
|
@@ -416,7 +433,10 @@ export class InteractiveCLI {
|
|
|
416
433
|
else {
|
|
417
434
|
MessageFormatter.info(`ℹ️ Showing all available collections/tables - you can push any collection to any database\n`, { prefix: "Collections" });
|
|
418
435
|
}
|
|
419
|
-
|
|
436
|
+
const result = await this.selectCollections(database, databasesClient, message, multiSelect, preferLocal, userWantsFiltering);
|
|
437
|
+
// Remember this selection for subsequent databases
|
|
438
|
+
this.lastSelectedCollectionIds = (result || []).map((c) => c.$id || c.id);
|
|
439
|
+
return result;
|
|
420
440
|
}
|
|
421
441
|
getTemplateDefaults(template) {
|
|
422
442
|
const defaults = {
|
package/dist/main.js
CHANGED
|
@@ -964,6 +964,7 @@ async function main() {
|
|
|
964
964
|
// Build DatabaseSelection[] with tableIds per DB
|
|
965
965
|
const databaseSelections = [];
|
|
966
966
|
const allConfigItems = controller.config.collections || controller.config.tables || [];
|
|
967
|
+
let lastSelectedTableIds = null;
|
|
967
968
|
for (const dbId of selectedDbIds) {
|
|
968
969
|
const db = availableDatabases.find(d => d.$id === dbId);
|
|
969
970
|
if (!db)
|
|
@@ -983,13 +984,39 @@ async function main() {
|
|
|
983
984
|
// Determine selected table IDs
|
|
984
985
|
let selectedTableIds = [];
|
|
985
986
|
if (parsedArgv.collectionIds) {
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
const eligibleIds = new Set(eligibleConfigItems.map((c) => c.$id || c.id));
|
|
989
|
-
selectedTableIds = ids.filter(id => eligibleIds.has(id));
|
|
987
|
+
// Non-interactive: respect provided table IDs as-is (apply to each selected DB)
|
|
988
|
+
selectedTableIds = parsedArgv.collectionIds.split(/[\,\s]+/).filter(Boolean);
|
|
990
989
|
}
|
|
991
990
|
else {
|
|
992
|
-
|
|
991
|
+
// If we have a previous selection, offer to reuse it
|
|
992
|
+
if (lastSelectedTableIds && lastSelectedTableIds.length > 0) {
|
|
993
|
+
const inquirer = (await import("inquirer")).default;
|
|
994
|
+
const { reuseMode } = await inquirer.prompt([
|
|
995
|
+
{
|
|
996
|
+
type: "list",
|
|
997
|
+
name: "reuseMode",
|
|
998
|
+
message: `How do you want to select tables for ${db.name}?`,
|
|
999
|
+
choices: [
|
|
1000
|
+
{ name: `Use same selection as previous (${lastSelectedTableIds.length} items)`, value: "same" },
|
|
1001
|
+
{ name: `Filter by this database (manual select)`, value: "filter" },
|
|
1002
|
+
{ name: `Show all available in this database (manual select)`, value: "all" }
|
|
1003
|
+
],
|
|
1004
|
+
default: "same"
|
|
1005
|
+
}
|
|
1006
|
+
]);
|
|
1007
|
+
if (reuseMode === "same") {
|
|
1008
|
+
selectedTableIds = [...lastSelectedTableIds];
|
|
1009
|
+
}
|
|
1010
|
+
else if (reuseMode === "all") {
|
|
1011
|
+
selectedTableIds = await SelectionDialogs.selectTablesForDatabase(dbId, db.name, availableTables, allConfigItems, { showSelectAll: false, allowNewOnly: false, defaultSelected: lastSelectedTableIds });
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
selectedTableIds = await SelectionDialogs.selectTablesForDatabase(dbId, db.name, availableTables, eligibleConfigItems, { showSelectAll: false, allowNewOnly: true, defaultSelected: lastSelectedTableIds });
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
else {
|
|
1018
|
+
selectedTableIds = await SelectionDialogs.selectTablesForDatabase(dbId, db.name, availableTables, eligibleConfigItems, { showSelectAll: false, allowNewOnly: true, defaultSelected: [] });
|
|
1019
|
+
}
|
|
993
1020
|
}
|
|
994
1021
|
databaseSelections.push({
|
|
995
1022
|
databaseId: db.$id,
|
|
@@ -998,6 +1025,9 @@ async function main() {
|
|
|
998
1025
|
tableNames: [],
|
|
999
1026
|
isNew: false,
|
|
1000
1027
|
});
|
|
1028
|
+
if (!parsedArgv.collectionIds) {
|
|
1029
|
+
lastSelectedTableIds = selectedTableIds;
|
|
1030
|
+
}
|
|
1001
1031
|
}
|
|
1002
1032
|
if (databaseSelections.every(sel => sel.tableIds.length === 0)) {
|
|
1003
1033
|
MessageFormatter.warning("No tables/collections selected for push", { prefix: "Push" });
|
|
@@ -1008,10 +1038,13 @@ async function main() {
|
|
|
1008
1038
|
collections: databaseSelections.reduce((sum, s) => sum + s.tableIds.length, 0),
|
|
1009
1039
|
details: databaseSelections.map(s => `${s.databaseId}: ${s.tableIds.length} items`),
|
|
1010
1040
|
};
|
|
1011
|
-
|
|
1012
|
-
if (!
|
|
1013
|
-
|
|
1014
|
-
|
|
1041
|
+
// Skip confirmation if both dbIds and collectionIds are provided (non-interactive)
|
|
1042
|
+
if (!(parsedArgv.dbIds && parsedArgv.collectionIds)) {
|
|
1043
|
+
const confirmed = await ConfirmationDialogs.showOperationSummary('Push', pushSummary, { confirmationRequired: true });
|
|
1044
|
+
if (!confirmed) {
|
|
1045
|
+
MessageFormatter.info("Push operation cancelled", { prefix: "Push" });
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1015
1048
|
}
|
|
1016
1049
|
await controller.selectivePush(databaseSelections, []);
|
|
1017
1050
|
operationStats.pushedDatabases = databaseSelections.length;
|
|
@@ -7,7 +7,7 @@ import { CollectionSchema, attributeSchema, AppwriteConfigSchema, permissionsSch
|
|
|
7
7
|
import { getDatabaseFromConfig } from "./afterImportActions.js";
|
|
8
8
|
import { getAdapterFromConfig } from "../utils/getClientFromConfig.js";
|
|
9
9
|
import { listBuckets } from "../storage/methods.js";
|
|
10
|
-
import { listFunctions, listFunctionDeployments } from "../functions/methods.js";
|
|
10
|
+
import { listFunctions, listFunctionDeployments, getFunction } from "../functions/methods.js";
|
|
11
11
|
import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
12
12
|
import { isLegacyDatabases } from "../utils/typeGuards.js";
|
|
13
13
|
/**
|
|
@@ -420,7 +420,18 @@ export class AppwriteToX {
|
|
|
420
420
|
const remoteFunctions = await listFunctions(this.config.appwriteClient, [
|
|
421
421
|
Query.limit(1000),
|
|
422
422
|
]);
|
|
423
|
-
|
|
423
|
+
// Fetch full details per function to ensure 'scopes' and other fields are present
|
|
424
|
+
const detailedFunctions = [];
|
|
425
|
+
for (const f of remoteFunctions.functions) {
|
|
426
|
+
try {
|
|
427
|
+
const full = await getFunction(this.config.appwriteClient, f.$id);
|
|
428
|
+
detailedFunctions.push(full);
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
detailedFunctions.push(f);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
this.updatedConfig.functions = detailedFunctions.map((func) => ({
|
|
424
435
|
$id: func.$id,
|
|
425
436
|
name: func.name,
|
|
426
437
|
runtime: func.runtime,
|
|
@@ -432,6 +443,7 @@ export class AppwriteToX {
|
|
|
432
443
|
logging: func.logging !== false,
|
|
433
444
|
entrypoint: func.entrypoint || "src/index.ts",
|
|
434
445
|
commands: func.commands || "npm install",
|
|
446
|
+
scopes: Array.isArray(func.scopes) ? func.scopes : [],
|
|
435
447
|
dirPath: `functions/${func.name}`,
|
|
436
448
|
specification: func.specification,
|
|
437
449
|
}));
|
|
@@ -20,19 +20,19 @@ export declare const clearProcessingState: () => void;
|
|
|
20
20
|
/**
|
|
21
21
|
* Check if a collection has already been fully processed
|
|
22
22
|
*/
|
|
23
|
-
export declare const isCollectionProcessed: (collectionId: string) => boolean;
|
|
23
|
+
export declare const isCollectionProcessed: (collectionId: string, databaseId: string) => boolean;
|
|
24
24
|
/**
|
|
25
25
|
* Mark a collection as fully processed
|
|
26
26
|
*/
|
|
27
|
-
export declare const markCollectionProcessed: (collectionId: string, collectionName
|
|
27
|
+
export declare const markCollectionProcessed: (collectionId: string, collectionName: string | undefined, databaseId: string) => void;
|
|
28
28
|
/**
|
|
29
29
|
* Check if a specific attribute has been processed
|
|
30
30
|
*/
|
|
31
|
-
export declare const isAttributeProcessed: (collectionId: string, attributeKey: string) => boolean;
|
|
31
|
+
export declare const isAttributeProcessed: (databaseId: string, collectionId: string, attributeKey: string) => boolean;
|
|
32
32
|
/**
|
|
33
33
|
* Mark a specific attribute as processed
|
|
34
34
|
*/
|
|
35
|
-
export declare const markAttributeProcessed: (collectionId: string, attributeKey: string) => void;
|
|
35
|
+
export declare const markAttributeProcessed: (databaseId: string, collectionId: string, attributeKey: string) => void;
|
|
36
36
|
/**
|
|
37
37
|
* Process only specific attributes in the queue, not entire collections
|
|
38
38
|
* This prevents triggering full collection re-processing cycles
|
|
@@ -7,8 +7,14 @@ import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
|
7
7
|
// Global state management
|
|
8
8
|
export const queuedOperations = [];
|
|
9
9
|
export const nameToIdMapping = new Map();
|
|
10
|
+
// Keys are scoped per database to avoid cross-database collisions
|
|
11
|
+
// Collections key format: `${databaseId}::${collectionId}`
|
|
12
|
+
// Attributes key format: `${databaseId}::${collectionId}::${attributeKey}`
|
|
10
13
|
export const processedCollections = new Set();
|
|
11
|
-
export const processedAttributes = new Set();
|
|
14
|
+
export const processedAttributes = new Set();
|
|
15
|
+
// Helpers to build scoped keys
|
|
16
|
+
const collectionKey = (databaseId, collectionId) => `${databaseId}::${collectionId}`;
|
|
17
|
+
const attributeKeyScoped = (databaseId, collectionId, key) => `${databaseId}::${collectionId}::${key}`;
|
|
12
18
|
export const enqueueOperation = (operation) => {
|
|
13
19
|
// Avoid duplicate queue entries for same attribute
|
|
14
20
|
const attributeKey = operation.attribute?.key;
|
|
@@ -64,38 +70,40 @@ export const clearProcessingState = () => {
|
|
|
64
70
|
/**
|
|
65
71
|
* Check if a collection has already been fully processed
|
|
66
72
|
*/
|
|
67
|
-
export const isCollectionProcessed = (collectionId) => {
|
|
68
|
-
return processedCollections.has(collectionId);
|
|
73
|
+
export const isCollectionProcessed = (collectionId, databaseId) => {
|
|
74
|
+
return processedCollections.has(collectionKey(databaseId, collectionId));
|
|
69
75
|
};
|
|
70
76
|
/**
|
|
71
77
|
* Mark a collection as fully processed
|
|
72
78
|
*/
|
|
73
|
-
export const markCollectionProcessed = (collectionId, collectionName) => {
|
|
74
|
-
processedCollections.add(collectionId);
|
|
79
|
+
export const markCollectionProcessed = (collectionId, collectionName, databaseId) => {
|
|
80
|
+
processedCollections.add(collectionKey(databaseId, collectionId));
|
|
75
81
|
const logData = {
|
|
82
|
+
databaseId,
|
|
76
83
|
collectionId,
|
|
77
84
|
collectionName,
|
|
78
85
|
totalProcessedCollections: processedCollections.size,
|
|
79
86
|
operation: 'markCollectionProcessed'
|
|
80
87
|
};
|
|
81
88
|
if (collectionName) {
|
|
82
|
-
MessageFormatter.success(`Marked collection '${collectionName}' (${collectionId}) as processed
|
|
89
|
+
MessageFormatter.success(`Marked collection '${collectionName}' (${collectionId}) as processed`, { prefix: 'Tables' });
|
|
83
90
|
}
|
|
84
91
|
logger.info('Collection marked as processed', logData);
|
|
85
92
|
};
|
|
86
93
|
/**
|
|
87
94
|
* Check if a specific attribute has been processed
|
|
88
95
|
*/
|
|
89
|
-
export const isAttributeProcessed = (collectionId, attributeKey) => {
|
|
90
|
-
return processedAttributes.has(
|
|
96
|
+
export const isAttributeProcessed = (databaseId, collectionId, attributeKey) => {
|
|
97
|
+
return processedAttributes.has(attributeKeyScoped(databaseId, collectionId, attributeKey));
|
|
91
98
|
};
|
|
92
99
|
/**
|
|
93
100
|
* Mark a specific attribute as processed
|
|
94
101
|
*/
|
|
95
|
-
export const markAttributeProcessed = (collectionId, attributeKey) => {
|
|
96
|
-
const identifier =
|
|
102
|
+
export const markAttributeProcessed = (databaseId, collectionId, attributeKey) => {
|
|
103
|
+
const identifier = attributeKeyScoped(databaseId, collectionId, attributeKey);
|
|
97
104
|
processedAttributes.add(identifier);
|
|
98
105
|
logger.debug('Attribute marked as processed', {
|
|
106
|
+
databaseId,
|
|
99
107
|
collectionId,
|
|
100
108
|
attributeKey,
|
|
101
109
|
identifier,
|
|
@@ -152,8 +160,8 @@ export const processQueue = async (db, dbId) => {
|
|
|
152
160
|
}
|
|
153
161
|
const attributeKey = operation.attribute.key;
|
|
154
162
|
const collectionId = operation.collectionId;
|
|
155
|
-
// Skip if this specific attribute was already processed
|
|
156
|
-
if (isAttributeProcessed(collectionId, attributeKey)) {
|
|
163
|
+
// Skip if this specific attribute was already processed (per database)
|
|
164
|
+
if (isAttributeProcessed(dbId, collectionId, attributeKey)) {
|
|
157
165
|
MessageFormatter.debug(`Attribute '${attributeKey}' already processed, removing from queue`);
|
|
158
166
|
logger.debug('Removing already processed attribute from queue', {
|
|
159
167
|
attributeKey,
|
|
@@ -268,7 +276,7 @@ export const processQueue = async (db, dbId) => {
|
|
|
268
276
|
targetCollectionName: targetCollection.name,
|
|
269
277
|
operation: 'processQueue'
|
|
270
278
|
});
|
|
271
|
-
markAttributeProcessed(collectionId, attributeKey);
|
|
279
|
+
markAttributeProcessed(dbId, collectionId, attributeKey);
|
|
272
280
|
queuedOperations.splice(i, 1);
|
|
273
281
|
progress = true;
|
|
274
282
|
}
|
package/dist/utilsController.js
CHANGED
|
@@ -454,6 +454,13 @@ export class UtilsController {
|
|
|
454
454
|
this.config.apiMode = this.adapter.getApiMode();
|
|
455
455
|
logger.debug(`Updated config.apiMode from adapter: ${this.config.apiMode}`, { prefix: "UtilsController" });
|
|
456
456
|
}
|
|
457
|
+
// Ensure we don't carry state between databases in a multi-db push
|
|
458
|
+
// This resets processed sets and name->id mapping per database
|
|
459
|
+
try {
|
|
460
|
+
const { clearProcessingState } = await import('./shared/operationQueue.js');
|
|
461
|
+
clearProcessingState();
|
|
462
|
+
}
|
|
463
|
+
catch { }
|
|
457
464
|
// Always prefer adapter path for unified behavior. LegacyAdapter internally translates when needed.
|
|
458
465
|
if (this.adapter) {
|
|
459
466
|
logger.debug("Using adapter for createOrUpdateCollections (unified path)", {
|
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.8.
|
|
4
|
+
"version": "1.8.9",
|
|
5
5
|
"main": "src/main.ts",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -138,36 +138,14 @@ export const functionCommands = {
|
|
|
138
138
|
return;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
//
|
|
142
|
-
|
|
141
|
+
// Discover per-function .fnconfig.yaml definitions and merge with central list for selection
|
|
142
|
+
// No global prompt; we'll handle conflicts per-function if both exist.
|
|
143
|
+
let discovered: any[] = [];
|
|
144
|
+
let central: any[] = (cli as any).controller!.config!.functions || [];
|
|
143
145
|
try {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
name: 'source',
|
|
148
|
-
message: 'Select function config source:',
|
|
149
|
-
choices: [
|
|
150
|
-
{ name: 'config.yaml functions (central)', value: 'central' },
|
|
151
|
-
{ name: '.fnconfig.yaml (discovered per-function)', value: 'fnconfig' },
|
|
152
|
-
{ name: 'Both (merge; .fnconfig overrides)', value: 'both' },
|
|
153
|
-
],
|
|
154
|
-
default: 'both'
|
|
155
|
-
}
|
|
156
|
-
]);
|
|
157
|
-
sourceChoice = answer.source;
|
|
158
|
-
} catch {}
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
const discovered = discoverFnConfigs((cli as any).currentDir);
|
|
162
|
-
const central = (cli as any).controller!.config!.functions || [];
|
|
163
|
-
if (sourceChoice === 'central') {
|
|
164
|
-
(cli as any).controller!.config!.functions = central as any;
|
|
165
|
-
} else if (sourceChoice === 'fnconfig') {
|
|
166
|
-
(cli as any).controller!.config!.functions = discovered as any;
|
|
167
|
-
} else {
|
|
168
|
-
const merged = mergeDiscoveredFunctions(central, discovered);
|
|
169
|
-
(cli as any).controller!.config!.functions = merged as any;
|
|
170
|
-
}
|
|
146
|
+
discovered = discoverFnConfigs((cli as any).currentDir) as any[];
|
|
147
|
+
const merged = mergeDiscoveredFunctions(central as any, discovered as any);
|
|
148
|
+
(cli as any).controller!.config!.functions = merged as any;
|
|
171
149
|
} catch {}
|
|
172
150
|
|
|
173
151
|
const functions = await (cli as any).selectFunctions(
|
|
@@ -181,32 +159,59 @@ export const functionCommands = {
|
|
|
181
159
|
return;
|
|
182
160
|
}
|
|
183
161
|
|
|
184
|
-
for (const functionConfig of functions) {
|
|
162
|
+
for (const functionConfig of functions) {
|
|
185
163
|
if (!functionConfig) {
|
|
186
164
|
MessageFormatter.error("Invalid function configuration", undefined, { prefix: "Functions" });
|
|
187
165
|
return;
|
|
188
166
|
}
|
|
189
167
|
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
168
|
+
// Resolve effective config for this function (prefer per-function choice if both sources exist)
|
|
169
|
+
const byIdOrName = (arr: any[]) => arr.find((f:any) => f?.$id === functionConfig.$id || f?.name === functionConfig.name);
|
|
170
|
+
const centralDef = byIdOrName(central as any[]);
|
|
171
|
+
const discoveredDef = byIdOrName(discovered as any[]);
|
|
172
|
+
|
|
173
|
+
let effectiveConfig = functionConfig;
|
|
174
|
+
if (centralDef && discoveredDef) {
|
|
175
|
+
try {
|
|
176
|
+
const answer = await inquirer.prompt([
|
|
177
|
+
{
|
|
178
|
+
type: 'list',
|
|
179
|
+
name: 'cfgChoice',
|
|
180
|
+
message: `Multiple configs found for '${functionConfig.name}'. Which to use?`,
|
|
181
|
+
choices: [
|
|
182
|
+
{ name: 'config.yaml (central)', value: 'central' },
|
|
183
|
+
{ name: '.fnconfig.yaml (local file)', value: 'fnconfig' },
|
|
184
|
+
{ name: 'Merge (.fnconfig overrides central)', value: 'merge' },
|
|
185
|
+
],
|
|
186
|
+
default: 'fnconfig'
|
|
187
|
+
}
|
|
188
|
+
]);
|
|
189
|
+
if (answer.cfgChoice === 'central') effectiveConfig = centralDef;
|
|
190
|
+
else if (answer.cfgChoice === 'fnconfig') effectiveConfig = discoveredDef;
|
|
191
|
+
else effectiveConfig = { ...centralDef, ...discoveredDef };
|
|
192
|
+
} catch {}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Ensure functions array exists
|
|
196
|
+
if (!(cli as any).controller.config.functions) {
|
|
197
|
+
(cli as any).controller.config.functions = [];
|
|
198
|
+
}
|
|
194
199
|
|
|
195
|
-
const functionNameLower =
|
|
196
|
-
.toLowerCase()
|
|
197
|
-
.replace(/\s+/g, "-");
|
|
200
|
+
const functionNameLower = effectiveConfig.name
|
|
201
|
+
.toLowerCase()
|
|
202
|
+
.replace(/\s+/g, "-");
|
|
198
203
|
|
|
199
204
|
// Debug logging
|
|
200
205
|
MessageFormatter.info(`🔍 Function deployment debug:`, { prefix: "Functions" });
|
|
201
|
-
MessageFormatter.info(` Function name: ${
|
|
202
|
-
MessageFormatter.info(` Function ID: ${
|
|
203
|
-
MessageFormatter.info(` Config dirPath: ${
|
|
204
|
-
if (
|
|
205
|
-
const expandedPath =
|
|
206
|
-
?
|
|
207
|
-
:
|
|
208
|
-
MessageFormatter.info(` Expanded dirPath: ${expandedPath}`, { prefix: "Functions" });
|
|
209
|
-
}
|
|
206
|
+
MessageFormatter.info(` Function name: ${effectiveConfig.name}`, { prefix: "Functions" });
|
|
207
|
+
MessageFormatter.info(` Function ID: ${effectiveConfig.$id}`, { prefix: "Functions" });
|
|
208
|
+
MessageFormatter.info(` Config dirPath: ${effectiveConfig.dirPath || 'undefined'}`, { prefix: "Functions" });
|
|
209
|
+
if (effectiveConfig.dirPath) {
|
|
210
|
+
const expandedPath = effectiveConfig.dirPath.startsWith('~/')
|
|
211
|
+
? effectiveConfig.dirPath.replace('~', os.homedir())
|
|
212
|
+
: effectiveConfig.dirPath;
|
|
213
|
+
MessageFormatter.info(` Expanded dirPath: ${expandedPath}`, { prefix: "Functions" });
|
|
214
|
+
}
|
|
210
215
|
MessageFormatter.info(` Appwrite folder: ${(cli as any).controller.getAppwriteFolderPath()}`, { prefix: "Functions" });
|
|
211
216
|
MessageFormatter.info(` Current working dir: ${process.cwd()}`, { prefix: "Functions" });
|
|
212
217
|
|
|
@@ -218,10 +223,10 @@ export const functionCommands = {
|
|
|
218
223
|
// Check locations in priority order:
|
|
219
224
|
const priorityLocations = [
|
|
220
225
|
// 1. Config dirPath if specified (with tilde expansion)
|
|
221
|
-
|
|
222
|
-
? (require('node:path').isAbsolute(expandTildePath(
|
|
223
|
-
? expandTildePath(
|
|
224
|
-
: require('node:path').resolve(yamlBaseDir, expandTildePath(
|
|
226
|
+
effectiveConfig.dirPath
|
|
227
|
+
? (require('node:path').isAbsolute(expandTildePath(effectiveConfig.dirPath))
|
|
228
|
+
? expandTildePath(effectiveConfig.dirPath)
|
|
229
|
+
: require('node:path').resolve(yamlBaseDir, expandTildePath(effectiveConfig.dirPath)))
|
|
225
230
|
: undefined,
|
|
226
231
|
// 2. Appwrite config folder/functions/name
|
|
227
232
|
join(
|
|
@@ -240,7 +245,7 @@ export const functionCommands = {
|
|
|
240
245
|
MessageFormatter.info(` ${i + 1}. ${loc}`, { prefix: "Functions" });
|
|
241
246
|
});
|
|
242
247
|
|
|
243
|
-
let functionPath: string | null = null;
|
|
248
|
+
let functionPath: string | null = null;
|
|
244
249
|
|
|
245
250
|
// Check each priority location
|
|
246
251
|
for (const location of priorityLocations) {
|
|
@@ -280,21 +285,21 @@ export const functionCommands = {
|
|
|
280
285
|
const { path: downloadedPath, function: remoteFunction } =
|
|
281
286
|
await downloadLatestFunctionDeployment(
|
|
282
287
|
(cli as any).controller.appwriteServer!,
|
|
283
|
-
|
|
284
|
-
join((cli as any).controller.getAppwriteFolderPath()!, "functions")
|
|
285
|
-
);
|
|
288
|
+
effectiveConfig.$id,
|
|
289
|
+
join((cli as any).controller.getAppwriteFolderPath()!, "functions")
|
|
290
|
+
);
|
|
286
291
|
MessageFormatter.success(`✨ Function downloaded to ${downloadedPath}`, { prefix: "Functions" });
|
|
287
292
|
|
|
288
293
|
functionPath = downloadedPath;
|
|
289
|
-
|
|
294
|
+
effectiveConfig.dirPath = downloadedPath;
|
|
290
295
|
|
|
291
296
|
const existingIndex = (cli as any).controller.config.functions.findIndex(
|
|
292
297
|
(f: any) => f?.$id === remoteFunction.$id
|
|
293
298
|
);
|
|
294
299
|
|
|
295
300
|
if (existingIndex >= 0) {
|
|
296
|
-
(cli as any).controller.config.functions[existingIndex].dirPath =
|
|
297
|
-
downloadedPath;
|
|
301
|
+
(cli as any).controller.config.functions[existingIndex].dirPath =
|
|
302
|
+
downloadedPath;
|
|
298
303
|
}
|
|
299
304
|
|
|
300
305
|
await (cli as any).reloadConfigWithSessionPreservation();
|
|
@@ -303,9 +308,9 @@ export const functionCommands = {
|
|
|
303
308
|
return;
|
|
304
309
|
}
|
|
305
310
|
} else {
|
|
306
|
-
MessageFormatter.error(`Function ${
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
311
|
+
MessageFormatter.error(`Function ${effectiveConfig.name} not found locally. Cannot deploy.`, undefined, { prefix: "Functions" });
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
309
314
|
}
|
|
310
315
|
|
|
311
316
|
if (!(cli as any).controller.appwriteServer) {
|
|
@@ -316,13 +321,13 @@ export const functionCommands = {
|
|
|
316
321
|
try {
|
|
317
322
|
await deployLocalFunction(
|
|
318
323
|
(cli as any).controller.appwriteServer,
|
|
319
|
-
|
|
320
|
-
{
|
|
321
|
-
...
|
|
322
|
-
dirPath: functionPath,
|
|
323
|
-
},
|
|
324
|
-
functionPath
|
|
325
|
-
);
|
|
324
|
+
effectiveConfig.name,
|
|
325
|
+
{
|
|
326
|
+
...effectiveConfig,
|
|
327
|
+
dirPath: functionPath,
|
|
328
|
+
},
|
|
329
|
+
functionPath
|
|
330
|
+
);
|
|
326
331
|
MessageFormatter.success("Function deployed successfully!", { prefix: "Functions" });
|
|
327
332
|
} catch (error) {
|
|
328
333
|
MessageFormatter.error("Failed to deploy function", error instanceof Error ? error : new Error(String(error)), { prefix: "Functions" });
|
|
@@ -1835,7 +1835,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
|
|
|
1835
1835
|
// Filter to only attributes that need processing (new, changed, or not yet processed)
|
|
1836
1836
|
const attributesToProcess = attributes.filter((attribute) => {
|
|
1837
1837
|
// Skip if already processed in this session
|
|
1838
|
-
if (isAttributeProcessed(currentCollection.$id, attribute.key)) {
|
|
1838
|
+
if (isAttributeProcessed(dbId, currentCollection.$id, attribute.key)) {
|
|
1839
1839
|
return false;
|
|
1840
1840
|
}
|
|
1841
1841
|
|
|
@@ -1879,7 +1879,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (
|
|
|
1879
1879
|
|
|
1880
1880
|
if (success) {
|
|
1881
1881
|
// Mark this specific attribute as processed
|
|
1882
|
-
markAttributeProcessed(currentCollection.$id, attribute.key);
|
|
1882
|
+
markAttributeProcessed(dbId, currentCollection.$id, attribute.key);
|
|
1883
1883
|
|
|
1884
1884
|
// Get updated collection data for next iteration
|
|
1885
1885
|
try {
|
|
@@ -215,7 +215,7 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
215
215
|
): Promise<void> => {
|
|
216
216
|
const collectionsToProcess =
|
|
217
217
|
selectedCollections.length > 0 ? selectedCollections : (config.collections || []);
|
|
218
|
-
if (!collectionsToProcess || collectionsToProcess.length === 0) return;
|
|
218
|
+
if (!collectionsToProcess || collectionsToProcess.length === 0) return;
|
|
219
219
|
|
|
220
220
|
const usedIds = new Set<string>();
|
|
221
221
|
MessageFormatter.info(`Processing ${collectionsToProcess.length} tables via adapter with intelligent state management`, { prefix: "Tables" });
|
|
@@ -238,11 +238,11 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
238
238
|
for (const collection of collectionsToProcess) {
|
|
239
239
|
const { attributes, indexes, ...collectionData } = collection as any;
|
|
240
240
|
|
|
241
|
-
// Check if this table has already been processed in this session
|
|
242
|
-
if (collectionData.$id && isCollectionProcessed(collectionData.$id)) {
|
|
243
|
-
MessageFormatter.info(`Table '${collectionData.name}' already processed, skipping`, { prefix: "Tables" });
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
241
|
+
// Check if this table has already been processed in this session (per database)
|
|
242
|
+
if (collectionData.$id && isCollectionProcessed(collectionData.$id, databaseId)) {
|
|
243
|
+
MessageFormatter.info(`Table '${collectionData.name}' already processed, skipping`, { prefix: "Tables" });
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
246
|
|
|
247
247
|
// Prepare permissions as strings (reuse Permission helper)
|
|
248
248
|
const permissions: string[] = [];
|
|
@@ -649,8 +649,8 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
|
649
649
|
MessageFormatter.warning(`Could not evaluate deletions: ${(e as Error)?.message || e}`, { prefix: 'Attributes' });
|
|
650
650
|
}
|
|
651
651
|
|
|
652
|
-
// Mark this table as fully processed to prevent re-processing
|
|
653
|
-
markCollectionProcessed(tableId, collectionData.name);
|
|
652
|
+
// Mark this table as fully processed for this database to prevent re-processing in the same DB only
|
|
653
|
+
markCollectionProcessed(tableId, collectionData.name, databaseId);
|
|
654
654
|
}
|
|
655
655
|
|
|
656
656
|
// Process queued relationships once mapping likely populated
|
package/src/interactiveCLI.ts
CHANGED
|
@@ -74,8 +74,9 @@ enum CHOICES {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
export class InteractiveCLI {
|
|
77
|
-
private controller: UtilsController | undefined;
|
|
78
|
-
private isUsingTypeScriptConfig: boolean = false;
|
|
77
|
+
private controller: UtilsController | undefined;
|
|
78
|
+
private isUsingTypeScriptConfig: boolean = false;
|
|
79
|
+
private lastSelectedCollectionIds: string[] = [];
|
|
79
80
|
|
|
80
81
|
constructor(private currentDir: string) {}
|
|
81
82
|
|
|
@@ -454,14 +455,14 @@ export class InteractiveCLI {
|
|
|
454
455
|
/**
|
|
455
456
|
* Enhanced collection/table selection with better guidance for mixed scenarios
|
|
456
457
|
*/
|
|
457
|
-
private async selectCollectionsAndTables(
|
|
458
|
-
database: Models.Database,
|
|
459
|
-
databasesClient: Databases,
|
|
460
|
-
message: string,
|
|
461
|
-
multiSelect = true,
|
|
462
|
-
preferLocal = false,
|
|
463
|
-
shouldFilterByDatabase = false
|
|
464
|
-
): Promise<Models.Collection[]> {
|
|
458
|
+
private async selectCollectionsAndTables(
|
|
459
|
+
database: Models.Database,
|
|
460
|
+
databasesClient: Databases,
|
|
461
|
+
message: string,
|
|
462
|
+
multiSelect = true,
|
|
463
|
+
preferLocal = false,
|
|
464
|
+
shouldFilterByDatabase = false
|
|
465
|
+
): Promise<Models.Collection[]> {
|
|
465
466
|
const configCollections = this.getLocalCollections();
|
|
466
467
|
const collectionsCount = configCollections.filter(c => !c._isFromTablesDir).length;
|
|
467
468
|
const tablesCount = configCollections.filter(c => c._isFromTablesDir).length;
|
|
@@ -481,28 +482,48 @@ export class InteractiveCLI {
|
|
|
481
482
|
// Show current database context clearly before view mode selection
|
|
482
483
|
MessageFormatter.info(`DB: ${database.name}`, { prefix: "Collections" });
|
|
483
484
|
|
|
484
|
-
// Ask user if they want to filter by database or
|
|
485
|
+
// Ask user if they want to filter by database, show all, or reuse previous selection
|
|
486
|
+
const choices: { name: string; value: string }[] = [
|
|
487
|
+
{
|
|
488
|
+
name: `Show all available collections/tables (${totalCount} total) - You can push any collection to any database`,
|
|
489
|
+
value: "all"
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
name: `Filter by database "${database.name}" - Show only related collections/tables`,
|
|
493
|
+
value: "filter"
|
|
494
|
+
}
|
|
495
|
+
];
|
|
496
|
+
if (this.lastSelectedCollectionIds && this.lastSelectedCollectionIds.length > 0) {
|
|
497
|
+
choices.unshift({
|
|
498
|
+
name: `Use same selection as before (${this.lastSelectedCollectionIds.length} items)`,
|
|
499
|
+
value: "same"
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
485
503
|
const { filterChoice } = await inquirer.prompt([
|
|
486
504
|
{
|
|
487
505
|
type: "list",
|
|
488
506
|
name: "filterChoice",
|
|
489
507
|
message: chalk.blue("How would you like to view collections/tables?"),
|
|
490
|
-
choices
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
508
|
+
choices,
|
|
509
|
+
default: choices[0]?.value || "all"
|
|
510
|
+
}
|
|
511
|
+
]);
|
|
512
|
+
|
|
513
|
+
// If user wants to reuse the previous selection, map IDs to current config and return
|
|
514
|
+
if (filterChoice === "same") {
|
|
515
|
+
const map = new Map<string, Models.Collection>(
|
|
516
|
+
this.getLocalCollections().map((c: any) => [c.$id || c.id, c as Models.Collection])
|
|
517
|
+
);
|
|
518
|
+
const selected = this.lastSelectedCollectionIds
|
|
519
|
+
.map((id) => map.get(id))
|
|
520
|
+
.filter((c): c is Models.Collection => !!c);
|
|
521
|
+
MessageFormatter.info(`Using same selection as previous: ${selected.length} item(s)`, { prefix: "Collections" });
|
|
522
|
+
return selected;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// User's choice overrides the parameter
|
|
526
|
+
const userWantsFiltering = filterChoice === "filter";
|
|
506
527
|
|
|
507
528
|
// Show appropriate informational message
|
|
508
529
|
if (userWantsFiltering) {
|
|
@@ -519,8 +540,18 @@ export class InteractiveCLI {
|
|
|
519
540
|
MessageFormatter.info(`ℹ️ Showing all available collections/tables - you can push any collection to any database\n`, { prefix: "Collections" });
|
|
520
541
|
}
|
|
521
542
|
|
|
522
|
-
|
|
523
|
-
|
|
543
|
+
const result = await this.selectCollections(
|
|
544
|
+
database,
|
|
545
|
+
databasesClient,
|
|
546
|
+
message,
|
|
547
|
+
multiSelect,
|
|
548
|
+
preferLocal,
|
|
549
|
+
userWantsFiltering
|
|
550
|
+
);
|
|
551
|
+
// Remember this selection for subsequent databases
|
|
552
|
+
this.lastSelectedCollectionIds = (result || []).map((c: any) => c.$id || c.id);
|
|
553
|
+
return result;
|
|
554
|
+
}
|
|
524
555
|
|
|
525
556
|
private getTemplateDefaults(template: string) {
|
|
526
557
|
const defaults = {
|
package/src/main.ts
CHANGED
|
@@ -1398,6 +1398,7 @@ async function main() {
|
|
|
1398
1398
|
// Build DatabaseSelection[] with tableIds per DB
|
|
1399
1399
|
const databaseSelections: DatabaseSelection[] = [];
|
|
1400
1400
|
const allConfigItems = controller.config.collections || controller.config.tables || [];
|
|
1401
|
+
let lastSelectedTableIds: string[] | null = null;
|
|
1401
1402
|
|
|
1402
1403
|
for (const dbId of selectedDbIds) {
|
|
1403
1404
|
const db = availableDatabases.find(d => d.$id === dbId);
|
|
@@ -1418,18 +1419,54 @@ async function main() {
|
|
|
1418
1419
|
// Determine selected table IDs
|
|
1419
1420
|
let selectedTableIds: string[] = [];
|
|
1420
1421
|
if (parsedArgv.collectionIds) {
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
const eligibleIds = new Set(eligibleConfigItems.map((c: any) => c.$id || c.id));
|
|
1424
|
-
selectedTableIds = ids.filter(id => eligibleIds.has(id));
|
|
1422
|
+
// Non-interactive: respect provided table IDs as-is (apply to each selected DB)
|
|
1423
|
+
selectedTableIds = parsedArgv.collectionIds.split(/[\,\s]+/).filter(Boolean);
|
|
1425
1424
|
} else {
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1425
|
+
// If we have a previous selection, offer to reuse it
|
|
1426
|
+
if (lastSelectedTableIds && lastSelectedTableIds.length > 0) {
|
|
1427
|
+
const inquirer = (await import("inquirer")).default;
|
|
1428
|
+
const { reuseMode } = await inquirer.prompt([
|
|
1429
|
+
{
|
|
1430
|
+
type: "list",
|
|
1431
|
+
name: "reuseMode",
|
|
1432
|
+
message: `How do you want to select tables for ${db.name}?`,
|
|
1433
|
+
choices: [
|
|
1434
|
+
{ name: `Use same selection as previous (${lastSelectedTableIds.length} items)`, value: "same" },
|
|
1435
|
+
{ name: `Filter by this database (manual select)`, value: "filter" },
|
|
1436
|
+
{ name: `Show all available in this database (manual select)`, value: "all" }
|
|
1437
|
+
],
|
|
1438
|
+
default: "same"
|
|
1439
|
+
}
|
|
1440
|
+
]);
|
|
1441
|
+
|
|
1442
|
+
if (reuseMode === "same") {
|
|
1443
|
+
selectedTableIds = [...lastSelectedTableIds];
|
|
1444
|
+
} else if (reuseMode === "all") {
|
|
1445
|
+
selectedTableIds = await SelectionDialogs.selectTablesForDatabase(
|
|
1446
|
+
dbId,
|
|
1447
|
+
db.name,
|
|
1448
|
+
availableTables,
|
|
1449
|
+
allConfigItems as any[],
|
|
1450
|
+
{ showSelectAll: false, allowNewOnly: false, defaultSelected: lastSelectedTableIds }
|
|
1451
|
+
);
|
|
1452
|
+
} else {
|
|
1453
|
+
selectedTableIds = await SelectionDialogs.selectTablesForDatabase(
|
|
1454
|
+
dbId,
|
|
1455
|
+
db.name,
|
|
1456
|
+
availableTables,
|
|
1457
|
+
eligibleConfigItems,
|
|
1458
|
+
{ showSelectAll: false, allowNewOnly: true, defaultSelected: lastSelectedTableIds }
|
|
1459
|
+
);
|
|
1460
|
+
}
|
|
1461
|
+
} else {
|
|
1462
|
+
selectedTableIds = await SelectionDialogs.selectTablesForDatabase(
|
|
1463
|
+
dbId,
|
|
1464
|
+
db.name,
|
|
1465
|
+
availableTables,
|
|
1466
|
+
eligibleConfigItems,
|
|
1467
|
+
{ showSelectAll: false, allowNewOnly: true, defaultSelected: [] }
|
|
1468
|
+
);
|
|
1469
|
+
}
|
|
1433
1470
|
}
|
|
1434
1471
|
|
|
1435
1472
|
databaseSelections.push({
|
|
@@ -1439,6 +1476,9 @@ async function main() {
|
|
|
1439
1476
|
tableNames: [],
|
|
1440
1477
|
isNew: false,
|
|
1441
1478
|
});
|
|
1479
|
+
if (!parsedArgv.collectionIds) {
|
|
1480
|
+
lastSelectedTableIds = selectedTableIds;
|
|
1481
|
+
}
|
|
1442
1482
|
}
|
|
1443
1483
|
|
|
1444
1484
|
if (databaseSelections.every(sel => sel.tableIds.length === 0)) {
|
|
@@ -1451,10 +1491,13 @@ async function main() {
|
|
|
1451
1491
|
collections: databaseSelections.reduce((sum, s) => sum + s.tableIds.length, 0),
|
|
1452
1492
|
details: databaseSelections.map(s => `${s.databaseId}: ${s.tableIds.length} items`),
|
|
1453
1493
|
};
|
|
1454
|
-
|
|
1455
|
-
if (!
|
|
1456
|
-
|
|
1457
|
-
|
|
1494
|
+
// Skip confirmation if both dbIds and collectionIds are provided (non-interactive)
|
|
1495
|
+
if (!(parsedArgv.dbIds && parsedArgv.collectionIds)) {
|
|
1496
|
+
const confirmed = await ConfirmationDialogs.showOperationSummary('Push', pushSummary, { confirmationRequired: true });
|
|
1497
|
+
if (!confirmed) {
|
|
1498
|
+
MessageFormatter.info("Push operation cancelled", { prefix: "Push" });
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1458
1501
|
}
|
|
1459
1502
|
|
|
1460
1503
|
await controller.selectivePush(databaseSelections, []);
|
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
import { getDatabaseFromConfig } from "./afterImportActions.js";
|
|
29
29
|
import { getAdapterFromConfig } from "../utils/getClientFromConfig.js";
|
|
30
30
|
import { listBuckets } from "../storage/methods.js";
|
|
31
|
-
import { listFunctions, listFunctionDeployments } from "../functions/methods.js";
|
|
31
|
+
import { listFunctions, listFunctionDeployments, getFunction } from "../functions/methods.js";
|
|
32
32
|
import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
33
33
|
import { isLegacyDatabases } from "../utils/typeGuards.js";
|
|
34
34
|
import type { DatabaseAdapter } from "../adapters/DatabaseAdapter.js";
|
|
@@ -570,27 +570,39 @@ export class AppwriteToX {
|
|
|
570
570
|
antivirus: bucket.antivirus,
|
|
571
571
|
}));
|
|
572
572
|
|
|
573
|
-
const remoteFunctions = await listFunctions(this.config.appwriteClient!, [
|
|
574
|
-
Query.limit(1000),
|
|
575
|
-
]);
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
573
|
+
const remoteFunctions = await listFunctions(this.config.appwriteClient!, [
|
|
574
|
+
Query.limit(1000),
|
|
575
|
+
]);
|
|
576
|
+
|
|
577
|
+
// Fetch full details per function to ensure 'scopes' and other fields are present
|
|
578
|
+
const detailedFunctions: any[] = [];
|
|
579
|
+
for (const f of remoteFunctions.functions) {
|
|
580
|
+
try {
|
|
581
|
+
const full = await getFunction(this.config.appwriteClient!, f.$id);
|
|
582
|
+
detailedFunctions.push(full);
|
|
583
|
+
} catch {
|
|
584
|
+
detailedFunctions.push(f);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
this.updatedConfig.functions = detailedFunctions.map(
|
|
589
|
+
(func: any) => ({
|
|
590
|
+
$id: func.$id,
|
|
591
|
+
name: func.name,
|
|
592
|
+
runtime: func.runtime as Runtime,
|
|
593
|
+
execute: func.execute,
|
|
594
|
+
events: func.events || [],
|
|
595
|
+
schedule: func.schedule || "",
|
|
596
|
+
timeout: func.timeout || 15,
|
|
597
|
+
enabled: func.enabled !== false,
|
|
598
|
+
logging: func.logging !== false,
|
|
599
|
+
entrypoint: func.entrypoint || "src/index.ts",
|
|
600
|
+
commands: func.commands || "npm install",
|
|
601
|
+
scopes: Array.isArray(func.scopes) ? func.scopes : [],
|
|
602
|
+
dirPath: `functions/${func.name}`,
|
|
603
|
+
specification: func.specification as Specification,
|
|
604
|
+
})
|
|
605
|
+
);
|
|
594
606
|
|
|
595
607
|
// Make sure to update the config with all changes including databases
|
|
596
608
|
updatedConfig.functions = this.updatedConfig.functions;
|
|
@@ -16,10 +16,17 @@ export interface QueuedOperation {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
// Global state management
|
|
19
|
-
export const queuedOperations: QueuedOperation[] = [];
|
|
20
|
-
export const nameToIdMapping: Map<string, string> = new Map();
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
export const queuedOperations: QueuedOperation[] = [];
|
|
20
|
+
export const nameToIdMapping: Map<string, string> = new Map();
|
|
21
|
+
// Keys are scoped per database to avoid cross-database collisions
|
|
22
|
+
// Collections key format: `${databaseId}::${collectionId}`
|
|
23
|
+
// Attributes key format: `${databaseId}::${collectionId}::${attributeKey}`
|
|
24
|
+
export const processedCollections: Set<string> = new Set();
|
|
25
|
+
export const processedAttributes: Set<string> = new Set();
|
|
26
|
+
|
|
27
|
+
// Helpers to build scoped keys
|
|
28
|
+
const collectionKey = (databaseId: string, collectionId: string) => `${databaseId}::${collectionId}`;
|
|
29
|
+
const attributeKeyScoped = (databaseId: string, collectionId: string, key: string) => `${databaseId}::${collectionId}::${key}`;
|
|
23
30
|
|
|
24
31
|
export const enqueueOperation = (operation: QueuedOperation) => {
|
|
25
32
|
// Avoid duplicate queue entries for same attribute
|
|
@@ -86,52 +93,54 @@ export const clearProcessingState = () => {
|
|
|
86
93
|
/**
|
|
87
94
|
* Check if a collection has already been fully processed
|
|
88
95
|
*/
|
|
89
|
-
export const isCollectionProcessed = (collectionId: string): boolean => {
|
|
90
|
-
return processedCollections.has(collectionId);
|
|
91
|
-
};
|
|
96
|
+
export const isCollectionProcessed = (collectionId: string, databaseId: string): boolean => {
|
|
97
|
+
return processedCollections.has(collectionKey(databaseId, collectionId));
|
|
98
|
+
};
|
|
92
99
|
|
|
93
100
|
/**
|
|
94
101
|
* Mark a collection as fully processed
|
|
95
102
|
*/
|
|
96
|
-
export const markCollectionProcessed = (collectionId: string, collectionName
|
|
97
|
-
processedCollections.add(collectionId);
|
|
98
|
-
|
|
99
|
-
const logData = {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
103
|
+
export const markCollectionProcessed = (collectionId: string, collectionName: string | undefined, databaseId: string) => {
|
|
104
|
+
processedCollections.add(collectionKey(databaseId, collectionId));
|
|
105
|
+
|
|
106
|
+
const logData = {
|
|
107
|
+
databaseId,
|
|
108
|
+
collectionId,
|
|
109
|
+
collectionName,
|
|
110
|
+
totalProcessedCollections: processedCollections.size,
|
|
111
|
+
operation: 'markCollectionProcessed'
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
if (collectionName) {
|
|
115
|
+
MessageFormatter.success(`Marked collection '${collectionName}' (${collectionId}) as processed`, { prefix: 'Tables' });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
logger.info('Collection marked as processed', logData);
|
|
119
|
+
};
|
|
112
120
|
|
|
113
121
|
/**
|
|
114
122
|
* Check if a specific attribute has been processed
|
|
115
123
|
*/
|
|
116
|
-
export const isAttributeProcessed = (collectionId: string, attributeKey: string): boolean => {
|
|
117
|
-
return processedAttributes.has(
|
|
118
|
-
};
|
|
124
|
+
export const isAttributeProcessed = (databaseId: string, collectionId: string, attributeKey: string): boolean => {
|
|
125
|
+
return processedAttributes.has(attributeKeyScoped(databaseId, collectionId, attributeKey));
|
|
126
|
+
};
|
|
119
127
|
|
|
120
128
|
/**
|
|
121
129
|
* Mark a specific attribute as processed
|
|
122
130
|
*/
|
|
123
|
-
export const markAttributeProcessed = (collectionId: string, attributeKey: string) => {
|
|
124
|
-
const identifier =
|
|
125
|
-
processedAttributes.add(identifier);
|
|
126
|
-
|
|
127
|
-
logger.debug('Attribute marked as processed', {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
};
|
|
131
|
+
export const markAttributeProcessed = (databaseId: string, collectionId: string, attributeKey: string) => {
|
|
132
|
+
const identifier = attributeKeyScoped(databaseId, collectionId, attributeKey);
|
|
133
|
+
processedAttributes.add(identifier);
|
|
134
|
+
|
|
135
|
+
logger.debug('Attribute marked as processed', {
|
|
136
|
+
databaseId,
|
|
137
|
+
collectionId,
|
|
138
|
+
attributeKey,
|
|
139
|
+
identifier,
|
|
140
|
+
totalProcessedAttributes: processedAttributes.size,
|
|
141
|
+
operation: 'markAttributeProcessed'
|
|
142
|
+
});
|
|
143
|
+
};
|
|
135
144
|
|
|
136
145
|
/**
|
|
137
146
|
* Process only specific attributes in the queue, not entire collections
|
|
@@ -193,8 +202,8 @@ export const processQueue = async (db: Databases | DatabaseAdapter, dbId: string
|
|
|
193
202
|
const attributeKey = operation.attribute.key;
|
|
194
203
|
const collectionId = operation.collectionId;
|
|
195
204
|
|
|
196
|
-
// Skip if this specific attribute was already processed
|
|
197
|
-
if (isAttributeProcessed(collectionId, attributeKey)) {
|
|
205
|
+
// Skip if this specific attribute was already processed (per database)
|
|
206
|
+
if (isAttributeProcessed(dbId, collectionId, attributeKey)) {
|
|
198
207
|
MessageFormatter.debug(`Attribute '${attributeKey}' already processed, removing from queue`);
|
|
199
208
|
logger.debug('Removing already processed attribute from queue', {
|
|
200
209
|
attributeKey,
|
|
@@ -327,7 +336,7 @@ export const processQueue = async (db: Databases | DatabaseAdapter, dbId: string
|
|
|
327
336
|
targetCollectionName: targetCollection.name,
|
|
328
337
|
operation: 'processQueue'
|
|
329
338
|
});
|
|
330
|
-
markAttributeProcessed(collectionId, attributeKey);
|
|
339
|
+
markAttributeProcessed(dbId, collectionId, attributeKey);
|
|
331
340
|
queuedOperations.splice(i, 1);
|
|
332
341
|
progress = true;
|
|
333
342
|
} else {
|
package/src/utilsController.ts
CHANGED
|
@@ -615,9 +615,9 @@ export class UtilsController {
|
|
|
615
615
|
deletedCollections?: { collectionId: string; collectionName: string }[],
|
|
616
616
|
collections: Models.Collection[] = []
|
|
617
617
|
) {
|
|
618
|
-
await this.init();
|
|
619
|
-
if (!this.database || !this.config)
|
|
620
|
-
throw new Error("Database or config not initialized");
|
|
618
|
+
await this.init();
|
|
619
|
+
if (!this.database || !this.config)
|
|
620
|
+
throw new Error("Database or config not initialized");
|
|
621
621
|
|
|
622
622
|
// Ensure apiMode is properly set from adapter
|
|
623
623
|
if (this.adapter && (!this.config.apiMode || this.config.apiMode === 'auto')) {
|
|
@@ -625,6 +625,13 @@ export class UtilsController {
|
|
|
625
625
|
logger.debug(`Updated config.apiMode from adapter: ${this.config.apiMode}`, { prefix: "UtilsController" });
|
|
626
626
|
}
|
|
627
627
|
|
|
628
|
+
// Ensure we don't carry state between databases in a multi-db push
|
|
629
|
+
// This resets processed sets and name->id mapping per database
|
|
630
|
+
try {
|
|
631
|
+
const { clearProcessingState } = await import('./shared/operationQueue.js');
|
|
632
|
+
clearProcessingState();
|
|
633
|
+
} catch {}
|
|
634
|
+
|
|
628
635
|
// Always prefer adapter path for unified behavior. LegacyAdapter internally translates when needed.
|
|
629
636
|
if (this.adapter) {
|
|
630
637
|
logger.debug("Using adapter for createOrUpdateCollections (unified path)", {
|