appwrite-utils-cli 1.6.4 → 1.6.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.
@@ -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,15 +173,38 @@ 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
- newIndex = await db.createIndex(dbId, collectionId, index.key, index.type, index.attributes, index.orders);
202
+ // Ensure orders array exists and matches attributes length
203
+ // Default to "asc" for each attribute if not specified
204
+ const orders = index.orders && index.orders.length === index.attributes.length
205
+ ? index.orders
206
+ : index.attributes.map(() => "asc");
207
+ newIndex = await db.createIndex(dbId, collectionId, index.key, index.type, index.attributes, orders);
181
208
  }
182
209
  return newIndex;
183
210
  };
@@ -68,7 +68,8 @@ export const checkForCollection = async (db, dbId, collection) => {
68
68
  const items = isLegacyDatabases(db) ? response.collections : (response.tables || response.collections);
69
69
  if (items && items.length > 0) {
70
70
  MessageFormatter.info(`Collection found: ${items[0].$id}`, { prefix: "Collections" });
71
- return { ...collection, ...items[0] };
71
+ // Return remote collection for update operations (don't merge local config over it)
72
+ return items[0];
72
73
  }
73
74
  else {
74
75
  MessageFormatter.info(`No collection found with name: ${collection.name}`, { prefix: "Collections" });
@@ -223,12 +224,14 @@ export const createOrUpdateCollections = async (database, databaseId, config, de
223
224
  attributes);
224
225
  // Add delay after creating attributes
225
226
  await delay(250);
226
- const indexesToUse = indexes && indexes.length > 0
227
- ? indexes
228
- : config.collections?.find((c) => c.$id === collectionToUse.$id)
229
- ?.indexes ?? [];
227
+ // ALWAYS use indexes from local config, NEVER from server
228
+ const localCollectionConfig = config.collections?.find(c => c.name === collectionData.name || c.$id === collectionData.$id);
229
+ const indexesToUse = localCollectionConfig?.indexes || [];
230
230
  MessageFormatter.progress("Creating Indexes", { prefix: "Collections" });
231
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 });
232
235
  // Mark this collection as fully processed to prevent re-processing
233
236
  markCollectionProcessed(collectionToUse.$id, collectionData.name);
234
237
  // Add delay after creating indexes
@@ -425,8 +428,9 @@ export const createOrUpdateCollectionsViaAdapter = async (adapter, databaseId, c
425
428
  }
426
429
  }
427
430
  }
428
- // Indexes
429
- const idxs = (indexes || []);
431
+ // ALWAYS use indexes from local config, NEVER from server (TablesDB path)
432
+ const localTableConfig = config.collections?.find(c => c.name === collectionData.name || c.$id === collectionData.$id);
433
+ const idxs = (localTableConfig?.indexes || []);
430
434
  for (const idx of idxs) {
431
435
  try {
432
436
  await adapter.createIndex({
package/dist/main.js CHANGED
@@ -697,24 +697,19 @@ async function main() {
697
697
  operationStats.wipedBuckets = wipeStats.buckets;
698
698
  }
699
699
  }
700
- if (parsedArgv.push || parsedArgv.sync) {
700
+ if (parsedArgv.push) {
701
+ // PUSH: Use LOCAL config collections only (pass empty array to use config.collections)
701
702
  const databases = options.databases || (await fetchAllDatabases(controller.database));
702
- let collections = [];
703
- if (options.collections) {
704
- for (const db of databases) {
705
- const dbCollections = await fetchAllCollections(db.$id, controller.database);
706
- collections = collections.concat(dbCollections.filter((c) => options.collections.includes(c.$id)));
707
- }
708
- }
709
- if (parsedArgv.push) {
710
- await controller.syncDb(databases, collections);
711
- operationStats.pushedDatabases = databases.length;
712
- operationStats.pushedCollections = collections.length;
713
- }
714
- else if (parsedArgv.sync) {
715
- await controller.synchronizeConfigurations(databases);
716
- operationStats.syncedDatabases = databases.length;
717
- }
703
+ // Pass empty array - syncDb will use config.collections (local schema)
704
+ await controller.syncDb(databases, []);
705
+ operationStats.pushedDatabases = databases.length;
706
+ operationStats.pushedCollections = controller.config?.collections?.length || 0;
707
+ }
708
+ else if (parsedArgv.sync) {
709
+ // SYNC: Pull from remote
710
+ const databases = options.databases || (await fetchAllDatabases(controller.database));
711
+ await controller.synchronizeConfigurations(databases);
712
+ operationStats.syncedDatabases = databases.length;
718
713
  }
719
714
  if (options.generateSchemas) {
720
715
  await controller.generateSchemas();
@@ -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 {};