Skip to content

Commit c6f4077

Browse files
Merge pull request #1 from abnegate/feature/multi-language-support
Feature/multi language support
2 parents 0be4dfa + d631545 commit c6f4077

File tree

9 files changed

+354
-59
lines changed

9 files changed

+354
-59
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
</a>
1010
</p>
1111

12-
> CLI tool to generate Typescript Definitions from your Appwrite Collections.
12+
> CLI tool to generate Typescript, Kotlin or PHP Definitions from your Appwrite Collections.
1313
1414
*This is work in progress*
1515

@@ -22,7 +22,7 @@ npm i appwrite-types-generator
2222
## CLI Usage
2323

2424
```sh
25-
aw-types generate -c config.json -o types/
25+
aw-types generate -c config.json -o types/ -l typescript
2626
```
2727

2828
## package.json

attributes.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const AttributeTypes = {
2+
TEXT: "text",
3+
EMAIL: "email",
4+
URL: "url",
5+
IP: "ip",
6+
WILDCARD: "wildcard",
7+
MARKDOWN: "markdown",
8+
NUMERIC: "numeric",
9+
BOOLEAN: "boolean",
10+
};

index.js

Lines changed: 67 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,37 @@
11
#!/usr/bin/env node
22

3-
import camelCase from "camelcase";
43
import yargs from "yargs";
54
import path from "path";
6-
import { Client, Database } from "node-appwrite";
7-
import { hideBin } from "yargs/helpers";
8-
import { mkdir, readFile, writeFile } from "fs/promises";
9-
import { existsSync } from "fs";
10-
import { exit } from "process";
5+
import {Client, Database} from "node-appwrite";
6+
import {hideBin} from "yargs/helpers";
7+
import {mkdir, readFile, writeFile} from "fs/promises";
8+
import {existsSync} from "fs";
9+
import {exit} from "process";
10+
import {Typescript} from "./languages/typescript.js"
11+
import {Kotlin} from "./languages/kotlin.js"
12+
import {Php} from "./languages/php.js";
1113

1214
const client = new Client();
1315
const database = new Database(client);
1416

15-
/**
16-
* To Pascal Case.
17-
* @param {string} v
18-
* @returns {string}
19-
*/
20-
const toPascal = v => camelCase(v, { pascalCase: true });
21-
22-
/**
23-
* Convert Collection to Typescript type.
24-
* @param {"text"|"numeric"|"boolean"} type
25-
* @returns {"string"|"number"|"boolean"}
26-
*/
27-
const toType = type => ({
28-
"text": "string",
29-
"email": "string",
30-
"url": "string",
31-
"ip": "string",
32-
"wildcard": "string",
33-
"markdown": "string",
34-
"numeric": "number",
35-
"boolean": "boolean"
36-
}[type]);
17+
const languageClasses = {
18+
"typescript": Typescript,
19+
"kotlin": Kotlin,
20+
"php": Php
21+
}
3722

3823
/**
3924
* Load configuration for Appwrite.
40-
* @param {string} file
25+
* @param {string} file
4126
* @returns {Promise<object>}
4227
*/
4328
const loadConfiguration = async file => {
29+
4430
try {
4531
if (!existsSync(file)) {
46-
throw new Error("Configuration not found.")
32+
onError("Configuration not found.")
4733
}
34+
4835
const data = await readFile(file, 'utf8');
4936
const config = JSON.parse(data);
5037

@@ -55,47 +42,67 @@ const loadConfiguration = async file => {
5542

5643
return config;
5744
} catch (err) {
58-
console.error(err.message)
59-
exit(1);
45+
onError(err.message)
6046
}
6147
}
6248

63-
const generate = async ({ output, config }) => {
64-
config = await loadConfiguration(config);
49+
const generate = async ({output, config, language}) => {
50+
await loadConfiguration(config);
51+
52+
const selectedClass = languageClasses[language];
53+
if (!selectedClass) {
54+
onError(`Language must be one of [${Object.keys(languageClasses)}]`);
55+
}
56+
const lang = new selectedClass();
6557

6658
/** @type {Array<object>} collections*/
6759
const collections = (await database.listCollections()).collections;
68-
const types = [];
60+
const types = buildCollectionTypes(collections)
61+
62+
if (!existsSync(output)) {
63+
await mkdir(output, {recursive: true});
64+
}
65+
66+
for (const type of types) {
67+
const content = lang.generate(type)
68+
69+
try {
70+
const destination = path.join(output, `./${type.name}${lang.getFileExtension()}`);
71+
await writeFile(destination, content);
72+
console.log(`Generated ${destination}`)
73+
} catch (err) {
74+
console.error(err.message)
75+
}
76+
}
77+
}
78+
79+
const buildCollectionTypes = (collections) => {
80+
let types = [];
6981
collections.forEach(collection => {
7082
types.push({
71-
name: toPascal(collection.name),
83+
name: collection.name,
7284
attributes: collection.rules.map(rule => {
7385
return {
7486
name: rule.key,
75-
type: toType(rule.type),
87+
type: rule.type,
7688
array: rule.array,
7789
required: rule.required
7890
}
91+
}).sort((a, b) => {
92+
return (a.required === b.required)
93+
? 0
94+
: a.required
95+
? -1
96+
: 1;
7997
})
8098
})
8199
})
82-
if (!existsSync(output)) {
83-
await mkdir(output, { recursive: true });
84-
}
85-
types.forEach(async (type) => {
86-
const content = `export type ${type.name} = {\n` +
87-
`${type.attributes.reduce((prev, next) => {
88-
return `${prev} ${next.name}${next.required ? "" : "?"}: ${next.type ?? "unknown"}${next.array ? "[]" : ""};\n`;
89-
}, "")}` +
90-
"}";
91-
try {
92-
const destination = path.join(output, `./${type.name}.d.ts`);
93-
await writeFile(destination, content);
94-
console.log(`Generated ${destination}`)
95-
} catch (err) {
96-
console.error(err.message)
97-
}
98-
});
100+
return types;
101+
}
102+
103+
const onError = function catchError(error) {
104+
console.error(error)
105+
exit(1);
99106
}
100107

101108
yargs(hideBin(process.argv))
@@ -112,5 +119,11 @@ yargs(hideBin(process.argv))
112119
description: "Configuration file.",
113120
required: true
114121
})
122+
.option("language", {
123+
alias: "l",
124+
type: "string",
125+
description: "Output language",
126+
default: "typescript"
127+
})
115128
.demandCommand(1)
116129
.argv;

