appwrite-utils-cli 1.7.9 → 1.8.2
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/CHANGELOG.md +14 -199
- package/README.md +87 -30
- package/dist/adapters/AdapterFactory.js +5 -25
- package/dist/adapters/DatabaseAdapter.d.ts +17 -2
- package/dist/adapters/LegacyAdapter.d.ts +2 -1
- package/dist/adapters/LegacyAdapter.js +212 -16
- package/dist/adapters/TablesDBAdapter.d.ts +2 -12
- package/dist/adapters/TablesDBAdapter.js +261 -57
- package/dist/cli/commands/databaseCommands.js +4 -3
- package/dist/cli/commands/functionCommands.js +17 -8
- package/dist/collections/attributes.js +447 -125
- package/dist/collections/methods.js +197 -186
- package/dist/collections/tableOperations.d.ts +86 -0
- package/dist/collections/tableOperations.js +434 -0
- package/dist/collections/transferOperations.d.ts +3 -2
- package/dist/collections/transferOperations.js +93 -12
- package/dist/config/yamlConfig.d.ts +221 -88
- package/dist/examples/yamlTerminologyExample.d.ts +1 -1
- package/dist/examples/yamlTerminologyExample.js +6 -3
- package/dist/functions/fnConfigDiscovery.d.ts +3 -0
- package/dist/functions/fnConfigDiscovery.js +108 -0
- package/dist/interactiveCLI.js +18 -15
- package/dist/main.js +211 -73
- package/dist/migrations/appwriteToX.d.ts +88 -23
- package/dist/migrations/comprehensiveTransfer.d.ts +2 -0
- package/dist/migrations/comprehensiveTransfer.js +83 -6
- package/dist/migrations/dataLoader.d.ts +227 -69
- package/dist/migrations/dataLoader.js +3 -3
- package/dist/migrations/importController.js +3 -3
- package/dist/migrations/relationships.d.ts +8 -2
- package/dist/migrations/services/ImportOrchestrator.js +3 -3
- package/dist/migrations/transfer.js +159 -37
- package/dist/shared/attributeMapper.d.ts +20 -0
- package/dist/shared/attributeMapper.js +203 -0
- package/dist/shared/selectionDialogs.js +8 -4
- package/dist/storage/schemas.d.ts +354 -92
- package/dist/utils/configDiscovery.js +4 -3
- package/dist/utils/versionDetection.d.ts +0 -4
- package/dist/utils/versionDetection.js +41 -173
- package/dist/utils/yamlConverter.js +89 -16
- package/dist/utils/yamlLoader.d.ts +1 -1
- package/dist/utils/yamlLoader.js +6 -2
- package/dist/utilsController.js +56 -19
- package/package.json +4 -4
- package/src/adapters/AdapterFactory.ts +119 -143
- package/src/adapters/DatabaseAdapter.ts +18 -3
- package/src/adapters/LegacyAdapter.ts +236 -105
- package/src/adapters/TablesDBAdapter.ts +773 -643
- package/src/cli/commands/databaseCommands.ts +13 -12
- package/src/cli/commands/functionCommands.ts +23 -14
- package/src/collections/attributes.ts +2054 -1611
- package/src/collections/methods.ts +208 -293
- package/src/collections/tableOperations.ts +506 -0
- package/src/collections/transferOperations.ts +218 -144
- package/src/examples/yamlTerminologyExample.ts +10 -5
- package/src/functions/fnConfigDiscovery.ts +103 -0
- package/src/interactiveCLI.ts +25 -20
- package/src/main.ts +549 -194
- package/src/migrations/comprehensiveTransfer.ts +126 -50
- package/src/migrations/dataLoader.ts +3 -3
- package/src/migrations/importController.ts +3 -3
- package/src/migrations/services/ImportOrchestrator.ts +3 -3
- package/src/migrations/transfer.ts +148 -131
- package/src/shared/attributeMapper.ts +229 -0
- package/src/shared/selectionDialogs.ts +29 -25
- package/src/utils/configDiscovery.ts +9 -3
- package/src/utils/versionDetection.ts +74 -228
- package/src/utils/yamlConverter.ts +94 -17
- package/src/utils/yamlLoader.ts +11 -4
- package/src/utilsController.ts +80 -30
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import type { CreateAttributeParams, UpdateAttributeParams } from "../adapters/DatabaseAdapter.js";
|
|
2
|
+
import type { Attribute } from "appwrite-utils";
|
|
3
|
+
|
|
4
|
+
function ensureNumber(n: any): number | undefined {
|
|
5
|
+
if (n === null || n === undefined) return undefined;
|
|
6
|
+
const num = Number(n);
|
|
7
|
+
return Number.isFinite(num) ? num : undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Map a schema Attribute into DatabaseAdapter CreateAttributeParams
|
|
12
|
+
* Only includes fields valid for the specific type to satisfy TS unions.
|
|
13
|
+
* Also normalizes min/max ordering for numeric types to avoid server errors.
|
|
14
|
+
*/
|
|
15
|
+
export function mapToCreateAttributeParams(
|
|
16
|
+
attr: Attribute,
|
|
17
|
+
base: { databaseId: string; tableId: string }
|
|
18
|
+
): CreateAttributeParams {
|
|
19
|
+
const type = String((attr as any).type || "").toLowerCase();
|
|
20
|
+
const required = !!(attr as any).required;
|
|
21
|
+
const array = !!(attr as any).array;
|
|
22
|
+
const xdefault = (attr as any).xdefault;
|
|
23
|
+
const encrypt = (attr as any).encrypted ?? (attr as any).encrypt;
|
|
24
|
+
|
|
25
|
+
// Numeric helpers
|
|
26
|
+
const rawMin = ensureNumber((attr as any).min);
|
|
27
|
+
const rawMax = ensureNumber((attr as any).max);
|
|
28
|
+
let min = rawMin;
|
|
29
|
+
let max = rawMax;
|
|
30
|
+
if (min !== undefined && max !== undefined && min >= max) {
|
|
31
|
+
// Swap to satisfy server-side validation
|
|
32
|
+
const tmp = min;
|
|
33
|
+
min = Math.min(min, max);
|
|
34
|
+
max = Math.max(tmp, max);
|
|
35
|
+
if (min === max) {
|
|
36
|
+
// If still equal, unset max to avoid error
|
|
37
|
+
max = undefined;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
switch (type) {
|
|
42
|
+
case "string":
|
|
43
|
+
return {
|
|
44
|
+
databaseId: base.databaseId,
|
|
45
|
+
tableId: base.tableId,
|
|
46
|
+
key: attr.key,
|
|
47
|
+
type,
|
|
48
|
+
size: (attr as any).size ?? 255,
|
|
49
|
+
required,
|
|
50
|
+
default: xdefault,
|
|
51
|
+
array,
|
|
52
|
+
encrypt: !!encrypt,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
case "integer":
|
|
56
|
+
return {
|
|
57
|
+
databaseId: base.databaseId,
|
|
58
|
+
tableId: base.tableId,
|
|
59
|
+
key: attr.key,
|
|
60
|
+
type,
|
|
61
|
+
required,
|
|
62
|
+
default: xdefault,
|
|
63
|
+
array,
|
|
64
|
+
min,
|
|
65
|
+
max,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
case "double":
|
|
69
|
+
case "float":
|
|
70
|
+
return {
|
|
71
|
+
databaseId: base.databaseId,
|
|
72
|
+
tableId: base.tableId,
|
|
73
|
+
key: attr.key,
|
|
74
|
+
type,
|
|
75
|
+
required,
|
|
76
|
+
default: xdefault,
|
|
77
|
+
array,
|
|
78
|
+
min,
|
|
79
|
+
max,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
case "boolean":
|
|
83
|
+
return {
|
|
84
|
+
databaseId: base.databaseId,
|
|
85
|
+
tableId: base.tableId,
|
|
86
|
+
key: attr.key,
|
|
87
|
+
type,
|
|
88
|
+
required,
|
|
89
|
+
default: xdefault,
|
|
90
|
+
array,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
case "datetime":
|
|
94
|
+
case "email":
|
|
95
|
+
case "ip":
|
|
96
|
+
case "url":
|
|
97
|
+
return {
|
|
98
|
+
databaseId: base.databaseId,
|
|
99
|
+
tableId: base.tableId,
|
|
100
|
+
key: attr.key,
|
|
101
|
+
type,
|
|
102
|
+
required,
|
|
103
|
+
default: xdefault,
|
|
104
|
+
array,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
case "enum":
|
|
108
|
+
{
|
|
109
|
+
const elements = (attr as any).elements;
|
|
110
|
+
if (!Array.isArray(elements) || elements.length === 0) {
|
|
111
|
+
// Creating an enum without elements is invalid – fail fast with clear messaging
|
|
112
|
+
throw new Error(
|
|
113
|
+
`Enum attribute '${(attr as any).key}' requires a non-empty 'elements' array for creation`
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
databaseId: base.databaseId,
|
|
119
|
+
tableId: base.tableId,
|
|
120
|
+
key: attr.key,
|
|
121
|
+
type,
|
|
122
|
+
required,
|
|
123
|
+
default: xdefault,
|
|
124
|
+
array,
|
|
125
|
+
elements: (attr as any).elements,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
case "relationship": {
|
|
129
|
+
// Relationship attributes require related collection and metadata
|
|
130
|
+
return {
|
|
131
|
+
databaseId: base.databaseId,
|
|
132
|
+
tableId: base.tableId,
|
|
133
|
+
key: attr.key,
|
|
134
|
+
type,
|
|
135
|
+
relatedCollection: (attr as any).relatedCollection,
|
|
136
|
+
relationType: (attr as any).relationType,
|
|
137
|
+
twoWay: (attr as any).twoWay,
|
|
138
|
+
twoWayKey: (attr as any).twoWayKey,
|
|
139
|
+
onDelete: (attr as any).onDelete,
|
|
140
|
+
side: (attr as any).side,
|
|
141
|
+
} as unknown as CreateAttributeParams;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
default:
|
|
145
|
+
return {
|
|
146
|
+
databaseId: base.databaseId,
|
|
147
|
+
tableId: base.tableId,
|
|
148
|
+
key: attr.key,
|
|
149
|
+
type,
|
|
150
|
+
required,
|
|
151
|
+
} as CreateAttributeParams;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Map a schema Attribute into DatabaseAdapter UpdateAttributeParams
|
|
157
|
+
* Omits fields that are not explicitly provided, and guards enum updates
|
|
158
|
+
* so we never send an empty elements array (preserve existing on server).
|
|
159
|
+
*/
|
|
160
|
+
export function mapToUpdateAttributeParams(
|
|
161
|
+
attr: Attribute,
|
|
162
|
+
base: { databaseId: string; tableId: string }
|
|
163
|
+
): UpdateAttributeParams {
|
|
164
|
+
const type = String((attr as any).type || "").toLowerCase();
|
|
165
|
+
const params: UpdateAttributeParams = {
|
|
166
|
+
databaseId: base.databaseId,
|
|
167
|
+
tableId: base.tableId,
|
|
168
|
+
key: (attr as any).key,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const setIfDefined = <K extends keyof UpdateAttributeParams>(
|
|
172
|
+
key: K,
|
|
173
|
+
value: UpdateAttributeParams[K]
|
|
174
|
+
) => {
|
|
175
|
+
if (value !== undefined) (params as any)[key] = value;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// Common fields
|
|
179
|
+
setIfDefined("type", type);
|
|
180
|
+
setIfDefined("required", (attr as any).required);
|
|
181
|
+
// Only send default if explicitly provided and not on required
|
|
182
|
+
if (!(attr as any).required && (attr as any).xdefault !== undefined) {
|
|
183
|
+
setIfDefined("default", (attr as any).xdefault as any);
|
|
184
|
+
}
|
|
185
|
+
setIfDefined("array", (attr as any).array);
|
|
186
|
+
// encrypt only applies to string types
|
|
187
|
+
if (type === "string") setIfDefined("encrypt", (attr as any).encrypted ?? (attr as any).encrypt);
|
|
188
|
+
|
|
189
|
+
// Numeric normalization
|
|
190
|
+
const toNum = (n: any) => (n === null || n === undefined ? undefined : (Number(n)));
|
|
191
|
+
let min = toNum((attr as any).min);
|
|
192
|
+
let max = toNum((attr as any).max);
|
|
193
|
+
if (min !== undefined && max !== undefined && min >= max) {
|
|
194
|
+
const tmp = min;
|
|
195
|
+
min = Math.min(min, max);
|
|
196
|
+
max = Math.max(tmp, max);
|
|
197
|
+
if (min === max) max = undefined;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
switch (type) {
|
|
201
|
+
case "string":
|
|
202
|
+
setIfDefined("size", (attr as any).size);
|
|
203
|
+
break;
|
|
204
|
+
case "integer":
|
|
205
|
+
case "float":
|
|
206
|
+
case "double":
|
|
207
|
+
setIfDefined("min", min);
|
|
208
|
+
setIfDefined("max", max);
|
|
209
|
+
break;
|
|
210
|
+
case "enum": {
|
|
211
|
+
const elements = (attr as any).elements;
|
|
212
|
+
if (Array.isArray(elements) && elements.length > 0) {
|
|
213
|
+
// Only include when non-empty; otherwise preserve existing on server
|
|
214
|
+
setIfDefined("elements", elements as string[]);
|
|
215
|
+
}
|
|
216
|
+
break;
|
|
217
|
+
}
|
|
218
|
+
case "relationship": {
|
|
219
|
+
setIfDefined("relatedCollection", (attr as any).relatedCollection);
|
|
220
|
+
setIfDefined("relationType", (attr as any).relationType);
|
|
221
|
+
setIfDefined("twoWay", (attr as any).twoWay);
|
|
222
|
+
setIfDefined("twoWayKey", (attr as any).twoWayKey);
|
|
223
|
+
setIfDefined("onDelete", (attr as any).onDelete);
|
|
224
|
+
break;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return params;
|
|
229
|
+
}
|
|
@@ -244,12 +244,13 @@ export class SelectionDialogs {
|
|
|
244
244
|
return; // Skip configured databases if only allowing new ones
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
-
choices.push({
|
|
248
|
-
name,
|
|
249
|
-
value: database.$id,
|
|
250
|
-
short: database.name,
|
|
251
|
-
|
|
252
|
-
|
|
247
|
+
choices.push({
|
|
248
|
+
name,
|
|
249
|
+
value: database.$id,
|
|
250
|
+
short: database.name,
|
|
251
|
+
// Do not preselect anything unless explicitly provided
|
|
252
|
+
checked: defaultSelected.includes(database.$id)
|
|
253
|
+
});
|
|
253
254
|
});
|
|
254
255
|
|
|
255
256
|
if (choices.length === 0) {
|
|
@@ -327,12 +328,13 @@ export class SelectionDialogs {
|
|
|
327
328
|
return; // Skip configured tables if only allowing new ones
|
|
328
329
|
}
|
|
329
330
|
|
|
330
|
-
choices.push({
|
|
331
|
-
name,
|
|
332
|
-
value: table.$id,
|
|
333
|
-
short: table.name,
|
|
334
|
-
|
|
335
|
-
|
|
331
|
+
choices.push({
|
|
332
|
+
name,
|
|
333
|
+
value: table.$id,
|
|
334
|
+
short: table.name,
|
|
335
|
+
// Do not preselect anything unless explicitly provided
|
|
336
|
+
checked: defaultSelected.includes(table.$id)
|
|
337
|
+
});
|
|
336
338
|
});
|
|
337
339
|
|
|
338
340
|
if (choices.length === 0) {
|
|
@@ -436,12 +438,13 @@ export class SelectionDialogs {
|
|
|
436
438
|
return; // Skip configured buckets if only allowing new ones
|
|
437
439
|
}
|
|
438
440
|
|
|
439
|
-
choices.push({
|
|
440
|
-
name: ` ${name}`,
|
|
441
|
-
value: bucket.$id,
|
|
442
|
-
short: bucket.name,
|
|
443
|
-
|
|
444
|
-
|
|
441
|
+
choices.push({
|
|
442
|
+
name: ` ${name}`,
|
|
443
|
+
value: bucket.$id,
|
|
444
|
+
short: bucket.name,
|
|
445
|
+
// Do not preselect anything unless explicitly provided
|
|
446
|
+
checked: defaultSelected.includes(bucket.$id)
|
|
447
|
+
});
|
|
445
448
|
});
|
|
446
449
|
}
|
|
447
450
|
});
|
|
@@ -480,12 +483,13 @@ export class SelectionDialogs {
|
|
|
480
483
|
return; // Skip configured buckets if only allowing new ones
|
|
481
484
|
}
|
|
482
485
|
|
|
483
|
-
choices.push({
|
|
484
|
-
name,
|
|
485
|
-
value: bucket.$id,
|
|
486
|
-
short: bucket.name,
|
|
487
|
-
|
|
488
|
-
|
|
486
|
+
choices.push({
|
|
487
|
+
name,
|
|
488
|
+
value: bucket.$id,
|
|
489
|
+
short: bucket.name,
|
|
490
|
+
// Do not preselect anything unless explicitly provided
|
|
491
|
+
checked: defaultSelected.includes(bucket.$id)
|
|
492
|
+
});
|
|
489
493
|
});
|
|
490
494
|
}
|
|
491
495
|
|
|
@@ -742,4 +746,4 @@ export class SelectionDialogs {
|
|
|
742
746
|
MessageFormatter.success(message, { skipLogging: true });
|
|
743
747
|
logger.info(`Selection dialog success: ${message}`);
|
|
744
748
|
}
|
|
745
|
-
}
|
|
749
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import path from "path";
|
|
2
2
|
import fs from "fs";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
CollectionCreateSchema,
|
|
5
|
+
type CollectionCreate,
|
|
6
|
+
type Collection
|
|
7
|
+
} from "appwrite-utils";
|
|
4
8
|
import { register } from "tsx/esm/api";
|
|
5
9
|
import { pathToFileURL } from "node:url";
|
|
6
10
|
import yaml from "js-yaml";
|
|
@@ -216,7 +220,7 @@ export const loadYamlCollection = (filePath: string): CollectionCreate | null =>
|
|
|
216
220
|
const parsedCollection = YamlCollectionSchema.parse(yamlData);
|
|
217
221
|
|
|
218
222
|
// Convert YAML collection to CollectionCreate format
|
|
219
|
-
const
|
|
223
|
+
const collectionInput: CollectionCreate = {
|
|
220
224
|
name: parsedCollection.name,
|
|
221
225
|
$id: parsedCollection.id || parsedCollection.name.toLowerCase().replace(/\s+/g, '_'),
|
|
222
226
|
documentSecurity: parsedCollection.documentSecurity,
|
|
@@ -241,7 +245,7 @@ export const loadYamlCollection = (filePath: string): CollectionCreate | null =>
|
|
|
241
245
|
twoWayKey: attr.twoWayKey,
|
|
242
246
|
onDelete: attr.onDelete as any,
|
|
243
247
|
side: attr.side as any,
|
|
244
|
-
|
|
248
|
+
encrypt: (attr as any).encrypt,
|
|
245
249
|
format: (attr as any).format
|
|
246
250
|
})),
|
|
247
251
|
indexes: parsedCollection.indexes.map(idx => ({
|
|
@@ -253,6 +257,8 @@ export const loadYamlCollection = (filePath: string): CollectionCreate | null =>
|
|
|
253
257
|
importDefs: parsedCollection.importDefs && Array.isArray(parsedCollection.importDefs) && parsedCollection.importDefs.length > 0 ? parsedCollection.importDefs : []
|
|
254
258
|
};
|
|
255
259
|
|
|
260
|
+
const collection = CollectionCreateSchema.parse(collectionInput);
|
|
261
|
+
|
|
256
262
|
return collection;
|
|
257
263
|
} catch (error) {
|
|
258
264
|
MessageFormatter.error(`Error loading YAML collection from ${filePath}`, error as Error, { prefix: "Config" });
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
* 3. Fallback: Default to legacy mode for safety
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { logger } from '../shared/logging.js';
|
|
14
|
-
import { MessageFormatter } from '../shared/messageFormatter.js';
|
|
13
|
+
import { logger } from '../shared/logging.js';
|
|
14
|
+
import { MessageFormatter } from '../shared/messageFormatter.js';
|
|
15
|
+
import { Client, Databases, TablesDB, Query } from 'node-appwrite';
|
|
15
16
|
|
|
16
17
|
export type ApiMode = 'legacy' | 'tablesdb';
|
|
17
18
|
|
|
@@ -30,11 +31,11 @@ export interface VersionDetectionResult {
|
|
|
30
31
|
* @param apiKey - API key for authentication
|
|
31
32
|
* @returns Promise resolving to version detection result
|
|
32
33
|
*/
|
|
33
|
-
export async function detectAppwriteVersion(
|
|
34
|
-
endpoint: string,
|
|
35
|
-
project: string,
|
|
36
|
-
apiKey: string
|
|
37
|
-
): Promise<VersionDetectionResult> {
|
|
34
|
+
export async function detectAppwriteVersion(
|
|
35
|
+
endpoint: string,
|
|
36
|
+
project: string,
|
|
37
|
+
apiKey: string
|
|
38
|
+
): Promise<VersionDetectionResult> {
|
|
38
39
|
const startTime = Date.now();
|
|
39
40
|
// Clean endpoint URL
|
|
40
41
|
const cleanEndpoint = endpoint.replace(/\/$/, '');
|
|
@@ -62,199 +63,72 @@ export async function detectAppwriteVersion(
|
|
|
62
63
|
};
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
// STEP 2:
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
logger.
|
|
102
|
-
endpoint: cleanEndpoint,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
// Fallback to legacy mode
|
|
132
|
-
const fallbackResult = {
|
|
133
|
-
apiMode: 'legacy' as ApiMode,
|
|
134
|
-
detectionMethod: 'fallback' as const,
|
|
135
|
-
confidence: 'low' as const
|
|
136
|
-
};
|
|
66
|
+
// STEP 2: If version is unknown, use SDK probe (no fake HTTP endpoints)
|
|
67
|
+
try {
|
|
68
|
+
logger.debug('Attempting SDK-based TablesDB probe', {
|
|
69
|
+
endpoint: cleanEndpoint,
|
|
70
|
+
operation: 'detectAppwriteVersion'
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const client = new Client().setEndpoint(cleanEndpoint).setProject(project);
|
|
74
|
+
if (apiKey && apiKey.trim().length > 0) client.setKey(apiKey);
|
|
75
|
+
|
|
76
|
+
const databases = new Databases(client);
|
|
77
|
+
// Try to get a database id to probe tables listing
|
|
78
|
+
let dbId: string | undefined;
|
|
79
|
+
try {
|
|
80
|
+
const dbList: any = await databases.list([Query.limit(1)]);
|
|
81
|
+
dbId = dbList?.databases?.[0]?.$id || dbList?.databases?.[0]?.id || dbList?.[0]?.$id;
|
|
82
|
+
} catch (e) {
|
|
83
|
+
// Ignore, we'll still attempt a conservative probe
|
|
84
|
+
logger.debug('Databases.list probe failed or returned no items', { operation: 'detectAppwriteVersion' });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const tables = new TablesDB(client);
|
|
88
|
+
if (dbId) {
|
|
89
|
+
// Probe listTables for the first database (limit 1)
|
|
90
|
+
await tables.listTables({ databaseId: dbId, queries: [Query.limit(1)] });
|
|
91
|
+
} else {
|
|
92
|
+
// No databases to probe; assume TablesDB available (cannot falsify-positively without a db)
|
|
93
|
+
logger.debug('No databases found to probe tables; assuming TablesDB if SDK available', { operation: 'detectAppwriteVersion' });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const result: VersionDetectionResult = {
|
|
97
|
+
apiMode: 'tablesdb',
|
|
98
|
+
detectionMethod: 'endpoint_probe', // repurpose label for SDK probe
|
|
99
|
+
confidence: 'medium',
|
|
100
|
+
serverVersion: serverVersion || undefined
|
|
101
|
+
};
|
|
102
|
+
logger.info('TablesDB detected via SDK probe', {
|
|
103
|
+
endpoint: cleanEndpoint,
|
|
104
|
+
result,
|
|
105
|
+
operation: 'detectAppwriteVersion'
|
|
106
|
+
});
|
|
107
|
+
return result;
|
|
108
|
+
} catch (error) {
|
|
109
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
110
|
+
logger.warn('SDK TablesDB probe failed; defaulting conservatively', {
|
|
111
|
+
endpoint: cleanEndpoint,
|
|
112
|
+
error: errorMessage,
|
|
113
|
+
operation: 'detectAppwriteVersion'
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Final fallback: default to tablesdb for modern environments when version unknown
|
|
118
|
+
const fallbackResult: VersionDetectionResult = {
|
|
119
|
+
apiMode: 'tablesdb',
|
|
120
|
+
detectionMethod: 'fallback',
|
|
121
|
+
confidence: 'low',
|
|
122
|
+
serverVersion: serverVersion || undefined
|
|
123
|
+
};
|
|
124
|
+
logger.info('Defaulting to TablesDB mode (fallback)', {
|
|
125
|
+
endpoint: cleanEndpoint,
|
|
126
|
+
result: fallbackResult,
|
|
127
|
+
operation: 'detectAppwriteVersion'
|
|
128
|
+
});
|
|
129
|
+
return fallbackResult;
|
|
130
|
+
}
|
|
137
131
|
|
|
138
|
-
logger.info('Falling back to legacy mode', {
|
|
139
|
-
endpoint: cleanEndpoint,
|
|
140
|
-
totalDuration: Date.now() - startTime,
|
|
141
|
-
result: fallbackResult,
|
|
142
|
-
operation: 'detectAppwriteVersion'
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
return fallbackResult;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Test TablesDB endpoint availability - most reliable detection method
|
|
150
|
-
*/
|
|
151
|
-
async function probeTablesDbEndpoint(
|
|
152
|
-
endpoint: string,
|
|
153
|
-
project: string,
|
|
154
|
-
apiKey: string
|
|
155
|
-
): Promise<VersionDetectionResult> {
|
|
156
|
-
const startTime = Date.now();
|
|
157
|
-
const url = `${endpoint}/tablesdb/`;
|
|
158
|
-
|
|
159
|
-
logger.debug('Probing TablesDB endpoint', {
|
|
160
|
-
url,
|
|
161
|
-
project,
|
|
162
|
-
operation: 'probeTablesDbEndpoint'
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const response = await fetch(url, {
|
|
166
|
-
method: 'GET',
|
|
167
|
-
headers: {
|
|
168
|
-
'Content-Type': 'application/json',
|
|
169
|
-
'X-Appwrite-Project': project,
|
|
170
|
-
'X-Appwrite-Key': apiKey
|
|
171
|
-
},
|
|
172
|
-
// Short timeout for faster detection
|
|
173
|
-
signal: AbortSignal.timeout(5000)
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
const duration = Date.now() - startTime;
|
|
177
|
-
|
|
178
|
-
logger.debug('TablesDB endpoint response received', {
|
|
179
|
-
url,
|
|
180
|
-
status: response.status,
|
|
181
|
-
statusText: response.statusText,
|
|
182
|
-
duration,
|
|
183
|
-
operation: 'probeTablesDbEndpoint'
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
if (response.ok) {
|
|
187
|
-
// ONLY 200 OK means TablesDB available
|
|
188
|
-
// 404 means endpoint doesn't exist (server < 1.8.0)
|
|
189
|
-
const result = {
|
|
190
|
-
apiMode: 'tablesdb' as ApiMode,
|
|
191
|
-
detectionMethod: 'endpoint_probe' as const,
|
|
192
|
-
confidence: 'high' as const
|
|
193
|
-
};
|
|
194
|
-
|
|
195
|
-
logger.info('TablesDB endpoint probe successful', {
|
|
196
|
-
url,
|
|
197
|
-
status: response.status,
|
|
198
|
-
result,
|
|
199
|
-
duration,
|
|
200
|
-
operation: 'probeTablesDbEndpoint'
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
return result;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// 501 Not Implemented or other errors = no TablesDB support
|
|
207
|
-
const error = new Error(`TablesDB endpoint returned ${response.status}: ${response.statusText}`);
|
|
208
|
-
logger.debug('TablesDB endpoint probe failed', {
|
|
209
|
-
url,
|
|
210
|
-
status: response.status,
|
|
211
|
-
statusText: response.statusText,
|
|
212
|
-
duration,
|
|
213
|
-
operation: 'probeTablesDbEndpoint'
|
|
214
|
-
});
|
|
215
|
-
throw error;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/**
|
|
219
|
-
* SDK capability detection as secondary method
|
|
220
|
-
*/
|
|
221
|
-
async function probeSdkCapabilities(): Promise<VersionDetectionResult> {
|
|
222
|
-
try {
|
|
223
|
-
// Try to import TablesDB SDK
|
|
224
|
-
let TablesDBModule;
|
|
225
|
-
try {
|
|
226
|
-
TablesDBModule = await import('node-appwrite-tablesdb');
|
|
227
|
-
} catch (importError) {
|
|
228
|
-
// TablesDB SDK not available, will fall back to legacy
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (TablesDBModule?.TablesDB) {
|
|
232
|
-
return {
|
|
233
|
-
apiMode: 'tablesdb',
|
|
234
|
-
detectionMethod: 'endpoint_probe',
|
|
235
|
-
confidence: 'medium'
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
} catch (error) {
|
|
239
|
-
// TablesDB SDK not available, assume legacy
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Check for legacy SDK availability
|
|
243
|
-
try {
|
|
244
|
-
const { Databases } = await import('node-appwrite');
|
|
245
|
-
if (Databases) {
|
|
246
|
-
return {
|
|
247
|
-
apiMode: 'legacy',
|
|
248
|
-
detectionMethod: 'endpoint_probe',
|
|
249
|
-
confidence: 'medium'
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
} catch (error) {
|
|
253
|
-
throw new Error('No Appwrite SDK available');
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
throw new Error('Unable to determine SDK capabilities');
|
|
257
|
-
}
|
|
258
132
|
|
|
259
133
|
/**
|
|
260
134
|
* Cached version detection to avoid repeated API calls
|
|
@@ -381,35 +255,7 @@ export function isCloudAppwriteEndpoint(endpoint: string): boolean {
|
|
|
381
255
|
* SDK feature detection as a fallback method
|
|
382
256
|
* Attempts to dynamically import TablesDB to check availability
|
|
383
257
|
*/
|
|
384
|
-
|
|
385
|
-
tablesDbAvailable: boolean;
|
|
386
|
-
legacyAvailable: boolean;
|
|
387
|
-
}> {
|
|
388
|
-
const result = {
|
|
389
|
-
tablesDbAvailable: false,
|
|
390
|
-
legacyAvailable: false
|
|
391
|
-
};
|
|
392
|
-
|
|
393
|
-
// Test TablesDB SDK availability
|
|
394
|
-
try {
|
|
395
|
-
const tablesModule = await import('node-appwrite-tablesdb');
|
|
396
|
-
if (tablesModule) {
|
|
397
|
-
result.tablesDbAvailable = true;
|
|
398
|
-
}
|
|
399
|
-
} catch (error) {
|
|
400
|
-
// TablesDB SDK not available
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// Test legacy SDK availability
|
|
404
|
-
try {
|
|
405
|
-
await import('node-appwrite');
|
|
406
|
-
result.legacyAvailable = true;
|
|
407
|
-
} catch (error) {
|
|
408
|
-
// Legacy SDK not available
|
|
409
|
-
}
|
|
410
|
-
|
|
411
|
-
return result;
|
|
412
|
-
}
|
|
258
|
+
// Removed dynamic SDK capability checks to avoid confusion and side effects.
|
|
413
259
|
|
|
414
260
|
/**
|
|
415
261
|
* Clear version detection cache (useful for testing)
|