diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index 1ce1b5e89..5aecfbe24 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -181,6 +181,51 @@ public function getFiles(): array 'destination' => 'lib/sdks.js', 'template' => 'cli/lib/sdks.js.twig', ], + [ + 'scope' => 'default', + 'destination' => 'lib/type-generation/attribute.js', + 'template' => 'cli/lib/type-generation/attribute.js.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'lib/type-generation/languages/language.js', + 'template' => 'cli/lib/type-generation/languages/language.js.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'lib/type-generation/languages/php.js', + 'template' => 'cli/lib/type-generation/languages/php.js.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'lib/type-generation/languages/typescript.js', + 'template' => 'cli/lib/type-generation/languages/typescript.js.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'lib/type-generation/languages/javascript.js', + 'template' => 'cli/lib/type-generation/languages/javascript.js.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'lib/type-generation/languages/kotlin.js', + 'template' => 'cli/lib/type-generation/languages/kotlin.js.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'lib/type-generation/languages/swift.js', + 'template' => 'cli/lib/type-generation/languages/swift.js.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'lib/type-generation/languages/java.js', + 'template' => 'cli/lib/type-generation/languages/java.js.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'lib/type-generation/languages/dart.js', + 'template' => 'cli/lib/type-generation/languages/dart.js.twig', + ], [ 'scope' => 'default', 'destination' => 'lib/questions.js', @@ -275,6 +320,11 @@ public function getFiles(): array 'scope' => 'default', 'destination' => 'lib/commands/organizations.js', 'template' => 'cli/lib/commands/organizations.js.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'lib/commands/types.js', + 'template' => 'cli/lib/commands/types.js.twig', ] ]; } diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index c54c2fe4b..a7946c3a9 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -14,6 +14,7 @@ const inquirer = require("inquirer"); {% if sdk.test != "true" %} const { login, logout, whoami, migrate, register } = require("./lib/commands/generic"); const { init } = require("./lib/commands/init"); +const { types } = require("./lib/commands/types"); const { pull } = require("./lib/commands/pull"); const { run } = require("./lib/commands/run"); const { push, deploy } = require("./lib/commands/push"); @@ -68,6 +69,7 @@ program .addCommand(init) .addCommand(pull) .addCommand(push) + .addCommand(types) .addCommand(deploy) .addCommand(run) .addCommand(logout) diff --git a/templates/cli/lib/commands/types.js.twig b/templates/cli/lib/commands/types.js.twig new file mode 100644 index 000000000..db6dead66 --- /dev/null +++ b/templates/cli/lib/commands/types.js.twig @@ -0,0 +1,126 @@ +const ejs = require("ejs"); +const fs = require("fs"); +const path = require("path"); +const { LanguageMeta, detectLanguage } = require("../type-generation/languages/language"); +const { Command, Option, Argument } = require("commander"); +const { localConfig } = require("../config"); +const { success, log, actionRunner } = require("../parser"); +const { PHP } = require("../type-generation/languages/php"); +const { TypeScript } = require("../type-generation/languages/typescript"); +const { Kotlin } = require("../type-generation/languages/kotlin"); +const { Swift } = require("../type-generation/languages/swift"); +const { Java } = require("../type-generation/languages/java"); +const { Dart } = require("../type-generation/languages/dart"); +const { JavaScript } = require("../type-generation/languages/javascript"); + +/** + * @param {string} language + * @returns {import("../type-generation/languages/language").LanguageMeta} + */ +function createLanguageMeta(language) { + switch (language) { + case "ts": + return new TypeScript(); + case "js": + return new JavaScript(); + case "php": + return new PHP(); + case "kotlin": + return new Kotlin(); + case "swift": + return new Swift(); + case "java": + return new Java(); + case "dart": + return new Dart(); + default: + throw new Error(`Language '${language}' is not supported`); + } +} + +const templateHelpers = { + toPascalCase: LanguageMeta.toPascalCase, + toCamelCase: LanguageMeta.toCamelCase, + toSnakeCase: LanguageMeta.toSnakeCase, + toKebabCase: LanguageMeta.toKebabCase, + toUpperSnakeCase: LanguageMeta.toUpperSnakeCase +} + +const typesOutputArgument = new Argument( + "", + "The directory to write the types to" +); + +const typesLanguageOption = new Option( + "-l, --language ", + "The language of the types" +) + .choices(["auto", "ts", "js", "php", "kotlin", "swift", "java", "dart"]) + .default("auto"); + +const typesCommand = actionRunner(async (rawOutputDirectory, {language}) => { + if (language === "auto") { + language = detectLanguage(); + log(`Detected language: ${language}`); + } + + const meta = createLanguageMeta(language); + + const outputDirectory = path.resolve(rawOutputDirectory); + if (!fs.existsSync(outputDirectory)) { + log(`Directory: ${outputDirectory} does not exist, creating...`); + fs.mkdirSync(outputDirectory, { recursive: true }); + } + + if (!fs.existsSync("appwrite.json")) { + throw new Error("appwrite.json not found in current directory"); + } + + const collections = localConfig.getCollections(); + if (collections.length === 0) { + throw new Error("No collections found in appwrite.json"); + } + + log(`Found ${collections.length} collections: ${collections.map(c => c.name).join(", ")}`); + + const totalAttributes = collections.reduce((count, collection) => count + collection.attributes.length, 0); + log(`Found ${totalAttributes} attributes across all collections`); + + const templater = ejs.compile(meta.getTemplate()); + + if (meta.isSingleFile()) { + const content = templater({ + collections, + ...templateHelpers, + getType: meta.getType + }); + + const destination = path.join(outputDirectory, meta.getFileName()); + + fs.writeFileSync(destination, content); + log(`Added types to ${destination}`); + } else { + for (const collection of collections) { + const content = templater({ + collection, + ...templateHelpers, + getType: meta.getType + }); + + const destination = path.join(outputDirectory, meta.getFileName(collection)); + + fs.writeFileSync(destination, content); + log(`Added types for ${collection.name} to ${destination}`); + } + } + + success(`Generated types for all the listed collections`); +}); + +const types = new Command("types") + .description("Generate types for your Appwrite project") + .addArgument(typesOutputArgument) + .addOption(typesLanguageOption) + .action(actionRunner(typesCommand)); + +module.exports = { types }; diff --git a/templates/cli/lib/type-generation/attribute.js.twig b/templates/cli/lib/type-generation/attribute.js.twig new file mode 100644 index 000000000..e92f0f026 --- /dev/null +++ b/templates/cli/lib/type-generation/attribute.js.twig @@ -0,0 +1,16 @@ +const AttributeType = { + STRING: "string", + INTEGER: "integer", + FLOAT: "float", + BOOLEAN: "boolean", + DATETIME: "datetime", + EMAIL: "email", + IP: "ip", + URL: "url", + ENUM: "enum", + RELATIONSHIP: "relationship", +}; + + module.exports = { + AttributeType, + }; diff --git a/templates/cli/lib/type-generation/languages/dart.js.twig b/templates/cli/lib/type-generation/languages/dart.js.twig new file mode 100644 index 000000000..65453e40e --- /dev/null +++ b/templates/cli/lib/type-generation/languages/dart.js.twig @@ -0,0 +1,152 @@ +/** @typedef {import('../attribute').Attribute} Attribute */ +const { AttributeType } = require('../attribute'); +const { LanguageMeta } = require("./language"); + +class Dart extends LanguageMeta { + getType(attribute) { + let type = ""; + switch (attribute.type) { + case AttributeType.STRING: + case AttributeType.EMAIL: + case AttributeType.DATETIME: + type = "String"; + if (attribute.format === AttributeType.ENUM) { + type = LanguageMeta.toPascalCase(attribute.key); + } + break; + case AttributeType.INTEGER: + type = "int"; + break; + case AttributeType.FLOAT: + type = "double"; + break; + case AttributeType.BOOLEAN: + type = "bool"; + break; + case AttributeType.RELATIONSHIP: + type = LanguageMeta.toPascalCase(attribute.relatedCollection); + if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { + type = `List<${type}>`; + } + break; + default: + throw new Error(`Unknown attribute type: ${attribute.type}`); + } + if (attribute.array) { + type = `List<${type}>`; + } + if (!attribute.required) { + type += "?"; + } + return type; + } + + getTemplate() { + return `<% for (const attribute of collection.attributes) { -%> +<% if (attribute.type === 'relationship') { -%> +import '<%- attribute.relatedCollection.toLowerCase() %>.dart'; + +<% } -%> +<% } -%> +<% for (const attribute of collection.attributes) { -%> +<% if (attribute.format === 'enum') { -%> +enum <%- toPascalCase(attribute.key) %> { +<% for (const element of attribute.elements) { -%> + <%- element %>, +<% } -%> +} + +<% } -%> +<% } -%> +class <%= toPascalCase(collection.name) %> { +<% for (const [index, attribute] of Object.entries(collection.attributes)) { -%> + <%- getType(attribute) %> <%= toCamelCase(attribute.key) %>; +<% } -%> + + <%= toPascalCase(collection.name) %>({ + <% for (const [index, attribute] of Object.entries(collection.attributes)) { -%> + <% if (attribute.required) { %>required <% } %>this.<%= toCamelCase(attribute.key) %>, + <% } -%> +}); + + factory <%= toPascalCase(collection.name) %>.fromMap(Map map) { + return <%= toPascalCase(collection.name) %>( +<% for (const [index, attribute] of Object.entries(collection.attributes)) { -%> + <%= toCamelCase(attribute.key) %>: <% if (attribute.type === 'string' || attribute.type === 'email' || attribute.type === 'datetime') { -%> +<% if (attribute.format === 'enum') { -%> +<% if (attribute.array) { -%> +(map['<%= attribute.key %>'] as List?)?.map((e) => <%- toPascalCase(attribute.key) %>.values.firstWhere((element) => element.name == e)).toList()<% if (!attribute.required) { %> ?? []<% } -%> +<% } else { -%> +<% if (!attribute.required) { -%> +map['<%= attribute.key %>'] != null ? <%- toPascalCase(attribute.key) %>.values.where((e) => e.name == map['<%= attribute.key %>']).firstOrNull : null<% } else { -%> +<%- toPascalCase(attribute.key) %>.values.firstWhere((e) => e.name == map['<%= attribute.key %>'])<% } -%> +<% } -%> +<% } else { -%> +<% if (attribute.array) { -%> +List.from(map['<%= attribute.key %>'] ?? [])<% if (!attribute.required) { %> ?? []<% } -%> +<% } else { -%> +map['<%= attribute.key %>']<% if (!attribute.required) { %>?<% } %>.toString()<% if (!attribute.required) { %> ?? null<% } -%> +<% } -%> +<% } -%> +<% } else if (attribute.type === 'integer') { -%> +<% if (attribute.array) { -%> +List.from(map['<%= attribute.key %>'] ?? [])<% if (!attribute.required) { %> ?? []<% } -%> +<% } else { -%> +map['<%= attribute.key %>']<% if (!attribute.required) { %> ?? null<% } -%> +<% } -%> +<% } else if (attribute.type === 'float') { -%> +<% if (attribute.array) { -%> +List.from(map['<%= attribute.key %>'] ?? [])<% if (!attribute.required) { %> ?? []<% } -%> +<% } else { -%> +map['<%= attribute.key %>']<% if (!attribute.required) { %> ?? null<% } -%> +<% } -%> +<% } else if (attribute.type === 'boolean') { -%> +<% if (attribute.array) { -%> +List.from(map['<%= attribute.key %>'] ?? [])<% if (!attribute.required) { %> ?? []<% } -%> +<% } else { -%> +map['<%= attribute.key %>']<% if (!attribute.required) { %> ?? null<% } -%> +<% } -%> +<% } else if (attribute.type === 'relationship') { -%> +<% if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { -%> +(map['<%= attribute.key %>'] as List?)?.map((e) => <%- toPascalCase(attribute.relatedCollection) %>.fromMap(e)).toList()<% if (!attribute.required) { %> ?? []<% } -%> +<% } else { -%> +<% if (!attribute.required) { -%> +map['<%= attribute.key %>'] != null ? <%- toPascalCase(attribute.relatedCollection) %>.fromMap(map['<%= attribute.key %>']) : null<% } else { -%> +<%- toPascalCase(attribute.relatedCollection) %>.fromMap(map['<%= attribute.key %>'])<% } -%> +<% } -%> +<% } -%>, +<% } -%> + ); + } + + Map toMap() { + return { +<% for (const [index, attribute] of Object.entries(collection.attributes)) { -%> + "<%= attribute.key %>": <% if (attribute.type === 'relationship') { -%> +<% if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { -%> +<%= toCamelCase(attribute.key) %><% if (!attribute.required) { %>?<% } %>.map((e) => e.toMap()).toList()<% if (!attribute.required) { %> ?? []<% } -%> +<% } else { -%> +<%= toCamelCase(attribute.key) %><% if (!attribute.required) { %>?<% } %>.toMap()<% if (!attribute.required) { %> ?? {}<% } -%> +<% } -%> +<% } else if (attribute.format === 'enum') { -%> +<% if (attribute.array) { -%> +<%= toCamelCase(attribute.key) %><% if (!attribute.required) { %>?<% } %>.map((e) => e.name).toList()<% if (!attribute.required) { %> ?? []<% } -%> +<% } else { -%> +<%= toCamelCase(attribute.key) %><% if (!attribute.required) { %>?<% } %>.name<% if (!attribute.required) { %> ?? null<% } -%> +<% } -%> +<% } else { -%> +<%= toCamelCase(attribute.key) -%> +<% } -%>, +<% } -%> + }; + } +} +`; + } + + getFileName(collection) { + return LanguageMeta.toSnakeCase(collection.name) + ".dart"; + } +} + +module.exports = { Dart }; \ No newline at end of file diff --git a/templates/cli/lib/type-generation/languages/java.js.twig b/templates/cli/lib/type-generation/languages/java.js.twig new file mode 100644 index 000000000..fb91be928 --- /dev/null +++ b/templates/cli/lib/type-generation/languages/java.js.twig @@ -0,0 +1,121 @@ +/** @typedef {import('../attribute').Attribute} Attribute */ +const { AttributeType } = require('../attribute'); +const { LanguageMeta } = require("./language"); + +class Java extends LanguageMeta { + getType(attribute) { + let type = ""; + switch (attribute.type) { + case AttributeType.STRING: + case AttributeType.EMAIL: + case AttributeType.DATETIME: + type = "String"; + if (attribute.format === AttributeType.ENUM) { + type = LanguageMeta.toPascalCase(attribute.key); + } + break; + case AttributeType.INTEGER: + type = "int"; + break; + case AttributeType.FLOAT: + type = "double"; + break; + case AttributeType.BOOLEAN: + type = "boolean"; + break; + case AttributeType.RELATIONSHIP: + type = LanguageMeta.toPascalCase(attribute.relatedCollection); + if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { + type = "List<" + type + ">"; + } + break; + default: + throw new Error(`Unknown attribute type: ${attribute.type}`); + } + if (attribute.array) { + type = "List<" + type + ">"; + } + return type; + } + + getTemplate() { + return `package io.appwrite.models; + +import java.util.*; +<% for (const attribute of collection.attributes) { -%> +<% if (attribute.type === 'relationship') { -%> +import <%- toPascalCase(attribute.relatedCollection) %>; + +<% } -%> +<% } -%> +public class <%- toPascalCase(collection.name) %> { +<% for (const attribute of collection.attributes) { -%> +<% if (attribute.format === 'enum') { -%> + + public enum <%- toPascalCase(attribute.key) %> { +<% for (const [index, element] of Object.entries(attribute.elements)) { -%> + <%- element %><%- index < attribute.elements.length - 1 ? ',' : ';' %> +<% } -%> + } + +<% } -%> +<% } -%> +<% for (const attribute of collection.attributes) { -%> + private <%- getType(attribute) %> <%- toCamelCase(attribute.key) %>; +<% } -%> + + public <%- toPascalCase(collection.name) %>() { + } + + public <%- toPascalCase(collection.name) %>( +<% for (const [index, attribute] of Object.entries(collection.attributes)) { -%> + <%- getType(attribute) %> <%= toCamelCase(attribute.key) %><%- index < collection.attributes.length - 1 ? ',' : '' %> +<% } -%> + ) { +<% for (const attribute of collection.attributes) { -%> + this.<%= toCamelCase(attribute.key) %> = <%= toCamelCase(attribute.key) %>; +<% } -%> + } + +<% for (const attribute of collection.attributes) { -%> + public <%- getType(attribute) %> get<%- toPascalCase(attribute.key) %>() { + return <%= toCamelCase(attribute.key) %>; + } + + public void set<%- toPascalCase(attribute.key) %>(<%- getType(attribute) %> <%= toCamelCase(attribute.key) %>) { + this.<%= toCamelCase(attribute.key) %> = <%= toCamelCase(attribute.key) %>; + } + +<% } -%> + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + <%- toPascalCase(collection.name) %> that = (<%- toPascalCase(collection.name) %>) obj; + return <% collection.attributes.forEach((attr, index) => { %>Objects.equals(<%= toCamelCase(attr.key) %>, that.<%= toCamelCase(attr.key) %>)<% if (index < collection.attributes.length - 1) { %> && + <% } }); %>; + } + + @Override + public int hashCode() { + return Objects.hash(<%= collection.attributes.map(attr => toCamelCase(attr.key)).join(', ') %>); + } + + @Override + public String toString() { + return "<%- toPascalCase(collection.name) %>{" + +<% for (const [index, attribute] of Object.entries(collection.attributes)) { -%> + "<%= toCamelCase(attribute.key) %>=" + <%= toCamelCase(attribute.key) %> + +<% } -%> + '}'; + } +} +`; + } + + getFileName(collection) { + return LanguageMeta.toPascalCase(collection.name) + ".java"; + } +} + +module.exports = { Java }; diff --git a/templates/cli/lib/type-generation/languages/javascript.js.twig b/templates/cli/lib/type-generation/languages/javascript.js.twig new file mode 100644 index 000000000..f6d632e71 --- /dev/null +++ b/templates/cli/lib/type-generation/languages/javascript.js.twig @@ -0,0 +1,84 @@ +/** @typedef {import('../attribute').Attribute} Attribute */ +const fs = require("fs"); +const path = require("path"); + +const { AttributeType } = require('../attribute'); +const { LanguageMeta } = require("./language"); + +class JavaScript extends LanguageMeta { + getType(attribute) { + let type = "" + switch (attribute.type) { + case AttributeType.STRING: + case AttributeType.EMAIL: + case AttributeType.DATETIME: + case AttributeType.IP: + case AttributeType.URL: + type = "string"; + if (attribute.format === AttributeType.ENUM) { + type = `"${attribute.elements.join('"|"')}"`; + } + break; + case AttributeType.INTEGER: + type = "number"; + break; + case AttributeType.FLOAT: + type = "number"; + break; + case AttributeType.BOOLEAN: + type = "boolean"; + break; + case AttributeType.RELATIONSHIP: + type = LanguageMeta.toPascalCase(attribute.relatedCollection); + if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { + type = `Array<${type}>`; + } + break; + default: + throw new Error(`Unknown attribute type: ${attribute.type}`); + } + if (attribute.array) { + type += "[]"; + } + if (!attribute.required) { + type += "|null|undefined"; + } + return type; + } + + isSingleFile() { + return true; + } + + _getAppwriteDependency() { + if (fs.existsSync(path.resolve(process.cwd(), 'package.json'))) { + const packageJsonRaw = fs.readFileSync(path.resolve(process.cwd(), 'package.json')); + const packageJson = JSON.parse(packageJsonRaw.toString('utf-8')); + return packageJson.dependencies['node-appwrite'] ? 'node-appwrite' : 'appwrite'; + } + + return "appwrite"; + } + + getTemplate() { + return `/** + * @typedef {import('${this._getAppwriteDependency()}').Models.Document} Document + */ + +<% for (const collection of collections) { %> +/** + * @typedef {Object} <%- toPascalCase(collection.name) %> +<% for (const attribute of collection.attributes) { -%> + * @property {<%- getType(attribute) %>} <%- toCamelCase(attribute.key) %> +<% } -%> + */ + +<% } %>`; + } + + getFileName(_) { + return "appwrite-types.js"; + } +} + +module.exports = { JavaScript }; \ No newline at end of file diff --git a/templates/cli/lib/type-generation/languages/kotlin.js.twig b/templates/cli/lib/type-generation/languages/kotlin.js.twig new file mode 100644 index 000000000..85c380a43 --- /dev/null +++ b/templates/cli/lib/type-generation/languages/kotlin.js.twig @@ -0,0 +1,75 @@ +/** @typedef {import('../attribute').Attribute} Attribute */ +const { AttributeType } = require('../attribute'); +const { LanguageMeta } = require("./language"); + +class Kotlin extends LanguageMeta { + getType(attribute) { + let type = ""; + switch (attribute.type) { + case AttributeType.STRING: + case AttributeType.EMAIL: + case AttributeType.DATETIME: + type = "String"; + if (attribute.format === AttributeType.ENUM) { + type = LanguageMeta.toPascalCase(attribute.key); + } + break; + case AttributeType.INTEGER: + type = "Int"; + break; + case AttributeType.FLOAT: + type = "Float"; + break; + case AttributeType.BOOLEAN: + type = "Boolean"; + break; + case AttributeType.RELATIONSHIP: + type = LanguageMeta.toPascalCase(attribute.relatedCollection); + if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { + type = `List<${type}>`; + } + break; + default: + throw new Error(`Unknown attribute type: ${attribute.type}`); + } + if (attribute.array) { + type = "List<" + type + ">"; + } + if (!attribute.required) { + type += "?"; + } + return type; + } + + getTemplate() { + return `package io.appwrite.models + +<% for (const attribute of collection.attributes) { -%> +<% if (attribute.type === 'relationship') { -%> +import <%- toPascalCase(attribute.relatedCollection) %> + +<% } -%> +<% } -%> +<% for (const attribute of collection.attributes) { -%> +<% if (attribute.format === 'enum') { -%> +enum class <%- toPascalCase(attribute.key) %> { +<% for (const [index, element] of Object.entries(attribute.elements)) { -%> + <%- element %><%- index < attribute.elements.length - 1 ? ',' : '' %> +<% } -%> +} + +<% } -%> +<% } -%> +data class <%- toPascalCase(collection.name) %>( +<% for (const attribute of collection.attributes) { -%> + val <%- toCamelCase(attribute.key) %>: <%- getType(attribute) %>, +<% } -%> +)`; + } + + getFileName(collection) { + return LanguageMeta.toPascalCase(collection.name) + ".kt"; + } +} + +module.exports = { Kotlin }; diff --git a/templates/cli/lib/type-generation/languages/language.js.twig b/templates/cli/lib/type-generation/languages/language.js.twig new file mode 100644 index 000000000..96ba0bba2 --- /dev/null +++ b/templates/cli/lib/type-generation/languages/language.js.twig @@ -0,0 +1,125 @@ +/** @typedef {import('../attribute').Attribute} Attribute */ +/** @typedef {import('../collection').Collection} Collection */ + +const fs = require("fs"); +const path = require("path"); + +class LanguageMeta { + constructor() { + if (new.target === LanguageMeta) { + throw new TypeError("Abstract classes can't be instantiated."); + } + } + + static toKebabCase(string) { + return string + .replace(/[^a-zA-Z0-9\s-_]/g, "") // Remove invalid characters + .replace(/([a-z])([A-Z])/g, "$1-$2") // Add hyphen between camelCase + .replace(/([A-Z])([A-Z][a-z])/g, "$1-$2") // Add hyphen between PascalCase + .replace(/[_\s]+/g, "-") // Replace spaces and underscores with hyphens + .replace(/^-+|-+$/g, "") // Remove leading and trailing hyphens + .replace(/--+/g, "-") // Replace multiple hyphens with a single hyphen + .toLowerCase(); + } + + static toSnakeCase(string) { + return this.toKebabCase(string).replace(/-/g, "_"); + } + + static toUpperSnakeCase(string) { + return this.toSnakeCase(string).toUpperCase(); + } + + static toCamelCase(string) { + return this.toKebabCase(string).replace(/-([a-z0-9])/g, (g) => + g[1].toUpperCase() + ); + } + + static toPascalCase(string) { + return this.toCamelCase(string).replace(/^./, (g) => g.toUpperCase()); + } + + /** + * Get the type literal of the given attribute. + * + * @abstract + * @param {Attribute} attribute + * @return {string} + */ + getType(attribute) { + throw new TypeError("Stub."); + } + + /** + * Returns true if the language uses a single file for all types. + * + * @returns {boolean} + */ + isSingleFile() { + return false; + } + + /** + * Get the EJS template used to generate the types for this language. + * + * @abstract + * @returns {string} + */ + getTemplate() { + throw new TypeError("Stub."); + } + + /** + * Get the file extension used by files of this language. + * + * @abstract + * @param {Collection|undefined} collection + * @returns {string} + */ + getFileName(collection) { + throw new TypeError("Stub."); + } +} + +const existsFiles = (...files) => + files.some((file) => fs.existsSync(path.join(process.cwd(), file))); + +/** + * @returns {string} + */ +function detectLanguage() { + if (existsFiles("tsconfig.json", "deno.json")) { + return "ts"; + } + if (existsFiles("package.json")) { + return "js"; + } + if (existsFiles("composer.json")) { + return "php"; + } + if (existsFiles("requirements.txt", "Pipfile", "pyproject.toml")) { + return "python"; + } + if (existsFiles("Gemfile", "Rakefile")) { + return "ruby"; + } + if (existsFiles("build.gradle.kts")) { + return "kotlin"; + } + if (existsFiles("build.gradle", "pom.xml")) { + return "java"; + } + if (existsFiles("*.csproj")) { + return "dotnet"; + } + if (existsFiles("Package.swift")) { + return "swift"; + } + if (existsFiles("pubspec.yaml")) { + return "dart"; + } + throw new Error("Could not detect language, please specify with -l"); +} + +module.exports = { LanguageMeta, detectLanguage }; diff --git a/templates/cli/lib/type-generation/languages/php.js.twig b/templates/cli/lib/type-generation/languages/php.js.twig new file mode 100644 index 000000000..6dd185397 --- /dev/null +++ b/templates/cli/lib/type-generation/languages/php.js.twig @@ -0,0 +1,100 @@ +/** @typedef {import('../attribute').Attribute} Attribute */ +const { AttributeType } = require('../attribute'); +const { LanguageMeta } = require("./language"); + +class PHP extends LanguageMeta { + getType(attribute) { + if (attribute.array) { + return "array"; + } + let type = "" + switch (attribute.type) { + case AttributeType.STRING: + case AttributeType.EMAIL: + case AttributeType.DATETIME: + type = "string"; + if (attribute.format === AttributeType.ENUM) { + type = LanguageMeta.toPascalCase(attribute.key); + } + break; + case AttributeType.INTEGER: + type = "int"; + break; + case AttributeType.FLOAT: + type = "float"; + break; + case AttributeType.BOOLEAN: + type = "bool"; + break; + case AttributeType.RELATIONSHIP: + type = LanguageMeta.toPascalCase(attribute.relatedCollection); + if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { + type = "array"; + } + break; + default: + throw new Error(`Unknown attribute type: ${attribute.type}`); + } + if (!attribute.required) { + type += "|null"; + } + return type; + } + + getTemplate() { + return ` +<% if (attribute.type === 'relationship' && !(attribute.relationType === 'manyToMany') && !(attribute.relationType === 'oneToMany' && attribute.side === 'parent')) { -%> +use Appwrite\\Models\\<%- toPascalCase(attribute.relatedCollection) %>; + +<% } -%> +<% } -%> +<% for (const attribute of collection.attributes) { -%> +<% if (attribute.format === 'enum') { -%> +enum <%- toPascalCase(attribute.key) %> { +<% for (const [index, element] of Object.entries(attribute.elements)) { -%> + case <%- element.toUpperCase() %> = '<%- element %>'; +<% } -%> +} + +<% } -%> +<% } -%> +class <%- toPascalCase(collection.name) %> { +<% for (const attribute of collection.attributes ){ -%> + private <%- getType(attribute) %> $<%- toCamelCase(attribute.key) %>; +<% } -%> + + public function __construct( +<% for (const attribute of collection.attributes ){ -%> +<% if (attribute.required) { -%> + <%- getType(attribute).replace('|null', '') %> $<%- toCamelCase(attribute.key) %><% if (collection.attributes.indexOf(attribute) < collection.attributes.length - 1) { %>,<% } %> +<% } else { -%> + ?<%- getType(attribute).replace('|null', '') %> $<%- toCamelCase(attribute.key) %> = null<% if (collection.attributes.indexOf(attribute) < collection.attributes.length - 1) { %>,<% } %> +<% } -%> +<% } -%> + ) { +<% for (const attribute of collection.attributes ){ -%> + $this-><%- toCamelCase(attribute.key) %> = $<%- toCamelCase(attribute.key) %>; +<% } -%> + } + +<% for (const attribute of collection.attributes ){ -%> + public function get<%- toPascalCase(attribute.key) %>(): <%- getType(attribute) %> { + return $this-><%- toCamelCase(attribute.key) %>; + } + + public function set<%- toPascalCase(attribute.key) %>(<%- getType(attribute) %> $<%- toCamelCase(attribute.key) %>): void { + $this-><%- toCamelCase(attribute.key) %> = $<%- toCamelCase(attribute.key) %>; + } +<% } -%> +}`; + } + + getFileName(collection) { + return LanguageMeta.toPascalCase(collection.name) + ".php"; + } +} + +module.exports = { PHP }; diff --git a/templates/cli/lib/type-generation/languages/swift.js.twig b/templates/cli/lib/type-generation/languages/swift.js.twig new file mode 100644 index 000000000..67c64d89c --- /dev/null +++ b/templates/cli/lib/type-generation/languages/swift.js.twig @@ -0,0 +1,156 @@ +/** @typedef {import('../attribute').Attribute} Attribute */ +const { AttributeType } = require('../attribute'); +const { LanguageMeta } = require("./language"); + +class Swift extends LanguageMeta { + getType(attribute) { + let type = ""; + switch (attribute.type) { + case AttributeType.STRING: + case AttributeType.EMAIL: + case AttributeType.DATETIME: + type = "String"; + if (attribute.format === AttributeType.ENUM) { + type = LanguageMeta.toPascalCase(attribute.key); + } + break; + case AttributeType.INTEGER: + type = "Int"; + break; + case AttributeType.FLOAT: + type = "Double"; + break; + case AttributeType.BOOLEAN: + type = "Bool"; + break; + case AttributeType.RELATIONSHIP: + type = LanguageMeta.toPascalCase(attribute.relatedCollection); + if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { + type = `[${type}]`; + } + break; + default: + throw new Error(`Unknown attribute type: ${attribute.type}`); + } + if (attribute.array) { + type = "[" + type + "]"; + } + if (!attribute.required) { + type += "?"; + } + return type; + } + + getTemplate() { + return `import Foundation + +<% for (const attribute of collection.attributes) { -%> +<% if (attribute.format === 'enum') { -%> +public enum <%- toPascalCase(attribute.key) %>: String, Codable, CaseIterable { +<% for (const [index, element] of Object.entries(attribute.elements)) { -%> + case <%- element %> = "<%- element %>" +<% } -%> +} + +<% } -%> +<% } -%> +public class <%- toPascalCase(collection.name) %>: Codable { +<% for (const attribute of collection.attributes) { -%> + public let <%- toCamelCase(attribute.key) %>: <%- getType(attribute) %> +<% } %> + enum CodingKeys: String, CodingKey { +<% for (const attribute of collection.attributes) { -%> + case <%- toCamelCase(attribute.key) %> = "<%- attribute.key %>" +<% } -%> + } + + init( +<% for (const attribute of collection.attributes) { -%> + <%- toCamelCase(attribute.key) %>: <%- getType(attribute) %><% if (attribute !== collection.attributes[collection.attributes.length - 1]) { %>,<% } %> +<% } -%> + ) { +<% for (const attribute of collection.attributes) { -%> + self.<%- toCamelCase(attribute.key) %> = <%- toCamelCase(attribute.key) %> +<% } -%> + } + + public required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + +<% for (const attribute of collection.attributes) { -%> +<% if (attribute.required) { -%> + self.<%- toCamelCase(attribute.key) %> = try container.decode(<%- getType(attribute).replace('?', '') %>.self, forKey: .<%- toCamelCase(attribute.key) %>) +<% } else { -%> + self.<%- toCamelCase(attribute.key) %> = try container.decodeIfPresent(<%- getType(attribute).replace('?', '') %>.self, forKey: .<%- toCamelCase(attribute.key) %>) +<% } -%> +<% } -%> + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + +<% for (const attribute of collection.attributes) { -%> +<% if (attribute.required) { -%> + try container.encode(<%- toCamelCase(attribute.key) %>, forKey: .<%- toCamelCase(attribute.key) %>) +<% } else { -%> + try container.encodeIfPresent(<%- toCamelCase(attribute.key) %>, forKey: .<%- toCamelCase(attribute.key) %>) +<% } -%> +<% } -%> + } + + public func toMap() -> [String: Any] { + return [ +<% for (const attribute of collection.attributes) { -%> +<% if (attribute.type === 'relationship') { -%> + "<%- attribute.key %>": <%- toCamelCase(attribute.key) %> as Any<% if (attribute !== collection.attributes[collection.attributes.length - 1]) { %>,<% } %> +<% } else if (attribute.array && attribute.type !== 'string' && attribute.type !== 'integer' && attribute.type !== 'float' && attribute.type !== 'boolean') { -%> + "<%- attribute.key %>": <%- toCamelCase(attribute.key) %>?.map { $0.toMap() } as Any<% if (attribute !== collection.attributes[collection.attributes.length - 1]) { %>,<% } %> +<% } else { -%> + "<%- attribute.key %>": <%- toCamelCase(attribute.key) %> as Any<% if (attribute !== collection.attributes[collection.attributes.length - 1]) { %>,<% } %> +<% } -%> +<% } -%> + ] + } + + public static func from(map: [String: Any]) -> <%- toPascalCase(collection.name) %> { + return <%- toPascalCase(collection.name) %>( +<% for (const attribute of collection.attributes) { -%> +<% if (attribute.type === 'relationship') { -%> + <%- toCamelCase(attribute.key) %>: map["<%- attribute.key %>"] as<% if (!attribute.required) { %>?<% } else { %>!<% } %> <%- toPascalCase(attribute.relatedCollection) %><% if (attribute !== collection.attributes[collection.attributes.length - 1]) { %>,<% } %> +<% } else if (attribute.array) { -%> +<% if (attribute.type === 'string') { -%> + <%- toCamelCase(attribute.key) %>: map["<%- attribute.key %>"] as<% if (!attribute.required) { %>?<% } else { %>!<% } %> [String]<% if (attribute !== collection.attributes[collection.attributes.length - 1]) { %>,<% } %> +<% } else if (attribute.type === 'integer') { -%> + <%- toCamelCase(attribute.key) %>: map["<%- attribute.key %>"] as<% if (!attribute.required) { %>?<% } else { %>!<% } %> [Int]<% if (attribute !== collection.attributes[collection.attributes.length - 1]) { %>,<% } %> +<% } else if (attribute.type === 'float') { -%> + <%- toCamelCase(attribute.key) %>: map["<%- attribute.key %>"] as<% if (!attribute.required) { %>?<% } else { %>!<% } %> [Double]<% if (attribute !== collection.attributes[collection.attributes.length - 1]) { %>,<% } %> +<% } else if (attribute.type === 'boolean') { -%> + <%- toCamelCase(attribute.key) %>: map["<%- attribute.key %>"] as<% if (!attribute.required) { %>?<% } else { %>!<% } %> [Bool]<% if (attribute !== collection.attributes[collection.attributes.length - 1]) { %>,<% } %> +<% } else { -%> + <%- toCamelCase(attribute.key) %>: (map["<%- attribute.key %>"] as<% if (!attribute.required) { %>?<% } else { %>!<% } %> [[String: Any]])<% if (!attribute.required) { %>?<% } %>.map { <%- toPascalCase(attribute.type) %>.from(map: $0) }<% if (attribute !== collection.attributes[collection.attributes.length - 1]) { %>,<% } %> +<% } -%> +<% } else { -%> +<% if (attribute.type === 'string' || attribute.type === 'email' || attribute.type === 'datetime' || attribute.type === 'enum') { -%> + <%- toCamelCase(attribute.key) %>: map["<%- attribute.key %>"] as<% if (!attribute.required) { %>?<% } else { %>!<% } %> String<% if (attribute !== collection.attributes[collection.attributes.length - 1]) { %>,<% } %> +<% } else if (attribute.type === 'integer') { -%> + <%- toCamelCase(attribute.key) %>: map["<%- attribute.key %>"] as<% if (!attribute.required) { %>?<% } else { %>!<% } %> Int<% if (attribute !== collection.attributes[collection.attributes.length - 1]) { %>,<% } %> +<% } else if (attribute.type === 'float') { -%> + <%- toCamelCase(attribute.key) %>: map["<%- attribute.key %>"] as<% if (!attribute.required) { %>?<% } else { %>!<% } %> Double<% if (attribute !== collection.attributes[collection.attributes.length - 1]) { %>,<% } %> +<% } else if (attribute.type === 'boolean') { -%> + <%- toCamelCase(attribute.key) %>: map["<%- attribute.key %>"] as<% if (!attribute.required) { %>?<% } else { %>!<% } %> Bool<% if (attribute !== collection.attributes[collection.attributes.length - 1]) { %>,<% } %> +<% } else { -%> + <%- toCamelCase(attribute.key) %>: <%- toPascalCase(attribute.type) %>.from(map: map["<%- attribute.key %>"] as! [String: Any])<% if (attribute !== collection.attributes[collection.attributes.length - 1]) { %>,<% } %> +<% } -%> +<% } -%> +<% } -%> + ) + } +}`; + } + + getFileName(collection) { + return LanguageMeta.toPascalCase(collection.name) + ".swift"; + } +} + +module.exports = { Swift }; diff --git a/templates/cli/lib/type-generation/languages/typescript.js.twig b/templates/cli/lib/type-generation/languages/typescript.js.twig new file mode 100644 index 000000000..6c11267d2 --- /dev/null +++ b/templates/cli/lib/type-generation/languages/typescript.js.twig @@ -0,0 +1,95 @@ +/** @typedef {import('../attribute').Attribute} Attribute */ +const fs = require("fs"); +const path = require("path"); + +const { AttributeType } = require('../attribute'); +const { LanguageMeta } = require("./language"); + +class TypeScript extends LanguageMeta { + getType(attribute) { + let type = "" + switch (attribute.type) { + case AttributeType.STRING: + case AttributeType.EMAIL: + case AttributeType.DATETIME: + case AttributeType.IP: + case AttributeType.URL: + type = "string"; + if (attribute.format === AttributeType.ENUM) { + type = LanguageMeta.toPascalCase(attribute.key); + } + break; + case AttributeType.INTEGER: + type = "number"; + break; + case AttributeType.FLOAT: + type = "number"; + break; + case AttributeType.BOOLEAN: + type = "boolean"; + break; + case AttributeType.RELATIONSHIP: + type = LanguageMeta.toPascalCase(attribute.relatedCollection); + if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { + type = `${type}[]`; + } + break; + default: + throw new Error(`Unknown attribute type: ${attribute.type}`); + } + if (attribute.array) { + type += "[]"; + } + if (!attribute.required) { + type += " | null"; + } + return type; + } + + isSingleFile() { + return true; + } + + _getAppwriteDependency() { + if (fs.existsSync(path.resolve(process.cwd(), 'package.json'))) { + const packageJsonRaw = fs.readFileSync(path.resolve(process.cwd(), 'package.json')); + const packageJson = JSON.parse(packageJsonRaw.toString('utf-8')); + return packageJson.dependencies['node-appwrite'] ? 'node-appwrite' : 'appwrite'; + } + + if (fs.existsSync(path.resolve(process.cwd(), 'deno.json'))) { + return "https://deno.land/x/appwrite/mod.ts"; + } + + return "appwrite"; + } + + getTemplate() { + return `import { Models } from '${this._getAppwriteDependency()}'; + +<% for (const collection of collections) { -%> +<% for (const attribute of collection.attributes) { -%> +<% if (attribute.format === 'enum') { -%> +export enum <%- toPascalCase(attribute.key) %> { +<% for (const [index, element] of Object.entries(attribute.elements)) { -%> + <%- element.toUpperCase() %> = "<%- element %>", +<% } -%> +} +<% } -%> +<% } -%> +<% } -%> +<% for (const collection of collections) { %> +export type <%- toPascalCase(collection.name) %> = Models.Document & { +<% for (const attribute of collection.attributes) { -%> + <%- toCamelCase(attribute.key) %>: <%- getType(attribute) %>; +<% } -%> +} +<% } %>`; + } + + getFileName(_) { + return "appwrite.d.ts"; + } +} + +module.exports = { TypeScript }; diff --git a/templates/cli/package.json.twig b/templates/cli/package.json.twig index 7ab1f7767..3b307ce63 100644 --- a/templates/cli/package.json.twig +++ b/templates/cli/package.json.twig @@ -23,6 +23,7 @@ }, "dependencies": { "undici": "^5.28.2", + "ejs": "^3.1.9", "chalk": "4.1.2", "cli-progress": "^3.12.0", "cli-table3": "^0.6.2",