diff --git a/src/SDK/Language/CLI.php b/src/SDK/Language/CLI.php index fed1e0388..d1b380a0b 100644 --- a/src/SDK/Language/CLI.php +++ b/src/SDK/Language/CLI.php @@ -167,6 +167,41 @@ 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' => 'lib/commands/generate.js', + 'template' => 'cli/lib/commands/generate.js.twig', + ], + [ + 'scope' => 'default', + 'destination' => 'flutter_res/apwrite.dart', + 'template' => 'cli/flutter_res/appwrite.dart.twig', + ], + [ + 'scope' => 'default', + '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' => 'default', + '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/flutter_res/appwrite.dart.twig b/templates/cli/flutter_res/appwrite.dart.twig new file mode 100644 index 000000000..6b509da4c --- /dev/null +++ b/templates/cli/flutter_res/appwrite.dart.twig @@ -0,0 +1,51 @@ +import 'package:appwrite/appwrite.dart'; + +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; + + // 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 diff --git a/templates/cli/generator/template_dart.dart.twig b/templates/cli/generator/template_dart.dart.twig new file mode 100644 index 000000000..6cb5cf6a6 --- /dev/null +++ b/templates/cli/generator/template_dart.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/generator/template_kotlin.kt.twig b/templates/cli/generator/template_kotlin.kt.twig new file mode 100644 index 000000000..10ad64684 --- /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: List, + %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 List, + %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 as Any, + %MAP_FIELDS% + ) + } +} diff --git a/templates/cli/generator/template_swift.swift.twig b/templates/cli/generator/template_swift.swift.twig new file mode 100644 index 000000000..d23f9f5d0 --- /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: [Any] + %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["$createdAt"] as! String, + updatedAt: map["$updatedAt"] as! String, + permissions: map["$permissions"] as! [Any], + %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 as Any, + %MAP_FIELDS% + ] + } +} \ No newline at end of file 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 diff --git a/templates/cli/index.js.twig b/templates/cli/index.js.twig index d4415db72..20ca8b15a 100644 --- a/templates/cli/index.js.twig +++ b/templates/cli/index.js.twig @@ -14,6 +14,8 @@ const { client } = require("./lib/commands/generic"); 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 %} const { {{ service.name | caseLower }} } = require("./lib/commands/{{ service.name | caseLower }}"); @@ -40,6 +42,8 @@ program .addCommand(init) .addCommand(deploy) .addCommand(logout) + .addCommand(flutter) + .addCommand(generate) {% endif %} {% for service in spec.services %} .addCommand({{ service.name | caseLower }}) diff --git a/templates/cli/lib/commands/flutter.js.twig b/templates/cli/lib/commands/flutter.js.twig new file mode 100644 index 000000000..2095aa7ab --- /dev/null +++ b/templates/cli/lib/commands/flutter.js.twig @@ -0,0 +1,381 @@ +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, questionsGeneratorChooseDatabase, questionsGeneratorChooseProject } = 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 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)); + +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..42358f623 --- /dev/null +++ b/templates/cli/lib/commands/generate.js.twig @@ -0,0 +1,421 @@ +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. 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.') + .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' || language === 'swift' || language === 'kt') { + 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 = ''; + let filename = toSnakeCase(collection.$id) + extension; + 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; + 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; + 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)) { + 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 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 `${getType(attr)}.from(map["${attr.key}"])`; + } + if (attr.array) { + return `${getType(attr)}.from(map["${attr.key}"])`; + } + return `map["${attr.key}"] as ${attr.required ? '!' : '?'} ${getType(attr)}${!attr.required} ? '?':''`; + } + + const constructorParams = attributes.map(attr => { + let out = ''; + 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}": `; + 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} as Any`; + }).join(',\n '); + + const replaceMaps = { + "%NAME%": name, + "%IMPORTS%": imports, + "%ATTRIBUTES%": properties, + "%CONSTRUCTOR_PARAMETERS%": constructorParams, + "%CONSTRUCTOR_ARGUMENTS%": constructorArgs, + "%CONSTRUCTOR_ASSIGNMENTS%": constructorAssignments, + "%MAP_FIELDS%": mapFields, + } + + for (let key in replaceMaps) { + template = template.replaceAll(key, replaceMaps[key]); + } + + 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 = ''; + + 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 0cb4540f3..fc4bb6b2d 100644 --- a/templates/cli/lib/questions.js.twig +++ b/templates/cli/lib/questions.js.twig @@ -389,6 +389,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 questionsGeneratorChooseProject = [ + { + 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 questionsGeneratorChooseDatabase = [ + { + 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, @@ -398,5 +544,9 @@ module.exports = { questionsDeployCollections, questionsDeployBuckets, questionsDeployTeams, - questionsGetEntrypoint + questionsGetEntrypoint, + questionsFlutterConfigure, + questionsFlutterSelectPlatforms, + questionsGeneratorChooseDatabase, + questionsGeneratorChooseProject, }; 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