diff --git a/.gitignore b/.gitignore index 370aa7a..05b5e51 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,13 @@ FIXES_APPLIED.md .agents/ skills-lock.json graphify-out/ -.superpowers/ \ No newline at end of file +.superpowers/ +docs/roadmap/ +docs/plans/ +docs/specs/ +docs/plan/ +.commandcode/ +test/ +.vercel +.env +.env.local diff --git a/.kilo/agent-manager.json b/.kilo/agent-manager.json new file mode 100644 index 0000000..93d494d --- /dev/null +++ b/.kilo/agent-manager.json @@ -0,0 +1,9 @@ +{ + "worktrees": {}, + "sessions": {}, + "tabOrder": { + "local": [ + "pending:1" + ] + } +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index fae8e3d..2c76624 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "typescript.tsdk": "node_modules/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true + "typescript.enablePromptUseWorkspaceTsdk": true, + "makefile.configureOnOpen": true } diff --git a/.vscodeignore b/.vscodeignore index 7c1744a..5f681fb 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -59,6 +59,7 @@ venv/** .cursor/ .nightly/ .kiro/ +.kilo/ .c8rc.phase-handlers.json .c8rc.phase-utils.json .c8rc.phase-report.json @@ -69,4 +70,6 @@ FIXES_APPLIED.md review-the-current-codebase-shimmering-bee.md .github/ .coverage/ -.coverage-unit/ \ No newline at end of file +.coverage-unit/ +packages/ +graphify-out/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c9ab5b..82976a2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,48 @@ All notable changes to the PostgreSQL Explorer extension will be documented in t The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.9] - 2026-05-23 +> Nightly releases - v1.3.11 + +### Added + +- **Parameter comment completions** — Typing `--` in a SQL cell with `$1`/`:name` params now suggests missing `-- $1=` / `-- :name=` definitions. A Quick Fix action inserts all missing stubs at once. + - Example: + ```sql + SELECT * FROM users WHERE id = $1 AND status = :status + ``` + triggers suggestions for: + ```sql + -- $1= + -- :status= + ``` +- **Production safety banner** — Connection form shows a warning banner when environment is Production, prompting you to enable read-only mode. + - **Environment tagging** is now clearer and more reliable, with badges on connection cards and a status bar indicator for active Production connections. + - **Query safety analyzer** now applies a Production risk multiplier to better catch dangerous queries and require confirmation. + - **Status bar risk indicator** now shows a warning icon whenever a Production connection is active. + +### Changed + +- SSL connections tagged as Production no longer silently downgrade to plaintext. +- Explain Analyzer, Schema Designer, AI Assistant, and Saved Queries now route through Pro feature gates with freemium counters. +- Freemium model is 100% functional with clear upgrade paths, and Pro features are fully unlocked in development builds (coming soon). + +--- + +## [1.2.7] - 2026-05-13 +> Nightly releases - v1.3.9 • v1.3.10 + +### Added + +- **Role Designer** — Added a new visual role-management editor from the role context menu, with live SQL preview, notebook handoff, and membership controls for Inherit / Admin Option grants. +- **Notebook parameter bank** — SQL parameter prompts now remember values per notebook, offer quick-pick reuse, and let you clear saved values without affecting other notebooks. +- **Streaming row counts** — Sliding-window result rendering now shows the total row count when it is available, so streamed results read as `start–end of total` instead of only a range. + +### Changed + +- **SQL completion catalog** — Column completion warm-cache queries now read from PostgreSQL catalogs directly, which improves coverage for views and materialized views. +- **Result cursor metadata** — Cursor window messages now carry optional total-row metadata through the renderer path. + ## [1.2.5] - 2026-05-07 > Nightly releases - v1.3.6 • v1.3.7 diff --git a/api/config.js b/api/config.js new file mode 100644 index 0000000..2f3c71c --- /dev/null +++ b/api/config.js @@ -0,0 +1,8 @@ +module.exports = async (req, res) => { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method Not Allowed' }); + } + return res.status(200).json({ + key_id: process.env.RAZORPAY_KEY_ID + }); +}; diff --git a/api/create-order.js b/api/create-order.js new file mode 100644 index 0000000..6112b60 --- /dev/null +++ b/api/create-order.js @@ -0,0 +1,63 @@ +const Razorpay = require('razorpay'); + +module.exports = async (req, res) => { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method Not Allowed' }); + } + + const { amount, currency, receipt } = req.body || {}; + + // Validate amount + if (!amount || isNaN(amount)) { + return res.status(400).json({ error: 'Amount is required and must be a number' }); + } + + const amountPaise = parseInt(amount, 10); + if (amountPaise < 100) { + return res.status(400).json({ error: 'Amount must be at least 100 paise' }); + } + + if (!currency) { + return res.status(400).json({ error: 'Currency is required' }); + } + + // Initialize Razorpay client + const key_id = process.env.RAZORPAY_KEY_ID; + const key_secret = process.env.RAZORPAY_KEY_SECRET; + + if (!key_id || !key_secret) { + console.error('Razorpay credentials missing from environment.'); + return res.status(500).json({ error: 'Internal Server Error: Razorpay credentials missing' }); + } + + const razorpay = new Razorpay({ + key_id: key_id, + key_secret: key_secret, + }); + + try { + const options = { + amount: amountPaise, + currency: currency, + receipt: receipt || `receipt_${Date.now()}` + }; + + const order = await razorpay.orders.create(options); + + return res.status(200).json({ + order_id: order.id, + amount: order.amount, + currency: order.currency + }); + } catch (error) { + console.error('Razorpay API error:', error); + + // Handle authentication failures (Razorpay SDK might throw or return standard error response) + const errorDescription = error.description || (error.error && error.error.description) || ''; + if (error.statusCode === 401 || errorDescription.includes('Key') || errorDescription.includes('signature')) { + return res.status(401).json({ error: 'Authentication failed with Razorpay API' }); + } + + return res.status(500).json({ error: error.message || 'Failed to create order with Razorpay API' }); + } +}; diff --git a/api/package.json b/api/package.json new file mode 100644 index 0000000..f052526 --- /dev/null +++ b/api/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "dependencies": { + "razorpay": "^2.9.0" + } +} diff --git a/api/verify-payment.js b/api/verify-payment.js new file mode 100644 index 0000000..46f3399 --- /dev/null +++ b/api/verify-payment.js @@ -0,0 +1,44 @@ +const crypto = require('crypto'); + +module.exports = async (req, res) => { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method Not Allowed' }); + } + + const { razorpay_order_id, razorpay_payment_id, razorpay_signature } = req.body || {}; + + // Error handling: Missing fields + if (!razorpay_order_id || !razorpay_payment_id || !razorpay_signature) { + return res.status(400).json({ error: 'Missing required signature verification fields' }); + } + + const secret = process.env.RAZORPAY_KEY_SECRET; + if (!secret) { + console.error('Razorpay Key Secret is missing from environment.'); + return res.status(500).json({ error: 'Internal Server Error: Razorpay Key Secret is missing' }); + } + + try { + // Generate signature using HMAC-SHA256 + const hmac = crypto.createHmac('sha256', secret); + hmac.update(`${razorpay_order_id}|${razorpay_payment_id}`); + const generated_signature = hmac.digest('hex'); + + // Secure timing-safe or straight comparison of generated signature and razorpay_signature + if (generated_signature === razorpay_signature) { + return res.status(200).json({ + success: true, + message: 'Payment signature verified successfully.' + }); + } else { + // Signature mismatch: return 400, do NOT mark as paid + return res.status(400).json({ + success: false, + error: 'Signature verification failed. Potential tampering detected.' + }); + } + } catch (error) { + console.error('Error verifying payment signature:', error); + return res.status(500).json({ error: error.message || 'Internal Server Error during verification' }); + } +}; diff --git a/docs/CNAME b/docs/CNAME index 8123bf2..9f19e7b 100644 --- a/docs/CNAME +++ b/docs/CNAME @@ -1 +1 @@ -pgstudio.astrx.dev +# This file is no longer needed. Custom domain is managed via Vercel Dashboard. diff --git a/src/commands/REFACTORING_GUIDE.md b/docs/REFACTORING_GUIDE.md similarity index 100% rename from src/commands/REFACTORING_GUIDE.md rename to docs/REFACTORING_GUIDE.md diff --git a/docs/WEBSITE_CONTEXT.md b/docs/WEBSITE_CONTEXT.md index d38e10d..bbe8d5f 100644 --- a/docs/WEBSITE_CONTEXT.md +++ b/docs/WEBSITE_CONTEXT.md @@ -1,16 +1,18 @@ # Docs Website Context -Last updated: 2026-04-19 +Last updated: 2026-05-22 Primary entry: docs/index.html +Hosting: Vercel (migrated from GitHub Pages) ## What This Website Is -This site is a product demo and marketing landing page for PgStudio, styled and behaved like a mini VS Code workbench. +This site is a product demo and marketing landing page for PgStudio, styled and behaved like a mini VS Code workbench. It also includes a Razorpay payment checkout for sponsorship/pro support. The core concept is: - Show value by simulation, not by static brochure copy. - Let users interact with a realistic "editor + explorer + SQL assistant" shell. - Keep installation CTA visible from both full and minimized states. +- Enable paid sponsorship via Razorpay checkout (requires server-side API). ## Visual and UX Concept @@ -97,6 +99,23 @@ Primary conversion links: - GitHub repository - Open VSX listing +## Deployment (Vercel) + +The site is deployed on Vercel with two components: +- **Static site**: Served from `docs/` directory (configured via `vercel.json` `outputDirectory`) +- **Serverless API**: Three functions in `api/` for Razorpay checkout: + - `GET /api/config` — serves the public Razorpay Key ID + - `POST /api/create-order` — creates a Razorpay order (uses `RAZORPAY_KEY_SECRET`) + - `POST /api/verify-payment` — verifies payment signatures + +Environment variables (`RAZORPAY_KEY_ID`, `RAZORPAY_KEY_SECRET`) are configured in the Vercel Dashboard, not committed to the repo. + +The `api/package.json` declares serverless-specific dependencies (only `razorpay` SDK). This is separate from the root `package.json` which is for the VS Code extension. + +For local development, use `node scripts/dev-server.js` which serves `docs/` statically and mounts the `api/` handlers as Express routes, or use `npx vercel dev` for a full Vercel emulation. + +Custom domain (`pgstudio.astrx.dev`) is managed via Vercel Dashboard → Domains. + ## Maintenance Rules When editing this site: @@ -105,7 +124,10 @@ When editing this site: - Treat this as a simulated product experience; avoid replacing interaction with static text. - Maintain install CTAs in both topbar and minimized overview. - Verify both desktop and mobile toggle flows after major UI changes. +- Never commit `RAZORPAY_KEY_SECRET` or live credentials; use Vercel environment variables. +- When adding new API endpoints, add them to both `api/` and `scripts/dev-server.js`. ## Known Environment Note This review is based on source-level inspection in this workspace. Runtime browser introspection from the agent was unavailable because chat browser tools are not enabled in the current VS Code environment. + diff --git a/docs/html/minimized-overview.html b/docs/html/minimized-overview.html index 4f577c9..3d38469 100644 --- a/docs/html/minimized-overview.html +++ b/docs/html/minimized-overview.html @@ -284,6 +284,24 @@

Simple, transparent pricing Install Free +
+ RECOMMENDED +
Pro Sponsor
+
Support continuous development and unlock pre-release features
+
₹1.00 / one-time
+ + +
Teams roadmap
Commercial tiers are not sold yet — follow progress on GitHub.
diff --git a/docs/index.html b/docs/index.html index c79d350..132e66f 100644 --- a/docs/index.html +++ b/docs/index.html @@ -542,6 +542,9 @@ + + + diff --git a/docs/js/checkout.js b/docs/js/checkout.js new file mode 100644 index 0000000..75b9737 --- /dev/null +++ b/docs/js/checkout.js @@ -0,0 +1,280 @@ +// Self-contained Razorpay Checkout Integration for PgStudio +// Features: Event Delegation, Dynamic Public Key Resolution, Custom Premium Glassmorphism Alerts. + +(function () { + // Inject toast alert styles dynamically to keep checkout styling modular and visually premium + const style = document.createElement('style'); + style.textContent = ` + .payment-toast { + position: fixed; + bottom: 24px; + right: 24px; + background: rgba(22, 22, 37, 0.85); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid rgba(255, 255, 255, 0.08); + padding: 16px 20px; + border-radius: 12px; + color: #f8f8f2; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35); + z-index: 10000; + display: flex; + align-items: center; + gap: 12px; + font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + font-size: 14px; + max-width: 380px; + transform: translateY(100px); + opacity: 0; + transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.4s ease; + } + .payment-toast.show { + transform: translateY(0); + opacity: 1; + } + .payment-toast.success { + border-left: 4px solid #10b981; + } + .payment-toast.error { + border-left: 4px solid #ef4444; + } + .payment-toast.warning { + border-left: 4px solid #f59e0b; + } + .payment-toast-icon { + font-size: 22px; + display: flex; + align-items: center; + justify-content: center; + } + .payment-toast-content { + flex: 1; + line-height: 1.4; + } + .payment-toast-close { + background: none; + border: none; + color: #9ca3af; + cursor: pointer; + font-weight: bold; + font-size: 18px; + margin-left: 12px; + padding: 0 4px; + display: flex; + align-items: center; + justify-content: center; + transition: color 0.2s ease; + } + .payment-toast-close:hover { + color: #f3f4f6; + } + + /* Loading Spinner */ + .spinner-dot { + width: 14px; + height: 14px; + border: 2px solid rgba(255, 255, 255, 0.3); + border-top-color: white; + border-radius: 50%; + animation: spin-anim 0.8s linear infinite; + display: inline-block; + } + @keyframes spin-anim { + to { transform: rotate(360deg); } + } + `; + document.head.appendChild(style); + + // Custom Toast Alert System + function showCheckoutAlert(type, message) { + // Remove existing toast if any + const existing = document.querySelector('.payment-toast'); + if (existing) existing.remove(); + + const toast = document.createElement('div'); + toast.className = `payment-toast ${type}`; + + let icon = '⚡'; + if (type === 'success') icon = '🎉'; + if (type === 'error') icon = '❌'; + if (type === 'warning') icon = 'ℹ️'; + + toast.innerHTML = ` +
${icon}
+
${message}
+ + `; + + document.body.appendChild(toast); + + // Fade-in animation + setTimeout(() => toast.classList.add('show'), 50); + + // Auto dismiss after 6 seconds + const dismissTimer = setTimeout(() => { + toast.classList.remove('show'); + setTimeout(() => toast.remove(), 400); + }, 6000); + + // Close button click handler + toast.querySelector('.payment-toast-close').addEventListener('click', () => { + clearTimeout(dismissTimer); + toast.classList.remove('show'); + setTimeout(() => toast.remove(), 400); + }); + } + + // Use Event Delegation to capture clicks on #btn-razorpay-checkout (since the html is loaded as a dynamic partial) + document.addEventListener('click', async (event) => { + const btn = event.target.closest('#btn-razorpay-checkout'); + if (!btn) return; + + event.preventDefault(); + if (btn.disabled) return; + + // Save original button content and disable + const originalContent = btn.innerHTML; + const originalText = btn.textContent.trim(); + + function setBtnLoading(text) { + btn.disabled = true; + btn.style.opacity = '0.7'; + btn.innerHTML = ` ${text}`; + } + + function resetButton() { + btn.disabled = false; + btn.style.opacity = '1'; + btn.innerHTML = originalContent; + } + + try { + setBtnLoading('Initializing Payment...'); + + // 1. Fetch Razorpay Key ID from config endpoint + const configRes = await fetch('/api/config'); + if (!configRes.ok) throw new Error('Failed to fetch API configurations'); + const configData = await configRes.json(); + const keyId = configData.key_id; + + if (!keyId) throw new Error('Razorpay Key ID is missing'); + + setBtnLoading('Creating Order...'); + + // 2. Call backend endpoint to create order (amount: 100 paise = ₹1.00) + const orderRes = await fetch('/api/create-order', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + amount: 100, + currency: 'INR', + receipt: `receipt_sponsor_${Date.now()}` + }) + }); + + if (!orderRes.ok) { + const errorData = await orderRes.json().catch(() => ({})); + throw new Error(errorData.error || 'Failed to create order'); + } + + const orderData = await orderRes.json(); + + setBtnLoading('Launching Checkout...'); + + // 3. Configure Razorpay Standard Checkout Options + const options = { + key: keyId, + amount: orderData.amount, + currency: orderData.currency, + name: 'PgStudio Pro', + description: 'One-time Pro Sponsorship Support', + order_id: orderData.order_id, + image: 'assets/postgres-vsc-icon.png', + prefill: { + name: 'PostgreSQL Developer', + email: 'developer@pgstudio.astrx.dev', + contact: '9999999999' + }, + notes: { + sponsorship_tier: 'Pro Support Plan' + }, + theme: { + color: '#4d5efc' // Matching PgStudio's brand color + }, + handler: async function (response) { + // On Payment Success: + // Receive razorpay_payment_id, razorpay_order_id, razorpay_signature + setBtnLoading('Verifying Transaction...'); + + try { + const verifyRes = await fetch('/api/verify-payment', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + razorpay_order_id: response.razorpay_order_id, + razorpay_payment_id: response.razorpay_payment_id, + razorpay_signature: response.razorpay_signature + }) + }); + + const verifyData = await verifyRes.json(); + + if (verifyRes.ok && verifyData.success) { + showCheckoutAlert( + 'success', + 'Thank you for your support!
Your payment of ₹1.00 has been verified successfully. Welcome to PgStudio Pro!' + ); + } else { + showCheckoutAlert( + 'error', + `Verification failed: ${verifyData.error || 'Payment signature mismatch'}` + ); + } + } catch (err) { + console.error('Signature verification call failed:', err); + showCheckoutAlert( + 'error', + 'Connection error during transaction signature verification.' + ); + } finally { + resetButton(); + } + }, + modal: { + ondismiss: function () { + // Handle modal dismiss (user cancelled) + showCheckoutAlert('warning', 'Sponsorship checkout cancelled by user.'); + resetButton(); + } + } + }; + + // 4. Open Razorpay Payment Modal + const rzp = new Razorpay(options); + + // Handle payment.failed event + rzp.on('payment.failed', function (response) { + console.error('Payment failure event:', response.error); + showCheckoutAlert( + 'error', + `Payment Failed: ${response.error.description || 'Transaction unsuccessful'}` + ); + resetButton(); + }); + + rzp.open(); + + } catch (error) { + console.error('Checkout initialization failed:', error); + showCheckoutAlert( + 'error', + `Checkout Error: ${error.message || 'Initialization failed'}` + ); + resetButton(); + } + }); +})(); diff --git a/package-lock.json b/package-lock.json index eaed4bf..9efbcf0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "postgres-explorer", - "version": "1.2.5", + "version": "1.3.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "postgres-explorer", - "version": "1.2.5", + "version": "1.3.10", "license": "MIT", "dependencies": { "@cursor/sdk": "^1.0.12", @@ -16,8 +16,10 @@ "d3-force": "^3.0.0", "d3-selection": "^3.0.0", "esbuild": ">=0.28.0", + "express": "^5.2.1", "pg": "^8.20.0", "pg-cursor": "^2.19.0", + "razorpay": "^2.9.6", "sql-formatter": "^15.7.3", "ssh2": "^1.17.0" }, @@ -3389,6 +3391,53 @@ "license": "ISC", "optional": true }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -3621,9 +3670,45 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, + "node_modules/axios": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/axios/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/azure-devops-node-api": { "version": "12.5.0", "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", @@ -3733,6 +3818,46 @@ "readable-stream": "^3.4.0" } }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -3880,6 +4005,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cacache": { "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", @@ -4042,7 +4176,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -4056,7 +4189,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -4371,7 +4503,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -4411,6 +4542,28 @@ "license": "ISC", "optional": true }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", @@ -4418,6 +4571,24 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, "node_modules/cpu-features": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz", @@ -4566,7 +4737,6 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "devOptional": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -4727,7 +4897,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -4740,6 +4909,15 @@ "license": "MIT", "optional": true }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -4828,7 +5006,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -4873,6 +5050,12 @@ "url": "https://bevry.me/fund" } }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.352", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.352.tgz", @@ -4887,6 +5070,15 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/encoding": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", @@ -4967,7 +5159,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4977,7 +5168,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4987,7 +5177,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -5000,7 +5189,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -5071,6 +5259,12 @@ "node": ">=6" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -5302,6 +5496,15 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -5311,6 +5514,74 @@ "node": ">=6" } }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/fast-check": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz", @@ -5444,6 +5715,27 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-cache-dir": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", @@ -5514,7 +5806,6 @@ "version": "1.16.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", - "dev": true, "funding": [ { "type": "individual", @@ -5552,7 +5843,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -5565,6 +5855,24 @@ "node": ">= 6" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fromentries": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/fromentries/-/fromentries-1.3.2.tgz", @@ -5644,7 +5952,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5747,7 +6054,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -5782,7 +6088,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -5908,7 +6213,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5951,7 +6255,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -5964,7 +6267,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -6004,7 +6306,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -6109,6 +6410,26 @@ "license": "BSD-2-Clause", "optional": true }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -6264,6 +6585,15 @@ "node": ">= 12" } }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-ci": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", @@ -6402,6 +6732,12 @@ "dev": true, "license": "MIT" }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -7168,7 +7504,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7188,6 +7523,27 @@ "dev": true, "license": "MIT" }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -7229,7 +7585,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -7239,7 +7594,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -7562,7 +7916,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "devOptional": true, "license": "MIT" }, "node_modules/mute-stream": { @@ -8120,7 +8473,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8139,6 +8491,18 @@ "node": ">= 0.4" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -8390,6 +8754,15 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/parsimmon": { "version": "1.18.1", "resolved": "https://registry.npmjs.org/parsimmon/-/parsimmon-1.18.1.tgz", @@ -8450,6 +8823,16 @@ "dev": true, "license": "ISC" }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/path-type": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-6.0.0.tgz", @@ -8792,6 +9175,28 @@ "node": ">=10" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/pump": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", @@ -8895,6 +9300,55 @@ "node": ">=0.12" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/razorpay": { + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/razorpay/-/razorpay-2.9.6.tgz", + "integrity": "sha512-zsHAQzd6e1Cc6BNoCNZQaf65ElL6O6yw0wulxmoG5VQDr363fZC90Mp1V5EktVzG45yPyNomNXWlf4cQ3622gQ==", + "license": "MIT", + "dependencies": { + "axios": "^1.6.8" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -9154,6 +9608,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/run-applescript": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", @@ -9274,6 +9744,57 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/send/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/send/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/serialize-javascript": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz", @@ -9284,6 +9805,25 @@ "node": ">=20.0.0" } }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -9291,6 +9831,12 @@ "devOptional": true, "license": "ISC" }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9318,7 +9864,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -9338,7 +9883,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -9355,7 +9899,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -9374,7 +9917,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -9758,6 +10300,15 @@ "node": ">=8" } }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -10304,6 +10855,15 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/tough-cookie": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", @@ -10503,6 +11063,62 @@ "node": ">=8" } }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/type-is/node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/typed-rest-client": { "version": "1.8.11", "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", @@ -10612,6 +11228,15 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -10695,6 +11320,15 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/version-range": { "version": "4.15.0", "resolved": "https://registry.npmjs.org/version-range/-/version-range-4.15.0.tgz", diff --git a/package.json b/package.json index ca6c259..1803d7f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "postgres-explorer", "displayName": "PgStudio (PostgreSQL Explorer)", - "version": "1.2.5", + "version": "1.3.11", "description": "PostgreSQL database explorer for VS Code with notebook support [Nightly]", "publisher": "ric-v", "private": false, @@ -581,6 +581,11 @@ "title": "Edit Role", "icon": "$(edit)" }, + { + "command": "postgres-explorer.openRoleDesigner", + "title": "Role Designer (Visual)", + "icon": "$(edit)" + }, { "command": "postgres-explorer.grantRevoke", "title": "Grant/Revoke Privileges", @@ -2153,6 +2158,11 @@ "when": "view == postgresExplorer && viewItem == table", "group": "inline@2" }, + { + "command": "postgres-explorer.openRoleDesigner", + "when": "view == postgresExplorer && viewItem == role", + "group": "1_actions@1" + }, { "command": "postgres-explorer.newNotebook", "when": "view == postgresExplorer && viewItem =~ /^(schema|table|partition)$/", @@ -3291,6 +3301,7 @@ ], "main": "./dist/extension.js", "scripts": { + "dev:site": "node scripts/dev-server.js", "vscode:prepublish": "npm run esbuild-base -- --minify && npm run esbuild-renderer -- --minify && npm run esbuild-erd-webview -- --minify", "prepare:nightly:manifests": "node ./scripts/prepare-nightly-manifests.js", "package:prerelease": "npx @vscode/vsce package --pre-release", @@ -3338,8 +3349,10 @@ "d3-force": "^3.0.0", "d3-selection": "^3.0.0", "esbuild": ">=0.28.0", + "express": "^5.2.1", "pg": "^8.20.0", "pg-cursor": "^2.19.0", + "razorpay": "^2.9.6", "sql-formatter": "^15.7.3", "ssh2": "^1.17.0" }, @@ -3387,4 +3400,4 @@ "webpack": "^5.76.0", "webpack-cli": "^5.0.0" } -} \ No newline at end of file +} diff --git a/package.json.bak b/package.json.bak new file mode 100644 index 0000000..d783e41 --- /dev/null +++ b/package.json.bak @@ -0,0 +1,3403 @@ +{ + "name": "postgres-explorer", + "displayName": "PgStudio (PostgreSQL Explorer)", + "version": "1.3.11", + "description": "PostgreSQL database explorer for VS Code with notebook support [Nightly]", + "publisher": "ric-v", + "private": false, + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/dev-asterix/PgStudio.git" + }, + "bugs": { + "url": "https://github.com/dev-asterix/PgStudio/issues" + }, + "homepage": "https://pgstudio.astrx.dev/", + "icon": "resources/postgres-explorer.png", + "galleryBanner": { + "color": "#575584", + "theme": "dark" + }, + "engines": { + "vscode": "^1.107.0", + "node": ">=18.0.0" + }, + "categories": [ + "Data Science", + "Notebooks", + "Other" + ], + "keywords": [ + "postgresql", + "postgres", + "database", + "sql", + "sql notebook", + "ai sql", + "database explorer", + "pgadmin", + "db client", + "postgresql vs code extension", + "postgres database explorer", + "sql notebook vs code", + "ai sql assistant" + ], + "contributes": { + "languages": [ + { + "id": "postgres", + "aliases": [ + "PostgreSQL", + "postgres" + ], + "extensions": [ + ".pgsql" + ], + "configuration": "./language-configuration.json" + } + ], + "commands": [ + { + "command": "postgres-explorer.addConnection", + "title": "Add PostgreSQL Connection", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.importConnectionFromDatabaseUrl", + "title": "Import Connection from DATABASE_URL (.env)", + "icon": "$(file-symlink-file)" + }, + { + "command": "postgres-explorer.refreshConnections", + "title": "Refresh Connections", + "icon": "$(refresh)" + }, + { + "command": "postgres-explorer.editConnection", + "title": "Edit Connection", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.duplicateConnection", + "title": "Duplicate Connection", + "icon": "$(copy)" + }, + { + "command": "postgres-explorer.addToFavorites", + "title": "Add to Favorites", + "icon": "$(star-empty)" + }, + { + "command": "postgres-explorer.removeFromFavorites", + "title": "Remove from Favorites", + "icon": "$(star-full)" + }, + { + "command": "postgres-explorer.manageConnections", + "title": "Manage Connections", + "icon": "$(settings-gear)" + }, + { + "command": "postgres-explorer.aiSettings", + "title": "AI Settings", + "icon": "$(sparkle)" + }, + { + "command": "postgres-explorer.showTableProperties", + "title": "Show Table Properties", + "icon": "$(info)" + }, + { + "command": "postgres-explorer.newNotebook", + "title": "New PostgreSQL Notebook", + "icon": "$(new-file)" + }, + { + "command": "postgres-explorer.jumpToSection", + "title": "Jump to Section", + "category": "PgStudio", + "icon": "$(list-unordered)" + }, + { + "command": "postgres-explorer.exportNotebook", + "title": "Export Notebook (HTML / Gist)", + "category": "PgStudio", + "icon": "$(export)" + }, + { + "command": "postgres-explorer.notebooks.open", + "title": "Open Notebook", + "category": "PgStudio", + "icon": "$(go-to-file)" + }, + { + "command": "postgres-explorer.notebooks.rename", + "title": "Rename Notebook", + "category": "PgStudio", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.notebooks.delete", + "title": "Delete Notebook", + "category": "PgStudio", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.notebooks.deleteFolder", + "title": "Delete Folder", + "category": "PgStudio", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.notebooks.refresh", + "title": "Refresh Notebooks", + "category": "PgStudio", + "icon": "$(refresh)" + }, + { + "command": "postgres-explorer.generateQuery", + "title": "AI: Generate Query", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.optimizeQuery", + "title": "AI: Optimize Query", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.openSqlAssistantTab", + "title": "SQL Assistant: Open in Editor Tab", + "category": "PgStudio", + "icon": "$(go-to-file)" + }, + { + "command": "postgres-explorer.connect", + "title": "Connect to PostgreSQL Database" + }, + { + "command": "postgres-explorer.disconnect", + "title": "Disconnect PostgreSQL Database" + }, + { + "command": "postgres-explorer.refresh", + "title": "Refresh", + "icon": "$(refresh)" + }, + { + "command": "postgres-explorer.deleteConnection", + "title": "Delete Connection", + "icon": "$(trash)" + }, + { + "command": "postgresExplorer.openColumnNotebook", + "title": "Open Column Analysis", + "category": "Postgres Explorer" + }, + { + "command": "postgres-explorer.showViewProperties", + "title": "Show View Properties", + "icon": "$(info)" + }, + { + "command": "postgres-explorer.showFunctionProperties", + "title": "Show Function Properties", + "icon": "$(info)" + }, + { + "command": "postgres-explorer.functionOperations", + "title": "Function Operations", + "icon": "$(notebook)" + }, + { + "command": "postgres-explorer.createReplaceFunction", + "title": "Edit Function Definition", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.callFunction", + "title": "Call Function", + "icon": "$(play)" + }, + { + "command": "postgres-explorer.dropFunction", + "title": "Drop Function", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.showProcedureProperties", + "title": "Show Procedure Properties", + "icon": "$(info)" + }, + { + "command": "postgres-explorer.procedureOperations", + "title": "Procedure Operations", + "icon": "$(notebook)" + }, + { + "command": "postgres-explorer.createReplaceProcedure", + "title": "Edit Procedure Definition", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.callProcedure", + "title": "Call Procedure", + "icon": "$(play)" + }, + { + "command": "postgres-explorer.dropProcedure", + "title": "Drop Procedure", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.viewTableData", + "title": "View Table Data", + "icon": "$(table)" + }, + { + "command": "postgres-explorer.dropTable", + "title": "Drop Table", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.editViewDefinition", + "title": "Edit View Definition", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.viewViewData", + "title": "View View Data", + "icon": "$(table)" + }, + { + "command": "postgres-explorer.dropView", + "title": "Drop View", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.tableOperations", + "title": "Table Operations", + "icon": "$(notebook)" + }, + { + "command": "postgres-explorer.viewOperations", + "title": "View Operations", + "icon": "$(notebook)" + }, + { + "command": "postgres-explorer.viewScriptSelect", + "title": "Script as SELECT", + "icon": "$(file-code)" + }, + { + "command": "postgres-explorer.viewScriptCreate", + "title": "Script as CREATE", + "icon": "$(file-code)" + }, + { + "command": "postgres-explorer.viewMaintenanceVacuum", + "title": "VACUUM", + "icon": "$(tools)" + }, + { + "command": "postgres-explorer.viewMaintenanceAnalyze", + "title": "ANALYZE", + "icon": "$(graph)" + }, + { + "command": "postgres-explorer.truncateTable", + "title": "Truncate Table", + "icon": "$(clear-all)" + }, + { + "command": "postgres-explorer.insertData", + "title": "Insert Data", + "icon": "$(insert)" + }, + { + "command": "postgres-explorer.updateData", + "title": "Update Data", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.quickClone", + "title": "Quick Clone Table", + "icon": "$(files)" + }, + { + "command": "postgres-explorer.exportTable", + "title": "Export Table Data", + "icon": "$(export)" + }, + { + "command": "postgres-explorer.createInSchema", + "title": "Create Object", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.schemaOperations", + "title": "Schema Operations", + "icon": "$(notebook)" + }, + { + "command": "postgres-explorer.createSchema", + "title": "Create Schema", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.pasteTable", + "title": "Smart Paste (create table from clipboard)", + "icon": "$(clippy)" + }, + { + "command": "postgres-explorer.refreshMaterializedView", + "title": "Refresh Materialized View", + "category": "Postgres Explorer", + "icon": "$(refresh)" + }, + { + "command": "postgres-explorer.materializedViewOperations", + "title": "Materialized View Operations", + "category": "Postgres Explorer", + "icon": "$(notebook)" + }, + { + "command": "postgres-explorer.foreignTableOperations", + "title": "Foreign Table Operations", + "category": "Postgres Explorer", + "icon": "$(notebook)" + }, + { + "command": "postgres-explorer.dropForeignTable", + "title": "Drop Foreign Table", + "category": "Postgres Explorer", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.typeOperations", + "title": "Type Operations", + "category": "Postgres Explorer", + "icon": "$(notebook)" + }, + { + "command": "postgres-explorer.dropType", + "title": "Drop Type", + "category": "Postgres Explorer", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.createTable", + "title": "Create Table", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.createView", + "title": "Create View", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.createFunction", + "title": "Create Function", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.createProcedure", + "title": "Create Procedure", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.createType", + "title": "Create Type", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.createMaterializedView", + "title": "Create Materialized View", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.createForeignTable", + "title": "Create Foreign Table", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.foreignDataWrapperOperations", + "title": "FDW Operations" + }, + { + "command": "postgres-explorer.showForeignDataWrapperProperties", + "title": "Show FDW Properties", + "icon": "$(info)" + }, + { + "command": "postgres-explorer.refreshForeignDataWrapper", + "title": "Refresh FDW", + "icon": "$(refresh)" + }, + { + "command": "postgres-explorer.createForeignServer", + "title": "Create Foreign Server", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.foreignServerOperations", + "title": "Server Operations" + }, + { + "command": "postgres-explorer.showForeignServerProperties", + "title": "Show Server Properties", + "icon": "$(info)" + }, + { + "command": "postgres-explorer.dropForeignServer", + "title": "Drop Foreign Server" + }, + { + "command": "postgres-explorer.refreshForeignServer", + "title": "Refresh Server", + "icon": "$(refresh)" + }, + { + "command": "postgres-explorer.createUserMapping", + "title": "Create User Mapping", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.userMappingOperations", + "title": "User Mapping Operations" + }, + { + "command": "postgres-explorer.showUserMappingProperties", + "title": "Show User Mapping Properties", + "icon": "$(info)" + }, + { + "command": "postgres-explorer.dropUserMapping", + "title": "Drop User Mapping" + }, + { + "command": "postgres-explorer.refreshUserMapping", + "title": "Refresh User Mapping", + "icon": "$(refresh)" + }, + { + "command": "postgres-explorer.createRole", + "title": "Create Role", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.enableExtension", + "title": "Enable Extension", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.viewMaterializedViewData", + "title": "View Data", + "category": "Postgres Explorer", + "icon": "$(table)" + }, + { + "command": "postgres-explorer.disconnectConnection", + "title": "Disconnect", + "category": "Postgres Explorer", + "icon": "$(debug-disconnect)" + }, + { + "command": "postgres-explorer.reconnectConnection", + "title": "Connect", + "category": "Postgres Explorer", + "icon": "$(plug)" + }, + { + "command": "postgres-explorer.editMatView", + "title": "Edit Materialized View", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.editType", + "title": "Edit Type", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.showTypeProperties", + "title": "Show Type Properties", + "icon": "$(info)" + }, + { + "command": "postgres-explorer.dropType", + "title": "Drop Type", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.editForeignTable", + "title": "Edit Foreign Table", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.viewForeignTableData", + "title": "Query Foreign Table Data", + "icon": "$(table)" + }, + { + "command": "postgres-explorer.showForeignTableProperties", + "title": "Show Foreign Table Properties", + "icon": "$(info)" + }, + { + "command": "postgres-explorer.dropForeignTable", + "title": "Drop Foreign Table", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.dropMatView", + "title": "Drop Materialized View", + "category": "Postgres Explorer", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.editMaterializedView", + "title": "Edit Materialized View", + "category": "Postgres Explorer", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.showMaterializedViewProperties", + "title": "Show Materialized View Properties", + "category": "Postgres Explorer", + "icon": "$(info)" + }, + { + "command": "postgres-explorer.extensionOperations", + "title": "Extension Operations", + "icon": "$(notebook)" + }, + { + "command": "postgres-explorer.dropExtension", + "title": "Drop Extension", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.editRole", + "title": "Edit Role", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.openRoleDesigner", + "title": "Role Designer (Visual)", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.grantRevoke", + "title": "Grant/Revoke Privileges", + "icon": "$(shield)" + }, + { + "command": "postgres-explorer.dropRole", + "title": "Drop Role", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.roleOperations", + "title": "Role Operations", + "icon": "$(notebook)" + }, + { + "command": "postgres-explorer.showRoleProperties", + "title": "Show Role Properties", + "icon": "$(info)" + }, + { + "command": "postgres-explorer.createInDatabase", + "title": "Create Object", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.databaseOperations", + "title": "Database Operations", + "icon": "$(notebook)" + }, + { + "command": "postgres-explorer.showDashboard", + "title": "Show Database Dashboard", + "icon": "$(graph)" + }, + { + "command": "postgres-explorer.showDashboardFromPalette", + "title": "Live Dashboard: Choose Connection & Database…", + "icon": "$(graph)" + }, + { + "command": "postgres-explorer.openListenNotify", + "title": "LISTEN/NOTIFY Monitor", + "icon": "$(broadcast)" + }, + { + "command": "postgres-explorer.openListenNotifyFromPalette", + "title": "LISTEN/NOTIFY Monitor: Choose Connection & Database…", + "icon": "$(broadcast)" + }, + { + "command": "postgres-explorer.showSchemaProperties", + "title": "Properties", + "icon": "$(settings)" + }, + { + "command": "postgres-explorer.editTable", + "title": "Edit Table Definition", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.scriptSelect", + "title": "SELECT Script", + "icon": "$(list-selection)" + }, + { + "command": "postgres-explorer.scriptInsert", + "title": "INSERT Script", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.scriptUpdate", + "title": "UPDATE Script", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.scriptDelete", + "title": "DELETE Script", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.scriptCreate", + "title": "CREATE Script", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.maintenanceVacuum", + "title": "VACUUM", + "icon": "$(broom)" + }, + { + "command": "postgres-explorer.maintenanceAnalyze", + "title": "ANALYZE", + "icon": "$(graph)" + }, + { + "command": "postgres-explorer.maintenanceReindex", + "title": "REINDEX", + "icon": "$(refresh)" + }, + { + "command": "postgres-explorer.backupDatabase", + "title": "Backup...", + "icon": "$(save)" + }, + { + "command": "postgres-explorer.restoreDatabase", + "title": "Restore...", + "icon": "$(cloud-upload)" + }, + { + "command": "postgres-explorer.openBackupWorkspace", + "title": "Backup & Restore Workspace: Choose Connection & Database…", + "category": "PostgreSQL", + "icon": "$(archive)" + }, + { + "command": "postgres-explorer.generateCreateScript", + "title": "CREATE Script", + "icon": "$(file-code)" + }, + { + "command": "postgres-explorer.disconnectDatabase", + "title": "Disconnect from database", + "icon": "$(plug)" + }, + { + "command": "postgres-explorer.maintenanceDatabase", + "title": "Maintenance...", + "icon": "$(tools)" + }, + { + "command": "postgres-explorer.queryTool", + "title": "Query Tool", + "icon": "$(terminal)" + }, + { + "command": "postgres-explorer.psqlTool", + "title": "PSQL Tool", + "category": "PostgreSQL" + }, + { + "command": "postgres-explorer.showConfiguration", + "title": "Show Configuration", + "category": "PostgreSQL" + }, + { + "command": "postgres-explorer.createDatabase", + "title": "Create Database", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.dropDatabase", + "title": "Drop Database", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.scriptAlterDatabase", + "title": "ALTER Script", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.aiAssist", + "title": "Put AI to work", + "icon": "$(sparkle)" + }, + { + "command": "postgres-explorer.attachToChat", + "title": "Attach to SQL Assistant", + "icon": "$(mention)" + }, + { + "command": "postgres-explorer.showColumnProperties", + "title": "Show Column Properties", + "icon": "$(info)" + }, + { + "command": "postgres-explorer.copyColumnName", + "title": "Copy Column Name", + "icon": "$(copy)" + }, + { + "command": "postgres-explorer.copyColumnNameQuoted", + "title": "Copy Column Name (Quoted)", + "icon": "$(copy)" + }, + { + "command": "postgres-explorer.generateSelectStatement", + "title": "SELECT Statement", + "icon": "$(code)" + }, + { + "command": "postgres-explorer.generateWhereClause", + "title": "WHERE Clause", + "icon": "$(code)" + }, + { + "command": "postgres-explorer.generateAlterColumnScript", + "title": "ALTER COLUMN Script", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.generateDropColumnScript", + "title": "DROP COLUMN Script", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.generateRenameColumnScript", + "title": "RENAME COLUMN Script", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.addColumnComment", + "title": "Add/Edit Column Comment", + "icon": "$(comment)" + }, + { + "command": "postgres-explorer.generateIndexOnColumn", + "title": "CREATE INDEX Script", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.viewColumnStatistics", + "title": "View Column Statistics", + "icon": "$(graph)" + }, + { + "command": "postgres-explorer.showConstraintProperties", + "title": "Properties", + "icon": "$(info)" + }, + { + "command": "postgres-explorer.copyConstraintName", + "title": "Copy Constraint Name", + "icon": "$(copy)" + }, + { + "command": "postgres-explorer.generateDropConstraintScript", + "title": "DROP CONSTRAINT Script", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.generateAlterConstraintScript", + "title": "ALTER CONSTRAINT Script", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.validateConstraint", + "title": "Validate Constraint", + "icon": "$(check)" + }, + { + "command": "postgres-explorer.generateAddConstraintScript", + "title": "ADD CONSTRAINT Templates", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.viewConstraintDependencies", + "title": "View Dependencies", + "icon": "$(references)" + }, + { + "command": "postgres-explorer.showIndexProperties", + "title": "Properties", + "icon": "$(info)" + }, + { + "command": "postgres-explorer.copyIndexName", + "title": "Copy Index Name", + "icon": "$(copy)" + }, + { + "command": "postgres-explorer.generateDropIndexScript", + "title": "DROP INDEX Script", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.generateReindexScript", + "title": "REINDEX Script", + "icon": "$(sync)" + }, + { + "command": "postgres-explorer.generateScriptCreate", + "title": "CREATE Script", + "category": "PostgreSQL Explorer" + }, + { + "command": "postgres-explorer.analyzeIndexUsage", + "title": "Analyze Index Usage", + "icon": "$(graph)" + }, + { + "command": "postgres-explorer.generateAlterIndexScript", + "title": "ALTER INDEX Script", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.addIndexComment", + "title": "Add Comment", + "icon": "$(comment)" + }, + { + "command": "postgres-explorer.constraintOperations", + "title": "Constraint Operations", + "icon": "$(notebook)" + }, + { + "command": "postgres-explorer.indexOperations", + "title": "Index Operations", + "icon": "$(notebook)" + }, + { + "command": "postgres-explorer.addColumn", + "title": "Add Column", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.addConstraint", + "title": "Add Constraint", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.addIndex", + "title": "Add Index", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.clearHistory", + "title": "Clear History", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.copyQuery", + "title": "Copy Query", + "icon": "$(copy)" + }, + { + "command": "postgres-explorer.openQuery", + "title": "Open Query", + "icon": "$(go-to-file)" + }, + { + "command": "postgres-explorer.deleteHistoryItem", + "title": "Delete", + "icon": "$(trash)" + }, + { + "command": "postgres-explorer.explainQuery", + "title": "EXPLAIN Query", + "icon": "$(graph)" + }, + { + "command": "postgres-explorer.tableProfile", + "title": "View Table Profile", + "icon": "$(graph)" + }, + { + "command": "postgres-explorer.tableActivity", + "title": "View Table Activity", + "icon": "$(pulse)" + }, + { + "command": "postgres-explorer.indexUsage", + "title": "View Index Usage", + "icon": "$(list-tree)" + }, + { + "command": "postgres-explorer.tableDefinition", + "title": "View Table Definition", + "icon": "$(symbol-class)" + }, + { + "command": "postgres-explorer.openDefinition", + "title": "Open Definition Viewer", + "category": "PgStudio", + "icon": "$(file-code)" + }, + { + "command": "postgres-explorer.ddlViewer.openEditableCopy", + "title": "DDL Viewer: Open as Editable Copy", + "category": "PgStudio", + "icon": "$(edit)" + }, + { + "command": "postgres-explorer.ddlViewer.copyToClipboard", + "title": "DDL Viewer: Copy to Clipboard", + "category": "PgStudio", + "icon": "$(copy)" + }, + { + "command": "postgres-explorer.ddlViewer.executeRoutine", + "title": "DDL Viewer: Execute Routine Scaffold", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.ddlViewer.toggleEnabled", + "title": "DDL Viewer: Toggle SQL Preview", + "category": "PgStudio", + "icon": "$(file-code)" + }, + { + "command": "postgres-explorer.switchConnection", + "title": "Switch Connection", + "icon": "$(server)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.switchDatabase", + "title": "Switch Database", + "icon": "$(database)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.switchConnectionProfile", + "title": "Switch Connection Profile", + "icon": "$(person)", + "category": "PgStudio: Profiles" + }, + { + "command": "postgres-explorer.switchWorkspaceDefaultConnection", + "title": "Set Workspace Default PostgreSQL Connection", + "icon": "$(root-folder)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.createConnectionProfile", + "title": "Create Connection Profile", + "icon": "$(add)", + "category": "PgStudio: Profiles" + }, + { + "command": "postgres-explorer.deleteConnectionProfile", + "title": "Delete Connection Profile", + "icon": "$(trash)", + "category": "PgStudio: Profiles" + }, + { + "command": "postgres-explorer.saveQueryToLibrary", + "title": "Save Query to Library", + "icon": "$(save)", + "category": "PgStudio: Queries" + }, + { + "command": "postgres-explorer.saveQueryToLibraryUI", + "title": "💾 Save Query (UI Form)", + "icon": "$(save)", + "category": "PgStudio: Queries" + }, + { + "command": "postgres-explorer.loadSavedQuery", + "title": "Load Saved Query", + "icon": "$(folder-opened)", + "category": "PgStudio: Queries" + }, + { + "command": "postgres-explorer.loadSavedQueryUI", + "title": "📂 Load Saved Query", + "icon": "$(folder-opened)", + "category": "PgStudio: Queries" + }, + { + "command": "postgres-explorer.viewSavedQuery", + "title": "📌 View Saved Query", + "icon": "$(preview)", + "category": "PgStudio: Queries" + }, + { + "command": "postgres-explorer.copySavedQuery", + "title": "📋 Copy Query", + "icon": "$(copy)", + "category": "PgStudio: Queries" + }, + { + "command": "postgres-explorer.editSavedQuery", + "title": "✏️ Edit Query", + "icon": "$(edit)", + "category": "PgStudio: Queries" + }, + { + "command": "postgres-explorer.openSavedQueryInNotebook", + "title": "📘 Open in Notebook", + "icon": "$(notebook)", + "category": "PgStudio: Queries" + }, + { + "command": "postgres-explorer.deleteSavedQuery", + "title": "Delete Saved Query", + "icon": "$(trash)", + "category": "PgStudio: Queries" + }, + { + "command": "postgres-explorer.searchSavedQueries", + "title": "Search Saved Queries", + "icon": "$(search)", + "category": "PgStudio: Queries" + }, + { + "command": "postgres-explorer.showQueryRecommendations", + "title": "Show Query Recommendations", + "icon": "$(lightbulb)", + "category": "PgStudio: Queries" + }, + { + "command": "postgres-explorer.exportSavedQueries", + "title": "Export Saved Queries", + "icon": "$(export)", + "category": "PgStudio: Queries" + }, + { + "command": "postgres-explorer.importSavedQueries", + "title": "Import Saved Queries", + "icon": "$(import)", + "category": "PgStudio: Queries" + }, + { + "command": "postgres-explorer.openTableDesigner", + "title": "Open Table Designer (Visual)", + "icon": "$(layout)", + "category": "PgStudio: Schema" + }, + { + "command": "postgres-explorer.createTableVisual", + "title": "Create Table (Visual)", + "icon": "$(add)", + "category": "PgStudio: Schema" + }, + { + "command": "postgres-explorer.openSchemaDiff", + "title": "Schema Diff", + "icon": "$(diff)", + "category": "PgStudio: Schema" + }, + { + "command": "postgres-explorer.openSchemaDiffFromPalette", + "title": "Schema Diff: Choose Connection & Schema…", + "icon": "$(diff)", + "category": "PgStudio: Schema" + }, + { + "command": "postgres-explorer.openErd", + "title": "View ERD (Entity-Relationship Diagram)", + "icon": "$(type-hierarchy)", + "category": "PgStudio: Schema" + }, + { + "command": "postgres-explorer.openErdMulti", + "title": "View ERD (pick schemas)…", + "icon": "$(type-hierarchy)", + "category": "PgStudio: Schema" + }, + { + "command": "postgres-explorer.importDbml", + "title": "Import DBML → CREATE TABLE…", + "icon": "$(file-code)", + "category": "PgStudio: Schema" + }, + { + "command": "postgres-explorer.importData", + "title": "Import Data…", + "icon": "$(cloud-upload)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.exportConnectionProfiles", + "title": "Export Connection Profiles…", + "icon": "$(export)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.importConnectionProfiles", + "title": "Import Connection Profiles…", + "icon": "$(import)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.formatSql", + "title": "Format SQL", + "icon": "$(symbol-keyword)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.telemetry.openModePicker", + "title": "Set Telemetry Mode", + "icon": "$(pulse)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.telemetry.setModeOff", + "title": "Telemetry: Off", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.telemetry.setModeBasic", + "title": "Telemetry: Basic", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.telemetry.setModeDetailed", + "title": "Telemetry: Detailed", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.showWhatsNew", + "title": "What's New in PgStudio", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.listTriggers", + "title": "List Triggers", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.createTrigger", + "title": "Create Trigger", + "icon": "$(add)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.dropTrigger", + "title": "Drop Trigger", + "icon": "$(trash)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.dropPolicy", + "title": "Drop RLS Policy", + "icon": "$(trash)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.enableTrigger", + "title": "Enable Trigger", + "icon": "$(play)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.disableTrigger", + "title": "Disable Trigger", + "icon": "$(debug-pause)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.showTriggerProperties", + "title": "Trigger Properties", + "icon": "$(info)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.triggerOperations", + "title": "Trigger Operations", + "icon": "$(notebook)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.listSequences", + "title": "List Sequences", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.createSequence", + "title": "Create Sequence", + "icon": "$(add)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.dropSequence", + "title": "Drop Sequence", + "icon": "$(trash)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.sequenceNextValue", + "title": "Next Value", + "icon": "$(arrow-right)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.showSequenceProperties", + "title": "Sequence Properties", + "icon": "$(info)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.sequenceOperations", + "title": "Sequence Operations", + "icon": "$(notebook)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.listPartitions", + "title": "List Partitions", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.detachPartition", + "title": "Detach Partition", + "icon": "$(debug-disconnect)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.showPartitionProperties", + "title": "Partition Properties", + "icon": "$(info)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.createPartition", + "title": "Create Partition", + "icon": "$(add)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.listDomains", + "title": "List Domains", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.createDomain", + "title": "Create Domain", + "icon": "$(add)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.dropDomain", + "title": "Drop Domain", + "icon": "$(trash)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.showDomainProperties", + "title": "Domain Properties", + "icon": "$(info)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.listAggregates", + "title": "List Aggregates", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.createAggregate", + "title": "Create Aggregate", + "icon": "$(add)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.dropAggregate", + "title": "Drop Aggregate", + "icon": "$(trash)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.showAggregateProperties", + "title": "Aggregate Properties", + "icon": "$(info)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.listEventTriggers", + "title": "List Event Triggers", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.createEventTrigger", + "title": "Create Event Trigger", + "icon": "$(add)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.dropEventTrigger", + "title": "Drop Event Trigger", + "icon": "$(trash)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.enableEventTrigger", + "title": "Enable Event Trigger", + "icon": "$(play)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.disableEventTrigger", + "title": "Disable Event Trigger", + "icon": "$(debug-pause)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.showEventTriggerProperties", + "title": "Event Trigger Properties", + "icon": "$(info)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.eventTriggerOperations", + "title": "Event Trigger Operations", + "icon": "$(notebook)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.listCronJobs", + "title": "List pg_cron Jobs", + "icon": "$(list-unordered)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.installPgCron", + "title": "Install pg_cron Extension", + "icon": "$(cloud-download)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.scheduleCronJob", + "title": "Schedule New pg_cron Job", + "icon": "$(add)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.showCronJobProperties", + "title": "Cron Job Details", + "icon": "$(info)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.unscheduleCronJob", + "title": "Unschedule Cron Job", + "icon": "$(trash)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.listRules", + "title": "List Rules", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.dropRule", + "title": "Drop Rule", + "icon": "$(trash)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.showRuleProperties", + "title": "Rule Properties", + "icon": "$(info)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.ruleOperations", + "title": "Rule Operations", + "icon": "$(notebook)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.listTablespaces", + "title": "List Tablespaces", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.showTablespaceProperties", + "title": "Tablespace Properties", + "icon": "$(info)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.tablespaceOperations", + "title": "Tablespace Operations", + "icon": "$(notebook)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.listPublications", + "title": "List Publications", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.createPublication", + "title": "Create Publication", + "icon": "$(add)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.dropPublication", + "title": "Drop Publication", + "icon": "$(trash)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.showPublicationProperties", + "title": "Publication Properties", + "icon": "$(info)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.publicationOperations", + "title": "Publication Operations", + "icon": "$(notebook)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.listSubscriptions", + "title": "List Subscriptions", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.dropSubscription", + "title": "Drop Subscription", + "icon": "$(trash)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.showSubscriptionProperties", + "title": "Subscription Properties", + "icon": "$(info)", + "category": "PgStudio" + }, + { + "command": "postgres-explorer.searchSchema", + "title": "Search Schema Objects", + "icon": "$(search)", + "category": "PgStudio" + } + ], + "submenus": [ + { + "id": "postgres-explorer.scriptsMenu", + "label": "Scripts", + "icon": "$(code)" + }, + { + "id": "postgres-explorer.maintenanceMenu", + "label": "Maintenance", + "icon": "$(tools)" + }, + { + "id": "postgres-explorer.viewScriptsMenu", + "label": "Scripts", + "icon": "$(code)" + }, + { + "id": "postgres-explorer.databaseScriptsMenu", + "label": "Scripts", + "icon": "$(code)" + }, + { + "id": "postgres-explorer.columnScriptsMenu", + "label": "Scripts", + "icon": "$(code)" + }, + { + "id": "postgres-explorer.constraintScriptsMenu", + "label": "Scripts" + }, + { + "id": "postgres-explorer.indexScriptsMenu", + "label": "Scripts" + } + ], + "viewsContainers": { + "activitybar": [ + { + "id": "postgres-explorer", + "title": "PG Studio", + "icon": "resources/postgres-vsc-icon.png" + }, + { + "id": "postgres-sql-assistant", + "title": "SQL Assistant", + "icon": "resources/sql-assistant-vsc-icon.png" + } + ] + }, + "views": { + "postgres-explorer": [ + { + "id": "postgresExplorer", + "name": "Connections", + "contextualTitle": "PG Studio", + "icon": "resources/postgres-vsc-icon.png" + }, + { + "id": "postgresExplorer.notebooks", + "name": "Notebooks", + "contextualTitle": "PG Studio", + "icon": "$(notebook)" + }, + { + "id": "postgresExplorer.savedQueries", + "name": "Saved Queries", + "contextualTitle": "PG Studio", + "icon": "$(save)", + "visibility": "collapsed" + }, + { + "id": "postgresExplorer.history", + "name": "Query History", + "contextualTitle": "PG Studio", + "icon": "$(history)", + "visibility": "collapsed" + } + ], + "postgres-sql-assistant": [ + { + "id": "postgresExplorer.chatView", + "name": "SQL Assistant", + "type": "webview", + "contextualTitle": "SQL Assistant", + "icon": "resources/sql-assistant-vsc-icon.png" + } + ] + }, + "walkthroughs": [ + { + "id": "pgstudioWelcome", + "title": "PgStudio: PostgreSQL in VS Code", + "description": "Connect to a database, explore objects, and run SQL in notebooks.", + "steps": [ + { + "id": "pgstudioWelcome.addConnection", + "title": "Add a database connection", + "description": "Save your server and credentials securely, then connect.\n[Add PostgreSQL Connection](command:postgres-explorer.addConnection)", + "media": { + "markdown": "walkthroughs/step-connection.md", + "altText": "Add a PostgreSQL connection" + }, + "completionEvents": [ + "onCommand:postgres-explorer.addConnection" + ] + }, + { + "id": "pgstudioWelcome.openExplorer", + "title": "Open the PG Studio explorer", + "description": "Browse databases, schemas, and objects from the sidebar.\n[Show PG Studio](command:workbench.view.extension.postgres-explorer)", + "media": { + "markdown": "walkthroughs/step-explorer.md", + "altText": "Database explorer tree" + }, + "completionEvents": [ + "onView:postgresExplorer" + ] + }, + { + "id": "pgstudioWelcome.newNotebook", + "title": "Create a SQL notebook", + "description": "Use interactive `.pgsql` notebooks for queries and results.\n[New SQL Notebook](command:postgres-explorer.newNotebook)", + "media": { + "markdown": "walkthroughs/step-notebook.md", + "altText": "PostgreSQL SQL notebook" + }, + "completionEvents": [ + "onCommand:postgres-explorer.newNotebook" + ] + } + ] + } + ], + "notebooks": [ + { + "type": "postgres-notebook", + "displayName": "PostgreSQL Notebook", + "selector": [ + { + "filenamePattern": "*.pgsql" + } + ], + "priority": "default", + "enableScripts": true + }, + { + "type": "postgres-query", + "displayName": "Postgres Query", + "selector": [ + { + "filenamePattern": "*.pgquery" + } + ], + "priority": "default", + "enableScripts": true + } + ], + "notebookRenderer": [ + { + "id": "postgres-query-renderer", + "displayName": "Postgres Query Renderer", + "entrypoint": "./dist/renderer_v2.js", + "mimeTypes": [ + "application/x-postgres-result", + "application/vnd.postgres-notebook.result", + "application/vnd.postgres-notebook.notices-live", + "application/vnd.postgres-notebook.error", + "application/x-postgres-notebook-header+json" + ], + "requiresMessaging": "always" + } + ], + "configuration": { + "title": "PostgreSQL Explorer", + "properties": { + "postgresExplorer.connections": { + "type": "array", + "default": [], + "description": "List of saved PostgreSQL connections", + "items": { + "type": "object", + "required": [ + "id", + "name", + "host", + "port", + "username" + ], + "properties": { + "id": { + "type": "string", + "description": "Unique identifier for the connection" + }, + "name": { + "type": "string", + "description": "Display name for the connection" + }, + "host": { + "type": "string", + "description": "Hostname or IP address of the PostgreSQL server" + }, + "port": { + "type": "number", + "description": "Port number of the PostgreSQL server", + "default": 5432 + }, + "username": { + "type": "string", + "description": "Username for authentication" + }, + "database": { + "type": "string", + "description": "Default database to connect to" + }, + "group": { + "type": "string", + "description": "Group name for organizing connections" + }, + "environment": { + "type": "string", + "enum": [ + "production", + "staging", + "development" + ], + "description": "Environment tag for safety warnings and visual indicators" + }, + "readOnlyMode": { + "type": "boolean", + "default": false, + "description": "Force read-only transactions for this connection" + } + } + } + }, + "postgresExplorer.aiProvider": { + "type": "string", + "enum": [ + "vscode-lm", + "github", + "cursor", + "openai", + "anthropic", + "gemini", + "custom" + ], + "default": "vscode-lm", + "description": "AI Provider to use for query assistance" + }, + "postgresExplorer.aiApiKey": { + "type": "string", + "default": "", + "description": "API Key for direct AI providers (OpenAI/Anthropic/Gemini/custom)" + }, + "postgresExplorer.aiModel": { + "type": "string", + "default": "", + "description": "Model name (e.g., gpt-4, claude-3-opus). Leave empty for defaults." + }, + "postgresExplorer.aiEndpoint": { + "type": "string", + "default": "", + "description": "Custom API endpoint (optional)" + }, + "postgresExplorer.streaming.enabled": { + "type": "boolean", + "default": true, + "description": "Enable cursor-based streaming for large result sets" + }, + "postgresExplorer.streaming.batchSize": { + "type": "number", + "default": 200, + "description": "Number of rows per batch when streaming results" + }, + "postgresExplorer.telemetry.mode": { + "type": "string", + "enum": [ + "off", + "basic", + "detailed" + ], + "default": "basic", + "description": "Telemetry collection mode. off disables telemetry. basic sends anonymous usage counters. detailed adds anonymized performance buckets." + }, + "postgresExplorer.telemetry.allowUsage": { + "type": "boolean", + "default": true, + "description": "Allow anonymous usage telemetry (commands/features/session counters)." + }, + "postgresExplorer.telemetry.allowPerformance": { + "type": "boolean", + "default": false, + "description": "Allow anonymized performance telemetry (duration/result-size buckets)." + }, + "postgresExplorer.telemetry.flushIntervalMs": { + "type": "number", + "default": 10000, + "minimum": 1000, + "description": "Telemetry flush interval in milliseconds." + }, + "postgresExplorer.telemetry.maxBatchSize": { + "type": "number", + "default": 25, + "minimum": 1, + "maximum": 100, + "description": "Maximum number of events per telemetry batch." + }, + "postgresExplorer.telemetry.posthogHost": { + "type": "string", + "default": "https://us.i.posthog.com", + "description": "PostHog ingestion host used for telemetry transport." + }, + "postgresExplorer.telemetry.posthogApiKey": { + "type": "string", + "default": "phc_ok5KcqPzWKC52rJHJV8Q9ALQWPKQYukgPFJj56WP8s9N", + "description": "PostHog project API key. Keep empty to disable remote telemetry sink." + }, + "postgresExplorer.queryTimeout": { + "type": "number", + "default": 0, + "description": "Global query timeout in milliseconds (0 for no timeout). Can be overridden per connection." + }, + "postgresExplorer.performance.slowQueryThresholdMs": { + "type": "number", + "default": 2000, + "description": "Threshold in milliseconds to flag slow queries in history and results." + }, + "postgresExplorer.performance.defaultLimit": { + "type": "number", + "default": 1000, + "description": "Default row limit for SELECT queries when auto-LIMIT is enabled." + }, + "postgresExplorer.performance.slidingWindowSelects": { + "type": "boolean", + "default": true, + "description": "Use a PostgreSQL SCROLL cursor for eligible SELECT queries (no SQL parameters): stream one window of rows into the cell and fetch more on scroll—bounded RAM on host and webview." + }, + "postgresExplorer.performance.slidingWindowRowCap": { + "type": "number", + "default": 100, + "minimum": 10, + "maximum": 500, + "description": "Maximum rows kept in the result grid at once when sliding-window streaming is active." + }, + "postgresExplorer.query.autoLimitEnabled": { + "type": "boolean", + "default": true, + "description": "Automatically append LIMIT clause to SELECT queries that don't have one. Always enabled in read-only mode." + }, + "postgresExplorer.query.byteaDisplayFormat": { + "type": "string", + "enum": [ + "hex0x", + "postgresql", + "json" + ], + "default": "hex0x", + "description": "Display format for binary (bytea) columns in query results: 0x-prefixed hex, PostgreSQL \\x hex, or JSON Buffer shape (debug)." + }, + "postgresExplorer.query.executionFailureStrategy": { + "type": "string", + "enum": [ + "continue-on-error", + "fail-on-error", + "prompt-on-error" + ], + "default": "continue-on-error", + "description": "Strategy when a statement fails in multi-statement execution: 'continue-on-error' (execute remaining, show summary), 'fail-on-error' (stop immediately), 'prompt-on-error' (ask user per failure)." + }, + "postgresExplorer.parameters.cacheLastValues": { + "type": "boolean", + "default": true, + "description": "Remember the last entered SQL parameter values in this workspace." + }, + "postgresExplorer.parameters.nullSentinel": { + "type": "string", + "default": "NULL", + "description": "Exact input text that is sent as SQL NULL for prompted parameters. Set empty string to disable." + }, + "postgresExplorer.autoRefresh.enabled": { + "type": "boolean", + "default": true, + "description": "Enable automatic background refresh of the explorer tree and notebooks panel." + }, + "postgresExplorer.autoRefresh.pollIntervalSeconds": { + "type": "number", + "default": 30, + "minimum": 10, + "description": "How often (in seconds) to poll PostgreSQL for schema changes." + }, + "postgresExplorer.formatter.keywordCase": { + "type": "string", + "enum": [ + "upper", + "lower", + "preserve" + ], + "default": "upper", + "description": "Keyword case used by the SQL formatter." + }, + "postgresExplorer.formatter.indentStyle": { + "type": "string", + "enum": [ + "standard", + "tabularLeft", + "tabularRight" + ], + "default": "standard", + "description": "Indent style used by the SQL formatter." + }, + "postgresExplorer.formatter.tabWidth": { + "type": "number", + "default": 2, + "minimum": 1, + "description": "Number of spaces per indent level in the SQL formatter." + }, + "postgresExplorer.formatter.useTabs": { + "type": "boolean", + "default": false, + "description": "Use tabs instead of spaces for SQL formatter indentation." + }, + "postgresExplorer.formatter.linesBetweenQueries": { + "type": "number", + "default": 1, + "minimum": 0, + "description": "Number of blank lines between SQL statements in the formatter output." + }, + "postgresExplorer.formatter.formatOnSave": { + "type": "boolean", + "default": false, + "description": "Automatically format .pgsql / .sql files on save." + }, + "pgstudio.ddlViewer.openOnSelection": { + "type": "boolean", + "default": true, + "description": "Open the Definition Viewer automatically when selecting supported tree items." + }, + "pgstudio.ddlViewer.enabled": { + "type": "boolean", + "default": true, + "description": "Enable SQL Preview (Definition Viewer) features and actions." + } + } + }, + "configurationDefaults": { + "[sql]": { + "editor.quickSuggestions": { + "other": true, + "comments": false, + "strings": true + }, + "editor.suggestOnTriggerCharacters": true, + "editor.wordBasedSuggestions": "allDocuments", + "editor.acceptSuggestionOnEnter": "on" + } + }, + "taskDefinitions": [ + { + "type": "pgstudio-pgdump", + "required": [ + "connectionId", + "databaseName", + "outputPath" + ], + "properties": { + "connectionId": { + "type": "string", + "description": "Connection id (postgresExplorer.connections[].id)" + }, + "databaseName": { + "type": "string", + "description": "Database to dump" + }, + "outputPath": { + "type": "string", + "description": "Output file or directory path (supports ${workspaceFolder})" + }, + "dumpFormat": { + "type": "string", + "enum": [ + "custom", + "plain", + "directory", + "tar" + ], + "description": "pg_dump -F mapping", + "default": "custom" + }, + "verbose": { + "type": "boolean", + "default": true + }, + "schemaOnly": { + "type": "boolean", + "default": false + }, + "dataOnly": { + "type": "boolean", + "default": false + } + } + } + ], + "menus": { + "notebook/toolbar": [ + { + "command": "postgres-explorer.jumpToSection", + "when": "notebookType == 'postgres-notebook'", + "group": "navigation@99" + }, + { + "command": "postgres-explorer.exportNotebook", + "when": "notebookType == 'postgres-notebook' || notebookType == 'postgres-query'", + "group": "navigation@98" + } + ], + "view/title": [ + { + "command": "postgres-explorer.notebooks.refresh", + "when": "view == postgresExplorer.notebooks", + "group": "navigation" + }, + { + "command": "postgres-explorer.addConnection", + "when": "view == postgresExplorer", + "group": "navigation" + }, + { + "command": "postgres-explorer.importConnectionFromDatabaseUrl", + "when": "view == postgresExplorer", + "group": "navigation" + }, + { + "command": "postgres-explorer.refreshConnections", + "when": "view == postgresExplorer", + "group": "navigation" + }, + { + "command": "postgres-explorer.ddlViewer.toggleEnabled", + "when": "view == postgresExplorer", + "group": "navigation" + }, + { + "command": "postgres-explorer.manageConnections", + "when": "view == postgresExplorer", + "group": "navigation" + }, + { + "command": "postgres-explorer.aiSettings", + "when": "view == postgresExplorer", + "group": "navigation" + }, + { + "command": "postgres-explorer.clearHistory", + "when": "view == postgresExplorer.history", + "group": "navigation" + }, + { + "command": "postgres-explorer.switchConnectionProfile", + "when": "view == postgresExplorer", + "group": "1_phase7" + }, + { + "command": "postgres-explorer.createConnectionProfile", + "when": "view == postgresExplorer", + "group": "1_phase7" + }, + { + "command": "postgres-explorer.showQueryRecommendations", + "when": "view == postgresExplorer", + "group": "2_phase7" + }, + { + "command": "postgres-explorer.loadSavedQuery", + "when": "view == postgresExplorer", + "group": "2_phase7" + }, + { + "command": "postgres-explorer.openSqlAssistantTab", + "when": "view == postgresExplorer.chatView", + "group": "navigation" + } + ], + "editor/title": [ + { + "command": "postgres-explorer.ddlViewer.copyToClipboard", + "when": "resourceScheme == pgstudio-ddl", + "group": "navigation@1" + }, + { + "command": "postgres-explorer.ddlViewer.toggleEnabled", + "when": "resourceScheme == pgstudio-ddl", + "group": "navigation@2" + }, + { + "command": "postgres-explorer.ddlViewer.openEditableCopy", + "when": "resourceScheme == pgstudio-ddl", + "group": "navigation@3" + } + ], + "view/item/context": [ + { + "command": "postgres-explorer.notebooks.open", + "when": "view == postgresExplorer.notebooks && viewItem == notebook-file", + "group": "inline@1" + }, + { + "command": "postgres-explorer.notebooks.rename", + "when": "view == postgresExplorer.notebooks && viewItem == notebook-file", + "group": "1_actions" + }, + { + "command": "postgres-explorer.notebooks.delete", + "when": "view == postgresExplorer.notebooks && viewItem == notebook-file", + "group": "9_destructive" + }, + { + "command": "postgres-explorer.notebooks.deleteFolder", + "when": "view == postgresExplorer.notebooks && viewItem =~ /^(connection-folder|db-folder)$/", + "group": "9_destructive" + }, + { + "command": "postgres-explorer.copyQuery", + "when": "view == postgresExplorer.history", + "group": "1_modification" + }, + { + "command": "postgres-explorer.openQuery", + "when": "view == postgresExplorer.history", + "group": "1_modification" + }, + { + "command": "postgres-explorer.deleteHistoryItem", + "when": "view == postgresExplorer.history", + "group": "2_destructive" + }, + { + "command": "postgres-explorer.attachToChat", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition|view|function|procedure|materialized-view|type|foreign-table|schema)$/", + "group": "inline@0" + }, + { + "command": "postgres-explorer.openDefinition", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition|view|function|procedure|index|materialized-view|sequence|type|trigger|policy|extension|role|foreign-table|foreign-data-wrapper|foreign-server)$/", + "group": "inline@1" + }, + { + "command": "postgres-explorer.showTableProperties", + "when": "view == postgresExplorer && viewItem == table", + "group": "inline@2" + }, + { + "command": "postgres-explorer.openRoleDesigner", + "when": "view == postgresExplorer && viewItem == role", + "group": "1_actions@1" + }, + { + "command": "postgres-explorer.newNotebook", + "when": "view == postgresExplorer && viewItem =~ /^(schema|table|partition)$/", + "group": "1_actions" + }, + { + "command": "postgres-explorer.deleteConnection", + "when": "view == postgresExplorer && viewItem == connection", + "group": "9_delete" + }, + { + "command": "postgres-explorer.addToFavorites", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition|view|function|procedure|materialized-view)$/ && !postgresExplorer.isFavorite", + "group": "0_favorites" + }, + { + "command": "postgres-explorer.removeFromFavorites", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition|view|function|procedure|materialized-view)$/ && postgresExplorer.isFavorite", + "group": "0_favorites" + }, + { + "command": "postgres-explorer.disconnectConnection", + "when": "view == postgresExplorer && viewItem == connection", + "group": "inline@1" + }, + { + "command": "postgres-explorer.reconnectConnection", + "when": "view == postgresExplorer && viewItem == connection-disconnected", + "group": "inline@1" + }, + { + "command": "postgres-explorer.editConnection", + "when": "view == postgresExplorer && (viewItem == connection || viewItem == connection-disconnected)", + "group": "inline@2" + }, + { + "command": "postgres-explorer.duplicateConnection", + "when": "view == postgresExplorer && (viewItem == connection || viewItem == connection-disconnected)", + "group": "inline@3" + }, + { + "command": "postgres-explorer.createDatabase", + "when": "view == postgresExplorer && viewItem == connection", + "group": "1_actions" + }, + { + "command": "postgres-explorer.showViewProperties", + "when": "view == postgresExplorer && viewItem == view", + "group": "inline@2" + }, + { + "command": "postgres-explorer.showFunctionProperties", + "when": "view == postgresExplorer && viewItem == function", + "group": "inline@2" + }, + { + "command": "postgres-explorer.functionOperations", + "when": "view == postgresExplorer && viewItem == function", + "group": "1_actions" + }, + { + "command": "postgres-explorer.createReplaceFunction", + "when": "view == postgresExplorer && viewItem == function", + "group": "1_actions@1" + }, + { + "command": "postgres-explorer.callFunction", + "when": "view == postgresExplorer && viewItem == function", + "group": "1_actions@2" + }, + { + "command": "postgres-explorer.dropFunction", + "when": "view == postgresExplorer && viewItem == function", + "group": "9_delete" + }, + { + "command": "postgres-explorer.showProcedureProperties", + "when": "view == postgresExplorer && viewItem == procedure", + "group": "inline@2" + }, + { + "command": "postgres-explorer.procedureOperations", + "when": "view == postgresExplorer && viewItem == procedure", + "group": "1_actions" + }, + { + "command": "postgres-explorer.createReplaceProcedure", + "when": "view == postgresExplorer && viewItem == procedure", + "group": "1_actions@1" + }, + { + "command": "postgres-explorer.callProcedure", + "when": "view == postgresExplorer && viewItem == procedure", + "group": "1_actions@2" + }, + { + "command": "postgres-explorer.dropProcedure", + "when": "view == postgresExplorer && viewItem == procedure", + "group": "9_delete" + }, + { + "command": "postgres-explorer.viewTableData", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition)$/", + "group": "inline@2" + }, + { + "command": "postgres-explorer.dropTable", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition)$/", + "group": "9_delete" + }, + { + "command": "postgres-explorer.viewViewData", + "when": "view == postgresExplorer && viewItem == view", + "group": "inline@2" + }, + { + "command": "postgres-explorer.dropView", + "when": "view == postgresExplorer && viewItem == view", + "group": "9_delete" + }, + { + "command": "postgres-explorer.tableOperations", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition)$/", + "group": "1_actions@0" + }, + { + "submenu": "postgres-explorer.scriptsMenu", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition)$/", + "group": "1_actions@1" + }, + { + "submenu": "postgres-explorer.maintenanceMenu", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition)$/", + "group": "1_actions@2" + }, + { + "command": "postgres-explorer.tableProfile", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition)$/", + "group": "2_analysis@0" + }, + { + "command": "postgres-explorer.tableActivity", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition)$/", + "group": "2_analysis@1" + }, + { + "command": "postgres-explorer.indexUsage", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition)$/", + "group": "2_analysis@2" + }, + { + "command": "postgres-explorer.tableDefinition", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition)$/", + "group": "2_analysis@3" + }, + { + "command": "postgres-explorer.viewOperations", + "when": "view == postgresExplorer && viewItem == view", + "group": "1_actions@0" + }, + { + "command": "postgres-explorer.editViewDefinition", + "when": "view == postgresExplorer && viewItem == view", + "group": "1_actions@1" + }, + { + "submenu": "postgres-explorer.viewScriptsMenu", + "when": "view == postgresExplorer && viewItem == view", + "group": "1_actions@2" + }, + { + "command": "postgres-explorer.truncateTable", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition)$/", + "group": "2_operations@1" + }, + { + "command": "postgres-explorer.quickClone", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition)$/", + "group": "2_operations@0" + }, + { + "command": "postgres-explorer.insertData", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition)$/", + "group": "2_operations@2" + }, + { + "command": "postgres-explorer.exportTable", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition)$/", + "group": "2_operations@3" + }, + { + "command": "postgres-explorer.updateData", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition)$/", + "group": "2_operations@3" + }, + { + "command": "postgres-explorer.showSchemaProperties", + "when": "view == postgresExplorer && viewItem == schema", + "group": "inline@2" + }, + { + "command": "postgres-explorer.pasteTable", + "when": "view == postgresExplorer && viewItem == schema", + "group": "1_actions" + }, + { + "command": "postgres-explorer.pasteTable", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition)$/", + "group": "1_actions" + }, + { + "command": "postgres-explorer.schemaOperations", + "when": "view == postgresExplorer && viewItem == schema", + "group": "inline@1", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.schemaOperations", + "when": "view == postgresExplorer && viewItem == schema", + "group": "1_actions", + "icon": "$(notebook)" + }, + { + "command": "postgres-explorer.refreshMaterializedView", + "when": "view == postgresExplorer && viewItem == materialized-view", + "group": "inline" + }, + { + "command": "postgres-explorer.materializedViewOperations", + "when": "view == postgresExplorer && viewItem == materialized-view", + "group": "1_operations@0" + }, + { + "command": "postgres-explorer.editMaterializedView", + "when": "view == postgresExplorer && viewItem == materialized-view", + "group": "1_operations@1" + }, + { + "command": "postgres-explorer.foreignTableOperations", + "when": "view == postgresExplorer && viewItem == foreign-table", + "group": "1_operations@0" + }, + { + "command": "postgres-explorer.typeOperations", + "when": "view == postgresExplorer && viewItem == type", + "group": "1_operations@0" + }, + { + "command": "postgres-explorer.editType", + "when": "view == postgresExplorer && viewItem == type", + "group": "1_operations@1" + }, + { + "command": "postgres-explorer.createDatabase", + "when": "view == postgresExplorer && viewItem == databases-group", + "group": "inline" + }, + { + "command": "postgres-explorer.createSchema", + "when": "view == postgresExplorer && viewItem == category-schemas", + "group": "inline" + }, + { + "command": "postgres-explorer.createRole", + "when": "view == postgresExplorer && viewItem == category-users-roles", + "group": "inline" + }, + { + "command": "postgres-explorer.enableExtension", + "when": "view == postgresExplorer && viewItem == category-extensions", + "group": "inline" + }, + { + "command": "postgres-explorer.createTable", + "when": "view == postgresExplorer && viewItem == category-tables", + "group": "inline" + }, + { + "command": "postgres-explorer.createView", + "when": "view == postgresExplorer && viewItem == category-views", + "group": "inline" + }, + { + "command": "postgres-explorer.createFunction", + "when": "view == postgresExplorer && viewItem == category-functions", + "group": "inline" + }, + { + "command": "postgres-explorer.createProcedure", + "when": "view == postgresExplorer && viewItem == category-procedures", + "group": "inline" + }, + { + "command": "postgres-explorer.createMaterializedView", + "when": "view == postgresExplorer && viewItem == category-materialized-views", + "group": "inline" + }, + { + "command": "postgres-explorer.createType", + "when": "view == postgresExplorer && viewItem == category-types", + "group": "inline" + }, + { + "command": "postgres-explorer.createForeignTable", + "when": "view == postgresExplorer && viewItem == category-foreign-tables", + "group": "inline" + }, + { + "command": "postgres-explorer.createForeignServer", + "when": "view == postgresExplorer && viewItem == category-foreign-data-wrappers", + "group": "inline" + }, + { + "command": "postgres-explorer.foreignDataWrapperOperations", + "when": "view == postgresExplorer && viewItem == foreign-data-wrapper", + "group": "1_operations@0" + }, + { + "command": "postgres-explorer.showForeignDataWrapperProperties", + "when": "view == postgresExplorer && viewItem == foreign-data-wrapper", + "group": "inline@1" + }, + { + "command": "postgres-explorer.foreignServerOperations", + "when": "view == postgresExplorer && viewItem == foreign-server", + "group": "1_operations@0" + }, + { + "command": "postgres-explorer.showForeignServerProperties", + "when": "view == postgresExplorer && viewItem == foreign-server", + "group": "inline@1" + }, + { + "command": "postgres-explorer.createUserMapping", + "when": "view == postgresExplorer && viewItem == foreign-server", + "group": "1_operations@1" + }, + { + "command": "postgres-explorer.dropForeignServer", + "when": "view == postgresExplorer && viewItem == foreign-server", + "group": "9_delete" + }, + { + "command": "postgres-explorer.userMappingOperations", + "when": "view == postgresExplorer && viewItem == user-mapping", + "group": "1_operations@0" + }, + { + "command": "postgres-explorer.showUserMappingProperties", + "when": "view == postgresExplorer && viewItem == user-mapping", + "group": "inline@1" + }, + { + "command": "postgres-explorer.dropUserMapping", + "when": "view == postgresExplorer && viewItem == user-mapping", + "group": "9_delete" + }, + { + "command": "postgres-explorer.addColumn", + "when": "view == postgresExplorer && viewItem == category-columns", + "group": "inline" + }, + { + "command": "postgres-explorer.addConstraint", + "when": "view == postgresExplorer && viewItem == category-constraints", + "group": "inline" + }, + { + "command": "postgres-explorer.addIndex", + "when": "view == postgresExplorer && viewItem == category-indexes", + "group": "inline" + }, + { + "command": "postgres-explorer.viewMaterializedViewData", + "when": "view == postgresExplorer && viewItem == materialized-view", + "group": "inline@2" + }, + { + "command": "postgres-explorer.showMaterializedViewProperties", + "when": "view == postgresExplorer && viewItem == materialized-view", + "group": "inline@3" + }, + { + "command": "postgres-explorer.viewMaterializedViewData", + "when": "view == postgresExplorer && viewItem == materialized-view", + "group": "2_query" + }, + { + "command": "postgres-explorer.dropMatView", + "when": "view == postgresExplorer && viewItem == materialized-view", + "group": "9_delete" + }, + { + "command": "postgres-explorer.showTypeProperties", + "when": "view == postgresExplorer && viewItem == type", + "group": "inline@1" + }, + { + "command": "postgres-explorer.dropType", + "when": "view == postgresExplorer && viewItem == type", + "group": "9_delete" + }, + { + "command": "postgres-explorer.showForeignTableProperties", + "when": "view == postgresExplorer && viewItem == foreign-table", + "group": "inline@1" + }, + { + "command": "postgres-explorer.editForeignTable", + "when": "view == postgresExplorer && viewItem == foreign-table", + "group": "inline@2" + }, + { + "command": "postgres-explorer.viewForeignTableData", + "when": "view == postgresExplorer && viewItem == foreign-table", + "group": "inline@3" + }, + { + "command": "postgres-explorer.dropForeignTable", + "when": "view == postgresExplorer && viewItem == foreign-table", + "group": "9_delete" + }, + { + "command": "postgres-explorer.extensionOperations", + "when": "view == postgresExplorer && viewItem == extension", + "group": "1_operations@0" + }, + { + "command": "postgres-explorer.dropExtension", + "when": "view == postgresExplorer && viewItem == extension-installed", + "group": "9_delete" + }, + { + "command": "postgres-explorer.editRole", + "when": "view == postgresExplorer && viewItem == role", + "group": "inline@1" + }, + { + "command": "postgres-explorer.grantRevoke", + "when": "view == postgresExplorer && viewItem == role", + "group": "1_actions" + }, + { + "command": "postgres-explorer.dropRole", + "when": "view == postgresExplorer && viewItem == role", + "group": "9_delete" + }, + { + "command": "postgres-explorer.roleOperations", + "when": "view == postgresExplorer && viewItem == role", + "group": "1_operations@0" + }, + { + "command": "postgres-explorer.showRoleProperties", + "when": "view == postgresExplorer && viewItem == role", + "group": "inline@0" + }, + { + "command": "postgres-explorer.createInDatabase", + "when": "view == postgresExplorer && viewItem == database", + "group": "inline@1", + "icon": "$(add)" + }, + { + "command": "postgres-explorer.showDashboard", + "when": "view == postgresExplorer && viewItem == database", + "group": "1_operations@1" + }, + { + "command": "postgres-explorer.openListenNotify", + "when": "view == postgresExplorer && viewItem == database", + "group": "1_operations@8" + }, + { + "command": "postgres-explorer.databaseOperations", + "when": "view == postgresExplorer && viewItem == database", + "group": "1_operations@3" + }, + { + "command": "postgres-explorer.backupDatabase", + "when": "view == postgresExplorer && viewItem == database", + "group": "1_operations@4" + }, + { + "command": "postgres-explorer.restoreDatabase", + "when": "view == postgresExplorer && viewItem == database", + "group": "1_operations@5" + }, + { + "command": "postgres-explorer.generateCreateScript", + "when": "view == postgresExplorer && viewItem == database", + "group": "1_operations@6" + }, + { + "command": "postgres-explorer.dropDatabase", + "when": "view == postgresExplorer && viewItem == database", + "group": "9_delete" + }, + { + "submenu": "postgres-explorer.databaseScriptsMenu", + "when": "view == postgresExplorer && viewItem == database", + "group": "1_operations@2" + }, + { + "command": "postgres-explorer.disconnectDatabase", + "when": "view == postgresExplorer && viewItem == database", + "group": "9_disconnect" + }, + { + "command": "postgres-explorer.maintenanceDatabase", + "when": "view == postgresExplorer && viewItem == database", + "group": "1_operations@7" + }, + { + "command": "postgres-explorer.queryTool", + "when": "view == postgresExplorer && viewItem == database", + "group": "inline@2" + }, + { + "command": "postgres-explorer.openErdMulti", + "when": "view == postgresExplorer && viewItem == database", + "group": "1_actions" + }, + { + "command": "postgres-explorer.psqlTool", + "when": "viewItem == database", + "group": "postgres-explorer-database@7" + }, + { + "command": "postgres-explorer.showConfiguration", + "when": "viewItem == database", + "group": "postgres-explorer-database@8" + }, + { + "command": "postgres-explorer.showColumnProperties", + "when": "view == postgresExplorer && viewItem == column", + "group": "inline@1" + }, + { + "command": "postgres-explorer.copyColumnName", + "when": "view == postgresExplorer && viewItem == column", + "group": "1_copy@1" + }, + { + "command": "postgres-explorer.copyColumnNameQuoted", + "when": "view == postgresExplorer && viewItem == column", + "group": "1_copy@2" + }, + { + "submenu": "postgres-explorer.columnScriptsMenu", + "when": "view == postgresExplorer && viewItem == column", + "group": "2_scripts" + }, + { + "command": "postgres-explorer.addColumnComment", + "when": "view == postgresExplorer && viewItem == column", + "group": "3_edit@1" + }, + { + "command": "postgres-explorer.generateIndexOnColumn", + "when": "view == postgresExplorer && viewItem == column", + "group": "3_edit@2" + }, + { + "command": "postgres-explorer.viewColumnStatistics", + "when": "view == postgresExplorer && viewItem == column", + "group": "4_info" + }, + { + "command": "postgres-explorer.showConstraintProperties", + "when": "view == postgresExplorer && viewItem == constraint", + "group": "inline@1" + }, + { + "command": "postgres-explorer.copyConstraintName", + "when": "view == postgresExplorer && viewItem == constraint", + "group": "1_copy@1" + }, + { + "submenu": "postgres-explorer.constraintScriptsMenu", + "when": "view == postgresExplorer && viewItem == constraint", + "group": "2_scripts" + }, + { + "command": "postgres-explorer.validateConstraint", + "when": "view == postgresExplorer && viewItem == constraint", + "group": "3_operations@1" + }, + { + "command": "postgres-explorer.constraintOperations", + "when": "view == postgresExplorer && viewItem == constraint", + "group": "1_actions@0" + }, + { + "command": "postgres-explorer.viewConstraintDependencies", + "when": "view == postgresExplorer && viewItem == constraint", + "group": "4_info" + }, + { + "command": "postgres-explorer.showIndexProperties", + "when": "view == postgresExplorer && viewItem == index", + "group": "inline@1" + }, + { + "command": "postgres-explorer.copyIndexName", + "when": "view == postgresExplorer && viewItem == index", + "group": "1_copy@1" + }, + { + "submenu": "postgres-explorer.indexScriptsMenu", + "when": "view == postgresExplorer && viewItem == index", + "group": "2_scripts" + }, + { + "command": "postgres-explorer.generateReindexScript", + "when": "view == postgresExplorer && viewItem == index", + "group": "3_operations@1" + }, + { + "command": "postgres-explorer.analyzeIndexUsage", + "when": "view == postgresExplorer && viewItem == index", + "group": "4_info@1" + }, + { + "command": "postgres-explorer.addIndexComment", + "when": "view == postgresExplorer && viewItem == index", + "group": "3_operations@2" + }, + { + "command": "postgres-explorer.indexOperations", + "when": "view == postgresExplorer && viewItem == index", + "group": "1_actions@0" + }, + { + "command": "postgres-explorer.copySavedQuery", + "when": "view == postgresExplorer.savedQueries && viewItem == savedQuery", + "group": "1_actions@1" + }, + { + "command": "postgres-explorer.editSavedQuery", + "when": "view == postgresExplorer.savedQueries && viewItem == savedQuery", + "group": "1_actions@2" + }, + { + "command": "postgres-explorer.openSavedQueryInNotebook", + "when": "view == postgresExplorer.savedQueries && viewItem == savedQuery", + "group": "1_actions@3" + }, + { + "command": "postgres-explorer.deleteSavedQuery", + "when": "view == postgresExplorer.savedQueries && viewItem == savedQuery", + "group": "2_delete@1" + }, + { + "command": "postgres-explorer.openTableDesigner", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition)$/", + "group": "1_actions" + }, + { + "command": "postgres-explorer.importData", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition)$/", + "group": "1_actions" + }, + { + "command": "postgres-explorer.openSchemaDiff", + "when": "view == postgresExplorer && viewItem =~ /^(table|partition)$/", + "group": "1_actions" + }, + { + "command": "postgres-explorer.createTableVisual", + "when": "view == postgresExplorer && viewItem == schema", + "group": "1_actions" + }, + { + "command": "postgres-explorer.openSchemaDiff", + "when": "view == postgresExplorer && viewItem == schema", + "group": "1_actions" + }, + { + "command": "postgres-explorer.openErd", + "when": "view == postgresExplorer && viewItem == schema", + "group": "1_actions" + }, + { + "command": "postgres-explorer.importData", + "when": "view == postgresExplorer && viewItem == schema", + "group": "1_actions" + }, + { + "command": "postgres-explorer.showTriggerProperties", + "when": "view == postgresExplorer && viewItem == trigger", + "group": "inline@1" + }, + { + "command": "postgres-explorer.enableTrigger", + "when": "view == postgresExplorer && viewItem == trigger", + "group": "1_actions@1" + }, + { + "command": "postgres-explorer.disableTrigger", + "when": "view == postgresExplorer && viewItem == trigger", + "group": "1_actions@2" + }, + { + "command": "postgres-explorer.triggerOperations", + "when": "view == postgresExplorer && viewItem == trigger", + "group": "1_actions@3" + }, + { + "command": "postgres-explorer.dropTrigger", + "when": "view == postgresExplorer && viewItem == trigger", + "group": "9_destructive" + }, + { + "command": "postgres-explorer.dropPolicy", + "when": "view == postgresExplorer && viewItem == policy", + "group": "9_destructive" + }, + { + "command": "postgres-explorer.createTrigger", + "when": "view == postgresExplorer && viewItem == category-triggers", + "group": "1_actions" + }, + { + "command": "postgres-explorer.listTriggers", + "when": "view == postgresExplorer && viewItem == category-triggers", + "group": "1_actions" + }, + { + "command": "postgres-explorer.showSequenceProperties", + "when": "view == postgresExplorer && viewItem == sequence", + "group": "inline@1" + }, + { + "command": "postgres-explorer.sequenceNextValue", + "when": "view == postgresExplorer && viewItem == sequence", + "group": "1_actions@1" + }, + { + "command": "postgres-explorer.sequenceOperations", + "when": "view == postgresExplorer && viewItem == sequence", + "group": "1_actions@2" + }, + { + "command": "postgres-explorer.dropSequence", + "when": "view == postgresExplorer && viewItem == sequence", + "group": "9_destructive" + }, + { + "command": "postgres-explorer.createSequence", + "when": "view == postgresExplorer && viewItem == category-sequences", + "group": "1_actions" + }, + { + "command": "postgres-explorer.listSequences", + "when": "view == postgresExplorer && viewItem == category-sequences", + "group": "1_actions" + }, + { + "command": "postgres-explorer.showPartitionProperties", + "when": "view == postgresExplorer && viewItem == partition", + "group": "inline@1" + }, + { + "command": "postgres-explorer.detachPartition", + "when": "view == postgresExplorer && viewItem == partition", + "group": "1_actions" + }, + { + "command": "postgres-explorer.createPartition", + "when": "view == postgresExplorer && viewItem == category-partitions", + "group": "1_actions" + }, + { + "command": "postgres-explorer.listPartitions", + "when": "view == postgresExplorer && viewItem == category-partitions", + "group": "1_actions" + }, + { + "command": "postgres-explorer.showDomainProperties", + "when": "view == postgresExplorer && viewItem == domain", + "group": "inline@1" + }, + { + "command": "postgres-explorer.dropDomain", + "when": "view == postgresExplorer && viewItem == domain", + "group": "9_destructive" + }, + { + "command": "postgres-explorer.createDomain", + "when": "view == postgresExplorer && viewItem == category-domains", + "group": "1_actions" + }, + { + "command": "postgres-explorer.listDomains", + "when": "view == postgresExplorer && viewItem == category-domains", + "group": "1_actions" + }, + { + "command": "postgres-explorer.showAggregateProperties", + "when": "view == postgresExplorer && viewItem == aggregate", + "group": "inline@1" + }, + { + "command": "postgres-explorer.dropAggregate", + "when": "view == postgresExplorer && viewItem == aggregate", + "group": "9_destructive" + }, + { + "command": "postgres-explorer.createAggregate", + "when": "view == postgresExplorer && viewItem == category-aggregates", + "group": "1_actions" + }, + { + "command": "postgres-explorer.listAggregates", + "when": "view == postgresExplorer && viewItem == category-aggregates", + "group": "1_actions" + }, + { + "command": "postgres-explorer.showEventTriggerProperties", + "when": "view == postgresExplorer && viewItem == event-trigger", + "group": "inline@1" + }, + { + "command": "postgres-explorer.enableEventTrigger", + "when": "view == postgresExplorer && viewItem == event-trigger", + "group": "1_actions@1" + }, + { + "command": "postgres-explorer.disableEventTrigger", + "when": "view == postgresExplorer && viewItem == event-trigger", + "group": "1_actions@2" + }, + { + "command": "postgres-explorer.eventTriggerOperations", + "when": "view == postgresExplorer && viewItem == event-trigger", + "group": "1_actions@3" + }, + { + "command": "postgres-explorer.dropEventTrigger", + "when": "view == postgresExplorer && viewItem == event-trigger", + "group": "9_destructive" + }, + { + "command": "postgres-explorer.createEventTrigger", + "when": "view == postgresExplorer && viewItem == category-event-triggers", + "group": "1_actions" + }, + { + "command": "postgres-explorer.listEventTriggers", + "when": "view == postgresExplorer && viewItem == category-event-triggers", + "group": "1_actions" + }, + { + "command": "postgres-explorer.listCronJobs", + "when": "view == postgresExplorer && viewItem == category-cron-jobs", + "group": "1_actions" + }, + { + "command": "postgres-explorer.installPgCron", + "when": "view == postgresExplorer && viewItem == category-cron-jobs", + "group": "1_actions@1" + }, + { + "command": "postgres-explorer.scheduleCronJob", + "when": "view == postgresExplorer && viewItem == category-cron-jobs", + "group": "1_actions@2" + }, + { + "command": "postgres-explorer.installPgCron", + "when": "view == postgresExplorer && viewItem == cron-setup", + "group": "inline@1" + }, + { + "command": "postgres-explorer.listCronJobs", + "when": "view == postgresExplorer && viewItem == cron-setup", + "group": "1_actions@1" + }, + { + "command": "postgres-explorer.showCronJobProperties", + "when": "view == postgresExplorer && viewItem == cron-job", + "group": "inline@1" + }, + { + "command": "postgres-explorer.unscheduleCronJob", + "when": "view == postgresExplorer && viewItem == cron-job", + "group": "9_destructive" + }, + { + "command": "postgres-explorer.showRuleProperties", + "when": "view == postgresExplorer && viewItem == rule", + "group": "inline@1" + }, + { + "command": "postgres-explorer.ruleOperations", + "when": "view == postgresExplorer && viewItem == rule", + "group": "1_actions" + }, + { + "command": "postgres-explorer.dropRule", + "when": "view == postgresExplorer && viewItem == rule", + "group": "9_destructive" + }, + { + "command": "postgres-explorer.listRules", + "when": "view == postgresExplorer && viewItem == category-rules", + "group": "1_actions" + }, + { + "command": "postgres-explorer.showTablespaceProperties", + "when": "view == postgresExplorer && viewItem == tablespace", + "group": "inline@1" + }, + { + "command": "postgres-explorer.tablespaceOperations", + "when": "view == postgresExplorer && viewItem == tablespace", + "group": "1_actions" + }, + { + "command": "postgres-explorer.listTablespaces", + "when": "view == postgresExplorer && viewItem == category-tablespaces", + "group": "1_actions" + }, + { + "command": "postgres-explorer.showPublicationProperties", + "when": "view == postgresExplorer && viewItem == publication", + "group": "inline@1" + }, + { + "command": "postgres-explorer.publicationOperations", + "when": "view == postgresExplorer && viewItem == publication", + "group": "1_actions" + }, + { + "command": "postgres-explorer.dropPublication", + "when": "view == postgresExplorer && viewItem == publication", + "group": "9_destructive" + }, + { + "command": "postgres-explorer.createPublication", + "when": "view == postgresExplorer && viewItem == category-publications", + "group": "1_actions" + }, + { + "command": "postgres-explorer.listPublications", + "when": "view == postgresExplorer && viewItem == category-publications", + "group": "1_actions" + }, + { + "command": "postgres-explorer.showSubscriptionProperties", + "when": "view == postgresExplorer && viewItem == subscription", + "group": "inline@1" + }, + { + "command": "postgres-explorer.dropSubscription", + "when": "view == postgresExplorer && viewItem == subscription", + "group": "9_destructive" + }, + { + "command": "postgres-explorer.listSubscriptions", + "when": "view == postgresExplorer && viewItem == category-subscriptions", + "group": "1_actions" + } + ], + "postgres-explorer.columnScriptsMenu": [ + { + "command": "postgres-explorer.generateSelectStatement", + "group": "1_query" + }, + { + "command": "postgres-explorer.generateWhereClause", + "group": "1_query" + }, + { + "command": "postgres-explorer.generateAlterColumnScript", + "group": "2_modify" + }, + { + "command": "postgres-explorer.generateRenameColumnScript", + "group": "2_modify" + }, + { + "command": "postgres-explorer.generateDropColumnScript", + "group": "3_delete" + } + ], + "postgres-explorer.constraintScriptsMenu": [ + { + "command": "postgres-explorer.generateAlterConstraintScript", + "group": "2_modify" + }, + { + "command": "postgres-explorer.generateDropConstraintScript", + "group": "3_delete" + } + ], + "postgres-explorer.indexScriptsMenu": [ + { + "command": "postgres-explorer.generateScriptCreate", + "group": "1_create" + }, + { + "command": "postgres-explorer.generateAlterIndexScript", + "group": "2_modify" + }, + { + "command": "postgres-explorer.generateDropIndexScript", + "group": "3_delete" + } + ], + "postgres-explorer.scriptsMenu": [ + { + "command": "postgres-explorer.scriptSelect", + "group": "1_read" + }, + { + "command": "postgres-explorer.scriptInsert", + "group": "2_write" + }, + { + "command": "postgres-explorer.scriptUpdate", + "group": "2_write" + }, + { + "command": "postgres-explorer.scriptDelete", + "group": "2_write" + }, + { + "command": "postgres-explorer.scriptCreate", + "group": "3_ddl" + } + ], + "postgres-explorer.maintenanceMenu": [ + { + "command": "postgres-explorer.maintenanceVacuum", + "group": "1_maintenance" + }, + { + "command": "postgres-explorer.maintenanceAnalyze", + "group": "1_maintenance" + }, + { + "command": "postgres-explorer.maintenanceReindex", + "group": "1_maintenance" + } + ], + "postgres-explorer.viewScriptsMenu": [ + { + "command": "postgres-explorer.viewScriptSelect", + "group": "1_read" + }, + { + "command": "postgres-explorer.viewScriptCreate", + "group": "2_ddl" + } + ], + "postgres-explorer.databaseScriptsMenu": [ + { + "command": "postgres-explorer.generateCreateScript", + "group": "1_create" + }, + { + "command": "postgres-explorer.scriptAlterDatabase", + "group": "2_write" + }, + { + "command": "postgres-explorer.dropDatabase", + "group": "3_delete" + } + ], + "notebook/cell/title": [ + { + "command": "postgres-explorer.aiAssist", + "when": "notebookType == postgres-notebook", + "group": "inline" + } + ] + }, + "keybindings": [ + { + "command": "editor.action.selectAll", + "key": "ctrl+a", + "mac": "cmd+a", + "when": "editorTextFocus && notebookEditorFocused && (notebookType == postgres-notebook || notebookType == postgres-query)" + }, + { + "command": "postgres-explorer.formatSql", + "key": "shift+alt+f", + "mac": "shift+alt+f", + "when": "editorLangId == sql || editorLangId == pgsql" + }, + { + "command": "postgres-explorer.newNotebook", + "key": "ctrl+alt+n", + "mac": "cmd+alt+n" + }, + { + "command": "postgres-explorer.addConnection", + "key": "ctrl+alt+a", + "mac": "cmd+alt+a" + }, + { + "command": "postgres-explorer.showDashboard", + "key": "ctrl+alt+d", + "mac": "cmd+alt+d" + }, + { + "command": "postgres-explorer.queryTool", + "key": "ctrl+alt+e", + "mac": "cmd+alt+e" + }, + { + "command": "postgres-explorer.searchSchema", + "key": "ctrl+shift+o", + "mac": "cmd+shift+o" + } + ] + }, + "activationEvents": [ + "onView:postgresExplorer", + "onView:postgresExplorer.notebooks", + "onView:postgresExplorer.savedQueries", + "onView:postgresExplorer.chatView", + "onNotebook:postgres-notebook", + "onNotebook:postgres-query", + "onCommand:postgres-explorer.connect", + "onCommand:postgres-explorer.newNotebook", + "onCommand:postgres-explorer.addConnection", + "onCommand:postgres-explorer.importConnectionFromDatabaseUrl", + "onCommand:postgres-explorer.switchWorkspaceDefaultConnection", + "onCommand:postgres-explorer.exportNotebook", + "onCommand:postgres-explorer.openListenNotify", + "onCommand:postgres-explorer.openListenNotifyFromPalette", + "onCommand:postgres-explorer.showDashboardFromPalette", + "onCommand:postgres-explorer.openSqlAssistantTab" + ], + "main": "./dist/extension.js", + "scripts": { + "dev:site": "node scripts/dev-server.js", + "vscode:prepublish": "npm run esbuild-base -- --minify && npm run esbuild-renderer -- --minify && npm run esbuild-erd-webview -- --minify", + "prepare:nightly:manifests": "node ./scripts/prepare-nightly-manifests.js", + "package:prerelease": "npx @vscode/vsce package --pre-release", + "package:openvsx:nightly": "npx @vscode/vsce package", + "esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=dist/extension.js --external:vscode --external:ssh2 --external:pg --external:@cursor/sdk --format=cjs --platform=node --main-fields=main", + "esbuild-renderer": "esbuild ./src/ui/renderer/renderer_v2.ts --bundle --outdir=dist --format=esm --platform=browser --splitting", + "esbuild-erd-webview": "esbuild ./src/schemaDesigner/erd/webview/main.ts --bundle --outfile=dist/erd-webview.js --format=iife --platform=browser", + "copy-templates": "cp -r templates dist/", + "compile": "tsc -p ./ && npm run esbuild-renderer && npm run esbuild-erd-webview && npm run copy-templates", + "watch": "tsc -watch -p ./", + "esbuild": "npm run esbuild-base -- --sourcemap && npm run esbuild-renderer -- --sourcemap && npm run esbuild-erd-webview -- --sourcemap", + "esbuild-watch": "npm run esbuild-base -- --sourcemap --watch & npm run esbuild-renderer -- --sourcemap --watch & npm run esbuild-erd-webview -- --sourcemap --watch", + "test": "npm run compile && mocha --loader ./node_modules/ts-node/esm/transpile-only.mjs -r tsconfig-paths/register -r src/test/setup.ts 'src/test/unit/**/*.test.ts'", + "test:unit": "npm run compile && TS_NODE_PROJECT=src/test/tsconfig.json node -r ts-node/register/transpile-only -r tsconfig-paths/register -r ./src/test/setup.ts ./node_modules/mocha/bin/mocha --exit 'src/test/unit/**/*.test.ts'", + "test:integration": "npm run compile && ts-mocha -p src/test/tsconfig.json -r tsconfig-paths/register -r src/test/setup.ts 'src/test/integration/**/*.test.ts'", + "test:renderer": "npm run compile && ts-mocha -p src/test/tsconfig.json -r tsconfig-paths/register -r src/test/setup.ts 'src/test/unit/RendererComponents.test.ts'", + "test:renderer:coverage": "npm run compile && nyc ts-mocha -p src/test/tsconfig.json -r tsconfig-paths/register -r src/test/setup.ts 'src/test/unit/RendererComponents.test.ts'", + "test:versions": "npm run compile && ts-mocha -p src/test/tsconfig.json -r tsconfig-paths/register -r src/test/setup.ts 'src/test/integration/ConnectionLifecycle.test.ts'", + "test:all": "npm run compile && ts-mocha -p src/test/tsconfig.json -r ts-node/register -r tsconfig-paths/register -r src/test/setup.ts 'src/test/**/*.test.ts'", + "lint": "eslint \"src/**/*.ts\" \"scripts/**/*.js\" \"*.js\"", + "lint:fix": "npm run lint -- --fix", + "format": "prettier --check \"src/**/*.{ts,js,json,md,css,html,yml,yaml}\" \"scripts/**/*.js\" \"*.js\" \"*.{md,json,yml,yaml,css,html}\"", + "format:fix": "prettier --write \"src/**/*.{ts,js,json,md,css,html,yml,yaml}\" \"scripts/**/*.js\" \"*.js\" \"*.{md,json,yml,yaml,css,html}\"", + "build-tests": "tsc -p src/test/tsconfig.json --outDir out_test --ignoreDeprecations 6.0", + "clean:src-js": "node ./scripts/clean-generated-src-js.js", + "postesbuild-base": "npm run clean:src-js", + "postesbuild": "npm run clean:src-js", + "postcompile": "npm run clean:src-js", + "postbuild-tests": "npm run clean:src-js", + "coverage": "npm run compile && nyc ts-mocha -p src/test/tsconfig.json -r tsconfig-paths/register -r src/test/setup.ts --exit 'src/test/unit/**/*.test.ts'", + "coverage:phase-utils": "npm run compile && npx c8 --config .c8rc.phase-utils.json ts-mocha -p src/test/tsconfig.json -r tsconfig-paths/register -r src/test/setup.ts 'src/test/unit/pgPassUtils.test.ts' 'src/test/unit/connectionUtils.test.ts' 'src/test/unit/NotebookTitle.test.ts'", + "coverage:phase-handlers": "npm run compile && npx c8 --config .c8rc.phase-handlers.json ts-mocha -p src/test/tsconfig.json -r tsconfig-paths/register -r src/test/setup.ts 'src/test/unit/CoreHandlers.test.ts' 'src/test/unit/ExplainHandlers.test.ts' 'src/test/unit/TransactionHandlers.test.ts' 'src/test/unit/QueryHandlers.test.ts' 'src/test/unit/QueryHandlers.extended.test.ts'", + "precoverage:phased": "npm run clean:coverage", + "clean:coverage": "node ./scripts/clean-coverage.js", + "precoverage:report": "npm run merge:coverage", + "merge:coverage": "node ./scripts/merge-coverage.js", + "coverage:phased": "npm run coverage:phase-utils && npm run coverage:phase-handlers && node ./scripts/merge-coverage.js && npm run coverage:report", + "coverage:report": "npx c8 report --temp-directory ./.nyc_output --config .c8rc.phase-report.json --reporter=html --reporter=text" + }, + "dependencies": { + "@cursor/sdk": "^1.0.12", + "@dbml/core": "^7.1.2", + "@types/pg-cursor": "^2.7.2", + "chart.js": "^4.5.1", + "d3-force": "^3.0.0", + "d3-selection": "^3.0.0", + "esbuild": ">=0.28.0", + "express": "^5.2.1", + "pg": "^8.20.0", + "pg-cursor": "^2.19.0", + "razorpay": "^2.9.6", + "sql-formatter": "^15.7.3", + "ssh2": "^1.17.0" + }, + "devDependencies": { + "@types/chai": "^5.2.3", + "@types/d3-force": "^3.0.10", + "@types/d3-selection": "^3.0.11", + "@types/jsdom": "^28.0.1", + "@types/mocha": "^10.0.10", + "@types/module-alias": "^2.0.4", + "@types/node": "^25.6.0", + "@types/pg": "^8.20.0", + "@types/sinon": "^21.0.1", + "@types/ssh2": "^1.15.5", + "@types/vscode": "^1.107.0", + "@types/vscode-notebook-renderer": "^1.72.4", + "@typescript-eslint/eslint-plugin": "^8.59.2", + "@typescript-eslint/parser": "^8.59.2", + "baseline-browser-mapping": "^2.10.27", + "chai": "^6.2.2", + "esbuild": ">=0.28.0", + "eslint": "^10.3.0", + "eslint-config-prettier": "^10.1.8", + "fast-check": "^4.7.0", + "jsdom": "^29.1.1", + "mocha": "^11.7.5", + "module-alias": "^2.3.4", + "nyc": "^18.0.0", + "ovsx": "^0.10.11", + "prettier": "^3.8.3", + "sinon": "^22.0.0", + "ts-mocha": "^11.1.0", + "ts-node": "^10.9.2", + "tsconfig-paths": "^4.2.0", + "typescript": "^6.0.3" + }, + "overrides": { + "diff": "^8.0.4", + "serialize-javascript": "^7.0.5", + "tar": ">=7.5.11", + "undici": ">=8.0.0", + "@tootallnate/once": ">=3.0.1" + }, + "resolutions": { + "webpack": "^5.76.0", + "webpack-cli": "^5.0.0" + } +} \ No newline at end of file diff --git a/scripts/dev-server.js b/scripts/dev-server.js new file mode 100644 index 0000000..4cfa166 --- /dev/null +++ b/scripts/dev-server.js @@ -0,0 +1,73 @@ +const express = require('express'); +const path = require('path'); +const fs = require('fs'); + +// Simple .env parser to load environment variables locally +const envPath = path.join(__dirname, '../.env'); +if (fs.existsSync(envPath)) { + try { + const envContent = fs.readFileSync(envPath, 'utf8'); + envContent.split(/\r?\n/).forEach(line => { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) return; + const index = trimmed.indexOf('='); + if (index > 0) { + const key = trimmed.substring(0, index).trim(); + const value = trimmed.substring(index + 1).trim(); + // Remove surrounding quotes if any + const cleanValue = value.replace(/^['"]|['"]$/g, ''); + process.env[key] = cleanValue; + } + }); + console.log('Successfully loaded credentials from .env'); + } catch (error) { + console.error('Error loading .env file:', error); + } +} else { + console.warn('.env file not found at project root. Using fallback/existing environment variables.'); +} + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Body parsing middlewares +app.use(express.json()); +app.use(express.urlencoded({ extended: true })); + +// Serve docs/ statically +app.use(express.static(path.join(__dirname, '../docs'))); + +// Import Serverless function modules +const configHandler = require('../api/config'); +const createOrderHandler = require('../api/create-order'); +const verifyPaymentHandler = require('../api/verify-payment'); + +// Standard Express wrapper for Serverless function signature (req, res) +const wrapServerless = (handler) => { + return async (req, res, next) => { + try { + await handler(req, res); + } catch (err) { + next(err); + } + }; +}; + +// API Endpoints +app.get('/api/config', wrapServerless(configHandler)); +app.post('/api/create-order', wrapServerless(createOrderHandler)); +app.post('/api/verify-payment', wrapServerless(verifyPaymentHandler)); + +// Global Error Handler +app.use((err, req, res, next) => { + console.error('Dev Server API Error:', err); + res.status(500).json({ error: err.message || 'Internal Server Error' }); +}); + +app.listen(PORT, () => { + console.log(`\n========================================================`); + console.log(`🚀 PgStudio Marketing & Razorpay Checkout Server`); + console.log(`🌐 Address: http://localhost:${PORT}`); + console.log(`🔑 RAZORPAY_KEY_ID: ${process.env.RAZORPAY_KEY_ID || 'Missing!'}`); + console.log(`========================================================\n`); +}); diff --git a/src/activation/commandSpecs.ts b/src/activation/commandSpecs.ts index d31892d..47f33f9 100644 --- a/src/activation/commandSpecs.ts +++ b/src/activation/commandSpecs.ts @@ -61,6 +61,7 @@ import { setTelemetryMode, showTelemetryModePicker } from '../commands/telemetry import { cmdOpenTableDesigner, cmdCreateTableVisual, + cmdOpenRoleDesigner, cmdOpenSchemaDiff, cmdOpenSchemaDiffFromPalette, cmdOpenErd, @@ -1384,6 +1385,10 @@ export function getCommandSpecs( command: 'postgres-explorer.openTableDesigner', callback: (item: DatabaseTreeItem) => cmdOpenTableDesigner(item, context) }, + { + command: 'postgres-explorer.openRoleDesigner', + callback: (item: DatabaseTreeItem) => cmdOpenRoleDesigner(item, context) + }, { command: 'postgres-explorer.createTableVisual', callback: (item: DatabaseTreeItem) => cmdCreateTableVisual(item, context) diff --git a/src/commands/schemaDesigner.ts b/src/commands/schemaDesigner.ts index c18656a..ee6bc19 100644 --- a/src/commands/schemaDesigner.ts +++ b/src/commands/schemaDesigner.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import { DatabaseTreeItem } from '../providers/DatabaseTreeProvider'; import { TableDesignerPanel } from '../schemaDesigner/TableDesignerPanel'; +import { RoleDesignerPanel } from '../schemaDesigner/RoleDesignerPanel'; import { SchemaDiffPanel } from '../schemaDesigner/SchemaDiffPanel'; import { ErdPanel } from '../schemaDesigner/ErdPanel'; import { ImportDataPanel } from '../schemaDesigner/ImportDataPanel'; @@ -37,6 +38,16 @@ export async function cmdCreateTableVisual( await TableDesignerPanel.openForCreate(item, context); } +/** + * Open the visual role designer for an existing role. + */ +export async function cmdOpenRoleDesigner( + item: DatabaseTreeItem, + context: vscode.ExtensionContext +): Promise { + await RoleDesignerPanel.openForRole(item, context); +} + /** * Open the Schema Diff panel to compare two schemas */ diff --git a/src/common/types.ts b/src/common/types.ts index 1a18b62..5fbef15 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -140,6 +140,9 @@ export interface QueryResults { windowSize: number; hasMoreBefore: boolean; hasMoreAfter: boolean; + totalRows?: number | null; + countAttempted?: boolean; + countError?: string; }; /** When true with slidingWindow, show the streaming hint banner (policy-gated). */ showSlidingCursorBanner?: boolean; diff --git a/src/features/connections/connectionForm.ts b/src/features/connections/connectionForm.ts index b5cb4d9..92a1a0d 100644 --- a/src/features/connections/connectionForm.ts +++ b/src/features/connections/connectionForm.ts @@ -344,7 +344,7 @@ export class ConnectionFormPanel { err.code === "ECONNRESET" || err.code === "EPROTO"; - if ((sslMode === "prefer" || sslMode === "allow") && isSSLFailure) { + if (connection.environment !== "production" && (sslMode === "prefer" || sslMode === "allow") && isSSLFailure) { // Retry without SSL - keep using targetDb so .pgpass still matches config = buildClientConfig( connection, @@ -378,6 +378,14 @@ export class ConnectionFormPanel { } catch (sslErr: any) { err = sslErr; } + } else if (connection.environment === "production" && isSSLFailure) { + const enrichedError = new Error( + `Production connection failed: ${err.message || "SSL connection failed"}.\n\n` + + `Security Alert: PgStudio blocked automatic SSL downgrade on a Production environment to protect your credentials. ` + + `If your database does not support SSL or you are using a secure SSH tunnel, please expand Advanced Options and explicitly set SSL Mode to "Disable — No SSL".` + ); + (enrichedError as any).code = err.code; + err = enrichedError; } // Database fallback: if the configured database doesn't exist yet, diff --git a/src/providers/NotebookKernel.ts b/src/providers/NotebookKernel.ts index 85850e7..fc984e6 100644 --- a/src/providers/NotebookKernel.ts +++ b/src/providers/NotebookKernel.ts @@ -1,5 +1,6 @@ import * as vscode from 'vscode'; import { CompletionProvider } from './kernel/CompletionProvider'; +import { ParamCommentCodeActionProvider } from './kernel/ParamCommentCodeActionProvider'; import { SqlExecutor } from './kernel/SqlExecutor'; import { getTransactionManager } from '../services/TransactionManager'; import { MessageHandlerRegistry } from '../services/MessageHandler'; @@ -33,6 +34,8 @@ export class PostgresKernel implements vscode.Disposable { readonly label = 'PostgreSQL'; readonly supportedLanguages = ['sql']; + private static _languageProvidersRegistered = false; + private readonly _controller: vscode.NotebookController; private readonly _executor: SqlExecutor; @@ -54,15 +57,26 @@ export class PostgresKernel implements vscode.Disposable { this._executor = new SqlExecutor(this._controller); - // Register completion provider - const completionProvider = new CompletionProvider(); - context.subscriptions.push( - vscode.languages.registerCompletionItemProvider( - { scheme: 'vscode-notebook-cell', language: 'sql' }, - completionProvider, - ' ', '.', '"' // Trigger characters - ) - ); + // Register language providers once across all kernel instances + if (!PostgresKernel._languageProvidersRegistered) { + PostgresKernel._languageProvidersRegistered = true; + + context.subscriptions.push( + vscode.languages.registerCompletionItemProvider( + { scheme: 'vscode-notebook-cell', language: 'sql' }, + new CompletionProvider(), + ' ', '.', '"', '-' // - enables -- param comment suggestions + ) + ); + + context.subscriptions.push( + vscode.languages.registerCodeActionsProvider( + { scheme: 'vscode-notebook-cell', language: 'sql' }, + new ParamCommentCodeActionProvider(), + { providedCodeActionKinds: ParamCommentCodeActionProvider.providedKinds } + ) + ); + } // Handle messages from renderer const registry = MessageHandlerRegistry.getInstance(); diff --git a/src/providers/SqlCompletionProvider.ts b/src/providers/SqlCompletionProvider.ts index 7c6dc19..4a5b274 100644 --- a/src/providers/SqlCompletionProvider.ts +++ b/src/providers/SqlCompletionProvider.ts @@ -361,16 +361,21 @@ export class SqlCompletionProvider implements vscode.CompletionItemProvider { } private static readonly CATALOG_COLUMNS_SQL = ` - SELECT - table_schema as schema, - table_name, - column_name, - data_type, - udt_schema, - udt_name - FROM information_schema.columns - WHERE table_schema NOT IN ('pg_catalog', 'information_schema') - ORDER BY table_schema, table_name, ordinal_position + SELECT + n.nspname as schema, + c.relname as table_name, + a.attname as column_name, + format_type(a.atttypid, a.atttypmod) as data_type, + tn.nspname as udt_schema, + t.typname as udt_name + FROM pg_class c + JOIN pg_namespace n ON n.oid = c.relnamespace + JOIN pg_attribute a ON a.attrelid = c.oid AND a.attnum > 0 AND NOT a.attisdropped + JOIN pg_type t ON t.oid = a.atttypid + JOIN pg_namespace tn ON tn.oid = t.typnamespace + WHERE c.relkind IN ('r', 'p', 'v', 'm', 'f') + AND n.nspname NOT IN ('pg_catalog', 'information_schema') + ORDER BY n.nspname, c.relname, a.attnum `; private static readonly CATALOG_COMPOSITE_SQL = ` diff --git a/src/providers/kernel/CompletionProvider.ts b/src/providers/kernel/CompletionProvider.ts index bd65733..8c9f169 100644 --- a/src/providers/kernel/CompletionProvider.ts +++ b/src/providers/kernel/CompletionProvider.ts @@ -1,14 +1,22 @@ import * as vscode from 'vscode'; +import { SqlParser } from './SqlParser'; export class CompletionProvider implements vscode.CompletionItemProvider { public async provideCompletionItems( document: vscode.TextDocument, position: vscode.Position, - token: vscode.CancellationToken, - context: vscode.CompletionContext + _token: vscode.CancellationToken, + _context: vscode.CompletionContext ): Promise { + const linePrefix = document.lineAt(position.line).text.substring(0, position.character); + + // On a comment line, suggest missing parameter comment definitions instead of keywords + if (linePrefix.trimStart().startsWith('--')) { + return this.getParamCommentCompletions(document, position); + } + const items: vscode.CompletionItem[] = []; // Add basic SQL keywords @@ -25,4 +33,38 @@ export class CompletionProvider implements vscode.CompletionItemProvider { return items; } + + private getParamCommentCompletions( + document: vscode.TextDocument, + position: vscode.Position + ): vscode.CompletionItem[] { + const docText = document.getText(); + const params = SqlParser.detectParameters(docText); + const commentParams = SqlParser.parseCommentParameters(docText); + + const items: vscode.CompletionItem[] = []; + const replaceRange = new vscode.Range(new vscode.Position(position.line, 0), position); + + for (const n of params.positional) { + if (commentParams.positional.has(n)) { continue; } + const item = new vscode.CompletionItem(`-- $${n}=`, vscode.CompletionItemKind.Value); + item.insertText = `-- $${n}=`; + item.range = replaceRange; + item.detail = `Define value for parameter $${n}`; + item.sortText = `0_${String(n).padStart(4, '0')}`; + items.push(item); + } + + for (const name of params.named) { + if (commentParams.named.has(name)) { continue; } + const item = new vscode.CompletionItem(`-- :${name}=`, vscode.CompletionItemKind.Value); + item.insertText = `-- :${name}=`; + item.range = replaceRange; + item.detail = `Define value for parameter :${name}`; + item.sortText = `1_${name}`; + items.push(item); + } + + return items; + } } diff --git a/src/providers/kernel/ParamCommentCodeActionProvider.ts b/src/providers/kernel/ParamCommentCodeActionProvider.ts new file mode 100644 index 0000000..77de2d6 --- /dev/null +++ b/src/providers/kernel/ParamCommentCodeActionProvider.ts @@ -0,0 +1,44 @@ + +import * as vscode from 'vscode'; +import { SqlParser } from './SqlParser'; + +export class ParamCommentCodeActionProvider implements vscode.CodeActionProvider { + static readonly providedKinds = [vscode.CodeActionKind.QuickFix]; + + provideCodeActions( + document: vscode.TextDocument, + _range: vscode.Range | vscode.Selection, + _context: vscode.CodeActionContext, + _token: vscode.CancellationToken + ): vscode.CodeAction[] { + const docText = document.getText(); + const params = SqlParser.detectParameters(docText); + const commentParams = SqlParser.parseCommentParameters(docText); + + const missingPositional = params.positional.filter(n => !commentParams.positional.has(n)); + const missingNamed = params.named.filter(name => !commentParams.named.has(name)); + + if (missingPositional.length === 0 && missingNamed.length === 0) { + return []; + } + + const lines: string[] = []; + for (const n of missingPositional) { + lines.push(`-- $${n}=`); + } + for (const name of missingNamed) { + lines.push(`-- :${name}=`); + } + const insertText = lines.join('\n') + '\n'; + + const action = new vscode.CodeAction( + `Add ${lines.length} missing parameter comment${lines.length > 1 ? 's' : ''}`, + vscode.CodeActionKind.QuickFix + ); + action.edit = new vscode.WorkspaceEdit(); + action.edit.insert(document.uri, new vscode.Position(0, 0), insertText); + action.isPreferred = true; + + return [action]; + } +} diff --git a/src/providers/kernel/SqlExecutor.ts b/src/providers/kernel/SqlExecutor.ts index 5df723d..cb7339c 100644 --- a/src/providers/kernel/SqlExecutor.ts +++ b/src/providers/kernel/SqlExecutor.ts @@ -19,6 +19,11 @@ import { getTransactionManager } from '../../services/TransactionManager'; import { QueryAnalyzer } from '../../services/QueryAnalyzer'; import { QueryPerformanceService } from '../../services/QueryPerformanceService'; import { extensionContext } from '../../extension'; +import { + clearNotebookParameterValues, + getNotebookParameterValues, + rememberNotebookParameterValue, +} from '../../services/NotebookParameterBank'; import { QueryCodeLensProvider } from '../QueryCodeLensProvider'; import { updateNotebookTitle } from '../../utils/notebookTitle'; import { ResultCursorService } from '../../services/ResultCursorService'; @@ -45,14 +50,14 @@ interface StatementResult { /** Failure strategy for multi-statement execution */ type FailureStrategy = 'continue-on-error' | 'fail-on-error' | 'prompt-on-error'; +interface NotebookParameterQuickPickItem extends vscode.QuickPickItem { + value: string; +} + export class SqlExecutor { private static readonly REVIEW_COUNT_KEY = 'postgresExplorer.reviewPrompt.successCount'; private static readonly REVIEW_SHOWN_KEY = 'postgresExplorer.reviewPrompt.shown'; private static readonly REVIEW_THRESHOLD = 3; - /** Workspace memento: last-used values for `:name` SQL parameters (keyed by parameter name). */ - private static readonly NAMED_PARAM_DEFAULTS_KEY = 'pgstudio.namedParamDefaults.v1'; - /** Workspace memento: last-used values for `$N` SQL parameters (keyed by sqlHash -> parameter index). */ - private static readonly POSITIONAL_PARAM_DEFAULTS_KEY = 'pgstudio.positionalParamDefaults.v1'; constructor(private readonly _controller: vscode.NotebookController) { } @@ -134,41 +139,107 @@ export class SqlExecutor { } /** - * Prompts for each `:name` value in order; persists last-used values per workspace. + * Prompts for each `:name` value in order; persists last-used values per notebook. * Returns `undefined` if the user cancels any prompt. */ - private async promptForNamedParameterValues(paramNames: string[]): Promise { + private async promptForNotebookParameterValue(options: { + notebookUri: string; + parameterKey: string; + title: string; + prompt: string; + placeHolder?: string; + initialValue?: string; + allowNullChoice?: boolean; + }): Promise { const paramsConfig = vscode.workspace.getConfiguration('postgresExplorer.parameters'); const cacheLastValues = paramsConfig.get('cacheLastValues', true); const nullSentinel = paramsConfig.get('nullSentinel', 'NULL'); - const cache = - cacheLastValues - ? extensionContext?.workspaceState.get>(SqlExecutor.NAMED_PARAM_DEFAULTS_KEY, {}) ?? {} - : {}; - const next: Record = { ...cache }; - const values: unknown[] = []; + const workspaceState = extensionContext?.workspaceState; + const rememberedValues = cacheLastValues && workspaceState + ? getNotebookParameterValues(workspaceState, options.notebookUri, options.parameterKey) + : []; + + const chooseExistingValue = async (): Promise => { + const quickPickItems: NotebookParameterQuickPickItem[] = rememberedValues.map((value) => ({ + label: this.formatNotebookParameterValueLabel(value), + description: 'Previous value in this notebook', + value, + })); + + quickPickItems.push({ + label: '$(edit) Enter new value...', + description: options.initialValue ? `Current default: ${this.formatNotebookParameterValueLabel(options.initialValue)}` : undefined, + value: '__new__', + }); - for (const name of paramNames) { - const existing = next[name] ?? ''; - const input = await vscode.window.showInputBox({ - title: `SQL parameter :${name}`, - prompt: `Value for :${name} (${nullSentinel ? `type ${nullSentinel} to send SQL NULL` : 'sent to PostgreSQL as text; casts in SQL still apply'})`, - value: existing, - ignoreFocusOut: true + if (options.allowNullChoice && nullSentinel) { + quickPickItems.push({ + label: `$(dash) Use ${nullSentinel}`, + description: 'Send SQL NULL for this parameter', + value: '__null__', + }); + } + + quickPickItems.push({ + label: '$(trash) Clear saved values for this notebook parameter', + description: 'Remove the notebook-local value bank for this parameter', + value: '__clear__', }); - if (input === undefined) { + + const selection = await vscode.window.showQuickPick(quickPickItems, { + title: options.title, + placeHolder: options.prompt, + ignoreFocusOut: true, + }); + + if (!selection) { + return undefined; + } + + if (selection.value === '__clear__') { + await clearNotebookParameterValues(workspaceState, options.notebookUri, options.parameterKey); return undefined; } - values.push(nullSentinel && input === nullSentinel ? null : input); - if (cacheLastValues) { - next[name] = input; + + if (selection.value === '__null__') { + return null; + } + + if (selection.value !== '__new__') { + if (cacheLastValues && workspaceState) { + await rememberNotebookParameterValue(workspaceState, options.notebookUri, options.parameterKey, selection.value); + } + return selection.value; + } + + return undefined; + }; + + if (rememberedValues.length > 0) { + const selection = await chooseExistingValue(); + if (selection !== undefined) { + return selection; } } - if (cacheLastValues && extensionContext) { - await extensionContext.workspaceState.update(SqlExecutor.NAMED_PARAM_DEFAULTS_KEY, next); + const input = await vscode.window.showInputBox({ + title: options.title, + prompt: options.prompt, + placeHolder: options.placeHolder, + value: options.initialValue ?? rememberedValues[0] ?? '', + ignoreFocusOut: true, + }); + + if (input === undefined) { + return undefined; } - return values; + + const isNull = Boolean(nullSentinel && input.toLowerCase() === nullSentinel.toLowerCase()); + if (cacheLastValues && workspaceState && !isNull) { + await rememberNotebookParameterValue(workspaceState, options.notebookUri, options.parameterKey, input); + } + + return isNull ? null : input; } private getSqlParameterContextSnippet(sql: string, parameterIndex: number): string | undefined { @@ -186,61 +257,54 @@ export class SqlExecutor { return snippet || undefined; } + private formatNotebookParameterValueLabel(value: string): string { + const normalized = value.replace(/\s+/g, ' ').trim(); + if (!normalized) { + return '(empty string)'; + } + return normalized.length > 60 ? `${normalized.slice(0, 57)}...` : normalized; + } + + private makeNotebookParameterKey(kind: 'named' | 'positional' | 'quoted', value: string): string { + return `${kind}:${value}`; + } + private async promptForPositionalParameterValues( + notebookUri: string, indices: number[], - sqlHash: string, - sql: string + sql: string, + commentValues?: Map ): Promise { - const paramsConfig = vscode.workspace.getConfiguration('postgresExplorer.parameters'); - const cacheLastValues = paramsConfig.get('cacheLastValues', true); - const nullSentinel = paramsConfig.get('nullSentinel', 'NULL'); - const cache = - cacheLastValues - ? extensionContext?.workspaceState.get>>( - SqlExecutor.POSITIONAL_PARAM_DEFAULTS_KEY, - {} - ) ?? {} - : {}; - - const statementDefaults = { ...(cache[sqlHash] ?? {}) }; const values: unknown[] = []; for (const parameterIndex of indices) { - const key = String(parameterIndex); + if (commentValues?.has(parameterIndex)) { + values.push(commentValues.get(parameterIndex) ?? null); + continue; + } const contextSnippet = this.getSqlParameterContextSnippet(sql, parameterIndex); - const input = await vscode.window.showInputBox({ + const input = await this.promptForNotebookParameterValue({ + notebookUri, + parameterKey: this.makeNotebookParameterKey('positional', String(parameterIndex)), title: `SQL parameter $${parameterIndex}`, - prompt: `Value for $${parameterIndex}${nullSentinel ? ` (type ${nullSentinel} to send SQL NULL)` : ''}`, + prompt: `Enter the raw value for $${parameterIndex}${contextSnippet ? ` - ${contextSnippet}` : ''} (do not add quotes; PgStudio sends the parameter value directly)`, placeHolder: contextSnippet, - value: statementDefaults[key] ?? '', - ignoreFocusOut: true + allowNullChoice: true, }); if (input === undefined) { return undefined; } - values.push(nullSentinel && input === nullSentinel ? null : input); - if (cacheLastValues) { - statementDefaults[key] = input; - } - } - - if (cacheLastValues && extensionContext) { - await extensionContext.workspaceState.update(SqlExecutor.POSITIONAL_PARAM_DEFAULTS_KEY, { - ...cache, - [sqlHash]: statementDefaults - }); + values.push(input); } return values; } private async promptForQuotedPsqlValues( + notebookUri: string, tokens: { name: string; kind: 'literal' | 'identifier' }[] ): Promise | undefined> { - const cache = - extensionContext?.workspaceState.get>(SqlExecutor.NAMED_PARAM_DEFAULTS_KEY, {}) ?? {}; - const next: Record = { ...cache }; const values: Record = {}; for (const token of tokens) { @@ -248,22 +312,52 @@ export class SqlExecutor { continue; } const tokenLabel = token.kind === 'literal' ? `:'${token.name}'` : `:"${token.name}"`; - const input = await vscode.window.showInputBox({ + const input = await this.promptForNotebookParameterValue({ + notebookUri, + parameterKey: this.makeNotebookParameterKey('quoted', `${token.kind}:${token.name}`), title: `SQL variable ${tokenLabel}`, prompt: `Value for ${tokenLabel}`, - value: next[token.name] ?? '', - ignoreFocusOut: true + allowNullChoice: false, }); if (input === undefined) { return undefined; } - values[token.name] = input; - next[token.name] = input; + values[token.name] = input ?? ''; } - if (extensionContext) { - await extensionContext.workspaceState.update(SqlExecutor.NAMED_PARAM_DEFAULTS_KEY, next); + return values; + } + + /** + * Prompts for each `:name` value in order; persists last-used values per notebook. + * Returns `undefined` if the user cancels any prompt. + */ + private async promptForNamedParameterValues( + notebookUri: string, + paramNames: string[], + commentValues?: Map + ): Promise { + const values: unknown[] = []; + + for (const name of paramNames) { + if (commentValues?.has(name)) { + values.push(commentValues.get(name) ?? null); + continue; + } + const input = await this.promptForNotebookParameterValue({ + notebookUri, + parameterKey: this.makeNotebookParameterKey('named', name), + title: `SQL parameter :${name}`, + prompt: `Enter the raw value for :${name} (do not add quotes; PgStudio sends the parameter value directly)`, + allowNullChoice: true, + }); + + if (input === undefined) { + return undefined; + } + + values.push(input); } return values; @@ -395,6 +489,27 @@ export class SqlExecutor { } } + private async persistPromptedParamComments( + cell: vscode.NotebookCell, + keys: (number | string)[], + vals: unknown[], + commentDefined: Map, + kind: 'positional' | 'named' + ): Promise { + const lines: string[] = []; + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if ((commentDefined as Map).has(key)) { continue; } + const v = vals[i]; + const valStr = v === null ? 'NULL' : String(v); + lines.push(kind === 'positional' ? `-- $${key}=${valStr}` : `-- :${key}=${valStr}`); + } + if (lines.length === 0) { return; } + const edit = new vscode.WorkspaceEdit(); + edit.insert(cell.document.uri, new vscode.Position(0, 0), lines.join('\n') + '\n'); + await vscode.workspace.applyEdit(edit); + } + /** * Optional execution directives embedded as SQL comments at top-level. * - pgstudio:full-dataset => disable streaming + disable auto-limit for this statement. @@ -442,17 +557,15 @@ export class SqlExecutor { throw new Error('Connection not found'); } - // Apply profile settings from metadata to connection (metadata takes precedence) + // Apply profile settings with floor protection (connection level readOnly cannot be downgraded) + let readOnlyMode = connection.readOnlyMode === true; if (metadata.readOnlyMode !== undefined) { - connection.readOnlyMode = metadata.readOnlyMode; + readOnlyMode = readOnlyMode || metadata.readOnlyMode; } - - // Apply profile settings from globalState if available - if (activeProfileContext) { - if (activeProfileContext.readOnlyMode !== undefined) { - connection.readOnlyMode = activeProfileContext.readOnlyMode; - } + if (activeProfileContext?.readOnlyMode !== undefined) { + readOnlyMode = readOnlyMode || activeProfileContext.readOnlyMode; } + connection.readOnlyMode = readOnlyMode; const client = await ConnectionManager.getInstance().getSessionClient({ id: connection.id, @@ -519,6 +632,14 @@ export class SqlExecutor { const queryAnalyzer = QueryAnalyzer.getInstance(); let userConfirmedDangerousOps: 'Execute' | 'Execute in Transaction' | 'Cancelled' | null = null; + // Resolve autoApplySafetyCheck (activeProfileContext has precedence over notebook metadata) + let autoApplySafetyCheck = true; + if (activeProfileContext?.autoApplySafetyCheck !== undefined) { + autoApplySafetyCheck = !!activeProfileContext.autoApplySafetyCheck; + } else if (metadata?.autoApplySafetyCheck !== undefined) { + autoApplySafetyCheck = !!metadata.autoApplySafetyCheck; + } + // Collect all dangerous operations and perform read-only checks const allDangerousOps: Array<{ stmt: string; analysis: any }> = []; for (const stmt of statements) { @@ -529,7 +650,7 @@ export class SqlExecutor { // Analyze for dangerous operations const analysis = queryAnalyzer.analyzeQuery(stmt, connection); - if (analysis.requiresConfirmation) { + if (analysis.requiresConfirmation && autoApplySafetyCheck) { allDangerousOps.push({ stmt, analysis }); } } @@ -582,6 +703,7 @@ export class SqlExecutor { liveNoticesActive = false; let query = statements[stmtIndex]; const stmtStartTime = Date.now(); + const notebookUri = cell.notebook.uri.toString(); const params = SqlParser.detectParameters(query); const hasPositional = params.positional.length > 0; @@ -591,10 +713,11 @@ export class SqlExecutor { throw new Error('Mixing $N and :name parameters in the same statement is not supported. Use one style per query.'); } + const commentParams = SqlParser.parseCommentParameters(query); let pgParamValues: unknown[] | undefined; if (params.quoted.length > 0) { - const quotedVals = await this.promptForQuotedPsqlValues(params.quoted); + const quotedVals = await this.promptForQuotedPsqlValues(notebookUri, params.quoted); if (!quotedVals) { client.removeListener('notice', noticeListener); execution.end(false, Date.now()); @@ -605,7 +728,7 @@ export class SqlExecutor { if (hasNamed) { const named = SqlParser.substituteNamedParametersWithPgPlaceholders(query); - const vals = await this.promptForNamedParameterValues(named.paramNames); + const vals = await this.promptForNamedParameterValues(notebookUri, named.paramNames, commentParams.named); if (vals === undefined) { client.removeListener('notice', noticeListener); execution.end(false, Date.now()); @@ -613,12 +736,14 @@ export class SqlExecutor { } query = named.text; pgParamValues = vals; + await this.persistPromptedParamComments(cell, named.paramNames, vals, commentParams.named, 'named'); } else if (hasPositional) { - const maxN = Math.max(...params.positional); + const indices = Array.from({ length: Math.max(...params.positional) }, (_, i) => i + 1); const vals = await this.promptForPositionalParameterValues( - Array.from({ length: maxN }, (_, i) => i + 1), - QueryAnalyzer.getInstance().getQueryHash(query), - query + notebookUri, + indices, + query, + commentParams.positional ); if (vals === undefined) { client.removeListener('notice', noticeListener); @@ -626,6 +751,7 @@ export class SqlExecutor { return; } pgParamValues = vals; + await this.persistPromptedParamComments(cell, indices, vals, commentParams.positional, 'positional'); } const directives = this.consumeExecutionDirectives(query); @@ -893,8 +1019,6 @@ export class SqlExecutor { }); } - await this.maybePromptForReview(); - } catch (err: any) { const stmtEndTime = Date.now(); const executionTime = (stmtEndTime - stmtStartTime) / 1000; @@ -1009,14 +1133,37 @@ export class SqlExecutor { client.removeListener('notice', noticeListener); execution.end(true, Date.now()); + void this.maybePromptForReview(); // Update notebook title after successful cell execution updateNotebookTitle(cell.notebook).catch(err => console.warn('Failed to update notebook title:', err)); } catch (err: any) { console.error('SqlExecutor: Execution failed:', err); + const pgErrorCode: string | undefined = err.code; + const errorData = { + success: false, + error: err.message || String(err), + query: cell.document.getText(), + executionTime: 0, + slowQuery: false, + canExplain: false, + errorCode: pgErrorCode, + errorExplanation: pgErrorCode ? getErrorExplanation(pgErrorCode) : undefined, + sourceCellIndex: cell.index, + }; + await execution.replaceOutput(new NotebookCellOutput([ - new NotebookCellOutputItem(Buffer.from(String(err), 'utf8'), 'application/vnd.code.notebook.error') + new NotebookCellOutputItem( + Buffer.from(JSON.stringify(errorData), 'utf8'), + 'application/vnd.postgres-notebook.error', + ), ])); + + // Update execution time pill in CodeLens bar (failure) + QueryCodeLensProvider.getInstance()?.updatePill(cell.document.uri.toString(), { + success: false + }); + execution.end(false, Date.now()); // Update notebook title even after failed execution (cell content may have changed) updateNotebookTitle(cell.notebook).catch(err => console.warn('Failed to update notebook title:', err)); diff --git a/src/providers/kernel/SqlParser.ts b/src/providers/kernel/SqlParser.ts index 33237e8..95a4d03 100644 --- a/src/providers/kernel/SqlParser.ts +++ b/src/providers/kernel/SqlParser.ts @@ -543,4 +543,37 @@ export class SqlParser { return { text: out, paramNames: ordered }; } + + public static parseCommentParameters(sql: string): { + positional: Map; + named: Map; + } { + const positional = new Map(); + const named = new Map(); + const lineRe = /^--\s*(\$(\d+)|:([a-zA-Z_][a-zA-Z0-9_]*))=(.*?)\s*$/; + + for (const line of sql.split('\n')) { + const trimmed = line.trim(); + if (!trimmed) { continue; } + if (!trimmed.startsWith('--')) { break; } + const m = lineRe.exec(trimmed); + if (!m) { continue; } + const val = SqlParser.parseCommentParamValue(m[4].trim()); + if (m[2]) { + positional.set(Number(m[2]), val); + } else if (m[3]) { + named.set(m[3], val); + } + } + + return { positional, named }; + } + + private static parseCommentParamValue(raw: string): string | null { + if (/^null$/i.test(raw)) { return null; } + if (raw.length >= 2 && raw.startsWith("'") && raw.endsWith("'")) { + return raw.slice(1, -1).replace(/''/g, "'"); + } + return raw; + } } diff --git a/src/schemaDesigner/RoleDesignerPanel.ts b/src/schemaDesigner/RoleDesignerPanel.ts new file mode 100644 index 0000000..6d6834d --- /dev/null +++ b/src/schemaDesigner/RoleDesignerPanel.ts @@ -0,0 +1,924 @@ +import * as vscode from 'vscode'; +import { ErrorHandlers } from '../commands/helper'; +import { createAndShowNotebook } from '../commands/connection'; +import { DatabaseTreeItem } from '../providers/DatabaseTreeProvider'; +import { resolveTreeItemConnection } from './connectionHelper'; +import { + buildRoleMigrationMarkdown, + buildRoleMigrationSql, + buildRolePreviewHtml, + buildRoleChangePreviewHtml, + buildRoleChangeMigrationSql, + buildRoleChangeMigrationMarkdown, + RoleDesignerDatabasePrivilege, + RoleDesignerDefaultTablePrivilege, + RoleDesignerMembership, + RoleDesignerSchemaPrivilege, + RoleDesignerState, + RoleDesignerTablePrivilege, +} from './RoleSQL'; + +type RoleDesignerQueryResult = { + roleName: string; + password: string; + connectionLimit: string; + validUntil: string; + flags: RoleDesignerState['flags']; + searchPath: string; + statementTimeout: string; + workMem: string; + databasePrivileges: RoleDesignerDatabasePrivilege[]; + schemaPrivileges: RoleDesignerSchemaPrivilege[]; + defaultTablePrivileges: RoleDesignerDefaultTablePrivilege[]; + tablePrivileges: RoleDesignerTablePrivilege[]; + memberOf: RoleDesignerMembership[]; + members: RoleDesignerMembership[]; + availableRoles: string[]; +}; + +export class RoleDesignerPanel { + public static readonly viewType = 'pgStudio.roleDesigner'; + + private static _panels = new Map(); + + private readonly _panel: vscode.WebviewPanel; + private readonly _disposables: vscode.Disposable[] = []; + private _currentState: RoleDesignerState | undefined; + private _originalState: RoleDesignerState | undefined; + + private constructor( + panel: vscode.WebviewPanel, + private readonly _extensionUri: vscode.Uri, + ) { + this._panel = panel; + this._panel.onDidDispose(() => this.dispose(), null, this._disposables); + } + + public static async openForRole(item: DatabaseTreeItem, context: vscode.ExtensionContext): Promise { + let dbConn: Awaited> | undefined; + try { + dbConn = await resolveTreeItemConnection(item); + if (!dbConn) { + return; + } + + const { client } = dbConn; + const roleName = item.label; + const panelKey = `${item.connectionId}:${item.databaseName}:${roleName}`; + + if (RoleDesignerPanel._panels.has(panelKey)) { + RoleDesignerPanel._panels.get(panelKey)!._panel.reveal(vscode.ViewColumn.One); + return; + } + + const notebookMetadata = dbConn.metadata; + + const roleRow = await client.query( + `SELECT + r.rolname, + r.rolsuper, + r.rolcreatedb, + r.rolcreaterole, + r.rolcanlogin, + r.rolinherit, + r.rolreplication, + r.rolbypassrls, + r.rolconnlimit, + r.rolvaliduntil, + COALESCE(pg_catalog.shobj_description(r.oid, 'pg_authid'), '') as description + FROM pg_roles r + WHERE r.rolname = $1`, + [roleName] + ); + + if (roleRow.rows.length === 0) { + throw new Error('Role not found'); + } + + let password = ''; + try { + const pwdResult = await client.query( + `SELECT rolpassword FROM pg_authid WHERE rolname = $1`, + [roleName] + ); + password = pwdResult.rows[0]?.rolpassword || ''; + } catch { + // Some roles cannot inspect pg_authid; show a placeholder instead of failing. + password = '(hidden)'; + } + + const databasesResult = await client.query( + `SELECT + datname, + has_database_privilege($1, datname, 'CONNECT') as can_connect, + has_database_privilege($1, datname, 'CREATE') as can_create, + has_database_privilege($1, datname, 'TEMP') as can_temp + FROM pg_database + WHERE datallowconn + ORDER BY datname`, + [roleName] + ); + + const schemasResult = await client.query( + `SELECT + n.nspname as schema_name, + has_schema_privilege($1, n.nspname, 'USAGE') as can_usage, + has_schema_privilege($1, n.nspname, 'CREATE') as can_create, + EXISTS ( + SELECT 1 + FROM pg_class c + WHERE c.relnamespace = n.oid + AND c.relkind IN ('r', 'p', 'v', 'm', 'f') + AND NOT has_table_privilege($1, format('%I.%I', n.nspname, c.relname), 'SELECT') + ) = false as can_select_all_tables + FROM pg_namespace n + WHERE n.nspname NOT LIKE 'pg_%' + AND n.nspname <> 'information_schema' + ORDER BY CASE WHEN n.nspname = 'public' THEN 0 ELSE 1 END, n.nspname`, + [roleName] + ); + + const defaultTablePrivileges = schemasResult.rows.map((row: any) => ({ + schemaName: row.schema_name, + select: false, + insert: false, + update: false, + delete: false, + })); + + const tablePrivilegesResult = await client.query( + `SELECT + tp.table_schema as schema_name, + tp.table_name, + bool_or(tp.privilege_type = 'SELECT') as can_select, + bool_or(tp.privilege_type = 'INSERT') as can_insert, + bool_or(tp.privilege_type = 'UPDATE') as can_update, + bool_or(tp.privilege_type = 'DELETE') as can_delete + FROM information_schema.table_privileges tp + WHERE tp.grantee = $1 + AND tp.table_schema NOT LIKE 'pg_%' + AND tp.table_schema <> 'information_schema' + GROUP BY tp.table_schema, tp.table_name + ORDER BY tp.table_schema, tp.table_name`, + [roleName] + ); + + const memberOfResult = await client.query( + `SELECT + g.rolname as role_name + FROM pg_auth_members am + JOIN pg_roles r ON r.oid = am.member + JOIN pg_roles g ON g.oid = am.roleid + WHERE r.rolname = $1 + ORDER BY g.rolname`, + [roleName] + ); + + const membersResult = await client.query( + `SELECT + m.rolname as role_name + FROM pg_auth_members am + JOIN pg_roles r ON r.oid = am.roleid + JOIN pg_roles m ON m.oid = am.member + WHERE r.rolname = $1 + ORDER BY m.rolname`, + [roleName] + ); + + const availableRolesResult = await client.query( + `SELECT rolname FROM pg_roles WHERE rolname != $1 ORDER BY rolname`, + [roleName] + ); + + const data = RoleDesignerPanel._toState({ + roleName, + password, + connectionLimit: String(roleRow.rows[0].rolconnlimit ?? '-1'), + validUntil: roleRow.rows[0].rolvaliduntil ? String(roleRow.rows[0].rolvaliduntil) : '', + flags: { + login: !!roleRow.rows[0].rolcanlogin, + superuser: !!roleRow.rows[0].rolsuper, + createdb: !!roleRow.rows[0].rolcreatedb, + createrole: !!roleRow.rows[0].rolcreaterole, + inherit: !!roleRow.rows[0].rolinherit, + replication: !!roleRow.rows[0].rolreplication, + bypassrls: !!roleRow.rows[0].rolbypassrls, + }, + searchPath: 'analytics, public', + statementTimeout: '30s', + workMem: '64MB', + databasePrivileges: databasesResult.rows.map((row: any) => ({ + databaseName: row.datname, + connect: !!row.can_connect, + create: !!row.can_create, + temp: !!row.can_temp, + })), + schemaPrivileges: schemasResult.rows.map((row: any) => ({ + schemaName: row.schema_name, + usage: !!row.can_usage, + create: !!row.can_create, + selectAllTables: !!row.can_select_all_tables, + })), + defaultTablePrivileges, + tablePrivileges: tablePrivilegesResult.rows.map((row: any) => ({ + schemaName: row.schema_name, + tableName: row.table_name, + select: !!row.can_select, + insert: !!row.can_insert, + update: !!row.can_update, + delete: !!row.can_delete, + })), + memberOf: memberOfResult.rows.map((row: any) => ({ + roleName: row.role_name, + enabled: true, + lastLogin: null, + })), + members: membersResult.rows.map((row: any) => ({ + roleName: row.role_name, + enabled: true, + lastLogin: null, + })), + availableRoles: availableRolesResult.rows.map((row: any) => row.rolname), + }); + + const panel = vscode.window.createWebviewPanel( + RoleDesignerPanel.viewType, + `Role Designer · ${roleName}`, + vscode.ViewColumn.One, + { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'resources')], + } + ); + + const designer = new RoleDesignerPanel(panel, context.extensionUri); + designer._originalState = data; + designer._currentState = data; + RoleDesignerPanel._panels.set(panelKey, designer); + + panel.onDidDispose(() => { + RoleDesignerPanel._panels.delete(panelKey); + }); + + panel.webview.html = RoleDesignerPanel._getHtml(panel.webview, data); + panel.webview.onDidReceiveMessage(async (message) => { + switch (message.type) { + case 'stateChanged': { + designer._currentState = message.state as RoleDesignerState; + panel.webview.postMessage({ + type: 'previewUpdated', + html: buildRoleChangePreviewHtml(designer._originalState!, designer._currentState), + }); + break; + } + case 'copySql': { + const sql = buildRoleChangeMigrationSql(designer._originalState!, message.state as RoleDesignerState); + await vscode.env.clipboard.writeText(sql); + vscode.window.showInformationMessage('Changes copied to clipboard (delta mode)'); + break; + } + case 'openNotebook': { + await RoleDesignerPanel._openNotebook( + message.state as RoleDesignerState, + notebookMetadata, + designer._originalState + ); + break; + } + } + }, null, designer._disposables); + + } catch (err: any) { + await ErrorHandlers.handleCommandError(err, 'open role designer'); + } finally { + if (dbConn && dbConn.release) { + dbConn.release(); + } + } + } + + private static _toState(data: RoleDesignerQueryResult): RoleDesignerState { + return { + roleName: data.roleName, + password: data.password, + connectionLimit: data.connectionLimit, + validUntil: data.validUntil, + flags: data.flags, + searchPath: data.searchPath, + statementTimeout: data.statementTimeout, + workMem: data.workMem, + databasePrivileges: data.databasePrivileges, + schemaPrivileges: data.schemaPrivileges, + defaultTablePrivileges: data.defaultTablePrivileges, + tablePrivileges: data.tablePrivileges, + memberOf: data.memberOf, + members: data.members, + availableRoles: data.availableRoles, + }; + } + + private static async _openNotebook( + state: RoleDesignerState, + metadata: any, + originalState?: RoleDesignerState + ): Promise { + const markdown = originalState + ? buildRoleChangeMigrationMarkdown(originalState, state) + : buildRoleMigrationMarkdown(state); + const sql = originalState + ? buildRoleChangeMigrationSql(originalState, state) + : buildRoleMigrationSql(state); + + const cells = [ + new vscode.NotebookCellData(vscode.NotebookCellKind.Markup, markdown, 'markdown'), + new vscode.NotebookCellData(vscode.NotebookCellKind.Code, sql, 'sql'), + ]; + + await createAndShowNotebook(cells, metadata); + } + + private static _getHtml(webview: vscode.Webview, state: RoleDesignerState): string { + const nonce = Math.random().toString(36).slice(2); + const initialState = JSON.stringify(state).replace(/ + + + + + Role Designer + + + +
+
+
+ Role Designer + EDIT MODE + ${state.roleName} +
+
+
Properties
+
DB Privileges
+
Membership
+
Schema / Table Privs
+
+
+
+
+
+
+
+
+
+
+
SQL Preview
+ Unsaved changes +
+
+ +
+
+ + + +`; + } + + private dispose(): void { + RoleDesignerPanel._panels.forEach((panel, key) => { + if (panel === this) { + RoleDesignerPanel._panels.delete(key); + } + }); + + while (this._disposables.length) { + const disposable = this._disposables.pop(); + disposable?.dispose(); + } + } +} diff --git a/src/schemaDesigner/RoleSQL.ts b/src/schemaDesigner/RoleSQL.ts new file mode 100644 index 0000000..46a65fe --- /dev/null +++ b/src/schemaDesigner/RoleSQL.ts @@ -0,0 +1,737 @@ +export type RolePrivilegeFlag = + | 'login' + | 'superuser' + | 'createdb' + | 'createrole' + | 'inherit' + | 'replication' + | 'bypassrls'; + +export interface RoleDesignerDatabasePrivilege { + databaseName: string; + connect: boolean; + create: boolean; + temp: boolean; +} + +export interface RoleDesignerSchemaPrivilege { + schemaName: string; + usage: boolean; + create: boolean; + selectAllTables: boolean; +} + +export interface RoleDesignerTablePrivilege { + schemaName: string; + tableName: string; + select: boolean; + insert: boolean; + update: boolean; + delete: boolean; +} + +export interface RoleDesignerDefaultTablePrivilege { + schemaName: string; + select: boolean; + insert: boolean; + update: boolean; + delete: boolean; +} + +export interface RoleDesignerMembership { + roleName: string; + enabled: boolean; + lastLogin?: string | null; + privilegeType?: 'inherit' | 'admin'; // inherit = basic, admin = WITH ADMIN OPTION +} + +export interface RoleDesignerState { + roleName: string; + password: string; + connectionLimit: string; + validUntil: string; + flags: Record; + searchPath: string; + statementTimeout: string; + workMem: string; + databasePrivileges: RoleDesignerDatabasePrivilege[]; + schemaPrivileges: RoleDesignerSchemaPrivilege[]; + defaultTablePrivileges: RoleDesignerDefaultTablePrivilege[]; + tablePrivileges: RoleDesignerTablePrivilege[]; + memberOf: RoleDesignerMembership[]; + members: RoleDesignerMembership[]; + availableRoles: string[]; // List of all available roles for dropdown +} + +function quoteIdent(identifier: string): string { + return `"${identifier.replace(/"/g, '""')}"`; +} + +function quoteLiteral(value: string): string { + return `'${value.replace(/'/g, "''")}'`; +} + +function joinStatements(statements: string[]): string { + return statements.filter(Boolean).join('\n\n'); +} + +function formatConnectionLimit(value: string): string { + const trimmed = value.trim(); + return trimmed === '' ? '-1' : trimmed; +} + +function normalizeSearchPath(searchPath: string): string { + return searchPath + .split(',') + .map(part => part.trim()) + .filter(Boolean) + .join(', '); +} + +function renderRoleWithClause(state: RoleDesignerState): string { + const parts: string[] = [state.flags.login ? 'LOGIN' : 'NOLOGIN']; + parts.push(state.flags.superuser ? 'SUPERUSER' : 'NOSUPERUSER'); + parts.push(state.flags.createdb ? 'CREATEDB' : 'NOCREATEDB'); + parts.push(state.flags.createrole ? 'CREATEROLE' : 'NOCREATEROLE'); + parts.push(state.flags.inherit ? 'INHERIT' : 'NOINHERIT'); + parts.push(state.flags.replication ? 'REPLICATION' : 'NOREPLICATION'); + parts.push(state.flags.bypassrls ? 'BYPASSRLS' : 'NOBYPASSRLS'); + parts.push(`CONNECTION LIMIT ${formatConnectionLimit(state.connectionLimit)}`); + return parts.join('\n '); +} + +function renderPropertyStatements(state: RoleDesignerState): string[] { + const role = quoteIdent(state.roleName); + const statements: string[] = []; + + statements.push(`ALTER ROLE ${role}\n WITH ${renderRoleWithClause(state)};`); + + if (state.validUntil.trim()) { + statements.push(`ALTER ROLE ${role}\n VALID UNTIL ${quoteLiteral(state.validUntil.trim())};`); + } else { + statements.push(`ALTER ROLE ${role}\n VALID UNTIL 'infinity';`); + } + + if (state.searchPath.trim()) { + statements.push(`ALTER ROLE ${role}\n SET search_path TO ${normalizeSearchPath(state.searchPath)};`); + } + + if (state.statementTimeout.trim()) { + statements.push(`ALTER ROLE ${role}\n SET statement_timeout TO ${quoteLiteral(state.statementTimeout.trim())};`); + } + + if (state.workMem.trim()) { + statements.push(`ALTER ROLE ${role}\n SET work_mem TO ${quoteLiteral(state.workMem.trim())};`); + } + + return statements; +} + +function renderDatabasePrivilegeStatements(state: RoleDesignerState): string[] { + const role = quoteIdent(state.roleName); + const statements: string[] = []; + + for (const row of state.databasePrivileges) { + const granted: string[] = []; + const revoked: string[] = []; + + if (row.connect) granted.push('CONNECT'); else revoked.push('CONNECT'); + if (row.create) granted.push('CREATE'); else revoked.push('CREATE'); + if (row.temp) granted.push('TEMP'); else revoked.push('TEMP'); + + if (granted.length > 0) { + statements.push(`GRANT ${granted.join(', ')} ON DATABASE ${quoteIdent(row.databaseName)} TO ${role};`); + } + if (revoked.length > 0) { + statements.push(`REVOKE ${revoked.join(', ')} ON DATABASE ${quoteIdent(row.databaseName)} FROM ${role};`); + } + } + + return statements; +} + +function renderSchemaPrivilegeStatements(state: RoleDesignerState): string[] { + const role = quoteIdent(state.roleName); + const statements: string[] = []; + + for (const row of state.schemaPrivileges) { + if (row.usage) { + statements.push(`GRANT USAGE ON SCHEMA ${quoteIdent(row.schemaName)} TO ${role};`); + } else { + statements.push(`REVOKE USAGE ON SCHEMA ${quoteIdent(row.schemaName)} FROM ${role};`); + } + + if (row.create) { + statements.push(`GRANT CREATE ON SCHEMA ${quoteIdent(row.schemaName)} TO ${role};`); + } else { + statements.push(`REVOKE CREATE ON SCHEMA ${quoteIdent(row.schemaName)} FROM ${role};`); + } + + if (row.selectAllTables) { + statements.push(`GRANT SELECT ON ALL TABLES IN SCHEMA ${quoteIdent(row.schemaName)} TO ${role};`); + } else { + statements.push(`REVOKE SELECT ON ALL TABLES IN SCHEMA ${quoteIdent(row.schemaName)} FROM ${role};`); + } + } + + return statements; +} + +function renderDefaultTablePrivilegeStatements(state: RoleDesignerState): string[] { + const role = quoteIdent(state.roleName); + const statements: string[] = []; + + for (const row of state.defaultTablePrivileges) { + const granted: string[] = []; + + if (row.select) granted.push('SELECT'); + if (row.insert) granted.push('INSERT'); + if (row.update) granted.push('UPDATE'); + if (row.delete) granted.push('DELETE'); + + if (granted.length > 0) { + statements.push(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${quoteIdent(row.schemaName)} GRANT ${granted.join(', ')} ON TABLES TO ${role};`); + } + } + + return statements; +} + +function renderTablePrivilegeStatements(state: RoleDesignerState): string[] { + const role = quoteIdent(state.roleName); + const statements: string[] = []; + + for (const row of state.tablePrivileges) { + const tableName = `${quoteIdent(row.schemaName)}.${quoteIdent(row.tableName)}`; + const granted: string[] = []; + const revoked: string[] = []; + + if (row.select) granted.push('SELECT'); else revoked.push('SELECT'); + if (row.insert) granted.push('INSERT'); else revoked.push('INSERT'); + if (row.update) granted.push('UPDATE'); else revoked.push('UPDATE'); + if (row.delete) granted.push('DELETE'); else revoked.push('DELETE'); + + if (granted.length > 0) { + statements.push(`GRANT ${granted.join(', ')} ON TABLE ${tableName} TO ${role};`); + } + if (revoked.length > 0) { + statements.push(`REVOKE ${revoked.join(', ')} ON TABLE ${tableName} FROM ${role};`); + } + } + + return statements; +} + +function renderMembershipStatements(roleName: string, memberships: RoleDesignerMembership[], direction: 'memberOf' | 'members'): string[] { + const statements: string[] = []; + const role = quoteIdent(roleName); + + for (const membership of memberships) { + const targetRole = quoteIdent(membership.roleName); + if (direction === 'memberOf') { + statements.push( + membership.enabled + ? `GRANT ${targetRole} TO ${role};` + : `REVOKE ${targetRole} FROM ${role};` + ); + } else { + statements.push( + membership.enabled + ? `GRANT ${role} TO ${targetRole};` + : `REVOKE ${role} FROM ${targetRole};` + ); + } + } + + return statements; +} + +export function buildRoleMigrationStatements(state: RoleDesignerState): string[] { + return [ + '-- Generated by PgStudio Role Designer', + '-- Review carefully before executing in a transaction', + '', + ...renderPropertyStatements(state), + ...renderDatabasePrivilegeStatements(state), + ...renderSchemaPrivilegeStatements(state), + ...renderDefaultTablePrivilegeStatements(state), + ...renderTablePrivilegeStatements(state), + ...renderMembershipStatements(state.roleName, state.memberOf, 'memberOf'), + ...renderMembershipStatements(state.roleName, state.members, 'members'), + ]; +} + +export function buildRoleMigrationSql(state: RoleDesignerState): string { + const statements = buildRoleMigrationStatements(state); + return ['BEGIN;', '', joinStatements(statements), '', 'COMMIT;'].join('\n'); +} + +export function buildRoleMigrationMarkdown(state: RoleDesignerState): string { + const statementCount = buildRoleMigrationStatements(state).filter(line => !line.startsWith('--') && line.trim() !== '').length; + return `### Role Designer: \`${state.roleName}\`\n\n` + + `
` + + `ℹ️ Review: This script is generated from the current visual state. Run it in a transaction for safety.
\n\n` + + `Generated **${statementCount}** SQL statement(s).`; +} + +export function buildRolePreviewHtml(state: RoleDesignerState): string { + const sql = buildRoleMigrationSql(state) + .replace(/&/g, '&') + .replace(//g, '>'); + + return `-- Generated by PgStudio Role Designer
` + + `-- Review carefully before executing in a transaction

