From 51ccec0fb006a57ac29aee29bd9b4924f230cfde Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 25 Jul 2025 11:33:48 +0530 Subject: [PATCH 1/2] feat: include client when generating types --- example.php | 6 +- src/SDK/Language/CLI.php | 5 ++ .../languages/typescript.js.twig | 65 +++++++++++++++++++ templates/cli/lib/commands/command.js.twig | 6 +- templates/cli/lib/commands/types.js.twig | 48 ++++++++++++-- 5 files changed, 118 insertions(+), 12 deletions(-) create mode 100644 templates/cli/lib/client-generation/languages/typescript.js.twig diff --git a/example.php b/example.php index 164ef6dad..9eafe7b9a 100644 --- a/example.php +++ b/example.php @@ -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)) { diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index f331a6a31..ab3da9f43 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -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', diff --git a/templates/cli/lib/client-generation/languages/typescript.js.twig b/templates/cli/lib/client-generation/languages/typescript.js.twig new file mode 100644 index 000000000..206836884 --- /dev/null +++ b/templates/cli/lib/client-generation/languages/typescript.js.twig @@ -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: Omit<<%- toPascalCase(collection.name) %>, keyof Models.Document>) => + databases.createDocument('<%- collection.databaseId %>', '<%- collection.$id %>', 'unique()', data), + get: (id: string) => + databases.getDocument<<%- toPascalCase(collection.name) %>>('<%- collection.databaseId %>', '<%- collection.$id %>', id), + update: (id: string, data: Partial, keyof Models.Document>>) => + 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 }; diff --git a/templates/cli/lib/commands/command.js.twig b/templates/cli/lib/commands/command.js.twig index 2b10c2f64..27edf1d50 100644 --- a/templates/cli/lib/commands/command.js.twig +++ b/templates/cli/lib/commands/command.js.twig @@ -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'); @@ -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 %} : diff --git a/templates/cli/lib/commands/types.js.twig b/templates/cli/lib/commands/types.js.twig index 19de8405a..97ca2e545 100644 --- a/templates/cli/lib/commands/types.js.twig +++ b/templates/cli/lib/commands/types.js.twig @@ -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 @@ -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; @@ -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(", ")}`); @@ -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({ @@ -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`); @@ -150,6 +185,7 @@ const types = new Command("types") .addArgument(typesOutputArgument) .addOption(typesLanguageOption) .addOption(typesStrictOption) + .addOption(typesIncludeClientOption) .action(actionRunner(typesCommand)); module.exports = { types }; From 74718058c36841f3ec9c6cb733d7daed326891b5 Mon Sep 17 00:00:00 2001 From: Chirag Aggarwal Date: Fri, 25 Jul 2025 12:51:19 +0530 Subject: [PATCH 2/2] fix: typing --- .../cli/lib/client-generation/languages/typescript.js.twig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/cli/lib/client-generation/languages/typescript.js.twig b/templates/cli/lib/client-generation/languages/typescript.js.twig index 206836884..efdb463a1 100644 --- a/templates/cli/lib/client-generation/languages/typescript.js.twig +++ b/templates/cli/lib/client-generation/languages/typescript.js.twig @@ -46,11 +46,11 @@ client export const db = { <% for (const collection of collections) { -%> <%- strict ? toCamelCase(collection.name) : collection.name %>: { - create: (data: Omit<<%- toPascalCase(collection.name) %>, keyof Models.Document>) => - databases.createDocument('<%- collection.databaseId %>', '<%- collection.$id %>', 'unique()', data), + 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, keyof Models.Document>>) => + 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),