appwrite-utils-cli 1.7.9 → 1.8.2
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 +14 -199
- package/README.md +87 -30
- package/dist/adapters/AdapterFactory.js +5 -25
- package/dist/adapters/DatabaseAdapter.d.ts +17 -2
- package/dist/adapters/LegacyAdapter.d.ts +2 -1
- package/dist/adapters/LegacyAdapter.js +212 -16
- package/dist/adapters/TablesDBAdapter.d.ts +2 -12
- package/dist/adapters/TablesDBAdapter.js +261 -57
- package/dist/cli/commands/databaseCommands.js +4 -3
- package/dist/cli/commands/functionCommands.js +17 -8
- package/dist/collections/attributes.js +447 -125
- package/dist/collections/methods.js +197 -186
- package/dist/collections/tableOperations.d.ts +86 -0
- package/dist/collections/tableOperations.js +434 -0
- package/dist/collections/transferOperations.d.ts +3 -2
- package/dist/collections/transferOperations.js +93 -12
- package/dist/config/yamlConfig.d.ts +221 -88
- package/dist/examples/yamlTerminologyExample.d.ts +1 -1
- package/dist/examples/yamlTerminologyExample.js +6 -3
- package/dist/functions/fnConfigDiscovery.d.ts +3 -0
- package/dist/functions/fnConfigDiscovery.js +108 -0
- package/dist/interactiveCLI.js +18 -15
- package/dist/main.js +211 -73
- package/dist/migrations/appwriteToX.d.ts +88 -23
- package/dist/migrations/comprehensiveTransfer.d.ts +2 -0
- package/dist/migrations/comprehensiveTransfer.js +83 -6
- package/dist/migrations/dataLoader.d.ts +227 -69
- package/dist/migrations/dataLoader.js +3 -3
- package/dist/migrations/importController.js +3 -3
- package/dist/migrations/relationships.d.ts +8 -2
- package/dist/migrations/services/ImportOrchestrator.js +3 -3
- package/dist/migrations/transfer.js +159 -37
- package/dist/shared/attributeMapper.d.ts +20 -0
- package/dist/shared/attributeMapper.js +203 -0
- package/dist/shared/selectionDialogs.js +8 -4
- package/dist/storage/schemas.d.ts +354 -92
- package/dist/utils/configDiscovery.js +4 -3
- package/dist/utils/versionDetection.d.ts +0 -4
- package/dist/utils/versionDetection.js +41 -173
- package/dist/utils/yamlConverter.js +89 -16
- package/dist/utils/yamlLoader.d.ts +1 -1
- package/dist/utils/yamlLoader.js +6 -2
- package/dist/utilsController.js +56 -19
- package/package.json +4 -4
- package/src/adapters/AdapterFactory.ts +119 -143
- package/src/adapters/DatabaseAdapter.ts +18 -3
- package/src/adapters/LegacyAdapter.ts +236 -105
- package/src/adapters/TablesDBAdapter.ts +773 -643
- package/src/cli/commands/databaseCommands.ts +13 -12
- package/src/cli/commands/functionCommands.ts +23 -14
- package/src/collections/attributes.ts +2054 -1611
- package/src/collections/methods.ts +208 -293
- package/src/collections/tableOperations.ts +506 -0
- package/src/collections/transferOperations.ts +218 -144
- package/src/examples/yamlTerminologyExample.ts +10 -5
- package/src/functions/fnConfigDiscovery.ts +103 -0
- package/src/interactiveCLI.ts +25 -20
- package/src/main.ts +549 -194
- package/src/migrations/comprehensiveTransfer.ts +126 -50
- package/src/migrations/dataLoader.ts +3 -3
- package/src/migrations/importController.ts +3 -3
- package/src/migrations/services/ImportOrchestrator.ts +3 -3
- package/src/migrations/transfer.ts +148 -131
- package/src/shared/attributeMapper.ts +229 -0
- package/src/shared/selectionDialogs.ts +29 -25
- package/src/utils/configDiscovery.ts +9 -3
- package/src/utils/versionDetection.ts +74 -228
- package/src/utils/yamlConverter.ts +94 -17
- package/src/utils/yamlLoader.ts +11 -4
- package/src/utilsController.ts +80 -30
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
import { Query } from "node-appwrite";
|
|
2
2
|
import { attributeSchema, parseAttribute, } from "appwrite-utils";
|
|
3
|
-
import { nameToIdMapping, enqueueOperation, markAttributeProcessed, isAttributeProcessed } from "../shared/operationQueue.js";
|
|
4
|
-
import { delay, tryAwaitWithRetry, calculateExponentialBackoff } 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 { Decimal } from "decimal.js";
|
|
6
7
|
import { logger } from "../shared/logging.js";
|
|
7
8
|
import { MessageFormatter } from "../shared/messageFormatter.js";
|
|
8
9
|
import { isDatabaseAdapter } from "../utils/typeGuards.js";
|
|
9
|
-
// Threshold for treating min/max values as undefined (1 trillion)
|
|
10
|
-
const MIN_MAX_THRESHOLD = 1_000_000_000_000;
|
|
11
10
|
// Extreme values that Appwrite may return, which should be treated as undefined
|
|
12
11
|
const EXTREME_MIN_INTEGER = -9223372036854776000;
|
|
13
12
|
const EXTREME_MAX_INTEGER = 9223372036854776000;
|
|
14
|
-
const EXTREME_MIN_FLOAT = -1.
|
|
15
|
-
const EXTREME_MAX_FLOAT = 1.
|
|
13
|
+
const EXTREME_MIN_FLOAT = -1.7976931348623157e308;
|
|
14
|
+
const EXTREME_MAX_FLOAT = 1.7976931348623157e308;
|
|
16
15
|
/**
|
|
17
16
|
* Type guard to check if an attribute has min/max properties
|
|
18
17
|
*/
|
|
19
18
|
const hasMinMaxProperties = (attribute) => {
|
|
20
|
-
return attribute.type ===
|
|
19
|
+
return (attribute.type === "integer" ||
|
|
20
|
+
attribute.type === "double" ||
|
|
21
|
+
attribute.type === "float");
|
|
21
22
|
};
|
|
22
23
|
/**
|
|
23
|
-
* Normalizes min/max values for integer and float attributes
|
|
24
|
-
*
|
|
24
|
+
* Normalizes min/max values for integer and float attributes using Decimal.js for precision
|
|
25
|
+
* Validates that min < max and handles extreme database values
|
|
25
26
|
*/
|
|
26
27
|
const normalizeMinMaxValues = (attribute) => {
|
|
27
28
|
if (!hasMinMaxProperties(attribute)) {
|
|
28
29
|
logger.debug(`Attribute '${attribute.key}' does not have min/max properties`, {
|
|
29
30
|
type: attribute.type,
|
|
30
|
-
operation:
|
|
31
|
+
operation: "normalizeMinMaxValues",
|
|
31
32
|
});
|
|
32
33
|
return {};
|
|
33
34
|
}
|
|
@@ -38,21 +39,20 @@ const normalizeMinMaxValues = (attribute) => {
|
|
|
38
39
|
type,
|
|
39
40
|
originalMin: min,
|
|
40
41
|
originalMax: max,
|
|
41
|
-
operation:
|
|
42
|
+
operation: "normalizeMinMaxValues",
|
|
42
43
|
});
|
|
43
|
-
// Handle min value
|
|
44
|
+
// Handle min value - only filter out extreme database values
|
|
44
45
|
if (normalizedMin !== undefined && normalizedMin !== null) {
|
|
45
46
|
const minValue = Number(normalizedMin);
|
|
46
47
|
const originalMin = normalizedMin;
|
|
47
|
-
// Check if it
|
|
48
|
+
// Check if it's an extreme database value (but don't filter out large numbers)
|
|
48
49
|
if (type === 'integer') {
|
|
49
|
-
if (
|
|
50
|
+
if (minValue === EXTREME_MIN_INTEGER) {
|
|
50
51
|
logger.debug(`Min value normalized to undefined for attribute '${attribute.key}'`, {
|
|
51
52
|
type,
|
|
52
53
|
originalValue: originalMin,
|
|
53
54
|
numericValue: minValue,
|
|
54
|
-
reason:
|
|
55
|
-
threshold: MIN_MAX_THRESHOLD,
|
|
55
|
+
reason: 'extreme_database_value',
|
|
56
56
|
extremeValue: EXTREME_MIN_INTEGER,
|
|
57
57
|
operation: 'normalizeMinMaxValues'
|
|
58
58
|
});
|
|
@@ -60,13 +60,12 @@ const normalizeMinMaxValues = (attribute) => {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
else { // float/double
|
|
63
|
-
if (
|
|
63
|
+
if (minValue === EXTREME_MIN_FLOAT) {
|
|
64
64
|
logger.debug(`Min value normalized to undefined for attribute '${attribute.key}'`, {
|
|
65
65
|
type,
|
|
66
66
|
originalValue: originalMin,
|
|
67
67
|
numericValue: minValue,
|
|
68
|
-
reason:
|
|
69
|
-
threshold: MIN_MAX_THRESHOLD,
|
|
68
|
+
reason: 'extreme_database_value',
|
|
70
69
|
extremeValue: EXTREME_MIN_FLOAT,
|
|
71
70
|
operation: 'normalizeMinMaxValues'
|
|
72
71
|
});
|
|
@@ -74,19 +73,18 @@ const normalizeMinMaxValues = (attribute) => {
|
|
|
74
73
|
}
|
|
75
74
|
}
|
|
76
75
|
}
|
|
77
|
-
// Handle max value
|
|
76
|
+
// Handle max value - only filter out extreme database values
|
|
78
77
|
if (normalizedMax !== undefined && normalizedMax !== null) {
|
|
79
78
|
const maxValue = Number(normalizedMax);
|
|
80
79
|
const originalMax = normalizedMax;
|
|
81
|
-
// Check if it
|
|
80
|
+
// Check if it's an extreme database value (but don't filter out large numbers)
|
|
82
81
|
if (type === 'integer') {
|
|
83
|
-
if (
|
|
82
|
+
if (maxValue === EXTREME_MAX_INTEGER) {
|
|
84
83
|
logger.debug(`Max value normalized to undefined for attribute '${attribute.key}'`, {
|
|
85
84
|
type,
|
|
86
85
|
originalValue: originalMax,
|
|
87
86
|
numericValue: maxValue,
|
|
88
|
-
reason:
|
|
89
|
-
threshold: MIN_MAX_THRESHOLD,
|
|
87
|
+
reason: 'extreme_database_value',
|
|
90
88
|
extremeValue: EXTREME_MAX_INTEGER,
|
|
91
89
|
operation: 'normalizeMinMaxValues'
|
|
92
90
|
});
|
|
@@ -94,13 +92,12 @@ const normalizeMinMaxValues = (attribute) => {
|
|
|
94
92
|
}
|
|
95
93
|
}
|
|
96
94
|
else { // float/double
|
|
97
|
-
if (
|
|
95
|
+
if (maxValue === EXTREME_MAX_FLOAT) {
|
|
98
96
|
logger.debug(`Max value normalized to undefined for attribute '${attribute.key}'`, {
|
|
99
97
|
type,
|
|
100
98
|
originalValue: originalMax,
|
|
101
99
|
numericValue: maxValue,
|
|
102
|
-
reason:
|
|
103
|
-
threshold: MIN_MAX_THRESHOLD,
|
|
100
|
+
reason: 'extreme_database_value',
|
|
104
101
|
extremeValue: EXTREME_MAX_FLOAT,
|
|
105
102
|
operation: 'normalizeMinMaxValues'
|
|
106
103
|
});
|
|
@@ -108,11 +105,115 @@ const normalizeMinMaxValues = (attribute) => {
|
|
|
108
105
|
}
|
|
109
106
|
}
|
|
110
107
|
}
|
|
108
|
+
// Validate that min < max using multiple comparison methods for reliability
|
|
109
|
+
if (normalizedMin !== undefined && normalizedMax !== undefined &&
|
|
110
|
+
normalizedMin !== null && normalizedMax !== null) {
|
|
111
|
+
logger.debug(`Validating min/max values for attribute '${attribute.key}'`, {
|
|
112
|
+
type,
|
|
113
|
+
normalizedMin,
|
|
114
|
+
normalizedMax,
|
|
115
|
+
normalizedMinType: typeof normalizedMin,
|
|
116
|
+
normalizedMaxType: typeof normalizedMax,
|
|
117
|
+
operation: 'normalizeMinMaxValues'
|
|
118
|
+
});
|
|
119
|
+
// Use multiple validation approaches to ensure reliability
|
|
120
|
+
let needsSwap = false;
|
|
121
|
+
let comparisonMethod = '';
|
|
122
|
+
try {
|
|
123
|
+
// Method 1: Direct number comparison (most reliable for normal numbers)
|
|
124
|
+
const minNum = Number(normalizedMin);
|
|
125
|
+
const maxNum = Number(normalizedMax);
|
|
126
|
+
if (!isNaN(minNum) && !isNaN(maxNum)) {
|
|
127
|
+
needsSwap = minNum >= maxNum;
|
|
128
|
+
comparisonMethod = 'direct_number_comparison';
|
|
129
|
+
logger.debug(`Direct number comparison: ${minNum} >= ${maxNum} = ${needsSwap}`, {
|
|
130
|
+
operation: 'normalizeMinMaxValues'
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
// Method 2: Fallback to string comparison for very large numbers
|
|
134
|
+
if (!needsSwap && (isNaN(minNum) || isNaN(maxNum) || Math.abs(minNum) > Number.MAX_SAFE_INTEGER || Math.abs(maxNum) > Number.MAX_SAFE_INTEGER)) {
|
|
135
|
+
const minStr = normalizedMin.toString();
|
|
136
|
+
const maxStr = normalizedMax.toString();
|
|
137
|
+
// Simple string length and lexicographical comparison for very large numbers
|
|
138
|
+
if (minStr.length !== maxStr.length) {
|
|
139
|
+
needsSwap = minStr.length > maxStr.length;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
needsSwap = minStr >= maxStr;
|
|
143
|
+
}
|
|
144
|
+
comparisonMethod = 'string_comparison_fallback';
|
|
145
|
+
logger.debug(`String comparison fallback: '${minStr}' >= '${maxStr}' = ${needsSwap}`, {
|
|
146
|
+
operation: 'normalizeMinMaxValues'
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// Method 3: Final validation using Decimal.js as last resort
|
|
150
|
+
if (!needsSwap && (typeof normalizedMin === 'string' || typeof normalizedMax === 'string')) {
|
|
151
|
+
try {
|
|
152
|
+
const minDecimal = new Decimal(normalizedMin.toString());
|
|
153
|
+
const maxDecimal = new Decimal(normalizedMax.toString());
|
|
154
|
+
needsSwap = minDecimal.greaterThanOrEqualTo(maxDecimal);
|
|
155
|
+
comparisonMethod = 'decimal_js_fallback';
|
|
156
|
+
logger.debug(`Decimal.js fallback: ${normalizedMin} >= ${normalizedMax} = ${needsSwap}`, {
|
|
157
|
+
operation: 'normalizeMinMaxValues'
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch (decimalError) {
|
|
161
|
+
logger.warn(`Decimal.js comparison failed for attribute '${attribute.key}': ${decimalError instanceof Error ? decimalError.message : String(decimalError)}`, {
|
|
162
|
+
operation: 'normalizeMinMaxValues'
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Log final validation result
|
|
167
|
+
if (needsSwap) {
|
|
168
|
+
logger.error(`Invalid min/max values detected for attribute '${attribute.key}': min (${normalizedMin}) must be less than max (${normalizedMax})`, {
|
|
169
|
+
type,
|
|
170
|
+
min: normalizedMin,
|
|
171
|
+
max: normalizedMax,
|
|
172
|
+
comparisonMethod,
|
|
173
|
+
operation: 'normalizeMinMaxValues'
|
|
174
|
+
});
|
|
175
|
+
// Swap values to ensure min < max (graceful handling)
|
|
176
|
+
logger.warn(`Swapping min/max values for attribute '${attribute.key}' to fix validation`, {
|
|
177
|
+
type,
|
|
178
|
+
originalMin: normalizedMin,
|
|
179
|
+
originalMax: normalizedMax,
|
|
180
|
+
newMin: normalizedMax,
|
|
181
|
+
newMax: normalizedMin,
|
|
182
|
+
comparisonMethod,
|
|
183
|
+
operation: 'normalizeMinMaxValues'
|
|
184
|
+
});
|
|
185
|
+
const temp = normalizedMin;
|
|
186
|
+
normalizedMin = normalizedMax;
|
|
187
|
+
normalizedMax = temp;
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
logger.debug(`Min/max validation passed for attribute '${attribute.key}'`, {
|
|
191
|
+
type,
|
|
192
|
+
min: normalizedMin,
|
|
193
|
+
max: normalizedMax,
|
|
194
|
+
comparisonMethod,
|
|
195
|
+
operation: 'normalizeMinMaxValues'
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
logger.error(`Critical error during min/max validation for attribute '${attribute.key}'`, {
|
|
201
|
+
type,
|
|
202
|
+
min: normalizedMin,
|
|
203
|
+
max: normalizedMax,
|
|
204
|
+
error: error instanceof Error ? error.message : String(error),
|
|
205
|
+
operation: 'normalizeMinMaxValues'
|
|
206
|
+
});
|
|
207
|
+
// If all comparison methods fail, set both to undefined to avoid API errors
|
|
208
|
+
normalizedMin = undefined;
|
|
209
|
+
normalizedMax = undefined;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
111
212
|
const result = { min: normalizedMin, max: normalizedMax };
|
|
112
213
|
logger.debug(`Min/max normalization complete for attribute '${attribute.key}'`, {
|
|
113
214
|
type,
|
|
114
215
|
result,
|
|
115
|
-
operation:
|
|
216
|
+
operation: "normalizeMinMaxValues",
|
|
116
217
|
});
|
|
117
218
|
return result;
|
|
118
219
|
};
|
|
@@ -122,6 +223,10 @@ const normalizeMinMaxValues = (attribute) => {
|
|
|
122
223
|
*/
|
|
123
224
|
const normalizeAttributeForComparison = (attribute) => {
|
|
124
225
|
const normalized = { ...attribute };
|
|
226
|
+
// Ignore defaults on required attributes to prevent false positives
|
|
227
|
+
if (normalized.required === true && "xdefault" in normalized) {
|
|
228
|
+
delete normalized.xdefault;
|
|
229
|
+
}
|
|
125
230
|
// Normalize min/max for numeric types
|
|
126
231
|
if (hasMinMaxProperties(attribute)) {
|
|
127
232
|
const { min, max } = normalizeMinMaxValues(attribute);
|
|
@@ -130,7 +235,8 @@ const normalizeAttributeForComparison = (attribute) => {
|
|
|
130
235
|
}
|
|
131
236
|
// Remove xdefault if null/undefined to ensure consistent comparison
|
|
132
237
|
// Appwrite sets xdefault: null for required attributes, but config files omit it
|
|
133
|
-
if (
|
|
238
|
+
if ("xdefault" in normalized &&
|
|
239
|
+
(normalized.xdefault === null || normalized.xdefault === undefined)) {
|
|
134
240
|
delete normalized.xdefault;
|
|
135
241
|
}
|
|
136
242
|
return normalized;
|
|
@@ -140,13 +246,13 @@ const normalizeAttributeForComparison = (attribute) => {
|
|
|
140
246
|
*/
|
|
141
247
|
const createAttributeViaAdapter = async (db, dbId, collectionId, attribute) => {
|
|
142
248
|
const startTime = Date.now();
|
|
143
|
-
const adapterType = isDatabaseAdapter(db) ?
|
|
249
|
+
const adapterType = isDatabaseAdapter(db) ? "adapter" : "legacy";
|
|
144
250
|
logger.info(`Creating attribute '${attribute.key}' via ${adapterType}`, {
|
|
145
251
|
type: attribute.type,
|
|
146
252
|
dbId,
|
|
147
253
|
collectionId,
|
|
148
254
|
adapterType,
|
|
149
|
-
operation:
|
|
255
|
+
operation: "createAttributeViaAdapter",
|
|
150
256
|
});
|
|
151
257
|
if (isDatabaseAdapter(db)) {
|
|
152
258
|
// Use the adapter's unified createAttribute method
|
|
@@ -158,38 +264,57 @@ const createAttributeViaAdapter = async (db, dbId, collectionId, attribute) => {
|
|
|
158
264
|
required: attribute.required || false,
|
|
159
265
|
array: attribute.array || false,
|
|
160
266
|
...(attribute.size && { size: attribute.size }),
|
|
161
|
-
...(attribute.xdefault !== undefined &&
|
|
162
|
-
|
|
163
|
-
...(attribute.
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
...(attribute.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
...(attribute.
|
|
170
|
-
|
|
267
|
+
...(attribute.xdefault !== undefined &&
|
|
268
|
+
!attribute.required && { default: attribute.xdefault }),
|
|
269
|
+
...(attribute.encrypted && {
|
|
270
|
+
encrypt: attribute.encrypted,
|
|
271
|
+
}),
|
|
272
|
+
...(attribute.min !== undefined && {
|
|
273
|
+
min: attribute.min,
|
|
274
|
+
}),
|
|
275
|
+
...(attribute.max !== undefined && {
|
|
276
|
+
max: attribute.max,
|
|
277
|
+
}),
|
|
278
|
+
...(attribute.elements && {
|
|
279
|
+
elements: attribute.elements,
|
|
280
|
+
}),
|
|
281
|
+
...(attribute.relatedCollection && {
|
|
282
|
+
relatedCollection: attribute.relatedCollection,
|
|
283
|
+
}),
|
|
284
|
+
...(attribute.relationType && {
|
|
285
|
+
relationType: attribute.relationType,
|
|
286
|
+
}),
|
|
287
|
+
...(attribute.twoWay !== undefined && {
|
|
288
|
+
twoWay: attribute.twoWay,
|
|
289
|
+
}),
|
|
290
|
+
...(attribute.onDelete && {
|
|
291
|
+
onDelete: attribute.onDelete,
|
|
292
|
+
}),
|
|
293
|
+
...(attribute.twoWayKey && {
|
|
294
|
+
twoWayKey: attribute.twoWayKey,
|
|
295
|
+
}),
|
|
171
296
|
};
|
|
172
297
|
logger.debug(`Adapter create parameters for '${attribute.key}'`, {
|
|
173
298
|
params,
|
|
174
|
-
operation:
|
|
299
|
+
operation: "createAttributeViaAdapter",
|
|
175
300
|
});
|
|
176
301
|
await db.createAttribute(params);
|
|
177
302
|
const duration = Date.now() - startTime;
|
|
178
303
|
logger.info(`Successfully created attribute '${attribute.key}' via adapter`, {
|
|
179
304
|
duration,
|
|
180
|
-
operation:
|
|
305
|
+
operation: "createAttributeViaAdapter",
|
|
181
306
|
});
|
|
182
307
|
}
|
|
183
308
|
else {
|
|
184
309
|
// Use legacy type-specific methods
|
|
185
310
|
logger.debug(`Using legacy creation for attribute '${attribute.key}'`, {
|
|
186
|
-
operation:
|
|
311
|
+
operation: "createAttributeViaAdapter",
|
|
187
312
|
});
|
|
188
313
|
await createLegacyAttribute(db, dbId, collectionId, attribute);
|
|
189
314
|
const duration = Date.now() - startTime;
|
|
190
315
|
logger.info(`Successfully created attribute '${attribute.key}' via legacy`, {
|
|
191
316
|
duration,
|
|
192
|
-
operation:
|
|
317
|
+
operation: "createAttributeViaAdapter",
|
|
193
318
|
});
|
|
194
319
|
}
|
|
195
320
|
};
|
|
@@ -203,9 +328,23 @@ const updateAttributeViaAdapter = async (db, dbId, collectionId, attribute) => {
|
|
|
203
328
|
databaseId: dbId,
|
|
204
329
|
tableId: collectionId,
|
|
205
330
|
key: attribute.key,
|
|
331
|
+
type: attribute.type,
|
|
206
332
|
required: attribute.required || false,
|
|
207
|
-
|
|
333
|
+
array: attribute.array || false,
|
|
334
|
+
size: attribute.size,
|
|
335
|
+
min: attribute.min,
|
|
336
|
+
max: attribute.max,
|
|
337
|
+
encrypt: attribute.encrypted ?? attribute.encrypt,
|
|
338
|
+
elements: attribute.elements,
|
|
339
|
+
relatedCollection: attribute.relatedCollection,
|
|
340
|
+
relationType: attribute.relationType,
|
|
341
|
+
twoWay: attribute.twoWay,
|
|
342
|
+
twoWayKey: attribute.twoWayKey,
|
|
343
|
+
onDelete: attribute.onDelete
|
|
208
344
|
};
|
|
345
|
+
if (!attribute.required && attribute.xdefault !== undefined) {
|
|
346
|
+
params.default = attribute.xdefault;
|
|
347
|
+
}
|
|
209
348
|
await db.updateAttribute(params);
|
|
210
349
|
}
|
|
211
350
|
else {
|
|
@@ -225,58 +364,80 @@ const createLegacyAttribute = async (db, dbId, collectionId, attribute) => {
|
|
|
225
364
|
collectionId,
|
|
226
365
|
normalizedMin,
|
|
227
366
|
normalizedMax,
|
|
228
|
-
operation:
|
|
367
|
+
operation: "createLegacyAttribute",
|
|
229
368
|
});
|
|
230
369
|
switch (attribute.type) {
|
|
231
370
|
case "string":
|
|
232
371
|
const stringParams = {
|
|
233
372
|
size: attribute.size || 255,
|
|
234
373
|
required: attribute.required || false,
|
|
235
|
-
defaultValue: attribute.xdefault !== undefined && !attribute.required
|
|
374
|
+
defaultValue: attribute.xdefault !== undefined && !attribute.required
|
|
375
|
+
? attribute.xdefault
|
|
376
|
+
: undefined,
|
|
236
377
|
array: attribute.array || false,
|
|
237
|
-
encrypted: attribute.encrypted
|
|
378
|
+
encrypted: attribute.encrypted,
|
|
238
379
|
};
|
|
239
380
|
logger.debug(`Creating string attribute '${attribute.key}'`, {
|
|
240
381
|
...stringParams,
|
|
241
|
-
operation:
|
|
382
|
+
operation: "createLegacyAttribute",
|
|
242
383
|
});
|
|
243
384
|
await db.createStringAttribute(dbId, collectionId, attribute.key, stringParams.size, stringParams.required, stringParams.defaultValue, stringParams.array, stringParams.encrypted);
|
|
244
385
|
break;
|
|
245
386
|
case "integer":
|
|
246
387
|
const integerParams = {
|
|
247
388
|
required: attribute.required || false,
|
|
248
|
-
min: normalizedMin !== undefined
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
389
|
+
min: normalizedMin !== undefined
|
|
390
|
+
? parseInt(String(normalizedMin))
|
|
391
|
+
: undefined,
|
|
392
|
+
max: normalizedMax !== undefined
|
|
393
|
+
? parseInt(String(normalizedMax))
|
|
394
|
+
: undefined,
|
|
395
|
+
defaultValue: attribute.xdefault !== undefined && !attribute.required
|
|
396
|
+
? attribute.xdefault
|
|
397
|
+
: undefined,
|
|
398
|
+
array: attribute.array || false,
|
|
252
399
|
};
|
|
253
400
|
logger.debug(`Creating integer attribute '${attribute.key}'`, {
|
|
254
401
|
...integerParams,
|
|
255
|
-
operation:
|
|
402
|
+
operation: "createLegacyAttribute",
|
|
256
403
|
});
|
|
257
404
|
await db.createIntegerAttribute(dbId, collectionId, attribute.key, integerParams.required, integerParams.min, integerParams.max, integerParams.defaultValue, integerParams.array);
|
|
258
405
|
break;
|
|
259
406
|
case "double":
|
|
260
407
|
case "float":
|
|
261
|
-
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
|
|
408
|
+
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
|
|
409
|
+
? attribute.xdefault
|
|
410
|
+
: undefined, attribute.array || false);
|
|
262
411
|
break;
|
|
263
412
|
case "boolean":
|
|
264
|
-
await db.createBooleanAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required
|
|
413
|
+
await db.createBooleanAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required
|
|
414
|
+
? attribute.xdefault
|
|
415
|
+
: undefined, attribute.array || false);
|
|
265
416
|
break;
|
|
266
417
|
case "datetime":
|
|
267
|
-
await db.createDatetimeAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required
|
|
418
|
+
await db.createDatetimeAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required
|
|
419
|
+
? attribute.xdefault
|
|
420
|
+
: undefined, attribute.array || false);
|
|
268
421
|
break;
|
|
269
422
|
case "email":
|
|
270
|
-
await db.createEmailAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required
|
|
423
|
+
await db.createEmailAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required
|
|
424
|
+
? attribute.xdefault
|
|
425
|
+
: undefined, attribute.array || false);
|
|
271
426
|
break;
|
|
272
427
|
case "ip":
|
|
273
|
-
await db.createIpAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required
|
|
428
|
+
await db.createIpAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required
|
|
429
|
+
? attribute.xdefault
|
|
430
|
+
: undefined, attribute.array || false);
|
|
274
431
|
break;
|
|
275
432
|
case "url":
|
|
276
|
-
await db.createUrlAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required
|
|
433
|
+
await db.createUrlAttribute(dbId, collectionId, attribute.key, attribute.required || false, attribute.xdefault !== undefined && !attribute.required
|
|
434
|
+
? attribute.xdefault
|
|
435
|
+
: undefined, attribute.array || false);
|
|
277
436
|
break;
|
|
278
437
|
case "enum":
|
|
279
|
-
await db.createEnumAttribute(dbId, collectionId, attribute.key, attribute.elements || [], attribute.required || false, attribute.xdefault !== undefined && !attribute.required
|
|
438
|
+
await db.createEnumAttribute(dbId, collectionId, attribute.key, attribute.elements || [], attribute.required || false, attribute.xdefault !== undefined && !attribute.required
|
|
439
|
+
? attribute.xdefault
|
|
440
|
+
: undefined, attribute.array || false);
|
|
280
441
|
break;
|
|
281
442
|
case "relationship":
|
|
282
443
|
await db.createRelationshipAttribute(dbId, collectionId, attribute.relatedCollection, attribute.relationType, attribute.twoWay, attribute.key, attribute.twoWayKey, attribute.onDelete);
|
|
@@ -285,8 +446,20 @@ const createLegacyAttribute = async (db, dbId, collectionId, attribute) => {
|
|
|
285
446
|
const error = new Error(`Unsupported attribute type: ${attribute.type}`);
|
|
286
447
|
logger.error(`Unsupported attribute type for '${attribute.key}'`, {
|
|
287
448
|
type: attribute.type,
|
|
288
|
-
supportedTypes: [
|
|
289
|
-
|
|
449
|
+
supportedTypes: [
|
|
450
|
+
"string",
|
|
451
|
+
"integer",
|
|
452
|
+
"double",
|
|
453
|
+
"float",
|
|
454
|
+
"boolean",
|
|
455
|
+
"datetime",
|
|
456
|
+
"email",
|
|
457
|
+
"ip",
|
|
458
|
+
"url",
|
|
459
|
+
"enum",
|
|
460
|
+
"relationship",
|
|
461
|
+
],
|
|
462
|
+
operation: "createLegacyAttribute",
|
|
290
463
|
});
|
|
291
464
|
throw error;
|
|
292
465
|
}
|
|
@@ -294,42 +467,72 @@ const createLegacyAttribute = async (db, dbId, collectionId, attribute) => {
|
|
|
294
467
|
logger.info(`Successfully created legacy attribute '${attribute.key}'`, {
|
|
295
468
|
type: attribute.type,
|
|
296
469
|
duration,
|
|
297
|
-
operation:
|
|
470
|
+
operation: "createLegacyAttribute",
|
|
298
471
|
});
|
|
299
472
|
};
|
|
300
473
|
/**
|
|
301
474
|
* Legacy attribute update using type-specific methods
|
|
302
475
|
*/
|
|
303
476
|
const updateLegacyAttribute = async (db, dbId, collectionId, attribute) => {
|
|
477
|
+
console.log(`DEBUG updateLegacyAttribute before normalizeMinMaxValues:`, {
|
|
478
|
+
key: attribute.key,
|
|
479
|
+
type: attribute.type,
|
|
480
|
+
min: attribute.min,
|
|
481
|
+
max: attribute.max
|
|
482
|
+
});
|
|
304
483
|
const { min: normalizedMin, max: normalizedMax } = normalizeMinMaxValues(attribute);
|
|
305
484
|
switch (attribute.type) {
|
|
306
485
|
case "string":
|
|
307
|
-
await db.updateStringAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined
|
|
486
|
+
await db.updateStringAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined
|
|
487
|
+
? attribute.xdefault
|
|
488
|
+
: null, attribute.size);
|
|
308
489
|
break;
|
|
309
490
|
case "integer":
|
|
310
|
-
await db.updateIntegerAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined
|
|
491
|
+
await db.updateIntegerAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined
|
|
492
|
+
? attribute.xdefault
|
|
493
|
+
: null, normalizedMin !== undefined
|
|
494
|
+
? parseInt(String(normalizedMin))
|
|
495
|
+
: undefined, normalizedMax !== undefined
|
|
496
|
+
? parseInt(String(normalizedMax))
|
|
497
|
+
: undefined);
|
|
311
498
|
break;
|
|
312
499
|
case "double":
|
|
313
500
|
case "float":
|
|
314
|
-
|
|
501
|
+
const minParam = normalizedMin !== undefined ? Number(normalizedMin) : undefined;
|
|
502
|
+
const maxParam = normalizedMax !== undefined ? Number(normalizedMax) : undefined;
|
|
503
|
+
await db.updateFloatAttribute(dbId, collectionId, attribute.key, attribute.required || false, minParam, maxParam, !attribute.required && attribute.xdefault !== undefined
|
|
504
|
+
? attribute.xdefault
|
|
505
|
+
: null);
|
|
315
506
|
break;
|
|
316
507
|
case "boolean":
|
|
317
|
-
await db.updateBooleanAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined
|
|
508
|
+
await db.updateBooleanAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined
|
|
509
|
+
? attribute.xdefault
|
|
510
|
+
: null);
|
|
318
511
|
break;
|
|
319
512
|
case "datetime":
|
|
320
|
-
await db.updateDatetimeAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined
|
|
513
|
+
await db.updateDatetimeAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined
|
|
514
|
+
? attribute.xdefault
|
|
515
|
+
: null);
|
|
321
516
|
break;
|
|
322
517
|
case "email":
|
|
323
|
-
await db.updateEmailAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined
|
|
518
|
+
await db.updateEmailAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined
|
|
519
|
+
? attribute.xdefault
|
|
520
|
+
: null);
|
|
324
521
|
break;
|
|
325
522
|
case "ip":
|
|
326
|
-
await db.updateIpAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined
|
|
523
|
+
await db.updateIpAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined
|
|
524
|
+
? attribute.xdefault
|
|
525
|
+
: null);
|
|
327
526
|
break;
|
|
328
527
|
case "url":
|
|
329
|
-
await db.updateUrlAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined
|
|
528
|
+
await db.updateUrlAttribute(dbId, collectionId, attribute.key, attribute.required || false, !attribute.required && attribute.xdefault !== undefined
|
|
529
|
+
? attribute.xdefault
|
|
530
|
+
: null);
|
|
330
531
|
break;
|
|
331
532
|
case "enum":
|
|
332
|
-
await db.updateEnumAttribute(dbId, collectionId, attribute.key, attribute.elements || [], attribute.required || false, !attribute.required && attribute.xdefault !== undefined
|
|
533
|
+
await db.updateEnumAttribute(dbId, collectionId, attribute.key, attribute.elements || [], attribute.required || false, !attribute.required && attribute.xdefault !== undefined
|
|
534
|
+
? attribute.xdefault
|
|
535
|
+
: null);
|
|
333
536
|
break;
|
|
334
537
|
case "relationship":
|
|
335
538
|
await db.updateRelationshipAttribute(dbId, collectionId, attribute.key, attribute.onDelete);
|
|
@@ -351,7 +554,7 @@ retryCount = 0, maxRetries = 5) => {
|
|
|
351
554
|
maxWaitTime,
|
|
352
555
|
retryCount,
|
|
353
556
|
maxRetries,
|
|
354
|
-
operation:
|
|
557
|
+
operation: "waitForAttributeAvailable",
|
|
355
558
|
});
|
|
356
559
|
// Calculate exponential backoff: 2s, 4s, 8s, 16s, 30s (capped at 30s)
|
|
357
560
|
if (retryCount > 0) {
|
|
@@ -375,7 +578,7 @@ retryCount = 0, maxRetries = 5) => {
|
|
|
375
578
|
dbId,
|
|
376
579
|
collectionId,
|
|
377
580
|
waitTime: Date.now() - startTime,
|
|
378
|
-
operation:
|
|
581
|
+
operation: "waitForAttributeAvailable",
|
|
379
582
|
};
|
|
380
583
|
switch (attribute.status) {
|
|
381
584
|
case "available":
|
|
@@ -405,13 +608,13 @@ retryCount = 0, maxRetries = 5) => {
|
|
|
405
608
|
catch (error) {
|
|
406
609
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
407
610
|
MessageFormatter.error(`Error checking attribute status: ${errorMessage}`);
|
|
408
|
-
logger.error(
|
|
611
|
+
logger.error("Error checking attribute status", {
|
|
409
612
|
attributeKey,
|
|
410
613
|
dbId,
|
|
411
614
|
collectionId,
|
|
412
615
|
error: errorMessage,
|
|
413
616
|
waitTime: Date.now() - startTime,
|
|
414
|
-
operation:
|
|
617
|
+
operation: "waitForAttributeAvailable",
|
|
415
618
|
});
|
|
416
619
|
return false;
|
|
417
620
|
}
|
|
@@ -464,7 +667,7 @@ const deleteAndRecreateCollection = async (db, dbId, collection, retryCount) =>
|
|
|
464
667
|
name: collection.name,
|
|
465
668
|
permissions: collection.$permissions,
|
|
466
669
|
documentSecurity: collection.documentSecurity,
|
|
467
|
-
enabled: collection.enabled
|
|
670
|
+
enabled: collection.enabled,
|
|
468
671
|
})).data
|
|
469
672
|
: await db.createCollection(dbId, collection.$id, collection.name, collection.$permissions, collection.documentSecurity, collection.enabled);
|
|
470
673
|
MessageFormatter.success(`✅ Recreated collection '${collection.name}'`);
|
|
@@ -491,7 +694,14 @@ const getComparableFields = (type) => {
|
|
|
491
694
|
case "enum":
|
|
492
695
|
return [...baseFields, "elements"];
|
|
493
696
|
case "relationship":
|
|
494
|
-
return [
|
|
697
|
+
return [
|
|
698
|
+
...baseFields,
|
|
699
|
+
"relationType",
|
|
700
|
+
"twoWay",
|
|
701
|
+
"twoWayKey",
|
|
702
|
+
"onDelete",
|
|
703
|
+
"relatedCollection",
|
|
704
|
+
];
|
|
495
705
|
case "boolean":
|
|
496
706
|
case "datetime":
|
|
497
707
|
case "email":
|
|
@@ -501,9 +711,21 @@ const getComparableFields = (type) => {
|
|
|
501
711
|
default:
|
|
502
712
|
// Fallback to all fields for unknown types
|
|
503
713
|
return [
|
|
504
|
-
"key",
|
|
505
|
-
"
|
|
506
|
-
"
|
|
714
|
+
"key",
|
|
715
|
+
"type",
|
|
716
|
+
"array",
|
|
717
|
+
"encrypted",
|
|
718
|
+
"required",
|
|
719
|
+
"size",
|
|
720
|
+
"min",
|
|
721
|
+
"max",
|
|
722
|
+
"xdefault",
|
|
723
|
+
"elements",
|
|
724
|
+
"relationType",
|
|
725
|
+
"twoWay",
|
|
726
|
+
"twoWayKey",
|
|
727
|
+
"onDelete",
|
|
728
|
+
"relatedCollection",
|
|
507
729
|
];
|
|
508
730
|
}
|
|
509
731
|
};
|
|
@@ -513,8 +735,16 @@ const attributesSame = (databaseAttribute, configAttribute) => {
|
|
|
513
735
|
const normalizedConfigAttr = normalizeAttributeForComparison(configAttribute);
|
|
514
736
|
// Use type-specific field list to avoid false positives from irrelevant fields
|
|
515
737
|
const attributesToCheck = getComparableFields(normalizedConfigAttr.type);
|
|
738
|
+
const fieldsToCheck = attributesToCheck.filter((attr) => {
|
|
739
|
+
if (attr !== "xdefault") {
|
|
740
|
+
return true;
|
|
741
|
+
}
|
|
742
|
+
const dbRequired = Boolean(normalizedDbAttr.required);
|
|
743
|
+
const configRequired = Boolean(normalizedConfigAttr.required);
|
|
744
|
+
return !(dbRequired || configRequired);
|
|
745
|
+
});
|
|
516
746
|
const differences = [];
|
|
517
|
-
const result =
|
|
747
|
+
const result = fieldsToCheck.every((attr) => {
|
|
518
748
|
// Check if both objects have the attribute
|
|
519
749
|
const dbHasAttr = attr in normalizedDbAttr;
|
|
520
750
|
const configHasAttr = attr in normalizedConfigAttr;
|
|
@@ -536,8 +766,14 @@ const attributesSame = (databaseAttribute, configAttribute) => {
|
|
|
536
766
|
return boolMatch;
|
|
537
767
|
}
|
|
538
768
|
// For numeric comparisons, compare numbers if both are numeric-like
|
|
539
|
-
if ((typeof dbValue === "number" ||
|
|
540
|
-
(typeof
|
|
769
|
+
if ((typeof dbValue === "number" ||
|
|
770
|
+
(typeof dbValue === "string" &&
|
|
771
|
+
dbValue !== "" &&
|
|
772
|
+
!isNaN(Number(dbValue)))) &&
|
|
773
|
+
(typeof configValue === "number" ||
|
|
774
|
+
(typeof configValue === "string" &&
|
|
775
|
+
configValue !== "" &&
|
|
776
|
+
!isNaN(Number(configValue))))) {
|
|
541
777
|
const numMatch = Number(dbValue) === Number(configValue);
|
|
542
778
|
if (!numMatch) {
|
|
543
779
|
differences.push(`${attr}: db=${dbValue} config=${configValue}`);
|
|
@@ -600,18 +836,12 @@ const attributesSame = (databaseAttribute, configAttribute) => {
|
|
|
600
836
|
differences.push(`${attr}: unexpected comparison state`);
|
|
601
837
|
return false;
|
|
602
838
|
});
|
|
603
|
-
// Log differences if any were found
|
|
604
|
-
if (differences.length > 0) {
|
|
605
|
-
logger.debug(`Attribute '${normalizedDbAttr.key}' comparison found differences:`, {
|
|
606
|
-
differences,
|
|
607
|
-
operation: 'attributesSame'
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
// Log differences if comparison failed (for debugging)
|
|
611
839
|
if (!result && differences.length > 0) {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
840
|
+
logger.debug(`Attribute mismatch detected for '${normalizedConfigAttr.key}'`, {
|
|
841
|
+
differences,
|
|
842
|
+
dbAttribute: normalizedDbAttr,
|
|
843
|
+
configAttribute: normalizedConfigAttr,
|
|
844
|
+
operation: "attributesSame",
|
|
615
845
|
});
|
|
616
846
|
}
|
|
617
847
|
return result;
|
|
@@ -646,7 +876,11 @@ export const createOrUpdateAttributeWithStatusCheck = async (db, dbId, collectio
|
|
|
646
876
|
// Try to delete the specific stuck attribute instead of the entire collection
|
|
647
877
|
try {
|
|
648
878
|
if (isDatabaseAdapter(db)) {
|
|
649
|
-
await db.deleteAttribute({
|
|
879
|
+
await db.deleteAttribute({
|
|
880
|
+
databaseId: dbId,
|
|
881
|
+
tableId: collection.$id,
|
|
882
|
+
key: attribute.key,
|
|
883
|
+
});
|
|
650
884
|
}
|
|
651
885
|
else {
|
|
652
886
|
await db.deleteAttribute(dbId, collection.$id, attribute.key);
|
|
@@ -656,7 +890,8 @@ export const createOrUpdateAttributeWithStatusCheck = async (db, dbId, collectio
|
|
|
656
890
|
await delay(3000);
|
|
657
891
|
// Get fresh collection data
|
|
658
892
|
const freshCollection = isDatabaseAdapter(db)
|
|
659
|
-
? (await db.getTable({ databaseId: dbId, tableId: collection.$id }))
|
|
893
|
+
? (await db.getTable({ databaseId: dbId, tableId: collection.$id }))
|
|
894
|
+
.data
|
|
660
895
|
: await db.getCollection(dbId, collection.$id);
|
|
661
896
|
// Retry with the same collection (attribute should be gone now)
|
|
662
897
|
return await createOrUpdateAttributeWithStatusCheck(db, dbId, freshCollection, attribute, retryCount + 1, maxRetries);
|
|
@@ -668,7 +903,8 @@ export const createOrUpdateAttributeWithStatusCheck = async (db, dbId, collectio
|
|
|
668
903
|
MessageFormatter.info(chalk.yellow(`Last resort: Recreating collection for attribute '${attribute.key}'`));
|
|
669
904
|
// Get fresh collection data
|
|
670
905
|
const freshCollection = isDatabaseAdapter(db)
|
|
671
|
-
? (await db.getTable({ databaseId: dbId, tableId: collection.$id }))
|
|
906
|
+
? (await db.getTable({ databaseId: dbId, tableId: collection.$id }))
|
|
907
|
+
.data
|
|
672
908
|
: await db.getCollection(dbId, collection.$id);
|
|
673
909
|
// Delete and recreate collection
|
|
674
910
|
const newCollection = await deleteAndRecreateCollection(db, dbId, freshCollection, retryCount + 1);
|
|
@@ -709,6 +945,30 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
|
709
945
|
catch (error) {
|
|
710
946
|
foundAttribute = undefined;
|
|
711
947
|
}
|
|
948
|
+
// If attribute exists but type changed, delete it so we can recreate with new type
|
|
949
|
+
if (foundAttribute &&
|
|
950
|
+
foundAttribute.type !== attribute.type) {
|
|
951
|
+
MessageFormatter.info(chalk.yellow(`Attribute '${attribute.key}' type changed from '${foundAttribute.type}' to '${attribute.type}'. Recreating attribute.`));
|
|
952
|
+
try {
|
|
953
|
+
if (isDatabaseAdapter(db)) {
|
|
954
|
+
await db.deleteAttribute({
|
|
955
|
+
databaseId: dbId,
|
|
956
|
+
tableId: collection.$id,
|
|
957
|
+
key: attribute.key
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
else {
|
|
961
|
+
await db.deleteAttribute(dbId, collection.$id, attribute.key);
|
|
962
|
+
}
|
|
963
|
+
// Remove from local collection metadata so downstream logic treats it as new
|
|
964
|
+
collection.attributes = collection.attributes.filter((attr) => attr.key !== attribute.key);
|
|
965
|
+
foundAttribute = undefined;
|
|
966
|
+
}
|
|
967
|
+
catch (deleteError) {
|
|
968
|
+
MessageFormatter.error(`Failed to delete attribute '${attribute.key}' before recreation: ${deleteError}`);
|
|
969
|
+
return "error";
|
|
970
|
+
}
|
|
971
|
+
}
|
|
712
972
|
if (foundAttribute &&
|
|
713
973
|
attributesSame(foundAttribute, attribute) &&
|
|
714
974
|
updateEnabled) {
|
|
@@ -721,17 +981,46 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
|
721
981
|
// MessageFormatter.info(
|
|
722
982
|
// `Updating attribute with same key ${attribute.key} but different values`
|
|
723
983
|
// );
|
|
984
|
+
// DEBUG: Log before object merge to detect corruption
|
|
985
|
+
if ((attribute.key === 'conversationType' || attribute.key === 'messageStreakCount')) {
|
|
986
|
+
console.log(`[DEBUG] MERGE - key="${attribute.key}"`, {
|
|
987
|
+
found: {
|
|
988
|
+
elements: foundAttribute?.elements,
|
|
989
|
+
min: foundAttribute?.min,
|
|
990
|
+
max: foundAttribute?.max
|
|
991
|
+
},
|
|
992
|
+
desired: {
|
|
993
|
+
elements: attribute?.elements,
|
|
994
|
+
min: attribute?.min,
|
|
995
|
+
max: attribute?.max
|
|
996
|
+
}
|
|
997
|
+
});
|
|
998
|
+
}
|
|
724
999
|
finalAttribute = {
|
|
725
1000
|
...foundAttribute,
|
|
726
1001
|
...attribute,
|
|
727
1002
|
};
|
|
1003
|
+
// DEBUG: Log after object merge to detect corruption
|
|
1004
|
+
if ((finalAttribute.key === 'conversationType' || finalAttribute.key === 'messageStreakCount')) {
|
|
1005
|
+
console.log(`[DEBUG] AFTER_MERGE - key="${finalAttribute.key}"`, {
|
|
1006
|
+
merged: {
|
|
1007
|
+
elements: finalAttribute?.elements,
|
|
1008
|
+
min: finalAttribute?.min,
|
|
1009
|
+
max: finalAttribute?.max
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
728
1013
|
action = "update";
|
|
729
1014
|
}
|
|
730
1015
|
else if (!updateEnabled &&
|
|
731
1016
|
foundAttribute &&
|
|
732
1017
|
!attributesSame(foundAttribute, attribute)) {
|
|
733
1018
|
if (isDatabaseAdapter(db)) {
|
|
734
|
-
await db.deleteAttribute({
|
|
1019
|
+
await db.deleteAttribute({
|
|
1020
|
+
databaseId: dbId,
|
|
1021
|
+
tableId: collection.$id,
|
|
1022
|
+
key: attribute.key,
|
|
1023
|
+
});
|
|
735
1024
|
}
|
|
736
1025
|
else {
|
|
737
1026
|
await db.deleteAttribute(dbId, collection.$id, attribute.key);
|
|
@@ -747,7 +1036,10 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
|
747
1036
|
// First try treating relatedCollection as an ID directly
|
|
748
1037
|
try {
|
|
749
1038
|
const byIdCollection = isDatabaseAdapter(db)
|
|
750
|
-
? (await db.getTable({
|
|
1039
|
+
? (await db.getTable({
|
|
1040
|
+
databaseId: dbId,
|
|
1041
|
+
tableId: finalAttribute.relatedCollection,
|
|
1042
|
+
})).data
|
|
751
1043
|
: await db.getCollection(dbId, finalAttribute.relatedCollection);
|
|
752
1044
|
collectionFoundViaRelatedCollection = byIdCollection;
|
|
753
1045
|
relatedCollectionId = byIdCollection.$id;
|
|
@@ -757,11 +1049,15 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
|
757
1049
|
catch (_) {
|
|
758
1050
|
// Not an ID or not found — fall back to name-based resolution below
|
|
759
1051
|
}
|
|
760
|
-
if (!collectionFoundViaRelatedCollection &&
|
|
1052
|
+
if (!collectionFoundViaRelatedCollection &&
|
|
1053
|
+
nameToIdMapping.has(finalAttribute.relatedCollection)) {
|
|
761
1054
|
relatedCollectionId = nameToIdMapping.get(finalAttribute.relatedCollection);
|
|
762
1055
|
try {
|
|
763
1056
|
collectionFoundViaRelatedCollection = isDatabaseAdapter(db)
|
|
764
|
-
? (await db.getTable({
|
|
1057
|
+
? (await db.getTable({
|
|
1058
|
+
databaseId: dbId,
|
|
1059
|
+
tableId: relatedCollectionId,
|
|
1060
|
+
})).data
|
|
765
1061
|
: await db.getCollection(dbId, relatedCollectionId);
|
|
766
1062
|
}
|
|
767
1063
|
catch (e) {
|
|
@@ -773,8 +1069,13 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
|
773
1069
|
}
|
|
774
1070
|
else if (!collectionFoundViaRelatedCollection) {
|
|
775
1071
|
const collectionsPulled = isDatabaseAdapter(db)
|
|
776
|
-
? await db.listTables({
|
|
777
|
-
|
|
1072
|
+
? await db.listTables({
|
|
1073
|
+
databaseId: dbId,
|
|
1074
|
+
queries: [Query.equal("name", finalAttribute.relatedCollection)],
|
|
1075
|
+
})
|
|
1076
|
+
: await db.listCollections(dbId, [
|
|
1077
|
+
Query.equal("name", finalAttribute.relatedCollection),
|
|
1078
|
+
]);
|
|
778
1079
|
if (collectionsPulled.total && collectionsPulled.total > 0) {
|
|
779
1080
|
collectionFoundViaRelatedCollection = isDatabaseAdapter(db)
|
|
780
1081
|
? collectionsPulled.tables?.[0]
|
|
@@ -808,8 +1109,9 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
|
808
1109
|
catch (error) {
|
|
809
1110
|
// Collection doesn't exist - create it
|
|
810
1111
|
if (error.code === 404 ||
|
|
811
|
-
(error instanceof Error &&
|
|
812
|
-
error.message.includes(
|
|
1112
|
+
(error instanceof Error &&
|
|
1113
|
+
(error.message.includes("collection_not_found") ||
|
|
1114
|
+
error.message.includes("Collection with the requested ID could not be found")))) {
|
|
813
1115
|
MessageFormatter.info(`Collection '${collection.name}' doesn't exist, creating it first...`);
|
|
814
1116
|
try {
|
|
815
1117
|
if (isDatabaseAdapter(db)) {
|
|
@@ -819,7 +1121,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
|
819
1121
|
name: collection.name,
|
|
820
1122
|
permissions: collection.$permissions || [],
|
|
821
1123
|
documentSecurity: collection.documentSecurity ?? false,
|
|
822
|
-
enabled: collection.enabled ?? true
|
|
1124
|
+
enabled: collection.enabled ?? true,
|
|
823
1125
|
});
|
|
824
1126
|
}
|
|
825
1127
|
else {
|
|
@@ -829,7 +1131,9 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
|
829
1131
|
await delay(500); // Wait for collection to be ready
|
|
830
1132
|
}
|
|
831
1133
|
catch (createError) {
|
|
832
|
-
MessageFormatter.error(`Failed to create collection '${collection.name}'`, createError instanceof Error
|
|
1134
|
+
MessageFormatter.error(`Failed to create collection '${collection.name}'`, createError instanceof Error
|
|
1135
|
+
? createError
|
|
1136
|
+
: new Error(String(createError)));
|
|
833
1137
|
return "error";
|
|
834
1138
|
}
|
|
835
1139
|
}
|
|
@@ -843,6 +1147,10 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
|
843
1147
|
await tryAwaitWithRetry(async () => await createAttributeViaAdapter(db, dbId, collection.$id, finalAttribute));
|
|
844
1148
|
}
|
|
845
1149
|
else {
|
|
1150
|
+
console.log(`Updating attribute '${finalAttribute.key}'...`);
|
|
1151
|
+
if (finalAttribute.type === "double" || finalAttribute.type === "integer") {
|
|
1152
|
+
console.log("finalAttribute:", finalAttribute);
|
|
1153
|
+
}
|
|
846
1154
|
await tryAwaitWithRetry(async () => await updateAttributeViaAdapter(db, dbId, collection.$id, finalAttribute));
|
|
847
1155
|
}
|
|
848
1156
|
return "processed";
|
|
@@ -851,9 +1159,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
|
851
1159
|
* Enhanced collection attribute creation with proper status monitoring
|
|
852
1160
|
*/
|
|
853
1161
|
export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId, collection, attributes) => {
|
|
854
|
-
const existingAttributes =
|
|
855
|
-
// @ts-expect-error
|
|
856
|
-
collection.attributes.map((attr) => parseAttribute(attr)) || [];
|
|
1162
|
+
const existingAttributes = collection.attributes.map((attr) => parseAttribute(attr)) || [];
|
|
857
1163
|
const attributesToRemove = existingAttributes.filter((attr) => !attributes.some((a) => a.key === attr.key));
|
|
858
1164
|
const indexesToRemove = collection.indexes.filter((index) => attributesToRemove.some((attr) => index.attributes.includes(attr.key)));
|
|
859
1165
|
// Handle attribute removal first
|
|
@@ -865,7 +1171,11 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
|
|
|
865
1171
|
for (const index of indexesToRemove) {
|
|
866
1172
|
await tryAwaitWithRetry(async () => {
|
|
867
1173
|
if (isDatabaseAdapter(db)) {
|
|
868
|
-
await db.deleteIndex({
|
|
1174
|
+
await db.deleteIndex({
|
|
1175
|
+
databaseId: dbId,
|
|
1176
|
+
tableId: collection.$id,
|
|
1177
|
+
key: index.key,
|
|
1178
|
+
});
|
|
869
1179
|
}
|
|
870
1180
|
else {
|
|
871
1181
|
await db.deleteIndex(dbId, collection.$id, index.key);
|
|
@@ -878,7 +1188,11 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
|
|
|
878
1188
|
MessageFormatter.info(chalk.red(`Removing attribute: ${attr.key} as it is no longer in the collection`));
|
|
879
1189
|
await tryAwaitWithRetry(async () => {
|
|
880
1190
|
if (isDatabaseAdapter(db)) {
|
|
881
|
-
await db.deleteAttribute({
|
|
1191
|
+
await db.deleteAttribute({
|
|
1192
|
+
databaseId: dbId,
|
|
1193
|
+
tableId: collection.$id,
|
|
1194
|
+
key: attr.key,
|
|
1195
|
+
});
|
|
882
1196
|
}
|
|
883
1197
|
else {
|
|
884
1198
|
await db.deleteAttribute(dbId, collection.$id, attr.key);
|
|
@@ -899,9 +1213,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
|
|
|
899
1213
|
}
|
|
900
1214
|
const existingAttributesMap = new Map();
|
|
901
1215
|
try {
|
|
902
|
-
const parsedAttributes = currentCollection.attributes.map((attr) =>
|
|
903
|
-
// @ts-expect-error
|
|
904
|
-
parseAttribute(attr));
|
|
1216
|
+
const parsedAttributes = currentCollection.attributes.map((attr) => parseAttribute(attr));
|
|
905
1217
|
parsedAttributes.forEach((attr) => existingAttributesMap.set(attr.key, attr));
|
|
906
1218
|
}
|
|
907
1219
|
catch (error) {
|
|
@@ -946,7 +1258,10 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
|
|
|
946
1258
|
try {
|
|
947
1259
|
currentCollection = isDatabaseAdapter(db)
|
|
948
1260
|
? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data
|
|
949
|
-
: await db.getCollection(
|
|
1261
|
+
: await db.getCollection({
|
|
1262
|
+
databaseId: dbId,
|
|
1263
|
+
collectionId: collection.$id,
|
|
1264
|
+
});
|
|
950
1265
|
}
|
|
951
1266
|
catch (error) {
|
|
952
1267
|
MessageFormatter.info(chalk.yellow(`Warning: Could not refresh collection data: ${error}`));
|
|
@@ -969,7 +1284,8 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
|
|
|
969
1284
|
// Refresh collection data before retry
|
|
970
1285
|
try {
|
|
971
1286
|
currentCollection = isDatabaseAdapter(db)
|
|
972
|
-
? (await db.getTable({ databaseId: dbId, tableId: collection.$id }))
|
|
1287
|
+
? (await db.getTable({ databaseId: dbId, tableId: collection.$id }))
|
|
1288
|
+
.data
|
|
973
1289
|
: await db.getCollection(dbId, collection.$id);
|
|
974
1290
|
}
|
|
975
1291
|
catch (error) {
|
|
@@ -990,9 +1306,7 @@ export const createUpdateCollectionAttributesWithStatusCheck = async (db, dbId,
|
|
|
990
1306
|
};
|
|
991
1307
|
export const createUpdateCollectionAttributes = async (db, dbId, collection, attributes) => {
|
|
992
1308
|
MessageFormatter.info(chalk.green(`Creating/Updating attributes for collection: ${collection.name}`));
|
|
993
|
-
const existingAttributes =
|
|
994
|
-
// @ts-expect-error
|
|
995
|
-
collection.attributes.map((attr) => parseAttribute(attr)) || [];
|
|
1309
|
+
const existingAttributes = collection.attributes.map((attr) => parseAttribute(attr)) || [];
|
|
996
1310
|
const attributesToRemove = existingAttributes.filter((attr) => !attributes.some((a) => a.key === attr.key));
|
|
997
1311
|
const indexesToRemove = collection.indexes.filter((index) => attributesToRemove.some((attr) => index.attributes.includes(attr.key)));
|
|
998
1312
|
if (attributesToRemove.length > 0) {
|
|
@@ -1003,7 +1317,11 @@ export const createUpdateCollectionAttributes = async (db, dbId, collection, att
|
|
|
1003
1317
|
for (const index of indexesToRemove) {
|
|
1004
1318
|
await tryAwaitWithRetry(async () => {
|
|
1005
1319
|
if (isDatabaseAdapter(db)) {
|
|
1006
|
-
await db.deleteIndex({
|
|
1320
|
+
await db.deleteIndex({
|
|
1321
|
+
databaseId: dbId,
|
|
1322
|
+
tableId: collection.$id,
|
|
1323
|
+
key: index.key,
|
|
1324
|
+
});
|
|
1007
1325
|
}
|
|
1008
1326
|
else {
|
|
1009
1327
|
await db.deleteIndex(dbId, collection.$id, index.key);
|
|
@@ -1016,7 +1334,11 @@ export const createUpdateCollectionAttributes = async (db, dbId, collection, att
|
|
|
1016
1334
|
MessageFormatter.info(chalk.red(`Removing attribute: ${attr.key} as it is no longer in the collection`));
|
|
1017
1335
|
await tryAwaitWithRetry(async () => {
|
|
1018
1336
|
if (isDatabaseAdapter(db)) {
|
|
1019
|
-
await db.deleteAttribute({
|
|
1337
|
+
await db.deleteAttribute({
|
|
1338
|
+
databaseId: dbId,
|
|
1339
|
+
tableId: collection.$id,
|
|
1340
|
+
key: attr.key,
|
|
1341
|
+
});
|
|
1020
1342
|
}
|
|
1021
1343
|
else {
|
|
1022
1344
|
await db.deleteAttribute(dbId, collection.$id, attr.key);
|