languages/kotlin.js

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import camelCase from "camelcase";
2+
import {Language} from "./language.js"
3+
import {AttributeTypes} from "../attributes.js";
4+
5+
export class Kotlin extends Language {
6+
7+
getType(type) {
8+
switch (type) {
9+
case AttributeTypes.TEXT:
10+
case AttributeTypes.EMAIL:
11+
case AttributeTypes.URL:
12+
case AttributeTypes.IP:
13+
case AttributeTypes.WILDCARD:
14+
case AttributeTypes.MARKDOWN:
15+
return "String"
16+
case AttributeTypes.NUMERIC:
17+
return "Number"
18+
case AttributeTypes.BOOLEAN:
19+
return "Boolean"
20+
}
21+
}
22+
23+
getTypeDefault(attribute) {
24+
if (!attribute.required) {
25+
return "null";
26+
}
27+
if (attribute.array) {
28+
return `listOf<${this.getType(attribute.type)}>()`;
29+
}
30+
switch (attribute.type) {
31+
case AttributeTypes.TEXT:
32+
case AttributeTypes.EMAIL:
33+
case AttributeTypes.URL:
34+
case AttributeTypes.IP:
35+
case AttributeTypes.WILDCARD:
36+
case AttributeTypes.MARKDOWN:
37+
return '""';
38+
case AttributeTypes.NUMERIC:
39+
return "0";
40+
case AttributeTypes.BOOLEAN:
41+
return "false";
42+
}
43+
}
44+
45+
getFileExtension() {
46+
return ".kt"
47+
}
48+
49+
getTypeOpenLine(name) {
50+
return `data class ${camelCase(name, {pascalCase: true})} (\n`
51+
}
52+
53+
getTypePropertyLine(attribute) {
54+
return `\t${attribute.name}: ${attribute.array ? "List<" : ""}${this.getType(attribute.type) ?? "Any"}${attribute.array ? ">" : ""}${attribute.required ? "" : "?"}${attribute.required ? "" : ` = ${this.getTypeDefault(attribute)}`},\n`;
55+
}
56+
57+
getTypeCloseLine() {
58+
return ")\n"
59+
}
60+
}

languages/language.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
export class Language {
2+
3+
constructor() {
4+
if (new.target === Language) {
5+
throw new TypeError("Abstract classes can't be instantiated.");
6+
}
7+
}
8+
9+
/**
10+
* Get this languages type mapping for the given type.
11+
*
12+
* @param type
13+
* @return {string}
14+
*/
15+
getType(type) {
16+
throw new TypeError("Stub.");
17+
}
18+
19+
/**
20+
* Get this languages type default for the given type.
21+
*
22+
* @param type
23+
* @param isArray
24+
* @return {string}
25+
*/
26+
getTypeDefault(attribute) {
27+
throw new TypeError("Stub.");
28+
}
29+
30+
/**
31+
* Get the file extension used by files of this language.
32+
*/
33+
getFileExtension() {
34+
throw new TypeError("Stub.");
35+
}
36+
37+
/**
38+
* Get a string that defines the opening line for a new type with the given name in this language.
39+
*
40+
* @param name
41+
*/
42+
getTypeOpenLine(name) {
43+
throw new TypeError("Stub.");
44+
}
45+
46+
/**
47+
* Get a string that defines a property line for the given attribute for this language.
48+
*
49+
* @param attribute
50+
*/
51+
getTypePropertyLine(attribute) {
52+
throw new TypeError("Stub.");
53+
}
54+
55+
/**
56+
* Get a string that closes a type definition for this language.
57+
*
58+
*/
59+
getTypeCloseLine() {
60+
throw new TypeError("Stub.");
61+
}
62+
63+
/**
64+
* Generate (as a string) a type definition with the given type for this language.
65+
*
66+
* @param type
67+
* @returns {string}
68+
*/
69+
generate(type) {
70+
return this.getTypeOpenLine(type.name)
71+
+ (type.attributes.map(attr => {
72+
return this.getTypePropertyLine(attr)
73+
}).join(""))
74+
+ this.getTypeCloseLine();
75+
}
76+
}

0 commit comments

Comments
 (0)