appwrite-utils-cli 1.5.0 → 1.5.2
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/dist/collections/attributes.js +18 -1
- package/dist/databases/setup.js +6 -2
- package/dist/interactiveCLI.js +9 -2
- package/dist/utilsController.js +4 -1
- package/package.json +1 -1
- package/src/collections/attributes.ts +75 -56
- package/src/databases/setup.ts +11 -7
- package/src/interactiveCLI.ts +16 -12
- package/src/utilsController.ts +15 -8
@@ -131,6 +131,15 @@ const attributesSame = (databaseAttribute, configAttribute) => {
|
|
131
131
|
(configValue === undefined || configValue === null)) {
|
132
132
|
return true;
|
133
133
|
}
|
134
|
+
// Normalize booleans: treat undefined and false as equivalent
|
135
|
+
if (typeof dbValue === "boolean" || typeof configValue === "boolean") {
|
136
|
+
return Boolean(dbValue) === Boolean(configValue);
|
137
|
+
}
|
138
|
+
// For numeric comparisons, compare numbers if both are numeric-like
|
139
|
+
if ((typeof dbValue === "number" || (typeof dbValue === "string" && dbValue !== "" && !isNaN(Number(dbValue)))) &&
|
140
|
+
(typeof configValue === "number" || (typeof configValue === "string" && configValue !== "" && !isNaN(Number(configValue))))) {
|
141
|
+
return Number(dbValue) === Number(configValue);
|
142
|
+
}
|
134
143
|
return dbValue === configValue;
|
135
144
|
}
|
136
145
|
// If neither has the attribute, consider it the same
|
@@ -140,10 +149,18 @@ const attributesSame = (databaseAttribute, configAttribute) => {
|
|
140
149
|
// If one has the attribute and the other doesn't, check if it's undefined or null
|
141
150
|
if (dbHasAttr && !configHasAttr) {
|
142
151
|
const dbValue = databaseAttribute[attr];
|
152
|
+
// Consider default-false booleans as equal to missing in config
|
153
|
+
if (typeof dbValue === "boolean") {
|
154
|
+
return dbValue === false; // missing in config equals false in db
|
155
|
+
}
|
143
156
|
return dbValue === undefined || dbValue === null;
|
144
157
|
}
|
145
158
|
if (!dbHasAttr && configHasAttr) {
|
146
159
|
const configValue = configAttribute[attr];
|
160
|
+
// Consider default-false booleans as equal to missing in db
|
161
|
+
if (typeof configValue === "boolean") {
|
162
|
+
return configValue === false; // missing in db equals false in config
|
163
|
+
}
|
147
164
|
return configValue === undefined || configValue === null;
|
148
165
|
}
|
149
166
|
// If we reach here, the attributes are different
|
@@ -355,7 +372,7 @@ export const createOrUpdateAttribute = async (db, dbId, collection, attribute) =
|
|
355
372
|
}
|
356
373
|
const minValue = finalAttribute.min !== undefined && finalAttribute.min !== null
|
357
374
|
? parseInt(finalAttribute.min)
|
358
|
-
: 9007199254740991;
|
375
|
+
: -9007199254740991;
|
359
376
|
const maxValue = finalAttribute.max !== undefined && finalAttribute.max !== null
|
360
377
|
? parseInt(finalAttribute.max)
|
361
378
|
: 9007199254740991;
|
package/dist/databases/setup.js
CHANGED
@@ -72,7 +72,8 @@ export const ensureDatabasesExist = async (config, databasesToEnsure) => {
|
|
72
72
|
throw new Error("Appwrite client is not initialized in the config");
|
73
73
|
}
|
74
74
|
const database = new Databases(config.appwriteClient);
|
75
|
-
|
75
|
+
// Work on a shallow copy so we don't mutate caller-provided arrays
|
76
|
+
const databasesToCreate = [...(databasesToEnsure || config.databases || [])];
|
76
77
|
if (!databasesToCreate.length) {
|
77
78
|
console.log("No databases to create");
|
78
79
|
return;
|
@@ -81,7 +82,10 @@ export const ensureDatabasesExist = async (config, databasesToEnsure) => {
|
|
81
82
|
const migrationsDatabase = existingDatabases.databases.find((d) => d.name.toLowerCase().trim().replace(" ", "") === "migrations");
|
82
83
|
if (config.useMigrations && existingDatabases.databases.length !== 0 && migrationsDatabase) {
|
83
84
|
console.log("Creating all databases including migrations");
|
84
|
-
|
85
|
+
// Ensure migrations exists, but do not mutate the caller's array
|
86
|
+
if (!databasesToCreate.some((d) => d.$id === migrationsDatabase.$id)) {
|
87
|
+
databasesToCreate.push(migrationsDatabase);
|
88
|
+
}
|
85
89
|
}
|
86
90
|
for (const db of databasesToCreate) {
|
87
91
|
if (!existingDatabases.databases.some((d) => d.name === db.name)) {
|
package/dist/interactiveCLI.js
CHANGED
@@ -235,7 +235,13 @@ export class InteractiveCLI {
|
|
235
235
|
...configCollections.filter((c) => !remoteCollections.some((rc) => rc.name === c.name)),
|
236
236
|
];
|
237
237
|
if (shouldFilterByDatabase) {
|
238
|
-
|
238
|
+
// Keep local entries that don't have databaseId (common in config),
|
239
|
+
// but still filter remote collections by selected database.
|
240
|
+
allCollections = allCollections.filter((c) => {
|
241
|
+
if (!c.databaseId)
|
242
|
+
return true;
|
243
|
+
return c.databaseId === database.$id;
|
244
|
+
});
|
239
245
|
}
|
240
246
|
const hasLocalAndRemote = allCollections.some((coll) => configCollections.some((c) => c.name === coll.name)) &&
|
241
247
|
allCollections.some((coll) => !configCollections.some((c) => c.name === coll.name));
|
@@ -867,7 +873,8 @@ export class InteractiveCLI {
|
|
867
873
|
console.log(chalk.yellow("No databases selected. Skipping database sync."));
|
868
874
|
return;
|
869
875
|
}
|
870
|
-
const collections = await this.selectCollections(databases[0], this.controller.database, chalk.blue("Select local collections to push:"), true, true // prefer local
|
876
|
+
const collections = await this.selectCollections(databases[0], this.controller.database, chalk.blue("Select local collections to push:"), true, true, // prefer local
|
877
|
+
true // filter by selected database
|
871
878
|
);
|
872
879
|
const { syncFunctions } = await inquirer.prompt([
|
873
880
|
{
|
package/dist/utilsController.js
CHANGED
@@ -442,9 +442,12 @@ export class UtilsController {
|
|
442
442
|
const allDatabases = await fetchAllDatabases(this.database);
|
443
443
|
databases = allDatabases;
|
444
444
|
}
|
445
|
+
// Ensure DBs exist (this may internally ensure migrations exists)
|
445
446
|
await this.ensureDatabasesExist(databases);
|
446
447
|
await this.ensureDatabaseConfigBucketsExist(databases);
|
447
|
-
|
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);
|
448
451
|
}
|
449
452
|
getAppwriteFolderPath() {
|
450
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.5.
|
4
|
+
"version": "1.5.2",
|
5
5
|
"main": "src/main.ts",
|
6
6
|
"type": "module",
|
7
7
|
"repository": {
|
@@ -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
|
-
|
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
|
+
}
|
276
291
|
|
277
|
-
if (!dbHasAttr && configHasAttr) {
|
278
|
-
const configValue = configAttribute[attr as keyof typeof configAttribute];
|
279
|
-
|
280
|
-
|
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;
|
@@ -646,27 +665,27 @@ export const createOrUpdateAttribute = async (
|
|
646
665
|
finalAttribute.array || false
|
647
666
|
)
|
648
667
|
);
|
649
|
-
} else {
|
650
|
-
if (
|
651
|
-
finalAttribute.min &&
|
652
|
-
BigInt(finalAttribute.min) === BigInt(-9223372036854776000)
|
653
|
-
) {
|
654
|
-
finalAttribute.min = undefined;
|
655
|
-
}
|
656
|
-
if (
|
657
|
-
finalAttribute.max &&
|
658
|
-
BigInt(finalAttribute.max) === BigInt(9223372036854776000)
|
659
|
-
) {
|
660
|
-
finalAttribute.max = undefined;
|
661
|
-
}
|
662
|
-
const minValue =
|
663
|
-
finalAttribute.min !== undefined && finalAttribute.min !== null
|
664
|
-
? parseInt(finalAttribute.min)
|
665
|
-
: 9007199254740991;
|
666
|
-
const maxValue =
|
667
|
-
finalAttribute.max !== undefined && finalAttribute.max !== null
|
668
|
-
? parseInt(finalAttribute.max)
|
669
|
-
: 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;
|
670
689
|
console.log(
|
671
690
|
`DEBUG: Updating integer attribute '${
|
672
691
|
finalAttribute.key
|
package/src/databases/setup.ts
CHANGED
@@ -111,12 +111,13 @@ export const setupMigrationDatabase = async (config: AppwriteConfig) => {
|
|
111
111
|
console.log("---------------------------------");
|
112
112
|
};
|
113
113
|
|
114
|
-
export const ensureDatabasesExist = async (config: AppwriteConfig, databasesToEnsure?: Models.Database[]) => {
|
114
|
+
export const ensureDatabasesExist = async (config: AppwriteConfig, databasesToEnsure?: Models.Database[]) => {
|
115
115
|
if (!config.appwriteClient) {
|
116
116
|
throw new Error("Appwrite client is not initialized in the config");
|
117
117
|
}
|
118
|
-
const database = new Databases(config.appwriteClient);
|
119
|
-
|
118
|
+
const database = new Databases(config.appwriteClient);
|
119
|
+
// Work on a shallow copy so we don't mutate caller-provided arrays
|
120
|
+
const databasesToCreate = [...(databasesToEnsure || config.databases || [])];
|
120
121
|
|
121
122
|
if (!databasesToCreate.length) {
|
122
123
|
console.log("No databases to create");
|
@@ -130,10 +131,13 @@ export const ensureDatabasesExist = async (config: AppwriteConfig, databasesToEn
|
|
130
131
|
const migrationsDatabase = existingDatabases.databases.find(
|
131
132
|
(d) => d.name.toLowerCase().trim().replace(" ", "") === "migrations"
|
132
133
|
);
|
133
|
-
if (config.useMigrations && existingDatabases.databases.length !== 0 && migrationsDatabase) {
|
134
|
-
console.log("Creating all databases including migrations");
|
135
|
-
|
136
|
-
|
134
|
+
if (config.useMigrations && existingDatabases.databases.length !== 0 && migrationsDatabase) {
|
135
|
+
console.log("Creating all databases including migrations");
|
136
|
+
// Ensure migrations exists, but do not mutate the caller's array
|
137
|
+
if (!databasesToCreate.some((d) => d.$id === migrationsDatabase.$id)) {
|
138
|
+
databasesToCreate.push(migrationsDatabase);
|
139
|
+
}
|
140
|
+
}
|
137
141
|
|
138
142
|
for (const db of databasesToCreate) {
|
139
143
|
if (!existingDatabases.databases.some((d) => d.name === db.name)) {
|
package/src/interactiveCLI.ts
CHANGED
@@ -312,11 +312,14 @@ export class InteractiveCLI {
|
|
312
312
|
),
|
313
313
|
];
|
314
314
|
|
315
|
-
if (shouldFilterByDatabase) {
|
316
|
-
|
317
|
-
|
318
|
-
)
|
319
|
-
|
315
|
+
if (shouldFilterByDatabase) {
|
316
|
+
// Keep local entries that don't have databaseId (common in config),
|
317
|
+
// but still filter remote collections by selected database.
|
318
|
+
allCollections = allCollections.filter((c) => {
|
319
|
+
if (!c.databaseId) return true;
|
320
|
+
return c.databaseId === database.$id;
|
321
|
+
});
|
322
|
+
}
|
320
323
|
|
321
324
|
const hasLocalAndRemote =
|
322
325
|
allCollections.some((coll) =>
|
@@ -1159,13 +1162,14 @@ export class InteractiveCLI {
|
|
1159
1162
|
return;
|
1160
1163
|
}
|
1161
1164
|
|
1162
|
-
const collections = await this.selectCollections(
|
1163
|
-
databases[0],
|
1164
|
-
this.controller!.database!,
|
1165
|
-
chalk.blue("Select local collections to push:"),
|
1166
|
-
true,
|
1167
|
-
true // prefer local
|
1168
|
-
|
1165
|
+
const collections = await this.selectCollections(
|
1166
|
+
databases[0],
|
1167
|
+
this.controller!.database!,
|
1168
|
+
chalk.blue("Select local collections to push:"),
|
1169
|
+
true,
|
1170
|
+
true, // prefer local
|
1171
|
+
true // filter by selected database
|
1172
|
+
);
|
1169
1173
|
|
1170
1174
|
const { syncFunctions } = await inquirer.prompt([
|
1171
1175
|
{
|
package/src/utilsController.ts
CHANGED
@@ -601,10 +601,10 @@ export class UtilsController {
|
|
601
601
|
await generator.updateConfig(this.config, isYamlProject);
|
602
602
|
}
|
603
603
|
|
604
|
-
async syncDb(
|
605
|
-
databases: Models.Database[] = [],
|
606
|
-
collections: Models.Collection[] = []
|
607
|
-
) {
|
604
|
+
async syncDb(
|
605
|
+
databases: Models.Database[] = [],
|
606
|
+
collections: Models.Collection[] = []
|
607
|
+
) {
|
608
608
|
await this.init();
|
609
609
|
if (!this.database) {
|
610
610
|
MessageFormatter.error("Database not initialized", undefined, { prefix: "Controller" });
|
@@ -614,10 +614,17 @@ export class UtilsController {
|
|
614
614
|
const allDatabases = await fetchAllDatabases(this.database);
|
615
615
|
databases = allDatabases;
|
616
616
|
}
|
617
|
-
|
618
|
-
await this.
|
619
|
-
await this.
|
620
|
-
|
617
|
+
// Ensure DBs exist (this may internally ensure migrations exists)
|
618
|
+
await this.ensureDatabasesExist(databases);
|
619
|
+
await this.ensureDatabaseConfigBucketsExist(databases);
|
620
|
+
|
621
|
+
// Do not push collections to the migrations database (prevents duplicate runs)
|
622
|
+
const dbsForCollections = databases.filter(
|
623
|
+
(db) => (this.config?.useMigrations ?? true) ? db.name.toLowerCase() !== "migrations" : true
|
624
|
+
);
|
625
|
+
|
626
|
+
await this.createOrUpdateCollectionsForDatabases(dbsForCollections, collections);
|
627
|
+
}
|
621
628
|
|
622
629
|
getAppwriteFolderPath() {
|
623
630
|
return this.appwriteFolderPath;
|