Skip to content
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

feat: types generator command #784

Open
wants to merge 19 commits into
base: master
Choose a base branch
from
Open
50 changes: 50 additions & 0 deletions src/SDK/Language/CLI.php
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,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/collection.js',
'template' => 'cli/lib/type-generation/collection.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/python.js',
'template' => 'cli/lib/type-generation/languages/python.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/questions.js',
Expand Down Expand Up @@ -176,6 +221,11 @@ public function getFiles(): array
'scope' => 'default',
'destination' => 'lib/commands/generic.js',
'template' => 'cli/lib/commands/generic.js.twig',
],
[
'scope' => 'default',
'destination' => 'lib/commands/types.js',
'template' => 'cli/lib/commands/types.js.twig',
]
];
}
Expand Down
70 changes: 69 additions & 1 deletion src/SDK/Language/Python.php
Original file line number Diff line number Diff line change
Expand Up @@ -200,13 +200,18 @@ public function getFiles(): array
'destination' => '.travis.yml',
'template' => 'python/.travis.yml.twig',
],
[
'scope' => 'definition',
'destination' => '{{ spec.title | caseSnake}}/models/{{ definition.name | caseSnake }}.py',
'template' => 'python/package/models/model.py.twig',
],
[
'scope' => 'enum',
'destination' => '{{ spec.title | caseSnake}}/enums/{{ enum.name | caseSnake }}.py',
'template' => 'python/package/enums/enum.py.twig',
],
];
}
}

/**
* @param array $parameter
Expand Down Expand Up @@ -344,12 +349,75 @@ public function getParamExample(array $param): string
return $output;
}

protected function getPropertyType(array $property, array $spec, ?string $generic = 'T'): string
{
if (\array_key_exists('sub_schema', $property)) {
$type = $this->toPascalCase($property['sub_schema']);

if ($this->hasGenericType($property['sub_schema'], $spec)) {
$type .= '[' . $generic . ']';
}

if ($property['type'] === 'array') {
$type = 'List[' . $type . ']';
}
} else {
$type = $this->getTypeName($property);
}

if (!$property['required']) {
$type = 'Optional[' . $type . '] = None';
}

return $type;
}

protected function hasGenericType(?string $model, array $spec): string
{
if (empty($model) || $model === 'any') {
return false;
}

$model = $spec['definitions'][$model];

if ($model['additionalProperties']) {
return true;
}

foreach ($model['properties'] as $property) {
if (!\array_key_exists('sub_schema', $property) || !$property['sub_schema']) {
continue;
}

return $this->hasGenericType($property['sub_schema'], $spec);
}

return false;
}

protected function getModelType(array $definition, array $spec, ?string $generic = 'T'): string
{
if ($this->hasGenericType($definition['name'], $spec)) {
return $this->toPascalCase($definition['name']) . '(Generic[' . $generic . '])';
}
return $this->toPascalCase($definition['name']);
}

public function getFilters(): array
{
return [
new TwigFilter('caseEnumKey', function (string $value) {
return $this->toUpperSnakeCase($value);
}),
new TwigFilter('hasGenericType', function (string $model, array $spec) {
return $this->hasGenericType($model, $spec);
}),
new TwigFilter('modelType', function (array $definition, array $spec, ?string $generic = 'T') {
return $this->getModelType($definition, $spec, $generic);
}),
new TwigFilter('propertyType', function (array $property, array $spec, ?string $generic = 'T') {
return $this->getPropertyType($property, $spec, $generic);
}),
];
}
}
2 changes: 2 additions & 0 deletions templates/cli/index.js.twig
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const { client } = require("./lib/commands/generic");
{% if sdk.test != "true" %}
const { login, logout } = require("./lib/commands/generic");
const { init } = require("./lib/commands/init");
const { types } = require("./lib/commands/types");
const { deploy } = require("./lib/commands/deploy");
{% endif %}
{% for service in spec.services %}
Expand All @@ -38,6 +39,7 @@ program
{% if sdk.test != "true" %}
.addCommand(login)
.addCommand(init)
.addCommand(types)
.addCommand(deploy)
.addCommand(logout)
{% endif %}
Expand Down
123 changes: 123 additions & 0 deletions templates/cli/lib/commands/types.js.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
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 { Python } = require("../type-generation/languages/python");
const { Kotlin } = require("../type-generation/languages/kotlin");
const { Swift } = require("../type-generation/languages/swift");
const { Java } = require("../type-generation/languages/java");

/**
* @param {string} language
* @returns {import("../type-generation/languages/language").LanguageMeta}
*/
function createLanguageMeta(language) {
switch (language) {
case "ts":
return new TypeScript();
case "php":
return new PHP();
case "python":
return new Python();
case "kotlin":
return new Kotlin();
case "swift":
return new Swift();
case "java":
return new Java();
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(
"<output-directory>",
"The directory to write the types to"
);

const typesLanguageOption = new Option(
"-l, --language <language>",
"The language of the types"
)
.choices(["ts", "php", "python", "kotlin", "swift", "java"])
.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();
});

const types = new Command("types")
.description("Generate types for your Appwrite project")
.addArgument(typesOutputArgument)
.addOption(typesLanguageOption)
.action(actionRunner(typesCommand));

module.exports = { types };
27 changes: 27 additions & 0 deletions templates/cli/lib/type-generation/attribute.js.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const AttributeType = {
STRING: "string",
INTEGER: "integer",
FLOAT: "float",
BOOLEAN: "boolean",
DATETIME: "datetime",
EMAIL: "email",
IP: "ip",
URL: "url",
ENUM: "enum",
RELATIONSHIP: "relationship",
};

/**
* @typedef {Object} Attribute
* @property {string} key - The unique identifier of the attribute.
* @property {"string"|"integer"|"float"|"boolean"|"datetime"|"email"|"ip"|"url"|"enum"|"relationship"} type - The type of the attribute.
* @property {string} status - The status of the attribute.
* @property {boolean} required - The required status of the attribute.
* @property {boolean} array - The array status of the attribute.
* @property {number} size - The size of the attribute.
* @property {string} default - The default value of the attribute.
*/

module.exports = {
AttributeType,
};
13 changes: 13 additions & 0 deletions templates/cli/lib/type-generation/collection.js.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Represents a collection within a database.
*
* @typedef {Object} Collection
* @property {string} $id - The unique identifier of the collection.
* @property {string[]} $permissions - The permissions for accessing the collection.
* @property {string} databaseId - The identifier of the database this collection belongs to.
* @property {string} name - The name of the collection.
* @property {boolean} enabled - Indicates if the collection is enabled.
* @property {boolean} documentSecurity - Indicates if document-level security is enabled for the collection.
* @property {import('./attribute.js).Attribute[]} attributes - The attributes (fields) defined in the collection.
* @property {unknown[]} indexes - The indexes defined on the collection for optimized query performance.
*/
Loading
Loading