appwrite-utils-cli 1.4.1 → 1.5.0
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/README.md +22 -1
- package/dist/adapters/TablesDBAdapter.js +7 -4
- package/dist/collections/attributes.d.ts +1 -1
- package/dist/collections/attributes.js +24 -6
- package/dist/collections/indexes.js +13 -3
- package/dist/collections/methods.d.ts +9 -0
- package/dist/collections/methods.js +268 -0
- package/dist/migrations/appwriteToX.d.ts +2 -2
- package/dist/migrations/comprehensiveTransfer.js +12 -0
- package/dist/migrations/dataLoader.d.ts +5 -5
- package/dist/migrations/relationships.d.ts +2 -2
- package/dist/shared/jsonSchemaGenerator.d.ts +1 -0
- package/dist/shared/jsonSchemaGenerator.js +6 -2
- package/dist/shared/operationQueue.js +14 -1
- package/dist/shared/schemaGenerator.d.ts +2 -1
- package/dist/shared/schemaGenerator.js +61 -78
- package/dist/storage/schemas.d.ts +8 -8
- package/dist/utils/loadConfigs.js +44 -19
- package/dist/utils/schemaStrings.d.ts +2 -1
- package/dist/utils/schemaStrings.js +61 -78
- package/dist/utils/setupFiles.js +19 -1
- package/dist/utils/versionDetection.d.ts +6 -0
- package/dist/utils/versionDetection.js +30 -0
- package/dist/utilsController.js +28 -4
- package/package.json +1 -1
- package/src/adapters/TablesDBAdapter.ts +20 -17
- package/src/collections/attributes.ts +122 -99
- package/src/collections/indexes.ts +36 -28
- package/src/collections/methods.ts +292 -19
- package/src/migrations/comprehensiveTransfer.ts +22 -8
- package/src/shared/jsonSchemaGenerator.ts +36 -29
- package/src/shared/operationQueue.ts +48 -33
- package/src/shared/schemaGenerator.ts +128 -134
- package/src/utils/loadConfigs.ts +48 -29
- package/src/utils/schemaStrings.ts +124 -130
- package/src/utils/setupFiles.ts +21 -5
- package/src/utils/versionDetection.ts +48 -21
- package/src/utilsController.ts +44 -24
@@ -16,6 +16,10 @@ export class SchemaGenerator {
|
|
16
16
|
this.appwriteFolderPath = appwriteFolderPath;
|
17
17
|
this.extractRelationships();
|
18
18
|
}
|
19
|
+
resolveCollectionName = (idOrName) => {
|
20
|
+
const col = this.config.collections?.find((c) => c.$id === idOrName || c.name === idOrName);
|
21
|
+
return col?.name ?? idOrName;
|
22
|
+
};
|
19
23
|
updateYamlCollections() {
|
20
24
|
const collections = this.config.collections;
|
21
25
|
delete this.config.collections;
|
@@ -265,7 +269,7 @@ export default appwriteConfig;
|
|
265
269
|
default:
|
266
270
|
break;
|
267
271
|
}
|
268
|
-
this.addRelationship(collection.name, relationshipAttr.relatedCollection, attr.key, relationshipAttr.twoWayKey, isArrayParent, isArrayChild);
|
272
|
+
this.addRelationship(collection.name, this.resolveCollectionName(relationshipAttr.relatedCollection), attr.key, relationshipAttr.twoWayKey, isArrayParent, isArrayChild);
|
269
273
|
console.log(`Extracted relationship: ${attr.key}\n\t${collection.name} -> ${relationshipAttr.relatedCollection}, databaseId: ${collection.databaseId}`);
|
270
274
|
}
|
271
275
|
});
|
@@ -307,7 +311,7 @@ export default appwriteConfig;
|
|
307
311
|
// Generate Zod schemas (TypeScript)
|
308
312
|
if (format === "zod" || format === "both") {
|
309
313
|
this.config.collections.forEach((collection) => {
|
310
|
-
const schemaString = this.
|
314
|
+
const schemaString = this.createSchemaStringV4(collection.name, collection.attributes || []);
|
311
315
|
const camelCaseName = toCamelCase(collection.name);
|
312
316
|
const schemaPath = path.join(schemasPath, `${camelCaseName}.ts`);
|
313
317
|
fs.writeFileSync(schemaPath, schemaString, { encoding: "utf-8" });
|
@@ -329,12 +333,13 @@ export default appwriteConfig;
|
|
329
333
|
console.log(`✓ Schema generation completed (format: ${format})`);
|
330
334
|
}
|
331
335
|
}
|
332
|
-
|
336
|
+
// Zod v4 recursive getter-based schemas
|
337
|
+
createSchemaStringV4 = (name, attributes) => {
|
333
338
|
const pascalName = toPascalCase(name);
|
334
339
|
let imports = `import { z } from "zod";\n`;
|
335
340
|
// Use the relationshipMap to find related collections
|
336
341
|
const relationshipDetails = this.relationshipMap.get(name) || [];
|
337
|
-
|
342
|
+
let relatedCollections = relationshipDetails
|
338
343
|
.filter((detail, index, self) => {
|
339
344
|
const uniqueKey = `${detail.parentCollection}-${detail.childCollection}-${detail.parentKey}-${detail.childKey}`;
|
340
345
|
return (index ===
|
@@ -349,86 +354,64 @@ export default appwriteConfig;
|
|
349
354
|
const isArray = detail.isArray ? "array" : "";
|
350
355
|
return [relatedCollectionName, key, isArray];
|
351
356
|
});
|
352
|
-
//
|
353
|
-
const
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
schemaString += ` $permissions: z.array(z.string()),\n`;
|
362
|
-
for (const attribute of attributes) {
|
363
|
-
if (attribute.type === "relationship") {
|
364
|
-
continue;
|
365
|
-
}
|
366
|
-
schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
|
357
|
+
// Include one-way relationship attributes directly (no twoWayKey)
|
358
|
+
const oneWayRels = [];
|
359
|
+
for (const attr of attributes) {
|
360
|
+
if (attr.type === "relationship" && attr.relatedCollection) {
|
361
|
+
const relatedName = this.resolveCollectionName(attr.relatedCollection);
|
362
|
+
const isArray = attr.relationType === "oneToMany" || attr.relationType === "manyToMany"
|
363
|
+
? "array"
|
364
|
+
: "";
|
365
|
+
oneWayRels.push([relatedName, attr.key, isArray]);
|
367
366
|
}
|
368
|
-
schemaString += `});\n\n`;
|
369
|
-
schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
|
370
367
|
}
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
let relatedCamelName = toCamelCase(relatedCollection[0]);
|
381
|
-
curNum++;
|
382
|
-
let endNameTypes = relatedPascalName;
|
383
|
-
let endNameLazy = `${relatedPascalName}Schema`;
|
384
|
-
if (relatedCollection[2] === "array") {
|
385
|
-
endNameTypes += "[]";
|
386
|
-
endNameLazy += ".array().default([])";
|
387
|
-
}
|
388
|
-
else if (!(relatedCollection[2] === "array")) {
|
389
|
-
endNameTypes += " | null";
|
390
|
-
endNameLazy += ".nullish()";
|
391
|
-
}
|
392
|
-
imports += `import { ${relatedPascalName}Schema, type ${relatedPascalName} } from "./${relatedCamelName}";\n`;
|
393
|
-
relatedTypes += `${relatedCollection[1]}?: ${endNameTypes};\n`;
|
394
|
-
if (relatedTypes.length > 0 && curNum !== maxNum) {
|
395
|
-
relatedTypes += " ";
|
396
|
-
}
|
397
|
-
relatedTypesLazy += `${relatedCollection[1]}: z.lazy(() => ${endNameLazy}),\n`;
|
398
|
-
if (relatedTypesLazy.length > 0 && curNum !== maxNum) {
|
399
|
-
relatedTypesLazy += " ";
|
400
|
-
}
|
368
|
+
// Merge and dedupe (by relatedName+key)
|
369
|
+
relatedCollections = [...relatedCollections, ...oneWayRels].filter((item, idx, self) => idx === self.findIndex((o) => `${o[0]}::${o[1]}` === `${item[0]}::${item[1]}`));
|
370
|
+
const hasRelationships = relatedCollections.length > 0;
|
371
|
+
// Build imports for related collections
|
372
|
+
if (hasRelationships) {
|
373
|
+
const importLines = relatedCollections.map((rel) => {
|
374
|
+
const relatedPascalName = toPascalCase(rel[0]);
|
375
|
+
const relatedCamelName = toCamelCase(rel[0]);
|
376
|
+
return `import { ${relatedPascalName}Schema } from "./${relatedCamelName}";`;
|
401
377
|
});
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
schemaString += `})
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
378
|
+
const unique = Array.from(new Set(importLines));
|
379
|
+
imports += unique.join("\n") + (unique.length ? "\n" : "");
|
380
|
+
}
|
381
|
+
let schemaString = `${imports}\n`;
|
382
|
+
// Single object schema with recursive getters (Zod v4)
|
383
|
+
schemaString += `export const ${pascalName}Schema = z.object({\n`;
|
384
|
+
schemaString += ` $id: z.string(),\n`;
|
385
|
+
schemaString += ` $createdAt: z.string(),\n`;
|
386
|
+
schemaString += ` $updatedAt: z.string(),\n`;
|
387
|
+
schemaString += ` $permissions: z.array(z.string()),\n`;
|
388
|
+
for (const attribute of attributes) {
|
389
|
+
if (attribute.type === "relationship")
|
390
|
+
continue;
|
391
|
+
schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
|
392
|
+
}
|
393
|
+
// Add recursive getters for relationships (respect required flag)
|
394
|
+
relatedCollections.forEach((rel) => {
|
395
|
+
const relatedPascalName = toPascalCase(rel[0]);
|
396
|
+
const isArray = rel[2] === "array";
|
397
|
+
const key = String(rel[1]);
|
398
|
+
const attrMeta = attributes.find(a => a.key === key && a.type === "relationship");
|
399
|
+
const isRequired = !!attrMeta?.required;
|
400
|
+
let getterBody = "";
|
401
|
+
if (isArray) {
|
402
|
+
getterBody = isRequired
|
403
|
+
? `${relatedPascalName}Schema.array()`
|
404
|
+
: `${relatedPascalName}Schema.array().nullish()`;
|
426
405
|
}
|
427
406
|
else {
|
428
|
-
|
407
|
+
getterBody = isRequired
|
408
|
+
? `${relatedPascalName}Schema`
|
409
|
+
: `${relatedPascalName}Schema.nullish()`;
|
429
410
|
}
|
430
|
-
schemaString += `
|
431
|
-
}
|
411
|
+
schemaString += ` get ${key}(){\n return ${getterBody}\n },\n`;
|
412
|
+
});
|
413
|
+
schemaString += `});\n\n`;
|
414
|
+
schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
|
432
415
|
return schemaString;
|
433
416
|
};
|
434
417
|
typeToZod = (attribute) => {
|
@@ -162,12 +162,12 @@ export declare const getMigrationCollectionSchemas: () => {
|
|
162
162
|
relatedCollection: string;
|
163
163
|
relationType: "oneToMany" | "manyToOne" | "oneToOne" | "manyToMany";
|
164
164
|
twoWay: boolean;
|
165
|
-
twoWayKey: string;
|
166
165
|
onDelete: "setNull" | "cascade" | "restrict";
|
167
|
-
side: "parent" | "child";
|
168
166
|
error?: string | undefined;
|
169
167
|
required?: boolean | undefined;
|
170
168
|
array?: boolean | undefined;
|
169
|
+
twoWayKey?: string | undefined;
|
170
|
+
side?: "parent" | "child" | undefined;
|
171
171
|
importMapping?: {
|
172
172
|
originalIdField: string;
|
173
173
|
targetField?: string | undefined;
|
@@ -313,12 +313,12 @@ export declare const getMigrationCollectionSchemas: () => {
|
|
313
313
|
relatedCollection: string;
|
314
314
|
relationType: "oneToMany" | "manyToOne" | "oneToOne" | "manyToMany";
|
315
315
|
twoWay: boolean;
|
316
|
-
twoWayKey: string;
|
317
316
|
onDelete: "setNull" | "cascade" | "restrict";
|
318
|
-
side: "parent" | "child";
|
319
317
|
error?: string | undefined;
|
320
318
|
required?: boolean | undefined;
|
321
319
|
array?: boolean | undefined;
|
320
|
+
twoWayKey?: string | undefined;
|
321
|
+
side?: "parent" | "child" | undefined;
|
322
322
|
importMapping?: {
|
323
323
|
originalIdField: string;
|
324
324
|
targetField?: string | undefined;
|
@@ -414,12 +414,12 @@ export declare const getMigrationCollectionSchemas: () => {
|
|
414
414
|
relatedCollection: string;
|
415
415
|
relationType: "oneToMany" | "manyToOne" | "oneToOne" | "manyToMany";
|
416
416
|
twoWay: boolean;
|
417
|
-
twoWayKey: string;
|
418
417
|
onDelete: "setNull" | "cascade" | "restrict";
|
419
|
-
side: "parent" | "child";
|
420
418
|
error?: string | undefined;
|
421
419
|
required?: boolean | undefined;
|
422
420
|
array?: boolean | undefined;
|
421
|
+
twoWayKey?: string | undefined;
|
422
|
+
side?: "parent" | "child" | undefined;
|
423
423
|
importMapping?: {
|
424
424
|
originalIdField: string;
|
425
425
|
targetField?: string | undefined;
|
@@ -565,12 +565,12 @@ export declare const getMigrationCollectionSchemas: () => {
|
|
565
565
|
relatedCollection: string;
|
566
566
|
relationType: "oneToMany" | "manyToOne" | "oneToOne" | "manyToMany";
|
567
567
|
twoWay: boolean;
|
568
|
-
twoWayKey: string;
|
569
568
|
onDelete: "setNull" | "cascade" | "restrict";
|
570
|
-
side: "parent" | "child";
|
571
569
|
error?: string | undefined;
|
572
570
|
required?: boolean | undefined;
|
573
571
|
array?: boolean | undefined;
|
572
|
+
twoWayKey?: string | undefined;
|
573
|
+
side?: "parent" | "child" | undefined;
|
574
574
|
importMapping?: {
|
575
575
|
originalIdField: string;
|
576
576
|
targetField?: string | undefined;
|
@@ -5,6 +5,7 @@ import { register } from "tsx/esm/api"; // Import the register function
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
6
6
|
import chalk from "chalk";
|
7
7
|
import { findYamlConfig, loadYamlConfig } from "../config/yamlConfig.js";
|
8
|
+
import { detectAppwriteVersionCached, fetchServerVersion, isVersionAtLeast } from "./versionDetection.js";
|
8
9
|
import yaml from "js-yaml";
|
9
10
|
import { z } from "zod";
|
10
11
|
import { MessageFormatter } from "../shared/messageFormatter.js";
|
@@ -149,16 +150,30 @@ export const loadConfigWithPath = async (configDir) => {
|
|
149
150
|
if (!config || !actualConfigPath) {
|
150
151
|
throw new Error("No valid configuration found");
|
151
152
|
}
|
152
|
-
// Determine collections
|
153
|
+
// Determine directory (collections or tables) based on server version / API mode
|
154
|
+
let dirName = "collections";
|
155
|
+
try {
|
156
|
+
const det = await detectAppwriteVersionCached(config.appwriteEndpoint, config.appwriteProject, config.appwriteKey);
|
157
|
+
if (det.apiMode === 'tablesdb' || isVersionAtLeast(det.serverVersion, '1.8.0')) {
|
158
|
+
dirName = 'tables';
|
159
|
+
}
|
160
|
+
else {
|
161
|
+
// Try health version if not provided
|
162
|
+
const ver = await fetchServerVersion(config.appwriteEndpoint);
|
163
|
+
if (isVersionAtLeast(ver || undefined, '1.8.0'))
|
164
|
+
dirName = 'tables';
|
165
|
+
}
|
166
|
+
}
|
167
|
+
catch { }
|
168
|
+
// Determine collections directory based on actual config file location and dirName
|
153
169
|
let collectionsDir;
|
154
170
|
const configFileDir = path.dirname(actualConfigPath);
|
155
|
-
|
156
|
-
if
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
collectionsDir = path.join(configFileDir, "collections");
|
171
|
+
collectionsDir = path.join(configFileDir, dirName);
|
172
|
+
// Fallback if not found
|
173
|
+
if (!fs.existsSync(collectionsDir)) {
|
174
|
+
const fallback = path.join(configFileDir, dirName === 'tables' ? 'collections' : 'tables');
|
175
|
+
if (fs.existsSync(fallback))
|
176
|
+
collectionsDir = fallback;
|
162
177
|
}
|
163
178
|
// Load collections if they exist
|
164
179
|
if (fs.existsSync(collectionsDir)) {
|
@@ -238,22 +253,32 @@ export const loadConfig = async (configDir) => {
|
|
238
253
|
if (!config) {
|
239
254
|
throw new Error("No valid configuration found");
|
240
255
|
}
|
241
|
-
// Determine collections
|
242
|
-
let
|
243
|
-
|
244
|
-
const
|
245
|
-
|
246
|
-
|
247
|
-
collectionsDir = path.join(configFileDir, "collections");
|
256
|
+
// Determine directory (collections or tables) based on server version / API mode
|
257
|
+
let dirName2 = "collections";
|
258
|
+
try {
|
259
|
+
const det = await detectAppwriteVersionCached(config.appwriteEndpoint, config.appwriteProject, config.appwriteKey);
|
260
|
+
if (det.apiMode === 'tablesdb' || isVersionAtLeast(det.serverVersion, '1.8.0')) {
|
261
|
+
dirName2 = 'tables';
|
248
262
|
}
|
249
263
|
else {
|
250
|
-
|
251
|
-
|
264
|
+
const ver = await fetchServerVersion(config.appwriteEndpoint);
|
265
|
+
if (isVersionAtLeast(ver || undefined, '1.8.0'))
|
266
|
+
dirName2 = 'tables';
|
252
267
|
}
|
253
268
|
}
|
269
|
+
catch { }
|
270
|
+
let collectionsDir;
|
271
|
+
if (actualConfigPath) {
|
272
|
+
const configFileDir = path.dirname(actualConfigPath);
|
273
|
+
collectionsDir = path.join(configFileDir, dirName2);
|
274
|
+
}
|
254
275
|
else {
|
255
|
-
|
256
|
-
|
276
|
+
collectionsDir = path.join(configDir, dirName2);
|
277
|
+
}
|
278
|
+
if (!fs.existsSync(collectionsDir)) {
|
279
|
+
const fallback = path.join(path.dirname(actualConfigPath || configDir), dirName2 === 'tables' ? 'collections' : 'tables');
|
280
|
+
if (fs.existsSync(fallback))
|
281
|
+
collectionsDir = fallback;
|
257
282
|
}
|
258
283
|
// Load collections if they exist
|
259
284
|
if (fs.existsSync(collectionsDir)) {
|
@@ -4,10 +4,11 @@ export declare class SchemaGenerator {
|
|
4
4
|
private config;
|
5
5
|
private appwriteFolderPath;
|
6
6
|
constructor(config: AppwriteConfig, appwriteFolderPath: string);
|
7
|
+
private resolveCollectionName;
|
7
8
|
updateTsSchemas(): void;
|
8
9
|
private extractRelationships;
|
9
10
|
private addRelationship;
|
10
11
|
generateSchemas(): void;
|
11
|
-
|
12
|
+
createSchemaStringV4: (name: string, attributes: Attribute[]) => string;
|
12
13
|
typeToZod: (attribute: Attribute) => string;
|
13
14
|
}
|
@@ -15,6 +15,10 @@ export class SchemaGenerator {
|
|
15
15
|
this.appwriteFolderPath = appwriteFolderPath;
|
16
16
|
this.extractRelationships();
|
17
17
|
}
|
18
|
+
resolveCollectionName = (idOrName) => {
|
19
|
+
const col = this.config.collections?.find((c) => c.$id === idOrName || c.name === idOrName);
|
20
|
+
return col?.name ?? idOrName;
|
21
|
+
};
|
18
22
|
updateTsSchemas() {
|
19
23
|
const collections = this.config.collections;
|
20
24
|
const functions = this.config.functions || [];
|
@@ -168,7 +172,7 @@ export class SchemaGenerator {
|
|
168
172
|
default:
|
169
173
|
break;
|
170
174
|
}
|
171
|
-
this.addRelationship(collection.name, relationshipAttr.relatedCollection, attr.key, relationshipAttr.twoWayKey, isArrayParent, isArrayChild);
|
175
|
+
this.addRelationship(collection.name, this.resolveCollectionName(relationshipAttr.relatedCollection), attr.key, relationshipAttr.twoWayKey, isArrayParent, isArrayChild);
|
172
176
|
console.log(`Extracted relationship: ${attr.key}\n\t${collection.name} -> ${relationshipAttr.relatedCollection}, databaseId: ${collection.databaseId}`);
|
173
177
|
}
|
174
178
|
});
|
@@ -201,19 +205,20 @@ export class SchemaGenerator {
|
|
201
205
|
return;
|
202
206
|
}
|
203
207
|
this.config.collections.forEach((collection) => {
|
204
|
-
const schemaString = this.
|
208
|
+
const schemaString = this.createSchemaStringV4(collection.name, collection.attributes);
|
205
209
|
const camelCaseName = toCamelCase(collection.name);
|
206
210
|
const schemaPath = path.join(this.appwriteFolderPath, "schemas", `${camelCaseName}.ts`);
|
207
211
|
fs.writeFileSync(schemaPath, schemaString, { encoding: "utf-8" });
|
208
212
|
console.log(`Schema written to ${schemaPath}`);
|
209
213
|
});
|
210
214
|
}
|
211
|
-
|
215
|
+
// Zod v4 recursive getter-based schemas
|
216
|
+
createSchemaStringV4 = (name, attributes) => {
|
212
217
|
const pascalName = toPascalCase(name);
|
213
218
|
let imports = `import { z } from "zod";\n`;
|
214
219
|
// Use the relationshipMap to find related collections
|
215
220
|
const relationshipDetails = this.relationshipMap.get(name) || [];
|
216
|
-
|
221
|
+
let relatedCollections = relationshipDetails
|
217
222
|
.filter((detail, index, self) => {
|
218
223
|
const uniqueKey = `${detail.parentCollection}-${detail.childCollection}-${detail.parentKey}-${detail.childKey}`;
|
219
224
|
return (index ===
|
@@ -228,86 +233,64 @@ export class SchemaGenerator {
|
|
228
233
|
const isArray = detail.isArray ? "array" : "";
|
229
234
|
return [relatedCollectionName, key, isArray];
|
230
235
|
});
|
231
|
-
//
|
232
|
-
const
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
schemaString += ` $permissions: z.array(z.string()),\n`;
|
241
|
-
for (const attribute of attributes) {
|
242
|
-
if (attribute.type === "relationship") {
|
243
|
-
continue;
|
244
|
-
}
|
245
|
-
schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
|
236
|
+
// Include one-way relationship attributes directly (no twoWayKey)
|
237
|
+
const oneWayRels = [];
|
238
|
+
for (const attr of attributes) {
|
239
|
+
if (attr.type === "relationship" && attr.relatedCollection) {
|
240
|
+
const relatedName = this.resolveCollectionName(attr.relatedCollection);
|
241
|
+
const isArray = attr.relationType === "oneToMany" || attr.relationType === "manyToMany"
|
242
|
+
? "array"
|
243
|
+
: "";
|
244
|
+
oneWayRels.push([relatedName, attr.key, isArray]);
|
246
245
|
}
|
247
|
-
schemaString += `});\n\n`;
|
248
|
-
schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
|
249
246
|
}
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
let relatedCamelName = toCamelCase(relatedCollection[0]);
|
260
|
-
curNum++;
|
261
|
-
let endNameTypes = relatedPascalName;
|
262
|
-
let endNameLazy = `${relatedPascalName}Schema`;
|
263
|
-
if (relatedCollection[2] === "array") {
|
264
|
-
endNameTypes += "[]";
|
265
|
-
endNameLazy += ".array().default([])";
|
266
|
-
}
|
267
|
-
else if (!(relatedCollection[2] === "array")) {
|
268
|
-
endNameTypes += " | null";
|
269
|
-
endNameLazy += ".nullish()";
|
270
|
-
}
|
271
|
-
imports += `import { ${relatedPascalName}Schema, type ${relatedPascalName} } from "./${relatedCamelName}";\n`;
|
272
|
-
relatedTypes += `${relatedCollection[1]}?: ${endNameTypes};\n`;
|
273
|
-
if (relatedTypes.length > 0 && curNum !== maxNum) {
|
274
|
-
relatedTypes += " ";
|
275
|
-
}
|
276
|
-
relatedTypesLazy += `${relatedCollection[1]}: z.lazy(() => ${endNameLazy}),\n`;
|
277
|
-
if (relatedTypesLazy.length > 0 && curNum !== maxNum) {
|
278
|
-
relatedTypesLazy += " ";
|
279
|
-
}
|
247
|
+
// Merge and dedupe (by relatedName+key)
|
248
|
+
relatedCollections = [...relatedCollections, ...oneWayRels].filter((item, idx, self) => idx === self.findIndex((o) => `${o[0]}::${o[1]}` === `${item[0]}::${item[1]}`));
|
249
|
+
const hasRelationships = relatedCollections.length > 0;
|
250
|
+
// Build imports for related collections
|
251
|
+
if (hasRelationships) {
|
252
|
+
const importLines = relatedCollections.map((rel) => {
|
253
|
+
const relatedPascalName = toPascalCase(rel[0]);
|
254
|
+
const relatedCamelName = toCamelCase(rel[0]);
|
255
|
+
return `import { ${relatedPascalName}Schema } from "./${relatedCamelName}";`;
|
280
256
|
});
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
schemaString += `})
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
257
|
+
const unique = Array.from(new Set(importLines));
|
258
|
+
imports += unique.join("\n") + (unique.length ? "\n" : "");
|
259
|
+
}
|
260
|
+
let schemaString = `${imports}\n`;
|
261
|
+
// Single object schema with recursive getters (Zod v4)
|
262
|
+
schemaString += `export const ${pascalName}Schema = z.object({\n`;
|
263
|
+
schemaString += ` $id: z.string(),\n`;
|
264
|
+
schemaString += ` $createdAt: z.string(),\n`;
|
265
|
+
schemaString += ` $updatedAt: z.string(),\n`;
|
266
|
+
schemaString += ` $permissions: z.array(z.string()),\n`;
|
267
|
+
for (const attribute of attributes) {
|
268
|
+
if (attribute.type === "relationship")
|
269
|
+
continue;
|
270
|
+
schemaString += ` ${attribute.key}: ${this.typeToZod(attribute)},\n`;
|
271
|
+
}
|
272
|
+
// Add recursive getters for relationships (respect required flag)
|
273
|
+
relatedCollections.forEach((rel) => {
|
274
|
+
const relatedPascalName = toPascalCase(rel[0]);
|
275
|
+
const isArray = rel[2] === "array";
|
276
|
+
const key = String(rel[1]);
|
277
|
+
const attrMeta = attributes.find(a => a.key === key && a.type === "relationship");
|
278
|
+
const isRequired = !!attrMeta?.required;
|
279
|
+
let getterBody = "";
|
280
|
+
if (isArray) {
|
281
|
+
getterBody = isRequired
|
282
|
+
? `${relatedPascalName}Schema.array()`
|
283
|
+
: `${relatedPascalName}Schema.array().nullish()`;
|
305
284
|
}
|
306
285
|
else {
|
307
|
-
|
286
|
+
getterBody = isRequired
|
287
|
+
? `${relatedPascalName}Schema`
|
288
|
+
: `${relatedPascalName}Schema.nullish()`;
|
308
289
|
}
|
309
|
-
schemaString += `
|
310
|
-
}
|
290
|
+
schemaString += ` get ${key}(){\n return ${getterBody}\n },\n`;
|
291
|
+
});
|
292
|
+
schemaString += `});\n\n`;
|
293
|
+
schemaString += `export type ${pascalName} = z.infer<typeof ${pascalName}Schema>;\n\n`;
|
311
294
|
return schemaString;
|
312
295
|
};
|
313
296
|
typeToZod = (attribute) => {
|
package/dist/utils/setupFiles.js
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
import { mkdirSync, writeFileSync, existsSync } from "node:fs";
|
2
2
|
import path from "node:path";
|
3
3
|
import { findAppwriteConfig } from "./loadConfigs.js";
|
4
|
+
import { loadYamlConfig } from "../config/yamlConfig.js";
|
5
|
+
import { fetchServerVersion, isVersionAtLeast } from "./versionDetection.js";
|
4
6
|
import { findYamlConfig } from "../config/yamlConfig.js";
|
5
7
|
import { ID } from "node-appwrite";
|
6
8
|
import { ulid } from "ulidx";
|
@@ -240,7 +242,23 @@ export const setupDirsFiles = async (example = false, currentDir, useYaml = true
|
|
240
242
|
const appwriteSchemaFolder = path.join(appwriteFolder, "schemas");
|
241
243
|
const appwriteYamlSchemaFolder = path.join(appwriteFolder, ".yaml_schemas");
|
242
244
|
const appwriteDataFolder = path.join(appwriteFolder, "importData");
|
243
|
-
|
245
|
+
// Decide between collections or tables folder
|
246
|
+
let useTables = false;
|
247
|
+
try {
|
248
|
+
// Try reading YAML config if present to detect version
|
249
|
+
const yamlPath = findYamlConfig(basePath);
|
250
|
+
if (yamlPath) {
|
251
|
+
const cfg = await loadYamlConfig(yamlPath);
|
252
|
+
if (cfg) {
|
253
|
+
const ver = await fetchServerVersion(cfg.appwriteEndpoint);
|
254
|
+
if (isVersionAtLeast(ver || undefined, '1.8.0'))
|
255
|
+
useTables = true;
|
256
|
+
}
|
257
|
+
}
|
258
|
+
}
|
259
|
+
catch { }
|
260
|
+
const targetFolderName = useTables ? "tables" : "collections";
|
261
|
+
const collectionsFolder = path.join(appwriteFolder, targetFolderName);
|
244
262
|
// Create directory structure
|
245
263
|
if (!existsSync(appwriteFolder)) {
|
246
264
|
mkdirSync(appwriteFolder, { recursive: true });
|
@@ -54,3 +54,9 @@ export declare function detectSdkSupport(): Promise<{
|
|
54
54
|
* Clear version detection cache (useful for testing)
|
55
55
|
*/
|
56
56
|
export declare function clearVersionDetectionCache(): void;
|
57
|
+
/**
|
58
|
+
* Fetch server version from /health/version (no auth required)
|
59
|
+
*/
|
60
|
+
export declare function fetchServerVersion(endpoint: string): Promise<string | null>;
|
61
|
+
/** Compare semantic versions (basic) */
|
62
|
+
export declare function isVersionAtLeast(current: string | undefined, target: string): boolean;
|
@@ -215,3 +215,33 @@ export async function detectSdkSupport() {
|
|
215
215
|
export function clearVersionDetectionCache() {
|
216
216
|
detectionCache.clear();
|
217
217
|
}
|
218
|
+
/**
|
219
|
+
* Fetch server version from /health/version (no auth required)
|
220
|
+
*/
|
221
|
+
export async function fetchServerVersion(endpoint) {
|
222
|
+
try {
|
223
|
+
const clean = endpoint.replace(/\/$/, '');
|
224
|
+
const res = await fetch(`${clean}/health/version`, { method: 'GET', signal: AbortSignal.timeout(5000) });
|
225
|
+
if (!res.ok)
|
226
|
+
return null;
|
227
|
+
const data = await res.json().catch(() => null);
|
228
|
+
const version = (data && (data.version || data.build || data.release)) ?? null;
|
229
|
+
return typeof version === 'string' ? version : null;
|
230
|
+
}
|
231
|
+
catch {
|
232
|
+
return null;
|
233
|
+
}
|
234
|
+
}
|
235
|
+
/** Compare semantic versions (basic) */
|
236
|
+
export function isVersionAtLeast(current, target) {
|
237
|
+
if (!current)
|
238
|
+
return false;
|
239
|
+
const toNums = (v) => v.split('.').map(n => parseInt(n, 10));
|
240
|
+
const [a1 = 0, a2 = 0, a3 = 0] = toNums(current);
|
241
|
+
const [b1, b2, b3] = toNums(target);
|
242
|
+
if (a1 !== b1)
|
243
|
+
return a1 > b1;
|
244
|
+
if (a2 !== b2)
|
245
|
+
return a2 > b2;
|
246
|
+
return a3 >= b3;
|
247
|
+
}
|