From 6fff7e5e004387065eac8d0ffe5f0a53dfba8133 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 7 May 2023 09:19:52 +0000 Subject: [PATCH 01/21] flutter command WIP --- index.js | 2 + lib/commands/flutter.js | 106 ++++++++++++++++++++++++++++++++++++++++ lib/questions.js | 74 +++++++++++++++++++++++++++- 3 files changed, 181 insertions(+), 1 deletion(-) mode change 100644 => 100755 index.js create mode 100644 lib/commands/flutter.js diff --git a/index.js b/index.js old mode 100644 new mode 100755 index 30c7764..8f803af --- a/index.js +++ b/index.js @@ -25,6 +25,7 @@ const { projects } = require("./lib/commands/projects"); const { storage } = require("./lib/commands/storage"); const { teams } = require("./lib/commands/teams"); const { users } = require("./lib/commands/users"); +const { flutter } = require("./lib/commands/flutter"); program .description(commandDescriptions['main']) @@ -59,6 +60,7 @@ program .addCommand(teams) .addCommand(users) .addCommand(client) + .addCommand(flutter) .parse(process.argv); process.stdout.columns = oldWidth; \ No newline at end of file diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js new file mode 100644 index 0000000..81182f1 --- /dev/null +++ b/lib/commands/flutter.js @@ -0,0 +1,106 @@ +const fs = require("fs"); +const path = require("path"); +const childProcess = require("child_process"); +const inquirer = require("inquirer"); +const { teamsCreate, teamsList } = require("./teams"); +const { projectsCreate, projectsCreatePlatform } = require("./projects"); +const { sdkForConsole } = require("../sdks"); +const { questionsConfigureFlutter } = require("../questions"); +const { success, log, actionRunner, commandDescriptions, error } = require("../parser"); +const { Command } = require("commander"); + +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 () => { + const filePath = path.join('./', 'pubspec.yaml'); + if(!fs.existsSync(filePath)) { + error("Unable to find `pubspec.yaml` file. Not a valid Flutter project."); + return; + } + + let response = {} + let answers = await inquirer.prompt(questionsConfigureFlutter) + if (!answers.project) process.exit(1) + + let sdk = await sdkForConsole(); + let project = {}; + if (answers.start == "new") { + 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."); + } + + // Which platforms to support? + + //get android package name + const manifestPath = path.join('./android', 'app/src/main/AndroidManifest.xml'); + let packageName = getAndroidPackageName(manifestPath); + if(!packageName) { + packageName = 'com.example.testandroidapp'; + } + console.log(packageName); + // select platform + response = await projectsCreatePlatform({ + projectId: project.id, + type: 'flutter-android', + name: 'Flutter Android', + key: packageName, + sdk, + parseOutput: false + }); + console.log(response); + +} + +const getAndroidPackageName = (manifestPath) => { + + if(!fs.existsSync(manifestPath)) { + return null; + } + + 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 + const match = manifestXml.match(regex); + + // Extract the package name from the match + const packageName = match[1]; + return packageName; +} + + +flutter.command("configure") + .description("Configure Flutter Appwrite project") + .action(actionRunner(configure)); + +module.exports = { + flutter, +} \ No newline at end of file diff --git a/lib/questions.js b/lib/questions.js index 70a762a..e25a866 100644 --- a/lib/questions.js +++ b/lib/questions.js @@ -352,6 +352,77 @@ const questionsDeployTeams = [ }, ] +const questionsConfigureFlutter = [ + { + 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; + } + } +]; + module.exports = { questionsInitProject, questionsLogin, @@ -361,5 +432,6 @@ module.exports = { questionsDeployCollections, questionsDeployBuckets, questionsDeployTeams, - questionsGetEntrypoint + questionsGetEntrypoint, + questionsConfigureFlutter }; From 32c71222f3037475f33b0bf86c90f669e32150ca Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 8 May 2023 01:28:55 +0000 Subject: [PATCH 02/21] setup platforms working --- lib/commands/flutter.js | 188 ++++++++++++++++++++++++++++++++++------ 1 file changed, 163 insertions(+), 25 deletions(-) diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index 81182f1..6f3851a 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -3,28 +3,29 @@ const path = require("path"); const childProcess = require("child_process"); const inquirer = require("inquirer"); const { teamsCreate, teamsList } = require("./teams"); -const { projectsCreate, projectsCreatePlatform } = require("./projects"); +const { projectsCreate, projectsCreatePlatform, projectsListPlatforms } = require("./projects"); const { sdkForConsole } = require("../sdks"); const { questionsConfigureFlutter } = require("../questions"); const { success, log, actionRunner, commandDescriptions, error } = require("../parser"); const { Command } = require("commander"); +const { name } = require("tar/lib/types"); const flutter = new Command("flutter") .description("Configure Flutter project to use Appwrite") .configureHelp({ helpWidth: process.stdout.columns || 80 }) - .action(actionRunner(async(_options, command) => { + .action(actionRunner(async (_options, command) => { command.help(); })); -const configure = async () => { +const configure = async (options) => { const filePath = path.join('./', 'pubspec.yaml'); - if(!fs.existsSync(filePath)) { + if (!fs.existsSync(filePath)) { error("Unable to find `pubspec.yaml` file. Not a valid Flutter project."); return; } - + let response = {} let answers = await inquirer.prompt(questionsConfigureFlutter) if (!answers.project) process.exit(1) @@ -51,7 +52,7 @@ const configure = async () => { } else { project = answers.project; } - if(!project.id) { + if (!project.id) { fail("Unable to get project. Try again."); } @@ -59,46 +60,183 @@ const configure = async () => { //get android package name const manifestPath = path.join('./android', 'app/src/main/AndroidManifest.xml'); - let packageName = getAndroidPackageName(manifestPath); - if(!packageName) { - packageName = 'com.example.testandroidapp'; + let androidPackageName = getAndroidPackageName(manifestPath); + if (!androidPackageName) { + androidPackageName = 'com.example.testandroidapp'; + } + + const iosProjectPath = path.join('./ios/', 'Runner.xcodeproj/project.pbxproj'); + let iosBundleId = getIOSBundleId(iosProjectPath); + log('Infered iOS bundle ID: ' + iosBundleId); + const macosConfigPath = path.join('./macos/', 'Runner/Configs/AppInfo.xcconfig') + let macosBundleId = getMacOSBundleId(macosConfigPath); + log('Infered MacOS bundle ID: ' + macosBundleId); + + let projectName = getPubspecName('./pubspec.yaml'); + log('Project Name: ' + projectName); + + response = await projectsListPlatforms({ projectId: project.id, sdk, parseOutput: false }) + + let platforms = response.platforms; + + let exists = { + android: false, + ios: false, + macos: false, + web: false, + linux: false, + windows: false, } - console.log(packageName); + + platforms.forEach(platform => { + if (platform.key === androidPackageName && platform.type === 'flutter-android') { + exists.android = true; + } + if (platform.key === iosBundleId && platform.type === 'flutter-ios') { + exists.ios = true; + } + if (platform.key === macosBundleId && platform.type === 'flutter-macos') { + exists.macos = true; + } + if (platform.key === projectName && platform.type === 'flutter-linux') { + exists.linux = true; + } + if (platform.key === projectName && platform.type === 'flutter-windows') { + exists.windows = true; + } + }) + // select platform - response = await projectsCreatePlatform({ - projectId: project.id, - type: 'flutter-android', - name: 'Flutter Android', - key: packageName, - sdk, - parseOutput: false - }); - console.log(response); - + if(!exists.android) { + response = await projectsCreatePlatform({ + projectId: project.id, + type: 'flutter-android', + name: `${projectName} (android)`, + key: androidPackageName, + sdk, + parseOutput: false + }); + log(`Android platform: ${androidPackageName} added successfully`); + } + if(!exists.ios) { + response = await projectsCreatePlatform({ + projectId: project.id, + type: 'flutter-ios', + name: `${projectName} (iOS)`, + key: iosBundleId, + sdk, + parseOutput: false + }); + log(`iOS platform: ${iosBundleId} added successfully`); + } + if(!exists.macos) { + response = await projectsCreatePlatform({ + projectId: project.id, + type: 'flutter-macos', + name: `${projectName} (MacOS)`, + key: macosBundleId, + sdk, + parseOutput: false + }); + log(`MacOS platform: ${macosBundleId} added successfully`); + } + + if(!exists.linux) { + response = await projectsCreatePlatform({ + projectId: project.id, + type: 'flutter-linux', + name: `${projectName} (Linux)`, + key: projectName, + sdk, + parseOutput: false + }) + log(`Linux platform: ${projectName} added successfully`); + } + + if(!exists.windows) { + response = await projectsCreatePlatform({ + projectId: project.id, + type: 'flutter-windows', + name: `${projectName} (Windows)`, + key: projectName, + sdk, + parseOutput: false + }) + log(`Windows platform: ${projectName} added successfully`); + } + + } const getAndroidPackageName = (manifestPath) => { - if(!fs.existsSync(manifestPath)) { + if (!fs.existsSync(manifestPath)) { return null; } - + 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 const match = manifestXml.match(regex); - + // Extract the package name from the match const packageName = match[1]; - return packageName; + return packageName; } +const getPubspecName = (pubspecPath) => { + + const yamlFile = fs.readFileSync(pubspecPath, 'utf8'); + + const regex = /^name:\s*(.*)$/m; + + const match = yamlFile.match(regex); + + const name = match[1]; + + return name; + +} + +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('--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('--platforms', 'Comma separated platforms. If not provided you will be listed with platforms to choose.') .action(actionRunner(configure)); module.exports = { From f7910bb1a72ad76a7cd855f47667db36ec56b705 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 8 May 2023 02:21:18 +0000 Subject: [PATCH 03/21] initialize SDK --- lib/commands/flutter.js | 46 ++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index 6f3851a..87f9c33 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -9,6 +9,9 @@ const { questionsConfigureFlutter } = require("../questions"); const { success, log, actionRunner, commandDescriptions, error } = require("../parser"); const { Command } = require("commander"); const { name } = require("tar/lib/types"); +const { globalConfig } = require("../config"); + +const appwriteFile = 'aW1wb3J0ICdwYWNrYWdlOmFwcHdyaXRlL2FwcHdyaXRlLmRhcnQnOwoKZmluYWwgY2xpZW50ID0gQ2xpZW50KCkKICAuc2V0RW5kcG9pbnQoJ3tFTkRQT0lOVH0nKQogIC5zZXRQcm9qZWN0KCd7UFJPSkVDVH0nKTsKCmZpbmFsIGFjY291bnQgPSBBY2NvdW50KGNsaWVudCk7CmZpbmFsIGRhdGFiYXNlcyA9IERhdGFiYXNlcyhjbGllbnQpOwpmaW5hbCBzdG9yYWdlID0gU3RvcmFnZShjbGllbnQpOwoK'; const flutter = new Command("flutter") .description("Configure Flutter project to use Appwrite") @@ -56,7 +59,15 @@ const configure = async (options) => { fail("Unable to get project. Try again."); } + // add appwrite dependency + addAppwriteDependency('./pubspec.yaml'); + + //initialize sdk + // TODO check if the file already exists and ask to replace + initializeSDK('./lib/appwrite.dart', project.id, globalConfig.getEndpoint()); + // Which platforms to support? + // TODO show platforms or take from options //get android package name const manifestPath = path.join('./android', 'app/src/main/AndroidManifest.xml'); @@ -116,7 +127,7 @@ const configure = async (options) => { sdk, parseOutput: false }); - log(`Android platform: ${androidPackageName} added successfully`); + success(`Android platform: ${androidPackageName} added successfully`); } if(!exists.ios) { response = await projectsCreatePlatform({ @@ -127,7 +138,7 @@ const configure = async (options) => { sdk, parseOutput: false }); - log(`iOS platform: ${iosBundleId} added successfully`); + success(`iOS platform: ${iosBundleId} added successfully`); } if(!exists.macos) { response = await projectsCreatePlatform({ @@ -138,7 +149,7 @@ const configure = async (options) => { sdk, parseOutput: false }); - log(`MacOS platform: ${macosBundleId} added successfully`); + success(`MacOS platform: ${macosBundleId} added successfully`); } if(!exists.linux) { @@ -150,7 +161,7 @@ const configure = async (options) => { sdk, parseOutput: false }) - log(`Linux platform: ${projectName} added successfully`); + success(`Linux platform: ${projectName} added successfully`); } if(!exists.windows) { @@ -162,12 +173,23 @@ const configure = async (options) => { sdk, parseOutput: false }) - log(`Windows platform: ${projectName} added successfully`); + success(`Windows platform: ${projectName} added successfully`); } } +const initializeSDK = (path, projectId, endpoint) => { + const buffer = Buffer.from(appwriteFile, 'base64'); + + let decodedString = buffer.toString('utf8'); + decodedString = decodedString.replace('{PROJECT}', projectId); + decodedString = decodedString.replace('{ENDPOINT}', endpoint); + + fs.writeFileSync(path, decodedString); + success('SDK initialized successfully'); +} + const getAndroidPackageName = (manifestPath) => { if (!fs.existsSync(manifestPath)) { @@ -190,17 +212,27 @@ const getAndroidPackageName = (manifestPath) => { const getPubspecName = (pubspecPath) => { const yamlFile = fs.readFileSync(pubspecPath, 'utf8'); - const regex = /^name:\s*(.*)$/m; const match = yamlFile.match(regex); - 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); + log('Added appwrite SDK'); + } else { + log('Appwrite SDK already added'); + } +} + const getIOSBundleId = (projectPath) => { if (!fs.existsSync(projectPath)) { return null; From 010f2565cbe8cae3546e9a2ac3e6f917166d7c30 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 8 May 2023 04:58:48 +0000 Subject: [PATCH 04/21] more configurations --- lib/commands/flutter.js | 70 ++++++++++++++++++++++++++++------------- lib/questions.js | 19 ++++++++++- 2 files changed, 66 insertions(+), 23 deletions(-) diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index 87f9c33..173d8be 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -5,7 +5,7 @@ const inquirer = require("inquirer"); const { teamsCreate, teamsList } = require("./teams"); const { projectsCreate, projectsCreatePlatform, projectsListPlatforms } = require("./projects"); const { sdkForConsole } = require("../sdks"); -const { questionsConfigureFlutter } = require("../questions"); +const { questionsConfigureFlutter, questionsFlutterSelectPlatforms } = require("../questions"); const { success, log, actionRunner, commandDescriptions, error } = require("../parser"); const { Command } = require("commander"); const { name } = require("tar/lib/types"); @@ -67,28 +67,54 @@ const configure = async (options) => { initializeSDK('./lib/appwrite.dart', project.id, globalConfig.getEndpoint()); // Which platforms to support? - // TODO show platforms or take from options + 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 - const manifestPath = path.join('./android', 'app/src/main/AndroidManifest.xml'); - let androidPackageName = getAndroidPackageName(manifestPath); - if (!androidPackageName) { - androidPackageName = 'com.example.testandroidapp'; + let androidPackageName = options.androidPackageName; + if(!androidPackageName && platforms.includes('android')) { + const manifestPath = path.join('./android', 'app/src/main/AndroidManifest.xml'); + androidPackageName = getAndroidPackageName(manifestPath); + if(!androidPackageName) { + error('Unable to determine android package name. Please provide using --androidPackageName'); + return; + } + log('Infered Android Package Name: ' + androidPackageName); } - const iosProjectPath = path.join('./ios/', 'Runner.xcodeproj/project.pbxproj'); - let iosBundleId = getIOSBundleId(iosProjectPath); - log('Infered iOS bundle ID: ' + iosBundleId); - const macosConfigPath = path.join('./macos/', 'Runner/Configs/AppInfo.xcconfig') - let macosBundleId = getMacOSBundleId(macosConfigPath); - log('Infered MacOS bundle ID: ' + macosBundleId); + let iosBundleId = options.iosBundleId; + if(!iosBundleId && platforms.includes('ios')) { + const iosProjectPath = path.join('./ios/', 'Runner.xcodeproj/project.pbxproj'); + iosBundleId = getIOSBundleId(iosProjectPath); + if(!androidPackageName) { + error('Unable to determine iOS bundle ID. Please provide using --iosBundleId'); + return; + } + log('Infered iOS bundle ID: ' + iosBundleId); + } + + let macosBundleId = options.macosBundleId; + if(!macosBundleId && platforms.includes('macos')) { + const macosConfigPath = path.join('./macos/', 'Runner/Configs/AppInfo.xcconfig') + macosBundleId = getMacOSBundleId(macosConfigPath); + log('Infered MacOS bundle ID: ' + macosBundleId); + } let projectName = getPubspecName('./pubspec.yaml'); log('Project Name: ' + projectName); response = await projectsListPlatforms({ projectId: project.id, sdk, parseOutput: false }) - let platforms = response.platforms; + let existingPlatforms = response.platforms; let exists = { android: false, @@ -99,7 +125,7 @@ const configure = async (options) => { windows: false, } - platforms.forEach(platform => { + existingPlatforms.forEach(platform => { if (platform.key === androidPackageName && platform.type === 'flutter-android') { exists.android = true; } @@ -118,7 +144,7 @@ const configure = async (options) => { }) // select platform - if(!exists.android) { + if(!exists.android && platforms.includes('android')) { response = await projectsCreatePlatform({ projectId: project.id, type: 'flutter-android', @@ -129,7 +155,7 @@ const configure = async (options) => { }); success(`Android platform: ${androidPackageName} added successfully`); } - if(!exists.ios) { + if(!exists.ios && platforms.includes('ios')) { response = await projectsCreatePlatform({ projectId: project.id, type: 'flutter-ios', @@ -140,7 +166,7 @@ const configure = async (options) => { }); success(`iOS platform: ${iosBundleId} added successfully`); } - if(!exists.macos) { + if(!exists.macos && platforms.includes('macos')) { response = await projectsCreatePlatform({ projectId: project.id, type: 'flutter-macos', @@ -152,7 +178,7 @@ const configure = async (options) => { success(`MacOS platform: ${macosBundleId} added successfully`); } - if(!exists.linux) { + if(!exists.linux && platforms.includes('linux')) { response = await projectsCreatePlatform({ projectId: project.id, type: 'flutter-linux', @@ -164,7 +190,7 @@ const configure = async (options) => { success(`Linux platform: ${projectName} added successfully`); } - if(!exists.windows) { + if(!exists.windows && platforms.includes('windows')) { response = await projectsCreatePlatform({ projectId: project.id, type: 'flutter-windows', @@ -266,9 +292,9 @@ const getMacOSBundleId = (projectPath) => { flutter.command("configure") .description("Configure Flutter Appwrite project") - .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('--platforms', 'Comma separated platforms. If not provided you will be listed with platforms to choose.') + .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('--platforms ', 'Comma separated platforms. If not provided you will be listed with platforms to choose.') .action(actionRunner(configure)); module.exports = { diff --git a/lib/questions.js b/lib/questions.js index e25a866..9f9077c 100644 --- a/lib/questions.js +++ b/lib/questions.js @@ -423,6 +423,22 @@ const questionsConfigureFlutter = [ } ]; +const questionsFlutterSelectPlatforms = [ + { + type: "checkbox", + name: "platforms", + message: "Choose your Platforms to configure.", + choices: [ + 'Android', + 'iOS', + 'Web', + 'Linux', + 'MacOS', + 'Windows', + ] + } +]; + module.exports = { questionsInitProject, questionsLogin, @@ -433,5 +449,6 @@ module.exports = { questionsDeployBuckets, questionsDeployTeams, questionsGetEntrypoint, - questionsConfigureFlutter + questionsConfigureFlutter, + questionsFlutterSelectPlatforms, }; From c97195d37bbdd91bdd61cf8ed3846db7567726c3 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 8 May 2023 05:27:43 +0000 Subject: [PATCH 05/21] cleanup and more checks --- lib/commands/flutter.js | 231 +++++++++++++++++++++------------------- 1 file changed, 122 insertions(+), 109 deletions(-) diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index 173d8be..99b6cbb 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -62,52 +62,26 @@ const configure = async (options) => { // add appwrite dependency addAppwriteDependency('./pubspec.yaml'); - //initialize sdk - // TODO check if the file already exists and ask to replace - initializeSDK('./lib/appwrite.dart', project.id, globalConfig.getEndpoint()); + const appwriteFilePath = './lib/appwrite.dart'; + await initializeSDK(appwriteFilePath, project.id, globalConfig.getEndpoint()); // Which platforms to support? - let platforms = options.platforms?.split(',')?.map(platform=>platform.toLowerCase()); + let platforms = options.platforms?.split(',')?.map(platform => platform.toLowerCase()); - if(!platforms || !platforms.length) { + if (!platforms || !platforms.length) { platforms = await inquirer.prompt(questionsFlutterSelectPlatforms); platforms = platforms.platforms.map(platform => platform.toLowerCase()); } - if(!platforms.length) { + if (!platforms.length) { error('No platforms selected'); return; } //get android package name let androidPackageName = options.androidPackageName; - if(!androidPackageName && platforms.includes('android')) { - const manifestPath = path.join('./android', 'app/src/main/AndroidManifest.xml'); - androidPackageName = getAndroidPackageName(manifestPath); - if(!androidPackageName) { - error('Unable to determine android package name. Please provide using --androidPackageName'); - return; - } - log('Infered Android Package Name: ' + androidPackageName); - } - let iosBundleId = options.iosBundleId; - if(!iosBundleId && platforms.includes('ios')) { - const iosProjectPath = path.join('./ios/', 'Runner.xcodeproj/project.pbxproj'); - iosBundleId = getIOSBundleId(iosProjectPath); - if(!androidPackageName) { - error('Unable to determine iOS bundle ID. Please provide using --iosBundleId'); - return; - } - log('Infered iOS bundle ID: ' + iosBundleId); - } - let macosBundleId = options.macosBundleId; - if(!macosBundleId && platforms.includes('macos')) { - const macosConfigPath = path.join('./macos/', 'Runner/Configs/AppInfo.xcconfig') - macosBundleId = getMacOSBundleId(macosConfigPath); - log('Infered MacOS bundle ID: ' + macosBundleId); - } let projectName = getPubspecName('./pubspec.yaml'); log('Project Name: ' + projectName); @@ -116,98 +90,137 @@ const configure = async (options) => { let existingPlatforms = response.platforms; - let exists = { - android: false, - ios: false, - macos: false, - web: false, - linux: false, - windows: false, - } - - existingPlatforms.forEach(platform => { - if (platform.key === androidPackageName && platform.type === 'flutter-android') { - exists.android = true; - } - if (platform.key === iosBundleId && platform.type === 'flutter-ios') { - exists.ios = true; + // select platform + if (platforms.includes('android')) { + if (!androidPackageName) { + const manifestPath = path.join('./android', 'app/src/main/AndroidManifest.xml'); + androidPackageName = getAndroidPackageName(manifestPath); + if (!androidPackageName) { + error('Unable to determine android package name. Please provide using --androidPackageName'); + return; + } + log('Infered Android Package Name: ' + androidPackageName); } - if (platform.key === macosBundleId && platform.type === 'flutter-macos') { - exists.macos = true; + const exists = existingPlatforms.find(platform => platform.key === androidPackageName && platform.type === 'flutter-android'); + if (!exists) { + response = await projectsCreatePlatform({ + projectId: project.id, + 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 (platform.key === projectName && platform.type === 'flutter-linux') { - exists.linux = true; + } + 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); } - if (platform.key === projectName && platform.type === 'flutter-windows') { - exists.windows = true; + const exists = existingPlatforms.find(platform => platform.key === iosBundleId && platform.type === 'flutter-ios'); + if (!exists) { + response = await projectsCreatePlatform({ + projectId: project.id, + type: 'flutter-ios', + name: `${projectName} (iOS)`, + key: iosBundleId, + sdk, + parseOutput: false + }); + success(`iOS platform: ${iosBundleId} added successfully`); + } else { + success(`iOS platform: ${iosBundleId} already exists`); } - }) - - // select platform - if(!exists.android && platforms.includes('android')) { - response = await projectsCreatePlatform({ - projectId: project.id, - type: 'flutter-android', - name: `${projectName} (android)`, - key: androidPackageName, - sdk, - parseOutput: false - }); - success(`Android platform: ${androidPackageName} added successfully`); } - if(!exists.ios && platforms.includes('ios')) { - response = await projectsCreatePlatform({ - projectId: project.id, - type: 'flutter-ios', - name: `${projectName} (iOS)`, - key: iosBundleId, - sdk, - parseOutput: false - }); - success(`iOS platform: ${iosBundleId} added successfully`); - } - if(!exists.macos && platforms.includes('macos')) { - response = await projectsCreatePlatform({ - projectId: project.id, - type: 'flutter-macos', - name: `${projectName} (MacOS)`, - key: macosBundleId, - sdk, - parseOutput: false - }); - success(`MacOS platform: ${macosBundleId} added successfully`); + 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: project.id, + 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(!exists.linux && platforms.includes('linux')) { - response = await projectsCreatePlatform({ - projectId: project.id, - type: 'flutter-linux', - name: `${projectName} (Linux)`, - key: projectName, - sdk, - parseOutput: false - }) - success(`Linux platform: ${projectName} added successfully`); + if (platforms.includes('linux')) { + const exists = existingPlatforms.find(platform => platform.key === projectName && platform.type === 'flutter-linux'); + if (!exists) { + response = await projectsCreatePlatform({ + projectId: project.id, + 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(!exists.windows && platforms.includes('windows')) { - response = await projectsCreatePlatform({ - projectId: project.id, - type: 'flutter-windows', - name: `${projectName} (Windows)`, - key: projectName, - sdk, - parseOutput: false - }) - success(`Windows platform: ${projectName} added successfully`); + if (platforms.includes('windows')) { + const exists = existingPlatforms.find(platform => platform.key === projectName && platform.type === 'flutter-windows'); + if (!exists) { + response = await projectsCreatePlatform({ + projectId: project.id, + type: 'flutter-windows', + name: `${projectName} (Windows)`, + key: projectName, + sdk, + parseOutput: false + }) + success(`Windows platform: ${projectName} added successfully`); + } else { + success(`Windows platform: ${projectName} already exists`); + } } } -const initializeSDK = (path, projectId, endpoint) => { +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') + } + } const buffer = Buffer.from(appwriteFile, 'base64'); - + let decodedString = buffer.toString('utf8'); decodedString = decodedString.replace('{PROJECT}', projectId); decodedString = decodedString.replace('{ENDPOINT}', endpoint); @@ -250,7 +263,7 @@ const getPubspecName = (pubspecPath) => { const addAppwriteDependency = (pubspecPath) => { // need to add appropriate version ? const file = fs.readFileSync(pubspecPath, 'utf8'); - if(!file.includes('appwrite:')) { + if (!file.includes('appwrite:')) { const out = file.replace('dependencies:', 'dependencies:\n appwrite:'); fs.writeFileSync(pubspecPath, out); log('Added appwrite SDK'); From e40586b7414f7917adca9676463fb595204a7d38 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 8 May 2023 06:01:59 +0000 Subject: [PATCH 06/21] web platform support --- lib/commands/flutter.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index 99b6cbb..6d6a43f 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -201,6 +201,27 @@ const configure = async (options) => { } } + if(platforms.includes('web')) { + if(!options.webHostname) { + error('Please provide --webHostname 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: project.id, + type: 'web', + name: `${projectName} (Windows)`, + hostname: options.webHostname, + sdk, + parseOutput: false + }) + success(`Web platform: ${options.webHostname} added successfully`); + } else { + success(`Web platform: ${options.webHostname} already exists`); + } + } + } @@ -308,6 +329,7 @@ flutter.command("configure") .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('--platforms ', 'Comma separated platforms. If not provided you will be listed with platforms to choose.') + .option('--webHostname ', 'Comma separated platforms. If not provided you will be listed with platforms to choose.') .action(actionRunner(configure)); module.exports = { From 034a6645b083bbf56c71e3331398d47e56d2f0a5 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 8 May 2023 06:03:45 +0000 Subject: [PATCH 07/21] update name --- lib/commands/flutter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index 6d6a43f..bc11b18 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -210,8 +210,8 @@ const configure = async (options) => { if (!exists) { response = await projectsCreatePlatform({ projectId: project.id, - type: 'web', - name: `${projectName} (Windows)`, + type: 'web', // TODO change to `flutter-web` once cloud fixed + name: `${projectName} (Web)`, hostname: options.webHostname, sdk, parseOutput: false From 8ecce7427bb1667c0551cdfe7ec61cb973af8e55 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 8 May 2023 11:46:35 +0000 Subject: [PATCH 08/21] web hostname prompt --- lib/commands/flutter.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index bc11b18..5eb313e 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -202,8 +202,18 @@ const configure = async (options) => { } if(platforms.includes('web')) { - if(!options.webHostname) { - error('Please provide --webHostname to add web platform'); + let hostname = options.webHostname; + 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')); From 464c37746292be89ee009e261d9f71ad0979b2fd Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 9 May 2023 01:27:48 +0000 Subject: [PATCH 09/21] basic generating dart classes fro collections --- lib/commands/flutter.js | 125 +++++++++++++++++++++++++++++++++++++--- lib/questions.js | 64 +++++++++++++++++++- lib/utils.js | 20 ++++++- 3 files changed, 199 insertions(+), 10 deletions(-) diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index 5eb313e..0e8cb4f 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -5,14 +5,18 @@ const inquirer = require("inquirer"); const { teamsCreate, teamsList } = require("./teams"); const { projectsCreate, projectsCreatePlatform, projectsListPlatforms } = require("./projects"); const { sdkForConsole } = require("../sdks"); -const { questionsConfigureFlutter, questionsFlutterSelectPlatforms } = require("../questions"); +const { questionsFlutterConfigure, questionsFlutterSelectPlatforms, questionsFlutterChooseDatabase, questionsFlutterChooseProject } = require("../questions"); const { success, log, actionRunner, commandDescriptions, error } = require("../parser"); const { Command } = require("commander"); -const { name } = require("tar/lib/types"); -const { globalConfig } = require("../config"); +const { globalConfig, localConfig } = require("../config"); +const { databasesList, databasesListCollections } = require("./databases"); +const { sdkForProject } = require("../sdks"); +const { toSnakeCase, toUpperCamelCase } = require("../utils"); const appwriteFile = 'aW1wb3J0ICdwYWNrYWdlOmFwcHdyaXRlL2FwcHdyaXRlLmRhcnQnOwoKZmluYWwgY2xpZW50ID0gQ2xpZW50KCkKICAuc2V0RW5kcG9pbnQoJ3tFTkRQT0lOVH0nKQogIC5zZXRQcm9qZWN0KCd7UFJPSkVDVH0nKTsKCmZpbmFsIGFjY291bnQgPSBBY2NvdW50KGNsaWVudCk7CmZpbmFsIGRhdGFiYXNlcyA9IERhdGFiYXNlcyhjbGllbnQpOwpmaW5hbCBzdG9yYWdlID0gU3RvcmFnZShjbGllbnQpOwoK'; +const modelTemplate = 'Y2xhc3MgJU5BTUUlIHsKICAlQVRUUklCVVRFUyUKCiAgJU5BTUUlKHsKICAgICVDT05TVFJVQ1RPUl9QQVJBTUVURVJTJQogIH0pOwoKICBmYWN0b3J5ICVOQU1FJS5mcm9tTWFwKE1hcDxTdHJpbmcsIGR5bmFtaWM+IG1hcCkgewogICAgcmV0dXJuICVOQU1FJSgKICAgICAgJUNPTlNUUlVDVE9SX0FSR1VNRU5UUyUKICAgICk7CiAgfQoKICBNYXA8U3RyaW5nLCBkeW5hbWljPiB0b01hcCgpIHsKICAgIHJldHVybiB7CiAgICAgICVNQVBfRklFTERTJQogICAgfTsKICB9CgogIEBvdmVycmlkZQogIFN0cmluZyB0b1N0cmluZygpIHsKICAgIHJldHVybiAnJU5BTUUlICcgKyB0b01hcCgpLnRvU3RyaW5nKCk7CiAgfQp9'; + const flutter = new Command("flutter") .description("Configure Flutter project to use Appwrite") .configureHelp({ @@ -22,6 +26,109 @@ const flutter = new Command("flutter") command.help(); })); +const generate = async (options) => { + let modelPath = './lib/models/'; + let answer = await inquirer.prompt(questionsFlutterChooseProject); + if (!answer.project) { + error('You must select a project.'); + return; + } + + localConfig.setProject(answer.project.id, answer.project.name); + + answer = await inquirer.prompt(questionsFlutterChooseDatabase); + + if (!answer.databases.length) { + error('Please select at least one database'); + return; + } + + const sdk = await sdkForProject(); + for (let index in answer.databases) { + let database = answer.databases[index]; + let response = await databasesListCollections({ + databaseId: database.id, + sdk, + parseOutput: false + }); + + let collections = response.collections; + + for (let index in collections) { + const collection = collections[index]; + const filename = toSnakeCase(collection.name) + '.dart'; + const className = toUpperCamelCase(collection.name); + + const buffer = Buffer.from(modelTemplate, 'base64'); + + let template = buffer.toString('utf8'); + let dartClass = generateDartClass(className, collection.attributes, template); + if (!fs.existsSync(modelPath)) { + fs.mkdirSync(modelPath, { recursive: true }); + } + fs.writeFileSync(modelPath + filename, dartClass, { + + }); + } + } +} + + +function generateDartClass(name, attributes, template) { + const getType = (type) => { + switch (type) { + case 'string': + case 'email': + case 'url': + case 'enum': + case 'datetime': + return 'String'; + case 'boolean': + return 'bool'; + case 'integer': + return 'int'; + case 'double': + return 'double'; + } + } + const properties = attributes.map(attr => { + let property = `final ${getType(attr.type)}${(!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 !== null) { + out += ` = ${JSON.stringify(attr.default)}`; + } + return out; + }).join(',\n '); + + const constructorArgs = attributes.map(attr => { + return `${attr.key}: map['${attr.key}']`; + }).join(',\n '); + + const mapFields = attributes.map(attr => { + return `'${attr.key}': ${attr.key}`; + }).join(',\n '); + + // const toStringFields = attributes.map(attr => { + // return `'${attr.key}': ` + '$' + `${attr.key}`; + // }).join(' '); + + return template + .replaceAll('%NAME%', name) + .replace('%ATTRIBUTES%', properties) + .replace('%CONSTRUCTOR_PARAMETERS%', constructorParams) + .replace('%CONSTRUCTOR_ARGUMENTS%', constructorArgs) + .replace('%MAP_FIELDS%', mapFields); +} + const configure = async (options) => { const filePath = path.join('./', 'pubspec.yaml'); if (!fs.existsSync(filePath)) { @@ -30,7 +137,7 @@ const configure = async (options) => { } let response = {} - let answers = await inquirer.prompt(questionsConfigureFlutter) + let answers = await inquirer.prompt(questionsFlutterConfigure) if (!answers.project) process.exit(1) let sdk = await sdkForConsole(); @@ -201,9 +308,9 @@ const configure = async (options) => { } } - if(platforms.includes('web')) { + if (platforms.includes('web')) { let hostname = options.webHostname; - if(!hostname) { + if (!hostname) { let answer = await inquirer.prompt({ type: "input", name: "hostname", @@ -211,7 +318,7 @@ const configure = async (options) => { default: "localhost" },); hostname = answer.hostname; - if(!hostname) { + if (!hostname) { error('Please provide Hostname to add web platform'); } return; @@ -342,6 +449,10 @@ flutter.command("configure") .option('--webHostname ', '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") + .action(actionRunner(generate)); + module.exports = { flutter, } \ No newline at end of file diff --git a/lib/questions.js b/lib/questions.js index 9f9077c..8844733 100644 --- a/lib/questions.js +++ b/lib/questions.js @@ -352,7 +352,7 @@ const questionsDeployTeams = [ }, ] -const questionsConfigureFlutter = [ +const questionsFlutterConfigure = [ { type: "list", name: "start", @@ -439,6 +439,64 @@ const questionsFlutterSelectPlatforms = [ } ]; +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, @@ -449,6 +507,8 @@ module.exports = { questionsDeployBuckets, questionsDeployTeams, questionsGetEntrypoint, - questionsConfigureFlutter, + questionsFlutterConfigure, questionsFlutterSelectPlatforms, + questionsFlutterChooseDatabase, + questionsFlutterChooseProject, }; diff --git a/lib/utils.js b/lib/utils.js index cb7d06c..58543fa 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -14,6 +14,24 @@ 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 and 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 fd5e651acc2dc8d4b830971a2bb3bab047c69a4f Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 9 May 2023 01:49:00 +0000 Subject: [PATCH 10/21] fix --- lib/commands/flutter.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index 0e8cb4f..c8a62a5 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -1,14 +1,13 @@ const fs = require("fs"); const path = require("path"); -const childProcess = require("child_process"); const inquirer = require("inquirer"); -const { teamsCreate, teamsList } = require("./teams"); +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, commandDescriptions, error } = require("../parser"); +const { success, log, actionRunner, error } = require("../parser"); const { Command } = require("commander"); -const { globalConfig, localConfig } = require("../config"); +const { globalConfig } = require("../config"); const { databasesList, databasesListCollections } = require("./databases"); const { sdkForProject } = require("../sdks"); const { toSnakeCase, toUpperCamelCase } = require("../utils"); @@ -34,7 +33,7 @@ const generate = async (options) => { return; } - localConfig.setProject(answer.project.id, answer.project.name); + globalConfig.setProject(answer.project.id, answer.project.name); answer = await inquirer.prompt(questionsFlutterChooseDatabase); From 06f8c3fb42f380acff5e610c215f72c76bf8bab3 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 9 May 2023 06:22:43 +0000 Subject: [PATCH 11/21] generator options --- lib/commands/flutter.js | 48 ++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index c8a62a5..e0a3a21 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -26,27 +26,39 @@ const flutter = new Command("flutter") })); const generate = async (options) => { - let modelPath = './lib/models/'; - let answer = await inquirer.prompt(questionsFlutterChooseProject); - if (!answer.project) { - error('You must select a project.'); - return; + let modelPath = options.modelPath ?? './lib/models/'; + if (!modelPath.endsWith('/')) { + modelPath += '/'; + } + let projectId = options.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; } - globalConfig.setProject(answer.project.id, answer.project.name); + globalConfig.setProject(projectId); - answer = await inquirer.prompt(questionsFlutterChooseDatabase); - - if (!answer.databases.length) { - error('Please select at least one database'); - return; + 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 answer.databases) { - let database = answer.databases[index]; + for (let index in databaseIds) { + let id = databaseIds[index]; let response = await databasesListCollections({ - databaseId: database.id, + databaseId: id, sdk, parseOutput: false }); @@ -65,9 +77,8 @@ const generate = async (options) => { if (!fs.existsSync(modelPath)) { fs.mkdirSync(modelPath, { recursive: true }); } - fs.writeFileSync(modelPath + filename, dartClass, { - - }); + fs.writeFileSync(modelPath + filename, dartClass); + success(`Generated ${className} class and saved to ${modelPath + filename}`); } } } @@ -450,6 +461,9 @@ flutter.command("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 = { From 1cc333d0e0ac4d3f29c9a7b2c132e360dd811113 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 9 May 2023 06:42:24 +0000 Subject: [PATCH 12/21] handle array type --- lib/commands/flutter.js | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index e0a3a21..c35fa47 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -14,7 +14,7 @@ const { toSnakeCase, toUpperCamelCase } = require("../utils"); const appwriteFile = 'aW1wb3J0ICdwYWNrYWdlOmFwcHdyaXRlL2FwcHdyaXRlLmRhcnQnOwoKZmluYWwgY2xpZW50ID0gQ2xpZW50KCkKICAuc2V0RW5kcG9pbnQoJ3tFTkRQT0lOVH0nKQogIC5zZXRQcm9qZWN0KCd7UFJPSkVDVH0nKTsKCmZpbmFsIGFjY291bnQgPSBBY2NvdW50KGNsaWVudCk7CmZpbmFsIGRhdGFiYXNlcyA9IERhdGFiYXNlcyhjbGllbnQpOwpmaW5hbCBzdG9yYWdlID0gU3RvcmFnZShjbGllbnQpOwoK'; -const modelTemplate = 'Y2xhc3MgJU5BTUUlIHsKICAlQVRUUklCVVRFUyUKCiAgJU5BTUUlKHsKICAgICVDT05TVFJVQ1RPUl9QQVJBTUVURVJTJQogIH0pOwoKICBmYWN0b3J5ICVOQU1FJS5mcm9tTWFwKE1hcDxTdHJpbmcsIGR5bmFtaWM+IG1hcCkgewogICAgcmV0dXJuICVOQU1FJSgKICAgICAgJUNPTlNUUlVDVE9SX0FSR1VNRU5UUyUKICAgICk7CiAgfQoKICBNYXA8U3RyaW5nLCBkeW5hbWljPiB0b01hcCgpIHsKICAgIHJldHVybiB7CiAgICAgICVNQVBfRklFTERTJQogICAgfTsKICB9CgogIEBvdmVycmlkZQogIFN0cmluZyB0b1N0cmluZygpIHsKICAgIHJldHVybiAnJU5BTUUlICcgKyB0b01hcCgpLnRvU3RyaW5nKCk7CiAgfQp9'; +const modelTemplate = 'Y2xhc3MgJU5BTUUlIHsKICBmaW5hbCBTdHJpbmcgJGlkOwogIGZpbmFsIFN0cmluZyAkY3JlYXRlZEF0OwogIGZpbmFsIFN0cmluZyAkdXBkYXRlZEF0OwogIGZpbmFsIFN0cmluZyAkY29sbGVjdGlvbklkOwogIGZpbmFsIFN0cmluZyAkZGF0YWJhc2VJZDsKICBmaW5hbCBMaXN0PFN0cmluZz4gJHBlcm1pc3Npb25zOwoKICAlQVRUUklCVVRFUyUKCiAgJU5BTUUlKHsKICAgIHJlcXVpcmVkIHRoaXMuJGlkLAogICAgcmVxdWlyZWQgdGhpcy4kY3JlYXRlZEF0LAogICAgcmVxdWlyZWQgdGhpcy4kdXBkYXRlZEF0LAogICAgcmVxdWlyZWQgdGhpcy4kY29sbGVjdGlvbklkLAogICAgcmVxdWlyZWQgdGhpcy4kcGVybWlzc2lvbnMsCiAgICAlQ09OU1RSVUNUT1JfUEFSQU1FVEVSUyUKICB9KTsKCiAgZmFjdG9yeSAlTkFNRSUuZnJvbU1hcChNYXA8U3RyaW5nLCBkeW5hbWljPiBtYXApIHsKICAgIHJldHVybiAlTkFNRSUoCiAgICAgICRpZDogbWFwWydcJGlkJ10sCiAgICAgICRjcmVhdGVkQXQ6IG1hcFsnXCRjcmVhdGVkQXQnXSwKICAgICAgJHVwZGF0ZWRBdDogbWFwWydcJHVwZGF0ZWRBdCddLAogICAgICAkY29sbGVjdGlvbklkOiBtYXBbJ1wkY29sbGVjdGlvbklkJ10sCiAgICAgICRwZXJtaXNzaW9uczogTGlzdDxTdHJpbmc+LmZyb20obWFwWydcJHBlcm1pc3Npb25zJ10pLAogICAgICAkZGF0YWJhc2VJZDogbWFwWydcJGRhdGFiYXNlSWQnXSwKICAgICAgJUNPTlNUUlVDVE9SX0FSR1VNRU5UUyUKICAgICk7CiAgfQoKICBNYXA8U3RyaW5nLCBkeW5hbWljPiB0b01hcCgpIHsKICAgIHJldHVybiB7CiAgICAgICdcJGlkJzogJGlkLAogICAgICAnXCRjcmVhdGVkQXQnOiAkY3JlYXRlZEF0LAogICAgICAnXCR1cGRhdGVkQXQnOiAkdXBkYXRlZEF0LAogICAgICAnXCRjb2xsZWN0aW9uSWQnOiAkY29sbGVjdGlvbklkLAogICAgICAnXCRwZXJtaXNzaW9ucyc6ICRwZXJtaXNzaW9ucywKICAgICAgJ1wkZGF0YWJhc2VJZCc6ICRkYXRhYmFzZUlkLAogICAgICAlTUFQX0ZJRUxEUyUKICAgIH07CiAgfQoKICBAb3ZlcnJpZGUKICBTdHJpbmcgdG9TdHJpbmcoKSB7CiAgICByZXR1cm4gJyVOQU1FJSAnICsgdG9NYXAoKS50b1N0cmluZygpOwogIH0KfQ=='; const flutter = new Command("flutter") .description("Configure Flutter project to use Appwrite") @@ -29,11 +29,11 @@ const generate = async (options) => { let modelPath = options.modelPath ?? './lib/models/'; if (!modelPath.endsWith('/')) { modelPath += '/'; - } + } let projectId = options.projectId; let databaseIds = options.databaseIds?.split(','); - if(!projectId) { + if (!projectId) { let answer = await inquirer.prompt(questionsFlutterChooseProject); if (!answer.project) { error('You must select a project.'); @@ -44,9 +44,9 @@ const generate = async (options) => { globalConfig.setProject(projectId); - if(!databaseIds) { + if (!databaseIds) { answer = await inquirer.prompt(questionsFlutterChooseDatabase); - + if (!answer.databases.length) { error('Please select at least one database'); return; @@ -85,24 +85,24 @@ const generate = async (options) => { function generateDartClass(name, attributes, template) { - const getType = (type) => { + const getType = (type, isArray = false) => { switch (type) { case 'string': case 'email': case 'url': case 'enum': case 'datetime': - return 'String'; + return isArray ? 'List' : 'String'; case 'boolean': - return 'bool'; + return isArray ? 'List' : 'bool'; case 'integer': - return 'int'; + return isArray ? 'List' : 'int'; case 'double': - return 'double'; + return isArray ? 'List' : 'double'; } } const properties = attributes.map(attr => { - let property = `final ${getType(attr.type)}${(!attr.required) ? '?': ''} ${attr.key}`; + let property = `final ${getType(attr.type, attr.array)}${(!attr.required) ? '?' : ''} ${attr.key}`; property += ';'; return property; }).join('\n '); @@ -120,17 +120,13 @@ function generateDartClass(name, attributes, template) { }).join(',\n '); const constructorArgs = attributes.map(attr => { - return `${attr.key}: map['${attr.key}']`; + return `${attr.key}: ${attr.array ? getType(attr.type, attr.array) + '.from(' : ''}map['${attr.key}'] ${attr.array ? ' ?? [])' : ''}`; }).join(',\n '); const mapFields = attributes.map(attr => { return `'${attr.key}': ${attr.key}`; }).join(',\n '); - // const toStringFields = attributes.map(attr => { - // return `'${attr.key}': ` + '$' + `${attr.key}`; - // }).join(' '); - return template .replaceAll('%NAME%', name) .replace('%ATTRIBUTES%', properties) From 24fa7e6ee75013418d507acdd3babfc42661b7f7 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 15 May 2023 08:33:28 +0000 Subject: [PATCH 13/21] support for relationship attribute --- lib/commands/flutter.js | 48 +++++++++++++++++++++++++++++++---------- lib/utils.js | 8 ++++--- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index c35fa47..28a91f9 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -67,8 +67,8 @@ const generate = async (options) => { for (let index in collections) { const collection = collections[index]; - const filename = toSnakeCase(collection.name) + '.dart'; - const className = toUpperCamelCase(collection.name); + const filename = toSnakeCase(collection.$id) + '.dart'; + const className = toUpperCamelCase(collection.$id); const buffer = Buffer.from(modelTemplate, 'base64'); @@ -85,24 +85,43 @@ const generate = async (options) => { function generateDartClass(name, attributes, template) { - const getType = (type, isArray = false) => { - switch (type) { + const getType = (attribute) => { + switch (attribute.type) { case 'string': case 'email': case 'url': case 'enum': case 'datetime': - return isArray ? 'List' : 'String'; + return attribute.array ? 'List' : 'String'; case 'boolean': - return isArray ? 'List' : 'bool'; + return attribute.array ? 'List' : 'bool'; case 'integer': - return isArray ? 'List' : 'int'; + return attribute.array ? 'List' : 'int'; case 'double': - return isArray ? 'List' : 'double'; + return attribute.array ? 'List' : 'double'; + case 'relationship': + 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.type, attr.array)}${(!attr.required) ? '?' : ''} ${attr.key}`; + let property = `final ${getType(attr)}${(!attr.required) ? '?' : ''} ${attr.key}`; property += ';'; return property; }).join('\n '); @@ -120,11 +139,18 @@ function generateDartClass(name, attributes, template) { }).join(',\n '); const constructorArgs = attributes.map(attr => { - return `${attr.key}: ${attr.array ? getType(attr.type, attr.array) + '.from(' : ''}map['${attr.key}'] ${attr.array ? ' ?? [])' : ''}`; + return `${attr.key}: ${getFromMap(attr)}`; }).join(',\n '); const mapFields = attributes.map(attr => { - return `'${attr.key}': ${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}`; }).join(',\n '); return template diff --git a/lib/utils.js b/lib/utils.js index 58543fa..fee5e65 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -23,12 +23,14 @@ function toSnakeCase(str) { } function toUpperCamelCase(str) { - // Split string into words and capitalize first letter of each word - const words = str.split(/\s+/).map(word => word.charAt(0).toUpperCase() + word.slice(1)); + // 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, From bfc4ca82158c49a4abfb33871e96874654f05291 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 16 May 2023 07:50:59 +0545 Subject: [PATCH 14/21] fix imports and relation attribute --- lib/commands/flutter.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index 28a91f9..0052191 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -14,7 +14,7 @@ const { toSnakeCase, toUpperCamelCase } = require("../utils"); const appwriteFile = 'aW1wb3J0ICdwYWNrYWdlOmFwcHdyaXRlL2FwcHdyaXRlLmRhcnQnOwoKZmluYWwgY2xpZW50ID0gQ2xpZW50KCkKICAuc2V0RW5kcG9pbnQoJ3tFTkRQT0lOVH0nKQogIC5zZXRQcm9qZWN0KCd7UFJPSkVDVH0nKTsKCmZpbmFsIGFjY291bnQgPSBBY2NvdW50KGNsaWVudCk7CmZpbmFsIGRhdGFiYXNlcyA9IERhdGFiYXNlcyhjbGllbnQpOwpmaW5hbCBzdG9yYWdlID0gU3RvcmFnZShjbGllbnQpOwoK'; -const modelTemplate = 'Y2xhc3MgJU5BTUUlIHsKICBmaW5hbCBTdHJpbmcgJGlkOwogIGZpbmFsIFN0cmluZyAkY3JlYXRlZEF0OwogIGZpbmFsIFN0cmluZyAkdXBkYXRlZEF0OwogIGZpbmFsIFN0cmluZyAkY29sbGVjdGlvbklkOwogIGZpbmFsIFN0cmluZyAkZGF0YWJhc2VJZDsKICBmaW5hbCBMaXN0PFN0cmluZz4gJHBlcm1pc3Npb25zOwoKICAlQVRUUklCVVRFUyUKCiAgJU5BTUUlKHsKICAgIHJlcXVpcmVkIHRoaXMuJGlkLAogICAgcmVxdWlyZWQgdGhpcy4kY3JlYXRlZEF0LAogICAgcmVxdWlyZWQgdGhpcy4kdXBkYXRlZEF0LAogICAgcmVxdWlyZWQgdGhpcy4kY29sbGVjdGlvbklkLAogICAgcmVxdWlyZWQgdGhpcy4kcGVybWlzc2lvbnMsCiAgICAlQ09OU1RSVUNUT1JfUEFSQU1FVEVSUyUKICB9KTsKCiAgZmFjdG9yeSAlTkFNRSUuZnJvbU1hcChNYXA8U3RyaW5nLCBkeW5hbWljPiBtYXApIHsKICAgIHJldHVybiAlTkFNRSUoCiAgICAgICRpZDogbWFwWydcJGlkJ10sCiAgICAgICRjcmVhdGVkQXQ6IG1hcFsnXCRjcmVhdGVkQXQnXSwKICAgICAgJHVwZGF0ZWRBdDogbWFwWydcJHVwZGF0ZWRBdCddLAogICAgICAkY29sbGVjdGlvbklkOiBtYXBbJ1wkY29sbGVjdGlvbklkJ10sCiAgICAgICRwZXJtaXNzaW9uczogTGlzdDxTdHJpbmc+LmZyb20obWFwWydcJHBlcm1pc3Npb25zJ10pLAogICAgICAkZGF0YWJhc2VJZDogbWFwWydcJGRhdGFiYXNlSWQnXSwKICAgICAgJUNPTlNUUlVDVE9SX0FSR1VNRU5UUyUKICAgICk7CiAgfQoKICBNYXA8U3RyaW5nLCBkeW5hbWljPiB0b01hcCgpIHsKICAgIHJldHVybiB7CiAgICAgICdcJGlkJzogJGlkLAogICAgICAnXCRjcmVhdGVkQXQnOiAkY3JlYXRlZEF0LAogICAgICAnXCR1cGRhdGVkQXQnOiAkdXBkYXRlZEF0LAogICAgICAnXCRjb2xsZWN0aW9uSWQnOiAkY29sbGVjdGlvbklkLAogICAgICAnXCRwZXJtaXNzaW9ucyc6ICRwZXJtaXNzaW9ucywKICAgICAgJ1wkZGF0YWJhc2VJZCc6ICRkYXRhYmFzZUlkLAogICAgICAlTUFQX0ZJRUxEUyUKICAgIH07CiAgfQoKICBAb3ZlcnJpZGUKICBTdHJpbmcgdG9TdHJpbmcoKSB7CiAgICByZXR1cm4gJyVOQU1FJSAnICsgdG9NYXAoKS50b1N0cmluZygpOwogIH0KfQ=='; +const modelTemplate = 'JUlNUE9SVFMlCmNsYXNzICVOQU1FJSB7CiAgZmluYWwgU3RyaW5nICRpZDsKICBmaW5hbCBTdHJpbmcgJGNyZWF0ZWRBdDsKICBmaW5hbCBTdHJpbmcgJHVwZGF0ZWRBdDsKICBmaW5hbCBTdHJpbmcgJGNvbGxlY3Rpb25JZDsKICBmaW5hbCBTdHJpbmcgJGRhdGFiYXNlSWQ7CiAgZmluYWwgTGlzdDxTdHJpbmc+ICRwZXJtaXNzaW9uczsKCiAgJUFUVFJJQlVURVMlCgogICVOQU1FJSh7CiAgICByZXF1aXJlZCB0aGlzLiRpZCwKICAgIHJlcXVpcmVkIHRoaXMuJGNyZWF0ZWRBdCwKICAgIHJlcXVpcmVkIHRoaXMuJHVwZGF0ZWRBdCwKICAgIHJlcXVpcmVkIHRoaXMuJGNvbGxlY3Rpb25JZCwKICAgIHJlcXVpcmVkIHRoaXMuJHBlcm1pc3Npb25zLAogICAgcmVxdWlyZWQgdGhpcy4kZGF0YWJhc2VJZCwKICAgICVDT05TVFJVQ1RPUl9QQVJBTUVURVJTJQogIH0pOwoKICBmYWN0b3J5ICVOQU1FJS5mcm9tTWFwKE1hcDxTdHJpbmcsIGR5bmFtaWM+IG1hcCkgewogICAgcmV0dXJuICVOQU1FJSgKICAgICAgJGlkOiBtYXBbJ1wkaWQnXSwKICAgICAgJGNyZWF0ZWRBdDogbWFwWydcJGNyZWF0ZWRBdCddLAogICAgICAkdXBkYXRlZEF0OiBtYXBbJ1wkdXBkYXRlZEF0J10sCiAgICAgICRjb2xsZWN0aW9uSWQ6IG1hcFsnXCRjb2xsZWN0aW9uSWQnXSwKICAgICAgJHBlcm1pc3Npb25zOiBMaXN0PFN0cmluZz4uZnJvbShtYXBbJ1wkcGVybWlzc2lvbnMnXSksCiAgICAgICRkYXRhYmFzZUlkOiBtYXBbJ1wkZGF0YWJhc2VJZCddLAogICAgICAlQ09OU1RSVUNUT1JfQVJHVU1FTlRTJQogICAgKTsKICB9CgogIE1hcDxTdHJpbmcsIGR5bmFtaWM+IHRvTWFwKCkgewogICAgcmV0dXJuIHsKICAgICAgJ1wkaWQnOiAkaWQsCiAgICAgICdcJGNyZWF0ZWRBdCc6ICRjcmVhdGVkQXQsCiAgICAgICdcJHVwZGF0ZWRBdCc6ICR1cGRhdGVkQXQsCiAgICAgICdcJGNvbGxlY3Rpb25JZCc6ICRjb2xsZWN0aW9uSWQsCiAgICAgICdcJHBlcm1pc3Npb25zJzogJHBlcm1pc3Npb25zLAogICAgICAnXCRkYXRhYmFzZUlkJzogJGRhdGFiYXNlSWQsCiAgICAgICVNQVBfRklFTERTJQogICAgfTsKICB9CgogIEBvdmVycmlkZQogIFN0cmluZyB0b1N0cmluZygpIHsKICAgIHJldHVybiAnJU5BTUUlICcgKyB0b01hcCgpLnRvU3RyaW5nKCk7CiAgfQp9'; const flutter = new Command("flutter") .description("Configure Flutter project to use Appwrite") @@ -85,6 +85,8 @@ const generate = async (options) => { function generateDartClass(name, attributes, template) { + let imports = ''; + const getType = (attribute) => { switch (attribute.type) { case 'string': @@ -100,6 +102,10 @@ function generateDartClass(name, attributes, template) { 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)}>`; @@ -132,7 +138,7 @@ function generateDartClass(name, attributes, template) { out += 'required '; } out += `this.${attr.key}`; - if (attr.default !== null) { + if (attr.default && attr.default !== null) { out += ` = ${JSON.stringify(attr.default)}`; } return out; @@ -155,6 +161,7 @@ function generateDartClass(name, attributes, template) { return template .replaceAll('%NAME%', name) + .replace('%IMPORTS%', imports) .replace('%ATTRIBUTES%', properties) .replace('%CONSTRUCTOR_PARAMETERS%', constructorParams) .replace('%CONSTRUCTOR_ARGUMENTS%', constructorArgs) From 74e08579dc9a4fca55d4cd8c8d2193e910fa7e1e Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 16 May 2023 10:32:20 +0545 Subject: [PATCH 15/21] use local config --- lib/commands/flutter.js | 70 +++++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index 0052191..3656275 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -7,7 +7,7 @@ const { sdkForConsole } = require("../sdks"); const { questionsFlutterConfigure, questionsFlutterSelectPlatforms, questionsFlutterChooseDatabase, questionsFlutterChooseProject } = require("../questions"); const { success, log, actionRunner, error } = require("../parser"); const { Command } = require("commander"); -const { globalConfig } = require("../config"); +const { globalConfig, localConfig } = require("../config"); const { databasesList, databasesListCollections } = require("./databases"); const { sdkForProject } = require("../sdks"); const { toSnakeCase, toUpperCamelCase } = require("../utils"); @@ -30,7 +30,7 @@ const generate = async (options) => { if (!modelPath.endsWith('/')) { modelPath += '/'; } - let projectId = options.projectId; + let projectId = options.projectId ?? localConfig.getProject().projectId; let databaseIds = options.databaseIds?.split(','); if (!projectId) { @@ -40,9 +40,9 @@ const generate = async (options) => { return; } projectId = answer.project.id; + localConfig.setProject(projectId); } - globalConfig.setProject(projectId); if (!databaseIds) { answer = await inquirer.prompt(questionsFlutterChooseDatabase); @@ -175,41 +175,49 @@ const configure = async (options) => { return; } - let response = {} - let answers = await inquirer.prompt(questionsFlutterConfigure) - if (!answers.project) process.exit(1) - - let sdk = await sdkForConsole(); - let project = {}; - if (answers.start == "new") { - response = await teamsCreate({ - teamId: 'unique()', - name: answers.project, - sdk, - parseOutput: false - }) + let projectId = options.projectId ?? localConfig.getProject().projectId; - let teamId = response['$id']; - response = await projectsCreate({ - projectId: answers.id, - name: answers.project, - teamId, - parseOutput: false - }) + let response = {} - project = response; - } else { - project = answers.project; - } - if (!project.id) { - fail("Unable to get project. Try again."); + if(!projectId) { + + let answers = await inquirer.prompt(questionsFlutterConfigure) + if (!answers.project) process.exit(1) + + let sdk = await sdkForConsole(); + let project = {}; + if (answers.start == "new") { + 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, project.id, globalConfig.getEndpoint()); + await initializeSDK(appwriteFilePath, projectId, globalConfig.getEndpoint()); // Which platforms to support? let platforms = options.platforms?.split(',')?.map(platform => platform.toLowerCase()); @@ -443,7 +451,7 @@ const addAppwriteDependency = (pubspecPath) => { if (!file.includes('appwrite:')) { const out = file.replace('dependencies:', 'dependencies:\n appwrite:'); fs.writeFileSync(pubspecPath, out); - log('Added appwrite SDK'); + success('Added appwrite SDK'); } else { log('Appwrite SDK already added'); } From b58bc2b3f7defc0cdd753376ffd123fb8ba32cc9 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 16 May 2023 10:46:26 +0545 Subject: [PATCH 16/21] finetuning configure command for different errors --- lib/commands/flutter.js | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index 3656275..32f2a27 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -177,17 +177,16 @@ const configure = async (options) => { let projectId = options.projectId ?? localConfig.getProject().projectId; - let response = {} + let sdk = await sdkForConsole(); if(!projectId) { let answers = await inquirer.prompt(questionsFlutterConfigure) if (!answers.project) process.exit(1) - let sdk = await sdkForConsole(); let project = {}; if (answers.start == "new") { - response = await teamsCreate({ + let response = await teamsCreate({ teamId: 'unique()', name: answers.project, sdk, @@ -236,11 +235,17 @@ const configure = async (options) => { 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: project.id, sdk, parseOutput: false }) + response = await projectsListPlatforms({ projectId: projectId, sdk, parseOutput: false }) let existingPlatforms = response.platforms; @@ -258,7 +263,7 @@ const configure = async (options) => { const exists = existingPlatforms.find(platform => platform.key === androidPackageName && platform.type === 'flutter-android'); if (!exists) { response = await projectsCreatePlatform({ - projectId: project.id, + projectId: projectId, type: 'flutter-android', name: `${projectName} (android)`, key: androidPackageName, @@ -283,7 +288,7 @@ const configure = async (options) => { const exists = existingPlatforms.find(platform => platform.key === iosBundleId && platform.type === 'flutter-ios'); if (!exists) { response = await projectsCreatePlatform({ - projectId: project.id, + projectId: projectId, type: 'flutter-ios', name: `${projectName} (iOS)`, key: iosBundleId, @@ -308,7 +313,7 @@ const configure = async (options) => { const exists = existingPlatforms.find(platform => platform.key === macosBundleId && platform.type === 'flutter-macos'); if (!exists) { response = await projectsCreatePlatform({ - projectId: project.id, + projectId: projectId, type: 'flutter-macos', name: `${projectName} (MacOS)`, key: macosBundleId, @@ -325,7 +330,7 @@ const configure = async (options) => { const exists = existingPlatforms.find(platform => platform.key === projectName && platform.type === 'flutter-linux'); if (!exists) { response = await projectsCreatePlatform({ - projectId: project.id, + projectId: projectId, type: 'flutter-linux', name: `${projectName} (Linux)`, key: projectName, @@ -342,7 +347,7 @@ const configure = async (options) => { const exists = existingPlatforms.find(platform => platform.key === projectName && platform.type === 'flutter-windows'); if (!exists) { response = await projectsCreatePlatform({ - projectId: project.id, + projectId: projectId, type: 'flutter-windows', name: `${projectName} (Windows)`, key: projectName, @@ -356,7 +361,6 @@ const configure = async (options) => { } if (platforms.includes('web')) { - let hostname = options.webHostname; if (!hostname) { let answer = await inquirer.prompt({ type: "input", @@ -373,7 +377,7 @@ const configure = async (options) => { 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: project.id, + projectId: projectId, type: 'web', // TODO change to `flutter-web` once cloud fixed name: `${projectName} (Web)`, hostname: options.webHostname, @@ -415,7 +419,6 @@ const initializeSDK = async (path, projectId, endpoint) => { } const getAndroidPackageName = (manifestPath) => { - if (!fs.existsSync(manifestPath)) { return null; } @@ -439,6 +442,9 @@ const getPubspecName = (pubspecPath) => { const regex = /^name:\s*(.*)$/m; const match = yamlFile.match(regex); + if(!match || match.length < 2) { + return null; + } const name = match[1]; return name; @@ -490,10 +496,12 @@ 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.') .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.') - .option('--webHostname ', 'Comma separated platforms. If not provided you will be listed with platforms to choose.') .action(actionRunner(configure)); flutter.command("generate") From bf57512d544de3c87260a195e1fc905590caf36c Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Sun, 21 May 2023 07:14:21 +0545 Subject: [PATCH 17/21] update review suggestions --- flutter_res/appwrite.dart | 10 +++++ flutter_res/template_model.dart | 50 ++++++++++++++++++++++++ lib/commands/flutter.js | 68 +++++++++++++++++---------------- 3 files changed, 95 insertions(+), 33 deletions(-) create mode 100644 flutter_res/appwrite.dart create mode 100644 flutter_res/template_model.dart diff --git a/flutter_res/appwrite.dart b/flutter_res/appwrite.dart new file mode 100644 index 0000000..b410ad6 --- /dev/null +++ b/flutter_res/appwrite.dart @@ -0,0 +1,10 @@ +import 'package:appwrite/appwrite.dart'; + +final client = Client() + .setEndpoint('{ENDPOINT}') + .setProject('{PROJECT}'); + +final account = Account(client); +final databases = Databases(client); +final storage = Storage(client); + diff --git a/flutter_res/template_model.dart b/flutter_res/template_model.dart new file mode 100644 index 0000000..6cb5cf6 --- /dev/null +++ b/flutter_res/template_model.dart @@ -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/lib/commands/flutter.js b/lib/commands/flutter.js index 32f2a27..7d84610 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -12,10 +12,6 @@ const { databasesList, databasesListCollections } = require("./databases"); const { sdkForProject } = require("../sdks"); const { toSnakeCase, toUpperCamelCase } = require("../utils"); -const appwriteFile = 'aW1wb3J0ICdwYWNrYWdlOmFwcHdyaXRlL2FwcHdyaXRlLmRhcnQnOwoKZmluYWwgY2xpZW50ID0gQ2xpZW50KCkKICAuc2V0RW5kcG9pbnQoJ3tFTkRQT0lOVH0nKQogIC5zZXRQcm9qZWN0KCd7UFJPSkVDVH0nKTsKCmZpbmFsIGFjY291bnQgPSBBY2NvdW50KGNsaWVudCk7CmZpbmFsIGRhdGFiYXNlcyA9IERhdGFiYXNlcyhjbGllbnQpOwpmaW5hbCBzdG9yYWdlID0gU3RvcmFnZShjbGllbnQpOwoK'; - -const modelTemplate = 'JUlNUE9SVFMlCmNsYXNzICVOQU1FJSB7CiAgZmluYWwgU3RyaW5nICRpZDsKICBmaW5hbCBTdHJpbmcgJGNyZWF0ZWRBdDsKICBmaW5hbCBTdHJpbmcgJHVwZGF0ZWRBdDsKICBmaW5hbCBTdHJpbmcgJGNvbGxlY3Rpb25JZDsKICBmaW5hbCBTdHJpbmcgJGRhdGFiYXNlSWQ7CiAgZmluYWwgTGlzdDxTdHJpbmc+ICRwZXJtaXNzaW9uczsKCiAgJUFUVFJJQlVURVMlCgogICVOQU1FJSh7CiAgICByZXF1aXJlZCB0aGlzLiRpZCwKICAgIHJlcXVpcmVkIHRoaXMuJGNyZWF0ZWRBdCwKICAgIHJlcXVpcmVkIHRoaXMuJHVwZGF0ZWRBdCwKICAgIHJlcXVpcmVkIHRoaXMuJGNvbGxlY3Rpb25JZCwKICAgIHJlcXVpcmVkIHRoaXMuJHBlcm1pc3Npb25zLAogICAgcmVxdWlyZWQgdGhpcy4kZGF0YWJhc2VJZCwKICAgICVDT05TVFJVQ1RPUl9QQVJBTUVURVJTJQogIH0pOwoKICBmYWN0b3J5ICVOQU1FJS5mcm9tTWFwKE1hcDxTdHJpbmcsIGR5bmFtaWM+IG1hcCkgewogICAgcmV0dXJuICVOQU1FJSgKICAgICAgJGlkOiBtYXBbJ1wkaWQnXSwKICAgICAgJGNyZWF0ZWRBdDogbWFwWydcJGNyZWF0ZWRBdCddLAogICAgICAkdXBkYXRlZEF0OiBtYXBbJ1wkdXBkYXRlZEF0J10sCiAgICAgICRjb2xsZWN0aW9uSWQ6IG1hcFsnXCRjb2xsZWN0aW9uSWQnXSwKICAgICAgJHBlcm1pc3Npb25zOiBMaXN0PFN0cmluZz4uZnJvbShtYXBbJ1wkcGVybWlzc2lvbnMnXSksCiAgICAgICRkYXRhYmFzZUlkOiBtYXBbJ1wkZGF0YWJhc2VJZCddLAogICAgICAlQ09OU1RSVUNUT1JfQVJHVU1FTlRTJQogICAgKTsKICB9CgogIE1hcDxTdHJpbmcsIGR5bmFtaWM+IHRvTWFwKCkgewogICAgcmV0dXJuIHsKICAgICAgJ1wkaWQnOiAkaWQsCiAgICAgICdcJGNyZWF0ZWRBdCc6ICRjcmVhdGVkQXQsCiAgICAgICdcJHVwZGF0ZWRBdCc6ICR1cGRhdGVkQXQsCiAgICAgICdcJGNvbGxlY3Rpb25JZCc6ICRjb2xsZWN0aW9uSWQsCiAgICAgICdcJHBlcm1pc3Npb25zJzogJHBlcm1pc3Npb25zLAogICAgICAnXCRkYXRhYmFzZUlkJzogJGRhdGFiYXNlSWQsCiAgICAgICVNQVBfRklFTERTJQogICAgfTsKICB9CgogIEBvdmVycmlkZQogIFN0cmluZyB0b1N0cmluZygpIHsKICAgIHJldHVybiAnJU5BTUUlICcgKyB0b01hcCgpLnRvU3RyaW5nKCk7CiAgfQp9'; - const flutter = new Command("flutter") .description("Configure Flutter project to use Appwrite") .configureHelp({ @@ -40,7 +36,7 @@ const generate = async (options) => { return; } projectId = answer.project.id; - localConfig.setProject(projectId); + localConfig.setProject(projectId, answer.project.name); } @@ -70,9 +66,7 @@ const generate = async (options) => { const filename = toSnakeCase(collection.$id) + '.dart'; const className = toUpperCamelCase(collection.$id); - const buffer = Buffer.from(modelTemplate, 'base64'); - - let template = buffer.toString('utf8'); + 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 }); @@ -102,11 +96,11 @@ function generateDartClass(name, attributes, template) { case 'double': return attribute.array ? 'List' : 'double'; case 'relationship': - if(imports.indexOf(toSnakeCase(attribute.relatedCollection)) === -1) { + 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' ) { + if ((attribute.relationType === 'oneToMany' && attribute.side === 'parent') || (attribute.relationType === 'manyToOne' && attribute.side === 'child') || attribute.relationType === 'manyToMany') { return `List<${toUpperCamelCase(attribute.relatedCollection)}>`; } @@ -115,13 +109,13 @@ function generateDartClass(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') { + 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) { + if (attr.array) { return `${getType(attr)}.from(map['${attr.key}'])`; } return `map['${attr.key}']`; @@ -150,8 +144,8 @@ function generateDartClass(name, attributes, template) { 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') { + 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()`; @@ -159,13 +153,20 @@ function generateDartClass(name, attributes, template) { return `${out}${attr.key}`; }).join(',\n '); - return template - .replaceAll('%NAME%', name) - .replace('%IMPORTS%', imports) - .replace('%ATTRIBUTES%', properties) - .replace('%CONSTRUCTOR_PARAMETERS%', constructorParams) - .replace('%CONSTRUCTOR_ARGUMENTS%', constructorArgs) - .replace('%MAP_FIELDS%', mapFields); + 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) => { @@ -179,20 +180,20 @@ const configure = async (options) => { let sdk = await sdkForConsole(); - if(!projectId) { + if (!projectId) { let answers = await inquirer.prompt(questionsFlutterConfigure) if (!answers.project) process.exit(1) - + let project = {}; - if (answers.start == "new") { + 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, @@ -200,7 +201,7 @@ const configure = async (options) => { teamId, parseOutput: false }) - + project = response; } else { project = answers.project; @@ -239,7 +240,7 @@ const configure = async (options) => { let projectName = getPubspecName('./pubspec.yaml'); - if(!projectName) { + 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; } @@ -408,9 +409,8 @@ const initializeSDK = async (path, projectId, endpoint) => { log('Overwriting appwrite.dart') } } - const buffer = Buffer.from(appwriteFile, 'base64'); - let decodedString = buffer.toString('utf8'); + let decodedString = fs.readFileSync(`${__dirname}/../../flutter_res/appwrite.dart`, 'utf8'); decodedString = decodedString.replace('{PROJECT}', projectId); decodedString = decodedString.replace('{ENDPOINT}', endpoint); @@ -431,9 +431,11 @@ const getAndroidPackageName = (manifestPath) => { // Search for the package attribute in the manifest file using the regular expression const match = manifestXml.match(regex); + if (!match || match.length < 2) { + return null; + } // Extract the package name from the match - const packageName = match[1]; - return packageName; + return match[1]; } const getPubspecName = (pubspecPath) => { @@ -442,7 +444,7 @@ const getPubspecName = (pubspecPath) => { const regex = /^name:\s*(.*)$/m; const match = yamlFile.match(regex); - if(!match || match.length < 2) { + if (!match || match.length < 2) { return null; } const name = match[1]; From 1dec368535fc49f49728b46d263e44f4025914c5 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 22 May 2023 06:40:55 +0545 Subject: [PATCH 18/21] fix android id not available in manifest file --- lib/commands/flutter.js | 41 ++++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index 7d84610..957cbdf 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -254,7 +254,8 @@ const configure = async (options) => { if (platforms.includes('android')) { if (!androidPackageName) { const manifestPath = path.join('./android', 'app/src/main/AndroidManifest.xml'); - androidPackageName = getAndroidPackageName(manifestPath); + 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; @@ -418,24 +419,42 @@ const initializeSDK = async (path, projectId, endpoint) => { success('SDK initialized successfully'); } -const getAndroidPackageName = (manifestPath) => { - if (!fs.existsSync(manifestPath)) { +const getAndroidPackageName = (manifestPath, buildPath) => { + if (!fs.existsSync(manifestPath) && !fs.existsSync(buildPath)) { return null; } - const manifestXml = fs.readFileSync(manifestPath, 'utf8'); + var match; + var applicationId; - // Define a regular expression to match the package attribute - const regex = /package="([^"]+)"/; + 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); - // Search for the package attribute in the manifest file using the regular expression - const match = manifestXml.match(regex); + if (match && match.length >= 2) { + applicationId = match[1]; + } + } - if (!match || match.length < 2) { - return null; + 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 match[1]; + return applicationId ?? null; } const getPubspecName = (pubspecPath) => { From 6d0fb75c3f099b11a3a3babb5327aed276692ca1 Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 22 May 2023 07:06:06 +0545 Subject: [PATCH 19/21] fix web platform not added --- lib/commands/flutter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index 957cbdf..47f21ce 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -373,8 +373,8 @@ const configure = async (options) => { hostname = answer.hostname; if (!hostname) { error('Please provide Hostname to add web platform'); + return; } - return; } const exists = existingPlatforms.find(platform => (platform.hostname === options.webHostname && platform.type === 'web') || (platforms.hostname === options.webHostname && platform.type === 'flutter-web')); if (!exists) { From 2b166e72c33018351de851908fb56308ad59761f Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Tue, 23 May 2023 06:49:07 +0545 Subject: [PATCH 20/21] fix web platform --- lib/commands/flutter.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index 47f21ce..ff1a651 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -382,11 +382,11 @@ const configure = async (options) => { projectId: projectId, type: 'web', // TODO change to `flutter-web` once cloud fixed name: `${projectName} (Web)`, - hostname: options.webHostname, + hostname: hostname, sdk, parseOutput: false }) - success(`Web platform: ${options.webHostname} added successfully`); + success(`Web platform: ${hostname} added successfully`); } else { success(`Web platform: ${options.webHostname} already exists`); } From aa7a27065297ed1e765f1998eec42a0c57ad45af Mon Sep 17 00:00:00 2001 From: Damodar Lohani Date: Mon, 17 Jul 2023 13:29:05 +0545 Subject: [PATCH 21/21] update for generator --- .../template_dart.dart | 0 generator/template_ts.ts | 11 + index.js | 2 + lib/commands/flutter.js | 2 +- lib/commands/generate.js | 244 ++++++++++++++++++ lib/questions.js | 8 +- 6 files changed, 262 insertions(+), 5 deletions(-) rename flutter_res/template_model.dart => generator/template_dart.dart (100%) create mode 100644 generator/template_ts.ts create mode 100644 lib/commands/generate.js diff --git a/flutter_res/template_model.dart b/generator/template_dart.dart similarity index 100% rename from flutter_res/template_model.dart rename to generator/template_dart.dart diff --git a/generator/template_ts.ts b/generator/template_ts.ts new file mode 100644 index 0000000..793a945 --- /dev/null +++ b/generator/template_ts.ts @@ -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/index.js b/index.js index 8f803af..b45c486 100755 --- a/index.js +++ b/index.js @@ -26,6 +26,7 @@ const { storage } = require("./lib/commands/storage"); const { teams } = require("./lib/commands/teams"); const { users } = require("./lib/commands/users"); const { flutter } = require("./lib/commands/flutter"); +const { generate } = require("./lib/commands/generate"); program .description(commandDescriptions['main']) @@ -61,6 +62,7 @@ program .addCommand(users) .addCommand(client) .addCommand(flutter) + .addCommand(generate) .parse(process.argv); process.stdout.columns = oldWidth; \ No newline at end of file diff --git a/lib/commands/flutter.js b/lib/commands/flutter.js index ff1a651..d8e8b6c 100644 --- a/lib/commands/flutter.js +++ b/lib/commands/flutter.js @@ -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: questionsFlutterChooseDatabase, questionsGeneratorChooseProject: questionsFlutterChooseProject } = require("../questions"); const { success, log, actionRunner, error } = require("../parser"); const { Command } = require("commander"); const { globalConfig, localConfig } = require("../config"); diff --git a/lib/commands/generate.js b/lib/commands/generate.js new file mode 100644 index 0000000..6bcba02 --- /dev/null +++ b/lib/commands/generate.js @@ -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/lib/questions.js b/lib/questions.js index 8844733..858fe06 100644 --- a/lib/questions.js +++ b/lib/questions.js @@ -439,7 +439,7 @@ const questionsFlutterSelectPlatforms = [ } ]; -const questionsFlutterChooseProject = [ +const questionsGeneratorChooseProject = [ { type: "list", name: "project", @@ -468,7 +468,7 @@ const questionsFlutterChooseProject = [ }, ] -const questionsFlutterChooseDatabase = [ +const questionsGeneratorChooseDatabase = [ { type: "checkbox", name: "databases", @@ -509,6 +509,6 @@ module.exports = { questionsGetEntrypoint, questionsFlutterConfigure, questionsFlutterSelectPlatforms, - questionsFlutterChooseDatabase, - questionsFlutterChooseProject, + questionsGeneratorChooseDatabase, + questionsGeneratorChooseProject, };