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.
@@ -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
- const missingAttributes = index.attributes.filter(attr => !existingAttributeKeys.includes(attr));
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 if (!existingIndex.indexes.some((existingIndex) => (existingIndex.key === index.key &&
173
- existingIndex.type === index.type &&
174
- existingIndex.attributes === index.attributes))) {
175
- // Existing index doesn't match, delete and recreate
176
- await db.deleteIndex(dbId, collectionId, existingIndex.indexes[0].key);
177
- createIndex = true;
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
- // For PUSH operations, only use indexes from local config (not remote)
228
- const indexesToUse = indexes || [];
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
- // Indexes
428
- const idxs = (indexes || []);
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
@@ -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 {};