appwrite-utils-cli 1.4.0 → 1.5.0
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 +24 -6
- package/dist/collections/indexes.js +13 -3
- package/dist/collections/methods.d.ts +9 -0
- package/dist/collections/methods.js +268 -0
- 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 +28 -4
- package/package.json +2 -2
- package/src/adapters/TablesDBAdapter.ts +20 -17
- package/src/collections/attributes.ts +122 -99
- package/src/collections/indexes.ts +36 -28
- package/src/collections/methods.ts +292 -19
- 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 +44 -24
@@ -6,10 +6,10 @@ import {
|
|
6
6
|
Query,
|
7
7
|
type Models,
|
8
8
|
} from "node-appwrite";
|
9
|
-
import type { AppwriteConfig, CollectionCreate, Indexes } from "appwrite-utils";
|
10
|
-
import type { DatabaseAdapter } from "../adapters/DatabaseAdapter.js";
|
11
|
-
import { getAdapterFromConfig } from "../utils/getClientFromConfig.js";
|
12
|
-
import { nameToIdMapping, processQueue, queuedOperations } from "../shared/operationQueue.js";
|
9
|
+
import type { AppwriteConfig, CollectionCreate, Indexes, Attribute } from "appwrite-utils";
|
10
|
+
import type { DatabaseAdapter } from "../adapters/DatabaseAdapter.js";
|
11
|
+
import { getAdapterFromConfig } from "../utils/getClientFromConfig.js";
|
12
|
+
import { nameToIdMapping, processQueue, queuedOperations } from "../shared/operationQueue.js";
|
13
13
|
import { createUpdateCollectionAttributes, createUpdateCollectionAttributesWithStatusCheck } from "./attributes.js";
|
14
14
|
import { createOrUpdateIndexes, createOrUpdateIndexesWithStatusCheck } from "./indexes.js";
|
15
15
|
import { SchemaGenerator } from "../shared/schemaGenerator.js";
|
@@ -290,11 +290,11 @@ export const wipeDatabase = async (
|
|
290
290
|
return collectionsDeleted;
|
291
291
|
};
|
292
292
|
|
293
|
-
export const wipeCollection = async (
|
294
|
-
database: Databases,
|
295
|
-
databaseId: string,
|
296
|
-
collectionId: string
|
297
|
-
): Promise<void> => {
|
293
|
+
export const wipeCollection = async (
|
294
|
+
database: Databases,
|
295
|
+
databaseId: string,
|
296
|
+
collectionId: string
|
297
|
+
): Promise<void> => {
|
298
298
|
const collections = await database.listCollections(databaseId, [
|
299
299
|
Query.equal("$id", collectionId),
|
300
300
|
]);
|
@@ -303,8 +303,78 @@ export const wipeCollection = async (
|
|
303
303
|
return;
|
304
304
|
}
|
305
305
|
const collection = collections.collections[0];
|
306
|
-
await wipeDocumentsFromCollection(database, databaseId, collection.$id);
|
307
|
-
};
|
306
|
+
await wipeDocumentsFromCollection(database, databaseId, collection.$id);
|
307
|
+
};
|
308
|
+
|
309
|
+
// TablesDB helpers for wiping
|
310
|
+
export const wipeAllTables = async (
|
311
|
+
adapter: DatabaseAdapter,
|
312
|
+
databaseId: string
|
313
|
+
): Promise<{ tableId: string; tableName: string }[]> => {
|
314
|
+
MessageFormatter.info(`Wiping tables in database: ${databaseId}`, { prefix: 'Wipe' });
|
315
|
+
const res = await adapter.listTables({ databaseId, queries: [Query.limit(500)] });
|
316
|
+
const tables: any[] = (res as any).tables || [];
|
317
|
+
const deleted: { tableId: string; tableName: string }[] = [];
|
318
|
+
const progress = ProgressManager.create(`wipe-db-${databaseId}`, tables.length, { title: 'Deleting tables' });
|
319
|
+
let processed = 0;
|
320
|
+
for (const t of tables) {
|
321
|
+
try {
|
322
|
+
await adapter.deleteTable({ databaseId, tableId: t.$id });
|
323
|
+
deleted.push({ tableId: t.$id, tableName: t.name });
|
324
|
+
} catch (e) {
|
325
|
+
MessageFormatter.error(`Failed deleting table ${t.$id}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Wipe' });
|
326
|
+
}
|
327
|
+
processed++; progress.update(processed);
|
328
|
+
await delay(100);
|
329
|
+
}
|
330
|
+
progress.stop();
|
331
|
+
return deleted;
|
332
|
+
};
|
333
|
+
|
334
|
+
export const wipeTableRows = async (
|
335
|
+
adapter: DatabaseAdapter,
|
336
|
+
databaseId: string,
|
337
|
+
tableId: string
|
338
|
+
): Promise<void> => {
|
339
|
+
try {
|
340
|
+
const initial = await adapter.listRows({ databaseId, tableId, queries: [Query.limit(1000)] });
|
341
|
+
let rows: any[] = (initial as any).rows || [];
|
342
|
+
let total = rows.length;
|
343
|
+
let cursor = rows.length >= 1000 ? rows[rows.length - 1].$id : undefined;
|
344
|
+
while (cursor) {
|
345
|
+
const resp = await adapter.listRows({ databaseId, tableId, queries: [Query.limit(1000), ...(cursor ? [Query.cursorAfter(cursor)] : [])] });
|
346
|
+
const more: any[] = (resp as any).rows || [];
|
347
|
+
rows.push(...more);
|
348
|
+
total = rows.length;
|
349
|
+
cursor = more.length >= 1000 ? more[more.length - 1].$id : undefined;
|
350
|
+
if (total % 10000 === 0) {
|
351
|
+
MessageFormatter.progress(`Found ${total} rows...`, { prefix: 'Wipe' });
|
352
|
+
}
|
353
|
+
}
|
354
|
+
MessageFormatter.info(`Found ${total} rows to delete`, { prefix: 'Wipe' });
|
355
|
+
if (total === 0) return;
|
356
|
+
const progress = ProgressManager.create(`delete-${tableId}`, total, { title: 'Deleting rows' });
|
357
|
+
let processed = 0;
|
358
|
+
const maxStackSize = 50;
|
359
|
+
const batches = chunk(rows, maxStackSize);
|
360
|
+
for (const batch of batches) {
|
361
|
+
await Promise.all(batch.map(async (row: any) => {
|
362
|
+
try {
|
363
|
+
await adapter.deleteRow({ databaseId, tableId, id: row.$id });
|
364
|
+
} catch (e: any) {
|
365
|
+
// ignore missing rows
|
366
|
+
}
|
367
|
+
processed++; progress.update(processed);
|
368
|
+
}));
|
369
|
+
await delay(50);
|
370
|
+
}
|
371
|
+
progress.stop();
|
372
|
+
MessageFormatter.success(`Completed deletion of ${total} rows from table ${tableId}`, { prefix: 'Wipe' });
|
373
|
+
} catch (error) {
|
374
|
+
MessageFormatter.error(`Error wiping rows from table ${tableId}`, error instanceof Error ? error : new Error(String(error)), { prefix: 'Wipe' });
|
375
|
+
throw error;
|
376
|
+
}
|
377
|
+
};
|
308
378
|
|
309
379
|
export const generateSchemas = async (
|
310
380
|
config: AppwriteConfig,
|
@@ -314,13 +384,23 @@ export const generateSchemas = async (
|
|
314
384
|
schemaGenerator.generateSchemas();
|
315
385
|
};
|
316
386
|
|
317
|
-
export const createOrUpdateCollections = async (
|
318
|
-
database: Databases,
|
319
|
-
databaseId: string,
|
320
|
-
config: AppwriteConfig,
|
321
|
-
deletedCollections?: { collectionId: string; collectionName: string }[],
|
322
|
-
selectedCollections: Models.Collection[] = []
|
323
|
-
): Promise<void> => {
|
387
|
+
export const createOrUpdateCollections = async (
|
388
|
+
database: Databases,
|
389
|
+
databaseId: string,
|
390
|
+
config: AppwriteConfig,
|
391
|
+
deletedCollections?: { collectionId: string; collectionName: string }[],
|
392
|
+
selectedCollections: Models.Collection[] = []
|
393
|
+
): Promise<void> => {
|
394
|
+
// If API mode is tablesdb, route to adapter-based implementation
|
395
|
+
try {
|
396
|
+
const { adapter, apiMode } = await getAdapterFromConfig(config);
|
397
|
+
if (apiMode === 'tablesdb') {
|
398
|
+
await createOrUpdateCollectionsViaAdapter(adapter, databaseId, config, deletedCollections, selectedCollections);
|
399
|
+
return;
|
400
|
+
}
|
401
|
+
} catch {
|
402
|
+
// Fallback to legacy path below
|
403
|
+
}
|
324
404
|
const collectionsToProcess =
|
325
405
|
selectedCollections.length > 0 ? selectedCollections : config.collections;
|
326
406
|
if (!collectionsToProcess) {
|
@@ -472,7 +552,200 @@ export const createOrUpdateCollections = async (
|
|
472
552
|
} else {
|
473
553
|
MessageFormatter.info("No queued operations to process", { prefix: "Collections" });
|
474
554
|
}
|
475
|
-
};
|
555
|
+
};
|
556
|
+
|
557
|
+
// New: Adapter-based implementation for TablesDB
|
558
|
+
export const createOrUpdateCollectionsViaAdapter = async (
|
559
|
+
adapter: DatabaseAdapter,
|
560
|
+
databaseId: string,
|
561
|
+
config: AppwriteConfig,
|
562
|
+
deletedCollections?: { collectionId: string; collectionName: string }[],
|
563
|
+
selectedCollections: Models.Collection[] = []
|
564
|
+
): Promise<void> => {
|
565
|
+
const collectionsToProcess =
|
566
|
+
selectedCollections.length > 0 ? selectedCollections : (config.collections || []);
|
567
|
+
if (!collectionsToProcess || collectionsToProcess.length === 0) return;
|
568
|
+
|
569
|
+
const usedIds = new Set<string>();
|
570
|
+
|
571
|
+
// Helper: create attributes through adapter
|
572
|
+
const createAttr = async (tableId: string, attr: Attribute) => {
|
573
|
+
const base: any = {
|
574
|
+
databaseId,
|
575
|
+
tableId,
|
576
|
+
key: attr.key,
|
577
|
+
type: (attr as any).type,
|
578
|
+
size: (attr as any).size,
|
579
|
+
required: !!(attr as any).required,
|
580
|
+
default: (attr as any).xdefault,
|
581
|
+
array: !!(attr as any).array,
|
582
|
+
min: (attr as any).min,
|
583
|
+
max: (attr as any).max,
|
584
|
+
elements: (attr as any).elements,
|
585
|
+
encrypt: (attr as any).encrypted,
|
586
|
+
relatedCollection: (attr as any).relatedCollection,
|
587
|
+
relationType: (attr as any).relationType,
|
588
|
+
twoWay: (attr as any).twoWay,
|
589
|
+
twoWayKey: (attr as any).twoWayKey,
|
590
|
+
onDelete: (attr as any).onDelete,
|
591
|
+
side: (attr as any).side,
|
592
|
+
};
|
593
|
+
await adapter.createAttribute(base);
|
594
|
+
await delay(150);
|
595
|
+
};
|
596
|
+
|
597
|
+
// Local queue for unresolved relationships
|
598
|
+
const relQueue: { tableId: string; attr: Attribute }[] = [];
|
599
|
+
|
600
|
+
for (const collection of collectionsToProcess) {
|
601
|
+
const { attributes, indexes, ...collectionData } = collection as any;
|
602
|
+
|
603
|
+
// Prepare permissions as strings (reuse Permission helper)
|
604
|
+
const permissions: string[] = [];
|
605
|
+
if (collection.$permissions && collection.$permissions.length > 0) {
|
606
|
+
for (const p of collection.$permissions as any[]) {
|
607
|
+
if (typeof p === 'string') permissions.push(p);
|
608
|
+
else {
|
609
|
+
switch (p.permission) {
|
610
|
+
case 'read': permissions.push(Permission.read(p.target)); break;
|
611
|
+
case 'create': permissions.push(Permission.create(p.target)); break;
|
612
|
+
case 'update': permissions.push(Permission.update(p.target)); break;
|
613
|
+
case 'delete': permissions.push(Permission.delete(p.target)); break;
|
614
|
+
case 'write': permissions.push(Permission.write(p.target)); break;
|
615
|
+
default: break;
|
616
|
+
}
|
617
|
+
}
|
618
|
+
}
|
619
|
+
}
|
620
|
+
|
621
|
+
// Find existing table by name
|
622
|
+
const list = await adapter.listTables({ databaseId, queries: [Query.equal('name', collectionData.name)] });
|
623
|
+
const items: any[] = (list as any).tables || [];
|
624
|
+
let table = items[0];
|
625
|
+
let tableId: string;
|
626
|
+
|
627
|
+
if (!table) {
|
628
|
+
// Determine ID (prefer provided $id or re-use deleted one)
|
629
|
+
let foundColl = deletedCollections?.find(
|
630
|
+
(coll) => coll.collectionName.toLowerCase().trim().replace(" ", "") === collectionData.name.toLowerCase().trim().replace(" ", "")
|
631
|
+
);
|
632
|
+
if (collectionData.$id) tableId = collectionData.$id;
|
633
|
+
else if (foundColl && !usedIds.has(foundColl.collectionId)) tableId = foundColl.collectionId;
|
634
|
+
else tableId = ID.unique();
|
635
|
+
usedIds.add(tableId);
|
636
|
+
|
637
|
+
const res = await adapter.createTable({
|
638
|
+
databaseId,
|
639
|
+
id: tableId,
|
640
|
+
name: collectionData.name,
|
641
|
+
permissions,
|
642
|
+
documentSecurity: !!collectionData.documentSecurity,
|
643
|
+
enabled: collectionData.enabled !== false
|
644
|
+
});
|
645
|
+
table = (res as any).data || res;
|
646
|
+
nameToIdMapping.set(collectionData.name, tableId);
|
647
|
+
} else {
|
648
|
+
tableId = table.$id;
|
649
|
+
await adapter.updateTable({
|
650
|
+
databaseId,
|
651
|
+
id: tableId,
|
652
|
+
name: collectionData.name,
|
653
|
+
permissions,
|
654
|
+
documentSecurity: !!collectionData.documentSecurity,
|
655
|
+
enabled: collectionData.enabled !== false
|
656
|
+
});
|
657
|
+
}
|
658
|
+
|
659
|
+
// Add small delay after table create/update
|
660
|
+
await delay(250);
|
661
|
+
|
662
|
+
// Create attributes: non-relationship first
|
663
|
+
const nonRel = (attributes || []).filter((a: Attribute) => a.type !== 'relationship');
|
664
|
+
for (const attr of nonRel) {
|
665
|
+
await createAttr(tableId, attr as Attribute);
|
666
|
+
}
|
667
|
+
|
668
|
+
// Relationship attributes — resolve relatedCollection to ID
|
669
|
+
const rels = (attributes || []).filter((a: Attribute) => a.type === 'relationship');
|
670
|
+
for (const attr of rels as any[]) {
|
671
|
+
const relNameOrId = attr.relatedCollection as string | undefined;
|
672
|
+
if (!relNameOrId) continue;
|
673
|
+
let relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
|
674
|
+
|
675
|
+
// If looks like a name (not ULID) and not in cache, try query by name
|
676
|
+
if (!nameToIdMapping.has(relNameOrId)) {
|
677
|
+
try {
|
678
|
+
const relList = await adapter.listTables({ databaseId, queries: [Query.equal('name', relNameOrId)] });
|
679
|
+
const relItems: any[] = (relList as any).tables || [];
|
680
|
+
if (relItems[0]?.$id) {
|
681
|
+
relId = relItems[0].$id;
|
682
|
+
nameToIdMapping.set(relNameOrId, relId);
|
683
|
+
}
|
684
|
+
} catch {}
|
685
|
+
}
|
686
|
+
|
687
|
+
if (relId && typeof relId === 'string') {
|
688
|
+
attr.relatedCollection = relId;
|
689
|
+
await createAttr(tableId, attr as Attribute);
|
690
|
+
} else {
|
691
|
+
// Defer if unresolved
|
692
|
+
relQueue.push({ tableId, attr: attr as Attribute });
|
693
|
+
}
|
694
|
+
}
|
695
|
+
|
696
|
+
// Indexes
|
697
|
+
const idxs = (indexes || []) as any[];
|
698
|
+
for (const idx of idxs) {
|
699
|
+
try {
|
700
|
+
await adapter.createIndex({
|
701
|
+
databaseId,
|
702
|
+
tableId,
|
703
|
+
key: idx.key,
|
704
|
+
type: idx.type,
|
705
|
+
attributes: idx.attributes,
|
706
|
+
orders: idx.orders || []
|
707
|
+
});
|
708
|
+
await delay(150);
|
709
|
+
} catch (e) {
|
710
|
+
MessageFormatter.error(`Failed to create index ${idx.key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Indexes' });
|
711
|
+
}
|
712
|
+
}
|
713
|
+
}
|
714
|
+
|
715
|
+
// Process queued relationships once mapping likely populated
|
716
|
+
for (const { tableId, attr } of relQueue) {
|
717
|
+
const relNameOrId = (attr as any).relatedCollection as string | undefined;
|
718
|
+
if (!relNameOrId) continue;
|
719
|
+
const relId = nameToIdMapping.get(relNameOrId) || relNameOrId;
|
720
|
+
if (relId) {
|
721
|
+
(attr as any).relatedCollection = relId;
|
722
|
+
try {
|
723
|
+
await adapter.createAttribute({
|
724
|
+
databaseId,
|
725
|
+
tableId,
|
726
|
+
key: (attr as any).key,
|
727
|
+
type: (attr as any).type,
|
728
|
+
size: (attr as any).size,
|
729
|
+
required: !!(attr as any).required,
|
730
|
+
default: (attr as any).xdefault,
|
731
|
+
array: !!(attr as any).array,
|
732
|
+
min: (attr as any).min,
|
733
|
+
max: (attr as any).max,
|
734
|
+
elements: (attr as any).elements,
|
735
|
+
relatedCollection: relId,
|
736
|
+
relationType: (attr as any).relationType,
|
737
|
+
twoWay: (attr as any).twoWay,
|
738
|
+
twoWayKey: (attr as any).twoWayKey,
|
739
|
+
onDelete: (attr as any).onDelete,
|
740
|
+
side: (attr as any).side
|
741
|
+
});
|
742
|
+
await delay(150);
|
743
|
+
} catch (e) {
|
744
|
+
MessageFormatter.error(`Failed queued relationship ${attr.key}`, e instanceof Error ? e : new Error(String(e)), { prefix: 'Attributes' });
|
745
|
+
}
|
746
|
+
}
|
747
|
+
}
|
748
|
+
};
|
476
749
|
|
477
750
|
export const generateMockData = async (
|
478
751
|
database: Databases,
|
@@ -16,7 +16,8 @@ import {
|
|
16
16
|
AppwriteException,
|
17
17
|
} from "node-appwrite";
|
18
18
|
import { InputFile } from "node-appwrite/file";
|
19
|
-
import { MessageFormatter } from "../shared/messageFormatter.js";
|
19
|
+
import { MessageFormatter } from "../shared/messageFormatter.js";
|
20
|
+
import { processQueue, queuedOperations } from "../shared/operationQueue.js";
|
20
21
|
import { ProgressManager } from "../shared/progressManager.js";
|
21
22
|
import { getClient } from "../utils/getClientFromConfig.js";
|
22
23
|
import {
|
@@ -624,13 +625,26 @@ export class ComprehensiveTransfer {
|
|
624
625
|
{ prefix: "Transfer" }
|
625
626
|
);
|
626
627
|
}
|
627
|
-
}
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
628
|
+
}
|
629
|
+
// After processing all collections' attributes and indexes, process any queued
|
630
|
+
// relationship attributes so dependencies are resolved within this phase.
|
631
|
+
if (queuedOperations.length > 0) {
|
632
|
+
MessageFormatter.info(
|
633
|
+
`Processing ${queuedOperations.length} queued relationship operations`,
|
634
|
+
{ prefix: "Transfer" }
|
635
|
+
);
|
636
|
+
await processQueue(this.targetDatabases, dbId);
|
637
|
+
} else {
|
638
|
+
MessageFormatter.info("No queued relationship operations to process", {
|
639
|
+
prefix: "Transfer",
|
640
|
+
});
|
641
|
+
}
|
642
|
+
} catch (error) {
|
643
|
+
MessageFormatter.error(
|
644
|
+
`Failed to create database structure for ${dbId}`,
|
645
|
+
error instanceof Error ? error : new Error(String(error)),
|
646
|
+
{ prefix: "Transfer" }
|
647
|
+
);
|
634
648
|
throw error;
|
635
649
|
}
|
636
650
|
}
|
@@ -33,16 +33,23 @@ export interface JsonSchema {
|
|
33
33
|
definitions?: Record<string, JsonSchemaProperty>;
|
34
34
|
}
|
35
35
|
|
36
|
-
export class JsonSchemaGenerator {
|
37
|
-
private config: AppwriteConfig;
|
38
|
-
private appwriteFolderPath: string;
|
39
|
-
private relationshipMap = new Map<string, any[]>();
|
40
|
-
|
41
|
-
constructor(config: AppwriteConfig, appwriteFolderPath: string) {
|
42
|
-
this.config = config;
|
43
|
-
this.appwriteFolderPath = appwriteFolderPath;
|
44
|
-
this.extractRelationships();
|
45
|
-
}
|
36
|
+
export class JsonSchemaGenerator {
|
37
|
+
private config: AppwriteConfig;
|
38
|
+
private appwriteFolderPath: string;
|
39
|
+
private relationshipMap = new Map<string, any[]>();
|
40
|
+
|
41
|
+
constructor(config: AppwriteConfig, appwriteFolderPath: string) {
|
42
|
+
this.config = config;
|
43
|
+
this.appwriteFolderPath = appwriteFolderPath;
|
44
|
+
this.extractRelationships();
|
45
|
+
}
|
46
|
+
|
47
|
+
private resolveCollectionName = (idOrName: string): string => {
|
48
|
+
const col = this.config.collections?.find(
|
49
|
+
(c) => c.$id === (idOrName as any) || c.name === idOrName
|
50
|
+
);
|
51
|
+
return col?.name ?? idOrName;
|
52
|
+
};
|
46
53
|
|
47
54
|
private extractRelationships(): void {
|
48
55
|
if (!this.config.collections) return;
|
@@ -51,16 +58,16 @@ export class JsonSchemaGenerator {
|
|
51
58
|
if (!collection.attributes) return;
|
52
59
|
|
53
60
|
collection.attributes.forEach((attr) => {
|
54
|
-
if (attr.type === "relationship" && attr.relatedCollection) {
|
55
|
-
const relationships = this.relationshipMap.get(collection.name) || [];
|
56
|
-
relationships.push({
|
57
|
-
attributeKey: attr.key,
|
58
|
-
relatedCollection: attr.relatedCollection,
|
59
|
-
relationType: attr.relationType,
|
60
|
-
isArray: attr.relationType === "oneToMany" || attr.relationType === "manyToMany"
|
61
|
-
});
|
62
|
-
this.relationshipMap.set(collection.name, relationships);
|
63
|
-
}
|
61
|
+
if (attr.type === "relationship" && attr.relatedCollection) {
|
62
|
+
const relationships = this.relationshipMap.get(collection.name) || [];
|
63
|
+
relationships.push({
|
64
|
+
attributeKey: attr.key,
|
65
|
+
relatedCollection: this.resolveCollectionName(attr.relatedCollection),
|
66
|
+
relationType: attr.relationType,
|
67
|
+
isArray: attr.relationType === "oneToMany" || attr.relationType === "manyToMany"
|
68
|
+
});
|
69
|
+
this.relationshipMap.set(collection.name, relationships);
|
70
|
+
}
|
64
71
|
});
|
65
72
|
});
|
66
73
|
}
|
@@ -153,14 +160,14 @@ export class JsonSchemaGenerator {
|
|
153
160
|
}
|
154
161
|
break;
|
155
162
|
|
156
|
-
case "relationship":
|
157
|
-
if (attribute.relatedCollection) {
|
158
|
-
// For relationships, reference the related collection schema
|
159
|
-
schema.$ref = `#/definitions/${toPascalCase(attribute.relatedCollection)}`;
|
160
|
-
} else {
|
161
|
-
schema.type = "string";
|
162
|
-
}
|
163
|
-
break;
|
163
|
+
case "relationship":
|
164
|
+
if (attribute.relatedCollection) {
|
165
|
+
// For relationships, reference the related collection schema
|
166
|
+
schema.$ref = `#/definitions/${toPascalCase(this.resolveCollectionName(attribute.relatedCollection))}`;
|
167
|
+
} else {
|
168
|
+
schema.type = "string";
|
169
|
+
}
|
170
|
+
break;
|
164
171
|
|
165
172
|
default:
|
166
173
|
schema.type = "string";
|
@@ -385,4 +392,4 @@ export default jsonSchemas;
|
|
385
392
|
|
386
393
|
return { valid: errors.length === 0, errors };
|
387
394
|
}
|
388
|
-
}
|
395
|
+
}
|
@@ -33,39 +33,54 @@ export const processQueue = async (db: Databases, dbId: string) => {
|
|
33
33
|
let collectionFound: Models.Collection | undefined;
|
34
34
|
|
35
35
|
// Handle relationship attribute operations
|
36
|
-
if (operation.attribute?.type === "relationship") {
|
37
|
-
// Attempt to resolve the collection directly if collectionId is specified
|
38
|
-
if (operation.collectionId) {
|
39
|
-
console.log(`\tFetching collection by ID: ${operation.collectionId}`);
|
40
|
-
try {
|
41
|
-
collectionFound = await tryAwaitWithRetry(
|
42
|
-
async () => await db.getCollection(dbId, operation.collectionId!)
|
43
|
-
);
|
44
|
-
} catch (e) {
|
45
|
-
console.log(
|
46
|
-
`\tCollection not found by ID: ${operation.collectionId}`
|
47
|
-
);
|
48
|
-
}
|
49
|
-
}
|
50
|
-
// Attempt to resolve related collection if specified and not already found
|
51
|
-
if (!collectionFound && operation.attribute?.relatedCollection) {
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
36
|
+
if (operation.attribute?.type === "relationship") {
|
37
|
+
// Attempt to resolve the collection directly if collectionId is specified
|
38
|
+
if (operation.collectionId) {
|
39
|
+
console.log(`\tFetching collection by ID: ${operation.collectionId}`);
|
40
|
+
try {
|
41
|
+
collectionFound = await tryAwaitWithRetry(
|
42
|
+
async () => await db.getCollection(dbId, operation.collectionId!)
|
43
|
+
);
|
44
|
+
} catch (e) {
|
45
|
+
console.log(
|
46
|
+
`\tCollection not found by ID: ${operation.collectionId}`
|
47
|
+
);
|
48
|
+
}
|
49
|
+
}
|
50
|
+
// Attempt to resolve related collection if specified and not already found
|
51
|
+
if (!collectionFound && operation.attribute?.relatedCollection) {
|
52
|
+
// First, try treating relatedCollection as an ID
|
53
|
+
try {
|
54
|
+
const relAttr: any = operation.attribute as any;
|
55
|
+
const byId = await tryAwaitWithRetry(
|
56
|
+
async () => await db.getCollection(dbId, relAttr.relatedCollection as string)
|
57
|
+
);
|
58
|
+
// We still need the target collection (operation.collectionId) to create the attribute on,
|
59
|
+
// so only use this branch to warm caches/mappings and continue to dependency checks.
|
60
|
+
// Do not override collectionFound with the related collection.
|
61
|
+
} catch (_) {
|
62
|
+
// Not an ID or not found; fall back to name-based cache
|
63
|
+
}
|
64
|
+
|
65
|
+
// Warm cache by name (used by attribute creation path), but do not use as target collection
|
66
|
+
const relAttr: any = operation.attribute as any;
|
67
|
+
await fetchAndCacheCollectionByName(
|
68
|
+
db,
|
69
|
+
dbId,
|
70
|
+
relAttr.relatedCollection
|
71
|
+
);
|
72
|
+
}
|
73
|
+
// Handle dependencies if collection still not found
|
74
|
+
if (!collectionFound) {
|
75
|
+
for (const dep of operation.dependencies || []) {
|
76
|
+
collectionFound = await fetchAndCacheCollectionByName(
|
77
|
+
db,
|
78
|
+
dbId,
|
79
|
+
dep
|
80
|
+
);
|
81
|
+
if (collectionFound) break; // Break early if collection is found
|
82
|
+
}
|
83
|
+
}
|
69
84
|
} else if (operation.collectionId) {
|
70
85
|
// Handle non-relationship operations with a specified collectionId
|
71
86
|
console.log(`\tFetching collection by ID: ${operation.collectionId}`);
|