appwrite-utils-cli 1.6.5 → 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.
- package/dist/collections/indexes.js +30 -8
- package/dist/collections/methods.js +9 -4
- 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/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
|
+
// 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 || [];
|
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
|
+
// 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 || []);
|
429
434
|
for (const idx of idxs) {
|
430
435
|
try {
|
431
436
|
await adapter.createIndex({
|
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 {};
|
package/dist/setupCommands.js
CHANGED
@@ -1 +1,484 @@
|
|
1
|
-
|
1
|
+
import { mkdirSync, writeFileSync, existsSync } from "node:fs";
|
2
|
+
import path from "node:path";
|
3
|
+
import { ulid } from "ulidx";
|
4
|
+
import { MessageFormatter } from "./shared/messageFormatter.js";
|
5
|
+
import { detectAppwriteVersionCached, fetchServerVersion, isVersionAtLeast } from "./utils/versionDetection.js";
|
6
|
+
import { loadAppwriteProjectConfig, findAppwriteProjectConfig, isTablesDBProject } from "./utils/projectConfig.js";
|
7
|
+
import { findYamlConfig, generateYamlConfigTemplate } from "./config/yamlConfig.js";
|
8
|
+
import { loadYamlConfig } from "./config/yamlConfig.js";
|
9
|
+
import { hasSessionAuth } from "./utils/sessionAuth.js";
|
10
|
+
/**
|
11
|
+
* Get terminology configuration based on API mode
|
12
|
+
*/
|
13
|
+
export function getTerminologyConfig(useTables) {
|
14
|
+
return useTables
|
15
|
+
? {
|
16
|
+
container: "table",
|
17
|
+
containerName: "Table",
|
18
|
+
fields: "columns",
|
19
|
+
fieldName: "Column",
|
20
|
+
security: "rowSecurity",
|
21
|
+
schemaRef: "table.schema.json",
|
22
|
+
items: "rows"
|
23
|
+
}
|
24
|
+
: {
|
25
|
+
container: "collection",
|
26
|
+
containerName: "Collection",
|
27
|
+
fields: "attributes",
|
28
|
+
fieldName: "Attribute",
|
29
|
+
security: "documentSecurity",
|
30
|
+
schemaRef: "collection.schema.json",
|
31
|
+
items: "documents"
|
32
|
+
};
|
33
|
+
}
|
34
|
+
/**
|
35
|
+
* Detect API mode using multiple detection sources
|
36
|
+
* Priority: appwrite.json > server version > default (collections)
|
37
|
+
*/
|
38
|
+
export async function detectApiMode(basePath) {
|
39
|
+
let useTables = false;
|
40
|
+
let detectionSource = "default";
|
41
|
+
let serverVersion;
|
42
|
+
try {
|
43
|
+
// Priority 1: Check for existing appwrite.json project config
|
44
|
+
const projectConfigPath = findAppwriteProjectConfig(basePath);
|
45
|
+
if (projectConfigPath) {
|
46
|
+
const projectConfig = loadAppwriteProjectConfig(projectConfigPath);
|
47
|
+
if (projectConfig) {
|
48
|
+
useTables = isTablesDBProject(projectConfig);
|
49
|
+
detectionSource = "appwrite.json";
|
50
|
+
MessageFormatter.info(`Detected ${useTables ? 'TablesDB' : 'Collections'} project from ${projectConfigPath}`, { prefix: "Setup" });
|
51
|
+
return {
|
52
|
+
apiMode: useTables ? 'tablesdb' : 'legacy',
|
53
|
+
useTables,
|
54
|
+
detectionSource
|
55
|
+
};
|
56
|
+
}
|
57
|
+
}
|
58
|
+
// Priority 2: Try reading existing YAML config for version detection
|
59
|
+
const yamlPath = findYamlConfig(basePath);
|
60
|
+
if (yamlPath) {
|
61
|
+
const cfg = await loadYamlConfig(yamlPath);
|
62
|
+
if (cfg) {
|
63
|
+
const endpoint = cfg.appwriteEndpoint;
|
64
|
+
const projectId = cfg.appwriteProject;
|
65
|
+
if (hasSessionAuth(endpoint, projectId)) {
|
66
|
+
MessageFormatter.info("Using session authentication for version detection", { prefix: "Setup" });
|
67
|
+
}
|
68
|
+
const ver = await fetchServerVersion(endpoint);
|
69
|
+
serverVersion = ver || undefined;
|
70
|
+
if (isVersionAtLeast(ver || undefined, '1.8.0')) {
|
71
|
+
useTables = true;
|
72
|
+
detectionSource = "server-version";
|
73
|
+
MessageFormatter.info(`Detected TablesDB support (Appwrite ${ver})`, { prefix: "Setup" });
|
74
|
+
}
|
75
|
+
else {
|
76
|
+
MessageFormatter.info(`Using Collections API (Appwrite ${ver || 'unknown'})`, { prefix: "Setup" });
|
77
|
+
}
|
78
|
+
return {
|
79
|
+
apiMode: useTables ? 'tablesdb' : 'legacy',
|
80
|
+
useTables,
|
81
|
+
detectionSource,
|
82
|
+
serverVersion
|
83
|
+
};
|
84
|
+
}
|
85
|
+
}
|
86
|
+
}
|
87
|
+
catch (error) {
|
88
|
+
MessageFormatter.warning(`Version detection failed, defaulting to Collections API: ${error instanceof Error ? error.message : String(error)}`, { prefix: "Setup" });
|
89
|
+
}
|
90
|
+
// Default to Collections API
|
91
|
+
return {
|
92
|
+
apiMode: 'legacy',
|
93
|
+
useTables: false,
|
94
|
+
detectionSource: 'default'
|
95
|
+
};
|
96
|
+
}
|
97
|
+
/**
|
98
|
+
* Create directory structure for Appwrite project
|
99
|
+
*/
|
100
|
+
export function createProjectDirectories(basePath, useTables) {
|
101
|
+
const appwriteFolder = path.join(basePath, ".appwrite");
|
102
|
+
const containerName = useTables ? "tables" : "collections";
|
103
|
+
const containerFolder = path.join(appwriteFolder, containerName);
|
104
|
+
const schemaFolder = path.join(appwriteFolder, "schemas");
|
105
|
+
const yamlSchemaFolder = path.join(appwriteFolder, ".yaml_schemas");
|
106
|
+
const dataFolder = path.join(appwriteFolder, "importData");
|
107
|
+
// Create all directories
|
108
|
+
for (const dir of [appwriteFolder, containerFolder, schemaFolder, yamlSchemaFolder, dataFolder]) {
|
109
|
+
if (!existsSync(dir)) {
|
110
|
+
mkdirSync(dir, { recursive: true });
|
111
|
+
}
|
112
|
+
}
|
113
|
+
return {
|
114
|
+
appwriteFolder,
|
115
|
+
containerFolder,
|
116
|
+
schemaFolder,
|
117
|
+
yamlSchemaFolder,
|
118
|
+
dataFolder
|
119
|
+
};
|
120
|
+
}
|
121
|
+
/**
|
122
|
+
* Create example YAML schema file with correct terminology
|
123
|
+
*/
|
124
|
+
export function createExampleSchema(containerFolder, terminology) {
|
125
|
+
const yamlExample = `# yaml-language-server: $schema=../.yaml_schemas/${terminology.schemaRef}
|
126
|
+
# Example ${terminology.containerName} Definition
|
127
|
+
name: Example${terminology.containerName}
|
128
|
+
id: example_${terminology.container}_${Date.now()}
|
129
|
+
${terminology.security}: false
|
130
|
+
enabled: true
|
131
|
+
permissions:
|
132
|
+
- permission: read
|
133
|
+
target: any
|
134
|
+
- permission: create
|
135
|
+
target: users
|
136
|
+
- permission: update
|
137
|
+
target: users
|
138
|
+
- permission: delete
|
139
|
+
target: users
|
140
|
+
${terminology.fields}:
|
141
|
+
- key: title
|
142
|
+
type: string
|
143
|
+
size: 255
|
144
|
+
required: true
|
145
|
+
description: "The title of the item"
|
146
|
+
- key: description
|
147
|
+
type: string
|
148
|
+
size: 1000
|
149
|
+
required: false
|
150
|
+
description: "A longer description"
|
151
|
+
- key: isActive
|
152
|
+
type: boolean
|
153
|
+
required: false
|
154
|
+
default: true${terminology.container === 'table' ? `
|
155
|
+
- key: uniqueCode
|
156
|
+
type: string
|
157
|
+
size: 50
|
158
|
+
required: false
|
159
|
+
unique: true
|
160
|
+
description: "Unique identifier code (TablesDB feature)"` : ''}
|
161
|
+
indexes:
|
162
|
+
- key: title_search
|
163
|
+
type: fulltext
|
164
|
+
attributes:
|
165
|
+
- title
|
166
|
+
importDefs: []
|
167
|
+
`;
|
168
|
+
const examplePath = path.join(containerFolder, `Example${terminology.containerName}.yaml`);
|
169
|
+
writeFileSync(examplePath, yamlExample);
|
170
|
+
MessageFormatter.info(`Created example ${terminology.container} definition with ${terminology.fields} terminology`, { prefix: "Setup" });
|
171
|
+
return examplePath;
|
172
|
+
}
|
173
|
+
/**
|
174
|
+
* Create JSON schema for YAML validation
|
175
|
+
*/
|
176
|
+
export function createYamlValidationSchema(yamlSchemaFolder, useTables) {
|
177
|
+
const schemaFileName = useTables ? "table.schema.json" : "collection.schema.json";
|
178
|
+
const containerType = useTables ? "Table" : "Collection";
|
179
|
+
const fieldsName = useTables ? "columns" : "attributes";
|
180
|
+
const fieldsDescription = useTables ? "Table columns (fields)" : "Collection attributes (fields)";
|
181
|
+
const securityField = useTables ? "rowSecurity" : "documentSecurity";
|
182
|
+
const securityDescription = useTables ? "Enable row-level permissions" : "Enable document-level permissions";
|
183
|
+
const itemType = useTables ? "row" : "document";
|
184
|
+
const schema = {
|
185
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
186
|
+
"$id": `https://appwrite-utils.dev/schemas/${schemaFileName}`,
|
187
|
+
"title": `Appwrite ${containerType} Definition`,
|
188
|
+
"description": `Schema for defining Appwrite ${useTables ? 'tables' : 'collections'} in YAML${useTables ? ' (TablesDB API)' : ''}`,
|
189
|
+
"type": "object",
|
190
|
+
"properties": {
|
191
|
+
"name": {
|
192
|
+
"type": "string",
|
193
|
+
"description": `The name of the ${useTables ? 'table' : 'collection'}`
|
194
|
+
},
|
195
|
+
"id": {
|
196
|
+
"type": "string",
|
197
|
+
"description": `The ID of the ${useTables ? 'table' : 'collection'} (optional, auto-generated if not provided)`,
|
198
|
+
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,35}$"
|
199
|
+
},
|
200
|
+
[securityField]: {
|
201
|
+
"type": "boolean",
|
202
|
+
"default": false,
|
203
|
+
"description": securityDescription
|
204
|
+
},
|
205
|
+
"enabled": {
|
206
|
+
"type": "boolean",
|
207
|
+
"default": true,
|
208
|
+
"description": `Whether the ${useTables ? 'table' : 'collection'} is enabled`
|
209
|
+
},
|
210
|
+
"permissions": {
|
211
|
+
"type": "array",
|
212
|
+
"description": `${containerType}-level permissions`,
|
213
|
+
"items": {
|
214
|
+
"type": "object",
|
215
|
+
"properties": {
|
216
|
+
"permission": {
|
217
|
+
"type": "string",
|
218
|
+
"enum": ["read", "create", "update", "delete"],
|
219
|
+
"description": "The permission type"
|
220
|
+
},
|
221
|
+
"target": {
|
222
|
+
"type": "string",
|
223
|
+
"description": "Permission target (e.g., 'any', 'users', 'users/verified', 'label:admin')"
|
224
|
+
}
|
225
|
+
},
|
226
|
+
"required": ["permission", "target"],
|
227
|
+
"additionalProperties": false
|
228
|
+
}
|
229
|
+
},
|
230
|
+
[fieldsName]: {
|
231
|
+
"type": "array",
|
232
|
+
"description": fieldsDescription,
|
233
|
+
"items": {
|
234
|
+
"type": "object",
|
235
|
+
"properties": {
|
236
|
+
"key": {
|
237
|
+
"type": "string",
|
238
|
+
"description": `${useTables ? 'Column' : 'Attribute'} name`,
|
239
|
+
"pattern": "^[a-zA-Z][a-zA-Z0-9]*$"
|
240
|
+
},
|
241
|
+
"type": {
|
242
|
+
"type": "string",
|
243
|
+
"enum": ["string", "integer", "double", "boolean", "datetime", "email", "ip", "url", "enum", "relationship"],
|
244
|
+
"description": `${useTables ? 'Column' : 'Attribute'} data type`
|
245
|
+
},
|
246
|
+
"size": {
|
247
|
+
"type": "number",
|
248
|
+
"description": `Maximum size for string ${useTables ? 'columns' : 'attributes'}`,
|
249
|
+
"minimum": 1,
|
250
|
+
"maximum": 1073741824
|
251
|
+
},
|
252
|
+
"required": {
|
253
|
+
"type": "boolean",
|
254
|
+
"default": false,
|
255
|
+
"description": `Whether the ${useTables ? 'column' : 'attribute'} is required`
|
256
|
+
},
|
257
|
+
"array": {
|
258
|
+
"type": "boolean",
|
259
|
+
"default": false,
|
260
|
+
"description": `Whether the ${useTables ? 'column' : 'attribute'} is an array`
|
261
|
+
},
|
262
|
+
...(useTables ? {
|
263
|
+
"unique": {
|
264
|
+
"type": "boolean",
|
265
|
+
"default": false,
|
266
|
+
"description": "Whether the column values must be unique (TablesDB feature)"
|
267
|
+
}
|
268
|
+
} : {}),
|
269
|
+
"default": {
|
270
|
+
"description": `Default value for the ${useTables ? 'column' : 'attribute'}`
|
271
|
+
},
|
272
|
+
"description": {
|
273
|
+
"type": "string",
|
274
|
+
"description": `${useTables ? 'Column' : 'Attribute'} description`
|
275
|
+
},
|
276
|
+
"min": {
|
277
|
+
"type": "number",
|
278
|
+
"description": `Minimum value for numeric ${useTables ? 'columns' : 'attributes'}`
|
279
|
+
},
|
280
|
+
"max": {
|
281
|
+
"type": "number",
|
282
|
+
"description": `Maximum value for numeric ${useTables ? 'columns' : 'attributes'}`
|
283
|
+
},
|
284
|
+
"elements": {
|
285
|
+
"type": "array",
|
286
|
+
"items": {
|
287
|
+
"type": "string"
|
288
|
+
},
|
289
|
+
"description": `Allowed values for enum ${useTables ? 'columns' : 'attributes'}`
|
290
|
+
},
|
291
|
+
"relatedCollection": {
|
292
|
+
"type": "string",
|
293
|
+
"description": `Related ${useTables ? 'table' : 'collection'} name for relationship ${useTables ? 'columns' : 'attributes'}`
|
294
|
+
},
|
295
|
+
"relationType": {
|
296
|
+
"type": "string",
|
297
|
+
"enum": ["oneToOne", "oneToMany", "manyToOne", "manyToMany"],
|
298
|
+
"description": "Type of relationship"
|
299
|
+
},
|
300
|
+
"twoWay": {
|
301
|
+
"type": "boolean",
|
302
|
+
"description": "Whether the relationship is bidirectional"
|
303
|
+
},
|
304
|
+
"twoWayKey": {
|
305
|
+
"type": "string",
|
306
|
+
"description": "Key name for the reverse relationship"
|
307
|
+
},
|
308
|
+
"onDelete": {
|
309
|
+
"type": "string",
|
310
|
+
"enum": ["cascade", "restrict", "setNull"],
|
311
|
+
"description": `Action to take when related ${itemType} is deleted`
|
312
|
+
},
|
313
|
+
"side": {
|
314
|
+
"type": "string",
|
315
|
+
"enum": ["parent", "child"],
|
316
|
+
"description": "Side of the relationship"
|
317
|
+
}
|
318
|
+
},
|
319
|
+
"required": ["key", "type"],
|
320
|
+
"additionalProperties": false,
|
321
|
+
"allOf": [
|
322
|
+
{
|
323
|
+
"if": {
|
324
|
+
"properties": { "type": { "const": "enum" } }
|
325
|
+
},
|
326
|
+
"then": {
|
327
|
+
"required": ["elements"]
|
328
|
+
}
|
329
|
+
},
|
330
|
+
{
|
331
|
+
"if": {
|
332
|
+
"properties": { "type": { "const": "relationship" } }
|
333
|
+
},
|
334
|
+
"then": {
|
335
|
+
"required": ["relatedCollection", "relationType"]
|
336
|
+
}
|
337
|
+
}
|
338
|
+
]
|
339
|
+
}
|
340
|
+
},
|
341
|
+
"indexes": {
|
342
|
+
"type": "array",
|
343
|
+
"description": `Database indexes for the ${useTables ? 'table' : 'collection'}`,
|
344
|
+
"items": {
|
345
|
+
"type": "object",
|
346
|
+
"properties": {
|
347
|
+
"key": {
|
348
|
+
"type": "string",
|
349
|
+
"description": "Index name"
|
350
|
+
},
|
351
|
+
"type": {
|
352
|
+
"type": "string",
|
353
|
+
"enum": ["key", "fulltext", "unique"],
|
354
|
+
"description": "Index type"
|
355
|
+
},
|
356
|
+
"attributes": {
|
357
|
+
"type": "array",
|
358
|
+
"items": {
|
359
|
+
"type": "string"
|
360
|
+
},
|
361
|
+
"description": `${useTables ? 'Columns' : 'Attributes'} to index`,
|
362
|
+
"minItems": 1
|
363
|
+
},
|
364
|
+
"orders": {
|
365
|
+
"type": "array",
|
366
|
+
"items": {
|
367
|
+
"type": "string",
|
368
|
+
"enum": ["ASC", "DESC"]
|
369
|
+
},
|
370
|
+
"description": `Sort order for each ${useTables ? 'column' : 'attribute'}`
|
371
|
+
}
|
372
|
+
},
|
373
|
+
"required": ["key", "type", "attributes"],
|
374
|
+
"additionalProperties": false
|
375
|
+
}
|
376
|
+
},
|
377
|
+
"importDefs": {
|
378
|
+
"type": "array",
|
379
|
+
"description": "Import definitions for data migration",
|
380
|
+
"default": []
|
381
|
+
}
|
382
|
+
},
|
383
|
+
"required": ["name"],
|
384
|
+
"additionalProperties": false
|
385
|
+
};
|
386
|
+
const schemaPath = path.join(yamlSchemaFolder, schemaFileName);
|
387
|
+
writeFileSync(schemaPath, JSON.stringify(schema, null, 2));
|
388
|
+
return schemaPath;
|
389
|
+
}
|
390
|
+
/**
|
391
|
+
* Initialize a new Appwrite project with correct directory structure and terminology
|
392
|
+
*/
|
393
|
+
export async function initProject(basePath, forceApiMode) {
|
394
|
+
const projectPath = basePath || process.cwd();
|
395
|
+
// Detect API mode
|
396
|
+
const detection = forceApiMode
|
397
|
+
? {
|
398
|
+
apiMode: forceApiMode,
|
399
|
+
useTables: forceApiMode === 'tablesdb',
|
400
|
+
detectionSource: 'forced',
|
401
|
+
}
|
402
|
+
: await detectApiMode(projectPath);
|
403
|
+
const { useTables, detectionSource } = detection;
|
404
|
+
const terminology = getTerminologyConfig(useTables);
|
405
|
+
// Create directory structure
|
406
|
+
const dirs = createProjectDirectories(projectPath, useTables);
|
407
|
+
// Generate YAML config
|
408
|
+
const configPath = path.join(dirs.appwriteFolder, "config.yaml");
|
409
|
+
generateYamlConfigTemplate(configPath);
|
410
|
+
// Create example schema file
|
411
|
+
createExampleSchema(dirs.containerFolder, terminology);
|
412
|
+
// Create JSON validation schema
|
413
|
+
createYamlValidationSchema(dirs.yamlSchemaFolder, useTables);
|
414
|
+
// Success messages
|
415
|
+
const containerType = useTables ? "TablesDB" : "Collections";
|
416
|
+
MessageFormatter.success(`Created YAML config and setup files/directories in .appwrite/ folder.`, { prefix: "Setup" });
|
417
|
+
MessageFormatter.info(`Project configured for ${containerType} API (${detectionSource} detection)`, { prefix: "Setup" });
|
418
|
+
MessageFormatter.info("You can now configure your project in .appwrite/config.yaml", { prefix: "Setup" });
|
419
|
+
MessageFormatter.info(`${terminology.containerName}s can be defined in .appwrite/${terminology.container}s/ as .yaml files`, { prefix: "Setup" });
|
420
|
+
MessageFormatter.info("Schemas will be generated in .appwrite/schemas/", { prefix: "Setup" });
|
421
|
+
MessageFormatter.info("Import data can be placed in .appwrite/importData/", { prefix: "Setup" });
|
422
|
+
if (useTables) {
|
423
|
+
MessageFormatter.info("TablesDB features: unique constraints, enhanced performance, row-level security", { prefix: "Setup" });
|
424
|
+
}
|
425
|
+
}
|
426
|
+
/**
|
427
|
+
* Create a new collection or table schema file
|
428
|
+
*/
|
429
|
+
export async function createSchema(name, basePath, forceApiMode) {
|
430
|
+
const projectPath = basePath || process.cwd();
|
431
|
+
// Detect API mode
|
432
|
+
const detection = forceApiMode
|
433
|
+
? {
|
434
|
+
apiMode: forceApiMode,
|
435
|
+
useTables: forceApiMode === 'tablesdb',
|
436
|
+
detectionSource: 'forced',
|
437
|
+
}
|
438
|
+
: await detectApiMode(projectPath);
|
439
|
+
const { useTables } = detection;
|
440
|
+
const terminology = getTerminologyConfig(useTables);
|
441
|
+
// Find or create container directory
|
442
|
+
const appwriteFolder = path.join(projectPath, ".appwrite");
|
443
|
+
const containerFolder = path.join(appwriteFolder, useTables ? "tables" : "collections");
|
444
|
+
if (!existsSync(containerFolder)) {
|
445
|
+
mkdirSync(containerFolder, { recursive: true });
|
446
|
+
}
|
447
|
+
// Create YAML schema file
|
448
|
+
const yamlSchema = `# yaml-language-server: $schema=../.yaml_schemas/${terminology.schemaRef}
|
449
|
+
# ${terminology.containerName} Definition: ${name}
|
450
|
+
name: ${name}
|
451
|
+
id: ${ulid()}
|
452
|
+
${terminology.security}: false
|
453
|
+
enabled: true
|
454
|
+
permissions:
|
455
|
+
- permission: read
|
456
|
+
target: any
|
457
|
+
- permission: create
|
458
|
+
target: users
|
459
|
+
- permission: update
|
460
|
+
target: users
|
461
|
+
- permission: delete
|
462
|
+
target: users
|
463
|
+
${terminology.fields}:
|
464
|
+
# Add your ${terminology.fields} here
|
465
|
+
# Example:
|
466
|
+
# - key: title
|
467
|
+
# type: string
|
468
|
+
# size: 255
|
469
|
+
# required: true
|
470
|
+
# description: "The title of the item"
|
471
|
+
indexes:
|
472
|
+
# Add your indexes here
|
473
|
+
# Example:
|
474
|
+
# - key: title_search
|
475
|
+
# type: fulltext
|
476
|
+
# attributes:
|
477
|
+
# - title
|
478
|
+
importDefs: []
|
479
|
+
`;
|
480
|
+
const schemaPath = path.join(containerFolder, `${name}.yaml`);
|
481
|
+
writeFileSync(schemaPath, yamlSchema);
|
482
|
+
MessageFormatter.success(`Created ${terminology.container} schema: ${schemaPath}`, { prefix: "Setup" });
|
483
|
+
MessageFormatter.info(`Add your ${terminology.fields} to define the ${terminology.container} structure`, { prefix: "Setup" });
|
484
|
+
}
|
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.6.
|
4
|
+
"version": "1.6.6",
|
5
5
|
"main": "src/main.ts",
|
6
6
|
"type": "module",
|
7
7
|
"repository": {
|
@@ -5,6 +5,9 @@ import { delay, tryAwaitWithRetry, calculateExponentialBackoff } from "../utils/
|
|
5
5
|
import { isLegacyDatabases } from "../utils/typeGuards.js";
|
6
6
|
import { MessageFormatter } from "../shared/messageFormatter.js";
|
7
7
|
|
8
|
+
// System attributes that are always available for indexing in Appwrite
|
9
|
+
const SYSTEM_ATTRIBUTES = ['$id', '$createdAt', '$updatedAt', '$permissions'];
|
10
|
+
|
8
11
|
// Interface for index with status
|
9
12
|
interface IndexWithStatus {
|
10
13
|
key: string;
|
@@ -116,12 +119,15 @@ export const createOrUpdateIndexWithStatusCheck = async (
|
|
116
119
|
// First, validate that all required attributes exist
|
117
120
|
const freshCollection = await db.getCollection(dbId, collectionId);
|
118
121
|
const existingAttributeKeys = freshCollection.attributes.map((attr: any) => attr.key);
|
119
|
-
|
120
|
-
|
122
|
+
|
123
|
+
// Include system attributes that are always available
|
124
|
+
const allAvailableAttributes = [...existingAttributeKeys, ...SYSTEM_ATTRIBUTES];
|
125
|
+
|
126
|
+
const missingAttributes = index.attributes.filter(attr => !allAvailableAttributes.includes(attr));
|
121
127
|
|
122
128
|
if (missingAttributes.length > 0) {
|
123
129
|
MessageFormatter.error(`Index '${index.key}' cannot be created: missing attributes [${missingAttributes.join(', ')}] (type: ${index.type})`);
|
124
|
-
MessageFormatter.error(`Available attributes: [${existingAttributeKeys.join(', ')}]`);
|
130
|
+
MessageFormatter.error(`Available attributes: [${existingAttributeKeys.join(', ')}, ${SYSTEM_ATTRIBUTES.join(', ')}]`);
|
125
131
|
return false; // Don't retry if attributes are missing
|
126
132
|
}
|
127
133
|
|
@@ -279,17 +285,35 @@ export const createOrUpdateIndex = async (
|
|
279
285
|
if (existingIndex.total === 0) {
|
280
286
|
// No existing index, create it
|
281
287
|
createIndex = true;
|
282
|
-
} else
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
288
|
+
} else {
|
289
|
+
const existing = existingIndex.indexes[0];
|
290
|
+
|
291
|
+
// Check key and type
|
292
|
+
const keyMatches = existing.key === index.key;
|
293
|
+
const typeMatches = existing.type === index.type;
|
294
|
+
|
295
|
+
// Compare attributes as SETS (order doesn't matter, only content)
|
296
|
+
const existingAttrsSet = new Set(existing.attributes);
|
297
|
+
const newAttrsSet = new Set(index.attributes);
|
298
|
+
const attributesMatch =
|
299
|
+
existingAttrsSet.size === newAttrsSet.size &&
|
300
|
+
[...existingAttrsSet].every(attr => newAttrsSet.has(attr));
|
301
|
+
|
302
|
+
// Compare orders as SETS if both exist (order doesn't matter)
|
303
|
+
let ordersMatch = true;
|
304
|
+
if (index.orders && existing.orders) {
|
305
|
+
const existingOrdersSet = new Set(existing.orders);
|
306
|
+
const newOrdersSet = new Set(index.orders);
|
307
|
+
ordersMatch =
|
308
|
+
existingOrdersSet.size === newOrdersSet.size &&
|
309
|
+
[...existingOrdersSet].every(ord => newOrdersSet.has(ord));
|
310
|
+
}
|
311
|
+
|
312
|
+
// Only recreate if something genuinely changed
|
313
|
+
if (!keyMatches || !typeMatches || !attributesMatch || !ordersMatch) {
|
314
|
+
await db.deleteIndex(dbId, collectionId, existing.key);
|
315
|
+
createIndex = true;
|
316
|
+
}
|
293
317
|
}
|
294
318
|
|
295
319
|
if (createIndex) {
|
@@ -339,8 +339,11 @@ export const createOrUpdateCollections = async (
|
|
339
339
|
// Add delay after creating attributes
|
340
340
|
await delay(250);
|
341
341
|
|
342
|
-
//
|
343
|
-
const
|
342
|
+
// ALWAYS use indexes from local config, NEVER from server
|
343
|
+
const localCollectionConfig = config.collections?.find(
|
344
|
+
c => c.name === collectionData.name || c.$id === collectionData.$id
|
345
|
+
);
|
346
|
+
const indexesToUse = localCollectionConfig?.indexes || [];
|
344
347
|
|
345
348
|
MessageFormatter.progress("Creating Indexes", { prefix: "Collections" });
|
346
349
|
await createOrUpdateIndexesWithStatusCheck(
|
@@ -351,6 +354,16 @@ export const createOrUpdateCollections = async (
|
|
351
354
|
indexesToUse as Indexes
|
352
355
|
);
|
353
356
|
|
357
|
+
// Delete indexes that exist on server but not in local config
|
358
|
+
const { deleteObsoleteIndexes } = await import('../shared/indexManager.js');
|
359
|
+
await deleteObsoleteIndexes(
|
360
|
+
database,
|
361
|
+
databaseId,
|
362
|
+
collectionToUse!,
|
363
|
+
{ indexes: indexesToUse } as any,
|
364
|
+
{ verbose: true }
|
365
|
+
);
|
366
|
+
|
354
367
|
// Mark this collection as fully processed to prevent re-processing
|
355
368
|
markCollectionProcessed(collectionToUse!.$id, collectionData.name);
|
356
369
|
|
@@ -562,8 +575,11 @@ export const createOrUpdateCollectionsViaAdapter = async (
|
|
562
575
|
}
|
563
576
|
}
|
564
577
|
|
565
|
-
//
|
566
|
-
const
|
578
|
+
// ALWAYS use indexes from local config, NEVER from server (TablesDB path)
|
579
|
+
const localTableConfig = config.collections?.find(
|
580
|
+
c => c.name === collectionData.name || c.$id === collectionData.$id
|
581
|
+
);
|
582
|
+
const idxs = (localTableConfig?.indexes || []) as any[];
|
567
583
|
for (const idx of idxs) {
|
568
584
|
try {
|
569
585
|
await adapter.createIndex({
|
package/src/setupCommands.ts
CHANGED
@@ -0,0 +1,597 @@
|
|
1
|
+
import { mkdirSync, writeFileSync, existsSync } from "node:fs";
|
2
|
+
import path from "node:path";
|
3
|
+
import { ulid } from "ulidx";
|
4
|
+
import { MessageFormatter } from "./shared/messageFormatter.js";
|
5
|
+
import {
|
6
|
+
detectAppwriteVersionCached,
|
7
|
+
fetchServerVersion,
|
8
|
+
isVersionAtLeast,
|
9
|
+
type ApiMode
|
10
|
+
} from "./utils/versionDetection.js";
|
11
|
+
import {
|
12
|
+
loadAppwriteProjectConfig,
|
13
|
+
findAppwriteProjectConfig,
|
14
|
+
isTablesDBProject
|
15
|
+
} from "./utils/projectConfig.js";
|
16
|
+
import { findYamlConfig, generateYamlConfigTemplate } from "./config/yamlConfig.js";
|
17
|
+
import { loadYamlConfig } from "./config/yamlConfig.js";
|
18
|
+
import { hasSessionAuth } from "./utils/sessionAuth.js";
|
19
|
+
|
20
|
+
/**
|
21
|
+
* Terminology configuration for API mode-specific naming
|
22
|
+
*/
|
23
|
+
interface TerminologyConfig {
|
24
|
+
container: "table" | "collection";
|
25
|
+
containerName: "Table" | "Collection";
|
26
|
+
fields: "columns" | "attributes";
|
27
|
+
fieldName: "Column" | "Attribute";
|
28
|
+
security: "rowSecurity" | "documentSecurity";
|
29
|
+
schemaRef: "table.schema.json" | "collection.schema.json";
|
30
|
+
items: "rows" | "documents";
|
31
|
+
}
|
32
|
+
|
33
|
+
/**
|
34
|
+
* Detection result with source information
|
35
|
+
*/
|
36
|
+
interface ApiModeDetectionResult {
|
37
|
+
apiMode: ApiMode;
|
38
|
+
useTables: boolean;
|
39
|
+
detectionSource: "appwrite.json" | "server-version" | "default";
|
40
|
+
serverVersion?: string;
|
41
|
+
}
|
42
|
+
|
43
|
+
/**
|
44
|
+
* Get terminology configuration based on API mode
|
45
|
+
*/
|
46
|
+
export function getTerminologyConfig(useTables: boolean): TerminologyConfig {
|
47
|
+
return useTables
|
48
|
+
? {
|
49
|
+
container: "table",
|
50
|
+
containerName: "Table",
|
51
|
+
fields: "columns",
|
52
|
+
fieldName: "Column",
|
53
|
+
security: "rowSecurity",
|
54
|
+
schemaRef: "table.schema.json",
|
55
|
+
items: "rows"
|
56
|
+
}
|
57
|
+
: {
|
58
|
+
container: "collection",
|
59
|
+
containerName: "Collection",
|
60
|
+
fields: "attributes",
|
61
|
+
fieldName: "Attribute",
|
62
|
+
security: "documentSecurity",
|
63
|
+
schemaRef: "collection.schema.json",
|
64
|
+
items: "documents"
|
65
|
+
};
|
66
|
+
}
|
67
|
+
|
68
|
+
/**
|
69
|
+
* Detect API mode using multiple detection sources
|
70
|
+
* Priority: appwrite.json > server version > default (collections)
|
71
|
+
*/
|
72
|
+
export async function detectApiMode(basePath: string): Promise<ApiModeDetectionResult> {
|
73
|
+
let useTables = false;
|
74
|
+
let detectionSource: "appwrite.json" | "server-version" | "default" = "default";
|
75
|
+
let serverVersion: string | undefined;
|
76
|
+
|
77
|
+
try {
|
78
|
+
// Priority 1: Check for existing appwrite.json project config
|
79
|
+
const projectConfigPath = findAppwriteProjectConfig(basePath);
|
80
|
+
if (projectConfigPath) {
|
81
|
+
const projectConfig = loadAppwriteProjectConfig(projectConfigPath);
|
82
|
+
if (projectConfig) {
|
83
|
+
useTables = isTablesDBProject(projectConfig);
|
84
|
+
detectionSource = "appwrite.json";
|
85
|
+
MessageFormatter.info(
|
86
|
+
`Detected ${useTables ? 'TablesDB' : 'Collections'} project from ${projectConfigPath}`,
|
87
|
+
{ prefix: "Setup" }
|
88
|
+
);
|
89
|
+
return {
|
90
|
+
apiMode: useTables ? 'tablesdb' : 'legacy',
|
91
|
+
useTables,
|
92
|
+
detectionSource
|
93
|
+
};
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
// Priority 2: Try reading existing YAML config for version detection
|
98
|
+
const yamlPath = findYamlConfig(basePath);
|
99
|
+
if (yamlPath) {
|
100
|
+
const cfg = await loadYamlConfig(yamlPath);
|
101
|
+
if (cfg) {
|
102
|
+
const endpoint = cfg.appwriteEndpoint;
|
103
|
+
const projectId = cfg.appwriteProject;
|
104
|
+
|
105
|
+
if (hasSessionAuth(endpoint, projectId)) {
|
106
|
+
MessageFormatter.info("Using session authentication for version detection", { prefix: "Setup" });
|
107
|
+
}
|
108
|
+
|
109
|
+
const ver = await fetchServerVersion(endpoint);
|
110
|
+
serverVersion = ver || undefined;
|
111
|
+
|
112
|
+
if (isVersionAtLeast(ver || undefined, '1.8.0')) {
|
113
|
+
useTables = true;
|
114
|
+
detectionSource = "server-version";
|
115
|
+
MessageFormatter.info(`Detected TablesDB support (Appwrite ${ver})`, { prefix: "Setup" });
|
116
|
+
} else {
|
117
|
+
MessageFormatter.info(`Using Collections API (Appwrite ${ver || 'unknown'})`, { prefix: "Setup" });
|
118
|
+
}
|
119
|
+
|
120
|
+
return {
|
121
|
+
apiMode: useTables ? 'tablesdb' : 'legacy',
|
122
|
+
useTables,
|
123
|
+
detectionSource,
|
124
|
+
serverVersion
|
125
|
+
};
|
126
|
+
}
|
127
|
+
}
|
128
|
+
} catch (error) {
|
129
|
+
MessageFormatter.warning(
|
130
|
+
`Version detection failed, defaulting to Collections API: ${error instanceof Error ? error.message : String(error)}`,
|
131
|
+
{ prefix: "Setup" }
|
132
|
+
);
|
133
|
+
}
|
134
|
+
|
135
|
+
// Default to Collections API
|
136
|
+
return {
|
137
|
+
apiMode: 'legacy',
|
138
|
+
useTables: false,
|
139
|
+
detectionSource: 'default'
|
140
|
+
};
|
141
|
+
}
|
142
|
+
|
143
|
+
/**
|
144
|
+
* Create directory structure for Appwrite project
|
145
|
+
*/
|
146
|
+
export function createProjectDirectories(basePath: string, useTables: boolean): {
|
147
|
+
appwriteFolder: string;
|
148
|
+
containerFolder: string;
|
149
|
+
schemaFolder: string;
|
150
|
+
yamlSchemaFolder: string;
|
151
|
+
dataFolder: string;
|
152
|
+
} {
|
153
|
+
const appwriteFolder = path.join(basePath, ".appwrite");
|
154
|
+
const containerName = useTables ? "tables" : "collections";
|
155
|
+
const containerFolder = path.join(appwriteFolder, containerName);
|
156
|
+
const schemaFolder = path.join(appwriteFolder, "schemas");
|
157
|
+
const yamlSchemaFolder = path.join(appwriteFolder, ".yaml_schemas");
|
158
|
+
const dataFolder = path.join(appwriteFolder, "importData");
|
159
|
+
|
160
|
+
// Create all directories
|
161
|
+
for (const dir of [appwriteFolder, containerFolder, schemaFolder, yamlSchemaFolder, dataFolder]) {
|
162
|
+
if (!existsSync(dir)) {
|
163
|
+
mkdirSync(dir, { recursive: true });
|
164
|
+
}
|
165
|
+
}
|
166
|
+
|
167
|
+
return {
|
168
|
+
appwriteFolder,
|
169
|
+
containerFolder,
|
170
|
+
schemaFolder,
|
171
|
+
yamlSchemaFolder,
|
172
|
+
dataFolder
|
173
|
+
};
|
174
|
+
}
|
175
|
+
|
176
|
+
/**
|
177
|
+
* Create example YAML schema file with correct terminology
|
178
|
+
*/
|
179
|
+
export function createExampleSchema(
|
180
|
+
containerFolder: string,
|
181
|
+
terminology: TerminologyConfig
|
182
|
+
): string {
|
183
|
+
const yamlExample = `# yaml-language-server: $schema=../.yaml_schemas/${terminology.schemaRef}
|
184
|
+
# Example ${terminology.containerName} Definition
|
185
|
+
name: Example${terminology.containerName}
|
186
|
+
id: example_${terminology.container}_${Date.now()}
|
187
|
+
${terminology.security}: false
|
188
|
+
enabled: true
|
189
|
+
permissions:
|
190
|
+
- permission: read
|
191
|
+
target: any
|
192
|
+
- permission: create
|
193
|
+
target: users
|
194
|
+
- permission: update
|
195
|
+
target: users
|
196
|
+
- permission: delete
|
197
|
+
target: users
|
198
|
+
${terminology.fields}:
|
199
|
+
- key: title
|
200
|
+
type: string
|
201
|
+
size: 255
|
202
|
+
required: true
|
203
|
+
description: "The title of the item"
|
204
|
+
- key: description
|
205
|
+
type: string
|
206
|
+
size: 1000
|
207
|
+
required: false
|
208
|
+
description: "A longer description"
|
209
|
+
- key: isActive
|
210
|
+
type: boolean
|
211
|
+
required: false
|
212
|
+
default: true${terminology.container === 'table' ? `
|
213
|
+
- key: uniqueCode
|
214
|
+
type: string
|
215
|
+
size: 50
|
216
|
+
required: false
|
217
|
+
unique: true
|
218
|
+
description: "Unique identifier code (TablesDB feature)"` : ''}
|
219
|
+
indexes:
|
220
|
+
- key: title_search
|
221
|
+
type: fulltext
|
222
|
+
attributes:
|
223
|
+
- title
|
224
|
+
importDefs: []
|
225
|
+
`;
|
226
|
+
|
227
|
+
const examplePath = path.join(containerFolder, `Example${terminology.containerName}.yaml`);
|
228
|
+
writeFileSync(examplePath, yamlExample);
|
229
|
+
|
230
|
+
MessageFormatter.info(
|
231
|
+
`Created example ${terminology.container} definition with ${terminology.fields} terminology`,
|
232
|
+
{ prefix: "Setup" }
|
233
|
+
);
|
234
|
+
|
235
|
+
return examplePath;
|
236
|
+
}
|
237
|
+
|
238
|
+
/**
|
239
|
+
* Create JSON schema for YAML validation
|
240
|
+
*/
|
241
|
+
export function createYamlValidationSchema(
|
242
|
+
yamlSchemaFolder: string,
|
243
|
+
useTables: boolean
|
244
|
+
): string {
|
245
|
+
const schemaFileName = useTables ? "table.schema.json" : "collection.schema.json";
|
246
|
+
const containerType = useTables ? "Table" : "Collection";
|
247
|
+
const fieldsName = useTables ? "columns" : "attributes";
|
248
|
+
const fieldsDescription = useTables ? "Table columns (fields)" : "Collection attributes (fields)";
|
249
|
+
const securityField = useTables ? "rowSecurity" : "documentSecurity";
|
250
|
+
const securityDescription = useTables ? "Enable row-level permissions" : "Enable document-level permissions";
|
251
|
+
const itemType = useTables ? "row" : "document";
|
252
|
+
|
253
|
+
const schema = {
|
254
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
255
|
+
"$id": `https://appwrite-utils.dev/schemas/${schemaFileName}`,
|
256
|
+
"title": `Appwrite ${containerType} Definition`,
|
257
|
+
"description": `Schema for defining Appwrite ${useTables ? 'tables' : 'collections'} in YAML${useTables ? ' (TablesDB API)' : ''}`,
|
258
|
+
"type": "object",
|
259
|
+
"properties": {
|
260
|
+
"name": {
|
261
|
+
"type": "string",
|
262
|
+
"description": `The name of the ${useTables ? 'table' : 'collection'}`
|
263
|
+
},
|
264
|
+
"id": {
|
265
|
+
"type": "string",
|
266
|
+
"description": `The ID of the ${useTables ? 'table' : 'collection'} (optional, auto-generated if not provided)`,
|
267
|
+
"pattern": "^[a-zA-Z0-9][a-zA-Z0-9._-]{0,35}$"
|
268
|
+
},
|
269
|
+
[securityField]: {
|
270
|
+
"type": "boolean",
|
271
|
+
"default": false,
|
272
|
+
"description": securityDescription
|
273
|
+
},
|
274
|
+
"enabled": {
|
275
|
+
"type": "boolean",
|
276
|
+
"default": true,
|
277
|
+
"description": `Whether the ${useTables ? 'table' : 'collection'} is enabled`
|
278
|
+
},
|
279
|
+
"permissions": {
|
280
|
+
"type": "array",
|
281
|
+
"description": `${containerType}-level permissions`,
|
282
|
+
"items": {
|
283
|
+
"type": "object",
|
284
|
+
"properties": {
|
285
|
+
"permission": {
|
286
|
+
"type": "string",
|
287
|
+
"enum": ["read", "create", "update", "delete"],
|
288
|
+
"description": "The permission type"
|
289
|
+
},
|
290
|
+
"target": {
|
291
|
+
"type": "string",
|
292
|
+
"description": "Permission target (e.g., 'any', 'users', 'users/verified', 'label:admin')"
|
293
|
+
}
|
294
|
+
},
|
295
|
+
"required": ["permission", "target"],
|
296
|
+
"additionalProperties": false
|
297
|
+
}
|
298
|
+
},
|
299
|
+
[fieldsName]: {
|
300
|
+
"type": "array",
|
301
|
+
"description": fieldsDescription,
|
302
|
+
"items": {
|
303
|
+
"type": "object",
|
304
|
+
"properties": {
|
305
|
+
"key": {
|
306
|
+
"type": "string",
|
307
|
+
"description": `${useTables ? 'Column' : 'Attribute'} name`,
|
308
|
+
"pattern": "^[a-zA-Z][a-zA-Z0-9]*$"
|
309
|
+
},
|
310
|
+
"type": {
|
311
|
+
"type": "string",
|
312
|
+
"enum": ["string", "integer", "double", "boolean", "datetime", "email", "ip", "url", "enum", "relationship"],
|
313
|
+
"description": `${useTables ? 'Column' : 'Attribute'} data type`
|
314
|
+
},
|
315
|
+
"size": {
|
316
|
+
"type": "number",
|
317
|
+
"description": `Maximum size for string ${useTables ? 'columns' : 'attributes'}`,
|
318
|
+
"minimum": 1,
|
319
|
+
"maximum": 1073741824
|
320
|
+
},
|
321
|
+
"required": {
|
322
|
+
"type": "boolean",
|
323
|
+
"default": false,
|
324
|
+
"description": `Whether the ${useTables ? 'column' : 'attribute'} is required`
|
325
|
+
},
|
326
|
+
"array": {
|
327
|
+
"type": "boolean",
|
328
|
+
"default": false,
|
329
|
+
"description": `Whether the ${useTables ? 'column' : 'attribute'} is an array`
|
330
|
+
},
|
331
|
+
...(useTables ? {
|
332
|
+
"unique": {
|
333
|
+
"type": "boolean",
|
334
|
+
"default": false,
|
335
|
+
"description": "Whether the column values must be unique (TablesDB feature)"
|
336
|
+
}
|
337
|
+
} : {}),
|
338
|
+
"default": {
|
339
|
+
"description": `Default value for the ${useTables ? 'column' : 'attribute'}`
|
340
|
+
},
|
341
|
+
"description": {
|
342
|
+
"type": "string",
|
343
|
+
"description": `${useTables ? 'Column' : 'Attribute'} description`
|
344
|
+
},
|
345
|
+
"min": {
|
346
|
+
"type": "number",
|
347
|
+
"description": `Minimum value for numeric ${useTables ? 'columns' : 'attributes'}`
|
348
|
+
},
|
349
|
+
"max": {
|
350
|
+
"type": "number",
|
351
|
+
"description": `Maximum value for numeric ${useTables ? 'columns' : 'attributes'}`
|
352
|
+
},
|
353
|
+
"elements": {
|
354
|
+
"type": "array",
|
355
|
+
"items": {
|
356
|
+
"type": "string"
|
357
|
+
},
|
358
|
+
"description": `Allowed values for enum ${useTables ? 'columns' : 'attributes'}`
|
359
|
+
},
|
360
|
+
"relatedCollection": {
|
361
|
+
"type": "string",
|
362
|
+
"description": `Related ${useTables ? 'table' : 'collection'} name for relationship ${useTables ? 'columns' : 'attributes'}`
|
363
|
+
},
|
364
|
+
"relationType": {
|
365
|
+
"type": "string",
|
366
|
+
"enum": ["oneToOne", "oneToMany", "manyToOne", "manyToMany"],
|
367
|
+
"description": "Type of relationship"
|
368
|
+
},
|
369
|
+
"twoWay": {
|
370
|
+
"type": "boolean",
|
371
|
+
"description": "Whether the relationship is bidirectional"
|
372
|
+
},
|
373
|
+
"twoWayKey": {
|
374
|
+
"type": "string",
|
375
|
+
"description": "Key name for the reverse relationship"
|
376
|
+
},
|
377
|
+
"onDelete": {
|
378
|
+
"type": "string",
|
379
|
+
"enum": ["cascade", "restrict", "setNull"],
|
380
|
+
"description": `Action to take when related ${itemType} is deleted`
|
381
|
+
},
|
382
|
+
"side": {
|
383
|
+
"type": "string",
|
384
|
+
"enum": ["parent", "child"],
|
385
|
+
"description": "Side of the relationship"
|
386
|
+
}
|
387
|
+
},
|
388
|
+
"required": ["key", "type"],
|
389
|
+
"additionalProperties": false,
|
390
|
+
"allOf": [
|
391
|
+
{
|
392
|
+
"if": {
|
393
|
+
"properties": { "type": { "const": "enum" } }
|
394
|
+
},
|
395
|
+
"then": {
|
396
|
+
"required": ["elements"]
|
397
|
+
}
|
398
|
+
},
|
399
|
+
{
|
400
|
+
"if": {
|
401
|
+
"properties": { "type": { "const": "relationship" } }
|
402
|
+
},
|
403
|
+
"then": {
|
404
|
+
"required": ["relatedCollection", "relationType"]
|
405
|
+
}
|
406
|
+
}
|
407
|
+
]
|
408
|
+
}
|
409
|
+
},
|
410
|
+
"indexes": {
|
411
|
+
"type": "array",
|
412
|
+
"description": `Database indexes for the ${useTables ? 'table' : 'collection'}`,
|
413
|
+
"items": {
|
414
|
+
"type": "object",
|
415
|
+
"properties": {
|
416
|
+
"key": {
|
417
|
+
"type": "string",
|
418
|
+
"description": "Index name"
|
419
|
+
},
|
420
|
+
"type": {
|
421
|
+
"type": "string",
|
422
|
+
"enum": ["key", "fulltext", "unique"],
|
423
|
+
"description": "Index type"
|
424
|
+
},
|
425
|
+
"attributes": {
|
426
|
+
"type": "array",
|
427
|
+
"items": {
|
428
|
+
"type": "string"
|
429
|
+
},
|
430
|
+
"description": `${useTables ? 'Columns' : 'Attributes'} to index`,
|
431
|
+
"minItems": 1
|
432
|
+
},
|
433
|
+
"orders": {
|
434
|
+
"type": "array",
|
435
|
+
"items": {
|
436
|
+
"type": "string",
|
437
|
+
"enum": ["ASC", "DESC"]
|
438
|
+
},
|
439
|
+
"description": `Sort order for each ${useTables ? 'column' : 'attribute'}`
|
440
|
+
}
|
441
|
+
},
|
442
|
+
"required": ["key", "type", "attributes"],
|
443
|
+
"additionalProperties": false
|
444
|
+
}
|
445
|
+
},
|
446
|
+
"importDefs": {
|
447
|
+
"type": "array",
|
448
|
+
"description": "Import definitions for data migration",
|
449
|
+
"default": []
|
450
|
+
}
|
451
|
+
},
|
452
|
+
"required": ["name"],
|
453
|
+
"additionalProperties": false
|
454
|
+
};
|
455
|
+
|
456
|
+
const schemaPath = path.join(yamlSchemaFolder, schemaFileName);
|
457
|
+
writeFileSync(schemaPath, JSON.stringify(schema, null, 2));
|
458
|
+
|
459
|
+
return schemaPath;
|
460
|
+
}
|
461
|
+
|
462
|
+
/**
|
463
|
+
* Initialize a new Appwrite project with correct directory structure and terminology
|
464
|
+
*/
|
465
|
+
export async function initProject(
|
466
|
+
basePath?: string,
|
467
|
+
forceApiMode?: 'legacy' | 'tablesdb'
|
468
|
+
): Promise<void> {
|
469
|
+
const projectPath = basePath || process.cwd();
|
470
|
+
|
471
|
+
// Detect API mode
|
472
|
+
const detection = forceApiMode
|
473
|
+
? {
|
474
|
+
apiMode: forceApiMode,
|
475
|
+
useTables: forceApiMode === 'tablesdb',
|
476
|
+
detectionSource: 'forced' as const,
|
477
|
+
}
|
478
|
+
: await detectApiMode(projectPath);
|
479
|
+
|
480
|
+
const { useTables, detectionSource } = detection;
|
481
|
+
const terminology = getTerminologyConfig(useTables);
|
482
|
+
|
483
|
+
// Create directory structure
|
484
|
+
const dirs = createProjectDirectories(projectPath, useTables);
|
485
|
+
|
486
|
+
// Generate YAML config
|
487
|
+
const configPath = path.join(dirs.appwriteFolder, "config.yaml");
|
488
|
+
generateYamlConfigTemplate(configPath);
|
489
|
+
|
490
|
+
// Create example schema file
|
491
|
+
createExampleSchema(dirs.containerFolder, terminology);
|
492
|
+
|
493
|
+
// Create JSON validation schema
|
494
|
+
createYamlValidationSchema(dirs.yamlSchemaFolder, useTables);
|
495
|
+
|
496
|
+
// Success messages
|
497
|
+
const containerType = useTables ? "TablesDB" : "Collections";
|
498
|
+
MessageFormatter.success(
|
499
|
+
`Created YAML config and setup files/directories in .appwrite/ folder.`,
|
500
|
+
{ prefix: "Setup" }
|
501
|
+
);
|
502
|
+
MessageFormatter.info(
|
503
|
+
`Project configured for ${containerType} API (${detectionSource} detection)`,
|
504
|
+
{ prefix: "Setup" }
|
505
|
+
);
|
506
|
+
MessageFormatter.info("You can now configure your project in .appwrite/config.yaml", { prefix: "Setup" });
|
507
|
+
MessageFormatter.info(
|
508
|
+
`${terminology.containerName}s can be defined in .appwrite/${terminology.container}s/ as .yaml files`,
|
509
|
+
{ prefix: "Setup" }
|
510
|
+
);
|
511
|
+
MessageFormatter.info("Schemas will be generated in .appwrite/schemas/", { prefix: "Setup" });
|
512
|
+
MessageFormatter.info("Import data can be placed in .appwrite/importData/", { prefix: "Setup" });
|
513
|
+
|
514
|
+
if (useTables) {
|
515
|
+
MessageFormatter.info(
|
516
|
+
"TablesDB features: unique constraints, enhanced performance, row-level security",
|
517
|
+
{ prefix: "Setup" }
|
518
|
+
);
|
519
|
+
}
|
520
|
+
}
|
521
|
+
|
522
|
+
/**
|
523
|
+
* Create a new collection or table schema file
|
524
|
+
*/
|
525
|
+
export async function createSchema(
|
526
|
+
name: string,
|
527
|
+
basePath?: string,
|
528
|
+
forceApiMode?: 'legacy' | 'tablesdb'
|
529
|
+
): Promise<void> {
|
530
|
+
const projectPath = basePath || process.cwd();
|
531
|
+
|
532
|
+
// Detect API mode
|
533
|
+
const detection = forceApiMode
|
534
|
+
? {
|
535
|
+
apiMode: forceApiMode,
|
536
|
+
useTables: forceApiMode === 'tablesdb',
|
537
|
+
detectionSource: 'forced' as const,
|
538
|
+
}
|
539
|
+
: await detectApiMode(projectPath);
|
540
|
+
|
541
|
+
const { useTables } = detection;
|
542
|
+
const terminology = getTerminologyConfig(useTables);
|
543
|
+
|
544
|
+
// Find or create container directory
|
545
|
+
const appwriteFolder = path.join(projectPath, ".appwrite");
|
546
|
+
const containerFolder = path.join(appwriteFolder, useTables ? "tables" : "collections");
|
547
|
+
|
548
|
+
if (!existsSync(containerFolder)) {
|
549
|
+
mkdirSync(containerFolder, { recursive: true });
|
550
|
+
}
|
551
|
+
|
552
|
+
// Create YAML schema file
|
553
|
+
const yamlSchema = `# yaml-language-server: $schema=../.yaml_schemas/${terminology.schemaRef}
|
554
|
+
# ${terminology.containerName} Definition: ${name}
|
555
|
+
name: ${name}
|
556
|
+
id: ${ulid()}
|
557
|
+
${terminology.security}: false
|
558
|
+
enabled: true
|
559
|
+
permissions:
|
560
|
+
- permission: read
|
561
|
+
target: any
|
562
|
+
- permission: create
|
563
|
+
target: users
|
564
|
+
- permission: update
|
565
|
+
target: users
|
566
|
+
- permission: delete
|
567
|
+
target: users
|
568
|
+
${terminology.fields}:
|
569
|
+
# Add your ${terminology.fields} here
|
570
|
+
# Example:
|
571
|
+
# - key: title
|
572
|
+
# type: string
|
573
|
+
# size: 255
|
574
|
+
# required: true
|
575
|
+
# description: "The title of the item"
|
576
|
+
indexes:
|
577
|
+
# Add your indexes here
|
578
|
+
# Example:
|
579
|
+
# - key: title_search
|
580
|
+
# type: fulltext
|
581
|
+
# attributes:
|
582
|
+
# - title
|
583
|
+
importDefs: []
|
584
|
+
`;
|
585
|
+
|
586
|
+
const schemaPath = path.join(containerFolder, `${name}.yaml`);
|
587
|
+
writeFileSync(schemaPath, yamlSchema);
|
588
|
+
|
589
|
+
MessageFormatter.success(
|
590
|
+
`Created ${terminology.container} schema: ${schemaPath}`,
|
591
|
+
{ prefix: "Setup" }
|
592
|
+
);
|
593
|
+
MessageFormatter.info(
|
594
|
+
`Add your ${terminology.fields} to define the ${terminology.container} structure`,
|
595
|
+
{ prefix: "Setup" }
|
596
|
+
);
|
597
|
+
}
|