appwrite-utils-cli 1.4.1 → 1.5.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/README.md +22 -1
- package/dist/adapters/TablesDBAdapter.js +7 -4
- package/dist/collections/attributes.d.ts +1 -1
- package/dist/collections/attributes.js +42 -7
- package/dist/collections/indexes.js +13 -3
- package/dist/collections/methods.d.ts +9 -0
- package/dist/collections/methods.js +268 -0
- package/dist/databases/setup.js +6 -2
- package/dist/interactiveCLI.js +2 -1
- package/dist/migrations/appwriteToX.d.ts +2 -2
- package/dist/migrations/comprehensiveTransfer.js +12 -0
- package/dist/migrations/dataLoader.d.ts +5 -5
- package/dist/migrations/relationships.d.ts +2 -2
- package/dist/shared/jsonSchemaGenerator.d.ts +1 -0
- package/dist/shared/jsonSchemaGenerator.js +6 -2
- package/dist/shared/operationQueue.js +14 -1
- package/dist/shared/schemaGenerator.d.ts +2 -1
- package/dist/shared/schemaGenerator.js +61 -78
- package/dist/storage/schemas.d.ts +8 -8
- package/dist/utils/loadConfigs.js +44 -19
- package/dist/utils/schemaStrings.d.ts +2 -1
- package/dist/utils/schemaStrings.js +61 -78
- package/dist/utils/setupFiles.js +19 -1
- package/dist/utils/versionDetection.d.ts +6 -0
- package/dist/utils/versionDetection.js +30 -0
- package/dist/utilsController.js +32 -5
- package/package.json +1 -1
- package/src/adapters/TablesDBAdapter.ts +20 -17
- package/src/collections/attributes.ts +198 -156
- package/src/collections/indexes.ts +36 -28
- package/src/collections/methods.ts +292 -19
- package/src/databases/setup.ts +11 -7
- package/src/interactiveCLI.ts +8 -7
- package/src/migrations/comprehensiveTransfer.ts +22 -8
- package/src/shared/jsonSchemaGenerator.ts +36 -29
- package/src/shared/operationQueue.ts +48 -33
- package/src/shared/schemaGenerator.ts +128 -134
- package/src/utils/loadConfigs.ts +48 -29
- package/src/utils/schemaStrings.ts +124 -130
- package/src/utils/setupFiles.ts +21 -5
- package/src/utils/versionDetection.ts +48 -21
- package/src/utilsController.ts +59 -32
package/dist/utils/setupFiles.js
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
import { mkdirSync, writeFileSync, existsSync } from "node:fs";
|
2
2
|
import path from "node:path";
|
3
3
|
import { findAppwriteConfig } from "./loadConfigs.js";
|
4
|
+
import { loadYamlConfig } from "../config/yamlConfig.js";
|
5
|
+
import { fetchServerVersion, isVersionAtLeast } from "./versionDetection.js";
|
4
6
|
import { findYamlConfig } from "../config/yamlConfig.js";
|
5
7
|
import { ID } from "node-appwrite";
|
6
8
|
import { ulid } from "ulidx";
|
@@ -240,7 +242,23 @@ export const setupDirsFiles = async (example = false, currentDir, useYaml = true
|
|
240
242
|
const appwriteSchemaFolder = path.join(appwriteFolder, "schemas");
|
241
243
|
const appwriteYamlSchemaFolder = path.join(appwriteFolder, ".yaml_schemas");
|
242
244
|
const appwriteDataFolder = path.join(appwriteFolder, "importData");
|
243
|
-
|
245
|
+
// Decide between collections or tables folder
|
246
|
+
let useTables = false;
|
247
|
+
try {
|
248
|
+
// Try reading YAML config if present to detect version
|
249
|
+
const yamlPath = findYamlConfig(basePath);
|
250
|
+
if (yamlPath) {
|
251
|
+
const cfg = await loadYamlConfig(yamlPath);
|
252
|
+
if (cfg) {
|
253
|
+
const ver = await fetchServerVersion(cfg.appwriteEndpoint);
|
254
|
+
if (isVersionAtLeast(ver || undefined, '1.8.0'))
|
255
|
+
useTables = true;
|
256
|
+
}
|
257
|
+
}
|
258
|
+
}
|
259
|
+
catch { }
|
260
|
+
const targetFolderName = useTables ? "tables" : "collections";
|
261
|
+
const collectionsFolder = path.join(appwriteFolder, targetFolderName);
|
244
262
|
// Create directory structure
|
245
263
|
if (!existsSync(appwriteFolder)) {
|
246
264
|
mkdirSync(appwriteFolder, { recursive: true });
|
@@ -54,3 +54,9 @@ export declare function detectSdkSupport(): Promise<{
|
|
54
54
|
* Clear version detection cache (useful for testing)
|
55
55
|
*/
|
56
56
|
export declare function clearVersionDetectionCache(): void;
|
57
|
+
/**
|
58
|
+
* Fetch server version from /health/version (no auth required)
|
59
|
+
*/
|
60
|
+
export declare function fetchServerVersion(endpoint: string): Promise<string | null>;
|
61
|
+
/** Compare semantic versions (basic) */
|
62
|
+
export declare function isVersionAtLeast(current: string | undefined, target: string): boolean;
|
@@ -215,3 +215,33 @@ export async function detectSdkSupport() {
|
|
215
215
|
export function clearVersionDetectionCache() {
|
216
216
|
detectionCache.clear();
|
217
217
|
}
|
218
|
+
/**
|
219
|
+
* Fetch server version from /health/version (no auth required)
|
220
|
+
*/
|
221
|
+
export async function fetchServerVersion(endpoint) {
|
222
|
+
try {
|
223
|
+
const clean = endpoint.replace(/\/$/, '');
|
224
|
+
const res = await fetch(`${clean}/health/version`, { method: 'GET', signal: AbortSignal.timeout(5000) });
|
225
|
+
if (!res.ok)
|
226
|
+
return null;
|
227
|
+
const data = await res.json().catch(() => null);
|
228
|
+
const version = (data && (data.version || data.build || data.release)) ?? null;
|
229
|
+
return typeof version === 'string' ? version : null;
|
230
|
+
}
|
231
|
+
catch {
|
232
|
+
return null;
|
233
|
+
}
|
234
|
+
}
|
235
|
+
/** Compare semantic versions (basic) */
|
236
|
+
export function isVersionAtLeast(current, target) {
|
237
|
+
if (!current)
|
238
|
+
return false;
|
239
|
+
const toNums = (v) => v.split('.').map(n => parseInt(n, 10));
|
240
|
+
const [a1 = 0, a2 = 0, a3 = 0] = toNums(current);
|
241
|
+
const [b1, b2, b3] = toNums(target);
|
242
|
+
if (a1 !== b1)
|
243
|
+
return a1 > b1;
|
244
|
+
if (a2 !== b2)
|
245
|
+
return a2 > b2;
|
246
|
+
return a3 >= b3;
|
247
|
+
}
|
package/dist/utilsController.js
CHANGED
@@ -7,12 +7,14 @@ import { ImportController } from "./migrations/importController.js";
|
|
7
7
|
import { ImportDataActions } from "./migrations/importDataActions.js";
|
8
8
|
import { setupMigrationDatabase, ensureDatabasesExist, wipeOtherDatabases, ensureCollectionsExist, } from "./databases/setup.js";
|
9
9
|
import { createOrUpdateCollections, wipeDatabase, generateSchemas, fetchAllCollections, wipeCollection, } from "./collections/methods.js";
|
10
|
+
import { wipeAllTables, wipeTableRows } from "./collections/methods.js";
|
10
11
|
import { backupDatabase, ensureDatabaseConfigBucketsExist, initOrGetBackupStorage, wipeDocumentStorage, } from "./storage/methods.js";
|
11
12
|
import path from "path";
|
12
13
|
import { converterFunctions, validationRules, } from "appwrite-utils";
|
13
14
|
import { afterImportActions } from "./migrations/afterImportActions.js";
|
14
15
|
import { transferDatabaseLocalToLocal, transferDatabaseLocalToRemote, transferStorageLocalToLocal, transferStorageLocalToRemote, transferUsersLocalToRemote, } from "./migrations/transfer.js";
|
15
16
|
import { getClient } from "./utils/getClientFromConfig.js";
|
17
|
+
import { getAdapterFromConfig } from "./utils/getClientFromConfig.js";
|
16
18
|
import { fetchAllDatabases } from "./databases/methods.js";
|
17
19
|
import { listFunctions, updateFunctionSpecifications, } from "./functions/methods.js";
|
18
20
|
import chalk from "chalk";
|
@@ -291,9 +293,20 @@ export class UtilsController {
|
|
291
293
|
}
|
292
294
|
async wipeDatabase(database, wipeBucket = false) {
|
293
295
|
await this.init();
|
294
|
-
if (!this.database)
|
296
|
+
if (!this.database || !this.config)
|
295
297
|
throw new Error("Database not initialized");
|
296
|
-
|
298
|
+
try {
|
299
|
+
const { adapter, apiMode } = await getAdapterFromConfig(this.config);
|
300
|
+
if (apiMode === 'tablesdb') {
|
301
|
+
await wipeAllTables(adapter, database.$id);
|
302
|
+
}
|
303
|
+
else {
|
304
|
+
await wipeDatabase(this.database, database.$id);
|
305
|
+
}
|
306
|
+
}
|
307
|
+
catch {
|
308
|
+
await wipeDatabase(this.database, database.$id);
|
309
|
+
}
|
297
310
|
if (wipeBucket) {
|
298
311
|
await this.wipeBucketFromDatabase(database);
|
299
312
|
}
|
@@ -323,9 +336,20 @@ export class UtilsController {
|
|
323
336
|
}
|
324
337
|
async wipeCollection(database, collection) {
|
325
338
|
await this.init();
|
326
|
-
if (!this.database)
|
339
|
+
if (!this.database || !this.config)
|
327
340
|
throw new Error("Database not initialized");
|
328
|
-
|
341
|
+
try {
|
342
|
+
const { adapter, apiMode } = await getAdapterFromConfig(this.config);
|
343
|
+
if (apiMode === 'tablesdb') {
|
344
|
+
await wipeTableRows(adapter, database.$id, collection.$id);
|
345
|
+
}
|
346
|
+
else {
|
347
|
+
await wipeCollection(this.database, database.$id, collection.$id);
|
348
|
+
}
|
349
|
+
}
|
350
|
+
catch {
|
351
|
+
await wipeCollection(this.database, database.$id, collection.$id);
|
352
|
+
}
|
329
353
|
}
|
330
354
|
async wipeDocumentStorage(bucketId) {
|
331
355
|
await this.init();
|
@@ -418,9 +442,12 @@ export class UtilsController {
|
|
418
442
|
const allDatabases = await fetchAllDatabases(this.database);
|
419
443
|
databases = allDatabases;
|
420
444
|
}
|
445
|
+
// Ensure DBs exist (this may internally ensure migrations exists)
|
421
446
|
await this.ensureDatabasesExist(databases);
|
422
447
|
await this.ensureDatabaseConfigBucketsExist(databases);
|
423
|
-
|
448
|
+
// Do not push collections to the migrations database (prevents duplicate runs)
|
449
|
+
const dbsForCollections = databases.filter((db) => (this.config?.useMigrations ?? true) ? db.name.toLowerCase() !== "migrations" : true);
|
450
|
+
await this.createOrUpdateCollectionsForDatabases(dbsForCollections, collections);
|
424
451
|
}
|
425
452
|
getAppwriteFolderPath() {
|
426
453
|
return this.appwriteFolderPath;
|
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "appwrite-utils-cli",
|
3
3
|
"description": "Appwrite Utility Functions to help with database management, data conversion, data import, migrations, and much more. Meant to be used as a CLI tool, I do not recommend installing this in frontend environments.",
|
4
|
-
"version": "1.
|
4
|
+
"version": "1.5.1",
|
5
5
|
"main": "src/main.ts",
|
6
6
|
"type": "module",
|
7
7
|
"repository": {
|
@@ -246,12 +246,13 @@ export class TablesDBAdapter extends BaseAdapter {
|
|
246
246
|
}
|
247
247
|
|
248
248
|
// Attribute Operations
|
249
|
-
async createAttribute(params: CreateAttributeParams): Promise<ApiResponse> {
|
250
|
-
try {
|
251
|
-
//
|
252
|
-
const
|
253
|
-
|
254
|
-
|
249
|
+
async createAttribute(params: CreateAttributeParams): Promise<ApiResponse> {
|
250
|
+
try {
|
251
|
+
// Prefer createColumn if available, fallback to createAttribute
|
252
|
+
const fn = this.tablesDB.createColumn || this.tablesDB.createAttribute;
|
253
|
+
const result = await fn.call(this.tablesDB, params);
|
254
|
+
return { data: result };
|
255
|
+
} catch (error) {
|
255
256
|
throw new AdapterError(
|
256
257
|
`Failed to create attribute: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
257
258
|
'CREATE_ATTRIBUTE_FAILED',
|
@@ -260,11 +261,12 @@ export class TablesDBAdapter extends BaseAdapter {
|
|
260
261
|
}
|
261
262
|
}
|
262
263
|
|
263
|
-
async updateAttribute(params: UpdateAttributeParams): Promise<ApiResponse> {
|
264
|
-
try {
|
265
|
-
const
|
266
|
-
|
267
|
-
|
264
|
+
async updateAttribute(params: UpdateAttributeParams): Promise<ApiResponse> {
|
265
|
+
try {
|
266
|
+
const fn = this.tablesDB.updateColumn || this.tablesDB.updateAttribute;
|
267
|
+
const result = await fn.call(this.tablesDB, params);
|
268
|
+
return { data: result };
|
269
|
+
} catch (error) {
|
268
270
|
throw new AdapterError(
|
269
271
|
`Failed to update attribute: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
270
272
|
'UPDATE_ATTRIBUTE_FAILED',
|
@@ -273,11 +275,12 @@ export class TablesDBAdapter extends BaseAdapter {
|
|
273
275
|
}
|
274
276
|
}
|
275
277
|
|
276
|
-
async deleteAttribute(params: DeleteAttributeParams): Promise<ApiResponse> {
|
277
|
-
try {
|
278
|
-
const
|
279
|
-
|
280
|
-
|
278
|
+
async deleteAttribute(params: DeleteAttributeParams): Promise<ApiResponse> {
|
279
|
+
try {
|
280
|
+
const fn = this.tablesDB.deleteColumn || this.tablesDB.deleteAttribute;
|
281
|
+
const result = await fn.call(this.tablesDB, params);
|
282
|
+
return { data: result };
|
283
|
+
} catch (error) {
|
281
284
|
throw new AdapterError(
|
282
285
|
`Failed to delete attribute: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
283
286
|
'DELETE_ATTRIBUTE_FAILED',
|
@@ -426,4 +429,4 @@ export class TablesDBAdapter extends BaseAdapter {
|
|
426
429
|
);
|
427
430
|
}
|
428
431
|
}
|
429
|
-
}
|
432
|
+
}
|
@@ -224,33 +224,33 @@ const attributesSame = (
|
|
224
224
|
databaseAttribute: Attribute,
|
225
225
|
configAttribute: Attribute
|
226
226
|
): boolean => {
|
227
|
-
const attributesToCheck = [
|
228
|
-
"key",
|
229
|
-
"type",
|
230
|
-
"array",
|
231
|
-
"encrypted",
|
232
|
-
"required",
|
233
|
-
"size",
|
234
|
-
"min",
|
235
|
-
"max",
|
236
|
-
"xdefault",
|
237
|
-
"elements",
|
238
|
-
"relationType",
|
239
|
-
"twoWay",
|
240
|
-
"twoWayKey",
|
241
|
-
"onDelete",
|
242
|
-
"relatedCollection",
|
243
|
-
];
|
244
|
-
|
245
|
-
return attributesToCheck.every((attr) => {
|
246
|
-
// Check if both objects have the attribute
|
247
|
-
const dbHasAttr = attr in databaseAttribute;
|
248
|
-
const configHasAttr = attr in configAttribute;
|
227
|
+
const attributesToCheck = [
|
228
|
+
"key",
|
229
|
+
"type",
|
230
|
+
"array",
|
231
|
+
"encrypted",
|
232
|
+
"required",
|
233
|
+
"size",
|
234
|
+
"min",
|
235
|
+
"max",
|
236
|
+
"xdefault",
|
237
|
+
"elements",
|
238
|
+
"relationType",
|
239
|
+
"twoWay",
|
240
|
+
"twoWayKey",
|
241
|
+
"onDelete",
|
242
|
+
"relatedCollection",
|
243
|
+
];
|
244
|
+
|
245
|
+
return attributesToCheck.every((attr) => {
|
246
|
+
// Check if both objects have the attribute
|
247
|
+
const dbHasAttr = attr in databaseAttribute;
|
248
|
+
const configHasAttr = attr in configAttribute;
|
249
249
|
|
250
250
|
// If both have the attribute, compare values
|
251
|
-
if (dbHasAttr && configHasAttr) {
|
252
|
-
const dbValue = databaseAttribute[attr as keyof typeof databaseAttribute];
|
253
|
-
const configValue = configAttribute[attr as keyof typeof configAttribute];
|
251
|
+
if (dbHasAttr && configHasAttr) {
|
252
|
+
const dbValue = databaseAttribute[attr as keyof typeof databaseAttribute];
|
253
|
+
const configValue = configAttribute[attr as keyof typeof configAttribute];
|
254
254
|
|
255
255
|
// Consider undefined and null as equivalent
|
256
256
|
if (
|
@@ -260,8 +260,19 @@ const attributesSame = (
|
|
260
260
|
return true;
|
261
261
|
}
|
262
262
|
|
263
|
-
|
264
|
-
|
263
|
+
// Normalize booleans: treat undefined and false as equivalent
|
264
|
+
if (typeof dbValue === "boolean" || typeof configValue === "boolean") {
|
265
|
+
return Boolean(dbValue) === Boolean(configValue);
|
266
|
+
}
|
267
|
+
// For numeric comparisons, compare numbers if both are numeric-like
|
268
|
+
if (
|
269
|
+
(typeof dbValue === "number" || (typeof dbValue === "string" && dbValue !== "" && !isNaN(Number(dbValue)))) &&
|
270
|
+
(typeof configValue === "number" || (typeof configValue === "string" && configValue !== "" && !isNaN(Number(configValue))))
|
271
|
+
) {
|
272
|
+
return Number(dbValue) === Number(configValue);
|
273
|
+
}
|
274
|
+
return dbValue === configValue;
|
275
|
+
}
|
265
276
|
|
266
277
|
// If neither has the attribute, consider it the same
|
267
278
|
if (!dbHasAttr && !configHasAttr) {
|
@@ -269,15 +280,23 @@ const attributesSame = (
|
|
269
280
|
}
|
270
281
|
|
271
282
|
// If one has the attribute and the other doesn't, check if it's undefined or null
|
272
|
-
if (dbHasAttr && !configHasAttr) {
|
273
|
-
const dbValue = databaseAttribute[attr as keyof typeof databaseAttribute];
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
283
|
+
if (dbHasAttr && !configHasAttr) {
|
284
|
+
const dbValue = databaseAttribute[attr as keyof typeof databaseAttribute];
|
285
|
+
// Consider default-false booleans as equal to missing in config
|
286
|
+
if (typeof dbValue === "boolean") {
|
287
|
+
return dbValue === false; // missing in config equals false in db
|
288
|
+
}
|
289
|
+
return dbValue === undefined || dbValue === null;
|
290
|
+
}
|
291
|
+
|
292
|
+
if (!dbHasAttr && configHasAttr) {
|
293
|
+
const configValue = configAttribute[attr as keyof typeof configAttribute];
|
294
|
+
// Consider default-false booleans as equal to missing in db
|
295
|
+
if (typeof configValue === "boolean") {
|
296
|
+
return configValue === false; // missing in db equals false in config
|
297
|
+
}
|
298
|
+
return configValue === undefined || configValue === null;
|
299
|
+
}
|
281
300
|
|
282
301
|
// If we reach here, the attributes are different
|
283
302
|
return false;
|
@@ -287,14 +306,14 @@ const attributesSame = (
|
|
287
306
|
/**
|
288
307
|
* Enhanced attribute creation with proper status monitoring and retry logic
|
289
308
|
*/
|
290
|
-
export const createOrUpdateAttributeWithStatusCheck = async (
|
291
|
-
db: Databases,
|
292
|
-
dbId: string,
|
293
|
-
collection: Models.Collection,
|
294
|
-
attribute: Attribute,
|
295
|
-
retryCount: number = 0,
|
296
|
-
maxRetries: number = 5
|
297
|
-
): Promise<boolean> => {
|
309
|
+
export const createOrUpdateAttributeWithStatusCheck = async (
|
310
|
+
db: Databases,
|
311
|
+
dbId: string,
|
312
|
+
collection: Models.Collection,
|
313
|
+
attribute: Attribute,
|
314
|
+
retryCount: number = 0,
|
315
|
+
maxRetries: number = 5
|
316
|
+
): Promise<boolean> => {
|
298
317
|
console.log(
|
299
318
|
chalk.blue(
|
300
319
|
`Creating/updating attribute '${attribute.key}' (attempt ${
|
@@ -303,20 +322,31 @@ export const createOrUpdateAttributeWithStatusCheck = async (
|
|
303
322
|
)
|
304
323
|
);
|
305
324
|
|
306
|
-
try {
|
307
|
-
// First, try to create/update the attribute using existing logic
|
308
|
-
await createOrUpdateAttribute(db, dbId, collection, attribute);
|
309
|
-
|
310
|
-
//
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
325
|
+
try {
|
326
|
+
// First, try to create/update the attribute using existing logic
|
327
|
+
const result = await createOrUpdateAttribute(db, dbId, collection, attribute);
|
328
|
+
|
329
|
+
// If the attribute was queued (relationship dependency unresolved),
|
330
|
+
// skip status polling and retry logic — the queue will handle it later.
|
331
|
+
if (result === "queued") {
|
332
|
+
console.log(
|
333
|
+
chalk.yellow(
|
334
|
+
`⏭️ Deferred relationship attribute '${attribute.key}' — queued for later once dependencies are available`
|
335
|
+
)
|
336
|
+
);
|
337
|
+
return true;
|
338
|
+
}
|
339
|
+
|
340
|
+
// Now wait for the attribute to become available
|
341
|
+
const success = await waitForAttributeAvailable(
|
342
|
+
db,
|
343
|
+
dbId,
|
344
|
+
collection.$id,
|
345
|
+
attribute.key,
|
346
|
+
60000, // 1 minute timeout
|
347
|
+
retryCount,
|
348
|
+
maxRetries
|
349
|
+
);
|
320
350
|
|
321
351
|
if (success) {
|
322
352
|
return true;
|
@@ -440,12 +470,12 @@ export const createOrUpdateAttributeWithStatusCheck = async (
|
|
440
470
|
}
|
441
471
|
};
|
442
472
|
|
443
|
-
export const createOrUpdateAttribute = async (
|
444
|
-
db: Databases,
|
445
|
-
dbId: string,
|
446
|
-
collection: Models.Collection,
|
447
|
-
attribute: Attribute
|
448
|
-
): Promise<
|
473
|
+
export const createOrUpdateAttribute = async (
|
474
|
+
db: Databases,
|
475
|
+
dbId: string,
|
476
|
+
collection: Models.Collection,
|
477
|
+
attribute: Attribute
|
478
|
+
): Promise<"queued" | "processed"> => {
|
449
479
|
let action = "create";
|
450
480
|
let foundAttribute: Attribute | undefined;
|
451
481
|
const updateEnabled = true;
|
@@ -460,13 +490,13 @@ export const createOrUpdateAttribute = async (
|
|
460
490
|
foundAttribute = undefined;
|
461
491
|
}
|
462
492
|
|
463
|
-
if (
|
464
|
-
foundAttribute &&
|
465
|
-
attributesSame(foundAttribute, attribute) &&
|
466
|
-
updateEnabled
|
467
|
-
) {
|
468
|
-
// No need to do anything, they are the same
|
469
|
-
return;
|
493
|
+
if (
|
494
|
+
foundAttribute &&
|
495
|
+
attributesSame(foundAttribute, attribute) &&
|
496
|
+
updateEnabled
|
497
|
+
) {
|
498
|
+
// No need to do anything, they are the same
|
499
|
+
return "processed";
|
470
500
|
} else if (
|
471
501
|
foundAttribute &&
|
472
502
|
!attributesSame(foundAttribute, attribute) &&
|
@@ -484,71 +514,82 @@ export const createOrUpdateAttribute = async (
|
|
484
514
|
!updateEnabled &&
|
485
515
|
foundAttribute &&
|
486
516
|
!attributesSame(foundAttribute, attribute)
|
487
|
-
) {
|
488
|
-
await db.deleteAttribute(dbId, collection.$id, attribute.key);
|
489
|
-
console.log(
|
490
|
-
`Deleted attribute: ${attribute.key} to recreate it because they diff (update disabled temporarily)`
|
491
|
-
);
|
492
|
-
return;
|
493
|
-
}
|
517
|
+
) {
|
518
|
+
await db.deleteAttribute(dbId, collection.$id, attribute.key);
|
519
|
+
console.log(
|
520
|
+
`Deleted attribute: ${attribute.key} to recreate it because they diff (update disabled temporarily)`
|
521
|
+
);
|
522
|
+
return "processed";
|
523
|
+
}
|
494
524
|
|
495
525
|
// console.log(`${action}-ing attribute: ${finalAttribute.key}`);
|
496
526
|
|
497
527
|
// Relationship attribute logic with adjustments
|
498
528
|
let collectionFoundViaRelatedCollection: Models.Collection | undefined;
|
499
529
|
let relatedCollectionId: string | undefined;
|
500
|
-
if (
|
501
|
-
finalAttribute.type === "relationship" &&
|
502
|
-
finalAttribute.relatedCollection
|
503
|
-
) {
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
|
511
|
-
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
nameToIdMapping
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
544
|
-
|
545
|
-
|
546
|
-
|
547
|
-
|
548
|
-
|
549
|
-
|
550
|
-
|
551
|
-
|
530
|
+
if (
|
531
|
+
finalAttribute.type === "relationship" &&
|
532
|
+
finalAttribute.relatedCollection
|
533
|
+
) {
|
534
|
+
// First try treating relatedCollection as an ID directly
|
535
|
+
try {
|
536
|
+
const byIdCollection = await db.getCollection(dbId, finalAttribute.relatedCollection);
|
537
|
+
collectionFoundViaRelatedCollection = byIdCollection;
|
538
|
+
relatedCollectionId = byIdCollection.$id;
|
539
|
+
// Cache by name for subsequent lookups
|
540
|
+
nameToIdMapping.set(byIdCollection.name, byIdCollection.$id);
|
541
|
+
} catch (_) {
|
542
|
+
// Not an ID or not found — fall back to name-based resolution below
|
543
|
+
}
|
544
|
+
|
545
|
+
if (!collectionFoundViaRelatedCollection && nameToIdMapping.has(finalAttribute.relatedCollection)) {
|
546
|
+
relatedCollectionId = nameToIdMapping.get(
|
547
|
+
finalAttribute.relatedCollection
|
548
|
+
);
|
549
|
+
try {
|
550
|
+
collectionFoundViaRelatedCollection = await db.getCollection(
|
551
|
+
dbId,
|
552
|
+
relatedCollectionId!
|
553
|
+
);
|
554
|
+
} catch (e) {
|
555
|
+
// console.log(
|
556
|
+
// `Collection not found: ${finalAttribute.relatedCollection} when nameToIdMapping was set`
|
557
|
+
// );
|
558
|
+
collectionFoundViaRelatedCollection = undefined;
|
559
|
+
}
|
560
|
+
} else if (!collectionFoundViaRelatedCollection) {
|
561
|
+
const collectionsPulled = await db.listCollections(dbId, [
|
562
|
+
Query.equal("name", finalAttribute.relatedCollection),
|
563
|
+
]);
|
564
|
+
if (collectionsPulled.total > 0) {
|
565
|
+
collectionFoundViaRelatedCollection = collectionsPulled.collections[0];
|
566
|
+
relatedCollectionId = collectionFoundViaRelatedCollection.$id;
|
567
|
+
nameToIdMapping.set(
|
568
|
+
finalAttribute.relatedCollection,
|
569
|
+
relatedCollectionId
|
570
|
+
);
|
571
|
+
}
|
572
|
+
}
|
573
|
+
// ONLY queue relationship attributes that have actual unresolved dependencies
|
574
|
+
if (!(relatedCollectionId && collectionFoundViaRelatedCollection)) {
|
575
|
+
console.log(
|
576
|
+
chalk.yellow(
|
577
|
+
`⏳ Queueing relationship attribute '${finalAttribute.key}' - related collection '${finalAttribute.relatedCollection}' not found yet`
|
578
|
+
)
|
579
|
+
);
|
580
|
+
enqueueOperation({
|
581
|
+
type: "attribute",
|
582
|
+
collectionId: collection.$id,
|
583
|
+
collection: collection,
|
584
|
+
attribute,
|
585
|
+
dependencies: [finalAttribute.relatedCollection],
|
586
|
+
});
|
587
|
+
return "queued";
|
588
|
+
}
|
589
|
+
}
|
590
|
+
finalAttribute = parseAttribute(finalAttribute);
|
591
|
+
// console.log(`Final Attribute: ${JSON.stringify(finalAttribute)}`);
|
592
|
+
switch (finalAttribute.type) {
|
552
593
|
case "string":
|
553
594
|
if (action === "create") {
|
554
595
|
await tryAwaitWithRetry(
|
@@ -624,27 +665,27 @@ export const createOrUpdateAttribute = async (
|
|
624
665
|
finalAttribute.array || false
|
625
666
|
)
|
626
667
|
);
|
627
|
-
} else {
|
628
|
-
if (
|
629
|
-
finalAttribute.min &&
|
630
|
-
BigInt(finalAttribute.min) === BigInt(-9223372036854776000)
|
631
|
-
) {
|
632
|
-
finalAttribute.min = undefined;
|
633
|
-
}
|
634
|
-
if (
|
635
|
-
finalAttribute.max &&
|
636
|
-
BigInt(finalAttribute.max) === BigInt(9223372036854776000)
|
637
|
-
) {
|
638
|
-
finalAttribute.max = undefined;
|
639
|
-
}
|
640
|
-
const minValue =
|
641
|
-
finalAttribute.min !== undefined && finalAttribute.min !== null
|
642
|
-
? parseInt(finalAttribute.min)
|
643
|
-
: 9007199254740991;
|
644
|
-
const maxValue =
|
645
|
-
finalAttribute.max !== undefined && finalAttribute.max !== null
|
646
|
-
? parseInt(finalAttribute.max)
|
647
|
-
: 9007199254740991;
|
668
|
+
} else {
|
669
|
+
if (
|
670
|
+
finalAttribute.min &&
|
671
|
+
BigInt(finalAttribute.min) === BigInt(-9223372036854776000)
|
672
|
+
) {
|
673
|
+
finalAttribute.min = undefined;
|
674
|
+
}
|
675
|
+
if (
|
676
|
+
finalAttribute.max &&
|
677
|
+
BigInt(finalAttribute.max) === BigInt(9223372036854776000)
|
678
|
+
) {
|
679
|
+
finalAttribute.max = undefined;
|
680
|
+
}
|
681
|
+
const minValue =
|
682
|
+
finalAttribute.min !== undefined && finalAttribute.min !== null
|
683
|
+
? parseInt(finalAttribute.min)
|
684
|
+
: -9007199254740991;
|
685
|
+
const maxValue =
|
686
|
+
finalAttribute.max !== undefined && finalAttribute.max !== null
|
687
|
+
? parseInt(finalAttribute.max)
|
688
|
+
: 9007199254740991;
|
648
689
|
console.log(
|
649
690
|
`DEBUG: Updating integer attribute '${
|
650
691
|
finalAttribute.key
|
@@ -910,11 +951,12 @@ export const createOrUpdateAttribute = async (
|
|
910
951
|
);
|
911
952
|
}
|
912
953
|
break;
|
913
|
-
default:
|
914
|
-
console.error("Invalid attribute type");
|
915
|
-
break;
|
916
|
-
}
|
917
|
-
|
954
|
+
default:
|
955
|
+
console.error("Invalid attribute type");
|
956
|
+
break;
|
957
|
+
}
|
958
|
+
return "processed";
|
959
|
+
};
|
918
960
|
|
919
961
|
/**
|
920
962
|
* Enhanced collection attribute creation with proper status monitoring
|