Skip to content

chore: type generation improvements #1102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jul 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion templates/cli/lib/commands/types.js.twig
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,22 @@ const typesLanguageOption = new Option(
.choices(["auto", "ts", "js", "php", "kotlin", "swift", "java", "dart"])
.default("auto");

const typesCommand = actionRunner(async (rawOutputDirectory, {language}) => {
const typesStrictOption = new Option(
"-s, --strict",
"Enable strict mode to automatically convert field names to follow language conventions"
)
.default(false);

const typesCommand = actionRunner(async (rawOutputDirectory, {language, strict}) => {
if (language === "auto") {
language = detectLanguage();
log(`Detected language: ${language}`);
}

if (strict) {
log(`Strict mode enabled: Field names will be converted to follow ${language} conventions`);
}

const meta = createLanguageMeta(language);

const rawOutputPath = rawOutputDirectory;
Expand Down Expand Up @@ -106,6 +116,7 @@ const typesCommand = actionRunner(async (rawOutputDirectory, {language}) => {
if (meta.isSingleFile()) {
const content = templater({
collections,
strict,
...templateHelpers,
getType: meta.getType
});
Expand All @@ -118,6 +129,7 @@ const typesCommand = actionRunner(async (rawOutputDirectory, {language}) => {
for (const collection of collections) {
const content = templater({
collection,
strict,
...templateHelpers,
getType: meta.getType
});
Expand All @@ -136,6 +148,7 @@ const types = new Command("types")
.description("Generate types for your Appwrite project")
.addArgument(typesOutputArgument)
.addOption(typesLanguageOption)
.addOption(typesStrictOption)
.action(actionRunner(typesCommand));

module.exports = { types };
86 changes: 71 additions & 15 deletions templates/cli/lib/type-generation/languages/dart.js.twig
Original file line number Diff line number Diff line change
@@ -1,8 +1,45 @@
/** @typedef {import('../attribute').Attribute} Attribute */
const { AttributeType } = require('../attribute');
const { LanguageMeta } = require("./language");
const fs = require('fs');
const path = require('path');

class Dart extends LanguageMeta {
getPackageName() {
const pubspecPath = path.join(this.getCurrentDirectory(), 'pubspec.yaml');
if (fs.existsSync(pubspecPath)) {
const pubspecContent = fs.readFileSync(pubspecPath, 'utf8');
const lines = pubspecContent.split('\n');

const dependenciesIndex = lines.findIndex(line => line.trim() === 'dependencies:');

if (dependenciesIndex !== -1) {
const indent = lines[dependenciesIndex].search(/\S|$/);
const dependencies = [];
for (let i = dependenciesIndex + 1; i < lines.length; i++) {
const line = lines[i];
if (line.trim() === '') continue;

const lineIndent = line.search(/\S|$/);
if (lineIndent <= indent && line.trim() !== '') {
break;
}

dependencies.push(line.trim());
}

if (dependencies.some(dep => dep.startsWith('dart_appwrite:'))) {
return 'dart_appwrite';
}
if (dependencies.some(dep => dep.startsWith('appwrite:'))) {
return 'appwrite';
}
}
}

return 'appwrite';
}

getType(attribute) {
let type = "";
switch (attribute.type) {
Expand Down Expand Up @@ -46,42 +83,55 @@ class Dart extends LanguageMeta {
}

getTemplate() {
return `<% for (const attribute of collection.attributes) { -%>
return `import 'package:${this.getPackageName()}/models.dart';
<% for (const attribute of collection.attributes) { -%>
<% if (attribute.type === 'relationship') { -%>
import '<%- attribute.relatedCollection.toLowerCase() %>.dart';

<% } -%>
<% } -%>
/**
* This file is auto-generated by the Appwrite CLI.
* You can regenerate it by running \`appwrite types -l dart ${this.getCurrentDirectory()}\`.
*/
/// This file is auto-generated by the Appwrite CLI.
/// You can regenerate it by running \`appwrite types -l dart ${this.getCurrentDirectory()}\`.

<% for (const attribute of collection.attributes) { -%>
<% if (attribute.format === 'enum') { -%>
enum <%- toPascalCase(attribute.key) %> {
<% for (const [index, element] of Object.entries(attribute.elements)) { -%>
<%- toSnakeCase(element) %><% if (index < attribute.elements.length - 1) { %>,<% } %>
<%- strict ? toCamelCase(element) : element %><% if (index < attribute.elements.length - 1) { %>,<% } %>
<% } -%>
}

<% } -%>
<% } -%>
class <%= toPascalCase(collection.name) %> {
class <%= toPascalCase(collection.name) %> extends Document {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to call super() in the constructor?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dont think so, this is how it will be called manually:
Screenshot 2025-07-09 at 3 05 08 PM

but people will usually just use fromMap

<% for (const [index, attribute] of Object.entries(collection.attributes)) { -%>
<%- getType(attribute) %> <%= toCamelCase(attribute.key) %>;
<%- getType(attribute) %> <%= strict ? toCamelCase(attribute.key) : attribute.key %>;
<% } -%>

<%= toPascalCase(collection.name) %>({
required super.$id,
required super.$collectionId,
required super.$databaseId,
required super.$createdAt,
required super.$updatedAt,
required super.$permissions,
required super.data,
<% for (const [index, attribute] of Object.entries(collection.attributes)) { -%>
<% if (attribute.required) { %>required <% } %>this.<%= toCamelCase(attribute.key) %><% if (index < collection.attributes.length - 1) { %>,<% } %>
<% if (attribute.required) { %>required <% } %>this.<%= strict ? toCamelCase(attribute.key) : attribute.key %><% if (index < collection.attributes.length - 1) { %>,<% } %>
<% } -%>
});

factory <%= toPascalCase(collection.name) %>.fromMap(Map<String, dynamic> map) {
return <%= toPascalCase(collection.name) %>(
$id: map['\\$id'].toString(),
$collectionId: map['\\$collectionId'].toString(),
$databaseId: map['\\$databaseId'].toString(),
$createdAt: map['\\$createdAt'].toString(),
$updatedAt: map['\\$updatedAt'].toString(),
$permissions: List<String>.from(map['\\$permissions'] ?? []),
data: map,
<% for (const [index, attribute] of Object.entries(collection.attributes)) { -%>
<%= toCamelCase(attribute.key) %>: <% if (attribute.type === 'string' || attribute.type === 'email' || attribute.type === 'datetime') { -%>
<%= strict ? toCamelCase(attribute.key) : attribute.key %>: <% if (attribute.type === 'string' || attribute.type === 'email' || attribute.type === 'datetime') { -%>
<% if (attribute.format === 'enum') { -%>
<% if (attribute.array) { -%>
(map['<%= attribute.key %>'] as List<dynamic>?)?.map((e) => <%- toPascalCase(attribute.key) %>.values.firstWhere((element) => element.name == e)).toList()<% if (!attribute.required) { %> ?? []<% } -%>
Expand Down Expand Up @@ -130,21 +180,27 @@ map['<%= attribute.key %>'] != null ? <%- toPascalCase(attribute.relatedCollecti

Map<String, dynamic> toMap() {
return {
"\\$id": $id,
"\\$collectionId": $collectionId,
"\\$databaseId": $databaseId,
"\\$createdAt": $createdAt,
"\\$updatedAt": $updatedAt,
"\\$permissions": $permissions,
<% 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) { %> ?? []<% } -%>
<%= strict ? toCamelCase(attribute.key) : attribute.key %><% if (!attribute.required) { %>?<% } %>.map((e) => e.toMap()).toList()<% if (!attribute.required) { %> ?? []<% } -%>
<% } else { -%>
<%= toCamelCase(attribute.key) %><% if (!attribute.required) { %>?<% } %>.toMap()<% if (!attribute.required) { %> ?? {}<% } -%>
<%= strict ? toCamelCase(attribute.key) : 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) { %> ?? []<% } -%>
<%= strict ? toCamelCase(attribute.key) : 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<% } -%>
<%= strict ? toCamelCase(attribute.key) : attribute.key %><% if (!attribute.required) { %>?<% } %>.name<% if (!attribute.required) { %> ?? null<% } -%>
<% } -%>
<% } else { -%>
<%= toCamelCase(attribute.key) -%>
<%= strict ? toCamelCase(attribute.key) : attribute.key -%>
<% } -%><% if (index < collection.attributes.length - 1) { %>,<% } %>
<% } -%>
};
Expand Down
72 changes: 36 additions & 36 deletions templates/cli/lib/type-generation/languages/java.js.twig
Original file line number Diff line number Diff line change
Expand Up @@ -61,63 +61,63 @@ public class <%- toPascalCase(collection.name) %> {
<% for (const attribute of collection.attributes) { -%>
<% if (attribute.format === 'enum') { -%>

public enum <%- toPascalCase(attribute.key) %> {
public enum <%- toPascalCase(attribute.key) %> {
<% for (const [index, element] of Object.entries(attribute.elements)) { -%>
<%- toSnakeCase(element) %><%- index < attribute.elements.length - 1 ? ',' : ';' %>
<%- strict ? toSnakeCase(element) : element %><%- index < attribute.elements.length - 1 ? ',' : ';' %>
<% } -%>
}
}

<% } -%>
<% } -%>
<% for (const attribute of collection.attributes) { -%>
private <%- getType(attribute) %> <%- toCamelCase(attribute.key) %>;
private <%- getType(attribute) %> <%- strict ? toCamelCase(attribute.key) : attribute.key %>;
<% } -%>

public <%- toPascalCase(collection.name) %>() {
}
public <%- toPascalCase(collection.name) %>() {
}

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 ? ',' : '' %>
<%- getType(attribute) %> <%= strict ? toCamelCase(attribute.key) : attribute.key %><%- index < collection.attributes.length - 1 ? ',' : '' %>
<% } -%>
) {
) {
<% for (const attribute of collection.attributes) { -%>
this.<%= toCamelCase(attribute.key) %> = <%= toCamelCase(attribute.key) %>;
this.<%= strict ? toCamelCase(attribute.key) : attribute.key %> = <%= strict ? toCamelCase(attribute.key) : attribute.key %>;
<% } -%>
}
}

<% for (const attribute of collection.attributes) { -%>
public <%- getType(attribute) %> get<%- toPascalCase(attribute.key) %>() {
return <%= toCamelCase(attribute.key) %>;
}
public <%- getType(attribute) %> get<%- toPascalCase(attribute.key) %>() {
return <%= strict ? toCamelCase(attribute.key) : attribute.key %>;
}

public void set<%- toPascalCase(attribute.key) %>(<%- getType(attribute) %> <%= toCamelCase(attribute.key) %>) {
this.<%= toCamelCase(attribute.key) %> = <%= toCamelCase(attribute.key) %>;
}
public void set<%- toPascalCase(attribute.key) %>(<%- getType(attribute) %> <%= strict ? toCamelCase(attribute.key) : attribute.key %>) {
this.<%= strict ? toCamelCase(attribute.key) : attribute.key %> = <%= strict ? toCamelCase(attribute.key) : 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 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 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) %> +
@Override
public String toString() {
return "<%- toPascalCase(collection.name) %>{" +
<% for (const attribute of collection.attributes) { -%>
"<%= strict ? toCamelCase(attribute.key) : attribute.key %>=" + <%= strict ? toCamelCase(attribute.key) : attribute.key %> +
<% } -%>
'}';
}
'}';
}
}
`;
}
Expand Down
29 changes: 20 additions & 9 deletions templates/cli/lib/type-generation/languages/javascript.js.twig
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class JavaScript extends LanguageMeta {
case AttributeType.URL:
type = "string";
if (attribute.format === AttributeType.ENUM) {
type = `"${attribute.elements.join('"|"')}"`;
type = LanguageMeta.toPascalCase(attribute.key);
}
break;
case AttributeType.INTEGER:
Expand All @@ -31,7 +31,7 @@ class JavaScript extends LanguageMeta {
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}>`;
type = `${type}[]`;
}
break;
default:
Expand Down Expand Up @@ -65,21 +65,32 @@ class JavaScript extends LanguageMeta {
}

getTemplate() {
return `/**
return `
// This file is auto-generated by the Appwrite CLI.
// You can regenerate it by running \`appwrite types -l js ${this.getCurrentDirectory()}\`.

/**
* @typedef {import('${this._getAppwriteDependency()}').Models.Document} Document
*/

<% for (const collection of collections) { -%>
<% for (const attribute of collection.attributes) { -%>
<% if (attribute.format === 'enum') { -%>
/**
* This file is auto-generated by the Appwrite CLI.
* You can regenerate it by running \`appwrite types -l js ${this.getCurrentDirectory()}\`.
* @typedef {"<%- attribute.elements.join('"|"') %>"} <%- toPascalCase(attribute.key) %>
*/
<% for (const collection of collections) { %>
/**
* @typedef {Object} <%- toPascalCase(collection.name) %>

<% } -%>
<% } -%>
<% } -%>
<% for (const collection of collections) { %>/**
* @typedef {Document & {
<% for (const attribute of collection.attributes) { -%>
* @property {<%- getType(attribute) %>} <%- toCamelCase(attribute.key) %>
* <%- strict ? toCamelCase(attribute.key) : attribute.key %>: <%- getType(attribute) %>;
<% } -%>
* }} <%- toPascalCase(collection.name) %>
*/

<% } %>`;
}

Expand Down
4 changes: 2 additions & 2 deletions templates/cli/lib/type-generation/languages/kotlin.js.twig
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,15 @@ import <%- toPascalCase(attribute.relatedCollection) %>
<% if (attribute.format === 'enum') { -%>
enum class <%- toPascalCase(attribute.key) %> {
<% for (const [index, element] of Object.entries(attribute.elements)) { -%>
<%- toUpperSnakeCase(element) %><%- index < attribute.elements.length - 1 ? ',' : '' %>
<%- strict ? toUpperSnakeCase(element) : element %><%- index < attribute.elements.length - 1 ? ',' : '' %>
<% } -%>
}

<% } -%>
<% } -%>
data class <%- toPascalCase(collection.name) %>(
<% for (const [index, attribute] of Object.entries(collection.attributes)) { -%>
val <%- toCamelCase(attribute.key) %>: <%- getType(attribute) %><% if (index < collection.attributes.length - 1) { %>,<% } %>
val <%- strict ? toCamelCase(attribute.key) : attribute.key %>: <%- getType(attribute) %><% if (index < collection.attributes.length - 1) { %>,<% } %>
<% } -%>
)`;
}
Expand Down
Loading