From fc4254ea4a38862a00abf753b4e004065fcd306b Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Wed, 7 Jun 2023 04:53:45 +0000 Subject: [PATCH 01/13] update with flutter extension files and changes --- src/SDK/Language/CLI.php | 15 + templates/cli/flutter_res/appwrite.dart.twig | 9 + .../cli/flutter_res/template_model.dart.twig | 50 ++ templates/cli/index.js.twig | 2 + templates/cli/lib/commands/flutter.js.twig | 537 ++++++++++++++++++ templates/cli/lib/questions.js.twig | 152 ++++- templates/cli/lib/utils.js.twig | 21 +- 7 files changed, 784 insertions(+), 2 deletions(-) create mode 100644 templates/cli/flutter_res/appwrite.dart.twig create mode 100644 templates/cli/flutter_res/template_model.dart.twig create mode 100644 templates/cli/lib/commands/flutter.js.twig diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index 89f17ad17..dc827bd97 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -156,6 +156,21 @@ public function getFiles(): array 'destination' => 'lib/commands/deploy.js', 'template' => 'cli/lib/commands/deploy.js.twig', ], + [ + 'scope' => 'default', + 'destination' => 'lib/commands/flutter.js', + 'template' => 'cli/lib/commands/flutter.js.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'flutter_res/apwrite.dart', + 'template' => 'cli/flutter_res/appwrite.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'flutter_res/template_model.dart', + 'template' => 'cli/flutter_res/template_model.dart.twig', + ], [ 'scope' => 'service', 'destination' => '/lib/commands/{{service.name | caseDash}}.js', diff --git a/templates/cli/flutter_res/appwrite.dart.twig b/templates/cli/flutter_res/appwrite.dart.twig new file mode 100644 index 000000000..dddf660d1 --- /dev/null +++ b/templates/cli/flutter_res/appwrite.dart.twig @@ -0,0 +1,9 @@ +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('{ENDPOINT}') + .setProject('{PROJECT}'); + +final account = Account(client); +final databases = Databases(client); +final storage = Storage(client); \ No newline at end of file diff --git a/templates/cli/flutter_res/template_model.dart.twig b/templates/cli/flutter_res/template_model.dart.twig new file mode 100644 index 000000000..6cb5cf6a6 --- /dev/null +++ b/templates/cli/flutter_res/template_model.dart.twig @@ -0,0 +1,50 @@ +%IMPORTS% +class %NAME% { + final String $id; + final String $createdAt; + final String $updatedAt; + final String $collectionId; + final String $databaseId; + final List $permissions; + + %ATTRIBUTES% + + %NAME%({ + required this.$id, + required this.$createdAt, + required this.$updatedAt, + required this.$collectionId, + required this.$permissions, + required this.$databaseId, + %CONSTRUCTOR_PARAMETERS% + }); + + factory %NAME%.fromMap(Map map) { + return %NAME%( + $id: map['\$id'], + $createdAt: map['\$createdAt'], + $updatedAt: map['\$updatedAt'], + $collectionId: map['\$collectionId'], + $permissions: List.from(map['\$permissions']), + $databaseId: map['\$databaseId'], + %CONSTRUCTOR_ARGUMENTS% + ); + } + + Map toMap() { + return { + '\$id': $id, + '\$createdAt': $createdAt, + '\$updatedAt': $updatedAt, + '\$collectionId': $collectionId, + '\$permissions': $permissions, + '\$databaseId': $databaseId, + %MAP_FIELDS% + }; + } + + @override + String toString() { + return '%NAME% ' + toMap().toString(); + } +} \ No newline at end of file diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index d4415db72..69fc57666 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -10,6 +10,7 @@ const chalk = require("chalk"); const { version } = require("./package.json"); const { commandDescriptions, cliConfig } = require("./lib/parser"); const { client } = require("./lib/commands/generic"); +const { flutter } = require("./lib/commands/flutter"); {% if sdk.test != "true" %} const { login, logout } = require("./lib/commands/generic"); const { init } = require("./lib/commands/init"); @@ -45,6 +46,7 @@ program .addCommand({{ service.name | caseLower }}) {% endfor %} .addCommand(client) + .addCommand(flutter) .parse(process.argv); process.stdout.columns = oldWidth; \ No newline at end of file diff --git a/templates/cli/lib/commands/flutter.js.twig b/templates/cli/lib/commands/flutter.js.twig new file mode 100644 index 000000000..ff1a65171 --- /dev/null +++ b/templates/cli/lib/commands/flutter.js.twig @@ -0,0 +1,537 @@ +const fs = require("fs"); +const path = require("path"); +const inquirer = require("inquirer"); +const { teamsCreate } = require("./teams"); +const { projectsCreate, projectsCreatePlatform, projectsListPlatforms } = require("./projects"); +const { sdkForConsole } = require("../sdks"); +const { questionsFlutterConfigure, questionsFlutterSelectPlatforms, questionsFlutterChooseDatabase, questionsFlutterChooseProject } = require("../questions"); +const { success, log, actionRunner, error } = require("../parser"); +const { Command } = require("commander"); +const { globalConfig, localConfig } = require("../config"); +const { databasesList, databasesListCollections } = require("./databases"); +const { sdkForProject } = require("../sdks"); +const { toSnakeCase, toUpperCamelCase } = require("../utils"); + +const flutter = new Command("flutter") + .description("Configure Flutter project to use Appwrite") + .configureHelp({ + helpWidth: process.stdout.columns || 80 + }) + .action(actionRunner(async (_options, command) => { + command.help(); + })); + +const generate = async (options) => { + let modelPath = options.modelPath ?? './lib/models/'; + if (!modelPath.endsWith('/')) { + modelPath += '/'; + } + let projectId = options.projectId ?? localConfig.getProject().projectId; + let databaseIds = options.databaseIds?.split(','); + + if (!projectId) { + let answer = await inquirer.prompt(questionsFlutterChooseProject); + if (!answer.project) { + error('You must select a project.'); + return; + } + projectId = answer.project.id; + localConfig.setProject(projectId, answer.project.name); + } + + + if (!databaseIds) { + answer = await inquirer.prompt(questionsFlutterChooseDatabase); + + if (!answer.databases.length) { + error('Please select at least one database'); + return; + } + databaseIds = answer.databases.map(database => database.id); + } + + const sdk = await sdkForProject(); + for (let index in databaseIds) { + let id = databaseIds[index]; + let response = await databasesListCollections({ + databaseId: id, + sdk, + parseOutput: false + }); + + let collections = response.collections; + + for (let index in collections) { + const collection = collections[index]; + const filename = toSnakeCase(collection.$id) + '.dart'; + const className = toUpperCamelCase(collection.$id); + + let template = fs.readFileSync(`${__dirname}/../../flutter_res/template_model.dart`, 'utf8'); + let dartClass = generateDartClass(className, collection.attributes, template); + if (!fs.existsSync(modelPath)) { + fs.mkdirSync(modelPath, { recursive: true }); + } + fs.writeFileSync(modelPath + filename, dartClass); + success(`Generated ${className} class and saved to ${modelPath + filename}`); + } + } +} + + +function generateDartClass(name, attributes, template) { + let imports = ''; + + const getType = (attribute) => { + switch (attribute.type) { + case 'string': + case 'email': + case 'url': + case 'enum': + case 'datetime': + return attribute.array ? 'List' : 'String'; + case 'boolean': + return attribute.array ? 'List' : 'bool'; + case 'integer': + return attribute.array ? 'List' : 'int'; + case 'double': + return attribute.array ? 'List' : 'double'; + case 'relationship': + if (imports.indexOf(toSnakeCase(attribute.relatedCollection)) === -1) { + imports += `import './${toSnakeCase(attribute.relatedCollection)}.dart';\n`; + } + + if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { + + return `List<${toUpperCamelCase(attribute.relatedCollection)}>`; + } + return toUpperCamelCase(attribute.relatedCollection); + } + } + + const getFromMap = (attr) => { + if (attr.type === 'relationship') { + if ((attr.relationType === 'oneToMany' && attr.side === 'parent') || (attr.relationType === 'manyToOne' && attr.side === 'child') || attr.relationType === 'manyToMany') { + return `${getType(attr)}.from((map['${attr.key}'] ?? []).map((p) => ${toUpperCamelCase(attr.relatedCollection)}.fromMap(p)))`; + } + return `map['${attr.key}'] != null ? ${getType(attr)}.fromMap(map['${attr.key}']) : null`; + } + if (attr.array) { + return `${getType(attr)}.from(map['${attr.key}'])`; + } + return `map['${attr.key}']`; + } + const properties = attributes.map(attr => { + let property = `final ${getType(attr)}${(!attr.required) ? '?' : ''} ${attr.key}`; + property += ';'; + return property; + }).join('\n '); + + const constructorParams = attributes.map(attr => { + let out = ''; + if (attr.required) { + out += 'required '; + } + out += `this.${attr.key}`; + if (attr.default && attr.default !== null) { + out += ` = ${JSON.stringify(attr.default)}`; + } + return out; + }).join(',\n '); + + const constructorArgs = attributes.map(attr => { + return `${attr.key}: ${getFromMap(attr)}`; + }).join(',\n '); + + const mapFields = attributes.map(attr => { + let out = `'${attr.key}': `; + if (attr.type === 'relationship') { + if ((attr.relationType === 'oneToMany' && attr.side === 'parent') || (attr.relationType === 'manyToOne' && attr.side === 'child') || attr.relationType === 'manyToMany') { + return `${out}${attr.key}?.map((p) => p.toMap())`; + } + return `${out}${attr.key}?.toMap()`; + } + return `${out}${attr.key}`; + }).join(',\n '); + + const replaceMaps = { + "%NAME%": name, + "%IMPORTS%": imports, + "%ATTRIBUTES%": properties, + "%CONSTRUCTOR_PARAMETERS%": constructorParams, + "%CONSTRUCTOR_ARGUMENTS%": constructorArgs, + "%MAP_FIELDS%": mapFields, + } + + for (let key in replaceMaps) { + template = template.replaceAll(key, replaceMaps[key]); + } + + return template; +} + +const configure = async (options) => { + const filePath = path.join('./', 'pubspec.yaml'); + if (!fs.existsSync(filePath)) { + error("Unable to find `pubspec.yaml` file. Not a valid Flutter project."); + return; + } + + let projectId = options.projectId ?? localConfig.getProject().projectId; + + let sdk = await sdkForConsole(); + + if (!projectId) { + + let answers = await inquirer.prompt(questionsFlutterConfigure) + if (!answers.project) process.exit(1) + + let project = {}; + if (answers.start === "new") { + let response = await teamsCreate({ + teamId: 'unique()', + name: answers.project, + sdk, + parseOutput: false + }) + + let teamId = response['$id']; + response = await projectsCreate({ + projectId: answers.id, + name: answers.project, + teamId, + parseOutput: false + }) + + project = response; + } else { + project = answers.project; + } + if (!project.id) { + fail("Unable to get project. Try again."); + } + localConfig.setProject(project.id, project.name); + projectId = project.id; + } + + // add appwrite dependency + addAppwriteDependency('./pubspec.yaml'); + + const appwriteFilePath = './lib/appwrite.dart'; + await initializeSDK(appwriteFilePath, projectId, globalConfig.getEndpoint()); + + // Which platforms to support? + let platforms = options.platforms?.split(',')?.map(platform => platform.toLowerCase()); + + if (!platforms || !platforms.length) { + platforms = await inquirer.prompt(questionsFlutterSelectPlatforms); + platforms = platforms.platforms.map(platform => platform.toLowerCase()); + } + + if (!platforms.length) { + error('No platforms selected'); + return; + } + + //get android package name + let androidPackageName = options.androidPackageName; + let iosBundleId = options.iosBundleId; + let macosBundleId = options.macosBundleId; + let hostname = options.webHostname; + + + let projectName = getPubspecName('./pubspec.yaml'); + if (!projectName) { + error('Unable to determine project name. Please make sure you are in a Flutter project root and pubspec.yaml is correctly configured.'); + return; + } + log('Project Name: ' + projectName); + + response = await projectsListPlatforms({ projectId: projectId, sdk, parseOutput: false }) + + let existingPlatforms = response.platforms; + + // select platform + if (platforms.includes('android')) { + if (!androidPackageName) { + const manifestPath = path.join('./android', 'app/src/main/AndroidManifest.xml'); + const buildPath = path.join('./android', 'app/build.gradle'); + androidPackageName = getAndroidPackageName(manifestPath, buildPath); + if (!androidPackageName) { + error('Unable to determine android package name. Please provide using --androidPackageName'); + return; + } + log('Infered Android Package Name: ' + androidPackageName); + } + const exists = existingPlatforms.find(platform => platform.key === androidPackageName && platform.type === 'flutter-android'); + if (!exists) { + response = await projectsCreatePlatform({ + projectId: projectId, + type: 'flutter-android', + name: `${projectName} (android)`, + key: androidPackageName, + sdk, + parseOutput: false + }); + success(`Android platform: ${androidPackageName} added successfully`); + } else { + success(`Android platform: ${androidPackageName} already exists`); + } + } + if (platforms.includes('ios')) { + if (!iosBundleId) { + const iosProjectPath = path.join('./ios/', 'Runner.xcodeproj/project.pbxproj'); + iosBundleId = getIOSBundleId(iosProjectPath); + if (!iosBundleId) { + error('Unable to determine iOS bundle ID. Please provide using --iosBundleId'); + return; + } + log('Infered iOS bundle ID: ' + iosBundleId); + } + const exists = existingPlatforms.find(platform => platform.key === iosBundleId && platform.type === 'flutter-ios'); + if (!exists) { + response = await projectsCreatePlatform({ + projectId: projectId, + type: 'flutter-ios', + name: `${projectName} (iOS)`, + key: iosBundleId, + sdk, + parseOutput: false + }); + success(`iOS platform: ${iosBundleId} added successfully`); + } else { + success(`iOS platform: ${iosBundleId} already exists`); + } + } + if (platforms.includes('macos')) { + if (!macosBundleId) { + const macosConfigPath = path.join('./macos/', 'Runner/Configs/AppInfo.xcconfig') + macosBundleId = getMacOSBundleId(macosConfigPath); + if (!macosBundleId) { + error('Unable to determine MacOS bundle ID. Please provide using --macosBundleId'); + return; + } + log('Infered MacOS bundle ID: ' + macosBundleId); + } + const exists = existingPlatforms.find(platform => platform.key === macosBundleId && platform.type === 'flutter-macos'); + if (!exists) { + response = await projectsCreatePlatform({ + projectId: projectId, + type: 'flutter-macos', + name: `${projectName} (MacOS)`, + key: macosBundleId, + sdk, + parseOutput: false + }); + success(`MacOS platform: ${macosBundleId} added successfully`); + } else { + success(`MacOS platform: ${macosBundleId} already exists`); + } + } + + if (platforms.includes('linux')) { + const exists = existingPlatforms.find(platform => platform.key === projectName && platform.type === 'flutter-linux'); + if (!exists) { + response = await projectsCreatePlatform({ + projectId: projectId, + type: 'flutter-linux', + name: `${projectName} (Linux)`, + key: projectName, + sdk, + parseOutput: false + }) + success(`Linux platform: ${projectName} added successfully`); + } else { + success(`Linux platform: ${projectName} already exists`); + } + } + + if (platforms.includes('windows')) { + const exists = existingPlatforms.find(platform => platform.key === projectName && platform.type === 'flutter-windows'); + if (!exists) { + response = await projectsCreatePlatform({ + projectId: projectId, + type: 'flutter-windows', + name: `${projectName} (Windows)`, + key: projectName, + sdk, + parseOutput: false + }) + success(`Windows platform: ${projectName} added successfully`); + } else { + success(`Windows platform: ${projectName} already exists`); + } + } + + if (platforms.includes('web')) { + if (!hostname) { + let answer = await inquirer.prompt({ + type: "input", + name: "hostname", + message: "What is your web app hostname?", + default: "localhost" + },); + hostname = answer.hostname; + if (!hostname) { + error('Please provide Hostname to add web platform'); + return; + } + } + const exists = existingPlatforms.find(platform => (platform.hostname === options.webHostname && platform.type === 'web') || (platforms.hostname === options.webHostname && platform.type === 'flutter-web')); + if (!exists) { + response = await projectsCreatePlatform({ + projectId: projectId, + type: 'web', // TODO change to `flutter-web` once cloud fixed + name: `${projectName} (Web)`, + hostname: hostname, + sdk, + parseOutput: false + }) + success(`Web platform: ${hostname} added successfully`); + } else { + success(`Web platform: ${options.webHostname} already exists`); + } + } + + +} + +const initializeSDK = async (path, projectId, endpoint) => { + if (fs.existsSync(path)) { + let response = await inquirer.prompt({ + type: "confirm", + name: "overwrite", + message: + `Appwrite file ( ${path} ) already exists. Do you want to overwrite?`, + }) + if (!response.overwrite) { + log('appwrite.dart already exists. Not overwriting') + return; + } else { + log('Overwriting appwrite.dart') + } + } + + let decodedString = fs.readFileSync(`${__dirname}/../../flutter_res/appwrite.dart`, 'utf8'); + decodedString = decodedString.replace('{PROJECT}', projectId); + decodedString = decodedString.replace('{ENDPOINT}', endpoint); + + fs.writeFileSync(path, decodedString); + success('SDK initialized successfully'); +} + +const getAndroidPackageName = (manifestPath, buildPath) => { + if (!fs.existsSync(manifestPath) && !fs.existsSync(buildPath)) { + return null; + } + + var match; + var applicationId; + + if (fs.existsSync(manifestPath)) { + const manifestXml = fs.readFileSync(manifestPath, 'utf8'); + // Define a regular expression to match the package attribute + const regex = /package="([^"]+)"/; + // Search for the package attribute in the manifest file using the regular expression + match = manifestXml.match(regex); + + if (match && match.length >= 2) { + applicationId = match[1]; + } + } + + if (!applicationId && fs.existsSync(buildPath)) { + const buildGradleContent = fs.readFileSync(buildPath, 'utf8'); + + // Define a regular expression to match the application ID + const regex1 = /applicationId\s+["']([^"']+)["']/; + + // Search for the application ID in the build.gradle file using the regular expression + match = buildGradleContent.match(regex1); + + if (match && match.length >= 2) { + applicationId = match[1] + } + } + + // Extract the package name from the match + return applicationId ?? null; +} + +const getPubspecName = (pubspecPath) => { + + const yamlFile = fs.readFileSync(pubspecPath, 'utf8'); + const regex = /^name:\s*(.*)$/m; + + const match = yamlFile.match(regex); + if (!match || match.length < 2) { + return null; + } + const name = match[1]; + + return name; + +} + +const addAppwriteDependency = (pubspecPath) => { + // need to add appropriate version ? + const file = fs.readFileSync(pubspecPath, 'utf8'); + if (!file.includes('appwrite:')) { + const out = file.replace('dependencies:', 'dependencies:\n appwrite:'); + fs.writeFileSync(pubspecPath, out); + success('Added appwrite SDK'); + } else { + log('Appwrite SDK already added'); + } +} + +const getIOSBundleId = (projectPath) => { + if (!fs.existsSync(projectPath)) { + return null; + } + + const projectFile = fs.readFileSync(projectPath, 'utf8'); + + const regex = /PRODUCT_BUNDLE_IDENTIFIER = ([^;]+)/; + const match = projectFile.match(regex); + + const bundleId = match[1]; + return bundleId; + +} + +const getMacOSBundleId = (projectPath) => { + if (!fs.existsSync(projectPath)) { + return null; + } + + const projectFile = fs.readFileSync(projectPath, 'utf8'); + const regex = /PRODUCT_BUNDLE_IDENTIFIER\s*=\s*([^;\n]+)/; + + const match = projectFile.match(regex); + const bundleId = match[1]; + + return bundleId; +} + + + +flutter.command("configure") + .description("Configure Flutter Appwrite project") + .option('--projectId ', 'Project ID to use. If not provided you will try to read from local config or you will be requested to select.') + .option('--androidPackageName ', 'Android package name. If not provided will try to read from AndroidManifest.xml file') + .option('--iosBundleId ', 'iOS bundle identifier. If not provided will try to read from iOS project') + .option('--macosBundleId ', 'Mac OS bundle identifier. If not provided will try to read from mac OS project') + .option('--webHostname ', 'Web app hostname. If not provided will be requested to enter.') + .option('--platforms ', 'Comma separated platforms. If not provided you will be listed with platforms to choose.') + .action(actionRunner(configure)); + +flutter.command("generate") + .description("Generate dart classes for collections") + .option('--modelPath ', 'Path where the generated models are saved. By default it\'s saved to lib/models folder.') + .option('--projectId ', 'Project ID to use to generate models for database. If not provided you will be requested to select.') + .option('--databaseIds ', 'Comma separated database IDs to generate models for. If not provided you will be requested to choose.') + .action(actionRunner(generate)); + +module.exports = { + flutter, +} \ No newline at end of file diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 0a8fb5857..95802c2c3 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -352,6 +352,152 @@ const questionsDeployTeams = [ }, ] + +const questionsFlutterConfigure = [ + { + type: "list", + name: "start", + when(answers) { + if (answers.override == undefined) { + return true + } + return answers.override; + }, + message: "Which project do you want to link?", + choices: [ + { + name: "Create a new Appwrite project", + value: "new", + }, + { + name: "Link this Flutter project to an existing Appwrite project", + value: "existing", + }, + ], + }, + { + type: "input", + name: "project", + message: "What would you like to name your project?", + default: "My Awesome Project", + when(answers) { + return answers.start == "new"; + }, + }, + { + type: "input", + name: "id", + message: "What ID would you like to have for your project?", + default: "unique()", + when(answers) { + return answers.start == "new"; + }, + }, + { + type: "list", + name: "project", + message: "Choose your Appwrite project.", + when(answers) { + return answers.start == "existing"; + }, + choices: async () => { + let response = await projectsList({ + parseOutput: false + }) + let projects = response["projects"] + let choices = projects.map((project, idx) => { + return { + name: `${project.name} (${project['$id']})`, + value: { + name: project.name, + id: project['$id'] + } + } + }) + + if (choices.length == 0) { + throw new Error("No projects found. Please create a new project.") + } + + return choices; + } + } +]; + +const questionsFlutterSelectPlatforms = [ + { + type: "checkbox", + name: "platforms", + message: "Choose your Platforms to configure.", + choices: [ + 'Android', + 'iOS', + 'Web', + 'Linux', + 'MacOS', + 'Windows', + ] + } +]; + +const questionsFlutterChooseProject = [ + { + type: "list", + name: "project", + message: "Choose Appwrite project.", + choices: async () => { + let response = await projectsList({ + parseOutput: false + }) + let projects = response["projects"] + let choices = projects.map((project, idx) => { + return { + name: `${project.name} (${project['$id']})`, + value: { + name: project.name, + id: project['$id'] + } + } + }) + + if (choices.length == 0) { + throw new Error("No projects found. Please create a new project.") + } + + return choices; + } + }, +] + +const questionsFlutterChooseDatabase = [ + { + type: "checkbox", + name: "databases", + message: "Select databases to generate classes.", + choices: async () => { + let response = await databasesList({ + parseOutput: false + }) + let databases = response["databases"] + let choices = databases.map((database, idx) => { + return { + name: `${database.name} (${database['$id']})`, + value: { + name: database.name, + id: database['$id'] + } + } + }) + + if (choices.length == 0) { + throw new Error("No databases found. Please create databases and collections.") + } + + return choices; + } + } +] + module.exports = { questionsInitProject, questionsLogin, @@ -361,5 +507,9 @@ module.exports = { questionsDeployCollections, questionsDeployBuckets, questionsDeployTeams, - questionsGetEntrypoint + questionsGetEntrypoint, + questionsFlutterConfigure, + questionsFlutterSelectPlatforms, + questionsFlutterChooseDatabase, + questionsFlutterChooseProject, }; diff --git a/templates/cli/lib/utils.js.twig b/templates/cli/lib/utils.js.twig index cb7d06ce3..059face7d 100644 --- a/templates/cli/lib/utils.js.twig +++ b/templates/cli/lib/utils.js.twig @@ -14,6 +14,25 @@ function getAllFiles(folder) { return files; } +function toSnakeCase(str) { + // Convert string to lower case and split into words + const words = str.toLowerCase().split(/\s+/); + + // Join words together with underscores + return words.join('_'); +} + +function toUpperCamelCase(str) { + // Split string into words using whitespace, underscore or hyphen as separator + // then capitalize first letter of each word + const words = str.split(/[\s_-]+/).map(word => word.charAt(0).toUpperCase() + word.slice(1)); + + // Join words together with no spaces + return words.join(''); +} + module.exports = { - getAllFiles + getAllFiles, + toSnakeCase, + toUpperCamelCase, }; \ No newline at end of file From aaf486fc0f985ee299704b49726d87a38c30f46e Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Thu, 22 Jun 2023 02:18:15 +0000 Subject: [PATCH 02/13] update --- templates/cli/flutter_res/appwrite.dart.twig | 54 +++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/templates/cli/flutter_res/appwrite.dart.twig b/templates/cli/flutter_res/appwrite.dart.twig index dddf660d1..6b509da4c 100644 --- a/templates/cli/flutter_res/appwrite.dart.twig +++ b/templates/cli/flutter_res/appwrite.dart.twig @@ -1,9 +1,51 @@ import 'package:appwrite/appwrite.dart'; -final client = Client() - .setEndpoint('{ENDPOINT}') - .setProject('{PROJECT}'); +class Appwrite { + final Client client = Client(); + late final Account account; + late final Databases databases; + late final Avatars avatars; + late final Storage storage; + late final Functions functions; + late final Graphql graphql; + late final Teams teams; -final account = Account(client); -final databases = Databases(client); -final storage = Storage(client); \ No newline at end of file + // Comment out the following constructor if you prefer singleton + Appwrite() { + client + .setEndpoint('{ENDPOINT}') + .setProject('{PROJECT}'); + + account = Account(client); + databases = Databases(client); + avatars = Avatars(client); + storage = Storage(client); + functions = Functions(client); + graphql = Graphql(client); + teams = Teams(client); + } + + // Uncoment the following code if you prefer using singleton + /* + static Appwrite? _instance; + + Appwrite._internal() { + client + .setEndpoint('{ENDPOINT}') + .setProject('{PROJECT}'); + + account = Account(client); + databases = Databases(client); + avatars = Avatars(client); + storage = Storage(client); + functions = Functions(client); + graphql = Graphql(client); + teams = Teams(client); + } + + static Appwrite get instance { + _instance ??= Appwrite._internal(); + return _instance!; + } + */ +} \ No newline at end of file From 9939ea548e3b4776d1aa14a2276d45bbb9a6905b Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 17 Jul 2023 07:38:13 +0000 Subject: [PATCH 03/13] generate data classes for ts as well --- templates/cli/lib/commands/flutter.js.twig | 158 +------------ templates/cli/lib/commands/generate.js.twig | 244 ++++++++++++++++++++ templates/cli/lib/questions.js.twig | 8 +- 3 files changed, 249 insertions(+), 161 deletions(-) create mode 100644 templates/cli/lib/commands/generate.js.twig diff --git a/templates/cli/lib/commands/flutter.js.twig b/templates/cli/lib/commands/flutter.js.twig index ff1a65171..2095aa7ab 100644 --- a/templates/cli/lib/commands/flutter.js.twig +++ b/templates/cli/lib/commands/flutter.js.twig @@ -4,7 +4,7 @@ const inquirer = require("inquirer"); const { teamsCreate } = require("./teams"); const { projectsCreate, projectsCreatePlatform, projectsListPlatforms } = require("./projects"); const { sdkForConsole } = require("../sdks"); -const { questionsFlutterConfigure, questionsFlutterSelectPlatforms, questionsFlutterChooseDatabase, questionsFlutterChooseProject } = require("../questions"); +const { questionsFlutterConfigure, questionsFlutterSelectPlatforms, questionsGeneratorChooseDatabase, questionsGeneratorChooseProject } = require("../questions"); const { success, log, actionRunner, error } = require("../parser"); const { Command } = require("commander"); const { globalConfig, localConfig } = require("../config"); @@ -21,154 +21,6 @@ const flutter = new Command("flutter") command.help(); })); -const generate = async (options) => { - let modelPath = options.modelPath ?? './lib/models/'; - if (!modelPath.endsWith('/')) { - modelPath += '/'; - } - let projectId = options.projectId ?? localConfig.getProject().projectId; - let databaseIds = options.databaseIds?.split(','); - - if (!projectId) { - let answer = await inquirer.prompt(questionsFlutterChooseProject); - if (!answer.project) { - error('You must select a project.'); - return; - } - projectId = answer.project.id; - localConfig.setProject(projectId, answer.project.name); - } - - - if (!databaseIds) { - answer = await inquirer.prompt(questionsFlutterChooseDatabase); - - if (!answer.databases.length) { - error('Please select at least one database'); - return; - } - databaseIds = answer.databases.map(database => database.id); - } - - const sdk = await sdkForProject(); - for (let index in databaseIds) { - let id = databaseIds[index]; - let response = await databasesListCollections({ - databaseId: id, - sdk, - parseOutput: false - }); - - let collections = response.collections; - - for (let index in collections) { - const collection = collections[index]; - const filename = toSnakeCase(collection.$id) + '.dart'; - const className = toUpperCamelCase(collection.$id); - - let template = fs.readFileSync(`${__dirname}/../../flutter_res/template_model.dart`, 'utf8'); - let dartClass = generateDartClass(className, collection.attributes, template); - if (!fs.existsSync(modelPath)) { - fs.mkdirSync(modelPath, { recursive: true }); - } - fs.writeFileSync(modelPath + filename, dartClass); - success(`Generated ${className} class and saved to ${modelPath + filename}`); - } - } -} - - -function generateDartClass(name, attributes, template) { - let imports = ''; - - const getType = (attribute) => { - switch (attribute.type) { - case 'string': - case 'email': - case 'url': - case 'enum': - case 'datetime': - return attribute.array ? 'List' : 'String'; - case 'boolean': - return attribute.array ? 'List' : 'bool'; - case 'integer': - return attribute.array ? 'List' : 'int'; - case 'double': - return attribute.array ? 'List' : 'double'; - case 'relationship': - if (imports.indexOf(toSnakeCase(attribute.relatedCollection)) === -1) { - imports += `import './${toSnakeCase(attribute.relatedCollection)}.dart';\n`; - } - - if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { - - return `List<${toUpperCamelCase(attribute.relatedCollection)}>`; - } - return toUpperCamelCase(attribute.relatedCollection); - } - } - - const getFromMap = (attr) => { - if (attr.type === 'relationship') { - if ((attr.relationType === 'oneToMany' && attr.side === 'parent') || (attr.relationType === 'manyToOne' && attr.side === 'child') || attr.relationType === 'manyToMany') { - return `${getType(attr)}.from((map['${attr.key}'] ?? []).map((p) => ${toUpperCamelCase(attr.relatedCollection)}.fromMap(p)))`; - } - return `map['${attr.key}'] != null ? ${getType(attr)}.fromMap(map['${attr.key}']) : null`; - } - if (attr.array) { - return `${getType(attr)}.from(map['${attr.key}'])`; - } - return `map['${attr.key}']`; - } - const properties = attributes.map(attr => { - let property = `final ${getType(attr)}${(!attr.required) ? '?' : ''} ${attr.key}`; - property += ';'; - return property; - }).join('\n '); - - const constructorParams = attributes.map(attr => { - let out = ''; - if (attr.required) { - out += 'required '; - } - out += `this.${attr.key}`; - if (attr.default && attr.default !== null) { - out += ` = ${JSON.stringify(attr.default)}`; - } - return out; - }).join(',\n '); - - const constructorArgs = attributes.map(attr => { - return `${attr.key}: ${getFromMap(attr)}`; - }).join(',\n '); - - const mapFields = attributes.map(attr => { - let out = `'${attr.key}': `; - if (attr.type === 'relationship') { - if ((attr.relationType === 'oneToMany' && attr.side === 'parent') || (attr.relationType === 'manyToOne' && attr.side === 'child') || attr.relationType === 'manyToMany') { - return `${out}${attr.key}?.map((p) => p.toMap())`; - } - return `${out}${attr.key}?.toMap()`; - } - return `${out}${attr.key}`; - }).join(',\n '); - - const replaceMaps = { - "%NAME%": name, - "%IMPORTS%": imports, - "%ATTRIBUTES%": properties, - "%CONSTRUCTOR_PARAMETERS%": constructorParams, - "%CONSTRUCTOR_ARGUMENTS%": constructorArgs, - "%MAP_FIELDS%": mapFields, - } - - for (let key in replaceMaps) { - template = template.replaceAll(key, replaceMaps[key]); - } - - return template; -} - const configure = async (options) => { const filePath = path.join('./', 'pubspec.yaml'); if (!fs.existsSync(filePath)) { @@ -514,7 +366,6 @@ const getMacOSBundleId = (projectPath) => { } - flutter.command("configure") .description("Configure Flutter Appwrite project") .option('--projectId ', 'Project ID to use. If not provided you will try to read from local config or you will be requested to select.') @@ -525,13 +376,6 @@ flutter.command("configure") .option('--platforms ', 'Comma separated platforms. If not provided you will be listed with platforms to choose.') .action(actionRunner(configure)); -flutter.command("generate") - .description("Generate dart classes for collections") - .option('--modelPath ', 'Path where the generated models are saved. By default it\'s saved to lib/models folder.') - .option('--projectId ', 'Project ID to use to generate models for database. If not provided you will be requested to select.') - .option('--databaseIds ', 'Comma separated database IDs to generate models for. If not provided you will be requested to choose.') - .action(actionRunner(generate)); - module.exports = { flutter, } \ No newline at end of file diff --git a/templates/cli/lib/commands/generate.js.twig b/templates/cli/lib/commands/generate.js.twig new file mode 100644 index 000000000..6bcba0275 --- /dev/null +++ b/templates/cli/lib/commands/generate.js.twig @@ -0,0 +1,244 @@ +const fs = require("fs"); +const inquirer = require("inquirer"); +const { questionsGeneratorChooseDatabase: questionsFlutterChooseDatabase, questionsGeneratorChooseProject: questionsFlutterChooseProject } = require("../questions"); +const { success, actionRunner, error } = require("../parser"); +const { Command } = require("commander"); +const { localConfig } = require("../config"); +const { databasesListCollections } = require("./databases"); +const { sdkForProject } = require("../sdks"); +const { toSnakeCase, toUpperCamelCase } = require("../utils"); + +const generate = new Command('generate'); + +generate + .argument('[language]', 'Language to generate models. Currently only `dart` and `ts` is supported.') + .description("Generate model classes") + .option('--modelPath ', 'Path where the generated models are saved. By default it\'s saved to lib/models folder.') + .option('--projectId ', 'Project ID to use to generate models for database. If not provided you will be requested to select.') + .option('--databaseIds ', 'Comma separated database IDs to generate models for. If not provided you will be requested to choose.') + .configureHelp({ + helpWidth: process.stdout.columns || 80 + }) + .action(actionRunner(async (language, options) => { + if (language === 'dart' || language === 'ts') { + generateModels({ ...options, language }) + return; + } + generate.help(); + })); + +const generateModels = async (options) => { + let modelPath = options.modelPath ?? './lib/models/'; + if (!modelPath.endsWith('/')) { + modelPath += '/'; + } + let projectId = options.projectId ?? localConfig.getProject().projectId; + let databaseIds = options.databaseIds?.split(','); + + if (!projectId) { + let answer = await inquirer.prompt(questionsFlutterChooseProject); + if (!answer.project) { + error('You must select a project.'); + return; + } + projectId = answer.project.id; + localConfig.setProject(projectId, answer.project.name); + } + + if (!databaseIds) { + answer = await inquirer.prompt(questionsFlutterChooseDatabase); + + if (!answer.databases.length) { + error('Please select at least one database'); + return; + } + databaseIds = answer.databases.map(database => database.id); + } + + const sdk = await sdkForProject(); + for (let index in databaseIds) { + let id = databaseIds[index]; + let response = await databasesListCollections({ + databaseId: id, + sdk, + parseOutput: false + }); + + let collections = response.collections; + + for (let index in collections) { + let extension = '.dart'; + const collection = collections[index]; + const className = toUpperCamelCase(collection.$id); + + let template = fs.readFileSync(`${__dirname}/../../generator/template_dart.dart`, 'utf8'); + + let data = ''; + switch (options.language) { + case 'dart': + extension = '.dart'; + data = generateDartClass(className, collection.attributes, template); + break; + case 'ts': + extension = '.ts'; + template = fs.readFileSync(`${__dirname}/../../generator/template_ts.ts`, 'utf8'); + data = generateTSClass(className, collection.attributes, template); + break; + } + + const filename = toSnakeCase(collection.$id) + extension; + if (!fs.existsSync(modelPath)) { + fs.mkdirSync(modelPath, { recursive: true }); + } + fs.writeFileSync(modelPath + filename, data); + success(`Generated ${className} class and saved to ${modelPath + filename}`); + } + } +} + +function generateTSClass(name, attributes, template) { + let imports = ''; + + const getType = (attribute) => { + switch (attribute.type) { + case 'string': + case 'email': + case 'url': + case 'enum': + case 'datetime': + return attribute.array ? 'string[]' : 'string'; + case 'boolean': + return attribute.array ? 'bool[]' : 'bool'; + case 'integer': + case 'double': + + case 'relationship': + if (imports.indexOf(toSnakeCase(attribute.relatedCollection)) === -1) { + imports += `import './${toSnakeCase(attribute.relatedCollection)}.ts';\n`; + } + + if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { + + return `${toUpperCamelCase(attribute.relatedCollection)}[]`; + } + return toUpperCamelCase(attribute.relatedCollection); + } + } + + const properties = attributes.map(attr => { + let property = `${attr.key}${(!attr.required) ? '?' : ''}: ${getType(attr)}`; + property += ';'; + return property; + }).join('\n '); + + const replaceMaps = { + "%NAME%": name, + "%IMPORTS%": imports, + "%ATTRIBUTES%": properties, + } + + for (let key in replaceMaps) { + template = template.replaceAll(key, replaceMaps[key]); + } + + return template; +} + +function generateDartClass(name, attributes, template) { + let imports = ''; + + const getType = (attribute) => { + switch (attribute.type) { + case 'string': + case 'email': + case 'url': + case 'enum': + case 'datetime': + return attribute.array ? 'List' : 'String'; + case 'boolean': + return attribute.array ? 'List' : 'bool'; + case 'integer': + return attribute.array ? 'List' : 'int'; + case 'double': + return attribute.array ? 'List' : 'double'; + case 'relationship': + if (imports.indexOf(toSnakeCase(attribute.relatedCollection)) === -1) { + imports += `import './${toSnakeCase(attribute.relatedCollection)}.dart';\n`; + } + + if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { + + return `List<${toUpperCamelCase(attribute.relatedCollection)}>`; + } + return toUpperCamelCase(attribute.relatedCollection); + } + } + + const getFromMap = (attr) => { + if (attr.type === 'relationship') { + if ((attr.relationType === 'oneToMany' && attr.side === 'parent') || (attr.relationType === 'manyToOne' && attr.side === 'child') || attr.relationType === 'manyToMany') { + return `${getType(attr)}.from((map['${attr.key}'] ?? []).map((p) => ${toUpperCamelCase(attr.relatedCollection)}.fromMap(p)))`; + } + return `map['${attr.key}'] != null ? ${getType(attr)}.fromMap(map['${attr.key}']) : null`; + } + if (attr.array) { + return `${getType(attr)}.from(map['${attr.key}'])`; + } + return `map['${attr.key}']`; + } + const properties = attributes.map(attr => { + let property = `final ${getType(attr)}${(!attr.required) ? '?' : ''} ${attr.key}`; + property += ';'; + return property; + }).join('\n '); + + const constructorParams = attributes.map(attr => { + let out = ''; + if (attr.required) { + out += 'required '; + } + out += `this.${attr.key}`; + if (attr.default && attr.default !== null) { + out += ` = ${JSON.stringify(attr.default)}`; + } + return out; + }).join(',\n '); + + const constructorArgs = attributes.map(attr => { + return `${attr.key}: ${getFromMap(attr)}`; + }).join(',\n '); + + const mapFields = attributes.map(attr => { + let out = `'${attr.key}': `; + if (attr.type === 'relationship') { + if ((attr.relationType === 'oneToMany' && attr.side === 'parent') || (attr.relationType === 'manyToOne' && attr.side === 'child') || attr.relationType === 'manyToMany') { + return `${out}${attr.key}?.map((p) => p.toMap())`; + } + return `${out}${attr.key}?.toMap()`; + } + return `${out}${attr.key}`; + }).join(',\n '); + + const replaceMaps = { + "%NAME%": name, + "%IMPORTS%": imports, + "%ATTRIBUTES%": properties, + "%CONSTRUCTOR_PARAMETERS%": constructorParams, + "%CONSTRUCTOR_ARGUMENTS%": constructorArgs, + "%MAP_FIELDS%": mapFields, + } + + for (let key in replaceMaps) { + template = template.replaceAll(key, replaceMaps[key]); + } + + return template; +} + +function generateJSType(name, attributes, template) { + +} + +module.exports = { + generate, +} \ No newline at end of file diff --git a/templates/cli/lib/questions.js.twig b/templates/cli/lib/questions.js.twig index 95802c2c3..4aec31dac 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -440,7 +440,7 @@ const questionsFlutterSelectPlatforms = [ } ]; -const questionsFlutterChooseProject = [ +const questionsGeneratorChooseProject = [ { type: "list", name: "project", @@ -469,7 +469,7 @@ const questionsFlutterChooseProject = [ }, ] -const questionsFlutterChooseDatabase = [ +const questionsGeneratorChooseDatabase = [ { type: "checkbox", name: "databases", @@ -510,6 +510,6 @@ module.exports = { questionsGetEntrypoint, questionsFlutterConfigure, questionsFlutterSelectPlatforms, - questionsFlutterChooseDatabase, - questionsFlutterChooseProject, + questionsGeneratorChooseDatabase, + questionsGeneratorChooseProject, }; From 1cd94eb728ecf09f060cc5de271f547f7a411b3e Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 17 Jul 2023 07:46:58 +0000 Subject: [PATCH 04/13] update new templates --- src/SDK/Language/CLI.php | 14 ++++++++++++-- .../template_dart.dart.twig} | 0 templates/cli/generator/template_ts.ts.twig | 11 +++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) rename templates/cli/{flutter_res/template_model.dart.twig => generator/template_dart.dart.twig} (100%) create mode 100644 templates/cli/generator/template_ts.ts.twig diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index dc827bd97..63f02d22c 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -161,6 +161,11 @@ public function getFiles(): array 'destination' => 'lib/commands/flutter.js', 'template' => 'cli/lib/commands/flutter.js.twig', ], + [ + 'scope' => 'default', + 'destination' => 'lib/commands/generate.js', + 'template' => 'cli/lib/commands/generate.js.twig', + ], [ 'scope' => 'default', 'destination' => 'flutter_res/apwrite.dart', @@ -168,8 +173,13 @@ public function getFiles(): array ], [ 'scope' => 'default', - 'destination' => 'flutter_res/template_model.dart', - 'template' => 'cli/flutter_res/template_model.dart.twig', + 'destination' => 'generator/template_dart.dart', + 'template' => 'cli/generator/template_dart.dart.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'generator/template_ts.ts', + 'template' => 'cli/generator/template_ts.ts.twig', ], [ 'scope' => 'service', diff --git a/templates/cli/flutter_res/template_model.dart.twig b/templates/cli/generator/template_dart.dart.twig similarity index 100% rename from templates/cli/flutter_res/template_model.dart.twig rename to templates/cli/generator/template_dart.dart.twig diff --git a/templates/cli/generator/template_ts.ts.twig b/templates/cli/generator/template_ts.ts.twig new file mode 100644 index 000000000..793a94596 --- /dev/null +++ b/templates/cli/generator/template_ts.ts.twig @@ -0,0 +1,11 @@ +%IMPORTS% + +export type %NAME% { + $id: string; + $collectionId: string; + $databaseId: string; + $createdAt: string; + $updatedAt: string; + $permissions: string[]; + %ATTRIBUTES% +} \ No newline at end of file From c6c4f4badf2d42d924c11b8c7ef06f113f3bc488 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 12 Nov 2023 03:17:16 +0000 Subject: [PATCH 05/13] swift template setup --- src/SDK/Language/CLI.php | 5 + .../cli/generator/template_swift.swift.twig | 51 ++++++++++ templates/cli/lib/commands/generate.js.twig | 96 ++++++++++++++++++- 3 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 templates/cli/generator/template_swift.swift.twig diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index bc0d3a159..6d357dfaa 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -187,6 +187,11 @@ public function getFiles(): array 'destination' => 'generator/template_ts.ts', 'template' => 'cli/generator/template_ts.ts.twig', ], + [ + 'scope' => 'default', + 'destination' => 'generator/template_swift.swift', + 'template' => 'cli/generator/template_swift.swift.twig', + ], [ 'scope' => 'service', 'destination' => '/lib/commands/{{service.name | caseDash}}.js', diff --git a/templates/cli/generator/template_swift.swift.twig b/templates/cli/generator/template_swift.swift.twig new file mode 100644 index 000000000..e41735583 --- /dev/null +++ b/templates/cli/generator/template_swift.swift.twig @@ -0,0 +1,51 @@ +public class %NAME% { + public let id: String + public let collectionId: String + public let databaseId: String + public let createdAt: String + public let updatedAt: String + public let permissions: Permissions + %ATTRIBUTES% + + init( + id: String, + collectionId: String, + databaseId: String, + createdAt: String, + updatedAt: String, + permissions: Permissions, + %CONSTRUCTOR_PARAMETERS% + ) { + self.id = id + self.collectionId = collectionId + self.databaseId = databaseId + self.createdAt = createdAt + self.updatedAt = updatedAt + self.permissions = permissions + %CONSTRUCTOR_ASSIGNMENTS% + } + + public static func from(map: [String: Any]) -> %NAME% { + return %NAME%( + id: map["$id"] as! String, + collectionId: map["$collectionId"] as! String, + databaseId: map["$databaseId"] as! String, + createdAt: map["$id"] as! String, + updatedAt: map["$id"] as! String, + permissions: map["$id"] as! Permissions, + %CONSTRUCTOR_ARGUMENTS% + ) + } + + public func toMap() -> [String: Any] { + return [ + "id": id as Any, + "collectionId": collectionId as Any, + "databaseId": databaseId as Any, + "createdAt": createdAt as Any, + "updatedAt": updatedAt as Any, + "permissions": permissions.toMap() as Any, + %MAP_FIELDS% + ] + } +} \ No newline at end of file diff --git a/templates/cli/lib/commands/generate.js.twig b/templates/cli/lib/commands/generate.js.twig index 6bcba0275..d16e5fa02 100644 --- a/templates/cli/lib/commands/generate.js.twig +++ b/templates/cli/lib/commands/generate.js.twig @@ -74,6 +74,7 @@ const generateModels = async (options) => { let template = fs.readFileSync(`${__dirname}/../../generator/template_dart.dart`, 'utf8'); let data = ''; + let filename = toSnakeCase(collection.$id) + extension; switch (options.language) { case 'dart': extension = '.dart'; @@ -84,9 +85,14 @@ const generateModels = async (options) => { template = fs.readFileSync(`${__dirname}/../../generator/template_ts.ts`, 'utf8'); data = generateTSClass(className, collection.attributes, template); break; + case 'swift': + extension = '.swift'; + template = fs.readFileSync(`${__dirname}/../../generator/template_swift.swift`, 'utf8'); + data = generateSwiftClass(className, collection.attributes, template); + filename = toUpperCamelCase(collection.$id) + extension; + break; } - const filename = toSnakeCase(collection.$id) + extension; if (!fs.existsSync(modelPath)) { fs.mkdirSync(modelPath, { recursive: true }); } @@ -144,6 +150,94 @@ function generateTSClass(name, attributes, template) { return template; } +function generateSwiftClass(name, attributes, template) { + let imports = ''; + + const getType = (attribute) => { + switch (attribute.type) { + case 'string': + case 'email': + case 'url': + case 'enum': + case 'datetime': + return attribute.array ? '[String]' : 'String'; + case 'boolean': + return attribute.array ? '[Bool]' : 'Bool'; + case 'integer': + return attribute.array ? '[Int]' : 'Int'; + case 'double': + return attribute.array ? '[Float]' : 'Float'; + case 'relationship': + + if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { + + return `[${toUpperCamelCase(attribute.relatedCollection)}]`; + } + return toUpperCamelCase(attribute.relatedCollection); + } + } + + const properties = attributes.map(attr => { + let property = `public let ${attr.key}${(!attr.required) ? '?' : ''}: ${getType(attr)}`; + return property; + }).join('\n '); + + const getFromMap = (attr) => { + if (attr.type === 'relationship') { + if ((attr.relationType === 'oneToMany' && attr.side === 'parent') || (attr.relationType === 'manyToOne' && attr.side === 'child') || attr.relationType === 'manyToMany') { + return `${getType(attr)}.from((map['${attr.key}'] as [[String: Any]]).map{${toUpperCamelCase(attr.relatedCollection)}.from(map: \$0)))`; + } + return `map['${attr.key}'] != null ? ${getType(attr)}.fromMap(map['${attr.key}']) : null`; + } + if (attr.array) { + return `${getType(attr)}.from(map['${attr.key}'])`; + } + return `map['${attr.key}']`; + } + + const constructorParams = attributes.map(attr => { + let out = ''; + if (attr.required) { + out += 'required '; + } + out += `this.${attr.key}`; + if (attr.default && attr.default !== null) { + out += ` = ${JSON.stringify(attr.default)}`; + } + return out; + }).join(',\n '); + + const constructorArgs = attributes.map(attr => { + return `${attr.key}: ${getFromMap(attr)}`; + }).join(',\n '); + + const mapFields = attributes.map(attr => { + let out = `'${attr.key}': `; + if (attr.type === 'relationship') { + if ((attr.relationType === 'oneToMany' && attr.side === 'parent') || (attr.relationType === 'manyToOne' && attr.side === 'child') || attr.relationType === 'manyToMany') { + return `${out}${attr.key}?.map((p) => p.toMap())`; + } + return `${out}${attr.key}?.toMap()`; + } + return `${out}${attr.key}`; + }).join(',\n '); + + const replaceMaps = { + "%NAME%": name, + "%IMPORTS%": imports, + "%ATTRIBUTES%": properties, + "%CONSTRUCTOR_PARAMETERS%": constructorParams, + "%CONSTRUCTOR_ARGUMENTS%": constructorArgs, + "%MAP_FIELDS%": mapFields, + } + + for (let key in replaceMaps) { + template = template.replaceAll(key, replaceMaps[key]); + } + + return template; +} + function generateDartClass(name, attributes, template) { let imports = ''; From 8b955429c8d429d7997eaeec17577b53848a96e3 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 12 Nov 2023 03:33:49 +0000 Subject: [PATCH 06/13] fixes to swift generator --- templates/cli/index.js.twig | 2 + templates/cli/lib/commands/generate.js.twig | 26 ++++--- testing_gen/appwrite.json | 4 + testing_gen/lib/models/Comments.swift | 66 +++++++++++++++++ testing_gen/lib/models/Posts.swift | 81 +++++++++++++++++++++ testing_gen/lib/models/Profiles.swift | 56 ++++++++++++++ 6 files changed, 224 insertions(+), 11 deletions(-) create mode 100644 testing_gen/appwrite.json create mode 100644 testing_gen/lib/models/Comments.swift create mode 100644 testing_gen/lib/models/Posts.swift create mode 100644 testing_gen/lib/models/Profiles.swift diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 69fc57666..91e5ef1d1 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -15,6 +15,7 @@ const { flutter } = require("./lib/commands/flutter"); const { login, logout } = require("./lib/commands/generic"); const { init } = require("./lib/commands/init"); const { deploy } = require("./lib/commands/deploy"); +const { generate } = require("./lib/commands/generate"); {% endif %} {% for service in spec.services %} const { {{ service.name | caseLower }} } = require("./lib/commands/{{ service.name | caseLower }}"); @@ -47,6 +48,7 @@ program {% endfor %} .addCommand(client) .addCommand(flutter) + .addCommand(generate) .parse(process.argv); process.stdout.columns = oldWidth; \ No newline at end of file diff --git a/templates/cli/lib/commands/generate.js.twig b/templates/cli/lib/commands/generate.js.twig index d16e5fa02..79ce59143 100644 --- a/templates/cli/lib/commands/generate.js.twig +++ b/templates/cli/lib/commands/generate.js.twig @@ -20,7 +20,7 @@ generate helpWidth: process.stdout.columns || 80 }) .action(actionRunner(async (language, options) => { - if (language === 'dart' || language === 'ts') { + if (language === 'dart' || language === 'ts' || language === 'swift') { generateModels({ ...options, language }) return; } @@ -185,41 +185,44 @@ function generateSwiftClass(name, attributes, template) { const getFromMap = (attr) => { if (attr.type === 'relationship') { if ((attr.relationType === 'oneToMany' && attr.side === 'parent') || (attr.relationType === 'manyToOne' && attr.side === 'child') || attr.relationType === 'manyToMany') { - return `${getType(attr)}.from((map['${attr.key}'] as [[String: Any]]).map{${toUpperCamelCase(attr.relatedCollection)}.from(map: \$0)))`; + return `${getType(attr)}.from((map["${attr.key}"] as [[String: Any]]).map{${toUpperCamelCase(attr.relatedCollection)}.from(map: \$0)))`; } - return `map['${attr.key}'] != null ? ${getType(attr)}.fromMap(map['${attr.key}']) : null`; + return `map["${attr.key}"] != null ? ${getType(attr)}.fromMap(map["${attr.key}"]) : null`; } if (attr.array) { - return `${getType(attr)}.from(map['${attr.key}'])`; + return `${getType(attr)}.from(map["${attr.key}"])`; } - return `map['${attr.key}']`; + return `map["${attr.key}"] as! ${getType(attr)}`; } const constructorParams = attributes.map(attr => { let out = ''; - if (attr.required) { - out += 'required '; - } - out += `this.${attr.key}`; + out += `${attr.key}: ${getType(attr)}`; if (attr.default && attr.default !== null) { out += ` = ${JSON.stringify(attr.default)}`; } return out; }).join(',\n '); + const constructorAssignments = attributes.map(attr => { + let out = ''; + out += `self.${attr.key} = ${attr.key}`; + return out; + }).join(',\n '); + const constructorArgs = attributes.map(attr => { return `${attr.key}: ${getFromMap(attr)}`; }).join(',\n '); const mapFields = attributes.map(attr => { - let out = `'${attr.key}': `; + let out = `"${attr.key}": `; if (attr.type === 'relationship') { if ((attr.relationType === 'oneToMany' && attr.side === 'parent') || (attr.relationType === 'manyToOne' && attr.side === 'child') || attr.relationType === 'manyToMany') { return `${out}${attr.key}?.map((p) => p.toMap())`; } return `${out}${attr.key}?.toMap()`; } - return `${out}${attr.key}`; + return `${out}${attr.key} as Any`; }).join(',\n '); const replaceMaps = { @@ -228,6 +231,7 @@ function generateSwiftClass(name, attributes, template) { "%ATTRIBUTES%": properties, "%CONSTRUCTOR_PARAMETERS%": constructorParams, "%CONSTRUCTOR_ARGUMENTS%": constructorArgs, + "%CONSTRUCTOR_ASSIGNMENTS%": constructorAssignments, "%MAP_FIELDS%": mapFields, } diff --git a/testing_gen/appwrite.json b/testing_gen/appwrite.json new file mode 100644 index 000000000..0eb294fae --- /dev/null +++ b/testing_gen/appwrite.json @@ -0,0 +1,4 @@ +{ + "projectId": "607dd16494c6b", + "projectName": "30 Days of Appwrite" +} \ No newline at end of file diff --git a/testing_gen/lib/models/Comments.swift b/testing_gen/lib/models/Comments.swift new file mode 100644 index 000000000..26f13bdf7 --- /dev/null +++ b/testing_gen/lib/models/Comments.swift @@ -0,0 +1,66 @@ +public class Comments { + public let id: String + public let collectionId: String + public let databaseId: String + public let createdAt: String + public let updatedAt: String + public let permissions: Permissions + public let text: String + public let postId: String + public let profileId: String + public let createdAt: Int + + init( + id: String, + collectionId: String, + databaseId: String, + createdAt: String, + updatedAt: String, + permissions: Permissions, + text: String, + postId: String, + profileId: String, + createdAt: Int + ) { + self.id = id + self.collectionId = collectionId + self.databaseId = databaseId + self.createdAt = createdAt + self.updatedAt = updatedAt + self.permissions = permissions + self.text = text, + self.postId = postId, + self.profileId = profileId, + self.createdAt = createdAt + } + + public static func from(map: [String: Any]) -> Comments { + return Comments( + id: map["$id"] as! String, + collectionId: map["$collectionId"] as! String, + databaseId: map["$databaseId"] as! String, + createdAt: map["$id"] as! String, + updatedAt: map["$id"] as! String, + permissions: map["$id"] as! Permissions, + text: map["text"], + postId: map["postId"], + profileId: map["profileId"], + createdAt: map["createdAt"] + ) + } + + public func toMap() -> [String: Any] { + return [ + "id": id as Any, + "collectionId": collectionId as Any, + "databaseId": databaseId as Any, + "createdAt": createdAt as Any, + "updatedAt": updatedAt as Any, + "permissions": permissions.toMap() as Any, + 'text': text, + 'postId': postId, + 'profileId': profileId, + 'createdAt': createdAt + ] + } +} \ No newline at end of file diff --git a/testing_gen/lib/models/Posts.swift b/testing_gen/lib/models/Posts.swift new file mode 100644 index 000000000..4e5618d78 --- /dev/null +++ b/testing_gen/lib/models/Posts.swift @@ -0,0 +1,81 @@ +public class Posts { + public let id: String + public let collectionId: String + public let databaseId: String + public let createdAt: String + public let updatedAt: String + public let permissions: Permissions + public let title: String + public let text: String + public let profileId: String + public let readingTime: String + public let published: Bool + public let coverId: String + public let createdAt: Int + + init( + id: String, + collectionId: String, + databaseId: String, + createdAt: String, + updatedAt: String, + permissions: Permissions, + title: String, + text: String, + profileId: String, + readingTime: String, + published: Bool, + coverId: String, + createdAt: Int + ) { + self.id = id + self.collectionId = collectionId + self.databaseId = databaseId + self.createdAt = createdAt + self.updatedAt = updatedAt + self.permissions = permissions + self.title = title, + self.text = text, + self.profileId = profileId, + self.readingTime = readingTime, + self.published = published, + self.coverId = coverId, + self.createdAt = createdAt + } + + public static func from(map: [String: Any]) -> Posts { + return Posts( + id: map["$id"] as! String, + collectionId: map["$collectionId"] as! String, + databaseId: map["$databaseId"] as! String, + createdAt: map["$id"] as! String, + updatedAt: map["$id"] as! String, + permissions: map["$id"] as! Permissions, + title: map["title"], + text: map["text"], + profileId: map["profileId"], + readingTime: map["readingTime"], + published: map["published"], + coverId: map["coverId"], + createdAt: map["createdAt"] + ) + } + + public func toMap() -> [String: Any] { + return [ + "id": id as Any, + "collectionId": collectionId as Any, + "databaseId": databaseId as Any, + "createdAt": createdAt as Any, + "updatedAt": updatedAt as Any, + "permissions": permissions.toMap() as Any, + 'title': title, + 'text': text, + 'profileId': profileId, + 'readingTime': readingTime, + 'published': published, + 'coverId': coverId, + 'createdAt': createdAt + ] + } +} \ No newline at end of file diff --git a/testing_gen/lib/models/Profiles.swift b/testing_gen/lib/models/Profiles.swift new file mode 100644 index 000000000..fc801763f --- /dev/null +++ b/testing_gen/lib/models/Profiles.swift @@ -0,0 +1,56 @@ +public class Profiles { + public let id: String + public let collectionId: String + public let databaseId: String + public let createdAt: String + public let updatedAt: String + public let permissions: Permissions + public let name: String + public let userId: String + + init( + id: String, + collectionId: String, + databaseId: String, + createdAt: String, + updatedAt: String, + permissions: Permissions, + name: String, + userId: String + ) { + self.id = id + self.collectionId = collectionId + self.databaseId = databaseId + self.createdAt = createdAt + self.updatedAt = updatedAt + self.permissions = permissions + self.name = name, + self.userId = userId + } + + public static func from(map: [String: Any]) -> Profiles { + return Profiles( + id: map["$id"] as! String, + collectionId: map["$collectionId"] as! String, + databaseId: map["$databaseId"] as! String, + createdAt: map["$id"] as! String, + updatedAt: map["$id"] as! String, + permissions: map["$id"] as! Permissions, + name: map["name"], + userId: map["userId"] + ) + } + + public func toMap() -> [String: Any] { + return [ + "id": id as Any, + "collectionId": collectionId as Any, + "databaseId": databaseId as Any, + "createdAt": createdAt as Any, + "updatedAt": updatedAt as Any, + "permissions": permissions.toMap() as Any, + 'name': name, + 'userId': userId + ] + } +} \ No newline at end of file From 6d132552d0caf3dce4e21a8b56ac99f129976f37 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 12 Nov 2023 04:49:36 +0000 Subject: [PATCH 07/13] remove files mistakenly committed --- testing_gen/appwrite.json | 4 -- testing_gen/lib/models/Comments.swift | 66 ---------------------- testing_gen/lib/models/Posts.swift | 81 --------------------------- testing_gen/lib/models/Profiles.swift | 56 ------------------ 4 files changed, 207 deletions(-) delete mode 100644 testing_gen/appwrite.json delete mode 100644 testing_gen/lib/models/Comments.swift delete mode 100644 testing_gen/lib/models/Posts.swift delete mode 100644 testing_gen/lib/models/Profiles.swift diff --git a/testing_gen/appwrite.json b/testing_gen/appwrite.json deleted file mode 100644 index 0eb294fae..000000000 --- a/testing_gen/appwrite.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "projectId": "607dd16494c6b", - "projectName": "30 Days of Appwrite" -} \ No newline at end of file diff --git a/testing_gen/lib/models/Comments.swift b/testing_gen/lib/models/Comments.swift deleted file mode 100644 index 26f13bdf7..000000000 --- a/testing_gen/lib/models/Comments.swift +++ /dev/null @@ -1,66 +0,0 @@ -public class Comments { - public let id: String - public let collectionId: String - public let databaseId: String - public let createdAt: String - public let updatedAt: String - public let permissions: Permissions - public let text: String - public let postId: String - public let profileId: String - public let createdAt: Int - - init( - id: String, - collectionId: String, - databaseId: String, - createdAt: String, - updatedAt: String, - permissions: Permissions, - text: String, - postId: String, - profileId: String, - createdAt: Int - ) { - self.id = id - self.collectionId = collectionId - self.databaseId = databaseId - self.createdAt = createdAt - self.updatedAt = updatedAt - self.permissions = permissions - self.text = text, - self.postId = postId, - self.profileId = profileId, - self.createdAt = createdAt - } - - public static func from(map: [String: Any]) -> Comments { - return Comments( - id: map["$id"] as! String, - collectionId: map["$collectionId"] as! String, - databaseId: map["$databaseId"] as! String, - createdAt: map["$id"] as! String, - updatedAt: map["$id"] as! String, - permissions: map["$id"] as! Permissions, - text: map["text"], - postId: map["postId"], - profileId: map["profileId"], - createdAt: map["createdAt"] - ) - } - - public func toMap() -> [String: Any] { - return [ - "id": id as Any, - "collectionId": collectionId as Any, - "databaseId": databaseId as Any, - "createdAt": createdAt as Any, - "updatedAt": updatedAt as Any, - "permissions": permissions.toMap() as Any, - 'text': text, - 'postId': postId, - 'profileId': profileId, - 'createdAt': createdAt - ] - } -} \ No newline at end of file diff --git a/testing_gen/lib/models/Posts.swift b/testing_gen/lib/models/Posts.swift deleted file mode 100644 index 4e5618d78..000000000 --- a/testing_gen/lib/models/Posts.swift +++ /dev/null @@ -1,81 +0,0 @@ -public class Posts { - public let id: String - public let collectionId: String - public let databaseId: String - public let createdAt: String - public let updatedAt: String - public let permissions: Permissions - public let title: String - public let text: String - public let profileId: String - public let readingTime: String - public let published: Bool - public let coverId: String - public let createdAt: Int - - init( - id: String, - collectionId: String, - databaseId: String, - createdAt: String, - updatedAt: String, - permissions: Permissions, - title: String, - text: String, - profileId: String, - readingTime: String, - published: Bool, - coverId: String, - createdAt: Int - ) { - self.id = id - self.collectionId = collectionId - self.databaseId = databaseId - self.createdAt = createdAt - self.updatedAt = updatedAt - self.permissions = permissions - self.title = title, - self.text = text, - self.profileId = profileId, - self.readingTime = readingTime, - self.published = published, - self.coverId = coverId, - self.createdAt = createdAt - } - - public static func from(map: [String: Any]) -> Posts { - return Posts( - id: map["$id"] as! String, - collectionId: map["$collectionId"] as! String, - databaseId: map["$databaseId"] as! String, - createdAt: map["$id"] as! String, - updatedAt: map["$id"] as! String, - permissions: map["$id"] as! Permissions, - title: map["title"], - text: map["text"], - profileId: map["profileId"], - readingTime: map["readingTime"], - published: map["published"], - coverId: map["coverId"], - createdAt: map["createdAt"] - ) - } - - public func toMap() -> [String: Any] { - return [ - "id": id as Any, - "collectionId": collectionId as Any, - "databaseId": databaseId as Any, - "createdAt": createdAt as Any, - "updatedAt": updatedAt as Any, - "permissions": permissions.toMap() as Any, - 'title': title, - 'text': text, - 'profileId': profileId, - 'readingTime': readingTime, - 'published': published, - 'coverId': coverId, - 'createdAt': createdAt - ] - } -} \ No newline at end of file diff --git a/testing_gen/lib/models/Profiles.swift b/testing_gen/lib/models/Profiles.swift deleted file mode 100644 index fc801763f..000000000 --- a/testing_gen/lib/models/Profiles.swift +++ /dev/null @@ -1,56 +0,0 @@ -public class Profiles { - public let id: String - public let collectionId: String - public let databaseId: String - public let createdAt: String - public let updatedAt: String - public let permissions: Permissions - public let name: String - public let userId: String - - init( - id: String, - collectionId: String, - databaseId: String, - createdAt: String, - updatedAt: String, - permissions: Permissions, - name: String, - userId: String - ) { - self.id = id - self.collectionId = collectionId - self.databaseId = databaseId - self.createdAt = createdAt - self.updatedAt = updatedAt - self.permissions = permissions - self.name = name, - self.userId = userId - } - - public static func from(map: [String: Any]) -> Profiles { - return Profiles( - id: map["$id"] as! String, - collectionId: map["$collectionId"] as! String, - databaseId: map["$databaseId"] as! String, - createdAt: map["$id"] as! String, - updatedAt: map["$id"] as! String, - permissions: map["$id"] as! Permissions, - name: map["name"], - userId: map["userId"] - ) - } - - public func toMap() -> [String: Any] { - return [ - "id": id as Any, - "collectionId": collectionId as Any, - "databaseId": databaseId as Any, - "createdAt": createdAt as Any, - "updatedAt": updatedAt as Any, - "permissions": permissions.toMap() as Any, - 'name': name, - 'userId': userId - ] - } -} \ No newline at end of file From a9f45f81911c8f571fd421f4d010ce6a4c06bc58 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 12 Nov 2023 05:19:33 +0000 Subject: [PATCH 08/13] swift optional fix --- templates/cli/lib/commands/generate.js.twig | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/templates/cli/lib/commands/generate.js.twig b/templates/cli/lib/commands/generate.js.twig index 79ce59143..0702abf9e 100644 --- a/templates/cli/lib/commands/generate.js.twig +++ b/templates/cli/lib/commands/generate.js.twig @@ -178,7 +178,7 @@ function generateSwiftClass(name, attributes, template) { } const properties = attributes.map(attr => { - let property = `public let ${attr.key}${(!attr.required) ? '?' : ''}: ${getType(attr)}`; + let property = `public let ${attr.key}${(!attr.required) ? '??' : ''}: ${getType(attr)}`; return property; }).join('\n '); @@ -187,12 +187,12 @@ function generateSwiftClass(name, attributes, template) { if ((attr.relationType === 'oneToMany' && attr.side === 'parent') || (attr.relationType === 'manyToOne' && attr.side === 'child') || attr.relationType === 'manyToMany') { return `${getType(attr)}.from((map["${attr.key}"] as [[String: Any]]).map{${toUpperCamelCase(attr.relatedCollection)}.from(map: \$0)))`; } - return `map["${attr.key}"] != null ? ${getType(attr)}.fromMap(map["${attr.key}"]) : null`; + return `${getType(attr)}.from(map["${attr.key}"])`; } if (attr.array) { return `${getType(attr)}.from(map["${attr.key}"])`; } - return `map["${attr.key}"] as! ${getType(attr)}`; + return `map["${attr.key}"] as ${attr.required ? '!' : '?'} ${getType(attr)}${!attr.required} ? '?':''`; } const constructorParams = attributes.map(attr => { From 9a2efa7273851180fff1c06186efe70c884a0943 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 25 Dec 2023 03:33:23 +0000 Subject: [PATCH 09/13] kotlin class generator --- src/SDK/Language/CLI.php | 5 ++ .../cli/generator/template_kotlin.kt.twig | 35 ++++++++ templates/cli/lib/commands/generate.js.twig | 83 ++++++++++++++++++- 3 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 templates/cli/generator/template_kotlin.kt.twig diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index caefb5664..d1b380a0b 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -197,6 +197,11 @@ public function getFiles(): array 'destination' => 'generator/template_swift.swift', 'template' => 'cli/generator/template_swift.swift.twig', ], + [ + 'scope' => 'default', + 'destination' => 'generator/template_kotlin.kt', + 'template' => 'cli/generator/template_kotlin.kt.twig', + ], [ 'scope' => 'service', 'destination' => '/lib/commands/{{service.name | caseDash}}.js', diff --git a/templates/cli/generator/template_kotlin.kt.twig b/templates/cli/generator/template_kotlin.kt.twig new file mode 100644 index 000000000..1cb4fa724 --- /dev/null +++ b/templates/cli/generator/template_kotlin.kt.twig @@ -0,0 +1,35 @@ +class %NAME%( + val id: String, + val collectionId: String, + val databaseId: String, + val createdAt: String, + val updatedAt: String, + val permissions: Permissions, + %CONSTRUCTOR_PARAMETERS% +) { + companion object { + fun from(map: Map): %NAME% { + return %NAME%( + id = map["$id"] as String, + collectionId = map["$collectionId"] as String, + databaseId = map["$databaseId"] as String, + createdAt = map["$createdAt"] as String, + updatedAt = map["$updatedAt"] as String, + permissions = map["$permissions"] as Permissions, + %CONSTRUCTOR_ARGUMENTS% + ) + } + } + + fun toMap(): Map { + return mapOf( + "id" to id, + "collectionId" to collectionId, + "databaseId" to databaseId, + "createdAt" to createdAt, + "updatedAt" to updatedAt, + "permissions" to permissions.toMap(), + %MAP_FIELDS% + ) + } +} diff --git a/templates/cli/lib/commands/generate.js.twig b/templates/cli/lib/commands/generate.js.twig index 0702abf9e..42358f623 100644 --- a/templates/cli/lib/commands/generate.js.twig +++ b/templates/cli/lib/commands/generate.js.twig @@ -11,7 +11,7 @@ const { toSnakeCase, toUpperCamelCase } = require("../utils"); const generate = new Command('generate'); generate - .argument('[language]', 'Language to generate models. Currently only `dart` and `ts` is supported.') + .argument('[language]', 'Language to generate models. Supports dart, swift, ts, kt,') .description("Generate model classes") .option('--modelPath ', 'Path where the generated models are saved. By default it\'s saved to lib/models folder.') .option('--projectId ', 'Project ID to use to generate models for database. If not provided you will be requested to select.') @@ -20,7 +20,7 @@ generate helpWidth: process.stdout.columns || 80 }) .action(actionRunner(async (language, options) => { - if (language === 'dart' || language === 'ts' || language === 'swift') { + if (language === 'dart' || language === 'ts' || language === 'swift' || language === 'kt') { generateModels({ ...options, language }) return; } @@ -91,6 +91,12 @@ const generateModels = async (options) => { data = generateSwiftClass(className, collection.attributes, template); filename = toUpperCamelCase(collection.$id) + extension; break; + case 'kt': + extension = '.kt'; + template = fs.readFileSync(`${__dirname}/../../generator/template_kotlin.kt`, 'utf8'); + data = generateKotlinClass(className, collection.attributes, template); + filename = toUpperCamelCase(collection.$id) + extension; + break; } if (!fs.existsSync(modelPath)) { @@ -242,6 +248,79 @@ function generateSwiftClass(name, attributes, template) { return template; } +function generateKotlinClass(name, attributes, template) { + let imports = ''; + + const getType = (attribute) => { + switch (attribute.type) { + case 'string': + case 'email': + case 'url': + case 'enum': + case 'datetime': + return attribute.array ? 'List' : 'String'; + case 'boolean': + return attribute.array ? 'List' : 'Boolean'; + case 'integer': + return attribute.array ? 'List' : 'Int'; + case 'double': + return attribute.array ? 'List' : 'Float'; + case 'relationship': + return attribute.array ? `List<${toUpperCamelCase(attribute.relatedCollection)}>` : toUpperCamelCase(attribute.relatedCollection); + } + } + + const properties = attributes.map(attr => { + let property = `val ${attr.key}: ${getType(attr)}${(!attr.required) ? '?' : ''}`; + return property; + }).join(',\n '); + + const getFromMap = (attr) => { + if (attr.array) { + return `map["${attr.key}"] as List<${getType(attr)}>`; + } + return `map["${attr.key}"] as ${getType(attr)}`; + } + + const constructorParams = attributes.map(attr => { + let out = ''; + out += `val ${attr.key}: ${getType(attr)}`; + if (attr.default && attr.default !== null) { + out += ` = ${JSON.stringify(attr.default)}`; + } + return out; + }).join(',\n '); + + const constructorArgs = attributes.map(attr => { + return `${attr.key} = ${getFromMap(attr)}`; + }).join(',\n '); + + const mapFields = attributes.map(attr => { + let out = `"${attr.key}" to `; + if (attr.type === 'relationship') { + out += `${attr.key}?.map { it.toMap() }`; + } else { + out += attr.key; + } + return out; + }).join(',\n '); + + const replaceMaps = { + "%NAME%": name, + "%IMPORTS%": imports, + "%CONSTRUCTOR_PARAMETERS%": constructorParams, + "%CONSTRUCTOR_ARGUMENTS%": constructorArgs, + "%ATTRIBUTES%": properties, + "%MAP_FIELDS%": mapFields, + } + + for (let key in replaceMaps) { + template = template.replaceAll(key, replaceMaps[key]); + } + + return template; +} + function generateDartClass(name, attributes, template) { let imports = ''; From 8cf4110d3e733da7ff1220ccccd4ade72b86c955 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 25 Dec 2023 03:38:44 +0000 Subject: [PATCH 10/13] fix kotlin and swift template --- templates/cli/generator/template_kotlin.kt.twig | 4 ++-- templates/cli/generator/template_swift.swift.twig | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/templates/cli/generator/template_kotlin.kt.twig b/templates/cli/generator/template_kotlin.kt.twig index 1cb4fa724..a60b8e65c 100644 --- a/templates/cli/generator/template_kotlin.kt.twig +++ b/templates/cli/generator/template_kotlin.kt.twig @@ -4,7 +4,7 @@ class %NAME%( val databaseId: String, val createdAt: String, val updatedAt: String, - val permissions: Permissions, + val permissions: List, %CONSTRUCTOR_PARAMETERS% ) { companion object { @@ -15,7 +15,7 @@ class %NAME%( databaseId = map["$databaseId"] as String, createdAt = map["$createdAt"] as String, updatedAt = map["$updatedAt"] as String, - permissions = map["$permissions"] as Permissions, + permissions = map["$permissions"] as List, %CONSTRUCTOR_ARGUMENTS% ) } diff --git a/templates/cli/generator/template_swift.swift.twig b/templates/cli/generator/template_swift.swift.twig index e41735583..1422b2b80 100644 --- a/templates/cli/generator/template_swift.swift.twig +++ b/templates/cli/generator/template_swift.swift.twig @@ -4,7 +4,7 @@ public class %NAME% { public let databaseId: String public let createdAt: String public let updatedAt: String - public let permissions: Permissions + public let permissions: [Any] %ATTRIBUTES% init( @@ -30,9 +30,9 @@ public class %NAME% { id: map["$id"] as! String, collectionId: map["$collectionId"] as! String, databaseId: map["$databaseId"] as! String, - createdAt: map["$id"] as! String, - updatedAt: map["$id"] as! String, - permissions: map["$id"] as! Permissions, + createdAt: map["$createdAt"] as! String, + updatedAt: map["$updatedAt"] as! String, + permissions: map["$permissions"] as! [Any], %CONSTRUCTOR_ARGUMENTS% ) } From 6fe841750c9f5aef1ee22fbe144c17c4a851737a Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 25 Dec 2023 04:08:21 +0000 Subject: [PATCH 11/13] fix template --- templates/cli/generator/template_kotlin.kt.twig | 2 +- templates/cli/generator/template_swift.swift.twig | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/cli/generator/template_kotlin.kt.twig b/templates/cli/generator/template_kotlin.kt.twig index a60b8e65c..10ad64684 100644 --- a/templates/cli/generator/template_kotlin.kt.twig +++ b/templates/cli/generator/template_kotlin.kt.twig @@ -28,7 +28,7 @@ class %NAME%( "databaseId" to databaseId, "createdAt" to createdAt, "updatedAt" to updatedAt, - "permissions" to permissions.toMap(), + "permissions" to permissions as Any, %MAP_FIELDS% ) } diff --git a/templates/cli/generator/template_swift.swift.twig b/templates/cli/generator/template_swift.swift.twig index 1422b2b80..d23f9f5d0 100644 --- a/templates/cli/generator/template_swift.swift.twig +++ b/templates/cli/generator/template_swift.swift.twig @@ -44,7 +44,7 @@ public class %NAME% { "databaseId": databaseId as Any, "createdAt": createdAt as Any, "updatedAt": updatedAt as Any, - "permissions": permissions.toMap() as Any, + "permissions": permissions as Any, %MAP_FIELDS% ] } From c534d50e17c12b7b3454317836adf92b295ae1a0 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 25 Dec 2023 04:21:34 +0000 Subject: [PATCH 12/13] do not load flutter module on test --- templates/cli/index.js.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 91e5ef1d1..12e295762 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -10,11 +10,11 @@ const chalk = require("chalk"); const { version } = require("./package.json"); const { commandDescriptions, cliConfig } = require("./lib/parser"); const { client } = require("./lib/commands/generic"); -const { flutter } = require("./lib/commands/flutter"); {% if sdk.test != "true" %} const { login, logout } = require("./lib/commands/generic"); const { init } = require("./lib/commands/init"); const { deploy } = require("./lib/commands/deploy"); +const { flutter } = require("./lib/commands/flutter"); const { generate } = require("./lib/commands/generate"); {% endif %} {% for service in spec.services %} From 3a5d2a8e442117dc59f17f89c82ce9e7fc18464e Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 25 Dec 2023 04:23:26 +0000 Subject: [PATCH 13/13] fix error --- templates/cli/index.js.twig | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index 12e295762..20ca8b15a 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -42,13 +42,13 @@ program .addCommand(init) .addCommand(deploy) .addCommand(logout) + .addCommand(flutter) + .addCommand(generate) {% endif %} {% for service in spec.services %} .addCommand({{ service.name | caseLower }}) {% endfor %} .addCommand(client) - .addCommand(flutter) - .addCommand(generate) .parse(process.argv); process.stdout.columns = oldWidth; \ No newline at end of file