@njdamstra/appwrite-utils-cli 1.11.5 → 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
|
}
|
|
@@ -724,6 +724,163 @@ 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
|
// ────────────────────────────────────────────────────────
|
|
729
886
|
async function createAttributeIfNotExists(adapter, params) {
|
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
|
}
|
|
@@ -1075,6 +1072,195 @@ function printDryRunSummary(plan: MigrationPlan): void {
|
|
|
1075
1072
|
console.log("");
|
|
1076
1073
|
}
|
|
1077
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
|
+
|
|
1078
1264
|
// ────────────────────────────────────────────────────────
|
|
1079
1265
|
// Utility
|
|
1080
1266
|
// ────────────────────────────────────────────────────────
|