appwrite-utils-cli 1.4.1 → 1.5.1
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 +42 -7
- package/dist/collections/indexes.js +13 -3
- package/dist/collections/methods.d.ts +9 -0
- package/dist/collections/methods.js +268 -0
- package/dist/databases/setup.js +6 -2
- package/dist/interactiveCLI.js +2 -1
- 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 +32 -5
- package/package.json +1 -1
- package/src/adapters/TablesDBAdapter.ts +20 -17
- package/src/collections/attributes.ts +198 -156
- package/src/collections/indexes.ts +36 -28
- package/src/collections/methods.ts +292 -19
- package/src/databases/setup.ts +11 -7
- package/src/interactiveCLI.ts +8 -7
- 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 +59 -32
@@ -31,6 +31,7 @@ export declare class JsonSchemaGenerator {
|
|
31
31
|
private appwriteFolderPath;
|
32
32
|
private relationshipMap;
|
33
33
|
constructor(config: AppwriteConfig, appwriteFolderPath: string);
|
34
|
+
private resolveCollectionName;
|
34
35
|
private extractRelationships;
|
35
36
|
private attributeToJsonSchemaProperty;
|
36
37
|
private getBaseTypeSchema;
|
@@ -11,6 +11,10 @@ export class JsonSchemaGenerator {
|
|
11
11
|
this.appwriteFolderPath = appwriteFolderPath;
|
12
12
|
this.extractRelationships();
|
13
13
|
}
|
14
|
+
resolveCollectionName = (idOrName) => {
|
15
|
+
const col = this.config.collections?.find((c) => c.$id === idOrName || c.name === idOrName);
|
16
|
+
return col?.name ?? idOrName;
|
17
|
+
};
|
14
18
|
extractRelationships() {
|
15
19
|
if (!this.config.collections)
|
16
20
|
return;
|
@@ -22,7 +26,7 @@ export class JsonSchemaGenerator {
|
|
22
26
|
const relationships = this.relationshipMap.get(collection.name) || [];
|
23
27
|
relationships.push({
|
24
28
|
attributeKey: attr.key,
|
25
|
-
relatedCollection: attr.relatedCollection,
|
29
|
+
relatedCollection: this.resolveCollectionName(attr.relatedCollection),
|
26
30
|
relationType: attr.relationType,
|
27
31
|
isArray: attr.relationType === "oneToMany" || attr.relationType === "manyToMany"
|
28
32
|
});
|
@@ -108,7 +112,7 @@ export class JsonSchemaGenerator {
|
|
108
112
|
case "relationship":
|
109
113
|
if (attribute.relatedCollection) {
|
110
114
|
// For relationships, reference the related collection schema
|
111
|
-
schema.$ref = `#/definitions/${toPascalCase(attribute.relatedCollection)}`;
|
115
|
+
schema.$ref = `#/definitions/${toPascalCase(this.resolveCollectionName(attribute.relatedCollection))}`;
|
112
116
|
}
|
113
117
|
else {
|
114
118
|
schema.type = "string";
|
@@ -33,7 +33,20 @@ export const processQueue = async (db, dbId) => {
|
|
33
33
|
}
|
34
34
|
// Attempt to resolve related collection if specified and not already found
|
35
35
|
if (!collectionFound && operation.attribute?.relatedCollection) {
|
36
|
-
|
36
|
+
// First, try treating relatedCollection as an ID
|
37
|
+
try {
|
38
|
+
const relAttr = operation.attribute;
|
39
|
+
const byId = await tryAwaitWithRetry(async () => await db.getCollection(dbId, relAttr.relatedCollection));
|
40
|
+
// We still need the target collection (operation.collectionId) to create the attribute on,
|
41
|
+
// so only use this branch to warm caches/mappings and continue to dependency checks.
|
42
|
+
// Do not override collectionFound with the related collection.
|
43
|
+
}
|
44
|
+
catch (_) {
|
45
|
+
// Not an ID or not found; fall back to name-based cache
|
46
|
+
}
|
47
|
+
// Warm cache by name (used by attribute creation path), but do not use as target collection
|
48
|
+
const relAttr = operation.attribute;
|
49
|
+
await fetchAndCacheCollectionByName(db, dbId, relAttr.relatedCollection);
|
37
50
|
}
|
38
51
|
// Handle dependencies if collection still not found
|
39
52
|
if (!collectionFound) {
|
@@ -4,6 +4,7 @@ export declare class SchemaGenerator {
|
|
4
4
|
private config;
|
5
5
|
private appwriteFolderPath;
|
6
6
|
constructor(config: AppwriteConfig, appwriteFolderPath: string);
|
7
|
+
private resolveCollectionName;
|
7
8
|
updateYamlCollections(): void;
|
8
9
|
updateTsSchemas(): void;
|
9
10
|
updateConfig(config: AppwriteConfig, isYamlConfig?: boolean): Promise<void>;
|
@@ -15,6 +16,6 @@ export declare class SchemaGenerator {
|
|
15
16
|
format?: "zod" | "json" | "both";
|
16
17
|
verbose?: boolean;
|
17
18
|
}): void;
|
18
|
-
|
19
|
+
createSchemaStringV4: (name: string, attributes: Attribute[]) => string;
|
19
20
|
typeToZod: (attribute: Attribute) => string;
|
20
21
|
}
|
@@ -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) => {
|