diff --git a/package.json b/package.json index ae7eb87b..7790ba75 100644 --- a/package.json +++ b/package.json @@ -578,7 +578,7 @@ }, { "title": "Refresh Cluster Overview", - "command":"vscode-couchbase.refreshClusterOverview", + "command": "vscode-couchbase.refreshClusterOverview", "icon": "$(refresh)", "category": "Couchbase" }, @@ -586,6 +586,11 @@ "title": "Check and Create Primary Index", "command": "vscode-couchbase.checkAndCreatePrimaryIndex", "category": "Couchbase" + }, + { + "command": "vscode-couchbase.tools.DDLExport", + "title": "DDL Export", + "category": "Couchbase" } ], "keybindings": [ @@ -755,6 +760,10 @@ { "command": "vscode-couchbase.tools.dataExport", "when": "false" + }, + { + "command": "vscode-couchbase.tools.DDLExport", + "when": "false" } ], "view/item/context": [ @@ -791,6 +800,10 @@ "submenu": "vscode-couchbase.toolsMenu", "when": "view == couchbase && viewItem == active_connection" }, + { + "submenu": "vscode-couchbase.toolsMenu", + "when": "view == couchbase && viewItem == active_connection" + }, { "command": "vscode-couchbase.useClusterConnection", "when": "view == couchbase && viewItem == connection" @@ -904,6 +917,10 @@ { "command": "vscode-couchbase.tools.dataExport", "group": "navigation" + }, + { + "command": "vscode-couchbase.tools.DDLExport", + "group": "navigation" } ] }, diff --git a/src/commands/extensionCommands/commands.ts b/src/commands/extensionCommands/commands.ts index febae97d..09dea4f4 100644 --- a/src/commands/extensionCommands/commands.ts +++ b/src/commands/extensionCommands/commands.ts @@ -55,4 +55,5 @@ export namespace Commands { export const refreshClusterOverview: string = "vscode-couchbase.refreshClusterOverview"; export const checkAndCreatePrimaryIndex: string = "vscode-couchbase.checkAndCreatePrimaryIndex"; export const dataExport: string = "vscode-couchbase.tools.dataExport"; + export const ddlExport: string = "vscode-couchbase.tools.DDLExport"; } diff --git a/src/commands/tools/ddlExport/ddlExport.ts b/src/commands/tools/ddlExport/ddlExport.ts new file mode 100644 index 00000000..c385fd83 --- /dev/null +++ b/src/commands/tools/ddlExport/ddlExport.ts @@ -0,0 +1,218 @@ +import * as vscode from "vscode"; +import * as path from 'path'; +import { IConnection } from "../../../types/IConnection"; +import { getActiveConnection } from "../../../util/connections"; +import { logger } from "../../../logger/logger"; +import { getLoader } from "../../../webViews/loader.webview"; +import { ddlExportWebview } from "../../../webViews/tools/ddlExport.webview"; +import { QueryIndex, ScopeSpec } from "couchbase"; +import { getCurrentDateTime } from "../../../util/util"; +import * as fs from "fs"; +import { getIndexDefinition } from "../../../util/indexUtils"; + +const validateFormData = (formData: any): string => { + const errors = []; + + if (!formData.bucket) { + errors.push("Please select a bucket"); + } else { + if (formData.scopes.length === 0) { + errors.push("Select one or more scopes"); + } + } + + if (!formData.fileDestination.trim()) { + errors.push("Please inform the file destination folder"); + } + + if (errors.length > 0) { + return errors.join("
"); + } + + return ""; +}; + +class DDLExport { + private static async exportScope(bucket: string, scope: ScopeSpec, includeIndexes: boolean): Promise { + const ddlExportData: string[] = []; + const scopePrefix = "`" + bucket + "`.`" + scope.name + "`"; + + if ("_default" !== scope.name) { + ddlExportData.push("CREATE SCOPE " + scopePrefix + ";"); + } + ddlExportData.push("\n"); + const connection = getActiveConnection(); + if (!connection) { + return ""; + } + + for (const collectionSpec of scope.collections) { + ddlExportData.push("\n"); + ddlExportData.push("/* DDL for collection " + scope.name + "." + collectionSpec.name + " */"); + ddlExportData.push("\n"); + if ("_default" !== collectionSpec.name) { + ddlExportData.push("CREATE COLLECTION " + scopePrefix + ".`" + collectionSpec.name + "`; \n"); + } + if (includeIndexes) { + const result = await listIndexes(connection, bucket, scope.name, collectionSpec.name); + ddlExportData.push(result?.map(index => getIndexDefinition(index)).join("; \n")!); + if (result!.length > 1) { + ddlExportData.push("; \n"); + } + } + ddlExportData.push("\n"); + } + + ddlExportData.push("\n\n"); + + return ddlExportData.join(""); + } + + public async exportScope(bucket: string, scopes: string[], filePath: string, includeIndexes: boolean): Promise { + try { + const connection = getActiveConnection(); + if (!connection) { + return; + } + + const ifAllScopes = scopes.includes("All Scopes"); + const ddlExportData: string[] = []; + const allScopes = await connection?.cluster?.bucket(bucket).collections().getAllScopes()!; + const selectedScopes: ScopeSpec[] = ifAllScopes ? allScopes : allScopes.filter(a => scopes.includes(a.name)); + + for (const scope of selectedScopes) { + ddlExportData.push("/*************************/ \n"); + ddlExportData.push("/** Scope " + scope.name + " */ \n"); + ddlExportData.push("/*************************/ \n"); + ddlExportData.push(await DDLExport.exportScope(bucket, scope, includeIndexes)); + ddlExportData.push("\n"); + } + + let fileName = bucket + "_" + (ifAllScopes ? "_all_" : DDLExport.getScopesFileName(scopes)) + getCurrentDateTime() + ".sqlpp"; + if (!filePath.endsWith("/")) { + fileName = "/" + fileName; + } + const fullPath = filePath + fileName; + fs.writeFile(fullPath, ddlExportData.join(''), function (err) { + if (err) { throw err; } + vscode.window.showInformationMessage("DDL Export: File exported successfully"); + }); + + } catch (e) { + logger.error("An error occurred while writing to export the DDL: " + e); + } + }; + + public static getScopesFileName(scopes: string[]): string { + if (scopes.length === 1) { + return scopes[0] + "_"; + } else { + return scopes.length + "_scopes_"; + } + } +}; + +export const listIndexes = async (connection: IConnection, bucket: string, scope: string, collection: string): Promise => { + if (connection) { + try { + const result = await connection?.cluster?.queryIndexes().getAllIndexes(bucket, { scopeName: scope, collectionName: collection }); + return result; + } catch { + return []; + } + } + +}; + +export const ddlExport = async () => { + const connection = getActiveConnection(); + if (!connection) { + return; + } + const currentPanel = vscode.window.createWebviewPanel( + "ddlExport", + "DDL Export", + vscode.ViewColumn.One, + { + enableScripts: true, + enableForms: true, + } + ); + currentPanel.iconPath = { + dark: vscode.Uri.file( + path.join(__filename, "..", "..", "images", "dark", "export_dark.svg") + ), + light: vscode.Uri.file( + path.join(__filename, "..", "..", "images", "light", "export_light.svg") + ), + }; + currentPanel.webview.html = getLoader("DDL Export"); + + // Get all buckets + const buckets = await connection.cluster?.buckets().getAllBuckets(); + if (buckets === undefined) { + vscode.window.showErrorMessage("Buckets not found"); + return; + } + + const bucketNameArr: string[] = buckets.map(b => b.name); + try { + currentPanel.webview.html = await ddlExportWebview(bucketNameArr); + currentPanel.webview.onDidReceiveMessage(async (message) => { + switch (message.command) { + case "vscode-couchbase.tools.DDLExport.runExport": + const formData = message.data; + const validationError = validateFormData(formData); + if (validationError === "") { + const ddl = new DDLExport(); + ddl.exportScope(formData.bucket, + formData.scopes, formData.fileDestination, formData.includeIndexes); + } else { + currentPanel.webview.postMessage({ + command: "vscode-couchbase.tools.DDLExport.formValidationError", + error: validationError, + }); + } + break; + case "vscode-couchbase.tools.DDLExport.getScopes": + const scopes = await connection.cluster + ?.bucket(message.bucketId) + .collections() + .getAllScopes(); + if (scopes === undefined) { + vscode.window.showErrorMessage("Scopes are undefined"); + break; + } + + currentPanel.webview.postMessage({ + command: "vscode-couchbase.tools.DDLExport.scopesInfo", + scopes: scopes, + }); + break; + case "vscode-couchbase.tools.DDLExport.getFolder": + const options: vscode.OpenDialogOptions = { + canSelectMany: false, + openLabel: "Choose Destination Folder", + canSelectFiles: false, + canSelectFolders: true, + }; + + vscode.window.showOpenDialog(options).then((fileUri) => { + if (fileUri && fileUri[0]) { + currentPanel.webview.postMessage({ + command: "vscode-couchbase.tools.DDLExport.folderInfo", + folder: fileUri[0].fsPath, + }); + } + }); + break; + } + }); + } catch (err) { + logger.error(`Failed to open data export webview`); + logger.debug(err); + vscode.window.showErrorMessage( + "Failed to open data export webview: " + err + ); + } +}; \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index 21b9f490..2058e401 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -65,6 +65,7 @@ import { clearDocumentFilter } from "./commands/documents/clearDocumentFilter"; import { getClusterOverviewData } from "./util/OverviewClusterUtils/getOverviewClusterData"; import { checkAndCreatePrimaryIndex } from "./commands/indexes/checkAndCreatePrimaryIndex"; import { dataExport } from "./pages/Tools/DataExport/dataExport"; +import { ddlExport } from "./commands/tools/ddlExport/ddlExport"; export function activate(context: vscode.ExtensionContext) { Global.setState(context.globalState); @@ -573,6 +574,12 @@ export function activate(context: vscode.ExtensionContext) { } ) ); + + context.subscriptions.push( + vscode.commands.registerCommand(Commands.ddlExport, async () => { + ddlExport(); + }) + ); } // this method is called when your extension is deactivated diff --git a/src/pages/Tools/DataExport/dataExport.ts b/src/pages/Tools/DataExport/dataExport.ts index 20df0d30..11978bb7 100644 --- a/src/pages/Tools/DataExport/dataExport.ts +++ b/src/pages/Tools/DataExport/dataExport.ts @@ -55,7 +55,7 @@ const validateFormData = (formData: any): string => { errors.push("Please inform the file destination folder"); } - if(!formData.threads.trim() || parseInt(formData.threads)<1){ + if (!formData.threads.trim() || parseInt(formData.threads) < 1) { errors.push("threads cannot be undefined or less than 1"); } @@ -109,10 +109,10 @@ export const dataExport = async () => { }); currentPanel.iconPath = { dark: vscode.Uri.file( - path.join(__filename, "..", "..", "images","dark","export_dark.svg") + path.join(__filename, "..", "..", "images", "dark", "export_dark.svg") ), light: vscode.Uri.file( - path.join(__filename, "..", "..", "images","light","export_light.svg") + path.join(__filename, "..", "..", "images", "light", "export_light.svg") ), }; currentPanel.webview.html = getLoader("Data Export"); @@ -123,10 +123,8 @@ export const dataExport = async () => { vscode.window.showErrorMessage("Buckets not found"); return; } - const bucketNameArr: string[] = []; - for (let bucket of buckets) { - bucketNameArr.push(bucket.name); - } + + const bucketNameArr: string[] = buckets.map(b => b.name); try { currentPanel.webview.html = await dataExportWebview(bucketNameArr); diff --git a/src/webViews/tools/dataExport.webview.ts b/src/webViews/tools/dataExport.webview.ts index ad49facc..c75cc8e9 100644 --- a/src/webViews/tools/dataExport.webview.ts +++ b/src/webViews/tools/dataExport.webview.ts @@ -19,6 +19,31 @@ export const dataExportWebview = async (buckets: string[]): Promise => { margin-bottom: 5px; } + input[type="checkbox"] { + appearance: none; + -webkit-appearance: none; + width: 15px; + height: 15px; + border-radius: 2px; + outline: none; + border: 1px solid #999; + background-color: white; + } + + input[type="checkbox"]:checked { + background-color: #ea2328; + } + + input[type="checkbox"]:checked::before { + content: '\\2714'; + display: flex; + justify-content: center; + align-items: center; + font-size: 12px; + color: white; + height: 100%; + } + form { max-width: 500px; margin: 0 auto; diff --git a/src/webViews/tools/ddlExport.webview.ts b/src/webViews/tools/ddlExport.webview.ts new file mode 100644 index 00000000..50c2484a --- /dev/null +++ b/src/webViews/tools/ddlExport.webview.ts @@ -0,0 +1,320 @@ +export const ddlExportWebview = async (buckets: string[]): Promise => { + return ` + + + + + + Data Export + + + + + +

DDL Export

+
+ + +
+ + +
+
+ + +
+
+ +
+
Choose
+ +
+
+
+ +
+ + + + + + `; +}; \ No newline at end of file