appwrite-utils-cli 1.5.2 → 1.6.1
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 +199 -0
- package/README.md +251 -29
- package/dist/adapters/AdapterFactory.d.ts +10 -3
- package/dist/adapters/AdapterFactory.js +213 -17
- package/dist/adapters/TablesDBAdapter.js +60 -17
- package/dist/backups/operations/bucketBackup.d.ts +19 -0
- package/dist/backups/operations/bucketBackup.js +197 -0
- package/dist/backups/operations/collectionBackup.d.ts +30 -0
- package/dist/backups/operations/collectionBackup.js +201 -0
- package/dist/backups/operations/comprehensiveBackup.d.ts +25 -0
- package/dist/backups/operations/comprehensiveBackup.js +238 -0
- package/dist/backups/schemas/bucketManifest.d.ts +93 -0
- package/dist/backups/schemas/bucketManifest.js +33 -0
- package/dist/backups/schemas/comprehensiveManifest.d.ts +108 -0
- package/dist/backups/schemas/comprehensiveManifest.js +32 -0
- package/dist/backups/tracking/centralizedTracking.d.ts +34 -0
- package/dist/backups/tracking/centralizedTracking.js +274 -0
- package/dist/cli/commands/configCommands.d.ts +8 -0
- package/dist/cli/commands/configCommands.js +160 -0
- package/dist/cli/commands/databaseCommands.d.ts +13 -0
- package/dist/cli/commands/databaseCommands.js +479 -0
- package/dist/cli/commands/functionCommands.d.ts +7 -0
- package/dist/cli/commands/functionCommands.js +289 -0
- package/dist/cli/commands/schemaCommands.d.ts +7 -0
- package/dist/cli/commands/schemaCommands.js +134 -0
- package/dist/cli/commands/transferCommands.d.ts +5 -0
- package/dist/cli/commands/transferCommands.js +384 -0
- package/dist/collections/attributes.d.ts +5 -4
- package/dist/collections/attributes.js +539 -246
- package/dist/collections/indexes.js +39 -37
- package/dist/collections/methods.d.ts +2 -16
- package/dist/collections/methods.js +90 -538
- package/dist/collections/transferOperations.d.ts +7 -0
- package/dist/collections/transferOperations.js +331 -0
- package/dist/collections/wipeOperations.d.ts +16 -0
- package/dist/collections/wipeOperations.js +328 -0
- package/dist/config/configMigration.d.ts +87 -0
- package/dist/config/configMigration.js +390 -0
- package/dist/config/configValidation.d.ts +66 -0
- package/dist/config/configValidation.js +358 -0
- package/dist/config/yamlConfig.d.ts +455 -1
- package/dist/config/yamlConfig.js +145 -52
- package/dist/databases/methods.js +3 -2
- package/dist/databases/setup.d.ts +1 -2
- package/dist/databases/setup.js +9 -87
- package/dist/examples/yamlTerminologyExample.d.ts +42 -0
- package/dist/examples/yamlTerminologyExample.js +269 -0
- package/dist/functions/deployments.js +11 -10
- package/dist/functions/methods.d.ts +1 -1
- package/dist/functions/methods.js +5 -4
- package/dist/init.js +9 -9
- package/dist/interactiveCLI.d.ts +8 -17
- package/dist/interactiveCLI.js +209 -1172
- package/dist/main.js +364 -21
- package/dist/migrations/afterImportActions.js +22 -30
- package/dist/migrations/appwriteToX.js +71 -25
- package/dist/migrations/dataLoader.js +35 -26
- package/dist/migrations/importController.js +29 -30
- package/dist/migrations/relationships.js +13 -12
- package/dist/migrations/services/ImportOrchestrator.js +16 -19
- package/dist/migrations/transfer.js +46 -46
- package/dist/migrations/yaml/YamlImportConfigLoader.d.ts +3 -1
- package/dist/migrations/yaml/YamlImportConfigLoader.js +6 -3
- package/dist/migrations/yaml/YamlImportIntegration.d.ts +9 -3
- package/dist/migrations/yaml/YamlImportIntegration.js +22 -11
- package/dist/migrations/yaml/generateImportSchemas.d.ts +14 -1
- package/dist/migrations/yaml/generateImportSchemas.js +736 -7
- package/dist/schemas/authUser.d.ts +1 -1
- package/dist/setupController.js +3 -2
- package/dist/shared/backupMetadataSchema.d.ts +94 -0
- package/dist/shared/backupMetadataSchema.js +38 -0
- package/dist/shared/backupTracking.d.ts +18 -0
- package/dist/shared/backupTracking.js +176 -0
- package/dist/shared/confirmationDialogs.js +15 -15
- package/dist/shared/errorUtils.d.ts +54 -0
- package/dist/shared/errorUtils.js +95 -0
- package/dist/shared/functionManager.js +20 -19
- package/dist/shared/indexManager.js +12 -11
- package/dist/shared/jsonSchemaGenerator.js +10 -26
- package/dist/shared/logging.d.ts +51 -0
- package/dist/shared/logging.js +70 -0
- package/dist/shared/messageFormatter.d.ts +2 -0
- package/dist/shared/messageFormatter.js +10 -0
- package/dist/shared/migrationHelpers.d.ts +6 -16
- package/dist/shared/migrationHelpers.js +24 -21
- package/dist/shared/operationLogger.d.ts +8 -1
- package/dist/shared/operationLogger.js +11 -24
- package/dist/shared/operationQueue.d.ts +28 -1
- package/dist/shared/operationQueue.js +268 -66
- package/dist/shared/operationsTable.d.ts +26 -0
- package/dist/shared/operationsTable.js +286 -0
- package/dist/shared/operationsTableSchema.d.ts +48 -0
- package/dist/shared/operationsTableSchema.js +35 -0
- package/dist/shared/relationshipExtractor.d.ts +56 -0
- package/dist/shared/relationshipExtractor.js +138 -0
- package/dist/shared/schemaGenerator.d.ts +19 -1
- package/dist/shared/schemaGenerator.js +56 -75
- package/dist/storage/backupCompression.d.ts +20 -0
- package/dist/storage/backupCompression.js +67 -0
- package/dist/storage/methods.d.ts +16 -2
- package/dist/storage/methods.js +98 -14
- package/dist/users/methods.js +9 -8
- package/dist/utils/configDiscovery.d.ts +78 -0
- package/dist/utils/configDiscovery.js +430 -0
- package/dist/utils/directoryUtils.d.ts +22 -0
- package/dist/utils/directoryUtils.js +59 -0
- package/dist/utils/getClientFromConfig.d.ts +17 -8
- package/dist/utils/getClientFromConfig.js +162 -17
- package/dist/utils/helperFunctions.d.ts +16 -2
- package/dist/utils/helperFunctions.js +19 -5
- package/dist/utils/loadConfigs.d.ts +34 -9
- package/dist/utils/loadConfigs.js +236 -316
- package/dist/utils/pathResolvers.d.ts +53 -0
- package/dist/utils/pathResolvers.js +72 -0
- package/dist/utils/projectConfig.d.ts +119 -0
- package/dist/utils/projectConfig.js +171 -0
- package/dist/utils/retryFailedPromises.js +4 -2
- package/dist/utils/sessionAuth.d.ts +48 -0
- package/dist/utils/sessionAuth.js +164 -0
- package/dist/utils/sessionPreservationExample.d.ts +1666 -0
- package/dist/utils/sessionPreservationExample.js +101 -0
- package/dist/utils/setupFiles.js +301 -41
- package/dist/utils/typeGuards.d.ts +35 -0
- package/dist/utils/typeGuards.js +57 -0
- package/dist/utils/versionDetection.js +145 -9
- package/dist/utils/yamlConverter.d.ts +53 -3
- package/dist/utils/yamlConverter.js +232 -13
- package/dist/utils/yamlLoader.d.ts +70 -0
- package/dist/utils/yamlLoader.js +263 -0
- package/dist/utilsController.d.ts +36 -3
- package/dist/utilsController.js +186 -56
- package/package.json +12 -2
- package/src/adapters/AdapterFactory.ts +263 -35
- package/src/adapters/TablesDBAdapter.ts +225 -36
- package/src/backups/operations/bucketBackup.ts +277 -0
- package/src/backups/operations/collectionBackup.ts +310 -0
- package/src/backups/operations/comprehensiveBackup.ts +342 -0
- package/src/backups/schemas/bucketManifest.ts +78 -0
- package/src/backups/schemas/comprehensiveManifest.ts +76 -0
- package/src/backups/tracking/centralizedTracking.ts +352 -0
- package/src/cli/commands/configCommands.ts +194 -0
- package/src/cli/commands/databaseCommands.ts +635 -0
- package/src/cli/commands/functionCommands.ts +379 -0
- package/src/cli/commands/schemaCommands.ts +163 -0
- package/src/cli/commands/transferCommands.ts +457 -0
- package/src/collections/attributes.ts +900 -621
- package/src/collections/attributes.ts.backup +1555 -0
- package/src/collections/indexes.ts +116 -114
- package/src/collections/methods.ts +295 -968
- package/src/collections/transferOperations.ts +516 -0
- package/src/collections/wipeOperations.ts +501 -0
- package/src/config/README.md +274 -0
- package/src/config/configMigration.ts +575 -0
- package/src/config/configValidation.ts +445 -0
- package/src/config/yamlConfig.ts +168 -55
- package/src/databases/methods.ts +3 -2
- package/src/databases/setup.ts +11 -138
- package/src/examples/yamlTerminologyExample.ts +341 -0
- package/src/functions/deployments.ts +14 -12
- package/src/functions/methods.ts +11 -11
- package/src/functions/templates/hono-typescript/README.md +286 -0
- package/src/functions/templates/hono-typescript/package.json +26 -0
- package/src/functions/templates/hono-typescript/src/adapters/request.ts +74 -0
- package/src/functions/templates/hono-typescript/src/adapters/response.ts +106 -0
- package/src/functions/templates/hono-typescript/src/app.ts +180 -0
- package/src/functions/templates/hono-typescript/src/context.ts +103 -0
- package/src/functions/templates/hono-typescript/src/index.ts +54 -0
- package/src/functions/templates/hono-typescript/src/middleware/appwrite.ts +119 -0
- package/src/functions/templates/hono-typescript/tsconfig.json +20 -0
- package/src/functions/templates/typescript-node/package.json +2 -1
- package/src/functions/templates/typescript-node/src/context.ts +103 -0
- package/src/functions/templates/typescript-node/src/index.ts +18 -12
- package/src/functions/templates/uv/pyproject.toml +1 -0
- package/src/functions/templates/uv/src/context.py +125 -0
- package/src/functions/templates/uv/src/index.py +35 -5
- package/src/init.ts +9 -11
- package/src/interactiveCLI.ts +274 -1563
- package/src/main.ts +418 -24
- package/src/migrations/afterImportActions.ts +71 -44
- package/src/migrations/appwriteToX.ts +100 -34
- package/src/migrations/dataLoader.ts +48 -34
- package/src/migrations/importController.ts +44 -39
- package/src/migrations/relationships.ts +28 -18
- package/src/migrations/services/ImportOrchestrator.ts +24 -27
- package/src/migrations/transfer.ts +159 -121
- package/src/migrations/yaml/YamlImportConfigLoader.ts +11 -4
- package/src/migrations/yaml/YamlImportIntegration.ts +47 -20
- package/src/migrations/yaml/generateImportSchemas.ts +751 -12
- package/src/setupController.ts +3 -2
- package/src/shared/backupMetadataSchema.ts +93 -0
- package/src/shared/backupTracking.ts +211 -0
- package/src/shared/confirmationDialogs.ts +19 -19
- package/src/shared/errorUtils.ts +110 -0
- package/src/shared/functionManager.ts +21 -20
- package/src/shared/indexManager.ts +12 -11
- package/src/shared/jsonSchemaGenerator.ts +38 -52
- package/src/shared/logging.ts +75 -0
- package/src/shared/messageFormatter.ts +14 -1
- package/src/shared/migrationHelpers.ts +45 -38
- package/src/shared/operationLogger.ts +11 -36
- package/src/shared/operationQueue.ts +322 -93
- package/src/shared/operationsTable.ts +338 -0
- package/src/shared/operationsTableSchema.ts +60 -0
- package/src/shared/relationshipExtractor.ts +214 -0
- package/src/shared/schemaGenerator.ts +179 -219
- package/src/storage/backupCompression.ts +88 -0
- package/src/storage/methods.ts +131 -34
- package/src/users/methods.ts +11 -9
- package/src/utils/configDiscovery.ts +502 -0
- package/src/utils/directoryUtils.ts +61 -0
- package/src/utils/getClientFromConfig.ts +205 -22
- package/src/utils/helperFunctions.ts +23 -5
- package/src/utils/loadConfigs.ts +313 -345
- package/src/utils/pathResolvers.ts +81 -0
- package/src/utils/projectConfig.ts +299 -0
- package/src/utils/retryFailedPromises.ts +4 -2
- package/src/utils/sessionAuth.ts +230 -0
- package/src/utils/setupFiles.ts +322 -54
- package/src/utils/typeGuards.ts +65 -0
- package/src/utils/versionDetection.ts +218 -64
- package/src/utils/yamlConverter.ts +296 -13
- package/src/utils/yamlLoader.ts +364 -0
- package/src/utilsController.ts +314 -110
- package/tests/README.md +497 -0
- package/tests/adapters/AdapterFactory.test.ts +277 -0
- package/tests/integration/syncOperations.test.ts +463 -0
- package/tests/jest.config.js +25 -0
- package/tests/migration/configMigration.test.ts +546 -0
- package/tests/setup.ts +62 -0
- package/tests/testUtils.ts +340 -0
- package/tests/utils/loadConfigs.test.ts +350 -0
- package/tests/validation/configValidation.test.ts +412 -0
- package/src/utils/schemaStrings.ts +0 -517
@@ -1,8 +1,335 @@
|
|
1
1
|
import { Query } from "node-appwrite";
|
2
2
|
import { attributeSchema, parseAttribute, } from "appwrite-utils";
|
3
|
-
import { nameToIdMapping, enqueueOperation } from "../shared/operationQueue.js";
|
4
|
-
import { delay, tryAwaitWithRetry } from "../utils/helperFunctions.js";
|
3
|
+
import { nameToIdMapping, enqueueOperation, markAttributeProcessed, isAttributeProcessed } from "../shared/operationQueue.js";
|
4
|
+
import { delay, tryAwaitWithRetry, calculateExponentialBackoff } from "../utils/helperFunctions.js";
|
5
5
|
import chalk from "chalk";
|
6
|
+
import { logger } from "../shared/logging.js";
|
7
|
+
import { MessageFormatter } from "../shared/messageFormatter.js";
|
8
|
+
import { isDatabaseAdapter } from "../utils/typeGuards.js";
|
9
|
+
// Threshold for treating min/max values as undefined (10 billion)
|
10
|
+
const MIN_MAX_THRESHOLD = 10_000_000_000;
|
11
|
+
// Extreme values that Appwrite may return, which should be treated as undefined
|
12
|
+
const EXTREME_MIN_INTEGER = -9223372036854776000;
|
13
|
+
const EXTREME_MAX_INTEGER = 9223372036854776000;
|
14
|
+
const EXTREME_MIN_FLOAT = -1.7976931348623157e+308;
|
15
|
+
const EXTREME_MAX_FLOAT = 1.7976931348623157e+308;
|
16
|
+
/**
|
17
|
+
* Type guard to check if an attribute has min/max properties
|
18
|
+
*/
|
19
|
+
const hasMinMaxProperties = (attribute) => {
|
20
|
+
return attribute.type === 'integer' || attribute.type === 'double' || attribute.type === 'float';
|
21
|
+
};
|
22
|
+
/**
|
23
|
+
* Normalizes min/max values for integer and float attributes
|
24
|
+
* Sets values to undefined if they exceed the threshold or are extreme values from database
|
25
|
+
*/
|
26
|
+
const normalizeMinMaxValues = (attribute) => {
|
27
|
+
if (!hasMinMaxProperties(attribute)) {
|
28
|
+
logger.debug(`Attribute '${attribute.key}' does not have min/max properties`, {
|
29
|
+
type: attribute.type,
|
30
|
+
operation: 'normalizeMinMaxValues'
|
31
|
+
});
|
32
|
+
return {};
|
33
|
+
}
|
34
|
+
const { type, min, max } = attribute;
|
35
|
+
let normalizedMin = min;
|
36
|
+
let normalizedMax = max;
|
37
|
+
logger.debug(`Normalizing min/max values for attribute '${attribute.key}'`, {
|
38
|
+
type,
|
39
|
+
originalMin: min,
|
40
|
+
originalMax: max,
|
41
|
+
operation: 'normalizeMinMaxValues'
|
42
|
+
});
|
43
|
+
// Handle min value
|
44
|
+
if (normalizedMin !== undefined && normalizedMin !== null) {
|
45
|
+
const minValue = Number(normalizedMin);
|
46
|
+
const originalMin = normalizedMin;
|
47
|
+
// Check if it exceeds threshold or is an extreme database value
|
48
|
+
if (type === 'integer') {
|
49
|
+
if (Math.abs(minValue) >= MIN_MAX_THRESHOLD || minValue === EXTREME_MIN_INTEGER) {
|
50
|
+
logger.debug(`Min value normalized to undefined for attribute '${attribute.key}'`, {
|
51
|
+
type,
|
52
|
+
originalValue: originalMin,
|
53
|
+
numericValue: minValue,
|
54
|
+
reason: Math.abs(minValue) >= MIN_MAX_THRESHOLD ? 'exceeds_threshold' : 'extreme_database_value',
|
55
|
+
threshold: MIN_MAX_THRESHOLD,
|
56
|
+
extremeValue: EXTREME_MIN_INTEGER,
|
57
|
+
operation: 'normalizeMinMaxValues'
|
58
|
+
});
|
59
|
+
normalizedMin = undefined;
|
60
|
+
}
|
61
|
+
}
|
62
|
+
else { // float/double
|
63
|
+
if (Math.abs(minValue) >= MIN_MAX_THRESHOLD || minValue === EXTREME_MIN_FLOAT) {
|
64
|
+
logger.debug(`Min value normalized to undefined for attribute '${attribute.key}'`, {
|
65
|
+
type,
|
66
|
+
originalValue: originalMin,
|
67
|
+
numericValue: minValue,
|
68
|
+
reason: Math.abs(minValue) >= MIN_MAX_THRESHOLD ? 'exceeds_threshold' : 'extreme_database_value',
|
69
|
+
threshold: MIN_MAX_THRESHOLD,
|
70
|
+
extremeValue: EXTREME_MIN_FLOAT,
|
71
|
+
operation: 'normalizeMinMaxValues'
|
72
|
+
});
|
73
|
+
normalizedMin = undefined;
|
74
|
+
}
|
75
|
+
}
|
76
|
+
}
|
77
|
+
// Handle max value
|
78
|
+
if (normalizedMax !== undefined && normalizedMax !== null) {
|
79
|
+
const maxValue = Number(normalizedMax);
|
80
|
+
const originalMax = normalizedMax;
|
81
|
+
// Check if it exceeds threshold or is an extreme database value
|
82
|
+
if (type === 'integer') {
|
83
|
+
if (Math.abs(maxValue) >= MIN_MAX_THRESHOLD || maxValue === EXTREME_MAX_INTEGER) {
|
84
|
+
logger.debug(`Max value normalized to undefined for attribute '${attribute.key}'`, {
|
85
|
+
type,
|
86
|
+
originalValue: originalMax,
|
87
|
+
numericValue: maxValue,
|
88
|
+
reason: Math.abs(maxValue) >= MIN_MAX_THRESHOLD ? 'exceeds_threshold' : 'extreme_database_value',
|
89
|
+
threshold: MIN_MAX_THRESHOLD,
|
90
|
+
extremeValue: EXTREME_MAX_INTEGER,
|
91
|
+
operation: 'normalizeMinMaxValues'
|
92
|
+
});
|
93
|
+
normalizedMax = undefined;
|
94
|
+
}
|
95
|
+
}
|
96
|
+
else { // float/double
|
97
|
+
if (Math.abs(maxValue) >= MIN_MAX_THRESHOLD || maxValue === EXTREME_MAX_FLOAT) {
|
98
|
+
logger.debug(`Max value normalized to undefined for attribute '${attribute.key}'`, {
|
99
|
+
type,
|
100
|
+
originalValue: originalMax,
|
101
|
+
numericValue: maxValue,
|
102
|
+
reason: Math.abs(maxValue) >= MIN_MAX_THRESHOLD ? 'exceeds_threshold' : 'extreme_database_value',
|
103
|
+
threshold: MIN_MAX_THRESHOLD,
|
104
|
+
extremeValue: EXTREME_MAX_FLOAT,
|
105
|
+
operation: 'normalizeMinMaxValues'
|
106
|
+
});
|
107
|
+
normalizedMax = undefined;
|
108
|
+
}
|
109
|
+
}
|
110
|
+
}
|
111
|
+
const result = { min: normalizedMin, max: normalizedMax };
|
112
|
+
logger.debug(`Min/max normalization complete for attribute '${attribute.key}'`, {
|
113
|
+
type,
|
114
|
+
result,
|
115
|
+
operation: 'normalizeMinMaxValues'
|
116
|
+
});
|
117
|
+
return result;
|
118
|
+
};
|
119
|
+
/**
|
120
|
+
* Normalizes an attribute for comparison by handling extreme database values
|
121
|
+
* This is used when comparing database attributes with config attributes
|
122
|
+
*/
|
123
|
+
const normalizeAttributeForComparison = (attribute) => {
|
124
|
+
if (!hasMinMaxProperties(attribute)) {
|
125
|
+
return attribute;
|
126
|
+
}
|
127
|
+
const { min, max } = normalizeMinMaxValues(attribute);
|
128
|
+
return { ...attribute, min, max };
|
129
|
+
};
|
130
|
+
/**
|
131
|
+
* Helper function to create an attribute using either the adapter or legacy API
|
132
|
+
*/
|
133
|
+
const createAttributeViaAdapter = async (db, dbId, collectionId, attribute) => {
|
134
|
+
const startTime = Date.now();
|
135
|
+
const adapterType = isDatabaseAdapter(db) ? 'adapter' : 'legacy';
|
136
|
+
logger.info(`Creating attribute '${attribute.key}' via ${adapterType}`, {
|
137
|
+
type: attribute.type,
|
138
|
+
dbId,
|
139
|
+
collectionId,
|
140
|
+
adapterType,
|
141
|
+
operation: 'createAttributeViaAdapter'
|
142
|
+
});
|
143
|
+
if (isDatabaseAdapter(db)) {
|
144
|
+
// Use the adapter's unified createAttribute method
|
145
|
+
const params = {
|
146
|
+
databaseId: dbId,
|
147
|
+
tableId: collectionId,
|
148
|
+
key: attribute.key,
|
149
|
+
type: attribute.type,
|
150
|
+
required: attribute.required || false,
|
151
|
+
array: attribute.array || false,
|
152
|
+
...(attribute.size && { size: attribute.size }),
|
153
|
+
...(attribute.xdefault !== undefined && !attribute.required && { default: attribute.xdefault }),
|
154
|
+
...(attribute.encrypted && { encrypt: attribute.encrypted }),
|
155
|
+
...(attribute.min !== undefined && { min: attribute.min }),
|
156
|
+
...(attribute.max !== undefined && { max: attribute.max }),
|
157
|
+
...(attribute.elements && { elements: attribute.elements }),
|
158
|
+
...(attribute.relatedCollection && { relatedCollection: attribute.relatedCollection }),
|
159
|
+
...(attribute.relationType && { relationType: attribute.relationType }),
|
160
|
+
...(attribute.twoWay !== undefined && { twoWay: attribute.twoWay }),
|
161
|
+
...(attribute.onDelete && { onDelete: attribute.onDelete }),
|
162
|
+
...(attribute.twoWayKey && { twoWayKey: attribute.twoWayKey })
|
163
|
+
};
|
164
|
+
logger.debug(`Adapter create parameters for '${attribute.key}'`, {
|
165
|
+
params,
|
166
|
+
operation: 'createAttributeViaAdapter'
|
167
|
+
});
|
168
|
+
await db.createAttribute(params);
|
169
|
+
const duration = Date.now() - startTime;
|
170
|
+
logger.info(`Successfully created attribute '${attribute.key}' via adapter`, {
|
171
|
+
duration,
|
172
|
+
operation: 'createAttributeViaAdapter'
|
173
|
+
});
|
174
|
+
}
|
175
|
+
else {
|
176
|
+
// Use legacy type-specific methods
|
177
|
+
logger.debug(`Using legacy creation for attribute '${attribute.key}'`, {
|
178
|
+
operation: 'createAttributeViaAdapter'
|
179
|
+
});
|
180
|
+
await createLegacyAttribute(db, dbId, collectionId, attribute);
|
181
|
+
const duration = Date.now() - startTime;
|
182
|
+
logger.info(`Successfully created attribute '${attribute.key}' via legacy`, {
|
183
|
+
duration,
|
184
|
+
operation: 'createAttributeViaAdapter'
|
185
|
+
});
|
186
|
+
}
|
187
|
+
};
|
188
|
+
/**
|
189
|
+
* Helper function to update an attribute using either the adapter or legacy API
|
190
|
+
*/
|
191
|
+
const updateAttributeViaAdapter = async (db, dbId, collectionId, attribute) => {
|
192
|
+
if (isDatabaseAdapter(db)) {
|
193
|
+
// Use the adapter's unified updateAttribute method
|
194
|
+
const params = {
|
195
|
+
databaseId: dbId,
|
196
|
+
tableId: collectionId,
|
197
|
+
key: attribute.key,
|
198
|
+
required: attribute.required || false,
|
199
|
+
...(attribute.xdefault !== undefined && !attribute.required && { default: attribute.xdefault })
|
200
|
+
};
|
201
|
+
await db.updateAttribute(params);
|
202
|
+
}
|
203
|
+
else {
|
204
|
+
// Use legacy type-specific methods
|
205
|
+
await updateLegacyAttribute(db, dbId, collectionId, attribute);
|
206
|
+
}
|
207
|
+
};
|
208
|
+
/**
|
209
|
+
* Legacy attribute creation using type-specific methods
|
210
|
+
*/
|
211
|
+
const createLegacyAttribute = async (db, dbId, collectionId, attribute) => {
|
212
|
+
const startTime = Date.now();
|
213
|
+
const { min: normalizedMin, max: normalizedMax } = normalizeMinMaxValues(attribute);
|
214
|
+
logger.info(`Creating legacy attribute '${attribute.key}'`, {
|
215
|
+
type: attribute.type,
|
216
|
+
dbId,
|
217
|
+
collectionId,
|
218
|
+
normalizedMin,
|
219
|
+
normalizedMax,
|
220
|
+
operation: 'createLegacyAttribute'
|
221
|
+
});
|
222
|
+
switch (attribute.type) {
|
223
|
+
case "string":
|
224
|
+
const stringParams = {
|
225
|
+
size: attribute.size || 255,
|
226
|
+
required: attribute.required || false,
|
227
|
+
defaultValue: attribute.xdefault !== undefined && !attribute.required ? attribute.xdefault : undefined,
|
228
|
+
array: attribute.array || false,
|
229
|
+
encrypted: attribute.encrypted
|
230
|
+
};
|
231
|
+
logger.debug(`Creating string attribute '${attribute.key}'`, {
|
232
|
+
...stringParams,
|
233
|
+
operation: 'createLegacyAttribute'
|
234
|
+
});
|
235
|
+
await db.createStringAttribute(dbId, collectionId, attribute.key, stringParams.size, stringParams.required, stringParams.defaultValue, stringParams.array, stringParams.encrypted);
|
236
|
+
break;
|
237
|
+
case "integer":
|
238
|
+
const integerParams = {
|
239
|
+
required: attribute.required || false,
|
240
|
+
min: normalizedMin !== undefined ? parseInt(String(normalizedMin)) : undefined,
|
241
|
+
max: normalizedMax !== undefined ? parseInt(String(normalizedMax)) : undefined,
|
242
|
+
defaultValue: attribute.xdefault !== undefined && !attribute.required ? attribute.xdefault : undefined,
|
243
|
+
array: attribute.array || false
|
244
|
+
};
|
245
|
+
logger.debug(`Creating integer attribute '${attribute.key}'`, {
|
246
|
+
...integerParams,
|
247
|
+
operation: 'createLegacyAttribute'
|
248
|
+
});
|
249
|
+
await db.createIntegerAttribute(dbId, collectionId, attribute.key, integerParams.required, integerParams.min, integerParams.max, integerParams.defaultValue, integerParams.array);
|
250
|
+
break;
|
251
|
+
case "double":
|
252
|
+
case "float":
|
253
|
+
await db.createFloatAttribute(dbId, collectionId, attribute.key, attribute.required || false, normalizedMin !== undefined ? Number(normalizedMin) : undefined, normalizedMax !== undefined ? Number(normalizedMax) : undefined, attribute.xdefault !== undefined && !attribute.required ? attribute.xdefault : undefined, attribute.array || false);
|
254
|
+
break;
|
255
|
+
case "boolean":
|
256
|
+
await db.createBooleanAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required ? attribute.xdefault : undefined, attribute.array || false);
|
257
|
+
break;
|
258
|
+
case "datetime":
|
259
|
+
await db.createDatetimeAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required ? attribute.xdefault : undefined, attribute.array || false);
|
260
|
+
break;
|
261
|
+
case "email":
|
262
|
+
await db.createEmailAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required ? attribute.xdefault : undefined, attribute.array || false);
|
263
|
+
break;
|
264
|
+
case "ip":
|
265
|
+
await db.createIpAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required ? attribute.xdefault : undefined, attribute.array || false);
|
266
|
+
break;
|
267
|
+
case "url":
|
268
|
+
await db.createUrlAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required ? attribute.xdefault : undefined, attribute.array || false);
|
269
|
+
break;
|
270
|
+
case "enum":
|
271
|
+
await db.createEnumAttribute(dbId, collectionId, attribute.key, attribute.elements || [], attribute.required || false, attribute.xdefault !== undefined && !attribute.required ? attribute.xdefault : undefined, attribute.array || false);
|
272
|
+
break;
|
273
|
+
case "relationship":
|
274
|
+
await db.createRelationshipAttribute(dbId, collectionId, attribute.relatedCollection, attribute.relationType, attribute.twoWay, attribute.key, attribute.twoWayKey, attribute.onDelete);
|
275
|
+
break;
|
276
|
+
default:
|
277
|
+
const error = new Error(`Unsupported attribute type: ${attribute.type}`);
|
278
|
+
logger.error(`Unsupported attribute type for '${attribute.key}'`, {
|
279
|
+
type: attribute.type,
|
280
|
+
supportedTypes: ['string', 'integer', 'double', 'float', 'boolean', 'datetime', 'email', 'ip', 'url', 'enum', 'relationship'],
|
281
|
+
operation: 'createLegacyAttribute'
|
282
|
+
});
|
283
|
+
throw error;
|
284
|
+
}
|
285
|
+
const duration = Date.now() - startTime;
|
286
|
+
logger.info(`Successfully created legacy attribute '${attribute.key}'`, {
|
287
|
+
type: attribute.type,
|
288
|
+
duration,
|
289
|
+
operation: 'createLegacyAttribute'
|
290
|
+
});
|
291
|
+
};
|
292
|
+
/**
|
293
|
+
* Legacy attribute update using type-specific methods
|
294
|
+
*/
|
295
|
+
const updateLegacyAttribute = async (db, dbId, collectionId, attribute) => {
|
296
|
+
const { min: normalizedMin, max: normalizedMax } = normalizeMinMaxValues(attribute);
|
297
|
+
switch (attribute.type) {
|
298
|
+
case "string":
|
299
|
+
await db.updateStringAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required ? attribute.xdefault : undefined, attribute.size);
|
300
|
+
break;
|
301
|
+
case "integer":
|
302
|
+
await db.updateIntegerAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required ? attribute.xdefault : undefined, normalizedMin !== undefined ? parseInt(String(normalizedMin)) : undefined, normalizedMax !== undefined ? parseInt(String(normalizedMax)) : undefined);
|
303
|
+
break;
|
304
|
+
case "double":
|
305
|
+
case "float":
|
306
|
+
await db.updateFloatAttribute(dbId, collectionId, attribute.key, attribute.required || false, normalizedMin !== undefined ? Number(normalizedMin) : undefined, normalizedMax !== undefined ? Number(normalizedMax) : undefined, attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
|
307
|
+
break;
|
308
|
+
case "boolean":
|
309
|
+
await db.updateBooleanAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
|
310
|
+
break;
|
311
|
+
case "datetime":
|
312
|
+
await db.updateDatetimeAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
|
313
|
+
break;
|
314
|
+
case "email":
|
315
|
+
await db.updateEmailAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
|
316
|
+
break;
|
317
|
+
case "ip":
|
318
|
+
await db.updateIpAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
|
319
|
+
break;
|
320
|
+
case "url":
|
321
|
+
await db.updateUrlAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
|
322
|
+
break;
|
323
|
+
case "enum":
|
324
|
+
await db.updateEnumAttribute(dbId, collectionId, attribute.key, attribute.elements || [], attribute.required || false, attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined);
|
325
|
+
break;
|
326
|
+
case "relationship":
|
327
|
+
await db.updateRelationshipAttribute(dbId, collectionId, attribute.key, attribute.onDelete);
|
328
|
+
break;
|
329
|
+
default:
|
330
|
+
throw new Error(`Unsupported attribute type for update: ${attribute.type}`);
|
331
|
+
}
|
332
|
+
};
|
6
333
|
/**
|
7
334
|
* Wait for attribute to become available, with retry logic for stuck attributes and exponential backoff
|
8
335
|
*/
|
@@ -10,56 +337,90 @@ const waitForAttributeAvailable = async (db, dbId, collectionId, attributeKey, m
|
|
10
337
|
retryCount = 0, maxRetries = 5) => {
|
11
338
|
const startTime = Date.now();
|
12
339
|
let checkInterval = 2000; // Start with 2 seconds
|
340
|
+
logger.info(`Waiting for attribute '${attributeKey}' to become available`, {
|
341
|
+
dbId,
|
342
|
+
collectionId,
|
343
|
+
maxWaitTime,
|
344
|
+
retryCount,
|
345
|
+
maxRetries,
|
346
|
+
operation: 'waitForAttributeAvailable'
|
347
|
+
});
|
13
348
|
// Calculate exponential backoff: 2s, 4s, 8s, 16s, 30s (capped at 30s)
|
14
349
|
if (retryCount > 0) {
|
15
|
-
const exponentialDelay =
|
16
|
-
|
350
|
+
const exponentialDelay = calculateExponentialBackoff(retryCount);
|
351
|
+
MessageFormatter.info(chalk.blue(`Waiting for attribute '${attributeKey}' to become available (retry ${retryCount}, backoff: ${exponentialDelay}ms)...`));
|
17
352
|
await delay(exponentialDelay);
|
18
353
|
}
|
19
354
|
else {
|
20
|
-
|
355
|
+
MessageFormatter.info(chalk.blue(`Waiting for attribute '${attributeKey}' to become available...`));
|
21
356
|
}
|
22
357
|
while (Date.now() - startTime < maxWaitTime) {
|
23
358
|
try {
|
24
|
-
const collection =
|
359
|
+
const collection = isDatabaseAdapter(db)
|
360
|
+
? (await db.getTable({ databaseId: dbId, tableId: collectionId })).data
|
361
|
+
: await db.getCollection(dbId, collectionId);
|
25
362
|
const attribute = collection.attributes.find((attr) => attr.key === attributeKey);
|
26
363
|
if (!attribute) {
|
27
|
-
|
364
|
+
MessageFormatter.error(`Attribute '${attributeKey}' not found`);
|
28
365
|
return false;
|
29
366
|
}
|
30
|
-
|
367
|
+
MessageFormatter.info(chalk.gray(`Attribute '${attributeKey}' status: ${attribute.status}`));
|
368
|
+
const statusInfo = {
|
369
|
+
attributeKey,
|
370
|
+
status: attribute.status,
|
371
|
+
error: attribute.error,
|
372
|
+
dbId,
|
373
|
+
collectionId,
|
374
|
+
waitTime: Date.now() - startTime,
|
375
|
+
operation: 'waitForAttributeAvailable'
|
376
|
+
};
|
31
377
|
switch (attribute.status) {
|
32
378
|
case "available":
|
33
|
-
|
379
|
+
MessageFormatter.info(chalk.green(`✅ Attribute '${attributeKey}' is now available`));
|
380
|
+
logger.info(`Attribute '${attributeKey}' became available`, statusInfo);
|
34
381
|
return true;
|
35
382
|
case "failed":
|
36
|
-
|
383
|
+
MessageFormatter.info(chalk.red(`❌ Attribute '${attributeKey}' failed: ${attribute.error}`));
|
384
|
+
logger.error(`Attribute '${attributeKey}' failed`, statusInfo);
|
37
385
|
return false;
|
38
386
|
case "stuck":
|
39
|
-
|
387
|
+
MessageFormatter.info(chalk.yellow(`⚠️ Attribute '${attributeKey}' is stuck, will retry...`));
|
388
|
+
logger.warn(`Attribute '${attributeKey}' is stuck`, statusInfo);
|
40
389
|
return false;
|
41
390
|
case "processing":
|
42
391
|
// Continue waiting
|
392
|
+
logger.debug(`Attribute '${attributeKey}' still processing`, statusInfo);
|
43
393
|
break;
|
44
394
|
case "deleting":
|
45
|
-
|
395
|
+
MessageFormatter.info(chalk.yellow(`Attribute '${attributeKey}' is being deleted`));
|
396
|
+
logger.warn(`Attribute '${attributeKey}' is being deleted`, statusInfo);
|
46
397
|
break;
|
47
398
|
default:
|
48
|
-
|
399
|
+
MessageFormatter.info(chalk.yellow(`Unknown status '${attribute.status}' for attribute '${attributeKey}'`));
|
400
|
+
logger.warn(`Unknown status for attribute '${attributeKey}'`, statusInfo);
|
49
401
|
break;
|
50
402
|
}
|
51
403
|
await delay(checkInterval);
|
52
404
|
}
|
53
405
|
catch (error) {
|
54
|
-
|
406
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
407
|
+
MessageFormatter.error(`Error checking attribute status: ${errorMessage}`);
|
408
|
+
logger.error('Error checking attribute status', {
|
409
|
+
attributeKey,
|
410
|
+
dbId,
|
411
|
+
collectionId,
|
412
|
+
error: errorMessage,
|
413
|
+
waitTime: Date.now() - startTime,
|
414
|
+
operation: 'waitForAttributeAvailable'
|
415
|
+
});
|
55
416
|
return false;
|
56
417
|
}
|
57
418
|
}
|
58
419
|
// Timeout reached
|
59
|
-
|
420
|
+
MessageFormatter.info(chalk.yellow(`⏰ Timeout waiting for attribute '${attributeKey}' (${maxWaitTime}ms)`));
|
60
421
|
// If we have retries left and this isn't the last retry, try recreating
|
61
422
|
if (retryCount < maxRetries) {
|
62
|
-
|
423
|
+
MessageFormatter.info(chalk.yellow(`🔄 Retrying attribute creation (attempt ${retryCount + 1}/${maxRetries})`));
|
63
424
|
return false; // Signal that we need to retry
|
64
425
|
}
|
65
426
|
return false;
|
@@ -68,7 +429,7 @@ retryCount = 0, maxRetries = 5) => {
|
|
68
429
|
* Wait for all attributes in a collection to become available
|
69
430
|
*/
|
70
431
|
const waitForAllAttributesAvailable = async (db, dbId, collectionId, attributeKeys, maxWaitTime = 60000) => {
|
71
|
-
|
432
|
+
MessageFormatter.info(chalk.blue(`Waiting for ${attributeKeys.length} attributes to become available...`));
|
72
433
|
const failedAttributes = [];
|
73
434
|
for (const attributeKey of attributeKeys) {
|
74
435
|
const success = await waitForAttributeAvailable(db, dbId, collectionId, attributeKey, maxWaitTime);
|
@@ -83,24 +444,41 @@ const waitForAllAttributesAvailable = async (db, dbId, collectionId, attributeKe
|
|
83
444
|
*/
|
84
445
|
const deleteAndRecreateCollection = async (db, dbId, collection, retryCount) => {
|
85
446
|
try {
|
86
|
-
|
447
|
+
MessageFormatter.info(chalk.yellow(`🗑️ Deleting collection '${collection.name}' for retry ${retryCount}`));
|
87
448
|
// Delete the collection
|
88
|
-
|
89
|
-
|
449
|
+
if (isDatabaseAdapter(db)) {
|
450
|
+
await db.deleteTable({ databaseId: dbId, tableId: collection.$id });
|
451
|
+
}
|
452
|
+
else {
|
453
|
+
await db.deleteCollection(dbId, collection.$id);
|
454
|
+
}
|
455
|
+
MessageFormatter.warning(`Deleted collection '${collection.name}'`);
|
90
456
|
// Wait a bit before recreating
|
91
457
|
await delay(2000);
|
92
458
|
// Recreate the collection
|
93
|
-
|
94
|
-
const newCollection =
|
95
|
-
|
459
|
+
MessageFormatter.info(`🔄 Recreating collection '${collection.name}'`);
|
460
|
+
const newCollection = isDatabaseAdapter(db)
|
461
|
+
? (await db.createTable({
|
462
|
+
databaseId: dbId,
|
463
|
+
id: collection.$id,
|
464
|
+
name: collection.name,
|
465
|
+
permissions: collection.$permissions,
|
466
|
+
documentSecurity: collection.documentSecurity,
|
467
|
+
enabled: collection.enabled
|
468
|
+
})).data
|
469
|
+
: await db.createCollection(dbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled);
|
470
|
+
MessageFormatter.success(`✅ Recreated collection '${collection.name}'`);
|
96
471
|
return newCollection;
|
97
472
|
}
|
98
473
|
catch (error) {
|
99
|
-
|
474
|
+
MessageFormatter.info(chalk.red(`Failed to delete/recreate collection '${collection.name}': ${error}`));
|
100
475
|
return null;
|
101
476
|
}
|
102
477
|
};
|
103
478
|
const attributesSame = (databaseAttribute, configAttribute) => {
|
479
|
+
// Normalize both attributes for comparison (handle extreme database values)
|
480
|
+
const normalizedDbAttr = normalizeAttributeForComparison(databaseAttribute);
|
481
|
+
const normalizedConfigAttr = normalizeAttributeForComparison(configAttribute);
|
104
482
|
const attributesToCheck = [
|
105
483
|
"key",
|
106
484
|
"type",
|
@@ -120,12 +498,12 @@ const attributesSame = (databaseAttribute, configAttribute) => {
|
|
120
498
|
];
|
121
499
|
return attributesToCheck.every((attr) => {
|
122
500
|
// Check if both objects have the attribute
|
123
|
-
const dbHasAttr = attr in
|
124
|
-
const configHasAttr = attr in
|
501
|
+
const dbHasAttr = attr in normalizedDbAttr;
|
502
|
+
const configHasAttr = attr in normalizedConfigAttr;
|
125
503
|
// If both have the attribute, compare values
|
126
504
|
if (dbHasAttr && configHasAttr) {
|
127
|
-
const dbValue =
|
128
|
-
const configValue =
|
505
|
+
const dbValue = normalizedDbAttr[attr];
|
506
|
+
const configValue = normalizedConfigAttr[attr];
|
129
507
|
// Consider undefined and null as equivalent
|
130
508
|
if ((dbValue === undefined || dbValue === null) &&
|
131
509
|
(configValue === undefined || configValue === null)) {
|
@@ -148,7 +526,7 @@ const attributesSame = (databaseAttribute, configAttribute) => {
|
|
148
526
|
}
|
149
527
|
// If one has the attribute and the other doesn't, check if it's undefined or null
|
150
528
|
if (dbHasAttr && !configHasAttr) {
|
151
|
-
const dbValue =
|
529
|
+
const dbValue = normalizedDbAttr[attr];
|
152
530
|
// Consider default-false booleans as equal to missing in config
|
153
531
|
if (typeof dbValue === "boolean") {
|
154
532
|
return dbValue === false; // missing in config equals false in db
|
@@ -156,7 +534,7 @@ const attributesSame = (databaseAttribute, configAttribute) => {
|
|
156
534
|
return dbValue === undefined || dbValue === null;
|
157
535
|
}
|
158
536
|
if (!dbHasAttr && configHasAttr) {
|
159
|
-
const configValue =
|
537
|
+
const configValue = normalizedConfigAttr[attr];
|
160
538
|
// Consider default-false booleans as equal to missing in db
|
161
539
|
if (typeof configValue === "boolean") {
|
162
540
|
return configValue === false; // missing in db equals false in config
|
@@ -171,14 +549,14 @@ const attributesSame = (databaseAttribute, configAttribute) => {
|
|
171
549
|
* Enhanced attribute creation with proper status monitoring and retry logic
|
172
550
|
*/
|
173
551
|
export const createOrUpdateAttributeWithStatusCheck = async (db, dbId, collection, attribute, retryCount = 0, maxRetries = 5) => {
|
174
|
-
|
552
|
+
MessageFormatter.info(chalk.blue(`Creating/updating attribute '${attribute.key}' (attempt ${retryCount + 1}/${maxRetries + 1})`));
|
175
553
|
try {
|
176
554
|
// First, try to create/update the attribute using existing logic
|
177
555
|
const result = await createOrUpdateAttribute(db, dbId, collection, attribute);
|
178
556
|
// If the attribute was queued (relationship dependency unresolved),
|
179
557
|
// skip status polling and retry logic — the queue will handle it later.
|
180
558
|
if (result === "queued") {
|
181
|
-
|
559
|
+
MessageFormatter.info(chalk.yellow(`⏭️ Deferred relationship attribute '${attribute.key}' — queued for later once dependencies are available`));
|
182
560
|
return true;
|
183
561
|
}
|
184
562
|
// Now wait for the attribute to become available
|
@@ -189,25 +567,34 @@ export const createOrUpdateAttributeWithStatusCheck = async (db, dbId, collectio
|
|
189
567
|
}
|
190
568
|
// If not successful and we have retries left, delete specific attribute and try again
|
191
569
|
if (retryCount < maxRetries) {
|
192
|
-
|
570
|
+
MessageFormatter.info(chalk.yellow(`Attribute '${attribute.key}' failed/stuck, deleting and retrying...`));
|
193
571
|
// Try to delete the specific stuck attribute instead of the entire collection
|
194
572
|
try {
|
195
|
-
|
196
|
-
|
573
|
+
if (isDatabaseAdapter(db)) {
|
574
|
+
await db.deleteAttribute({ databaseId: dbId, tableId: collection.$id, key: attribute.key });
|
575
|
+
}
|
576
|
+
else {
|
577
|
+
await db.deleteAttribute(dbId, collection.$id, attribute.key);
|
578
|
+
}
|
579
|
+
MessageFormatter.info(chalk.yellow(`Deleted stuck attribute '${attribute.key}', will retry creation`));
|
197
580
|
// Wait a bit before retry
|
198
581
|
await delay(3000);
|
199
582
|
// Get fresh collection data
|
200
|
-
const freshCollection =
|
583
|
+
const freshCollection = isDatabaseAdapter(db)
|
584
|
+
? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data
|
585
|
+
: await db.getCollection(dbId, collection.$id);
|
201
586
|
// Retry with the same collection (attribute should be gone now)
|
202
587
|
return await createOrUpdateAttributeWithStatusCheck(db, dbId, freshCollection, attribute, retryCount + 1, maxRetries);
|
203
588
|
}
|
204
589
|
catch (deleteError) {
|
205
|
-
|
590
|
+
MessageFormatter.info(chalk.red(`Failed to delete stuck attribute '${attribute.key}': ${deleteError}`));
|
206
591
|
// If attribute deletion fails, only then try collection recreation as last resort
|
207
592
|
if (retryCount >= maxRetries - 1) {
|
208
|
-
|
593
|
+
MessageFormatter.info(chalk.yellow(`Last resort: Recreating collection for attribute '${attribute.key}'`));
|
209
594
|
// Get fresh collection data
|
210
|
-
const freshCollection =
|
595
|
+
const freshCollection = isDatabaseAdapter(db)
|
596
|
+
? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data
|
597
|
+
: await db.getCollection(dbId, collection.$id);
|
211
598
|
// Delete and recreate collection
|
212
599
|
const newCollection = await deleteAndRecreateCollection(db, dbId, freshCollection, retryCount + 1);
|
213
600
|
if (newCollection) {
|
@@ -221,13 +608,13 @@ export const createOrUpdateAttributeWithStatusCheck = async (db, dbId, collectio
|
|
221
608
|
}
|
222
609
|
}
|
223
610
|
}
|
224
|
-
|
611
|
+
MessageFormatter.info(chalk.red(`❌ Failed to create attribute '${attribute.key}' after ${maxRetries + 1} attempts`));
|
225
612
|
return false;
|
226
613
|
}
|
227
614
|
catch (error) {
|
228
|
-
|
615
|
+
MessageFormatter.info(chalk.red(`Error creating attribute '${attribute.key}': ${error}`));
|
229
616
|
if (retryCount < maxRetries) {
|
230
|
-
|
617
|
+
MessageFormatter.info(chalk.yellow(`Retrying attribute '${attribute.key}' due to error...`));
|
231
618
|
// Wait a bit before retry
|
232
619
|
await delay(2000);
|
233
620
|
return await createOrUpdateAttributeWithStatusCheck(db, dbId, collection, attribute, retryCount + 1, maxRetries);
|
@@ -243,7 +630,6 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
243
630
|
try {
|
244
631
|
const collectionAttr = collection.attributes.find((attr) => attr.key === attribute.key);
|
245
632
|
foundAttribute = parseAttribute(collectionAttr);
|
246
|
-
// console.log(`Found attribute: ${JSON.stringify(foundAttribute)}`);
|
247
633
|
}
|
248
634
|
catch (error) {
|
249
635
|
foundAttribute = undefined;
|
@@ -257,7 +643,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
257
643
|
else if (foundAttribute &&
|
258
644
|
!attributesSame(foundAttribute, attribute) &&
|
259
645
|
updateEnabled) {
|
260
|
-
//
|
646
|
+
// MessageFormatter.info(
|
261
647
|
// `Updating attribute with same key ${attribute.key} but different values`
|
262
648
|
// );
|
263
649
|
finalAttribute = {
|
@@ -269,11 +655,15 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
269
655
|
else if (!updateEnabled &&
|
270
656
|
foundAttribute &&
|
271
657
|
!attributesSame(foundAttribute, attribute)) {
|
272
|
-
|
273
|
-
|
658
|
+
if (isDatabaseAdapter(db)) {
|
659
|
+
await db.deleteAttribute({ databaseId: dbId, tableId: collection.$id, key: attribute.key });
|
660
|
+
}
|
661
|
+
else {
|
662
|
+
await db.deleteAttribute(dbId, collection.$id, attribute.key);
|
663
|
+
}
|
664
|
+
MessageFormatter.info(`Deleted attribute: ${attribute.key} to recreate it because they diff (update disabled temporarily)`);
|
274
665
|
return "processed";
|
275
666
|
}
|
276
|
-
// console.log(`${action}-ing attribute: ${finalAttribute.key}`);
|
277
667
|
// Relationship attribute logic with adjustments
|
278
668
|
let collectionFoundViaRelatedCollection;
|
279
669
|
let relatedCollectionId;
|
@@ -281,7 +671,9 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
281
671
|
finalAttribute.relatedCollection) {
|
282
672
|
// First try treating relatedCollection as an ID directly
|
283
673
|
try {
|
284
|
-
const byIdCollection =
|
674
|
+
const byIdCollection = isDatabaseAdapter(db)
|
675
|
+
? (await db.getTable({ databaseId: dbId, tableId: finalAttribute.relatedCollection })).data
|
676
|
+
: await db.getCollection(dbId, finalAttribute.relatedCollection);
|
285
677
|
collectionFoundViaRelatedCollection = byIdCollection;
|
286
678
|
relatedCollectionId = byIdCollection.$id;
|
287
679
|
// Cache by name for subsequent lookups
|
@@ -293,28 +685,34 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
293
685
|
if (!collectionFoundViaRelatedCollection && nameToIdMapping.has(finalAttribute.relatedCollection)) {
|
294
686
|
relatedCollectionId = nameToIdMapping.get(finalAttribute.relatedCollection);
|
295
687
|
try {
|
296
|
-
collectionFoundViaRelatedCollection =
|
688
|
+
collectionFoundViaRelatedCollection = isDatabaseAdapter(db)
|
689
|
+
? (await db.getTable({ databaseId: dbId, tableId: relatedCollectionId })).data
|
690
|
+
: await db.getCollection(dbId, relatedCollectionId);
|
297
691
|
}
|
298
692
|
catch (e) {
|
299
|
-
//
|
693
|
+
// MessageFormatter.info(
|
300
694
|
// `Collection not found: ${finalAttribute.relatedCollection} when nameToIdMapping was set`
|
301
695
|
// );
|
302
696
|
collectionFoundViaRelatedCollection = undefined;
|
303
697
|
}
|
304
698
|
}
|
305
699
|
else if (!collectionFoundViaRelatedCollection) {
|
306
|
-
const collectionsPulled =
|
307
|
-
Query.equal("name", finalAttribute.relatedCollection)
|
308
|
-
|
309
|
-
if (collectionsPulled.total > 0) {
|
310
|
-
collectionFoundViaRelatedCollection =
|
311
|
-
|
312
|
-
|
700
|
+
const collectionsPulled = isDatabaseAdapter(db)
|
701
|
+
? await db.listTables({ databaseId: dbId, queries: [Query.equal("name", finalAttribute.relatedCollection)] })
|
702
|
+
: await db.listCollections(dbId, [Query.equal("name", finalAttribute.relatedCollection)]);
|
703
|
+
if (collectionsPulled.total && collectionsPulled.total > 0) {
|
704
|
+
collectionFoundViaRelatedCollection = isDatabaseAdapter(db)
|
705
|
+
? collectionsPulled.tables?.[0]
|
706
|
+
: collectionsPulled.collections?.[0];
|
707
|
+
relatedCollectionId = collectionFoundViaRelatedCollection?.$id;
|
708
|
+
if (relatedCollectionId) {
|
709
|
+
nameToIdMapping.set(finalAttribute.relatedCollection, relatedCollectionId);
|
710
|
+
}
|
313
711
|
}
|
314
712
|
}
|
315
713
|
// ONLY queue relationship attributes that have actual unresolved dependencies
|
316
714
|
if (!(relatedCollectionId && collectionFoundViaRelatedCollection)) {
|
317
|
-
|
715
|
+
MessageFormatter.info(chalk.yellow(`⏳ Queueing relationship attribute '${finalAttribute.key}' - related collection '${finalAttribute.relatedCollection}' not found yet`));
|
318
716
|
enqueueOperation({
|
319
717
|
type: "attribute",
|
320
718
|
collectionId: collection.$id,
|
@@ -326,158 +724,12 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
326
724
|
}
|
327
725
|
}
|
328
726
|
finalAttribute = parseAttribute(finalAttribute);
|
329
|
-
//
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
: null, finalAttribute.array || false, finalAttribute.encrypted));
|
336
|
-
}
|
337
|
-
else {
|
338
|
-
await tryAwaitWithRetry(async () => await db.updateStringAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required
|
339
|
-
? finalAttribute.xdefault
|
340
|
-
: null, finalAttribute.size));
|
341
|
-
}
|
342
|
-
break;
|
343
|
-
case "integer":
|
344
|
-
if (action === "create") {
|
345
|
-
if (finalAttribute.min &&
|
346
|
-
BigInt(finalAttribute.min) === BigInt(-9223372036854776000)) {
|
347
|
-
finalAttribute.min = undefined;
|
348
|
-
}
|
349
|
-
if (finalAttribute.max &&
|
350
|
-
BigInt(finalAttribute.max) === BigInt(9223372036854776000)) {
|
351
|
-
finalAttribute.max = undefined;
|
352
|
-
}
|
353
|
-
const minValue = finalAttribute.min !== undefined && finalAttribute.min !== null
|
354
|
-
? parseInt(finalAttribute.min)
|
355
|
-
: -9007199254740991;
|
356
|
-
const maxValue = finalAttribute.max !== undefined && finalAttribute.max !== null
|
357
|
-
? parseInt(finalAttribute.max)
|
358
|
-
: 9007199254740991;
|
359
|
-
console.log(`DEBUG: Creating integer attribute '${finalAttribute.key}' with min=${minValue}, max=${maxValue}, minType=${typeof minValue}, maxType=${typeof maxValue}`);
|
360
|
-
await tryAwaitWithRetry(async () => await db.createIntegerAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, minValue, maxValue, finalAttribute.xdefault !== undefined && !finalAttribute.required
|
361
|
-
? finalAttribute.xdefault
|
362
|
-
: null, finalAttribute.array || false));
|
363
|
-
}
|
364
|
-
else {
|
365
|
-
if (finalAttribute.min &&
|
366
|
-
BigInt(finalAttribute.min) === BigInt(-9223372036854776000)) {
|
367
|
-
finalAttribute.min = undefined;
|
368
|
-
}
|
369
|
-
if (finalAttribute.max &&
|
370
|
-
BigInt(finalAttribute.max) === BigInt(9223372036854776000)) {
|
371
|
-
finalAttribute.max = undefined;
|
372
|
-
}
|
373
|
-
const minValue = finalAttribute.min !== undefined && finalAttribute.min !== null
|
374
|
-
? parseInt(finalAttribute.min)
|
375
|
-
: -9007199254740991;
|
376
|
-
const maxValue = finalAttribute.max !== undefined && finalAttribute.max !== null
|
377
|
-
? parseInt(finalAttribute.max)
|
378
|
-
: 9007199254740991;
|
379
|
-
console.log(`DEBUG: Updating integer attribute '${finalAttribute.key}' with min=${minValue}, max=${maxValue}, minType=${typeof minValue}, maxType=${typeof maxValue}`);
|
380
|
-
await tryAwaitWithRetry(async () => await db.updateIntegerAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required
|
381
|
-
? finalAttribute.xdefault
|
382
|
-
: null, minValue, maxValue));
|
383
|
-
}
|
384
|
-
break;
|
385
|
-
case "double":
|
386
|
-
case "float": // Backward compatibility
|
387
|
-
if (action === "create") {
|
388
|
-
await tryAwaitWithRetry(async () => await db.createFloatAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.min, finalAttribute.max, finalAttribute.xdefault !== undefined && !finalAttribute.required
|
389
|
-
? finalAttribute.xdefault
|
390
|
-
: null, finalAttribute.array || false));
|
391
|
-
}
|
392
|
-
else {
|
393
|
-
await tryAwaitWithRetry(async () => await db.updateFloatAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.min, finalAttribute.max, finalAttribute.xdefault !== undefined && !finalAttribute.required
|
394
|
-
? finalAttribute.xdefault
|
395
|
-
: null));
|
396
|
-
}
|
397
|
-
break;
|
398
|
-
case "boolean":
|
399
|
-
if (action === "create") {
|
400
|
-
await tryAwaitWithRetry(async () => await db.createBooleanAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required
|
401
|
-
? finalAttribute.xdefault
|
402
|
-
: null, finalAttribute.array || false));
|
403
|
-
}
|
404
|
-
else {
|
405
|
-
await tryAwaitWithRetry(async () => await db.updateBooleanAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required
|
406
|
-
? finalAttribute.xdefault
|
407
|
-
: null));
|
408
|
-
}
|
409
|
-
break;
|
410
|
-
case "datetime":
|
411
|
-
if (action === "create") {
|
412
|
-
await tryAwaitWithRetry(async () => await db.createDatetimeAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required
|
413
|
-
? finalAttribute.xdefault
|
414
|
-
: null, finalAttribute.array || false));
|
415
|
-
}
|
416
|
-
else {
|
417
|
-
await tryAwaitWithRetry(async () => await db.updateDatetimeAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required
|
418
|
-
? finalAttribute.xdefault
|
419
|
-
: null));
|
420
|
-
}
|
421
|
-
break;
|
422
|
-
case "email":
|
423
|
-
if (action === "create") {
|
424
|
-
await tryAwaitWithRetry(async () => await db.createEmailAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required
|
425
|
-
? finalAttribute.xdefault
|
426
|
-
: null, finalAttribute.array || false));
|
427
|
-
}
|
428
|
-
else {
|
429
|
-
await tryAwaitWithRetry(async () => await db.updateEmailAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required
|
430
|
-
? finalAttribute.xdefault
|
431
|
-
: null));
|
432
|
-
}
|
433
|
-
break;
|
434
|
-
case "ip":
|
435
|
-
if (action === "create") {
|
436
|
-
await tryAwaitWithRetry(async () => await db.createIpAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required
|
437
|
-
? finalAttribute.xdefault
|
438
|
-
: null, finalAttribute.array || false));
|
439
|
-
}
|
440
|
-
else {
|
441
|
-
await tryAwaitWithRetry(async () => await db.updateIpAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required
|
442
|
-
? finalAttribute.xdefault
|
443
|
-
: null));
|
444
|
-
}
|
445
|
-
break;
|
446
|
-
case "url":
|
447
|
-
if (action === "create") {
|
448
|
-
await tryAwaitWithRetry(async () => await db.createUrlAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required
|
449
|
-
? finalAttribute.xdefault
|
450
|
-
: null, finalAttribute.array || false));
|
451
|
-
}
|
452
|
-
else {
|
453
|
-
await tryAwaitWithRetry(async () => await db.updateUrlAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required
|
454
|
-
? finalAttribute.xdefault
|
455
|
-
: null));
|
456
|
-
}
|
457
|
-
break;
|
458
|
-
case "enum":
|
459
|
-
if (action === "create") {
|
460
|
-
await tryAwaitWithRetry(async () => await db.createEnumAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.elements, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required
|
461
|
-
? finalAttribute.xdefault
|
462
|
-
: null, finalAttribute.array || false));
|
463
|
-
}
|
464
|
-
else {
|
465
|
-
await tryAwaitWithRetry(async () => await db.updateEnumAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.elements, finalAttribute.required || false, finalAttribute.xdefault !== undefined && !finalAttribute.required
|
466
|
-
? finalAttribute.xdefault
|
467
|
-
: null));
|
468
|
-
}
|
469
|
-
break;
|
470
|
-
case "relationship":
|
471
|
-
if (action === "create") {
|
472
|
-
await tryAwaitWithRetry(async () => await db.createRelationshipAttribute(dbId, collection.$id, relatedCollectionId, finalAttribute.relationType, finalAttribute.twoWay, finalAttribute.key, finalAttribute.twoWayKey, finalAttribute.onDelete));
|
473
|
-
}
|
474
|
-
else {
|
475
|
-
await tryAwaitWithRetry(async () => await db.updateRelationshipAttribute(dbId, collection.$id, finalAttribute.key, finalAttribute.onDelete));
|
476
|
-
}
|
477
|
-
break;
|
478
|
-
default:
|
479
|
-
console.error("Invalid attribute type");
|
480
|
-
break;
|
727
|
+
// Use adapter-based attribute creation/update
|
728
|
+
if (action === "create") {
|
729
|
+
await tryAwaitWithRetry(async () => await createAttributeViaAdapter(db, dbId, collection.$id, finalAttribute));
|
730
|
+
}
|
731
|
+
else {
|
732
|
+
await tryAwaitWithRetry(async () => await updateAttributeViaAdapter(db, dbId, collection.$id, finalAttribute));
|
481
733
|
}
|
482
734
|
return "processed";
|
483
735
|
};
|
@@ -485,7 +737,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
485
737
|
* Enhanced collection attribute creation with proper status monitoring
|
486
738
|
*/
|
487
739
|
export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId, collection, attributes) => {
|
488
|
-
|
740
|
+
MessageFormatter.info(chalk.green(`Creating/Updating attributes for collection: ${collection.name} with status monitoring`));
|
489
741
|
const existingAttributes =
|
490
742
|
// @ts-expect-error
|
491
743
|
collection.attributes.map((attr) => parseAttribute(attr)) || [];
|
@@ -494,28 +746,44 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
|
|
494
746
|
// Handle attribute removal first
|
495
747
|
if (attributesToRemove.length > 0) {
|
496
748
|
if (indexesToRemove.length > 0) {
|
497
|
-
|
749
|
+
MessageFormatter.info(chalk.red(`Removing indexes as they rely on an attribute that is being removed: ${indexesToRemove
|
498
750
|
.map((index) => index.key)
|
499
751
|
.join(", ")}`));
|
500
752
|
for (const index of indexesToRemove) {
|
501
|
-
await tryAwaitWithRetry(async () =>
|
753
|
+
await tryAwaitWithRetry(async () => {
|
754
|
+
if (isDatabaseAdapter(db)) {
|
755
|
+
await db.deleteIndex({ databaseId: dbId, tableId: collection.$id, key: index.key });
|
756
|
+
}
|
757
|
+
else {
|
758
|
+
await db.deleteIndex(dbId, collection.$id, index.key);
|
759
|
+
}
|
760
|
+
});
|
502
761
|
await delay(500); // Longer delay for deletions
|
503
762
|
}
|
504
763
|
}
|
505
764
|
for (const attr of attributesToRemove) {
|
506
|
-
|
507
|
-
await tryAwaitWithRetry(async () =>
|
765
|
+
MessageFormatter.info(chalk.red(`Removing attribute: ${attr.key} as it is no longer in the collection`));
|
766
|
+
await tryAwaitWithRetry(async () => {
|
767
|
+
if (isDatabaseAdapter(db)) {
|
768
|
+
await db.deleteAttribute({ databaseId: dbId, tableId: collection.$id, key: attr.key });
|
769
|
+
}
|
770
|
+
else {
|
771
|
+
await db.deleteAttribute(dbId, collection.$id, attr.key);
|
772
|
+
}
|
773
|
+
});
|
508
774
|
await delay(500); // Longer delay for deletions
|
509
775
|
}
|
510
776
|
}
|
511
777
|
// First, get fresh collection data and determine which attributes actually need processing
|
512
|
-
|
778
|
+
MessageFormatter.info(chalk.blue(`Analyzing ${attributes.length} attributes to determine which need processing...`));
|
513
779
|
let currentCollection = collection;
|
514
780
|
try {
|
515
|
-
currentCollection =
|
781
|
+
currentCollection = isDatabaseAdapter(db)
|
782
|
+
? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data
|
783
|
+
: await db.getCollection(dbId, collection.$id);
|
516
784
|
}
|
517
785
|
catch (error) {
|
518
|
-
|
786
|
+
MessageFormatter.info(chalk.yellow(`Warning: Could not refresh collection data: ${error}`));
|
519
787
|
}
|
520
788
|
const existingAttributesMap = new Map();
|
521
789
|
try {
|
@@ -525,29 +793,34 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
|
|
525
793
|
parsedAttributes.forEach((attr) => existingAttributesMap.set(attr.key, attr));
|
526
794
|
}
|
527
795
|
catch (error) {
|
528
|
-
|
796
|
+
MessageFormatter.info(chalk.yellow(`Warning: Could not parse existing attributes: ${error}`));
|
529
797
|
}
|
530
|
-
// Filter to only attributes that need processing (new or
|
798
|
+
// Filter to only attributes that need processing (new, changed, or not yet processed)
|
531
799
|
const attributesToProcess = attributes.filter((attribute) => {
|
800
|
+
// Skip if already processed in this session
|
801
|
+
if (isAttributeProcessed(currentCollection.$id, attribute.key)) {
|
802
|
+
MessageFormatter.info(chalk.gray(`⏭️ Attribute '${attribute.key}' already processed in this session (skipping)`));
|
803
|
+
return false;
|
804
|
+
}
|
532
805
|
const existing = existingAttributesMap.get(attribute.key);
|
533
806
|
if (!existing) {
|
534
|
-
|
807
|
+
MessageFormatter.info(`➕ New attribute: ${attribute.key}`);
|
535
808
|
return true;
|
536
809
|
}
|
537
810
|
const needsUpdate = !attributesSame(existing, attribute);
|
538
811
|
if (needsUpdate) {
|
539
|
-
|
812
|
+
MessageFormatter.info(`🔄 Changed attribute: ${attribute.key}`);
|
540
813
|
}
|
541
814
|
else {
|
542
|
-
|
815
|
+
MessageFormatter.info(chalk.gray(`✅ Unchanged attribute: ${attribute.key} (skipping)`));
|
543
816
|
}
|
544
817
|
return needsUpdate;
|
545
818
|
});
|
546
819
|
if (attributesToProcess.length === 0) {
|
547
|
-
|
820
|
+
MessageFormatter.info(chalk.green(`✅ All ${attributes.length} attributes are already up to date for collection: ${collection.name}`));
|
548
821
|
return true;
|
549
822
|
}
|
550
|
-
|
823
|
+
MessageFormatter.info(chalk.blue(`Creating ${attributesToProcess.length} attributes sequentially with status monitoring...`));
|
551
824
|
let remainingAttributes = [...attributesToProcess];
|
552
825
|
let overallRetryCount = 0;
|
553
826
|
const maxOverallRetries = 3;
|
@@ -555,58 +828,64 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
|
|
555
828
|
overallRetryCount < maxOverallRetries) {
|
556
829
|
const attributesToProcessThisRound = [...remainingAttributes];
|
557
830
|
remainingAttributes = []; // Reset for next iteration
|
558
|
-
|
831
|
+
MessageFormatter.info(chalk.blue(`\n=== Attempt ${overallRetryCount + 1}/${maxOverallRetries} - Processing ${attributesToProcessThisRound.length} attributes ===`));
|
559
832
|
for (const attribute of attributesToProcessThisRound) {
|
560
|
-
|
833
|
+
MessageFormatter.info(chalk.blue(`\n--- Processing attribute: ${attribute.key} ---`));
|
561
834
|
const success = await createOrUpdateAttributeWithStatusCheck(db, dbId, currentCollection, attribute);
|
562
835
|
if (success) {
|
563
|
-
|
836
|
+
MessageFormatter.info(chalk.green(`✅ Successfully created attribute: ${attribute.key}`));
|
837
|
+
// Mark this specific attribute as processed
|
838
|
+
markAttributeProcessed(currentCollection.$id, attribute.key);
|
564
839
|
// Get updated collection data for next iteration
|
565
840
|
try {
|
566
|
-
currentCollection =
|
841
|
+
currentCollection = isDatabaseAdapter(db)
|
842
|
+
? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data
|
843
|
+
: await db.getCollection(dbId, collection.$id);
|
567
844
|
}
|
568
845
|
catch (error) {
|
569
|
-
|
846
|
+
MessageFormatter.info(chalk.yellow(`Warning: Could not refresh collection data: ${error}`));
|
570
847
|
}
|
571
848
|
// Add delay between successful attributes
|
572
849
|
await delay(1000);
|
573
850
|
}
|
574
851
|
else {
|
575
|
-
|
852
|
+
MessageFormatter.info(chalk.red(`❌ Failed to create attribute: ${attribute.key}, will retry in next round`));
|
576
853
|
remainingAttributes.push(attribute); // Add back to retry list
|
577
854
|
}
|
578
855
|
}
|
579
856
|
if (remainingAttributes.length === 0) {
|
580
|
-
|
857
|
+
MessageFormatter.info(chalk.green(`\n✅ Successfully created all ${attributesToProcess.length} attributes for collection: ${collection.name}`));
|
581
858
|
return true;
|
582
859
|
}
|
583
860
|
overallRetryCount++;
|
584
861
|
if (overallRetryCount < maxOverallRetries) {
|
585
|
-
|
862
|
+
MessageFormatter.info(chalk.yellow(`\n⏳ Waiting 5 seconds before retrying ${attributesToProcess.length} failed attributes...`));
|
586
863
|
await delay(5000);
|
587
864
|
// Refresh collection data before retry
|
588
865
|
try {
|
589
|
-
currentCollection =
|
590
|
-
|
866
|
+
currentCollection = isDatabaseAdapter(db)
|
867
|
+
? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data
|
868
|
+
: await db.getCollection(dbId, collection.$id);
|
869
|
+
MessageFormatter.info(`Refreshed collection data for retry`);
|
591
870
|
}
|
592
871
|
catch (error) {
|
593
|
-
|
872
|
+
MessageFormatter.info(chalk.yellow(`Warning: Could not refresh collection data for retry: ${error}`));
|
594
873
|
}
|
595
874
|
}
|
596
875
|
}
|
597
876
|
// If we get here, some attributes still failed after all retries
|
598
877
|
if (attributesToProcess.length > 0) {
|
599
|
-
|
878
|
+
MessageFormatter.info(chalk.red(`\n❌ Failed to create ${attributesToProcess.length} attributes after ${maxOverallRetries} attempts: ${attributesToProcess
|
600
879
|
.map((a) => a.key)
|
601
880
|
.join(", ")}`));
|
602
|
-
|
881
|
+
MessageFormatter.info(chalk.red(`This may indicate a fundamental issue with the attribute definitions or Appwrite instance`));
|
603
882
|
return false;
|
604
883
|
}
|
605
|
-
|
884
|
+
MessageFormatter.info(chalk.green(`\n✅ Successfully created all ${attributes.length} attributes for collection: ${collection.name}`));
|
606
885
|
return true;
|
607
886
|
};
|
608
887
|
export const createUpdateCollectionAttributes = async (db, dbId, collection, attributes) => {
|
609
|
-
|
888
|
+
MessageFormatter.info(chalk.green(`Creating/Updating attributes for collection: ${collection.name}`));
|
610
889
|
const existingAttributes =
|
611
890
|
// @ts-expect-error
|
612
891
|
collection.attributes.map((attr) => parseAttribute(attr)) || [];
|
@@ -614,17 +893,31 @@ export const createUpdateCollectionAttributes = async (db, dbId, collection, att
|
|
614
893
|
const indexesToRemove = collection.indexes.filter((index) => attributesToRemove.some((attr) => index.attributes.includes(attr.key)));
|
615
894
|
if (attributesToRemove.length > 0) {
|
616
895
|
if (indexesToRemove.length > 0) {
|
617
|
-
|
896
|
+
MessageFormatter.info(chalk.red(`Removing indexes as they rely on an attribute that is being removed: ${indexesToRemove
|
618
897
|
.map((index) => index.key)
|
619
898
|
.join(", ")}`));
|
620
899
|
for (const index of indexesToRemove) {
|
621
|
-
await tryAwaitWithRetry(async () =>
|
900
|
+
await tryAwaitWithRetry(async () => {
|
901
|
+
if (isDatabaseAdapter(db)) {
|
902
|
+
await db.deleteIndex({ databaseId: dbId, tableId: collection.$id, key: index.key });
|
903
|
+
}
|
904
|
+
else {
|
905
|
+
await db.deleteIndex(dbId, collection.$id, index.key);
|
906
|
+
}
|
907
|
+
});
|
622
908
|
await delay(100);
|
623
909
|
}
|
624
910
|
}
|
625
911
|
for (const attr of attributesToRemove) {
|
626
|
-
|
627
|
-
await tryAwaitWithRetry(async () =>
|
912
|
+
MessageFormatter.info(chalk.red(`Removing attribute: ${attr.key} as it is no longer in the collection`));
|
913
|
+
await tryAwaitWithRetry(async () => {
|
914
|
+
if (isDatabaseAdapter(db)) {
|
915
|
+
await db.deleteAttribute({ databaseId: dbId, tableId: collection.$id, key: attr.key });
|
916
|
+
}
|
917
|
+
else {
|
918
|
+
await db.deleteAttribute(dbId, collection.$id, attr.key);
|
919
|
+
}
|
920
|
+
});
|
628
921
|
await delay(50);
|
629
922
|
}
|
630
923
|
}
|
@@ -635,11 +928,11 @@ export const createUpdateCollectionAttributes = async (db, dbId, collection, att
|
|
635
928
|
const results = await Promise.allSettled(attributePromises);
|
636
929
|
results.forEach((result) => {
|
637
930
|
if (result.status === "rejected") {
|
638
|
-
|
931
|
+
MessageFormatter.error("An attribute promise was rejected:", result.reason);
|
639
932
|
}
|
640
933
|
});
|
641
934
|
// Add delay after each batch
|
642
935
|
await delay(200);
|
643
936
|
}
|
644
|
-
|
937
|
+
MessageFormatter.info(`Finished creating/updating attributes for collection: ${collection.name}`);
|
645
938
|
};
|