` + + sql.replace(/\n/g, '
').replace(/ /g, '  '); +} + +// Delta detection and generation for incremental preview +function getPropertyChanges(original: RoleDesignerState, current: RoleDesignerState): string[] { + const role = quoteIdent(current.roleName); + const statements: string[] = []; + + // Check if any WITH clause flags changed + const flagsChanged = Object.keys(current.flags).some( + flag => current.flags[flag as RolePrivilegeFlag] !== original.flags[flag as RolePrivilegeFlag] + ); + if (flagsChanged) { + statements.push(`ALTER ROLE ${role}\n WITH ${renderRoleWithClause(current)};`); + } + + // Check VALID UNTIL + const validUntilChanged = current.validUntil.trim() !== original.validUntil.trim(); + if (validUntilChanged) { + const validUntil = current.validUntil.trim() ? quoteLiteral(current.validUntil.trim()) : "'infinity'"; + statements.push(`ALTER ROLE ${role}\n VALID UNTIL ${validUntil};`); + } + + // Check search_path + const searchPathChanged = normalizeSearchPath(current.searchPath) !== normalizeSearchPath(original.searchPath); + if (searchPathChanged) { + if (current.searchPath.trim()) { + statements.push(`ALTER ROLE ${role}\n SET search_path TO ${normalizeSearchPath(current.searchPath)};`); + } else { + statements.push(`ALTER ROLE ${role}\n RESET search_path;`); + } + } + + // Check statement_timeout + const statementTimeoutChanged = current.statementTimeout.trim() !== original.statementTimeout.trim(); + if (statementTimeoutChanged) { + if (current.statementTimeout.trim()) { + statements.push(`ALTER ROLE ${role}\n SET statement_timeout TO ${quoteLiteral(current.statementTimeout.trim())};`); + } else { + statements.push(`ALTER ROLE ${role}\n RESET statement_timeout;`); + } + } + + // Check work_mem + const workMemChanged = current.workMem.trim() !== original.workMem.trim(); + if (workMemChanged) { + if (current.workMem.trim()) { + statements.push(`ALTER ROLE ${role}\n SET work_mem TO ${quoteLiteral(current.workMem.trim())};`); + } else { + statements.push(`ALTER ROLE ${role}\n RESET work_mem;`); + } + } + + return statements; +} + +function getDatabasePrivilegeChanges(original: RoleDesignerState, current: RoleDesignerState): string[] { + const role = quoteIdent(current.roleName); + const statements: string[] = []; + + const currentMap = new Map(current.databasePrivileges.map(p => [p.databaseName, p])); + const originalMap = new Map(original.databasePrivileges.map(p => [p.databaseName, p])); + + const allDbNames = new Set([...currentMap.keys(), ...originalMap.keys()]); + + for (const dbName of allDbNames) { + const currPriv = currentMap.get(dbName); + const origPriv = originalMap.get(dbName); + + if (!currPriv && origPriv) { + // Database privilege was removed + const revoked: string[] = []; + if (origPriv.connect) revoked.push('CONNECT'); + if (origPriv.create) revoked.push('CREATE'); + if (origPriv.temp) revoked.push('TEMP'); + if (revoked.length > 0) { + statements.push(`REVOKE ${revoked.join(', ')} ON DATABASE ${quoteIdent(dbName)} FROM ${role};`); + } + } else if (currPriv && !origPriv) { + // Database privilege was added + const granted: string[] = []; + if (currPriv.connect) granted.push('CONNECT'); + if (currPriv.create) granted.push('CREATE'); + if (currPriv.temp) granted.push('TEMP'); + if (granted.length > 0) { + statements.push(`GRANT ${granted.join(', ')} ON DATABASE ${quoteIdent(dbName)} TO ${role};`); + } + } else if (currPriv && origPriv) { + // Database privilege exists in both, check for changes + const granted: string[] = []; + const revoked: string[] = []; + + // CONNECT + if (currPriv.connect && !origPriv.connect) granted.push('CONNECT'); + if (!currPriv.connect && origPriv.connect) revoked.push('CONNECT'); + + // CREATE + if (currPriv.create && !origPriv.create) granted.push('CREATE'); + if (!currPriv.create && origPriv.create) revoked.push('CREATE'); + + // TEMP + if (currPriv.temp && !origPriv.temp) granted.push('TEMP'); + if (!currPriv.temp && origPriv.temp) revoked.push('TEMP'); + + if (granted.length > 0) { + statements.push(`GRANT ${granted.join(', ')} ON DATABASE ${quoteIdent(dbName)} TO ${role};`); + } + if (revoked.length > 0) { + statements.push(`REVOKE ${revoked.join(', ')} ON DATABASE ${quoteIdent(dbName)} FROM ${role};`); + } + } + } + + return statements; +} + +function getSchemaPrivilegeChanges(original: RoleDesignerState, current: RoleDesignerState): string[] { + const role = quoteIdent(current.roleName); + const statements: string[] = []; + + const currentMap = new Map(current.schemaPrivileges.map(p => [p.schemaName, p])); + const originalMap = new Map(original.schemaPrivileges.map(p => [p.schemaName, p])); + + const allSchemaNames = new Set([...currentMap.keys(), ...originalMap.keys()]); + + for (const schemaName of allSchemaNames) { + const currPriv = currentMap.get(schemaName); + const origPriv = originalMap.get(schemaName); + + if (!currPriv && origPriv) { + // Schema privilege was removed + if (origPriv.usage) { + statements.push(`REVOKE USAGE ON SCHEMA ${quoteIdent(schemaName)} FROM ${role};`); + } + if (origPriv.create) { + statements.push(`REVOKE CREATE ON SCHEMA ${quoteIdent(schemaName)} FROM ${role};`); + } + if (origPriv.selectAllTables) { + statements.push(`REVOKE SELECT ON ALL TABLES IN SCHEMA ${quoteIdent(schemaName)} FROM ${role};`); + } + } else if (currPriv && !origPriv) { + // Schema privilege was added + if (currPriv.usage) { + statements.push(`GRANT USAGE ON SCHEMA ${quoteIdent(schemaName)} TO ${role};`); + } + if (currPriv.create) { + statements.push(`GRANT CREATE ON SCHEMA ${quoteIdent(schemaName)} TO ${role};`); + } + if (currPriv.selectAllTables) { + statements.push(`GRANT SELECT ON ALL TABLES IN SCHEMA ${quoteIdent(schemaName)} TO ${role};`); + } + } else if (currPriv && origPriv) { + // Schema privilege exists in both, check for changes + // USAGE + if (currPriv.usage && !origPriv.usage) { + statements.push(`GRANT USAGE ON SCHEMA ${quoteIdent(schemaName)} TO ${role};`); + } else if (!currPriv.usage && origPriv.usage) { + statements.push(`REVOKE USAGE ON SCHEMA ${quoteIdent(schemaName)} FROM ${role};`); + } + + // CREATE + if (currPriv.create && !origPriv.create) { + statements.push(`GRANT CREATE ON SCHEMA ${quoteIdent(schemaName)} TO ${role};`); + } else if (!currPriv.create && origPriv.create) { + statements.push(`REVOKE CREATE ON SCHEMA ${quoteIdent(schemaName)} FROM ${role};`); + } + + // SELECT ALL TABLES + if (currPriv.selectAllTables && !origPriv.selectAllTables) { + statements.push(`GRANT SELECT ON ALL TABLES IN SCHEMA ${quoteIdent(schemaName)} TO ${role};`); + } else if (!currPriv.selectAllTables && origPriv.selectAllTables) { + statements.push(`REVOKE SELECT ON ALL TABLES IN SCHEMA ${quoteIdent(schemaName)} FROM ${role};`); + } + } + } + + return statements; +} + +function getDefaultTablePrivilegeChanges(original: RoleDesignerState, current: RoleDesignerState): string[] { + const role = quoteIdent(current.roleName); + const statements: string[] = []; + + const currentMap = new Map(current.defaultTablePrivileges.map(p => [p.schemaName, p])); + const originalMap = new Map(original.defaultTablePrivileges.map(p => [p.schemaName, p])); + + const allSchemaNames = new Set([...currentMap.keys(), ...originalMap.keys()]); + + for (const schemaName of allSchemaNames) { + const currPriv = currentMap.get(schemaName); + const origPriv = originalMap.get(schemaName); + + if (!currPriv && origPriv) { + // Default table privilege was removed + const revoked: string[] = []; + if (origPriv.select) revoked.push('SELECT'); + if (origPriv.insert) revoked.push('INSERT'); + if (origPriv.update) revoked.push('UPDATE'); + if (origPriv.delete) revoked.push('DELETE'); + if (revoked.length > 0) { + statements.push(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${quoteIdent(schemaName)} REVOKE ${revoked.join(', ')} ON TABLES FROM ${role};`); + } + } else if (currPriv && !origPriv) { + // Default table privilege was added + const granted: string[] = []; + if (currPriv.select) granted.push('SELECT'); + if (currPriv.insert) granted.push('INSERT'); + if (currPriv.update) granted.push('UPDATE'); + if (currPriv.delete) granted.push('DELETE'); + if (granted.length > 0) { + statements.push(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${quoteIdent(schemaName)} GRANT ${granted.join(', ')} ON TABLES TO ${role};`); + } + } else if (currPriv && origPriv) { + // Default table privilege exists in both, check for changes + const granted: string[] = []; + const revoked: string[] = []; + + if (currPriv.select && !origPriv.select) granted.push('SELECT'); + if (!currPriv.select && origPriv.select) revoked.push('SELECT'); + + if (currPriv.insert && !origPriv.insert) granted.push('INSERT'); + if (!currPriv.insert && origPriv.insert) revoked.push('INSERT'); + + if (currPriv.update && !origPriv.update) granted.push('UPDATE'); + if (!currPriv.update && origPriv.update) revoked.push('UPDATE'); + + if (currPriv.delete && !origPriv.delete) granted.push('DELETE'); + if (!currPriv.delete && origPriv.delete) revoked.push('DELETE'); + + if (granted.length > 0) { + statements.push(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${quoteIdent(schemaName)} GRANT ${granted.join(', ')} ON TABLES TO ${role};`); + } + if (revoked.length > 0) { + statements.push(`ALTER DEFAULT PRIVILEGES IN SCHEMA ${quoteIdent(schemaName)} REVOKE ${revoked.join(', ')} ON TABLES FROM ${role};`); + } + } + } + + return statements; +} + +function getTablePrivilegeChanges(original: RoleDesignerState, current: RoleDesignerState): string[] { + const role = quoteIdent(current.roleName); + const statements: string[] = []; + + const currentMap = new Map( + current.tablePrivileges.map(p => [`${p.schemaName}.${p.tableName}`, p]) + ); + const originalMap = new Map( + original.tablePrivileges.map(p => [`${p.schemaName}.${p.tableName}`, p]) + ); + + const allTableKeys = new Set([...currentMap.keys(), ...originalMap.keys()]); + + for (const key of allTableKeys) { + const currPriv = currentMap.get(key); + const origPriv = originalMap.get(key); + + if (!currPriv && origPriv) { + // Table privilege was removed + const tableName = `${quoteIdent(origPriv.schemaName)}.${quoteIdent(origPriv.tableName)}`; + const revoked: string[] = []; + if (origPriv.select) revoked.push('SELECT'); + if (origPriv.insert) revoked.push('INSERT'); + if (origPriv.update) revoked.push('UPDATE'); + if (origPriv.delete) revoked.push('DELETE'); + if (revoked.length > 0) { + statements.push(`REVOKE ${revoked.join(', ')} ON TABLE ${tableName} FROM ${role};`); + } + } else if (currPriv && !origPriv) { + // Table privilege was added + const tableName = `${quoteIdent(currPriv.schemaName)}.${quoteIdent(currPriv.tableName)}`; + const granted: string[] = []; + if (currPriv.select) granted.push('SELECT'); + if (currPriv.insert) granted.push('INSERT'); + if (currPriv.update) granted.push('UPDATE'); + if (currPriv.delete) granted.push('DELETE'); + if (granted.length > 0) { + statements.push(`GRANT ${granted.join(', ')} ON TABLE ${tableName} TO ${role};`); + } + } else if (currPriv && origPriv) { + // Table privilege exists in both, check for changes + const tableName = `${quoteIdent(currPriv.schemaName)}.${quoteIdent(currPriv.tableName)}`; + const granted: string[] = []; + const revoked: string[] = []; + + if (currPriv.select && !origPriv.select) granted.push('SELECT'); + if (!currPriv.select && origPriv.select) revoked.push('SELECT'); + + if (currPriv.insert && !origPriv.insert) granted.push('INSERT'); + if (!currPriv.insert && origPriv.insert) revoked.push('INSERT'); + + if (currPriv.update && !origPriv.update) granted.push('UPDATE'); + if (!currPriv.update && origPriv.update) revoked.push('UPDATE'); + + if (currPriv.delete && !origPriv.delete) granted.push('DELETE'); + if (!currPriv.delete && origPriv.delete) revoked.push('DELETE'); + + if (granted.length > 0) { + statements.push(`GRANT ${granted.join(', ')} ON TABLE ${tableName} TO ${role};`); + } + if (revoked.length > 0) { + statements.push(`REVOKE ${revoked.join(', ')} ON TABLE ${tableName} FROM ${role};`); + } + } + } + + return statements; +} + +function getMembershipChanges( + roleName: string, + original: RoleDesignerMembership[], + current: RoleDesignerMembership[], + direction: 'memberOf' | 'members' +): string[] { + const statements: string[] = []; + const role = quoteIdent(roleName); + + // Create maps keyed by roleName + const currentMap = new Map(current.map(m => [m.roleName, m])); + const originalMap = new Map(original.map(m => [m.roleName, m])); + + const allMemberRoles = new Set([...currentMap.keys(), ...originalMap.keys()]); + + for (const memberRole of allMemberRoles) { + const currMembership = currentMap.get(memberRole); + const origMembership = originalMap.get(memberRole); + const targetRole = quoteIdent(memberRole); + + if (!currMembership && origMembership) { + // Membership was removed + if (origMembership.enabled) { + if (direction === 'memberOf') { + statements.push(`REVOKE ${targetRole} FROM ${role};`); + } else { + statements.push(`REVOKE ${role} FROM ${targetRole};`); + } + } + } else if (currMembership && !origMembership) { + // Membership was added + if (currMembership.enabled) { + const adminOpt = currMembership.privilegeType === 'admin' ? ' WITH ADMIN OPTION' : ''; + if (direction === 'memberOf') { + statements.push(`GRANT ${targetRole} TO ${role}${adminOpt};`); + } else { + statements.push(`GRANT ${role} TO ${targetRole}${adminOpt};`); + } + } + } else if (currMembership && origMembership) { + // Membership exists in both, check for changes + const currEnabled = currMembership.enabled; + const origEnabled = origMembership.enabled; + const currPrivType = currMembership.privilegeType || 'inherit'; + const origPrivType = origMembership.privilegeType || 'inherit'; + + if (currEnabled && !origEnabled) { + // Changed from revoked to granted + const adminOpt = currPrivType === 'admin' ? ' WITH ADMIN OPTION' : ''; + if (direction === 'memberOf') { + statements.push(`GRANT ${targetRole} TO ${role}${adminOpt};`); + } else { + statements.push(`GRANT ${role} TO ${targetRole}${adminOpt};`); + } + } else if (!currEnabled && origEnabled) { + // Changed from granted to revoked + if (direction === 'memberOf') { + statements.push(`REVOKE ${targetRole} FROM ${role};`); + } else { + statements.push(`REVOKE ${role} FROM ${targetRole};`); + } + } else if (currEnabled && origEnabled && currPrivType !== origPrivType) { + // Privilege type changed while enabled + // Need to revoke and re-grant with new option + if (direction === 'memberOf') { + statements.push(`REVOKE ${targetRole} FROM ${role};`); + const adminOpt = currPrivType === 'admin' ? ' WITH ADMIN OPTION' : ''; + statements.push(`GRANT ${targetRole} TO ${role}${adminOpt};`); + } else { + statements.push(`REVOKE ${role} FROM ${targetRole};`); + const adminOpt = currPrivType === 'admin' ? ' WITH ADMIN OPTION' : ''; + statements.push(`GRANT ${role} TO ${targetRole}${adminOpt};`); + } + } + } + } + + return statements; +} + +export function buildRoleChangeStatements( + original: RoleDesignerState, + current: RoleDesignerState +): string[] { + if (!original) { + return buildRoleMigrationStatements(current); + } + + const statements: string[] = []; + + statements.push('-- Changes made to role'); + if (statements.length === 0 || statements[statements.length - 1].trim()) { + // Only add comment if not empty + } + + statements.push(...getPropertyChanges(original, current)); + statements.push(...getDatabasePrivilegeChanges(original, current)); + statements.push(...getSchemaPrivilegeChanges(original, current)); + statements.push(...getDefaultTablePrivilegeChanges(original, current)); + statements.push(...getTablePrivilegeChanges(original, current)); + statements.push(...getMembershipChanges(current.roleName, original.memberOf, current.memberOf, 'memberOf')); + statements.push(...getMembershipChanges(current.roleName, original.members, current.members, 'members')); + + return statements.filter(Boolean); +} + +export function buildRoleChangePreviewHtml(original: RoleDesignerState, current: RoleDesignerState): string { + const statements = buildRoleChangeStatements(original, current); + + if (statements.length === 0) { + return '-- No changes detected'; + } + + const sql = statements + .join('\n\n') + .replace(/&/g, '&') + .replace(//g, '>'); + + return `-- Changes made to role

` + + sql.replace(/\n/g, '
').replace(/ /g, '  '); +} + +export function buildRoleChangeMigrationSql( + original: RoleDesignerState, + current: RoleDesignerState +): string { + const statements = buildRoleChangeStatements(original, current); + return ['BEGIN;', '', joinStatements(statements), '', 'COMMIT;'].join('\n'); +} + +export function buildRoleChangeMigrationMarkdown( + original: RoleDesignerState, + current: RoleDesignerState +): string { + const statements = buildRoleChangeStatements(original, current); + const statementCount = statements.filter(line => !line.startsWith('--') && line.trim() !== '').length; + + return `### Role Designer Changes: \`${current.roleName}\`\n\n` + + `
` + + `ℹ️ Delta Mode: This script contains only the modifications to the role configuration. Run it in a transaction for safety.
\n\n` + + `Generated **${statementCount}** SQL statement(s).`; +} diff --git a/src/services/ConnectionManager.ts b/src/services/ConnectionManager.ts index 9bf44e7..424b980 100644 --- a/src/services/ConnectionManager.ts +++ b/src/services/ConnectionManager.ts @@ -179,6 +179,17 @@ export class ConnectionManager { return client; } + + if (config.environment === 'production' && this.isSSLFailure(err)) { + const enrichedError = new Error( + `Production connection failed: ${err.message || 'SSL connection failed'}.\n\n` + + `Security Alert: PgStudio blocked automatic SSL downgrade on a Production environment to protect your credentials. ` + + `If your database does not support SSL or you are using a secure SSH tunnel, please open Connection Settings, expand Advanced Options, and explicitly set SSL Mode to "Disable — No SSL".` + ); + (enrichedError as any).code = (err as any).code; + throw enrichedError; + } + throw err; } } @@ -239,6 +250,15 @@ export class ConnectionManager { } } else { telemetry.trackEvent('connection_error', { errorCategory: this.categorizeConnectionError(err) }); + if (config.environment === 'production' && this.isSSLFailure(err)) { + const enrichedError = new Error( + `Production connection failed: ${err.message || 'SSL connection failed'}.\n\n` + + `Security Alert: PgStudio blocked automatic SSL downgrade on a Production environment to protect your credentials. ` + + `If your database does not support SSL or you are using a secure SSH tunnel, please open Connection Settings, expand Advanced Options, and explicitly set SSL Mode to "Disable — No SSL".` + ); + (enrichedError as any).code = (err as any).code; + throw enrichedError; + } throw err; } } diff --git a/src/services/NotebookParameterBank.ts b/src/services/NotebookParameterBank.ts new file mode 100644 index 0000000..fb2fdd3 --- /dev/null +++ b/src/services/NotebookParameterBank.ts @@ -0,0 +1,87 @@ +import * as vscode from 'vscode'; + +export interface NotebookParameterBankState { + [notebookUri: string]: Record; +} + +const STORAGE_KEY = 'pgstudio.notebookParameterBank.v1'; +const MAX_VALUES_PER_PARAMETER = 20; + +function readState(workspaceState?: vscode.Memento): NotebookParameterBankState { + return workspaceState?.get(STORAGE_KEY, {}) ?? {}; +} + +function writeState(workspaceState: vscode.Memento | undefined, state: NotebookParameterBankState): Promise { + if (!workspaceState) { + return Promise.resolve(); + } + + return Promise.resolve(workspaceState.update(STORAGE_KEY, state)) as Promise; +} + +function normalizeValues(values: string[]): string[] { + const next: string[] = []; + + for (const value of values) { + if (!next.includes(value)) { + next.push(value); + } + } + + return next.slice(0, MAX_VALUES_PER_PARAMETER); +} + +export function getNotebookParameterValues( + workspaceState: vscode.Memento | undefined, + notebookUri: string, + parameterKey: string +): string[] { + const state = readState(workspaceState); + const notebookState = state[notebookUri] ?? {}; + return [...(notebookState[parameterKey] ?? [])]; +} + +export async function rememberNotebookParameterValue( + workspaceState: vscode.Memento | undefined, + notebookUri: string, + parameterKey: string, + value: string +): Promise { + if (!workspaceState) { + return; + } + + const state = readState(workspaceState); + const notebookState = { ...(state[notebookUri] ?? {}) }; + const currentValues = notebookState[parameterKey] ?? []; + notebookState[parameterKey] = normalizeValues([value, ...currentValues.filter((existing) => existing !== value)]); + state[notebookUri] = notebookState; + + await writeState(workspaceState, state); +} + +export async function clearNotebookParameterValues( + workspaceState: vscode.Memento | undefined, + notebookUri: string, + parameterKey?: string +): Promise { + if (!workspaceState) { + return; + } + + const state = readState(workspaceState); + const notebookState = { ...(state[notebookUri] ?? {}) }; + + if (parameterKey) { + delete notebookState[parameterKey]; + if (Object.keys(notebookState).length > 0) { + state[notebookUri] = notebookState; + } else { + delete state[notebookUri]; + } + } else { + delete state[notebookUri]; + } + + await writeState(workspaceState, state); +} \ No newline at end of file diff --git a/src/services/QueryAnalyzer.ts b/src/services/QueryAnalyzer.ts index 008327f..fd969c5 100644 --- a/src/services/QueryAnalyzer.ts +++ b/src/services/QueryAnalyzer.ts @@ -266,7 +266,8 @@ export class QueryAnalyzer { const isDangerous = operations.length > 0; const requiresConfirmation = this.shouldRequireConfirmation( operations, - connection + connection, + query ); return { @@ -275,7 +276,7 @@ export class QueryAnalyzer { riskScore, requiresConfirmation, warningMessage: requiresConfirmation - ? this.buildWarningMessage(operations, connection) + ? this.buildWarningMessage(operations, connection, query) : undefined, }; } @@ -356,8 +357,14 @@ export class QueryAnalyzer { */ private shouldRequireConfirmation( operations: DangerousOperation[], - connection?: ConnectionConfig + connection?: ConnectionConfig, + query?: string ): boolean { + // Require confirmation for all non-read-only queries (writes) on production connections. + if (connection?.environment === 'production' && query && !this.isReadOnlyQuery(query)) { + return true; + } + // Always require confirmation for destructive operations. if (operations.some((op) => ['DROP', 'TRUNCATE', 'DELETE', 'UPDATE', 'ALTER'].includes(op.type))) { return true; @@ -416,7 +423,8 @@ export class QueryAnalyzer { */ private buildWarningMessage( operations: DangerousOperation[], - connection?: ConnectionConfig + connection?: ConnectionConfig, + query?: string ): string { const envPrefix = connection?.environment === 'production' @@ -433,6 +441,10 @@ export class QueryAnalyzer { return `• ${op.reason}${objectList}\n Impact: ${op.estimatedImpact}`; }); + if (opMessages.length === 0 && connection?.environment === 'production' && query && !this.isReadOnlyQuery(query)) { + opMessages.push('• Write/execution operation on a production database\n Impact: Potential modification to production data'); + } + return ( envPrefix + 'This query contains potentially dangerous operations:\n\n' + @@ -445,18 +457,12 @@ export class QueryAnalyzer { * Check if a query is safe for read-only mode */ public isReadOnlyQuery(query: string): boolean { - const normalizedQuery = this.normalizeQuery(query); - - // Check for any write operations - const writePatterns = [ - /\b(INSERT|UPDATE|DELETE|DROP|CREATE|ALTER|TRUNCATE|GRANT|REVOKE)\b/i, - ]; - - const hasWriteOperation = writePatterns.some((pattern) => - pattern.test(normalizedQuery) - ); - - return !hasWriteOperation; + const clean = SqlParser.stripCommentsAndStrings(query).trim(); + if (!clean) { + return true; + } + // Whitelist approach: only allow SELECT, WITH, EXPLAIN, SHOW, VALUES + return /^\s*(SELECT|WITH|EXPLAIN|SHOW|VALUES)\b/i.test(clean); } /** diff --git a/src/services/ResultCursorService.ts b/src/services/ResultCursorService.ts index d4f9c8a..83510b2 100644 --- a/src/services/ResultCursorService.ts +++ b/src/services/ResultCursorService.ts @@ -12,6 +12,9 @@ export interface SlidingWindowPayload { windowSize: number; hasMoreBefore: boolean; hasMoreAfter: boolean; + totalRows?: number; + countAttempted?: boolean; + countError?: string; } interface SessionRecord { @@ -20,6 +23,9 @@ interface SessionRecord { windowSize: number; notebookUri: string; cellUri: string; + totalRows?: number; + countAttempted?: boolean; + countError?: string; idleTimer?: NodeJS.Timeout; } @@ -156,15 +162,59 @@ export class ResultCursorService { beganOwnReadOnlyTx = false; } + // store session early so refreshIdleTimer can work if count/fetch takes long ResultCursorService.sessions.set(sessionId, { cursorQuoted, client, windowSize, notebookUri: options.notebookUri, cellUri: options.cellUri, + totalRows: undefined, + countAttempted: false, + countError: undefined, }); ResultCursorService.refreshIdleTimer(sessionId); + // Attempt to estimate total rows for UI (best-effort; errors ignored) + const countStartTime = Date.now(); + let sessionRecord = ResultCursorService.sessions.get(sessionId); + if (sessionRecord) sessionRecord.countAttempted = true; + + let countSql: string | undefined; + try { + console.log(`[ResultCursorService] Starting row count for session ${sessionId.substring(0, 8)}`); + // Strip comments from inner SQL to avoid syntax errors in wrapped COUNT query + const innerSqlClean = stripSqlComments(innerSql); + countSql = `SELECT COUNT(*) AS cnt FROM (${innerSqlClean}) AS pgstudio_count`; + console.log(`[ResultCursorService] COUNT query: ${countSql.substring(0, 120)}...`); + + const cres = await client.query(countSql); + const countDuration = Date.now() - countStartTime; + + const cntVal = cres?.rows?.[0]?.cnt ?? cres?.rows?.[0]?.count; + const n = cntVal !== undefined && cntVal !== null ? Number(cntVal) : undefined; + + sessionRecord = ResultCursorService.sessions.get(sessionId); + if (sessionRecord) { + sessionRecord.totalRows = Number.isFinite(n) ? n : undefined; + console.log(`[ResultCursorService] Row count succeeded: ${sessionRecord.totalRows} rows (${countDuration}ms) for session ${sessionId.substring(0, 8)}`); + } + } catch (e) { + const countDuration = Date.now() - countStartTime; + const errorMsg = e instanceof Error ? e.message : String(e); + sessionRecord = ResultCursorService.sessions.get(sessionId); + if (sessionRecord) { + sessionRecord.countError = errorMsg; + const queryPreview = countSql ? countSql.substring(0, 150) : 'unknown'; + console.warn( + `[ResultCursorService] Row count failed after ${countDuration}ms for session ${sessionId.substring(0, 8)}:\n` + + ` Error: ${errorMsg}\n` + + ` Query: ${queryPreview}...`, + e instanceof Error ? e.stack : '' + ); + } + } + let page: { rows: any[]; fields: Array<{ name: string; dataTypeID: number }> } | null; try { page = await ResultCursorService.fetchWindowInternal(sessionId, 1); @@ -181,6 +231,11 @@ export class ResultCursorService { const hasMoreBefore = false; const hasMoreAfter = page.rows.length === windowSize; + const srec = ResultCursorService.sessions.get(sessionId); + const totalRows = srec?.totalRows; + const countAttempted = srec?.countAttempted ?? false; + const countError = srec?.countError; + return { sessionId, rows: page.rows, @@ -191,6 +246,9 @@ export class ResultCursorService { windowSize, hasMoreBefore, hasMoreAfter, + totalRows, + countAttempted, + countError, }, }; } catch (e) { @@ -239,6 +297,9 @@ export class ResultCursorService { windowSize: number; hasMoreBefore: boolean; hasMoreAfter: boolean; + totalRows?: number; + countAttempted?: boolean; + countError?: string; } | null> { const s = ResultCursorService.sessions.get(sessionId); if (!s) { @@ -259,6 +320,9 @@ export class ResultCursorService { windowSize: s.windowSize, hasMoreBefore, hasMoreAfter, + totalRows: s.totalRows, + countAttempted: s.countAttempted, + countError: s.countError, }; } catch (e) { console.error('[ResultCursorService] fetchPage failed:', e); diff --git a/src/services/handlers/CoreHandlers.ts b/src/services/handlers/CoreHandlers.ts index b7ab7d5..388c547 100644 --- a/src/services/handlers/CoreHandlers.ts +++ b/src/services/handlers/CoreHandlers.ts @@ -6,6 +6,8 @@ import { PostgresMetadata } from '../../common/types'; import { DatabaseTreeItem } from '../../providers/DatabaseTreeProvider'; import { ConnectionManager } from '../ConnectionManager'; import { errorResponse, okResponse } from './messaging'; +import { extensionContext } from '../../extension'; +import { QueryAnalyzer } from '../../services/QueryAnalyzer'; export class ShowConnectionSwitcherHandler implements IMessageHandler { constructor(private statusBar: any) { } @@ -278,7 +280,7 @@ export class ImportRequestHandler implements IMessageHandler { async handle(message: any, context: { editor: vscode.NotebookEditor }) { if (!context.editor) return; - const metadata = context.editor.notebook.metadata; + const metadata = context.editor.notebook.metadata as PostgresMetadata; const connectionId = metadata?.connectionId; if (!connectionId) { vscode.window.showErrorMessage('No active connection found for this notebook.'); @@ -291,13 +293,36 @@ export class ImportRequestHandler implements IMessageHandler { return; } + const notebookKey = `activeProfile-${context.editor.notebook.uri.toString()}`; + const activeProfileContext = extensionContext?.globalState.get(notebookKey); + + const connectionConfig = { + ...connection, + database: metadata?.databaseName || connection.database + }; + + // Apply profile overrides with floor protection + let readOnlyMode = connection.readOnlyMode === true; + if (metadata?.readOnlyMode !== undefined) { + readOnlyMode = readOnlyMode || metadata.readOnlyMode; + } + if (activeProfileContext?.readOnlyMode !== undefined) { + readOnlyMode = readOnlyMode || activeProfileContext.readOnlyMode; + } + connectionConfig.readOnlyMode = readOnlyMode; + + if (connectionConfig.readOnlyMode) { + vscode.window.showErrorMessage('Import operations are not allowed in read-only mode'); + return; + } + const { data } = message; if (!data || !Array.isArray(data) || data.length === 0) { vscode.window.showWarningMessage('No data received for import.'); return; } - await performImport(message, connection); + await performImport(message, connectionConfig); } } @@ -532,11 +557,6 @@ export class ShowErrorMessageHandler implements IMessageHandler { } export class RunDerivedQueryHandler implements IMessageHandler { - private isReadOnlyQuery(query: string): boolean { - const clean = query.replace(/--.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '').trim(); - return /^\s*(SELECT|WITH)\b/i.test(clean); - } - async handle(message: any, context: { editor?: vscode.NotebookEditor }) { if (!context.editor) { return; @@ -547,7 +567,7 @@ export class RunDerivedQueryHandler implements IMessageHandler { vscode.window.showErrorMessage('No derived query provided.'); return; } - if (!this.isReadOnlyQuery(sql)) { + if (!QueryAnalyzer.getInstance().isReadOnlyQuery(sql)) { vscode.window.showErrorMessage('Only SELECT/WITH derived queries are allowed.'); return; } diff --git a/src/services/handlers/CursorWindowHandler.ts b/src/services/handlers/CursorWindowHandler.ts index 76b3882..8a29718 100644 --- a/src/services/handlers/CursorWindowHandler.ts +++ b/src/services/handlers/CursorWindowHandler.ts @@ -67,6 +67,9 @@ export class CursorWindowHandler implements IMessageHandler { windowSize: page.windowSize, hasMoreBefore: page.hasMoreBefore, hasMoreAfter: page.hasMoreAfter, + totalRows: (page as any).totalRows, + countAttempted: (page as any).countAttempted, + countError: (page as any).countError, }, }, { contextLabel: 'Cursor window', notifyOnFailure: false }, diff --git a/src/services/handlers/QueryHandlers.ts b/src/services/handlers/QueryHandlers.ts index 7232986..835e0a4 100644 --- a/src/services/handlers/QueryHandlers.ts +++ b/src/services/handlers/QueryHandlers.ts @@ -6,6 +6,7 @@ import { ErrorHandlers } from '../../commands/helper'; import { ConnectionUtils } from '../../utils/connectionUtils'; import { SqlExecutor } from '../../providers/kernel/SqlExecutor'; import { safelyPostMessage } from './messaging'; +import { extensionContext } from '../../extension'; export { FkLookupHandler } from './FkLookupHandler'; function quoteIdentifier(identifier: string): string { @@ -51,15 +52,33 @@ export class ExecuteUpdateBackgroundHandler implements IMessageHandler { throw new Error('No connection in notebook metadata'); } + const connection = ConnectionUtils.findConnection(metadata.connectionId); + if (!connection) { + throw new Error('Connection not found'); + } + + const notebookKey = `activeProfile-${notebook.uri.toString()}`; + const activeProfileContext = extensionContext?.globalState.get(notebookKey); + const connectionConfig = { - id: metadata.connectionId, - name: metadata.host, - host: metadata.host, - port: metadata.port, - username: metadata.username, - database: metadata.databaseName + ...connection, + database: metadata.databaseName || connection.database }; + // Apply profile overrides with floor protection + let readOnlyMode = connection.readOnlyMode === true; + if (metadata.readOnlyMode !== undefined) { + readOnlyMode = readOnlyMode || metadata.readOnlyMode; + } + if (activeProfileContext?.readOnlyMode !== undefined) { + readOnlyMode = readOnlyMode || activeProfileContext.readOnlyMode; + } + connectionConfig.readOnlyMode = readOnlyMode; + + if (connectionConfig.readOnlyMode) { + throw new Error('Write operations are not allowed in read-only mode'); + } + client = await ConnectionManager.getInstance().getPooledClient(connectionConfig); let successCount = 0; @@ -147,11 +166,28 @@ export class DeleteRowsHandler implements IMessageHandler { const connection = ConnectionUtils.findConnection(metadata.connectionId); if (!connection) throw new Error('Connection not found'); + const notebookKey = `activeProfile-${notebook.uri.toString()}`; + const activeProfileContext = extensionContext?.globalState.get(notebookKey); + const config = { ...connection, database: metadata.databaseName || connection.database }; + // Apply profile overrides with floor protection + let readOnlyMode = connection.readOnlyMode === true; + if (metadata.readOnlyMode !== undefined) { + readOnlyMode = readOnlyMode || metadata.readOnlyMode; + } + if (activeProfileContext?.readOnlyMode !== undefined) { + readOnlyMode = readOnlyMode || activeProfileContext.readOnlyMode; + } + config.readOnlyMode = readOnlyMode; + + if (config.readOnlyMode) { + throw new Error('Write operations are not allowed in read-only mode'); + } + client = await ConnectionManager.getInstance().getSessionClient(config, notebook.uri.toString()); const quotedSchema = quoteIdentifier(schema); @@ -253,15 +289,31 @@ export class SaveChangesHandler implements IMessageHandler { } // Use ConnectionManager to get a pooled client + const connection = ConnectionUtils.findConnection(metadata.connectionId); + if (!connection) throw new Error('Connection not found'); + + const notebookKey = `activeProfile-${notebook.uri.toString()}`; + const activeProfileContext = extensionContext?.globalState.get(notebookKey); + const connectionConfig = { - id: metadata.connectionId, - name: metadata.host, - host: metadata.host, - port: metadata.port, - username: metadata.username, - database: metadata.databaseName + ...connection, + database: metadata.databaseName || connection.database }; + // Apply profile overrides with floor protection + let readOnlyMode = connection.readOnlyMode === true; + if (metadata.readOnlyMode !== undefined) { + readOnlyMode = readOnlyMode || metadata.readOnlyMode; + } + if (activeProfileContext?.readOnlyMode !== undefined) { + readOnlyMode = readOnlyMode || activeProfileContext.readOnlyMode; + } + connectionConfig.readOnlyMode = readOnlyMode; + + if (connectionConfig.readOnlyMode) { + throw new Error('Write operations are not allowed in read-only mode'); + } + client = await ConnectionManager.getInstance().getPooledClient(connectionConfig); const quotedSchema = quoteIdentifier(schema); diff --git a/src/test/unit/NotebookParameterBank.test.ts b/src/test/unit/NotebookParameterBank.test.ts new file mode 100644 index 0000000..a076ce8 --- /dev/null +++ b/src/test/unit/NotebookParameterBank.test.ts @@ -0,0 +1,47 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import { + clearNotebookParameterValues, + getNotebookParameterValues, + rememberNotebookParameterValue, +} from '../../services/NotebookParameterBank'; + +function createWorkspaceState(initialState: Record = {}) { + const state = { ...initialState }; + return { + get: (key: string, defaultValue?: T) => (key in state ? state[key] : defaultValue), + update: sinon.stub().callsFake(async (key: string, value: any) => { + state[key] = value; + }), + state, + }; +} + +describe('NotebookParameterBank', () => { + it('keeps parameter values notebook-local and most-recent first', async () => { + const workspaceState = createWorkspaceState(); + + await rememberNotebookParameterValue(workspaceState as any, 'notebook-a', 'named:customer_id', '10'); + await rememberNotebookParameterValue(workspaceState as any, 'notebook-a', 'named:customer_id', '12'); + await rememberNotebookParameterValue(workspaceState as any, 'notebook-b', 'named:customer_id', '99'); + + expect(getNotebookParameterValues(workspaceState as any, 'notebook-a', 'named:customer_id')).to.deep.equal(['12', '10']); + expect(getNotebookParameterValues(workspaceState as any, 'notebook-b', 'named:customer_id')).to.deep.equal(['99']); + }); + + it('clears one parameter bucket without affecting the rest of the notebook', async () => { + const workspaceState = createWorkspaceState({ + 'pgstudio.notebookParameterBank.v1': { + 'notebook-a': { + 'named:customer_id': ['10'], + 'named:status': ['active'], + }, + }, + }); + + await clearNotebookParameterValues(workspaceState as any, 'notebook-a', 'named:customer_id'); + + expect(getNotebookParameterValues(workspaceState as any, 'notebook-a', 'named:customer_id')).to.deep.equal([]); + expect(getNotebookParameterValues(workspaceState as any, 'notebook-a', 'named:status')).to.deep.equal(['active']); + }); +}); \ No newline at end of file diff --git a/src/test/unit/SqlCompletionProvider.test.ts b/src/test/unit/SqlCompletionProvider.test.ts index 435cea4..e8012d3 100644 --- a/src/test/unit/SqlCompletionProvider.test.ts +++ b/src/test/unit/SqlCompletionProvider.test.ts @@ -179,6 +179,34 @@ describe('SqlCompletionProvider', () => { expect(getPooledClientStub.calledOnce).to.be.true; }); + it('loads materialized view columns into qualified completions', async () => { + (getConfigurationStub as sinon.SinonStub).returns({ + get: (key: string) => (key === 'postgresExplorer.connections' + ? [{ id: 'conn-1', name: 'Main', host: 'localhost', port: 5432, username: 'postgres' }] + : undefined) + } as any); + + setupCacheResults( + [ + { schema: 'public', object_name: 'sales_mv', object_type: 'materialized view' } + ], + [ + { schema: 'public', table_name: 'sales_mv', column_name: 'id', data_type: 'integer' }, + { schema: 'public', table_name: 'sales_mv', column_name: 'total', data_type: 'numeric' } + ] + ); + + const provider = new SqlCompletionProvider(); + const sql = 'SELECT * FROM public.sales_mv m WHERE m.'; + const document = createNotebookCellDocument(sql); + attachNotebook(document, { connectionId: 'conn-1', databaseName: 'appdb' }); + + await provider.warmCache('conn-1', 'appdb'); + + const columnQuery = queryStub.getCalls().map(call => String(call.args[0])).find(sql => sql.includes('pg_attribute')); + expect(columnQuery).to.contain("c.relkind IN ('r', 'p', 'v', 'm', 'f')"); + }); + it('deduplicates repeated database objects before returning completions', async () => { (getConfigurationStub as sinon.SinonStub).returns({ get: (key: string) => (key === 'postgresExplorer.connections' diff --git a/src/test/unit/SqlExecutor.parameterBank.test.ts b/src/test/unit/SqlExecutor.parameterBank.test.ts new file mode 100644 index 0000000..6801106 --- /dev/null +++ b/src/test/unit/SqlExecutor.parameterBank.test.ts @@ -0,0 +1,81 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; +import * as vscode from 'vscode'; +import * as extensionModule from '../../extension'; +import { SqlExecutor } from '../../providers/kernel/SqlExecutor'; + +function createWorkspaceState(initialState: Record = {}) { + const state = { ...initialState }; + return { + get: (key: string, defaultValue?: T) => (key in state ? state[key] : defaultValue), + update: sinon.stub().callsFake(async (key: string, value: any) => { + state[key] = value; + }), + state, + }; +} + +describe('SqlExecutor - notebook parameter bank', () => { + let sandbox: sinon.SinonSandbox; + let executor: SqlExecutor; + let workspaceState: ReturnType; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + executor = new SqlExecutor({} as any); + workspaceState = createWorkspaceState(); + (extensionModule as any).extensionContext = { workspaceState: workspaceState as any }; + + sandbox.stub(vscode.workspace, 'getConfiguration').callsFake((section?: string) => { + if (section === 'postgresExplorer.parameters') { + return { + get: (key: string, defaultValue?: unknown) => { + if (key === 'cacheLastValues') { + return true; + } + if (key === 'nullSentinel') { + return 'NULL'; + } + return defaultValue; + }, + } as any; + } + return { get: (_key: string, defaultValue?: unknown) => defaultValue } as any; + }); + }); + + afterEach(() => { + sandbox.restore(); + (extensionModule as any).extensionContext = undefined; + }); + + it('reuses a previous value from the same notebook', async () => { + workspaceState.state['pgstudio.notebookParameterBank.v1'] = { + 'vscode-notebook:test': { + 'named:color': ['blue', 'green'], + }, + }; + + const quickPick = sandbox.stub(vscode.window, 'showQuickPick').resolves({ label: 'blue', value: 'blue' } as any); + const inputBox = sandbox.stub(vscode.window, 'showInputBox'); + + const values = await (executor as any).promptForNamedParameterValues('vscode-notebook:test', ['color']); + + expect(values).to.deep.equal(['blue']); + expect(quickPick.calledOnce).to.be.true; + expect(inputBox.called).to.be.false; + expect(workspaceState.state['pgstudio.notebookParameterBank.v1']['vscode-notebook:test']['named:color'][0]).to.equal('blue'); + }); + + it('stores a new value in the notebook bank when no history exists', async () => { + const quickPick = sandbox.stub(vscode.window, 'showQuickPick'); + const inputBox = sandbox.stub(vscode.window, 'showInputBox').resolves('acme'); + + const values = await (executor as any).promptForNamedParameterValues('vscode-notebook:test', ['customer']); + + expect(values).to.deep.equal(['acme']); + expect(quickPick.called).to.be.false; + expect(inputBox.calledOnce).to.be.true; + expect(workspaceState.state['pgstudio.notebookParameterBank.v1']['vscode-notebook:test']['named:customer']).to.deep.equal(['acme']); + }); +}); \ No newline at end of file diff --git a/src/ui/renderer/queryResult/renderQueryResult.ts b/src/ui/renderer/queryResult/renderQueryResult.ts index 8a54ffa..9995dfc 100644 --- a/src/ui/renderer/queryResult/renderQueryResult.ts +++ b/src/ui/renderer/queryResult/renderQueryResult.ts @@ -342,7 +342,19 @@ export function renderPostgresNotebookResult( let text: string; if (slideMeta) { const lastRow = slideMeta.windowStartRow + Math.max(currentRows.length, 1) - 1; - text = `${slideMeta.windowStartRow.toLocaleString()}–${lastRow.toLocaleString()} · window ${slideMeta.windowSize.toLocaleString()} · streaming`; + if (typeof slideMeta.totalRows === 'number') { + text = `${slideMeta.windowStartRow.toLocaleString()}–${lastRow.toLocaleString()} of ${slideMeta.totalRows.toLocaleString()} · window ${slideMeta.windowSize.toLocaleString()} · streaming`; + } else { + text = `${slideMeta.windowStartRow.toLocaleString()}–${lastRow.toLocaleString()} · window ${slideMeta.windowSize.toLocaleString()} · streaming`; + // Add diagnostic info for why total is unknown + if ((slideMeta as any).countAttempted) { + if ((slideMeta as any).countError) { + text += ` (total count failed: ${(slideMeta as any).countError.substring(0, 50)})`; + } else { + text += ' (total row count in progress)'; + } + } + } if (executionTime !== undefined) { const ms = Math.round(executionTime * 1000); text += ms >= 1000 ? ` · ${executionTime.toFixed(2)}s` : ` · ${ms}ms`; @@ -363,7 +375,20 @@ export function renderPostgresNotebookResult( : slideMeta ? (() => { const lastRow = slideMeta.windowStartRow + Math.max(rows?.length ?? 0, 1) - 1; - let t = `${slideMeta.windowStartRow.toLocaleString()}–${lastRow.toLocaleString()} · window ${slideMeta.windowSize.toLocaleString()} · streaming`; + let t: string; + if (typeof slideMeta.totalRows === 'number') { + t = `${slideMeta.windowStartRow.toLocaleString()}–${lastRow.toLocaleString()} of ${slideMeta.totalRows.toLocaleString()} · window ${slideMeta.windowSize.toLocaleString()} · streaming`; + } else { + t = `${slideMeta.windowStartRow.toLocaleString()}–${lastRow.toLocaleString()} · window ${slideMeta.windowSize.toLocaleString()} · streaming`; + // Add diagnostic info for why total is unknown + if ((slideMeta as any).countAttempted) { + if ((slideMeta as any).countError) { + t += ` (total count failed: ${(slideMeta as any).countError.substring(0, 50)})`; + } else { + t += ' (total row count in progress)'; + } + } + } if (executionTime !== undefined) { const ms = Math.round(executionTime * 1000); t += ms >= 1000 ? ` · ${executionTime.toFixed(2)}s` : ` · ${ms}ms`; @@ -1072,6 +1097,7 @@ export function renderPostgresNotebookResult( windowSize: getSlideWindowSize(), hasMoreBefore: slideHasMoreBefore, hasMoreAfter: slideHasMoreAfter, + totalRows: slideMeta.totalRows, }; }; @@ -1203,6 +1229,13 @@ export function renderPostgresNotebookResult( slideHasMoreBefore = message.slidingWindow.hasMoreBefore; slideHasMoreAfter = message.slidingWindow.hasMoreAfter; } + if (typeof message.slidingWindow.totalRows === 'number') { + // preserve known total rows on incoming window updates + slideMeta = { + ...(slideMeta ?? {}), + totalRows: message.slidingWindow.totalRows, + } as any; + } if (slideBufferedStartRow > 1) { slideHasMoreBefore = true; } diff --git a/templates/connection-form/index.html b/templates/connection-form/index.html index f6658d7..ead4abf 100644 --- a/templates/connection-form/index.html +++ b/templates/connection-form/index.html @@ -123,6 +123,15 @@

{{HEADER_TITLE}}

+ + +
+ ⚠️ +
+
Production Database Connection
+
It is highly recommended to enable Read-Only Mode to prevent accidental modifications or deletes. Enable Read-Only Mode
+
+
diff --git a/templates/connection-form/scripts.js b/templates/connection-form/scripts.js index 18c137d..a90c42d 100644 --- a/templates/connection-form/scripts.js +++ b/templates/connection-form/scripts.js @@ -144,19 +144,114 @@ document.getElementById('cloudAuthKind').addEventListener('change', (event) => { }); }); +// ── Production warning banner logic ─────────────────────────────────────────── +const environmentSelect = document.getElementById('environment'); +const readOnlyCheckbox = document.getElementById('readOnlyMode'); +const productionWarningDiv = document.getElementById('productionWarning'); +const enableReadOnlyLink = document.getElementById('enableReadOnlyLink'); + +function updateProductionWarning() { + const env = environmentSelect.value; + const isReadOnly = readOnlyCheckbox.checked; + if (env === 'production' && !isReadOnly) { + productionWarningDiv.style.display = 'flex'; + } else { + productionWarningDiv.style.display = 'none'; + } +} + +environmentSelect.addEventListener('change', updateProductionWarning); +readOnlyCheckbox.addEventListener('change', updateProductionWarning); + +enableReadOnlyLink.addEventListener('click', (e) => { + e.preventDefault(); + readOnlyCheckbox.checked = true; + updateProductionWarning(); + // Trigger input listener on readOnlyMode to reset tested state if applicable + readOnlyCheckbox.dispatchEvent(new Event('input', { bubbles: true })); +}); + +// Run initial check (after form population or on page load) +updateProductionWarning(); + // ── Message helpers ─────────────────────────────────────────────────────────── -function showMessage(text, type = 'info') { - const icons = { success: '✓', error: '✗', info: 'ℹ' }; +function showMessage(text, type = 'info', actions = []) { + const icons = { success: '✓', error: '✗', info: 'ℹ', warning: '⚠' }; messageDiv.className = 'message ' + type; messageDiv.style.display = 'flex'; + messageDiv.style.alignItems = actions.length ? 'flex-start' : 'center'; while (messageDiv.firstChild) { messageDiv.removeChild(messageDiv.firstChild); } + const iconSpan = document.createElement('span'); iconSpan.className = 'message-icon'; - iconSpan.textContent = icons[type]; + iconSpan.textContent = icons[type] || icons.info; + + const content = document.createElement('div'); + content.className = 'message-content'; const textSpan = document.createElement('span'); textSpan.textContent = text; + content.appendChild(textSpan); + messageDiv.appendChild(iconSpan); - messageDiv.appendChild(textSpan); + messageDiv.appendChild(content); + + if (actions.length) { + const actionsDiv = document.createElement('div'); + actionsDiv.className = 'message-actions'; + for (const action of actions) { + const button = document.createElement('button'); + button.type = 'button'; + button.className = action.className || 'btn-secondary'; + button.textContent = action.label; + button.title = action.title || action.label; + button.addEventListener('click', action.onClick); + actionsDiv.appendChild(button); + } + messageDiv.appendChild(actionsDiv); + } +} + +function ensureAdvancedOptionsVisible() { + const section = document.getElementById('advanced-section'); + const arrow = document.getElementById('advanced-arrow'); + if (section.style.display === 'none' || !section.style.display) { + section.style.display = 'block'; + arrow.style.transform = 'rotate(180deg)'; + } +} + +function isSslDowngradeError(errorText) { + return /blocked automatic SSL downgrade/i.test(errorText || '') || + /explicitly set SSL Mode to "Disable — No SSL"/i.test(errorText || ''); +} + +function showSslDisableFixMessage(rawError) { + const sslSelect = document.getElementById('sslmode'); + + showMessage( + rawError, + 'error', + [ + { + label: 'Set SSL Mode to Disable', + title: 'Switch SSL mode to Disable — No SSL', + onClick: () => { + sslSelect.value = 'disable'; + ensureAdvancedOptionsVisible(); + updateSSLCertFields(); + isTested = false; + if (!isEditMode) { + addBtn.disabled = true; + } + showMessage( + 'SSL mode set to Disable — No SSL. Retest the connection before saving.', + 'warning' + ); + sslSelect.focus(); + } + } + ] + ); } function hideMessage() { @@ -257,7 +352,11 @@ window.addEventListener('message', event => { break; } case 'testError': - showMessage(message.error || 'Connection failed', 'error'); + if (isSslDowngradeError(message.error)) { + showSslDisableFixMessage(message.error || 'Connection failed'); + } else { + showMessage(message.error || 'Connection failed', 'error'); + } isTested = false; // In edit mode keep save enabled even after a failed test if (!isEditMode) { addBtn.disabled = true; } diff --git a/templates/connection-form/styles.css b/templates/connection-form/styles.css index d99dc24..f3cda08 100644 --- a/templates/connection-form/styles.css +++ b/templates/connection-form/styles.css @@ -123,6 +123,21 @@ body { animation: slideDown 0.2s ease; } +.message-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 4px; +} + +.message-actions { + display: flex; + align-items: flex-start; + gap: 8px; + margin-left: auto; + flex-wrap: wrap; +} + @keyframes slideDown { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } @@ -149,6 +164,15 @@ body { border: 1px solid rgba(96, 165, 250, 0.3); color: var(--accent-color); } +.message.warning { + background: rgba(245, 158, 11, 0.1); + border: 1px solid rgba(245, 158, 11, 0.38); + color: var(--vscode-editorWarning-foreground, #f59e0b); +} + +.message.warning .message-actions button { + border-color: rgba(245, 158, 11, 0.45); +} /* ── Actions ── */ .actions { @@ -405,3 +429,43 @@ button:disabled { button[title] { cursor: pointer; } .hidden { display: none !important; } + +/* ── Production warning banner ── */ +.production-warning { + display: none; + grid-column: span 2; + align-items: flex-start; + gap: 10px; + padding: 12px 14px; + background: rgba(239, 68, 68, 0.08); + border: 1px solid rgba(239, 68, 68, 0.3); + border-radius: 6px; + font-size: 12px; + color: var(--danger-color); + animation: slideDown 0.2s ease; + margin-top: 4px; +} + +.production-warning-content { + display: flex; + flex-direction: column; + gap: 4px; +} + +.production-warning-title { + font-weight: 600; + display: flex; + align-items: center; + gap: 6px; +} + +.production-warning-link { + color: var(--accent-color); + text-decoration: none; + font-weight: 500; + cursor: pointer; +} + +.production-warning-link:hover { + text-decoration: underline; +} diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..4ceb8cc --- /dev/null +++ b/vercel.json @@ -0,0 +1,13 @@ +{ + "outputDirectory": "docs", + "functions": { + "api/*.js": { + "memory": 128, + "maxDuration": 10 + } + }, + "rewrites": [ + { "source": "/api/(.*)", "destination": "/api/$1" }, + { "source": "/(.*)", "destination": "/$1" } + ] +}