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.
@@ -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
+ }