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
@@ -0,0 +1,1555 @@
|
|
1
|
+
import { Query, type Databases, type Models } from "node-appwrite";
|
2
|
+
import {
|
3
|
+
attributeSchema,
|
4
|
+
parseAttribute,
|
5
|
+
type Attribute,
|
6
|
+
} from "appwrite-utils";
|
7
|
+
import {
|
8
|
+
nameToIdMapping,
|
9
|
+
enqueueOperation,
|
10
|
+
markAttributeProcessed,
|
11
|
+
isAttributeProcessed
|
12
|
+
} from "../shared/operationQueue.js";
|
13
|
+
import { delay, tryAwaitWithRetry, calculateExponentialBackoff } from "../utils/helperFunctions.js";
|
14
|
+
import chalk from "chalk";
|
15
|
+
import type { DatabaseAdapter, CreateAttributeParams, UpdateAttributeParams, DeleteAttributeParams } from "../adapters/DatabaseAdapter.js";
|
16
|
+
import { logger } from "../shared/logging.js";
|
17
|
+
import { MessageFormatter } from "../shared/messageFormatter.js";
|
18
|
+
import { isDatabaseAdapter } from "../utils/typeGuards.js";
|
19
|
+
|
20
|
+
// Threshold for treating min/max values as undefined (10 billion)
|
21
|
+
const MIN_MAX_THRESHOLD = 10_000_000_000;
|
22
|
+
|
23
|
+
// Extreme values that Appwrite may return, which should be treated as undefined
|
24
|
+
const EXTREME_MIN_INTEGER = -9223372036854776000;
|
25
|
+
const EXTREME_MAX_INTEGER = 9223372036854776000;
|
26
|
+
const EXTREME_MIN_FLOAT = -1.7976931348623157e+308;
|
27
|
+
const EXTREME_MAX_FLOAT = 1.7976931348623157e+308;
|
28
|
+
|
29
|
+
/**
|
30
|
+
* Type guard to check if an attribute has min/max properties
|
31
|
+
*/
|
32
|
+
const hasMinMaxProperties = (attribute: Attribute): attribute is Attribute & { min?: number; max?: number } => {
|
33
|
+
return attribute.type === 'integer' || attribute.type === 'double' || attribute.type === 'float';
|
34
|
+
};
|
35
|
+
|
36
|
+
/**
|
37
|
+
* Normalizes min/max values for integer and float attributes
|
38
|
+
* Sets values to undefined if they exceed the threshold or are extreme values from database
|
39
|
+
*/
|
40
|
+
const normalizeMinMaxValues = (attribute: Attribute): { min?: number; max?: number } => {
|
41
|
+
if (!hasMinMaxProperties(attribute)) {
|
42
|
+
logger.debug(`Attribute '${attribute.key}' does not have min/max properties`, {
|
43
|
+
type: attribute.type,
|
44
|
+
operation: 'normalizeMinMaxValues'
|
45
|
+
});
|
46
|
+
return {};
|
47
|
+
}
|
48
|
+
|
49
|
+
const { type, min, max } = attribute;
|
50
|
+
let normalizedMin = min;
|
51
|
+
let normalizedMax = max;
|
52
|
+
|
53
|
+
logger.debug(`Normalizing min/max values for attribute '${attribute.key}'`, {
|
54
|
+
type,
|
55
|
+
originalMin: min,
|
56
|
+
originalMax: max,
|
57
|
+
operation: 'normalizeMinMaxValues'
|
58
|
+
});
|
59
|
+
|
60
|
+
// Handle min value
|
61
|
+
if (normalizedMin !== undefined && normalizedMin !== null) {
|
62
|
+
const minValue = Number(normalizedMin);
|
63
|
+
const originalMin = normalizedMin;
|
64
|
+
|
65
|
+
// Check if it exceeds threshold or is an extreme database value
|
66
|
+
if (type === 'integer') {
|
67
|
+
if (Math.abs(minValue) >= MIN_MAX_THRESHOLD || minValue === EXTREME_MIN_INTEGER) {
|
68
|
+
logger.debug(`Min value normalized to undefined for attribute '${attribute.key}'`, {
|
69
|
+
type,
|
70
|
+
originalValue: originalMin,
|
71
|
+
numericValue: minValue,
|
72
|
+
reason: Math.abs(minValue) >= MIN_MAX_THRESHOLD ? 'exceeds_threshold' : 'extreme_database_value',
|
73
|
+
threshold: MIN_MAX_THRESHOLD,
|
74
|
+
extremeValue: EXTREME_MIN_INTEGER,
|
75
|
+
operation: 'normalizeMinMaxValues'
|
76
|
+
});
|
77
|
+
normalizedMin = undefined;
|
78
|
+
}
|
79
|
+
} else { // float/double
|
80
|
+
if (Math.abs(minValue) >= MIN_MAX_THRESHOLD || minValue === EXTREME_MIN_FLOAT) {
|
81
|
+
logger.debug(`Min value normalized to undefined for attribute '${attribute.key}'`, {
|
82
|
+
type,
|
83
|
+
originalValue: originalMin,
|
84
|
+
numericValue: minValue,
|
85
|
+
reason: Math.abs(minValue) >= MIN_MAX_THRESHOLD ? 'exceeds_threshold' : 'extreme_database_value',
|
86
|
+
threshold: MIN_MAX_THRESHOLD,
|
87
|
+
extremeValue: EXTREME_MIN_FLOAT,
|
88
|
+
operation: 'normalizeMinMaxValues'
|
89
|
+
});
|
90
|
+
normalizedMin = undefined;
|
91
|
+
}
|
92
|
+
}
|
93
|
+
}
|
94
|
+
|
95
|
+
// Handle max value
|
96
|
+
if (normalizedMax !== undefined && normalizedMax !== null) {
|
97
|
+
const maxValue = Number(normalizedMax);
|
98
|
+
const originalMax = normalizedMax;
|
99
|
+
|
100
|
+
// Check if it exceeds threshold or is an extreme database value
|
101
|
+
if (type === 'integer') {
|
102
|
+
if (Math.abs(maxValue) >= MIN_MAX_THRESHOLD || maxValue === EXTREME_MAX_INTEGER) {
|
103
|
+
logger.debug(`Max value normalized to undefined for attribute '${attribute.key}'`, {
|
104
|
+
type,
|
105
|
+
originalValue: originalMax,
|
106
|
+
numericValue: maxValue,
|
107
|
+
reason: Math.abs(maxValue) >= MIN_MAX_THRESHOLD ? 'exceeds_threshold' : 'extreme_database_value',
|
108
|
+
threshold: MIN_MAX_THRESHOLD,
|
109
|
+
extremeValue: EXTREME_MAX_INTEGER,
|
110
|
+
operation: 'normalizeMinMaxValues'
|
111
|
+
});
|
112
|
+
normalizedMax = undefined;
|
113
|
+
}
|
114
|
+
} else { // float/double
|
115
|
+
if (Math.abs(maxValue) >= MIN_MAX_THRESHOLD || maxValue === EXTREME_MAX_FLOAT) {
|
116
|
+
logger.debug(`Max value normalized to undefined for attribute '${attribute.key}'`, {
|
117
|
+
type,
|
118
|
+
originalValue: originalMax,
|
119
|
+
numericValue: maxValue,
|
120
|
+
reason: Math.abs(maxValue) >= MIN_MAX_THRESHOLD ? 'exceeds_threshold' : 'extreme_database_value',
|
121
|
+
threshold: MIN_MAX_THRESHOLD,
|
122
|
+
extremeValue: EXTREME_MAX_FLOAT,
|
123
|
+
operation: 'normalizeMinMaxValues'
|
124
|
+
});
|
125
|
+
normalizedMax = undefined;
|
126
|
+
}
|
127
|
+
}
|
128
|
+
}
|
129
|
+
|
130
|
+
const result = { min: normalizedMin, max: normalizedMax };
|
131
|
+
logger.debug(`Min/max normalization complete for attribute '${attribute.key}'`, {
|
132
|
+
type,
|
133
|
+
result,
|
134
|
+
operation: 'normalizeMinMaxValues'
|
135
|
+
});
|
136
|
+
|
137
|
+
return result;
|
138
|
+
};
|
139
|
+
|
140
|
+
/**
|
141
|
+
* Normalizes an attribute for comparison by handling extreme database values
|
142
|
+
* This is used when comparing database attributes with config attributes
|
143
|
+
*/
|
144
|
+
const normalizeAttributeForComparison = (attribute: Attribute): Attribute => {
|
145
|
+
if (!hasMinMaxProperties(attribute)) {
|
146
|
+
return attribute;
|
147
|
+
}
|
148
|
+
|
149
|
+
const { min, max } = normalizeMinMaxValues(attribute);
|
150
|
+
return { ...(attribute as any), min, max };
|
151
|
+
};
|
152
|
+
|
153
|
+
/**
|
154
|
+
* Helper function to create an attribute using either the adapter or legacy API
|
155
|
+
*/
|
156
|
+
const createAttributeViaAdapter = async (
|
157
|
+
db: Databases | DatabaseAdapter,
|
158
|
+
dbId: string,
|
159
|
+
collectionId: string,
|
160
|
+
attribute: Attribute
|
161
|
+
): Promise<void> => {
|
162
|
+
const startTime = Date.now();
|
163
|
+
const adapterType = isDatabaseAdapter(db) ? 'adapter' : 'legacy';
|
164
|
+
|
165
|
+
logger.info(`Creating attribute '${attribute.key}' via ${adapterType}`, {
|
166
|
+
type: attribute.type,
|
167
|
+
dbId,
|
168
|
+
collectionId,
|
169
|
+
adapterType,
|
170
|
+
operation: 'createAttributeViaAdapter'
|
171
|
+
});
|
172
|
+
|
173
|
+
if (isDatabaseAdapter(db)) {
|
174
|
+
// Use the adapter's unified createAttribute method
|
175
|
+
const params: CreateAttributeParams = {
|
176
|
+
databaseId: dbId,
|
177
|
+
tableId: collectionId,
|
178
|
+
key: attribute.key,
|
179
|
+
type: attribute.type,
|
180
|
+
required: attribute.required || false,
|
181
|
+
array: attribute.array || false,
|
182
|
+
...((attribute as any).size && { size: (attribute as any).size }),
|
183
|
+
...((attribute as any).xdefault !== undefined && !attribute.required && { default: (attribute as any).xdefault }),
|
184
|
+
...((attribute as any).encrypted && { encrypt: (attribute as any).encrypted }),
|
185
|
+
...((attribute as any).min !== undefined && { min: (attribute as any).min }),
|
186
|
+
...((attribute as any).max !== undefined && { max: (attribute as any).max }),
|
187
|
+
...((attribute as any).elements && { elements: (attribute as any).elements }),
|
188
|
+
...((attribute as any).relatedCollection && { relatedCollection: (attribute as any).relatedCollection }),
|
189
|
+
...((attribute as any).relationType && { relationType: (attribute as any).relationType }),
|
190
|
+
...((attribute as any).twoWay !== undefined && { twoWay: (attribute as any).twoWay }),
|
191
|
+
...((attribute as any).onDelete && { onDelete: (attribute as any).onDelete }),
|
192
|
+
...((attribute as any).twoWayKey && { twoWayKey: (attribute as any).twoWayKey })
|
193
|
+
};
|
194
|
+
|
195
|
+
logger.debug(`Adapter create parameters for '${attribute.key}'`, {
|
196
|
+
params,
|
197
|
+
operation: 'createAttributeViaAdapter'
|
198
|
+
});
|
199
|
+
|
200
|
+
await db.createAttribute(params);
|
201
|
+
|
202
|
+
const duration = Date.now() - startTime;
|
203
|
+
logger.info(`Successfully created attribute '${attribute.key}' via adapter`, {
|
204
|
+
duration,
|
205
|
+
operation: 'createAttributeViaAdapter'
|
206
|
+
});
|
207
|
+
} else {
|
208
|
+
// Use legacy type-specific methods
|
209
|
+
logger.debug(`Using legacy creation for attribute '${attribute.key}'`, {
|
210
|
+
operation: 'createAttributeViaAdapter'
|
211
|
+
});
|
212
|
+
await createLegacyAttribute(db, dbId, collectionId, attribute);
|
213
|
+
|
214
|
+
const duration = Date.now() - startTime;
|
215
|
+
logger.info(`Successfully created attribute '${attribute.key}' via legacy`, {
|
216
|
+
duration,
|
217
|
+
operation: 'createAttributeViaAdapter'
|
218
|
+
});
|
219
|
+
}
|
220
|
+
};
|
221
|
+
|
222
|
+
/**
|
223
|
+
* Helper function to update an attribute using either the adapter or legacy API
|
224
|
+
*/
|
225
|
+
const updateAttributeViaAdapter = async (
|
226
|
+
db: Databases | DatabaseAdapter,
|
227
|
+
dbId: string,
|
228
|
+
collectionId: string,
|
229
|
+
attribute: Attribute
|
230
|
+
): Promise<void> => {
|
231
|
+
if (isDatabaseAdapter(db)) {
|
232
|
+
// Use the adapter's unified updateAttribute method
|
233
|
+
const params: UpdateAttributeParams = {
|
234
|
+
databaseId: dbId,
|
235
|
+
tableId: collectionId,
|
236
|
+
key: attribute.key,
|
237
|
+
required: attribute.required || false,
|
238
|
+
...((attribute as any).xdefault !== undefined && !attribute.required && { default: (attribute as any).xdefault })
|
239
|
+
};
|
240
|
+
await db.updateAttribute(params);
|
241
|
+
} else {
|
242
|
+
// Use legacy type-specific methods
|
243
|
+
await updateLegacyAttribute(db, dbId, collectionId, attribute);
|
244
|
+
}
|
245
|
+
};
|
246
|
+
|
247
|
+
/**
|
248
|
+
* Legacy attribute creation using type-specific methods
|
249
|
+
*/
|
250
|
+
const createLegacyAttribute = async (
|
251
|
+
db: Databases,
|
252
|
+
dbId: string,
|
253
|
+
collectionId: string,
|
254
|
+
attribute: Attribute
|
255
|
+
): Promise<void> => {
|
256
|
+
const startTime = Date.now();
|
257
|
+
const { min: normalizedMin, max: normalizedMax } = normalizeMinMaxValues(attribute);
|
258
|
+
|
259
|
+
logger.info(`Creating legacy attribute '${attribute.key}'`, {
|
260
|
+
type: attribute.type,
|
261
|
+
dbId,
|
262
|
+
collectionId,
|
263
|
+
normalizedMin,
|
264
|
+
normalizedMax,
|
265
|
+
operation: 'createLegacyAttribute'
|
266
|
+
});
|
267
|
+
|
268
|
+
switch (attribute.type) {
|
269
|
+
case "string":
|
270
|
+
const stringParams = {
|
271
|
+
size: (attribute as any).size || 255,
|
272
|
+
required: attribute.required || false,
|
273
|
+
defaultValue: (attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
|
274
|
+
array: attribute.array || false,
|
275
|
+
encrypted: (attribute as any).encrypted
|
276
|
+
};
|
277
|
+
logger.debug(`Creating string attribute '${attribute.key}'`, {
|
278
|
+
...stringParams,
|
279
|
+
operation: 'createLegacyAttribute'
|
280
|
+
});
|
281
|
+
await db.createStringAttribute(
|
282
|
+
dbId,
|
283
|
+
collectionId,
|
284
|
+
attribute.key,
|
285
|
+
stringParams.size,
|
286
|
+
stringParams.required,
|
287
|
+
stringParams.defaultValue,
|
288
|
+
stringParams.array,
|
289
|
+
stringParams.encrypted
|
290
|
+
);
|
291
|
+
break;
|
292
|
+
case "integer":
|
293
|
+
const integerParams = {
|
294
|
+
required: attribute.required || false,
|
295
|
+
min: normalizedMin !== undefined ? parseInt(String(normalizedMin)) : undefined,
|
296
|
+
max: normalizedMax !== undefined ? parseInt(String(normalizedMax)) : undefined,
|
297
|
+
defaultValue: (attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
|
298
|
+
array: attribute.array || false
|
299
|
+
};
|
300
|
+
logger.debug(`Creating integer attribute '${attribute.key}'`, {
|
301
|
+
...integerParams,
|
302
|
+
operation: 'createLegacyAttribute'
|
303
|
+
});
|
304
|
+
await db.createIntegerAttribute(
|
305
|
+
dbId,
|
306
|
+
collectionId,
|
307
|
+
attribute.key,
|
308
|
+
integerParams.required,
|
309
|
+
integerParams.min,
|
310
|
+
integerParams.max,
|
311
|
+
integerParams.defaultValue,
|
312
|
+
integerParams.array
|
313
|
+
);
|
314
|
+
break;
|
315
|
+
case "double":
|
316
|
+
case "float":
|
317
|
+
await db.createFloatAttribute(
|
318
|
+
dbId,
|
319
|
+
collectionId,
|
320
|
+
attribute.key,
|
321
|
+
attribute.required || false,
|
322
|
+
normalizedMin !== undefined ? Number(normalizedMin) : undefined,
|
323
|
+
normalizedMax !== undefined ? Number(normalizedMax) : undefined,
|
324
|
+
(attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
|
325
|
+
attribute.array || false
|
326
|
+
);
|
327
|
+
break;
|
328
|
+
case "boolean":
|
329
|
+
await db.createBooleanAttribute(
|
330
|
+
dbId,
|
331
|
+
collectionId,
|
332
|
+
attribute.key,
|
333
|
+
attribute.required || false,
|
334
|
+
(attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
|
335
|
+
attribute.array || false
|
336
|
+
);
|
337
|
+
break;
|
338
|
+
case "datetime":
|
339
|
+
await db.createDatetimeAttribute(
|
340
|
+
dbId,
|
341
|
+
collectionId,
|
342
|
+
attribute.key,
|
343
|
+
attribute.required || false,
|
344
|
+
(attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
|
345
|
+
attribute.array || false
|
346
|
+
);
|
347
|
+
break;
|
348
|
+
case "email":
|
349
|
+
await db.createEmailAttribute(
|
350
|
+
dbId,
|
351
|
+
collectionId,
|
352
|
+
attribute.key,
|
353
|
+
attribute.required || false,
|
354
|
+
(attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
|
355
|
+
attribute.array || false
|
356
|
+
);
|
357
|
+
break;
|
358
|
+
case "ip":
|
359
|
+
await db.createIpAttribute(
|
360
|
+
dbId,
|
361
|
+
collectionId,
|
362
|
+
attribute.key,
|
363
|
+
attribute.required || false,
|
364
|
+
(attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
|
365
|
+
attribute.array || false
|
366
|
+
);
|
367
|
+
break;
|
368
|
+
case "url":
|
369
|
+
await db.createUrlAttribute(
|
370
|
+
dbId,
|
371
|
+
collectionId,
|
372
|
+
attribute.key,
|
373
|
+
attribute.required || false,
|
374
|
+
(attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
|
375
|
+
attribute.array || false
|
376
|
+
);
|
377
|
+
break;
|
378
|
+
case "enum":
|
379
|
+
await db.createEnumAttribute(
|
380
|
+
dbId,
|
381
|
+
collectionId,
|
382
|
+
attribute.key,
|
383
|
+
(attribute as any).elements || [],
|
384
|
+
attribute.required || false,
|
385
|
+
(attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
|
386
|
+
attribute.array || false
|
387
|
+
);
|
388
|
+
break;
|
389
|
+
case "relationship":
|
390
|
+
await db.createRelationshipAttribute(
|
391
|
+
dbId,
|
392
|
+
collectionId,
|
393
|
+
(attribute as any).relatedCollection!,
|
394
|
+
(attribute as any).relationType!,
|
395
|
+
(attribute as any).twoWay,
|
396
|
+
attribute.key,
|
397
|
+
(attribute as any).twoWayKey,
|
398
|
+
(attribute as any).onDelete
|
399
|
+
);
|
400
|
+
break;
|
401
|
+
default:
|
402
|
+
const error = new Error(`Unsupported attribute type: ${(attribute as any).type}`);
|
403
|
+
logger.error(`Unsupported attribute type for '${(attribute as any).key}'`, {
|
404
|
+
type: (attribute as any).type,
|
405
|
+
supportedTypes: ['string', 'integer', 'double', 'float', 'boolean', 'datetime', 'email', 'ip', 'url', 'enum', 'relationship'],
|
406
|
+
operation: 'createLegacyAttribute'
|
407
|
+
});
|
408
|
+
throw error;
|
409
|
+
}
|
410
|
+
|
411
|
+
const duration = Date.now() - startTime;
|
412
|
+
logger.info(`Successfully created legacy attribute '${attribute.key}'`, {
|
413
|
+
type: attribute.type,
|
414
|
+
duration,
|
415
|
+
operation: 'createLegacyAttribute'
|
416
|
+
});
|
417
|
+
};
|
418
|
+
|
419
|
+
/**
|
420
|
+
* Legacy attribute update using type-specific methods
|
421
|
+
*/
|
422
|
+
const updateLegacyAttribute = async (
|
423
|
+
db: Databases,
|
424
|
+
dbId: string,
|
425
|
+
collectionId: string,
|
426
|
+
attribute: Attribute
|
427
|
+
): Promise<void> => {
|
428
|
+
const { min: normalizedMin, max: normalizedMax } = normalizeMinMaxValues(attribute);
|
429
|
+
|
430
|
+
switch (attribute.type) {
|
431
|
+
case "string":
|
432
|
+
await db.updateStringAttribute(
|
433
|
+
dbId,
|
434
|
+
collectionId,
|
435
|
+
attribute.key,
|
436
|
+
attribute.required || false,
|
437
|
+
(attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
|
438
|
+
attribute.size
|
439
|
+
);
|
440
|
+
break;
|
441
|
+
case "integer":
|
442
|
+
await db.updateIntegerAttribute(
|
443
|
+
dbId,
|
444
|
+
collectionId,
|
445
|
+
attribute.key,
|
446
|
+
attribute.required || false,
|
447
|
+
(attribute as any).xdefault !== undefined && !attribute.required ? (attribute as any).xdefault : undefined,
|
448
|
+
normalizedMin !== undefined ? parseInt(String(normalizedMin)) : undefined,
|
449
|
+
normalizedMax !== undefined ? parseInt(String(normalizedMax)) : undefined
|
450
|
+
);
|
451
|
+
break;
|
452
|
+
case "double":
|
453
|
+
case "float":
|
454
|
+
await db.updateFloatAttribute(
|
455
|
+
dbId,
|
456
|
+
collectionId,
|
457
|
+
attribute.key,
|
458
|
+
attribute.required || false,
|
459
|
+
normalizedMin !== undefined ? Number(normalizedMin) : undefined,
|
460
|
+
normalizedMax !== undefined ? Number(normalizedMax) : undefined,
|
461
|
+
attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
|
462
|
+
);
|
463
|
+
break;
|
464
|
+
case "boolean":
|
465
|
+
await db.updateBooleanAttribute(
|
466
|
+
dbId,
|
467
|
+
collectionId,
|
468
|
+
attribute.key,
|
469
|
+
attribute.required || false,
|
470
|
+
attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
|
471
|
+
);
|
472
|
+
break;
|
473
|
+
case "datetime":
|
474
|
+
await db.updateDatetimeAttribute(
|
475
|
+
dbId,
|
476
|
+
collectionId,
|
477
|
+
attribute.key,
|
478
|
+
attribute.required || false,
|
479
|
+
attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
|
480
|
+
);
|
481
|
+
break;
|
482
|
+
case "email":
|
483
|
+
await db.updateEmailAttribute(
|
484
|
+
dbId,
|
485
|
+
collectionId,
|
486
|
+
attribute.key,
|
487
|
+
attribute.required || false,
|
488
|
+
attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
|
489
|
+
);
|
490
|
+
break;
|
491
|
+
case "ip":
|
492
|
+
await db.updateIpAttribute(
|
493
|
+
dbId,
|
494
|
+
collectionId,
|
495
|
+
attribute.key,
|
496
|
+
attribute.required || false,
|
497
|
+
attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
|
498
|
+
);
|
499
|
+
break;
|
500
|
+
case "url":
|
501
|
+
await db.updateUrlAttribute(
|
502
|
+
dbId,
|
503
|
+
collectionId,
|
504
|
+
attribute.key,
|
505
|
+
attribute.required || false,
|
506
|
+
attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
|
507
|
+
);
|
508
|
+
break;
|
509
|
+
case "enum":
|
510
|
+
await db.updateEnumAttribute(
|
511
|
+
dbId,
|
512
|
+
collectionId,
|
513
|
+
attribute.key,
|
514
|
+
(attribute as any).elements || [],
|
515
|
+
attribute.required || false,
|
516
|
+
attribute.xdefault !== undefined && attribute.xdefault !== null && !attribute.required ? attribute.xdefault : undefined
|
517
|
+
);
|
518
|
+
break;
|
519
|
+
case "relationship":
|
520
|
+
await db.updateRelationshipAttribute(
|
521
|
+
dbId,
|
522
|
+
collectionId,
|
523
|
+
attribute.key,
|
524
|
+
(attribute as any).onDelete
|
525
|
+
);
|
526
|
+
break;
|
527
|
+
default:
|
528
|
+
throw new Error(`Unsupported attribute type for update: ${(attribute as any).type}`);
|
529
|
+
}
|
530
|
+
};
|
531
|
+
|
532
|
+
// Interface for attribute with status (fixing the type issue)
|
533
|
+
interface AttributeWithStatus {
|
534
|
+
key: string;
|
535
|
+
type: string;
|
536
|
+
status: "available" | "processing" | "deleting" | "stuck" | "failed";
|
537
|
+
error: string;
|
538
|
+
required: boolean;
|
539
|
+
array?: boolean;
|
540
|
+
$createdAt: string;
|
541
|
+
$updatedAt: string;
|
542
|
+
[key: string]: any; // For type-specific fields
|
543
|
+
}
|
544
|
+
|
545
|
+
/**
|
546
|
+
* Wait for attribute to become available, with retry logic for stuck attributes and exponential backoff
|
547
|
+
*/
|
548
|
+
const waitForAttributeAvailable = async (
|
549
|
+
db: Databases | DatabaseAdapter,
|
550
|
+
dbId: string,
|
551
|
+
collectionId: string,
|
552
|
+
attributeKey: string,
|
553
|
+
maxWaitTime: number = 60000, // 1 minute
|
554
|
+
retryCount: number = 0,
|
555
|
+
maxRetries: number = 5
|
556
|
+
): Promise<boolean> => {
|
557
|
+
const startTime = Date.now();
|
558
|
+
let checkInterval = 2000; // Start with 2 seconds
|
559
|
+
|
560
|
+
logger.info(`Waiting for attribute '${attributeKey}' to become available`, {
|
561
|
+
dbId,
|
562
|
+
collectionId,
|
563
|
+
maxWaitTime,
|
564
|
+
retryCount,
|
565
|
+
maxRetries,
|
566
|
+
operation: 'waitForAttributeAvailable'
|
567
|
+
});
|
568
|
+
|
569
|
+
// Calculate exponential backoff: 2s, 4s, 8s, 16s, 30s (capped at 30s)
|
570
|
+
if (retryCount > 0) {
|
571
|
+
const exponentialDelay = calculateExponentialBackoff(retryCount);
|
572
|
+
console.log(
|
573
|
+
chalk.blue(
|
574
|
+
`Waiting for attribute '${attributeKey}' to become available (retry ${retryCount}, backoff: ${exponentialDelay}ms)...`
|
575
|
+
)
|
576
|
+
);
|
577
|
+
await delay(exponentialDelay);
|
578
|
+
} else {
|
579
|
+
console.log(
|
580
|
+
chalk.blue(
|
581
|
+
`Waiting for attribute '${attributeKey}' to become available...`
|
582
|
+
)
|
583
|
+
);
|
584
|
+
}
|
585
|
+
|
586
|
+
while (Date.now() - startTime < maxWaitTime) {
|
587
|
+
try {
|
588
|
+
const collection = isDatabaseAdapter(db)
|
589
|
+
? (await db.getTable({ databaseId: dbId, tableId: collectionId })).data
|
590
|
+
: await db.getCollection(dbId, collectionId);
|
591
|
+
const attribute = (collection.attributes as any[]).find(
|
592
|
+
(attr: AttributeWithStatus) => attr.key === attributeKey
|
593
|
+
) as AttributeWithStatus | undefined;
|
594
|
+
|
595
|
+
if (!attribute) {
|
596
|
+
console.log(chalk.red(`Attribute '${attributeKey}' not found`));
|
597
|
+
return false;
|
598
|
+
}
|
599
|
+
|
600
|
+
console.log(
|
601
|
+
chalk.gray(`Attribute '${attributeKey}' status: ${attribute.status}`)
|
602
|
+
);
|
603
|
+
|
604
|
+
const statusInfo = {
|
605
|
+
attributeKey,
|
606
|
+
status: attribute.status,
|
607
|
+
error: attribute.error,
|
608
|
+
dbId,
|
609
|
+
collectionId,
|
610
|
+
waitTime: Date.now() - startTime,
|
611
|
+
operation: 'waitForAttributeAvailable'
|
612
|
+
};
|
613
|
+
|
614
|
+
switch (attribute.status) {
|
615
|
+
case "available":
|
616
|
+
console.log(
|
617
|
+
chalk.green(`✅ Attribute '${attributeKey}' is now available`)
|
618
|
+
);
|
619
|
+
logger.info(`Attribute '${attributeKey}' became available`, statusInfo);
|
620
|
+
return true;
|
621
|
+
|
622
|
+
case "failed":
|
623
|
+
console.log(
|
624
|
+
chalk.red(
|
625
|
+
`❌ Attribute '${attributeKey}' failed: ${attribute.error}`
|
626
|
+
)
|
627
|
+
);
|
628
|
+
logger.error(`Attribute '${attributeKey}' failed`, statusInfo);
|
629
|
+
return false;
|
630
|
+
|
631
|
+
case "stuck":
|
632
|
+
console.log(
|
633
|
+
chalk.yellow(
|
634
|
+
`⚠️ Attribute '${attributeKey}' is stuck, will retry...`
|
635
|
+
)
|
636
|
+
);
|
637
|
+
logger.warn(`Attribute '${attributeKey}' is stuck`, statusInfo);
|
638
|
+
return false;
|
639
|
+
|
640
|
+
case "processing":
|
641
|
+
// Continue waiting
|
642
|
+
logger.debug(`Attribute '${attributeKey}' still processing`, statusInfo);
|
643
|
+
break;
|
644
|
+
|
645
|
+
case "deleting":
|
646
|
+
console.log(
|
647
|
+
chalk.yellow(`Attribute '${attributeKey}' is being deleted`)
|
648
|
+
);
|
649
|
+
logger.warn(`Attribute '${attributeKey}' is being deleted`, statusInfo);
|
650
|
+
break;
|
651
|
+
|
652
|
+
default:
|
653
|
+
console.log(
|
654
|
+
chalk.yellow(
|
655
|
+
`Unknown status '${attribute.status}' for attribute '${attributeKey}'`
|
656
|
+
)
|
657
|
+
);
|
658
|
+
logger.warn(`Unknown status for attribute '${attributeKey}'`, statusInfo);
|
659
|
+
break;
|
660
|
+
}
|
661
|
+
|
662
|
+
await delay(checkInterval);
|
663
|
+
} catch (error) {
|
664
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
665
|
+
console.log(chalk.red(`Error checking attribute status: ${errorMessage}`));
|
666
|
+
|
667
|
+
logger.error('Error checking attribute status', {
|
668
|
+
attributeKey,
|
669
|
+
dbId,
|
670
|
+
collectionId,
|
671
|
+
error: errorMessage,
|
672
|
+
waitTime: Date.now() - startTime,
|
673
|
+
operation: 'waitForAttributeAvailable'
|
674
|
+
});
|
675
|
+
|
676
|
+
return false;
|
677
|
+
}
|
678
|
+
}
|
679
|
+
|
680
|
+
// Timeout reached
|
681
|
+
console.log(
|
682
|
+
chalk.yellow(
|
683
|
+
`⏰ Timeout waiting for attribute '${attributeKey}' (${maxWaitTime}ms)`
|
684
|
+
)
|
685
|
+
);
|
686
|
+
|
687
|
+
// If we have retries left and this isn't the last retry, try recreating
|
688
|
+
if (retryCount < maxRetries) {
|
689
|
+
console.log(
|
690
|
+
chalk.yellow(
|
691
|
+
`🔄 Retrying attribute creation (attempt ${
|
692
|
+
retryCount + 1
|
693
|
+
}/${maxRetries})`
|
694
|
+
)
|
695
|
+
);
|
696
|
+
return false; // Signal that we need to retry
|
697
|
+
}
|
698
|
+
|
699
|
+
return false;
|
700
|
+
};
|
701
|
+
|
702
|
+
/**
|
703
|
+
* Wait for all attributes in a collection to become available
|
704
|
+
*/
|
705
|
+
const waitForAllAttributesAvailable = async (
|
706
|
+
db: Databases | DatabaseAdapter,
|
707
|
+
dbId: string,
|
708
|
+
collectionId: string,
|
709
|
+
attributeKeys: string[],
|
710
|
+
maxWaitTime: number = 60000
|
711
|
+
): Promise<string[]> => {
|
712
|
+
console.log(
|
713
|
+
chalk.blue(
|
714
|
+
`Waiting for ${attributeKeys.length} attributes to become available...`
|
715
|
+
)
|
716
|
+
);
|
717
|
+
|
718
|
+
const failedAttributes: string[] = [];
|
719
|
+
|
720
|
+
for (const attributeKey of attributeKeys) {
|
721
|
+
const success = await waitForAttributeAvailable(
|
722
|
+
db,
|
723
|
+
dbId,
|
724
|
+
collectionId,
|
725
|
+
attributeKey,
|
726
|
+
maxWaitTime
|
727
|
+
);
|
728
|
+
if (!success) {
|
729
|
+
failedAttributes.push(attributeKey);
|
730
|
+
}
|
731
|
+
}
|
732
|
+
|
733
|
+
return failedAttributes;
|
734
|
+
};
|
735
|
+
|
736
|
+
/**
|
737
|
+
* Delete collection and recreate with retry logic
|
738
|
+
*/
|
739
|
+
const deleteAndRecreateCollection = async (
|
740
|
+
db: Databases | DatabaseAdapter,
|
741
|
+
dbId: string,
|
742
|
+
collection: Models.Collection,
|
743
|
+
retryCount: number
|
744
|
+
): Promise<Models.Collection | null> => {
|
745
|
+
try {
|
746
|
+
console.log(
|
747
|
+
chalk.yellow(
|
748
|
+
`🗑️ Deleting collection '${collection.name}' for retry ${retryCount}`
|
749
|
+
)
|
750
|
+
);
|
751
|
+
|
752
|
+
// Delete the collection
|
753
|
+
if (isDatabaseAdapter(db)) {
|
754
|
+
await db.deleteTable({ databaseId: dbId, tableId: collection.$id });
|
755
|
+
} else {
|
756
|
+
await db.deleteCollection(dbId, collection.$id);
|
757
|
+
}
|
758
|
+
console.log(chalk.yellow(`Deleted collection '${collection.name}'`));
|
759
|
+
|
760
|
+
// Wait a bit before recreating
|
761
|
+
await delay(2000);
|
762
|
+
|
763
|
+
// Recreate the collection
|
764
|
+
console.log(chalk.blue(`🔄 Recreating collection '${collection.name}'`));
|
765
|
+
const newCollection = isDatabaseAdapter(db)
|
766
|
+
? (await db.createTable({
|
767
|
+
databaseId: dbId,
|
768
|
+
id: collection.$id,
|
769
|
+
name: collection.name,
|
770
|
+
permissions: collection.$permissions,
|
771
|
+
documentSecurity: collection.documentSecurity,
|
772
|
+
enabled: collection.enabled
|
773
|
+
})).data
|
774
|
+
: await db.createCollection(
|
775
|
+
dbId,
|
776
|
+
collection.$id,
|
777
|
+
collection.name,
|
778
|
+
collection.$permissions,
|
779
|
+
collection.documentSecurity,
|
780
|
+
collection.enabled
|
781
|
+
);
|
782
|
+
|
783
|
+
console.log(chalk.green(`✅ Recreated collection '${collection.name}'`));
|
784
|
+
return newCollection;
|
785
|
+
} catch (error) {
|
786
|
+
console.log(
|
787
|
+
chalk.red(
|
788
|
+
`Failed to delete/recreate collection '${collection.name}': ${error}`
|
789
|
+
)
|
790
|
+
);
|
791
|
+
return null;
|
792
|
+
}
|
793
|
+
};
|
794
|
+
|
795
|
+
const attributesSame = (
|
796
|
+
databaseAttribute: Attribute,
|
797
|
+
configAttribute: Attribute
|
798
|
+
): boolean => {
|
799
|
+
// Normalize both attributes for comparison (handle extreme database values)
|
800
|
+
const normalizedDbAttr = normalizeAttributeForComparison(databaseAttribute);
|
801
|
+
const normalizedConfigAttr = normalizeAttributeForComparison(configAttribute);
|
802
|
+
|
803
|
+
const attributesToCheck = [
|
804
|
+
"key",
|
805
|
+
"type",
|
806
|
+
"array",
|
807
|
+
"encrypted",
|
808
|
+
"required",
|
809
|
+
"size",
|
810
|
+
"min",
|
811
|
+
"max",
|
812
|
+
"xdefault",
|
813
|
+
"elements",
|
814
|
+
"relationType",
|
815
|
+
"twoWay",
|
816
|
+
"twoWayKey",
|
817
|
+
"onDelete",
|
818
|
+
"relatedCollection",
|
819
|
+
];
|
820
|
+
|
821
|
+
return attributesToCheck.every((attr) => {
|
822
|
+
// Check if both objects have the attribute
|
823
|
+
const dbHasAttr = attr in normalizedDbAttr;
|
824
|
+
const configHasAttr = attr in normalizedConfigAttr;
|
825
|
+
|
826
|
+
// If both have the attribute, compare values
|
827
|
+
if (dbHasAttr && configHasAttr) {
|
828
|
+
const dbValue = normalizedDbAttr[attr as keyof typeof normalizedDbAttr];
|
829
|
+
const configValue = normalizedConfigAttr[attr as keyof typeof normalizedConfigAttr];
|
830
|
+
|
831
|
+
// Consider undefined and null as equivalent
|
832
|
+
if (
|
833
|
+
(dbValue === undefined || dbValue === null) &&
|
834
|
+
(configValue === undefined || configValue === null)
|
835
|
+
) {
|
836
|
+
return true;
|
837
|
+
}
|
838
|
+
|
839
|
+
// Normalize booleans: treat undefined and false as equivalent
|
840
|
+
if (typeof dbValue === "boolean" || typeof configValue === "boolean") {
|
841
|
+
return Boolean(dbValue) === Boolean(configValue);
|
842
|
+
}
|
843
|
+
// For numeric comparisons, compare numbers if both are numeric-like
|
844
|
+
if (
|
845
|
+
(typeof dbValue === "number" || (typeof dbValue === "string" && dbValue !== "" && !isNaN(Number(dbValue)))) &&
|
846
|
+
(typeof configValue === "number" || (typeof configValue === "string" && configValue !== "" && !isNaN(Number(configValue))))
|
847
|
+
) {
|
848
|
+
return Number(dbValue) === Number(configValue);
|
849
|
+
}
|
850
|
+
return dbValue === configValue;
|
851
|
+
}
|
852
|
+
|
853
|
+
// If neither has the attribute, consider it the same
|
854
|
+
if (!dbHasAttr && !configHasAttr) {
|
855
|
+
return true;
|
856
|
+
}
|
857
|
+
|
858
|
+
// If one has the attribute and the other doesn't, check if it's undefined or null
|
859
|
+
if (dbHasAttr && !configHasAttr) {
|
860
|
+
const dbValue = normalizedDbAttr[attr as keyof typeof normalizedDbAttr];
|
861
|
+
// Consider default-false booleans as equal to missing in config
|
862
|
+
if (typeof dbValue === "boolean") {
|
863
|
+
return dbValue === false; // missing in config equals false in db
|
864
|
+
}
|
865
|
+
return dbValue === undefined || dbValue === null;
|
866
|
+
}
|
867
|
+
|
868
|
+
if (!dbHasAttr && configHasAttr) {
|
869
|
+
const configValue = normalizedConfigAttr[attr as keyof typeof normalizedConfigAttr];
|
870
|
+
// Consider default-false booleans as equal to missing in db
|
871
|
+
if (typeof configValue === "boolean") {
|
872
|
+
return configValue === false; // missing in db equals false in config
|
873
|
+
}
|
874
|
+
return configValue === undefined || configValue === null;
|
875
|
+
}
|
876
|
+
|
877
|
+
// If we reach here, the attributes are different
|
878
|
+
return false;
|
879
|
+
});
|
880
|
+
};
|
881
|
+
|
882
|
+
/**
|
883
|
+
* Enhanced attribute creation with proper status monitoring and retry logic
|
884
|
+
*/
|
885
|
+
export const createOrUpdateAttributeWithStatusCheck = async (
|
886
|
+
db: Databases | DatabaseAdapter,
|
887
|
+
dbId: string,
|
888
|
+
collection: Models.Collection,
|
889
|
+
attribute: Attribute,
|
890
|
+
retryCount: number = 0,
|
891
|
+
maxRetries: number = 5
|
892
|
+
): Promise<boolean> => {
|
893
|
+
console.log(
|
894
|
+
chalk.blue(
|
895
|
+
`Creating/updating attribute '${attribute.key}' (attempt ${
|
896
|
+
retryCount + 1
|
897
|
+
}/${maxRetries + 1})`
|
898
|
+
)
|
899
|
+
);
|
900
|
+
|
901
|
+
try {
|
902
|
+
// First, try to create/update the attribute using existing logic
|
903
|
+
const result = await createOrUpdateAttribute(db, dbId, collection, attribute);
|
904
|
+
|
905
|
+
// If the attribute was queued (relationship dependency unresolved),
|
906
|
+
// skip status polling and retry logic — the queue will handle it later.
|
907
|
+
if (result === "queued") {
|
908
|
+
console.log(
|
909
|
+
chalk.yellow(
|
910
|
+
`⏭️ Deferred relationship attribute '${attribute.key}' — queued for later once dependencies are available`
|
911
|
+
)
|
912
|
+
);
|
913
|
+
return true;
|
914
|
+
}
|
915
|
+
|
916
|
+
// Now wait for the attribute to become available
|
917
|
+
const success = await waitForAttributeAvailable(
|
918
|
+
db,
|
919
|
+
dbId,
|
920
|
+
collection.$id,
|
921
|
+
attribute.key,
|
922
|
+
60000, // 1 minute timeout
|
923
|
+
retryCount,
|
924
|
+
maxRetries
|
925
|
+
);
|
926
|
+
|
927
|
+
if (success) {
|
928
|
+
return true;
|
929
|
+
}
|
930
|
+
|
931
|
+
// If not successful and we have retries left, delete specific attribute and try again
|
932
|
+
if (retryCount < maxRetries) {
|
933
|
+
console.log(
|
934
|
+
chalk.yellow(
|
935
|
+
`Attribute '${attribute.key}' failed/stuck, deleting and retrying...`
|
936
|
+
)
|
937
|
+
);
|
938
|
+
|
939
|
+
// Try to delete the specific stuck attribute instead of the entire collection
|
940
|
+
try {
|
941
|
+
if (isDatabaseAdapter(db)) {
|
942
|
+
await db.deleteAttribute({ databaseId: dbId, tableId: collection.$id, key: attribute.key });
|
943
|
+
} else {
|
944
|
+
await db.deleteAttribute(dbId, collection.$id, attribute.key);
|
945
|
+
}
|
946
|
+
console.log(
|
947
|
+
chalk.yellow(
|
948
|
+
`Deleted stuck attribute '${attribute.key}', will retry creation`
|
949
|
+
)
|
950
|
+
);
|
951
|
+
|
952
|
+
// Wait a bit before retry
|
953
|
+
await delay(3000);
|
954
|
+
|
955
|
+
// Get fresh collection data
|
956
|
+
const freshCollection = isDatabaseAdapter(db)
|
957
|
+
? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data
|
958
|
+
: await db.getCollection(dbId, collection.$id);
|
959
|
+
|
960
|
+
// Retry with the same collection (attribute should be gone now)
|
961
|
+
return await createOrUpdateAttributeWithStatusCheck(
|
962
|
+
db,
|
963
|
+
dbId,
|
964
|
+
freshCollection,
|
965
|
+
attribute,
|
966
|
+
retryCount + 1,
|
967
|
+
maxRetries
|
968
|
+
);
|
969
|
+
} catch (deleteError) {
|
970
|
+
console.log(
|
971
|
+
chalk.red(
|
972
|
+
`Failed to delete stuck attribute '${attribute.key}': ${deleteError}`
|
973
|
+
)
|
974
|
+
);
|
975
|
+
|
976
|
+
// If attribute deletion fails, only then try collection recreation as last resort
|
977
|
+
if (retryCount >= maxRetries - 1) {
|
978
|
+
console.log(
|
979
|
+
chalk.yellow(
|
980
|
+
`Last resort: Recreating collection for attribute '${attribute.key}'`
|
981
|
+
)
|
982
|
+
);
|
983
|
+
|
984
|
+
// Get fresh collection data
|
985
|
+
const freshCollection = isDatabaseAdapter(db)
|
986
|
+
? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data
|
987
|
+
: await db.getCollection(dbId, collection.$id);
|
988
|
+
|
989
|
+
// Delete and recreate collection
|
990
|
+
const newCollection = await deleteAndRecreateCollection(
|
991
|
+
db,
|
992
|
+
dbId,
|
993
|
+
freshCollection,
|
994
|
+
retryCount + 1
|
995
|
+
);
|
996
|
+
|
997
|
+
if (newCollection) {
|
998
|
+
// Retry with the new collection
|
999
|
+
return await createOrUpdateAttributeWithStatusCheck(
|
1000
|
+
db,
|
1001
|
+
dbId,
|
1002
|
+
newCollection,
|
1003
|
+
attribute,
|
1004
|
+
retryCount + 1,
|
1005
|
+
maxRetries
|
1006
|
+
);
|
1007
|
+
}
|
1008
|
+
} else {
|
1009
|
+
// Continue to next retry without collection recreation
|
1010
|
+
return await createOrUpdateAttributeWithStatusCheck(
|
1011
|
+
db,
|
1012
|
+
dbId,
|
1013
|
+
collection,
|
1014
|
+
attribute,
|
1015
|
+
retryCount + 1,
|
1016
|
+
maxRetries
|
1017
|
+
);
|
1018
|
+
}
|
1019
|
+
}
|
1020
|
+
}
|
1021
|
+
|
1022
|
+
console.log(
|
1023
|
+
chalk.red(
|
1024
|
+
`❌ Failed to create attribute '${attribute.key}' after ${
|
1025
|
+
maxRetries + 1
|
1026
|
+
} attempts`
|
1027
|
+
)
|
1028
|
+
);
|
1029
|
+
return false;
|
1030
|
+
} catch (error) {
|
1031
|
+
console.log(
|
1032
|
+
chalk.red(`Error creating attribute '${attribute.key}': ${error}`)
|
1033
|
+
);
|
1034
|
+
|
1035
|
+
if (retryCount < maxRetries) {
|
1036
|
+
console.log(
|
1037
|
+
chalk.yellow(`Retrying attribute '${attribute.key}' due to error...`)
|
1038
|
+
);
|
1039
|
+
|
1040
|
+
// Wait a bit before retry
|
1041
|
+
await delay(2000);
|
1042
|
+
|
1043
|
+
return await createOrUpdateAttributeWithStatusCheck(
|
1044
|
+
db,
|
1045
|
+
dbId,
|
1046
|
+
collection,
|
1047
|
+
attribute,
|
1048
|
+
retryCount + 1,
|
1049
|
+
maxRetries
|
1050
|
+
);
|
1051
|
+
}
|
1052
|
+
|
1053
|
+
return false;
|
1054
|
+
}
|
1055
|
+
};
|
1056
|
+
|
1057
|
+
export const createOrUpdateAttribute = async (
|
1058
|
+
db: Databases | DatabaseAdapter,
|
1059
|
+
dbId: string,
|
1060
|
+
collection: Models.Collection,
|
1061
|
+
attribute: Attribute
|
1062
|
+
): Promise<"queued" | "processed"> => {
|
1063
|
+
let action = "create";
|
1064
|
+
let foundAttribute: Attribute | undefined;
|
1065
|
+
const updateEnabled = true;
|
1066
|
+
let finalAttribute: any = attribute;
|
1067
|
+
try {
|
1068
|
+
const collectionAttr = collection.attributes.find(
|
1069
|
+
(attr: any) => attr.key === attribute.key
|
1070
|
+
) as unknown as any;
|
1071
|
+
foundAttribute = parseAttribute(collectionAttr);
|
1072
|
+
// console.log(`Found attribute: ${JSON.stringify(foundAttribute)}`);
|
1073
|
+
} catch (error) {
|
1074
|
+
foundAttribute = undefined;
|
1075
|
+
}
|
1076
|
+
|
1077
|
+
if (
|
1078
|
+
foundAttribute &&
|
1079
|
+
attributesSame(foundAttribute, attribute) &&
|
1080
|
+
updateEnabled
|
1081
|
+
) {
|
1082
|
+
// No need to do anything, they are the same
|
1083
|
+
return "processed";
|
1084
|
+
} else if (
|
1085
|
+
foundAttribute &&
|
1086
|
+
!attributesSame(foundAttribute, attribute) &&
|
1087
|
+
updateEnabled
|
1088
|
+
) {
|
1089
|
+
// console.log(
|
1090
|
+
// `Updating attribute with same key ${attribute.key} but different values`
|
1091
|
+
// );
|
1092
|
+
finalAttribute = {
|
1093
|
+
...foundAttribute,
|
1094
|
+
...attribute,
|
1095
|
+
};
|
1096
|
+
action = "update";
|
1097
|
+
} else if (
|
1098
|
+
!updateEnabled &&
|
1099
|
+
foundAttribute &&
|
1100
|
+
!attributesSame(foundAttribute, attribute)
|
1101
|
+
) {
|
1102
|
+
if (isDatabaseAdapter(db)) {
|
1103
|
+
await db.deleteAttribute({ databaseId: dbId, tableId: collection.$id, key: attribute.key });
|
1104
|
+
} else {
|
1105
|
+
await db.deleteAttribute(dbId, collection.$id, attribute.key);
|
1106
|
+
}
|
1107
|
+
console.log(
|
1108
|
+
`Deleted attribute: ${attribute.key} to recreate it because they diff (update disabled temporarily)`
|
1109
|
+
);
|
1110
|
+
return "processed";
|
1111
|
+
}
|
1112
|
+
|
1113
|
+
// console.log(`${action}-ing attribute: ${finalAttribute.key}`);
|
1114
|
+
|
1115
|
+
// Relationship attribute logic with adjustments
|
1116
|
+
let collectionFoundViaRelatedCollection: Models.Collection | undefined;
|
1117
|
+
let relatedCollectionId: string | undefined;
|
1118
|
+
if (
|
1119
|
+
finalAttribute.type === "relationship" &&
|
1120
|
+
finalAttribute.relatedCollection
|
1121
|
+
) {
|
1122
|
+
// First try treating relatedCollection as an ID directly
|
1123
|
+
try {
|
1124
|
+
const byIdCollection = isDatabaseAdapter(db)
|
1125
|
+
? (await db.getTable({ databaseId: dbId, tableId: finalAttribute.relatedCollection })).data
|
1126
|
+
: await db.getCollection(dbId, finalAttribute.relatedCollection);
|
1127
|
+
collectionFoundViaRelatedCollection = byIdCollection;
|
1128
|
+
relatedCollectionId = byIdCollection.$id;
|
1129
|
+
// Cache by name for subsequent lookups
|
1130
|
+
nameToIdMapping.set(byIdCollection.name, byIdCollection.$id);
|
1131
|
+
} catch (_) {
|
1132
|
+
// Not an ID or not found — fall back to name-based resolution below
|
1133
|
+
}
|
1134
|
+
|
1135
|
+
if (!collectionFoundViaRelatedCollection && nameToIdMapping.has(finalAttribute.relatedCollection)) {
|
1136
|
+
relatedCollectionId = nameToIdMapping.get(
|
1137
|
+
finalAttribute.relatedCollection
|
1138
|
+
);
|
1139
|
+
try {
|
1140
|
+
collectionFoundViaRelatedCollection = isDatabaseAdapter(db)
|
1141
|
+
? (await db.getTable({ databaseId: dbId, tableId: relatedCollectionId! })).data
|
1142
|
+
: await db.getCollection(dbId, relatedCollectionId!);
|
1143
|
+
} catch (e) {
|
1144
|
+
// console.log(
|
1145
|
+
// `Collection not found: ${finalAttribute.relatedCollection} when nameToIdMapping was set`
|
1146
|
+
// );
|
1147
|
+
collectionFoundViaRelatedCollection = undefined;
|
1148
|
+
}
|
1149
|
+
} else if (!collectionFoundViaRelatedCollection) {
|
1150
|
+
const collectionsPulled = isDatabaseAdapter(db)
|
1151
|
+
? await db.listTables({ databaseId: dbId, queries: [Query.equal("name", finalAttribute.relatedCollection)] })
|
1152
|
+
: await db.listCollections(dbId, [Query.equal("name", finalAttribute.relatedCollection)]);
|
1153
|
+
if (collectionsPulled.total && collectionsPulled.total > 0) {
|
1154
|
+
collectionFoundViaRelatedCollection = isDatabaseAdapter(db)
|
1155
|
+
? (collectionsPulled as any).tables?.[0]
|
1156
|
+
: (collectionsPulled as any).collections?.[0];
|
1157
|
+
relatedCollectionId = collectionFoundViaRelatedCollection?.$id;
|
1158
|
+
if (relatedCollectionId) {
|
1159
|
+
nameToIdMapping.set(
|
1160
|
+
finalAttribute.relatedCollection,
|
1161
|
+
relatedCollectionId
|
1162
|
+
);
|
1163
|
+
}
|
1164
|
+
}
|
1165
|
+
}
|
1166
|
+
// ONLY queue relationship attributes that have actual unresolved dependencies
|
1167
|
+
if (!(relatedCollectionId && collectionFoundViaRelatedCollection)) {
|
1168
|
+
console.log(
|
1169
|
+
chalk.yellow(
|
1170
|
+
`⏳ Queueing relationship attribute '${finalAttribute.key}' - related collection '${finalAttribute.relatedCollection}' not found yet`
|
1171
|
+
)
|
1172
|
+
);
|
1173
|
+
enqueueOperation({
|
1174
|
+
type: "attribute",
|
1175
|
+
collectionId: collection.$id,
|
1176
|
+
collection: collection,
|
1177
|
+
attribute,
|
1178
|
+
dependencies: [finalAttribute.relatedCollection],
|
1179
|
+
});
|
1180
|
+
return "queued";
|
1181
|
+
}
|
1182
|
+
}
|
1183
|
+
finalAttribute = parseAttribute(finalAttribute);
|
1184
|
+
// console.log(`Final Attribute: ${JSON.stringify(finalAttribute)}`);
|
1185
|
+
|
1186
|
+
// Use adapter-based attribute creation/update
|
1187
|
+
if (action === "create") {
|
1188
|
+
await tryAwaitWithRetry(
|
1189
|
+
async () => await createAttributeViaAdapter(db, dbId, collection.$id, finalAttribute)
|
1190
|
+
);
|
1191
|
+
} else {
|
1192
|
+
await tryAwaitWithRetry(
|
1193
|
+
async () => await updateAttributeViaAdapter(db, dbId, collection.$id, finalAttribute)
|
1194
|
+
);
|
1195
|
+
}
|
1196
|
+
return "processed";
|
1197
|
+
};
|
1198
|
+
|
1199
|
+
/**
|
1200
|
+
* Enhanced collection attribute creation with proper status monitoring
|
1201
|
+
*/
|
1202
|
+
export const createUpdateCollectionAttributesWithStatusCheck = async (
|
1203
|
+
db: Databases | DatabaseAdapter,
|
1204
|
+
dbId: string,
|
1205
|
+
collection: Models.Collection,
|
1206
|
+
attributes: Attribute[]
|
1207
|
+
): Promise<boolean> => {
|
1208
|
+
console.log(
|
1209
|
+
chalk.green(
|
1210
|
+
`Creating/Updating attributes for collection: ${collection.name} with status monitoring`
|
1211
|
+
)
|
1212
|
+
);
|
1213
|
+
|
1214
|
+
const existingAttributes: Attribute[] =
|
1215
|
+
// @ts-expect-error
|
1216
|
+
collection.attributes.map((attr) => parseAttribute(attr)) || [];
|
1217
|
+
|
1218
|
+
const attributesToRemove = existingAttributes.filter(
|
1219
|
+
(attr) => !attributes.some((a) => a.key === attr.key)
|
1220
|
+
);
|
1221
|
+
const indexesToRemove = collection.indexes.filter((index) =>
|
1222
|
+
attributesToRemove.some((attr) => index.attributes.includes(attr.key))
|
1223
|
+
);
|
1224
|
+
|
1225
|
+
// Handle attribute removal first
|
1226
|
+
if (attributesToRemove.length > 0) {
|
1227
|
+
if (indexesToRemove.length > 0) {
|
1228
|
+
console.log(
|
1229
|
+
chalk.red(
|
1230
|
+
`Removing indexes as they rely on an attribute that is being removed: ${indexesToRemove
|
1231
|
+
.map((index) => index.key)
|
1232
|
+
.join(", ")}`
|
1233
|
+
)
|
1234
|
+
);
|
1235
|
+
for (const index of indexesToRemove) {
|
1236
|
+
await tryAwaitWithRetry(
|
1237
|
+
async () => {
|
1238
|
+
if (isDatabaseAdapter(db)) {
|
1239
|
+
await db.deleteIndex({ databaseId: dbId, tableId: collection.$id, key: index.key });
|
1240
|
+
} else {
|
1241
|
+
await db.deleteIndex(dbId, collection.$id, index.key);
|
1242
|
+
}
|
1243
|
+
}
|
1244
|
+
);
|
1245
|
+
await delay(500); // Longer delay for deletions
|
1246
|
+
}
|
1247
|
+
}
|
1248
|
+
for (const attr of attributesToRemove) {
|
1249
|
+
console.log(
|
1250
|
+
chalk.red(
|
1251
|
+
`Removing attribute: ${attr.key} as it is no longer in the collection`
|
1252
|
+
)
|
1253
|
+
);
|
1254
|
+
await tryAwaitWithRetry(
|
1255
|
+
async () => {
|
1256
|
+
if (isDatabaseAdapter(db)) {
|
1257
|
+
await db.deleteAttribute({ databaseId: dbId, tableId: collection.$id, key: attr.key });
|
1258
|
+
} else {
|
1259
|
+
await db.deleteAttribute(dbId, collection.$id, attr.key);
|
1260
|
+
}
|
1261
|
+
}
|
1262
|
+
);
|
1263
|
+
await delay(500); // Longer delay for deletions
|
1264
|
+
}
|
1265
|
+
}
|
1266
|
+
|
1267
|
+
// First, get fresh collection data and determine which attributes actually need processing
|
1268
|
+
console.log(
|
1269
|
+
chalk.blue(
|
1270
|
+
`Analyzing ${attributes.length} attributes to determine which need processing...`
|
1271
|
+
)
|
1272
|
+
);
|
1273
|
+
|
1274
|
+
let currentCollection = collection;
|
1275
|
+
try {
|
1276
|
+
currentCollection = isDatabaseAdapter(db)
|
1277
|
+
? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data
|
1278
|
+
: await db.getCollection(dbId, collection.$id);
|
1279
|
+
} catch (error) {
|
1280
|
+
console.log(
|
1281
|
+
chalk.yellow(`Warning: Could not refresh collection data: ${error}`)
|
1282
|
+
);
|
1283
|
+
}
|
1284
|
+
|
1285
|
+
const existingAttributesMap = new Map<string, Attribute>();
|
1286
|
+
try {
|
1287
|
+
const parsedAttributes = currentCollection.attributes.map((attr) =>
|
1288
|
+
// @ts-expect-error
|
1289
|
+
parseAttribute(attr)
|
1290
|
+
);
|
1291
|
+
parsedAttributes.forEach((attr) =>
|
1292
|
+
existingAttributesMap.set(attr.key, attr)
|
1293
|
+
);
|
1294
|
+
} catch (error) {
|
1295
|
+
console.log(
|
1296
|
+
chalk.yellow(`Warning: Could not parse existing attributes: ${error}`)
|
1297
|
+
);
|
1298
|
+
}
|
1299
|
+
|
1300
|
+
// Filter to only attributes that need processing (new, changed, or not yet processed)
|
1301
|
+
const attributesToProcess = attributes.filter((attribute) => {
|
1302
|
+
// Skip if already processed in this session
|
1303
|
+
if (isAttributeProcessed(currentCollection.$id, attribute.key)) {
|
1304
|
+
console.log(
|
1305
|
+
chalk.gray(`⏭️ Attribute '${attribute.key}' already processed in this session (skipping)`)
|
1306
|
+
);
|
1307
|
+
return false;
|
1308
|
+
}
|
1309
|
+
|
1310
|
+
const existing = existingAttributesMap.get(attribute.key);
|
1311
|
+
if (!existing) {
|
1312
|
+
console.log(chalk.blue(`➕ New attribute: ${attribute.key}`));
|
1313
|
+
return true;
|
1314
|
+
}
|
1315
|
+
|
1316
|
+
const needsUpdate = !attributesSame(existing, attribute);
|
1317
|
+
if (needsUpdate) {
|
1318
|
+
console.log(chalk.blue(`🔄 Changed attribute: ${attribute.key}`));
|
1319
|
+
} else {
|
1320
|
+
console.log(
|
1321
|
+
chalk.gray(`✅ Unchanged attribute: ${attribute.key} (skipping)`)
|
1322
|
+
);
|
1323
|
+
}
|
1324
|
+
return needsUpdate;
|
1325
|
+
});
|
1326
|
+
|
1327
|
+
if (attributesToProcess.length === 0) {
|
1328
|
+
console.log(
|
1329
|
+
chalk.green(
|
1330
|
+
`✅ All ${attributes.length} attributes are already up to date for collection: ${collection.name}`
|
1331
|
+
)
|
1332
|
+
);
|
1333
|
+
return true;
|
1334
|
+
}
|
1335
|
+
|
1336
|
+
console.log(
|
1337
|
+
chalk.blue(
|
1338
|
+
`Creating ${attributesToProcess.length} attributes sequentially with status monitoring...`
|
1339
|
+
)
|
1340
|
+
);
|
1341
|
+
|
1342
|
+
let remainingAttributes = [...attributesToProcess];
|
1343
|
+
let overallRetryCount = 0;
|
1344
|
+
const maxOverallRetries = 3;
|
1345
|
+
|
1346
|
+
while (
|
1347
|
+
remainingAttributes.length > 0 &&
|
1348
|
+
overallRetryCount < maxOverallRetries
|
1349
|
+
) {
|
1350
|
+
const attributesToProcessThisRound = [...remainingAttributes];
|
1351
|
+
remainingAttributes = []; // Reset for next iteration
|
1352
|
+
|
1353
|
+
console.log(
|
1354
|
+
chalk.blue(
|
1355
|
+
`\n=== Attempt ${
|
1356
|
+
overallRetryCount + 1
|
1357
|
+
}/${maxOverallRetries} - Processing ${
|
1358
|
+
attributesToProcessThisRound.length
|
1359
|
+
} attributes ===`
|
1360
|
+
)
|
1361
|
+
);
|
1362
|
+
|
1363
|
+
for (const attribute of attributesToProcessThisRound) {
|
1364
|
+
console.log(
|
1365
|
+
chalk.blue(`\n--- Processing attribute: ${attribute.key} ---`)
|
1366
|
+
);
|
1367
|
+
|
1368
|
+
const success = await createOrUpdateAttributeWithStatusCheck(
|
1369
|
+
db,
|
1370
|
+
dbId,
|
1371
|
+
currentCollection,
|
1372
|
+
attribute
|
1373
|
+
);
|
1374
|
+
|
1375
|
+
if (success) {
|
1376
|
+
console.log(
|
1377
|
+
chalk.green(`✅ Successfully created attribute: ${attribute.key}`)
|
1378
|
+
);
|
1379
|
+
|
1380
|
+
// Mark this specific attribute as processed
|
1381
|
+
markAttributeProcessed(currentCollection.$id, attribute.key);
|
1382
|
+
|
1383
|
+
// Get updated collection data for next iteration
|
1384
|
+
try {
|
1385
|
+
currentCollection = isDatabaseAdapter(db)
|
1386
|
+
? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data as Models.Collection
|
1387
|
+
: await db.getCollection(dbId, collection.$id);
|
1388
|
+
} catch (error) {
|
1389
|
+
console.log(
|
1390
|
+
chalk.yellow(`Warning: Could not refresh collection data: ${error}`)
|
1391
|
+
);
|
1392
|
+
}
|
1393
|
+
|
1394
|
+
// Add delay between successful attributes
|
1395
|
+
await delay(1000);
|
1396
|
+
} else {
|
1397
|
+
console.log(
|
1398
|
+
chalk.red(
|
1399
|
+
`❌ Failed to create attribute: ${attribute.key}, will retry in next round`
|
1400
|
+
)
|
1401
|
+
);
|
1402
|
+
remainingAttributes.push(attribute); // Add back to retry list
|
1403
|
+
}
|
1404
|
+
}
|
1405
|
+
|
1406
|
+
if (remainingAttributes.length === 0) {
|
1407
|
+
console.log(
|
1408
|
+
chalk.green(
|
1409
|
+
`\n✅ Successfully created all ${attributesToProcess.length} attributes for collection: ${collection.name}`
|
1410
|
+
)
|
1411
|
+
);
|
1412
|
+
return true;
|
1413
|
+
}
|
1414
|
+
|
1415
|
+
overallRetryCount++;
|
1416
|
+
|
1417
|
+
if (overallRetryCount < maxOverallRetries) {
|
1418
|
+
console.log(
|
1419
|
+
chalk.yellow(
|
1420
|
+
`\n⏳ Waiting 5 seconds before retrying ${attributesToProcess.length} failed attributes...`
|
1421
|
+
)
|
1422
|
+
);
|
1423
|
+
await delay(5000);
|
1424
|
+
|
1425
|
+
// Refresh collection data before retry
|
1426
|
+
try {
|
1427
|
+
currentCollection = isDatabaseAdapter(db)
|
1428
|
+
? (await db.getTable({ databaseId: dbId, tableId: collection.$id })).data as Models.Collection
|
1429
|
+
: await db.getCollection(dbId, collection.$id);
|
1430
|
+
console.log(chalk.blue(`Refreshed collection data for retry`));
|
1431
|
+
} catch (error) {
|
1432
|
+
console.log(
|
1433
|
+
chalk.yellow(
|
1434
|
+
`Warning: Could not refresh collection data for retry: ${error}`
|
1435
|
+
)
|
1436
|
+
);
|
1437
|
+
}
|
1438
|
+
}
|
1439
|
+
}
|
1440
|
+
|
1441
|
+
// If we get here, some attributes still failed after all retries
|
1442
|
+
if (attributesToProcess.length > 0) {
|
1443
|
+
console.log(
|
1444
|
+
chalk.red(
|
1445
|
+
`\n❌ Failed to create ${
|
1446
|
+
attributesToProcess.length
|
1447
|
+
} attributes after ${maxOverallRetries} attempts: ${attributesToProcess
|
1448
|
+
.map((a) => a.key)
|
1449
|
+
.join(", ")}`
|
1450
|
+
)
|
1451
|
+
);
|
1452
|
+
console.log(
|
1453
|
+
chalk.red(
|
1454
|
+
`This may indicate a fundamental issue with the attribute definitions or Appwrite instance`
|
1455
|
+
)
|
1456
|
+
);
|
1457
|
+
return false;
|
1458
|
+
}
|
1459
|
+
|
1460
|
+
console.log(
|
1461
|
+
chalk.green(
|
1462
|
+
`\n✅ Successfully created all ${attributes.length} attributes for collection: ${collection.name}`
|
1463
|
+
)
|
1464
|
+
);
|
1465
|
+
return true;
|
1466
|
+
};
|
1467
|
+
|
1468
|
+
export const createUpdateCollectionAttributes = async (
|
1469
|
+
db: Databases | DatabaseAdapter,
|
1470
|
+
dbId: string,
|
1471
|
+
collection: Models.Collection,
|
1472
|
+
attributes: Attribute[]
|
1473
|
+
): Promise<void> => {
|
1474
|
+
console.log(
|
1475
|
+
chalk.green(
|
1476
|
+
`Creating/Updating attributes for collection: ${collection.name}`
|
1477
|
+
)
|
1478
|
+
);
|
1479
|
+
|
1480
|
+
const existingAttributes: Attribute[] =
|
1481
|
+
// @ts-expect-error
|
1482
|
+
collection.attributes.map((attr) => parseAttribute(attr)) || [];
|
1483
|
+
|
1484
|
+
const attributesToRemove = existingAttributes.filter(
|
1485
|
+
(attr) => !attributes.some((a) => a.key === attr.key)
|
1486
|
+
);
|
1487
|
+
const indexesToRemove = collection.indexes.filter((index) =>
|
1488
|
+
attributesToRemove.some((attr) => index.attributes.includes(attr.key))
|
1489
|
+
);
|
1490
|
+
|
1491
|
+
if (attributesToRemove.length > 0) {
|
1492
|
+
if (indexesToRemove.length > 0) {
|
1493
|
+
console.log(
|
1494
|
+
chalk.red(
|
1495
|
+
`Removing indexes as they rely on an attribute that is being removed: ${indexesToRemove
|
1496
|
+
.map((index) => index.key)
|
1497
|
+
.join(", ")}`
|
1498
|
+
)
|
1499
|
+
);
|
1500
|
+
for (const index of indexesToRemove) {
|
1501
|
+
await tryAwaitWithRetry(
|
1502
|
+
async () => {
|
1503
|
+
if (isDatabaseAdapter(db)) {
|
1504
|
+
await db.deleteIndex({ databaseId: dbId, tableId: collection.$id, key: index.key });
|
1505
|
+
} else {
|
1506
|
+
await db.deleteIndex(dbId, collection.$id, index.key);
|
1507
|
+
}
|
1508
|
+
}
|
1509
|
+
);
|
1510
|
+
await delay(100);
|
1511
|
+
}
|
1512
|
+
}
|
1513
|
+
for (const attr of attributesToRemove) {
|
1514
|
+
console.log(
|
1515
|
+
chalk.red(
|
1516
|
+
`Removing attribute: ${attr.key} as it is no longer in the collection`
|
1517
|
+
)
|
1518
|
+
);
|
1519
|
+
await tryAwaitWithRetry(
|
1520
|
+
async () => {
|
1521
|
+
if (isDatabaseAdapter(db)) {
|
1522
|
+
await db.deleteAttribute({ databaseId: dbId, tableId: collection.$id, key: attr.key });
|
1523
|
+
} else {
|
1524
|
+
await db.deleteAttribute(dbId, collection.$id, attr.key);
|
1525
|
+
}
|
1526
|
+
}
|
1527
|
+
);
|
1528
|
+
await delay(50);
|
1529
|
+
}
|
1530
|
+
}
|
1531
|
+
|
1532
|
+
const batchSize = 3;
|
1533
|
+
for (let i = 0; i < attributes.length; i += batchSize) {
|
1534
|
+
const batch = attributes.slice(i, i + batchSize);
|
1535
|
+
const attributePromises = batch.map((attribute) =>
|
1536
|
+
tryAwaitWithRetry(
|
1537
|
+
async () =>
|
1538
|
+
await createOrUpdateAttribute(db, dbId, collection, attribute)
|
1539
|
+
)
|
1540
|
+
);
|
1541
|
+
|
1542
|
+
const results = await Promise.allSettled(attributePromises);
|
1543
|
+
results.forEach((result) => {
|
1544
|
+
if (result.status === "rejected") {
|
1545
|
+
console.error("An attribute promise was rejected:", result.reason);
|
1546
|
+
}
|
1547
|
+
});
|
1548
|
+
|
1549
|
+
// Add delay after each batch
|
1550
|
+
await delay(200);
|
1551
|
+
}
|
1552
|
+
console.log(
|
1553
|
+
`Finished creating/updating attributes for collection: ${collection.name}`
|
1554
|
+
);
|
1555
|
+
};
|