Skip to content

feat: include client when generating types #1123

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

Closed
wants to merge 2 commits into from
Closed
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
6 changes: 3 additions & 3 deletions example.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ function getSSLPage($url) {
}

// Leave the platform you want uncommented
$platform = 'client';
// $platform = 'console';
// $platform = 'client';
$platform = 'console';
// $platform = 'server';

$version = '1.8.x';
$version = '1.7.x';
$spec = getSSLPage("https://raw.githubusercontent.com/appwrite/appwrite/{$version}/app/config/specs/swagger2-{$version}-{$platform}.json");

if(empty($spec)) {
Expand Down
5 changes: 5 additions & 0 deletions src/SDK/Language/CLI.php
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,11 @@ public function getFiles(): array
'destination' => 'lib/type-generation/languages/dart.js',
'template' => 'cli/lib/type-generation/languages/dart.js.twig',
],
[
'scope' => 'default',
'destination' => 'lib/client-generation/languages/typescript.js',
'template' => 'cli/lib/client-generation/languages/typescript.js.twig',
],
[
'scope' => 'default',
'destination' => 'lib/questions.js',
Expand Down
65 changes: 65 additions & 0 deletions templates/cli/lib/client-generation/languages/typescript.js.twig
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/** @typedef {import('../../type-generation/attribute').Attribute} Attribute */
const fs = require("fs");
const path = require("path");

const { LanguageMeta } = require("../../type-generation/languages/language");

class TypeScriptClient {
constructor() {
// Empty constructor
}

_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";
}

getFileName() {
return "appwrite.db.ts";
}

getTemplate() {
return `import { Client, Databases, type Models } from '${this._getAppwriteDependency()}';
<% const typeNames = collections.map(c => toPascalCase(c.name)); -%>
<% const importPath = typesFileName.replace(/\.d\.ts$/, '').replace(/\.ts$/, ''); -%>
import type { <%- typeNames.join(', ') %> } from './<%- importPath %>';

// This file is auto-generated by the Appwrite CLI.
// You can regenerate it by running \`appwrite ${process.argv.slice(2).join(' ')}\`.

const client = new Client();
const databases = new Databases(client);

client
.setEndpoint('<%- endpoint %>')
.setProject('<%- projectId %>');

export const db = {
<% for (const collection of collections) { -%>
<%- strict ? toCamelCase(collection.name) : collection.name %>: {
create: (data: <%- toPascalCase(collection.name) %>) =>
databases.createDocument<<%- toPascalCase(collection.name) %>>('<%- collection.databaseId %>', '<%- collection.$id %>', 'unique()', data),
get: (id: string) =>
databases.getDocument<<%- toPascalCase(collection.name) %>>('<%- collection.databaseId %>', '<%- collection.$id %>', id),
update: (id: string, data: Partial<<%- toPascalCase(collection.name) %>>) =>
databases.updateDocument<<%- toPascalCase(collection.name) %>>('<%- collection.databaseId %>', '<%- collection.$id %>', id, data),
delete: (id: string) =>
databases.deleteDocument('<%- collection.databaseId %>', '<%- collection.$id %>', id),
list: (queries?: string[]) =>
databases.listDocuments<<%- toPascalCase(collection.name) %>>('<%- collection.databaseId %>', '<%- collection.$id %>', queries),
},
<% } -%>
};`;
}
}

