appwrite-utils-cli 1.6.5 → 1.6.7
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/indexes.js +30 -8
- package/dist/collections/methods.js +9 -4
- package/dist/config/services/ConfigLoaderService.d.ts +17 -0
- package/dist/config/services/ConfigLoaderService.js +84 -0
- package/dist/setupCommands.d.ts +57 -0
- package/dist/setupCommands.js +484 -1
- package/package.json +1 -1
- package/src/collections/indexes.ts +38 -14
- package/src/collections/methods.ts +20 -4
- package/src/config/services/ConfigLoaderService.ts +119 -0
- package/src/setupCommands.ts +597 -0
@@ -3,6 +3,8 @@ import { Databases, IndexType, Query } from "node-appwrite";
|
|
3
3
|
import { delay, tryAwaitWithRetry, calculateExponentialBackoff } from "../utils/helperFunctions.js";
|
4
4
|
import { isLegacyDatabases } from "../utils/typeGuards.js";
|
5
5
|
import { MessageFormatter } from "../shared/messageFormatter.js";
|
6
|
+
// System attributes that are always available for indexing in Appwrite
|
7
|
+
const SYSTEM_ATTRIBUTES = ['$id', '$createdAt', '$updatedAt', '$permissions'];
|
6
8
|
/**
|
7
9
|
* Wait for index to become available, with retry logic for stuck indexes and exponential backoff
|
8
10
|
*/
|
@@ -72,10 +74,12 @@ export const createOrUpdateIndexWithStatusCheck = async (dbId, db, collectionId,
|
|
72
74
|
// First, validate that all required attributes exist
|
73
75
|
const freshCollection = await db.getCollection(dbId, collectionId);
|
74
76
|
const existingAttributeKeys = freshCollection.attributes.map((attr) => attr.key);
|
75
|
-
|
77
|
+
// Include system attributes that are always available
|
78
|
+
const allAvailableAttributes = [...existingAttributeKeys, ...SYSTEM_ATTRIBUTES];
|
79
|
+
const missingAttributes = index.attributes.filter(attr => !allAvailableAttributes.includes(attr));
|
76
80
|
if (missingAttributes.length > 0) {
|
77
81
|
MessageFormatter.error(`Index '${index.key}' cannot be created: missing attributes [${missingAttributes.join(', ')}] (type: ${index.type})`);
|
78
|
-
MessageFormatter.error(`Available attributes: [${existingAttributeKeys.join(', ')}]`);
|
82
|
+
MessageFormatter.error(`Available attributes: [${existingAttributeKeys.join(', ')}, ${SYSTEM_ATTRIBUTES.join(', ')}]`);
|
79
83
|
return false; // Don't retry if attributes are missing
|
80
84
|
}
|
81
85
|
// Try to create/update the index using existing logic
|
@@ -169,12 +173,30 @@ export const createOrUpdateIndex = async (dbId, db, collectionId, index) => {
|
|
169
173
|
// No existing index, create it
|
170
174
|
createIndex = true;
|
171
175
|
}
|
172
|
-
else
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
176
|
+
else {
|
177
|
+
const existing = existingIndex.indexes[0];
|
178
|
+
// Check key and type
|
179
|
+
const keyMatches = existing.key === index.key;
|
180
|
+
const typeMatches = existing.type === index.type;
|
181
|
+
// Compare attributes as SETS (order doesn't matter, only content)
|
182
|
+
const existingAttrsSet = new Set(existing.attributes);
|
183
|
+
const newAttrsSet = new Set(index.attributes);
|
184
|
+
const attributesMatch = existingAttrsSet.size === newAttrsSet.size &&
|
185
|
+
[...existingAttrsSet].every(attr => newAttrsSet.has(attr));
|
186
|
+
// Compare orders as SETS if both exist (order doesn't matter)
|
187
|
+
let ordersMatch = true;
|
188
|
+
if (index.orders && existing.orders) {
|
189
|
+
const existingOrdersSet = new Set(existing.orders);
|
190
|
+
const newOrdersSet = new Set(index.orders);
|
191
|
+
ordersMatch =
|
192
|
+
existingOrdersSet.size === newOrdersSet.size &&
|
193
|
+
[...existingOrdersSet].every(ord => newOrdersSet.has(ord));
|
194
|
+
}
|
195
|
+
// Only recreate if something genuinely changed
|
196
|
+
if (!keyMatches || !typeMatches || !attributesMatch || !ordersMatch) {
|
197
|
+
await db.deleteIndex(dbId, collectionId, existing.key);
|
198
|
+
createIndex = true;
|
199
|
+
}
|
178
200
|
}
|
179
201
|
if (createIndex) {
|
180
202
|
// Ensure orders array exists and matches attributes length
|
@@ -224,10 +224,14 @@ export const createOrUpdateCollections = async (database, databaseId, config, de
|
|
224
224
|
attributes);
|
225
225
|
// Add delay after creating attributes
|
226
226
|
await delay(250);
|
227
|
-
//
|
228
|
-
const
|
227
|
+
// Prefer local config indexes, but fall back to collection's own indexes if no local config exists
|
228
|
+
const localCollectionConfig = config.collections?.find(c => c.name === collectionData.name || c.$id === collectionData.$id);
|
229
|
+
const indexesToUse = localCollectionConfig?.indexes ?? indexes ?? [];
|
229
230
|
MessageFormatter.progress("Creating Indexes", { prefix: "Collections" });
|
230
231
|
await createOrUpdateIndexesWithStatusCheck(databaseId, database, collectionToUse.$id, collectionToUse, indexesToUse);
|
232
|
+
// Delete indexes that exist on server but not in local config
|
233
|
+
const { deleteObsoleteIndexes } = await import('../shared/indexManager.js');
|
234
|
+
await deleteObsoleteIndexes(database, databaseId, collectionToUse, { indexes: indexesToUse }, { verbose: true });
|
231
235
|
// Mark this collection as fully processed to prevent re-processing
|
232
236
|
markCollectionProcessed(collectionToUse.$id, collectionData.name);
|
233
237
|
// Add delay after creating indexes
|
@@ -424,8 +428,9 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
|
|
424
428
|
}
|
425
429
|
}
|
426
430
|
}
|
427
|
-
//
|
428
|
-
const
|
431
|
+
// Prefer local config indexes, but fall back to collection's own indexes if no local config exists (TablesDB path)
|
432
|
+
const localTableConfig = config.collections?.find(c => c.name === collectionData.name || c.$id === collectionData.$id);
|
433
|
+
const idxs = (localTableConfig?.indexes ?? indexes ?? []);
|
429
434
|
for (const idx of idxs) {
|
430
435
|
try {
|
431
436
|
await adapter.createIndex({
|
@@ -90,6 +90,23 @@ export declare class ConfigLoaderService {
|
|
90
90
|
source2: string;
|
91
91
|
}>;
|
92
92
|
}>;
|
93
|
+
/**
|
94
|
+
* Loads tables first (higher priority), then collections (backward compatibility)
|
95
|
+
* Used for TablesDB projects (>= 1.8.0)
|
96
|
+
* @param tablesDir Path to the tables directory
|
97
|
+
* @param collectionsDir Path to the collections directory
|
98
|
+
* @returns Loading result with items, counts, and conflicts
|
99
|
+
*/
|
100
|
+
loadTablesFirst(tablesDir: string, collectionsDir: string): Promise<{
|
101
|
+
items: Collection[];
|
102
|
+
fromCollections: number;
|
103
|
+
fromTables: number;
|
104
|
+
conflicts: Array<{
|
105
|
+
name: string;
|
106
|
+
source1: string;
|
107
|
+
source2: string;
|
108
|
+
}>;
|
109
|
+
}>;
|
93
110
|
/**
|
94
111
|
* Validates that a configuration file can be loaded
|
95
112
|
* @param configPath Path to the configuration file
|
@@ -112,6 +112,42 @@ export class ConfigLoaderService {
|
|
112
112
|
};
|
113
113
|
}
|
114
114
|
}
|
115
|
+
// Load collections and tables from their respective directories
|
116
|
+
const configDir = path.dirname(yamlPath);
|
117
|
+
const collectionsDir = path.join(configDir, config.schemaConfig?.collectionsDirectory || "collections");
|
118
|
+
const tablesDir = path.join(configDir, config.schemaConfig?.tablesDirectory || "tables");
|
119
|
+
// Detect API mode to determine priority order
|
120
|
+
let apiMode = 'legacy';
|
121
|
+
try {
|
122
|
+
const { detectAppwriteVersionCached } = await import('../../utils/versionDetection.js');
|
123
|
+
const detection = await detectAppwriteVersionCached(config.appwriteEndpoint, config.appwriteProject, config.appwriteKey);
|
124
|
+
apiMode = detection.apiMode;
|
125
|
+
}
|
126
|
+
catch {
|
127
|
+
// Fallback to legacy if detection fails
|
128
|
+
}
|
129
|
+
// Load with correct priority based on API mode
|
130
|
+
const { items, conflicts, fromCollections, fromTables } = apiMode === 'tablesdb'
|
131
|
+
? await this.loadTablesFirst(tablesDir, collectionsDir)
|
132
|
+
: await this.loadCollectionsAndTables(collectionsDir, tablesDir);
|
133
|
+
config.collections = items;
|
134
|
+
// Report what was loaded
|
135
|
+
if (fromTables > 0 && fromCollections > 0) {
|
136
|
+
MessageFormatter.success(`Loaded ${items.length} total items: ${fromCollections} from collections/, ${fromTables} from tables/`, { prefix: "Config" });
|
137
|
+
}
|
138
|
+
else if (fromCollections > 0) {
|
139
|
+
MessageFormatter.success(`Loaded ${fromCollections} collections from collections/`, { prefix: "Config" });
|
140
|
+
}
|
141
|
+
else if (fromTables > 0) {
|
142
|
+
MessageFormatter.success(`Loaded ${fromTables} tables from tables/`, { prefix: "Config" });
|
143
|
+
}
|
144
|
+
// Report conflicts
|
145
|
+
if (conflicts.length > 0) {
|
146
|
+
MessageFormatter.warning(`Found ${conflicts.length} naming conflicts`, { prefix: "Config" });
|
147
|
+
conflicts.forEach(conflict => {
|
148
|
+
MessageFormatter.info(` - '${conflict.name}': ${conflict.source1} (used) vs ${conflict.source2} (skipped)`, { prefix: "Config" });
|
149
|
+
});
|
150
|
+
}
|
115
151
|
MessageFormatter.success(`Loaded YAML config from: ${yamlPath}`, {
|
116
152
|
prefix: "Config",
|
117
153
|
});
|
@@ -377,6 +413,54 @@ export class ConfigLoaderService {
|
|
377
413
|
conflicts,
|
378
414
|
};
|
379
415
|
}
|
416
|
+
/**
|
417
|
+
* Loads tables first (higher priority), then collections (backward compatibility)
|
418
|
+
* Used for TablesDB projects (>= 1.8.0)
|
419
|
+
* @param tablesDir Path to the tables directory
|
420
|
+
* @param collectionsDir Path to the collections directory
|
421
|
+
* @returns Loading result with items, counts, and conflicts
|
422
|
+
*/
|
423
|
+
async loadTablesFirst(tablesDir, collectionsDir) {
|
424
|
+
const items = [];
|
425
|
+
const loadedNames = new Set();
|
426
|
+
const conflicts = [];
|
427
|
+
// Load from tables/ directory first (HIGHER priority for TablesDB)
|
428
|
+
if (fs.existsSync(tablesDir)) {
|
429
|
+
const tables = await this.loadTables(tablesDir, { markAsTablesDir: true });
|
430
|
+
for (const table of tables) {
|
431
|
+
const name = table.name || table.tableId || table.$id || "";
|
432
|
+
loadedNames.add(name);
|
433
|
+
items.push(table);
|
434
|
+
}
|
435
|
+
}
|
436
|
+
// Load from collections/ directory second (LOWER priority, backward compatibility)
|
437
|
+
if (fs.existsSync(collectionsDir)) {
|
438
|
+
const collections = await this.loadCollections(collectionsDir);
|
439
|
+
for (const collection of collections) {
|
440
|
+
const name = collection.name || collection.$id || "";
|
441
|
+
// Check for conflicts - tables win
|
442
|
+
if (loadedNames.has(name)) {
|
443
|
+
conflicts.push({
|
444
|
+
name,
|
445
|
+
source1: "tables/",
|
446
|
+
source2: "collections/",
|
447
|
+
});
|
448
|
+
}
|
449
|
+
else {
|
450
|
+
loadedNames.add(name);
|
451
|
+
items.push(collection);
|
452
|
+
}
|
453
|
+
}
|
454
|
+
}
|
455
|
+
const fromTables = items.filter((item) => item._isFromTablesDir).length;
|
456
|
+
const fromCollections = items.filter((item) => !item._isFromTablesDir).length;
|
457
|
+
return {
|
458
|
+
items,
|
459
|
+
fromCollections,
|
460
|
+
fromTables,
|
461
|
+
conflicts,
|
462
|
+
};
|
463
|
+
}
|
380
464
|
/**
|
381
465
|
* Validates that a configuration file can be loaded
|
382
466
|
* @param configPath Path to the configuration file
|
package/dist/setupCommands.d.ts
CHANGED
@@ -1 +1,58 @@
|
|
1
|
+
import { type ApiMode } from "./utils/versionDetection.js";
|
2
|
+
/**
|
3
|
+
* Terminology configuration for API mode-specific naming
|
4
|
+
*/
|
5
|
+
interface TerminologyConfig {
|
6
|
+
container: "table" | "collection";
|
7
|
+
containerName: "Table" | "Collection";
|
8
|
+
fields: "columns" | "attributes";
|
9
|
+
fieldName: "Column" | "Attribute";
|
10
|
+
security: "rowSecurity" | "documentSecurity";
|
11
|
+
schemaRef: "table.schema.json" | "collection.schema.json";
|
12
|
+
items: "rows" | "documents";
|
13
|
+
}
|
14
|
+
/**
|
15
|
+
* Detection result with source information
|
16
|
+
*/
|
17
|
+
interface ApiModeDetectionResult {
|
18
|
+
apiMode: ApiMode;
|
19
|
+
useTables: boolean;
|
20
|
+
detectionSource: "appwrite.json" | "server-version" | "default";
|
21
|
+
serverVersion?: string;
|
22
|
+
}
|
23
|
+
/**
|
24
|
+
* Get terminology configuration based on API mode
|
25
|
+
*/
|
26
|
+
export declare function getTerminologyConfig(useTables: boolean): TerminologyConfig;
|
27
|
+
/**
|
28
|
+
* Detect API mode using multiple detection sources
|
29
|
+
* Priority: appwrite.json > server version > default (collections)
|
30
|
+
*/
|
31
|
+
export declare function detectApiMode(basePath: string): Promise<ApiModeDetectionResult>;
|
32
|
+
/**
|
33
|
+
* Create directory structure for Appwrite project
|
34
|
+
*/
|
35
|
+
export declare function createProjectDirectories(basePath: string, useTables: boolean): {
|
36
|
+
appwriteFolder: string;
|
37
|
+
containerFolder: string;
|
38
|
+
schemaFolder: string;
|
39
|
+
yamlSchemaFolder: string;
|
40
|
+
dataFolder: string;
|
41
|
+
};
|
42
|
+
/**
|
43
|
+
* Create example YAML schema file with correct terminology
|
44
|
+
*/
|
45
|
+
export declare function createExampleSchema(containerFolder: string, terminology: TerminologyConfig): string;
|
46
|
+
/**
|
47
|
+
* Create JSON schema for YAML validation
|
48
|
+
*/
|
49
|
+
export declare function createYamlValidationSchema(yamlSchemaFolder: string, useTables: boolean): string;
|
50
|
+
/**
|
51
|
+
* Initialize a new Appwrite project with correct directory structure and terminology
|
52
|
+
*/
|
53
|
+
export declare function initProject(basePath?: string, forceApiMode?: 'legacy' | 'tablesdb'): Promise<void>;
|
54
|
+
/**
|
55
|
+
* Create a new collection or table schema file
|
56
|
+
*/
|
57
|
+
export declare function createSchema(name: string, basePath?: string, forceApiMode?: 'legacy' | 'tablesdb'): Promise<void>;
|
1
58
|
export {};
|