@njdamstra/appwrite-utils-cli 1.11.4 → 1.11.6
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.
|
@@ -290,7 +290,7 @@ export async function executeMigrationPlan(adapter, options) {
|
|
|
290
290
|
},
|
|
291
291
|
]);
|
|
292
292
|
if (updateYaml) {
|
|
293
|
-
|
|
293
|
+
await updateCollectionYaml(first.collectionName, entries, checkpoint);
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
}
|
|
@@ -320,7 +320,7 @@ async function migrateOneAttribute(adapter, entry, cpEntry, checkpoint, checkpoi
|
|
|
320
320
|
MessageFormatter.info(` Creating backup attribute ${backupKey}...`, {
|
|
321
321
|
prefix: "Migrate",
|
|
322
322
|
});
|
|
323
|
-
await
|
|
323
|
+
await createAttributeIfNotExists(adapter, {
|
|
324
324
|
databaseId,
|
|
325
325
|
tableId: collectionId,
|
|
326
326
|
key: backupKey,
|
|
@@ -328,7 +328,7 @@ async function migrateOneAttribute(adapter, entry, cpEntry, checkpoint, checkpoi
|
|
|
328
328
|
size: entry.currentSize,
|
|
329
329
|
required: false, // always optional for backup
|
|
330
330
|
array: entry.isArray,
|
|
331
|
-
})
|
|
331
|
+
});
|
|
332
332
|
const available = await waitForAttribute(adapter, databaseId, collectionId, backupKey);
|
|
333
333
|
if (!available)
|
|
334
334
|
throw new Error(`Backup attribute ${backupKey} stuck`);
|
|
@@ -385,7 +385,7 @@ async function migrateOneAttribute(adapter, entry, cpEntry, checkpoint, checkpoi
|
|
|
385
385
|
if (entry.hasDefault && entry.defaultValue !== undefined) {
|
|
386
386
|
createParams.default = entry.defaultValue;
|
|
387
387
|
}
|
|
388
|
-
await
|
|
388
|
+
await createAttributeIfNotExists(adapter, createParams);
|
|
389
389
|
const available = await waitForAttribute(adapter, databaseId, collectionId, attributeKey);
|
|
390
390
|
if (!available)
|
|
391
391
|
throw new Error(`New attribute ${attributeKey} stuck after creation`);
|
|
@@ -507,7 +507,7 @@ async function verifyDataCopy(adapter, databaseId, collectionId, sourceKey, targ
|
|
|
507
507
|
for (const doc of docs) {
|
|
508
508
|
if (!(sourceKey in doc))
|
|
509
509
|
continue;
|
|
510
|
-
if (doc[sourceKey] !== doc[targetKey]) {
|
|
510
|
+
if (JSON.stringify(doc[sourceKey]) !== JSON.stringify(doc[targetKey])) {
|
|
511
511
|
throw new Error(`Verification failed: doc ${doc.$id} has ${sourceKey}=${JSON.stringify(doc[sourceKey])} but ${targetKey}=${JSON.stringify(doc[targetKey])}`);
|
|
512
512
|
}
|
|
513
513
|
}
|
|
@@ -515,7 +515,7 @@ async function verifyDataCopy(adapter, databaseId, collectionId, sourceKey, targ
|
|
|
515
515
|
// ────────────────────────────────────────────────────────
|
|
516
516
|
// Helper: wait for attribute to become available
|
|
517
517
|
// ────────────────────────────────────────────────────────
|
|
518
|
-
async function waitForAttribute(adapter, databaseId, collectionId, key, maxWaitMs =
|
|
518
|
+
async function waitForAttribute(adapter, databaseId, collectionId, key, maxWaitMs = 120_000) {
|
|
519
519
|
const start = Date.now();
|
|
520
520
|
const checkInterval = 2000;
|
|
521
521
|
while (Date.now() - start < maxWaitMs) {
|
|
@@ -724,8 +724,181 @@ function printDryRunSummary(plan) {
|
|
|
724
724
|
console.log("");
|
|
725
725
|
}
|
|
726
726
|
// ────────────────────────────────────────────────────────
|
|
727
|
+
// Update local collection YAML after migration
|
|
728
|
+
// ────────────────────────────────────────────────────────
|
|
729
|
+
async function updateCollectionYaml(collectionName, entries, checkpoint) {
|
|
730
|
+
// Find candidate YAML files
|
|
731
|
+
const candidates = findYamlFiles(process.cwd(), collectionName);
|
|
732
|
+
if (candidates.length === 0) {
|
|
733
|
+
MessageFormatter.warning(`No YAML file found for collection "${collectionName}". Skipping local config update.`, { prefix: "YAML" });
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
let yamlPath;
|
|
737
|
+
if (candidates.length === 1) {
|
|
738
|
+
const { usePath } = await inquirer.prompt([
|
|
739
|
+
{
|
|
740
|
+
type: "confirm",
|
|
741
|
+
name: "usePath",
|
|
742
|
+
message: `Found: ${candidates[0]}. Use this file?`,
|
|
743
|
+
default: true,
|
|
744
|
+
},
|
|
745
|
+
]);
|
|
746
|
+
if (!usePath) {
|
|
747
|
+
const { customPath } = await inquirer.prompt([
|
|
748
|
+
{
|
|
749
|
+
type: "input",
|
|
750
|
+
name: "customPath",
|
|
751
|
+
message: "Enter path to collection YAML file:",
|
|
752
|
+
},
|
|
753
|
+
]);
|
|
754
|
+
if (!customPath || !fs.existsSync(customPath)) {
|
|
755
|
+
MessageFormatter.warning("Invalid path. Skipping YAML update.", {
|
|
756
|
+
prefix: "YAML",
|
|
757
|
+
});
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
yamlPath = customPath;
|
|
761
|
+
}
|
|
762
|
+
else {
|
|
763
|
+
yamlPath = candidates[0];
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
const { selectedPath } = await inquirer.prompt([
|
|
768
|
+
{
|
|
769
|
+
type: "list",
|
|
770
|
+
name: "selectedPath",
|
|
771
|
+
message: `Multiple YAML files found for "${collectionName}". Select one:`,
|
|
772
|
+
choices: [
|
|
773
|
+
...candidates.map((c) => ({ name: c, value: c })),
|
|
774
|
+
{ name: "Enter custom path", value: "__custom__" },
|
|
775
|
+
],
|
|
776
|
+
},
|
|
777
|
+
]);
|
|
778
|
+
if (selectedPath === "__custom__") {
|
|
779
|
+
const { customPath } = await inquirer.prompt([
|
|
780
|
+
{
|
|
781
|
+
type: "input",
|
|
782
|
+
name: "customPath",
|
|
783
|
+
message: "Enter path to collection YAML file:",
|
|
784
|
+
},
|
|
785
|
+
]);
|
|
786
|
+
if (!customPath || !fs.existsSync(customPath)) {
|
|
787
|
+
MessageFormatter.warning("Invalid path. Skipping YAML update.", {
|
|
788
|
+
prefix: "YAML",
|
|
789
|
+
});
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
yamlPath = customPath;
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
yamlPath = selectedPath;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
// Load and parse YAML
|
|
799
|
+
let doc;
|
|
800
|
+
try {
|
|
801
|
+
const content = fs.readFileSync(yamlPath, "utf8");
|
|
802
|
+
doc = yaml.load(content);
|
|
803
|
+
}
|
|
804
|
+
catch (err) {
|
|
805
|
+
MessageFormatter.error(`Failed to parse ${yamlPath}: ${err.message}`, undefined, { prefix: "YAML" });
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
if (!doc || !Array.isArray(doc.attributes)) {
|
|
809
|
+
MessageFormatter.warning(`No "attributes" array found in ${yamlPath}. Skipping.`, { prefix: "YAML" });
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
// Update attributes that were successfully migrated
|
|
813
|
+
let updated = 0;
|
|
814
|
+
for (const entry of entries) {
|
|
815
|
+
const cp = findCheckpointEntry(checkpoint, entry);
|
|
816
|
+
if (cp?.phase !== "completed")
|
|
817
|
+
continue;
|
|
818
|
+
const attr = doc.attributes.find((a) => a.key === entry.attributeKey && a.type === "string");
|
|
819
|
+
if (!attr)
|
|
820
|
+
continue;
|
|
821
|
+
attr.type = entry.targetType;
|
|
822
|
+
if (entry.targetType !== "varchar") {
|
|
823
|
+
delete attr.size;
|
|
824
|
+
}
|
|
825
|
+
updated++;
|
|
826
|
+
}
|
|
827
|
+
if (updated === 0) {
|
|
828
|
+
MessageFormatter.info("No matching string attributes found in YAML to update.", { prefix: "YAML" });
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
// Write back
|
|
832
|
+
try {
|
|
833
|
+
const output = yaml.dump(doc, {
|
|
834
|
+
lineWidth: 120,
|
|
835
|
+
noRefs: true,
|
|
836
|
+
sortKeys: false,
|
|
837
|
+
});
|
|
838
|
+
fs.writeFileSync(yamlPath, output, "utf8");
|
|
839
|
+
MessageFormatter.success(`Updated ${updated} attribute(s) in ${yamlPath}`, { prefix: "YAML" });
|
|
840
|
+
}
|
|
841
|
+
catch (err) {
|
|
842
|
+
MessageFormatter.error(`Failed to write ${yamlPath}: ${err.message}`, undefined, { prefix: "YAML" });
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
function findYamlFiles(searchRoot, collectionName) {
|
|
846
|
+
const results = [];
|
|
847
|
+
const lowerName = collectionName.toLowerCase();
|
|
848
|
+
function walk(dir) {
|
|
849
|
+
let dirEntries;
|
|
850
|
+
try {
|
|
851
|
+
dirEntries = fs.readdirSync(dir, { withFileTypes: true });
|
|
852
|
+
}
|
|
853
|
+
catch {
|
|
854
|
+
return; // skip inaccessible directories
|
|
855
|
+
}
|
|
856
|
+
for (const ent of dirEntries) {
|
|
857
|
+
const fullPath = path.join(dir, ent.name);
|
|
858
|
+
if (ent.isDirectory()) {
|
|
859
|
+
// Skip node_modules, .git, and other common non-config dirs
|
|
860
|
+
if (ent.name === "node_modules" || ent.name === ".git" || ent.name === "dist")
|
|
861
|
+
continue;
|
|
862
|
+
walk(fullPath);
|
|
863
|
+
}
|
|
864
|
+
else if (ent.isFile() &&
|
|
865
|
+
ent.name.toLowerCase() === `${lowerName}.yaml`) {
|
|
866
|
+
// Verify it looks like a collection config (has a name field)
|
|
867
|
+
try {
|
|
868
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
869
|
+
const parsed = yaml.load(content);
|
|
870
|
+
if (parsed && parsed.name === collectionName) {
|
|
871
|
+
results.push(fullPath);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
catch {
|
|
875
|
+
// skip unparseable files
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
walk(searchRoot);
|
|
881
|
+
return results;
|
|
882
|
+
}
|
|
883
|
+
// ────────────────────────────────────────────────────────
|
|
727
884
|
// Utility
|
|
728
885
|
// ────────────────────────────────────────────────────────
|
|
886
|
+
async function createAttributeIfNotExists(adapter, params) {
|
|
887
|
+
try {
|
|
888
|
+
await tryAwaitWithRetry(() => adapter.createAttribute(params), 0, true);
|
|
889
|
+
}
|
|
890
|
+
catch (err) {
|
|
891
|
+
const code = err?.code || err?.originalError?.code;
|
|
892
|
+
const type = err?.originalError?.type || "";
|
|
893
|
+
if (code === 409 || type === "column_already_exists") {
|
|
894
|
+
MessageFormatter.info(` (backup attribute already exists, reusing)`, {
|
|
895
|
+
prefix: "Migrate",
|
|
896
|
+
});
|
|
897
|
+
return;
|
|
898
|
+
}
|
|
899
|
+
throw err;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
729
902
|
function delay(ms) {
|
|
730
903
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
731
904
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@njdamstra/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.11.
|
|
4
|
+
"version": "1.11.6",
|
|
5
5
|
"main": "dist/main.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"repository": {
|
|
@@ -398,10 +398,7 @@ export async function executeMigrationPlan(
|
|
|
398
398
|
},
|
|
399
399
|
]);
|
|
400
400
|
if (updateYaml) {
|
|
401
|
-
|
|
402
|
-
"Local YAML update: use your editor to change 'type: string' to the new types in your collection YAML files.",
|
|
403
|
-
{ prefix: "Execute" }
|
|
404
|
-
);
|
|
401
|
+
await updateCollectionYaml(first.collectionName, entries, checkpoint);
|
|
405
402
|
}
|
|
406
403
|
}
|
|
407
404
|
}
|
|
@@ -458,17 +455,15 @@ async function migrateOneAttribute(
|
|
|
458
455
|
MessageFormatter.info(` Creating backup attribute ${backupKey}...`, {
|
|
459
456
|
prefix: "Migrate",
|
|
460
457
|
});
|
|
461
|
-
await
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
})
|
|
471
|
-
);
|
|
458
|
+
await createAttributeIfNotExists(adapter, {
|
|
459
|
+
databaseId,
|
|
460
|
+
tableId: collectionId,
|
|
461
|
+
key: backupKey,
|
|
462
|
+
type: "string", // backup keeps original type
|
|
463
|
+
size: entry.currentSize,
|
|
464
|
+
required: false, // always optional for backup
|
|
465
|
+
array: entry.isArray,
|
|
466
|
+
});
|
|
472
467
|
const available = await waitForAttribute(
|
|
473
468
|
adapter,
|
|
474
469
|
databaseId,
|
|
@@ -560,7 +555,7 @@ async function migrateOneAttribute(
|
|
|
560
555
|
createParams.default = entry.defaultValue;
|
|
561
556
|
}
|
|
562
557
|
|
|
563
|
-
await
|
|
558
|
+
await createAttributeIfNotExists(adapter, createParams as any);
|
|
564
559
|
const available = await waitForAttribute(
|
|
565
560
|
adapter,
|
|
566
561
|
databaseId,
|
|
@@ -756,7 +751,7 @@ async function verifyDataCopy(
|
|
|
756
751
|
const docs = res?.documents || res?.rows || [];
|
|
757
752
|
for (const doc of docs) {
|
|
758
753
|
if (!(sourceKey in doc)) continue;
|
|
759
|
-
if (doc[sourceKey] !== doc[targetKey]) {
|
|
754
|
+
if (JSON.stringify(doc[sourceKey]) !== JSON.stringify(doc[targetKey])) {
|
|
760
755
|
throw new Error(
|
|
761
756
|
`Verification failed: doc ${doc.$id} has ${sourceKey}=${JSON.stringify(doc[sourceKey])} but ${targetKey}=${JSON.stringify(doc[targetKey])}`
|
|
762
757
|
);
|
|
@@ -773,7 +768,7 @@ async function waitForAttribute(
|
|
|
773
768
|
databaseId: string,
|
|
774
769
|
collectionId: string,
|
|
775
770
|
key: string,
|
|
776
|
-
maxWaitMs: number =
|
|
771
|
+
maxWaitMs: number = 120_000
|
|
777
772
|
): Promise<boolean> {
|
|
778
773
|
const start = Date.now();
|
|
779
774
|
const checkInterval = 2000;
|
|
@@ -1077,10 +1072,218 @@ function printDryRunSummary(plan: MigrationPlan): void {
|
|
|
1077
1072
|
console.log("");
|
|
1078
1073
|
}
|
|
1079
1074
|
|
|
1075
|
+
// ────────────────────────────────────────────────────────
|
|
1076
|
+
// Update local collection YAML after migration
|
|
1077
|
+
// ────────────────────────────────────────────────────────
|
|
1078
|
+
|
|
1079
|
+
async function updateCollectionYaml(
|
|
1080
|
+
collectionName: string,
|
|
1081
|
+
entries: MigrationPlanEntry[],
|
|
1082
|
+
checkpoint: MigrationCheckpoint
|
|
1083
|
+
): Promise<void> {
|
|
1084
|
+
// Find candidate YAML files
|
|
1085
|
+
const candidates = findYamlFiles(process.cwd(), collectionName);
|
|
1086
|
+
|
|
1087
|
+
if (candidates.length === 0) {
|
|
1088
|
+
MessageFormatter.warning(
|
|
1089
|
+
`No YAML file found for collection "${collectionName}". Skipping local config update.`,
|
|
1090
|
+
{ prefix: "YAML" }
|
|
1091
|
+
);
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
let yamlPath: string;
|
|
1096
|
+
|
|
1097
|
+
if (candidates.length === 1) {
|
|
1098
|
+
const { usePath } = await inquirer.prompt([
|
|
1099
|
+
{
|
|
1100
|
+
type: "confirm",
|
|
1101
|
+
name: "usePath",
|
|
1102
|
+
message: `Found: ${candidates[0]}. Use this file?`,
|
|
1103
|
+
default: true,
|
|
1104
|
+
},
|
|
1105
|
+
]);
|
|
1106
|
+
if (!usePath) {
|
|
1107
|
+
const { customPath } = await inquirer.prompt([
|
|
1108
|
+
{
|
|
1109
|
+
type: "input",
|
|
1110
|
+
name: "customPath",
|
|
1111
|
+
message: "Enter path to collection YAML file:",
|
|
1112
|
+
},
|
|
1113
|
+
]);
|
|
1114
|
+
if (!customPath || !fs.existsSync(customPath)) {
|
|
1115
|
+
MessageFormatter.warning("Invalid path. Skipping YAML update.", {
|
|
1116
|
+
prefix: "YAML",
|
|
1117
|
+
});
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
yamlPath = customPath;
|
|
1121
|
+
} else {
|
|
1122
|
+
yamlPath = candidates[0];
|
|
1123
|
+
}
|
|
1124
|
+
} else {
|
|
1125
|
+
const { selectedPath } = await inquirer.prompt([
|
|
1126
|
+
{
|
|
1127
|
+
type: "list",
|
|
1128
|
+
name: "selectedPath",
|
|
1129
|
+
message: `Multiple YAML files found for "${collectionName}". Select one:`,
|
|
1130
|
+
choices: [
|
|
1131
|
+
...candidates.map((c) => ({ name: c, value: c })),
|
|
1132
|
+
{ name: "Enter custom path", value: "__custom__" },
|
|
1133
|
+
],
|
|
1134
|
+
},
|
|
1135
|
+
]);
|
|
1136
|
+
if (selectedPath === "__custom__") {
|
|
1137
|
+
const { customPath } = await inquirer.prompt([
|
|
1138
|
+
{
|
|
1139
|
+
type: "input",
|
|
1140
|
+
name: "customPath",
|
|
1141
|
+
message: "Enter path to collection YAML file:",
|
|
1142
|
+
},
|
|
1143
|
+
]);
|
|
1144
|
+
if (!customPath || !fs.existsSync(customPath)) {
|
|
1145
|
+
MessageFormatter.warning("Invalid path. Skipping YAML update.", {
|
|
1146
|
+
prefix: "YAML",
|
|
1147
|
+
});
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
yamlPath = customPath;
|
|
1151
|
+
} else {
|
|
1152
|
+
yamlPath = selectedPath;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// Load and parse YAML
|
|
1157
|
+
let doc: any;
|
|
1158
|
+
try {
|
|
1159
|
+
const content = fs.readFileSync(yamlPath, "utf8");
|
|
1160
|
+
doc = yaml.load(content);
|
|
1161
|
+
} catch (err: any) {
|
|
1162
|
+
MessageFormatter.error(
|
|
1163
|
+
`Failed to parse ${yamlPath}: ${err.message}`,
|
|
1164
|
+
undefined,
|
|
1165
|
+
{ prefix: "YAML" }
|
|
1166
|
+
);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (!doc || !Array.isArray(doc.attributes)) {
|
|
1171
|
+
MessageFormatter.warning(
|
|
1172
|
+
`No "attributes" array found in ${yamlPath}. Skipping.`,
|
|
1173
|
+
{ prefix: "YAML" }
|
|
1174
|
+
);
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Update attributes that were successfully migrated
|
|
1179
|
+
let updated = 0;
|
|
1180
|
+
for (const entry of entries) {
|
|
1181
|
+
const cp = findCheckpointEntry(checkpoint, entry);
|
|
1182
|
+
if (cp?.phase !== "completed") continue;
|
|
1183
|
+
|
|
1184
|
+
const attr = doc.attributes.find(
|
|
1185
|
+
(a: any) => a.key === entry.attributeKey && a.type === "string"
|
|
1186
|
+
);
|
|
1187
|
+
if (!attr) continue;
|
|
1188
|
+
|
|
1189
|
+
attr.type = entry.targetType;
|
|
1190
|
+
if (entry.targetType !== "varchar") {
|
|
1191
|
+
delete attr.size;
|
|
1192
|
+
}
|
|
1193
|
+
updated++;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
if (updated === 0) {
|
|
1197
|
+
MessageFormatter.info(
|
|
1198
|
+
"No matching string attributes found in YAML to update.",
|
|
1199
|
+
{ prefix: "YAML" }
|
|
1200
|
+
);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
// Write back
|
|
1205
|
+
try {
|
|
1206
|
+
const output = yaml.dump(doc, {
|
|
1207
|
+
lineWidth: 120,
|
|
1208
|
+
noRefs: true,
|
|
1209
|
+
sortKeys: false,
|
|
1210
|
+
});
|
|
1211
|
+
fs.writeFileSync(yamlPath, output, "utf8");
|
|
1212
|
+
MessageFormatter.success(
|
|
1213
|
+
`Updated ${updated} attribute(s) in ${yamlPath}`,
|
|
1214
|
+
{ prefix: "YAML" }
|
|
1215
|
+
);
|
|
1216
|
+
} catch (err: any) {
|
|
1217
|
+
MessageFormatter.error(
|
|
1218
|
+
`Failed to write ${yamlPath}: ${err.message}`,
|
|
1219
|
+
undefined,
|
|
1220
|
+
{ prefix: "YAML" }
|
|
1221
|
+
);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function findYamlFiles(searchRoot: string, collectionName: string): string[] {
|
|
1226
|
+
const results: string[] = [];
|
|
1227
|
+
const lowerName = collectionName.toLowerCase();
|
|
1228
|
+
|
|
1229
|
+
function walk(dir: string): void {
|
|
1230
|
+
let dirEntries: fs.Dirent[];
|
|
1231
|
+
try {
|
|
1232
|
+
dirEntries = fs.readdirSync(dir, { withFileTypes: true });
|
|
1233
|
+
} catch {
|
|
1234
|
+
return; // skip inaccessible directories
|
|
1235
|
+
}
|
|
1236
|
+
for (const ent of dirEntries) {
|
|
1237
|
+
const fullPath = path.join(dir, ent.name);
|
|
1238
|
+
if (ent.isDirectory()) {
|
|
1239
|
+
// Skip node_modules, .git, and other common non-config dirs
|
|
1240
|
+
if (ent.name === "node_modules" || ent.name === ".git" || ent.name === "dist") continue;
|
|
1241
|
+
walk(fullPath);
|
|
1242
|
+
} else if (
|
|
1243
|
+
ent.isFile() &&
|
|
1244
|
+
ent.name.toLowerCase() === `${lowerName}.yaml`
|
|
1245
|
+
) {
|
|
1246
|
+
// Verify it looks like a collection config (has a name field)
|
|
1247
|
+
try {
|
|
1248
|
+
const content = fs.readFileSync(fullPath, "utf8");
|
|
1249
|
+
const parsed = yaml.load(content) as any;
|
|
1250
|
+
if (parsed && parsed.name === collectionName) {
|
|
1251
|
+
results.push(fullPath);
|
|
1252
|
+
}
|
|
1253
|
+
} catch {
|
|
1254
|
+
// skip unparseable files
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
walk(searchRoot);
|
|
1261
|
+
return results;
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1080
1264
|
// ────────────────────────────────────────────────────────
|
|
1081
1265
|
// Utility
|
|
1082
1266
|
// ────────────────────────────────────────────────────────
|
|
1083
1267
|
|
|
1268
|
+
async function createAttributeIfNotExists(
|
|
1269
|
+
adapter: DatabaseAdapter,
|
|
1270
|
+
params: Parameters<DatabaseAdapter["createAttribute"]>[0]
|
|
1271
|
+
): Promise<void> {
|
|
1272
|
+
try {
|
|
1273
|
+
await tryAwaitWithRetry(() => adapter.createAttribute(params), 0, true);
|
|
1274
|
+
} catch (err: any) {
|
|
1275
|
+
const code = err?.code || err?.originalError?.code;
|
|
1276
|
+
const type = err?.originalError?.type || "";
|
|
1277
|
+
if (code === 409 || type === "column_already_exists") {
|
|
1278
|
+
MessageFormatter.info(` (backup attribute already exists, reusing)`, {
|
|
1279
|
+
prefix: "Migrate",
|
|
1280
|
+
});
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
throw err;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1084
1287
|
function delay(ms: number): Promise<void> {
|
|
1085
1288
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1086
1289
|
}
|