module.exports = { TypeScriptClient };
6 changes: 3 additions & 3 deletions templates/cli/lib/commands/command.js.twig
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const libClient = require('../client.js');
const { getAllFiles, showConsoleLink } = require('../utils.js');
const { Command } = require('commander');
const { sdkForProject, sdkForConsole } = require('../sdks')
const { parse, actionRunner, parseInteger, parseBool, commandDescriptions, success, log } = require('../parser')
const { parse, actionRunner, parseInteger, parseBool, commandDescriptions, success, log, warn } = require('../parser')
const { localConfig, globalConfig } = require("../config");
const { File } = require('undici');
const { ReadableStream } = require('stream/web');
Expand Down Expand Up @@ -74,9 +74,9 @@ const {{ service.name | caseLower }}{{ method.name | caseUcfirst }} = async ({
}) => {
{% if method.deprecated %}
{% if method.since and method.replaceWith %}
console.warn('Warning: This command is deprecated since {{ method.since }}.{% if method.replaceWith %} Please use "{{ method.replaceWith | replace({'.': ' '}) | caseKebab }}" instead.{% endif %}');
warn('Warning: This command is deprecated since {{ method.since }}.{% if method.replaceWith %} Please use "{{ method.replaceWith | replace({'.': ' '}) | caseKebab }}" instead.{% endif %}');
{% else %}
console.warn('Warning: This command is deprecated.');
warn('Warning: This command is deprecated.');
{% endif %}
{% endif %}
let client = !sdk ? await {% if service.name == "projects" %}sdkForConsole(){% else %}sdkForProject(){% endif %} :
Expand Down
48 changes: 42 additions & 6 deletions templates/cli/lib/commands/types.js.twig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ 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");
const { TypeScriptClient } = require("../client-generation/languages/typescript");

/**
* @param {string} language
Expand Down Expand Up @@ -64,16 +65,30 @@ const typesStrictOption = new Option(
)
.default(false);

const typesCommand = actionRunner(async (rawOutputDirectory, {language, strict}) => {
const typesIncludeClientOption = new Option(
"-c, --include-client",
"Generate a Prisma-style client file (TypeScript only)"
)
.default(false);

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

if (includeClient && language !== "ts") {
throw new Error("Client generation is only supported for TypeScript. Please use --language ts or ensure TypeScript is auto-detected.");
}

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

if (includeClient) {
log("Client generation enabled: Will generate appwrite.db.ts file");
}

const meta = createLanguageMeta(language);

const rawOutputPath = rawOutputDirectory;
Expand All @@ -97,13 +112,10 @@ const typesCommand = actionRunner(async (rawOutputDirectory, {language, strict})
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");
const configFileName = path.basename(localConfig.path);
throw new Error(`No collections found in configuration. Make sure ${configFileName} exists and contains collections.`);
}

log(`Found ${collections.length} collections: ${collections.map(c => c.name).join(", ")}`);
Expand All @@ -125,6 +137,24 @@ const typesCommand = actionRunner(async (rawOutputDirectory, {language, strict})

fs.writeFileSync(destination, content);
log(`Added types to ${destination}`);

// Generate client file if requested and language is TypeScript
if (includeClient && language === "ts") {
const clientMeta = new TypeScriptClient();
const clientTemplate = ejs.compile(clientMeta.getTemplate());
const clientContent = clientTemplate({
collections,
strict,
projectId: localConfig.getProject()['projectId'],
endpoint: localConfig.getEndpoint(),
...templateHelpers,
typesFileName: meta.getFileName(),
});

const clientDestination = path.join(outputDirectory, clientMeta.getFileName());
fs.writeFileSync(clientDestination, clientContent);
log(`Added client to ${clientDestination}`);
}
} else {
for (const collection of collections) {
const content = templater({
Expand All @@ -140,6 +170,11 @@ const typesCommand = actionRunner(async (rawOutputDirectory, {language, strict})
fs.writeFileSync(destination, content);
log(`Added types for ${collection.name} to ${destination}`);
}

// Client generation for multi-file languages not supported yet
if (includeClient) {
warn("Client generation for multi-file languages is not yet supported");
}
}

success(`Generated types for all the listed collections`);
Expand All @@ -150,6 +185,7 @@ const types = new Command("types")
.addArgument(typesOutputArgument)
.addOption(typesLanguageOption)
.addOption(typesStrictOption)
.addOption(typesIncludeClientOption)
.action(actionRunner(typesCommand));

module.exports = { types };