From cf8121ce281c9a7633027031b4e68419d7c99706 Mon Sep 17 00:00:00 2001 From: cB-Guru-Sharan-Kumar-Ram Date: Fri, 3 Jul 2026 16:52:19 +0530 Subject: [PATCH 1/2] docs: add VitePress documentation site with GitHub Pages deploy --- .github/dependabot.yml | 8 + .github/workflows/deploy-site.yml | 60 + site/.gitignore | 6 + site/README.md | 19 + site/docs/.vitepress/config.mjs | 177 ++ site/docs/.vitepress/shims/crypto.js | 35 + site/docs/.vitepress/theme/ConsoleHome.vue | 167 ++ .../theme/components/Capabilities.vue | 245 ++ .../.vitepress/theme/components/DataTable.vue | 200 ++ .../theme/components/DialectShowcase.vue | 170 ++ .../theme/components/FeatureIcon.vue | 74 + .../theme/components/HeroConsole.vue | 543 ++++ .../.vitepress/theme/components/Pipeline.vue | 121 + .../theme/components/Playground.vue | 591 +++++ .../theme/components/SampleDataDemo.vue | 717 +++++ .../theme/fonts/geist-mono-400.woff2 | Bin 0 -> 12792 bytes .../theme/fonts/geist-mono-600.woff2 | Bin 0 -> 13388 bytes .../theme/fonts/geist-sans-400.woff2 | Bin 0 -> 33400 bytes .../theme/fonts/geist-sans-500.woff2 | Bin 0 -> 34716 bytes .../theme/fonts/space-grotesk-500.woff2 | Bin 0 -> 13312 bytes .../theme/fonts/space-grotesk-700.woff2 | Bin 0 -> 12840 bytes site/docs/.vitepress/theme/index.js | 28 + site/docs/.vitepress/theme/styles/base.css | 181 ++ site/docs/.vitepress/theme/styles/code.css | 65 + site/docs/.vitepress/theme/styles/fonts.css | 47 + site/docs/.vitepress/theme/styles/home.css | 16 + site/docs/.vitepress/theme/styles/nav.css | 76 + site/docs/.vitepress/theme/styles/sidebar.css | 43 + site/docs/.vitepress/theme/styles/tokens.css | 103 + site/docs/.vitepress/theme/utils/demoData.js | 326 +++ site/docs/.vitepress/theme/utils/sql.js | 33 + site/docs/api/reference.md | 199 ++ site/docs/dialects/mysql.md | 75 + site/docs/dialects/postgres.md | 121 + site/docs/examples/cursor.md | 96 + site/docs/examples/filtering.md | 225 ++ site/docs/examples/joined-table.md | 84 + site/docs/examples/multi-table-union.md | 57 + site/docs/examples/pagination.md | 120 + site/docs/examples/sample-data.md | 10 + site/docs/examples/searching.md | 114 + site/docs/examples/single-table.md | 58 + site/docs/examples/sorting.md | 95 + site/docs/guide/getting-started.md | 111 + site/docs/guide/installation.md | 52 + site/docs/guide/migration.md | 79 + site/docs/index.md | 9 + site/docs/playground.md | 35 + site/docs/public/favicon.svg | 16 + site/docs/public/logo.svg | 17 + site/docs/v2/constructor.md | 57 + site/docs/v2/cursor-pagination.md | 172 ++ site/docs/v2/filters-and-operators.md | 114 + site/docs/v2/overview.md | 76 + site/docs/v2/paginate.md | 124 + site/docs/v2/return-shape.md | 60 + site/docs/v2/search.md | 52 + site/docs/v2/sorting.md | 50 + site/package-lock.json | 2359 +++++++++++++++++ site/package.json | 19 + site/scripts/build-lib.mjs | 33 + site/scripts/gen-demo-sql.mjs | 19 + 62 files changed, 8759 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/deploy-site.yml create mode 100644 site/.gitignore create mode 100644 site/README.md create mode 100644 site/docs/.vitepress/config.mjs create mode 100644 site/docs/.vitepress/shims/crypto.js create mode 100644 site/docs/.vitepress/theme/ConsoleHome.vue create mode 100644 site/docs/.vitepress/theme/components/Capabilities.vue create mode 100644 site/docs/.vitepress/theme/components/DataTable.vue create mode 100644 site/docs/.vitepress/theme/components/DialectShowcase.vue create mode 100644 site/docs/.vitepress/theme/components/FeatureIcon.vue create mode 100644 site/docs/.vitepress/theme/components/HeroConsole.vue create mode 100644 site/docs/.vitepress/theme/components/Pipeline.vue create mode 100644 site/docs/.vitepress/theme/components/Playground.vue create mode 100644 site/docs/.vitepress/theme/components/SampleDataDemo.vue create mode 100644 site/docs/.vitepress/theme/fonts/geist-mono-400.woff2 create mode 100644 site/docs/.vitepress/theme/fonts/geist-mono-600.woff2 create mode 100644 site/docs/.vitepress/theme/fonts/geist-sans-400.woff2 create mode 100644 site/docs/.vitepress/theme/fonts/geist-sans-500.woff2 create mode 100644 site/docs/.vitepress/theme/fonts/space-grotesk-500.woff2 create mode 100644 site/docs/.vitepress/theme/fonts/space-grotesk-700.woff2 create mode 100644 site/docs/.vitepress/theme/index.js create mode 100644 site/docs/.vitepress/theme/styles/base.css create mode 100644 site/docs/.vitepress/theme/styles/code.css create mode 100644 site/docs/.vitepress/theme/styles/fonts.css create mode 100644 site/docs/.vitepress/theme/styles/home.css create mode 100644 site/docs/.vitepress/theme/styles/nav.css create mode 100644 site/docs/.vitepress/theme/styles/sidebar.css create mode 100644 site/docs/.vitepress/theme/styles/tokens.css create mode 100644 site/docs/.vitepress/theme/utils/demoData.js create mode 100644 site/docs/.vitepress/theme/utils/sql.js create mode 100644 site/docs/api/reference.md create mode 100644 site/docs/dialects/mysql.md create mode 100644 site/docs/dialects/postgres.md create mode 100644 site/docs/examples/cursor.md create mode 100644 site/docs/examples/filtering.md create mode 100644 site/docs/examples/joined-table.md create mode 100644 site/docs/examples/multi-table-union.md create mode 100644 site/docs/examples/pagination.md create mode 100644 site/docs/examples/sample-data.md create mode 100644 site/docs/examples/searching.md create mode 100644 site/docs/examples/single-table.md create mode 100644 site/docs/examples/sorting.md create mode 100644 site/docs/guide/getting-started.md create mode 100644 site/docs/guide/installation.md create mode 100644 site/docs/guide/migration.md create mode 100644 site/docs/index.md create mode 100644 site/docs/playground.md create mode 100644 site/docs/public/favicon.svg create mode 100644 site/docs/public/logo.svg create mode 100644 site/docs/v2/constructor.md create mode 100644 site/docs/v2/cursor-pagination.md create mode 100644 site/docs/v2/filters-and-operators.md create mode 100644 site/docs/v2/overview.md create mode 100644 site/docs/v2/paginate.md create mode 100644 site/docs/v2/return-shape.md create mode 100644 site/docs/v2/search.md create mode 100644 site/docs/v2/sorting.md create mode 100644 site/package-lock.json create mode 100644 site/package.json create mode 100644 site/scripts/build-lib.mjs create mode 100644 site/scripts/gen-demo-sql.mjs diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..211a005 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,8 @@ +version: 2 +updates: + # Site dependencies — opens a PR when a new pagi-help (or vitepress) version is published. + - package-ecosystem: npm + directory: /site + schedule: + interval: daily + open-pull-requests-limit: 5 diff --git a/.github/workflows/deploy-site.yml b/.github/workflows/deploy-site.yml new file mode 100644 index 0000000..95ee6e1 --- /dev/null +++ b/.github/workflows/deploy-site.yml @@ -0,0 +1,60 @@ +name: Deploy Site + +on: + push: + branches: [master] + paths: + - "site/**" + - ".github/workflows/deploy-site.yml" + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + defaults: + run: + working-directory: site + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: site/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build site + run: npm run docs:build + + - name: Configure Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: site/docs/.vitepress/dist + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/site/.gitignore b/site/.gitignore new file mode 100644 index 0000000..29cb39b --- /dev/null +++ b/site/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +docs/.vitepress/cache/ +docs/.vitepress/dist/ +docs/public/pagihelp.global.js +*.log +.DS_Store diff --git a/site/README.md b/site/README.md new file mode 100644 index 0000000..814183e --- /dev/null +++ b/site/README.md @@ -0,0 +1,19 @@ +# PagiHelp Documentation Site + +The documentation website for [`pagi-help`](https://github.com/Codebucket-Solutions/PagiHelp), built with [VitePress](https://vitepress.dev) — live at **https://codebucket-solutions.github.io/PagiHelp/**. + +This folder is self-contained and is **not** published to npm with the library. + +## Local development + +```bash +cd site +npm install +npm run docs:dev # start local dev server +npm run docs:build # production build -> docs/.vitepress/dist +npm run docs:preview # preview the production build +``` + +## Deployment + +Pushing changes under `site/` to `master` triggers [`deploy-site.yml`](../.github/workflows/deploy-site.yml), which builds and publishes the site to GitHub Pages. The demo/playground bundle the published `pagi-help` npm package — Dependabot opens a PR when a new version is released, and merging it updates the live site. diff --git a/site/docs/.vitepress/config.mjs b/site/docs/.vitepress/config.mjs new file mode 100644 index 0000000..b842c1d --- /dev/null +++ b/site/docs/.vitepress/config.mjs @@ -0,0 +1,177 @@ +import { defineConfig } from "vitepress"; +import { readFileSync } from "node:fs"; + +const REPO = "https://github.com/Codebucket-Solutions/PagiHelp"; + +// GitHub Pages serves under /PagiHelp/; override with DOCS_BASE (e.g. "/") for a domain root. +const base = process.env.DOCS_BASE || "/PagiHelp/"; + +// Version badge: live from the npm registry at build time, package.json as offline fallback. +const pkg = JSON.parse( + readFileSync(new URL("../../../package.json", import.meta.url), "utf8") +); +async function libVersion() { + try { + const res = await fetch("https://registry.npmjs.org/pagi-help/latest", { + signal: AbortSignal.timeout(5000), + }); + if (res.ok) return (await res.json()).version; + } catch {} + return pkg.version; +} +const version = await libVersion(); + +export default defineConfig({ + base, + title: "PagiHelp", + description: + "Pagination query builder for MySQL and PostgreSQL — filters, search, sorting, unions, and cursor pagination.", + lang: "en-US", + cleanUrls: true, + lastUpdated: true, + ignoreDeadLinks: false, + + // Dark-first identity; the appearance toggle still offers light mode. + appearance: "dark", + + markdown: { + theme: { light: "github-light", dark: "tokyo-night" }, + }, + + head: [ + ["link", { rel: "icon", href: `${base}favicon.svg`, type: "image/svg+xml" }], + ["meta", { name: "theme-color", content: "#7c5cff" }], + ["meta", { property: "og:title", content: "PagiHelp" }], + [ + "meta", + { + property: "og:description", + content: + "Pagination that compiles to SQL — for MySQL and PostgreSQL.", + }, + ], + ], + + themeConfig: { + logo: "/logo.svg", + + nav: [ + { text: "Guide", link: "/guide/getting-started", activeMatch: "/guide/" }, + { text: "API", link: "/v2/overview", activeMatch: "/v2/" }, + { text: "Dialects", link: "/dialects/mysql", activeMatch: "/dialects/" }, + { + text: "Examples", + link: "/examples/filtering", + activeMatch: "/examples/", + }, + { text: "Demo", link: "/examples/sample-data" }, + { text: "Playground", link: "/playground" }, + { text: "Reference", link: "/api/reference" }, + { + text: "v" + version, + items: [ + { text: "npm", link: "https://www.npmjs.com/package/pagi-help" }, + { text: "Migrating to v2", link: "/guide/migration" }, + ], + }, + ], + + sidebar: { + "/guide/": [ + { + text: "Introduction", + items: [ + { text: "Getting Started", link: "/guide/getting-started" }, + { text: "Installation", link: "/guide/installation" }, + ], + }, + { + text: "Migration", + items: [ + { text: "Migrating to v2", link: "/guide/migration" }, + ], + }, + ], + "/v2/": [ + { + text: "API", + items: [ + { text: "Overview", link: "/v2/overview" }, + { text: "Constructor", link: "/v2/constructor" }, + { text: "paginate()", link: "/v2/paginate" }, + { text: "Cursor Pagination", link: "/v2/cursor-pagination" }, + { + text: "Filters & Operators", + link: "/v2/filters-and-operators", + }, + { text: "Sorting", link: "/v2/sorting" }, + { text: "Search", link: "/v2/search" }, + { text: "Return Shape", link: "/v2/return-shape" }, + ], + }, + ], + "/dialects/": [ + { + text: "Dialects", + items: [ + { text: "MySQL", link: "/dialects/mysql" }, + { text: "PostgreSQL", link: "/dialects/postgres" }, + ], + }, + ], + "/examples/": [ + { + text: "By Feature", + items: [ + { text: "Filtering", link: "/examples/filtering" }, + { text: "Searching", link: "/examples/searching" }, + { text: "Sorting", link: "/examples/sorting" }, + { text: "Pagination", link: "/examples/pagination" }, + ], + }, + { + text: "By Query Shape", + items: [ + { text: "Single Table", link: "/examples/single-table" }, + { text: "Joined Table", link: "/examples/joined-table" }, + { text: "Multi-Table Union", link: "/examples/multi-table-union" }, + { text: "Cursor Pagination", link: "/examples/cursor" }, + ], + }, + { + text: "Interactive", + items: [ + { text: "Interactive Demo", link: "/examples/sample-data" }, + { text: "Playground", link: "/playground" }, + ], + }, + ], + "/api/": [ + { + text: "Reference", + items: [{ text: "API Reference", link: "/api/reference" }], + }, + ], + }, + + socialLinks: [ + { icon: "github", link: REPO, ariaLabel: "GitHub repository" }, + { + icon: { + svg: 'npm', + }, + link: "https://www.npmjs.com/package/pagi-help", + ariaLabel: "npm package", + }, + ], + + search: { + provider: "local", + }, + + footer: { + message: "Released under the MIT License.", + copyright: "Copyright © Codebucket Solutions · Author: Abhinav Gautam", + }, + }, +}); diff --git a/site/docs/.vitepress/shims/crypto.js b/site/docs/.vitepress/shims/crypto.js new file mode 100644 index 0000000..037c1e1 --- /dev/null +++ b/site/docs/.vitepress/shims/crypto.js @@ -0,0 +1,35 @@ +// Minimal browser shim for Node's `crypto` — enough for cursor fingerprints. +// paginate() (search/sort/filter/pagination) never calls this. + +function fnv1aHex(input) { + // 128-bit-ish hex by hashing the string four times with different seeds. + const seeds = [0x811c9dc5, 0x01000193, 0x9e3779b9, 0x85ebca6b]; + return seeds + .map((seed) => { + let h = seed >>> 0; + for (let i = 0; i < input.length; i++) { + h ^= input.codePointAt(i); + h = Math.imul(h, 0x01000193) >>> 0; + } + return ("00000000" + h.toString(16)).slice(-8); + }) + .join(""); +} + +function createHash() { + let buffer = ""; + return { + update(value) { + buffer += String(value); + return this; + }, + digest() { + return fnv1aHex(buffer); + }, + }; +} + +const cryptoShim = { createHash }; + +export { createHash }; +export default cryptoShim; diff --git a/site/docs/.vitepress/theme/ConsoleHome.vue b/site/docs/.vitepress/theme/ConsoleHome.vue new file mode 100644 index 0000000..1b412e7 --- /dev/null +++ b/site/docs/.vitepress/theme/ConsoleHome.vue @@ -0,0 +1,167 @@ + + + + + diff --git a/site/docs/.vitepress/theme/components/Capabilities.vue b/site/docs/.vitepress/theme/components/Capabilities.vue new file mode 100644 index 0000000..ac89743 --- /dev/null +++ b/site/docs/.vitepress/theme/components/Capabilities.vue @@ -0,0 +1,245 @@ + + + + + diff --git a/site/docs/.vitepress/theme/components/DataTable.vue b/site/docs/.vitepress/theme/components/DataTable.vue new file mode 100644 index 0000000..d6d46aa --- /dev/null +++ b/site/docs/.vitepress/theme/components/DataTable.vue @@ -0,0 +1,200 @@ + + + + + diff --git a/site/docs/.vitepress/theme/components/DialectShowcase.vue b/site/docs/.vitepress/theme/components/DialectShowcase.vue new file mode 100644 index 0000000..2c672d3 --- /dev/null +++ b/site/docs/.vitepress/theme/components/DialectShowcase.vue @@ -0,0 +1,170 @@ + + + + + diff --git a/site/docs/.vitepress/theme/components/FeatureIcon.vue b/site/docs/.vitepress/theme/components/FeatureIcon.vue new file mode 100644 index 0000000..22e79a4 --- /dev/null +++ b/site/docs/.vitepress/theme/components/FeatureIcon.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/site/docs/.vitepress/theme/components/HeroConsole.vue b/site/docs/.vitepress/theme/components/HeroConsole.vue new file mode 100644 index 0000000..374366a --- /dev/null +++ b/site/docs/.vitepress/theme/components/HeroConsole.vue @@ -0,0 +1,543 @@ + + + + + diff --git a/site/docs/.vitepress/theme/components/Pipeline.vue b/site/docs/.vitepress/theme/components/Pipeline.vue new file mode 100644 index 0000000..8015dcb --- /dev/null +++ b/site/docs/.vitepress/theme/components/Pipeline.vue @@ -0,0 +1,121 @@ + + + + + diff --git a/site/docs/.vitepress/theme/components/Playground.vue b/site/docs/.vitepress/theme/components/Playground.vue new file mode 100644 index 0000000..13c2de8 --- /dev/null +++ b/site/docs/.vitepress/theme/components/Playground.vue @@ -0,0 +1,591 @@ + + + + + diff --git a/site/docs/.vitepress/theme/components/SampleDataDemo.vue b/site/docs/.vitepress/theme/components/SampleDataDemo.vue new file mode 100644 index 0000000..d318ef1 --- /dev/null +++ b/site/docs/.vitepress/theme/components/SampleDataDemo.vue @@ -0,0 +1,717 @@ + + + + + diff --git a/site/docs/.vitepress/theme/fonts/geist-mono-400.woff2 b/site/docs/.vitepress/theme/fonts/geist-mono-400.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..62a80b06a3ee4067daaf9c7ceeb2f98c7524b935 GIT binary patch literal 12792 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 K9Bm^J@gnS2;ehdd2 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 literal 0 HcmV?d00001 diff --git a/site/docs/.vitepress/theme/fonts/geist-sans-400.woff2 b/site/docs/.vitepress/theme/fonts/geist-sans-400.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..9867e874ec0bdf03d2d0528134a590d261ab0038 GIT binary patch literal 33400 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 AegFUf literal 0 HcmV?d00001 diff --git a/site/docs/.vitepress/theme/fonts/space-grotesk-500.woff2 b/site/docs/.vitepress/theme/fonts/space-grotesk-500.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..0db251fc0bcec46bafd3fe49ebb5acf7d93284ce GIT binary patch literal 13312 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 GLCRiVB40oN literal 0 HcmV?d00001 diff --git a/site/docs/.vitepress/theme/index.js b/site/docs/.vitepress/theme/index.js new file mode 100644 index 0000000..ea03a79 --- /dev/null +++ b/site/docs/.vitepress/theme/index.js @@ -0,0 +1,28 @@ +import DefaultTheme from "vitepress/theme"; +import { h } from "vue"; +import Playground from "./components/Playground.vue"; +import SampleDataDemo from "./components/SampleDataDemo.vue"; +import ConsoleHome from "./ConsoleHome.vue"; + +// Design layer — import order matters: tokens → fonts → base → surfaces. +import "./styles/tokens.css"; +import "./styles/fonts.css"; +import "./styles/base.css"; +import "./styles/nav.css"; +import "./styles/sidebar.css"; +import "./styles/code.css"; +import "./styles/home.css"; + +export default { + extends: DefaultTheme, + // index.md uses layout:home with no hero/features, so our injected sections render while nav/search/footer stay. + Layout() { + return h(DefaultTheme.Layout, null, { + "home-hero-before": () => h(ConsoleHome), + }); + }, + enhanceApp({ app }) { + app.component("Playground", Playground); + app.component("SampleDataDemo", SampleDataDemo); + }, +}; diff --git a/site/docs/.vitepress/theme/styles/base.css b/site/docs/.vitepress/theme/styles/base.css new file mode 100644 index 0000000..ef580c8 --- /dev/null +++ b/site/docs/.vitepress/theme/styles/base.css @@ -0,0 +1,181 @@ +/* Typography scale, links, selection, scrollbar, buttons, motion. */ + +html { + scroll-behavior: smooth; +} + +body { + font-family: var(--ph-font-body); + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +h1, +h2, +h3, +.VPHero .name, +.VPHero .text { + font-family: var(--ph-font-display); + letter-spacing: -0.02em; +} + +h1 { + font-weight: 700; +} +h2, +h3 { + font-weight: 600; +} + +.vp-doc h1 { + font-size: 2.1rem; + line-height: 1.2; +} +.vp-doc h2 { + margin-top: 2.4rem; + padding-top: 1.6rem; +} + +.vp-doc p, +.vp-doc li { + line-height: 1.75; +} + +.vp-doc a { + font-weight: 500; + text-decoration-color: var(--vp-c-brand-soft); + text-underline-offset: 3px; + transition: color 0.15s ease, text-decoration-color 0.15s ease; +} +.vp-doc a:hover { + text-decoration-color: var(--ph-violet); +} + +::selection { + background: rgba(124, 92, 255, 0.32); + color: var(--ph-text-1); +} + +:where(a, button, input, select, textarea, summary):focus-visible { + outline: 2px solid var(--ph-violet); + outline-offset: 2px; + border-radius: 4px; +} + +* { + scrollbar-width: thin; + scrollbar-color: var(--ph-border) transparent; +} +*::-webkit-scrollbar { + width: 10px; + height: 10px; +} +*::-webkit-scrollbar-thumb { + background: var(--ph-border); + border-radius: 8px; + border: 2px solid transparent; + background-clip: padding-box; +} +*::-webkit-scrollbar-thumb:hover { + background: var(--ph-text-3); + background-clip: padding-box; +} + +.VPButton.brand { + border: none; + background: var(--ph-grad); + background-size: 160% 160%; + color: #fff; + box-shadow: 0 8px 24px rgba(124, 92, 255, 0.32); + transition: transform 0.22s ease, box-shadow 0.22s ease, + background-position 0.6s ease; +} +.VPButton.brand:hover { + transform: translateY(-2px); + background-position: 100% 50%; + box-shadow: 0 14px 32px rgba(124, 92, 255, 0.42); +} + +.VPButton.alt { + border: 1px solid var(--vp-c-divider); + background: transparent; + transition: transform 0.22s ease, border-color 0.22s ease, color 0.22s ease; +} +.VPButton.alt:hover { + transform: translateY(-2px); + border-color: var(--ph-violet); + color: var(--ph-violet-hi); +} + +:not(pre) > code { + font-family: var(--ph-font-mono); + border-radius: 6px; + background: var(--vp-c-brand-soft) !important; + color: var(--vp-c-brand-1) !important; + padding: 0.18em 0.4em !important; + font-size: 0.86em; +} + +.custom-block { + border-radius: 12px; + border-width: 0 0 0 3px; +} +.custom-block.tip { + border-left-color: var(--ph-cyan); +} +.custom-block.info { + border-left-color: var(--ph-violet); +} +.custom-block.warning { + border-left-color: #fbbf24; +} +.custom-block.danger { + border-left-color: #f87171; +} + +.VPFooter { + position: relative; + background: var(--vp-c-bg-alt); + border-top: 1px solid var(--vp-c-divider); +} +.VPFooter::before { + content: ""; + position: absolute; + top: -1px; + left: 0; + right: 0; + height: 2px; + background: var(--ph-grad); + opacity: 0.6; +} +.VPFooter .message, +.VPFooter .copyright { + font-family: var(--ph-font-mono); + font-size: 12.5px; +} + +.VPContent { + animation: ph-fade 0.32s ease; +} +@keyframes ph-fade { + from { + opacity: 0; + transform: translateY(6px); + } + to { + opacity: 1; + transform: none; + } +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + .VPContent, + .VPButton.brand, + .VPButton.alt { + animation: none; + transition: none; + } +} diff --git a/site/docs/.vitepress/theme/styles/code.css b/site/docs/.vitepress/theme/styles/code.css new file mode 100644 index 0000000..71ec361 --- /dev/null +++ b/site/docs/.vitepress/theme/styles/code.css @@ -0,0 +1,65 @@ +/* Code blocks — console window chrome over VitePress's Shiki output. */ + +.vp-doc div[class*="language-"] { + border: 1px solid var(--vp-c-divider); + border-radius: 12px; + background: var(--vp-c-bg-soft); + padding-top: 38px; + box-shadow: 0 10px 28px rgba(2, 6, 16, 0.18); +} + +.vp-doc div[class*="language-"]::before { + content: ""; + position: absolute; + top: 14px; + left: 16px; + width: 10px; + height: 10px; + border-radius: 50%; + background: #f87171; + box-shadow: 18px 0 0 #fbbf24, 36px 0 0 var(--ph-cyan); + z-index: 3; +} + +.vp-doc div[class*="language-"] > span.lang { + top: 11px; + right: 14px; + font-family: var(--ph-font-mono); + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--ph-text-3); + opacity: 1; +} + +.vp-doc div[class*="language-"] > button.copy { + top: 7px; + right: 56px; + width: 28px; + height: 24px; + border: 1px solid var(--vp-c-divider); + border-radius: 6px; + background-color: var(--vp-c-bg-alt); + background-size: 14px; +} +.vp-doc div[class*="language-"] > button.copy:hover { + border-color: var(--ph-violet); +} + +.vp-doc .vp-code-group .tabs { + font-family: var(--ph-font-mono); +} +.vp-doc .vp-code-group .tabs label { + font-size: 12px; +} +.vp-doc .vp-code-group .tabs input:checked + label { + color: var(--vp-c-brand-1); +} +.vp-doc .vp-code-group .tabs::after { + background: var(--ph-grad); +} + +pre, +code { + font-family: var(--ph-font-mono); +} diff --git a/site/docs/.vitepress/theme/styles/fonts.css b/site/docs/.vitepress/theme/styles/fonts.css new file mode 100644 index 0000000..047980e --- /dev/null +++ b/site/docs/.vitepress/theme/styles/fonts.css @@ -0,0 +1,47 @@ +/* Self-hosted fonts (bundled by Vite, no runtime CDN). + Display: Space Grotesk · Body: Geist Sans · Mono: Geist Mono */ + +@font-face { + font-family: "Space Grotesk"; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url("../fonts/space-grotesk-500.woff2") format("woff2"); +} +@font-face { + font-family: "Space Grotesk"; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url("../fonts/space-grotesk-700.woff2") format("woff2"); +} + +@font-face { + font-family: "Geist Sans"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("../fonts/geist-sans-400.woff2") format("woff2"); +} +@font-face { + font-family: "Geist Sans"; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url("../fonts/geist-sans-500.woff2") format("woff2"); +} + +@font-face { + font-family: "Geist Mono"; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url("../fonts/geist-mono-400.woff2") format("woff2"); +} +@font-face { + font-family: "Geist Mono"; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url("../fonts/geist-mono-600.woff2") format("woff2"); +} diff --git a/site/docs/.vitepress/theme/styles/home.css b/site/docs/.vitepress/theme/styles/home.css new file mode 100644 index 0000000..dcd1fca --- /dev/null +++ b/site/docs/.vitepress/theme/styles/home.css @@ -0,0 +1,16 @@ +/* Home — let the injected console-home sections sit flush and full-width + (the default VPHome hero/features render nothing without frontmatter). */ + +.VPHome { + margin-bottom: 0 !important; + padding-bottom: 0 !important; +} + +.VPHome > .VPHomeHero:empty, +.VPHome > .VPHomeFeatures:empty { + display: none; +} + +.VPContent.is-home { + padding: 0 !important; +} diff --git a/site/docs/.vitepress/theme/styles/nav.css b/site/docs/.vitepress/theme/styles/nav.css new file mode 100644 index 0000000..7bce589 --- /dev/null +++ b/site/docs/.vitepress/theme/styles/nav.css @@ -0,0 +1,76 @@ +/* Nav bar — glassy, mono wordmark, command-palette search. */ + +.VPNavBar { + border-bottom: 1px solid transparent; +} +.VPNavBar.has-sidebar .content, +.VPNavBar .wrapper { + backdrop-filter: saturate(160%) blur(12px); +} + +/* Translucent surface so content scrolls under the bar */ +.VPNavBar:not(.home) { + background: color-mix(in srgb, var(--ph-bg-alt) 72%, transparent); + border-bottom-color: var(--vp-c-divider); +} +.VPNavBar.home.top { + background: transparent; +} +.VPNavBar:not(.top) { + background: color-mix(in srgb, var(--ph-bg-alt) 78%, transparent); + border-bottom-color: var(--vp-c-divider); +} + +.VPNavBarTitle .title { + font-family: var(--ph-font-display); + font-weight: 700; + letter-spacing: -0.02em; + gap: 8px; +} + +.VPNavBarMenuLink, +.VPNavBarMenuGroup .text { + font-family: var(--ph-font-mono); + font-size: 13px; + letter-spacing: -0.01em; +} +.VPNavBarMenuLink { + position: relative; +} +.VPNavBarMenuLink.active { + color: var(--vp-c-brand-1); +} +.VPNavBarMenuLink.active::after { + content: ""; + position: absolute; + left: 12px; + right: 12px; + bottom: 6px; + height: 2px; + border-radius: 2px; + background: var(--ph-grad); +} + +.DocSearch-Button, +.VPNavBarSearchButton { + border: 1px solid var(--vp-c-divider); + border-radius: 8px; +} +.VPNavBarSearch .DocSearch-Button-Keys, +.VPNavBarSearch .DocSearch-Button-Placeholder { + font-family: var(--ph-font-mono); +} + +/* Brand-colored social icons: npm red, GitHub mark black/white per theme. */ +.VPSocialLink[aria-label="npm package"], +.VPSocialLink[aria-label="npm package"]:hover { + color: #cb3837; +} +.VPSocialLink[aria-label="GitHub repository"], +.VPSocialLink[aria-label="GitHub repository"]:hover { + color: #24292f; +} +.dark .VPSocialLink[aria-label="GitHub repository"], +.dark .VPSocialLink[aria-label="GitHub repository"]:hover { + color: #ffffff; +} diff --git a/site/docs/.vitepress/theme/styles/sidebar.css b/site/docs/.vitepress/theme/styles/sidebar.css new file mode 100644 index 0000000..85d7322 --- /dev/null +++ b/site/docs/.vitepress/theme/styles/sidebar.css @@ -0,0 +1,43 @@ +/* Sidebar — mono section labels, accent active item. */ + +.VPSidebar { + background: var(--vp-c-bg-alt); +} + +.VPSidebarItem.level-0 > .item > .text, +.VPSidebarItem.collapsible.level-0 > .item .text { + font-family: var(--ph-font-mono); + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.12em; + color: var(--ph-text-3); +} + +.VPSidebarItem .link .text { + transition: color 0.15s ease; +} + +.VPSidebarItem.is-active > .item .link .text, +.VPSidebarItem.is-active > .item .text { + color: var(--vp-c-brand-1); + font-weight: 600; +} +.VPSidebarItem.is-link.is-active > .item { + position: relative; +} +.VPSidebarItem.is-link.is-active > .item::before { + content: ""; + position: absolute; + left: -14px; + top: 50%; + transform: translateY(-50%); + width: 2px; + height: 16px; + border-radius: 2px; + background: var(--ph-grad); +} + +.VPDocAsideOutline .title, +.VPDocOutlineItem { + font-family: var(--ph-font-mono); +} diff --git a/site/docs/.vitepress/theme/styles/tokens.css b/site/docs/.vitepress/theme/styles/tokens.css new file mode 100644 index 0000000..0fcf858 --- /dev/null +++ b/site/docs/.vitepress/theme/styles/tokens.css @@ -0,0 +1,103 @@ +/* "Query Console" design tokens, mapped onto VitePress CSS variables. + Dark is the default theme; light is a toggle. */ + +:root { + /* Brand accents (shared across themes) */ + --ph-violet: #7c5cff; + --ph-violet-hi: #9d86ff; + --ph-cyan: #22d3ee; + --ph-grad: linear-gradient(110deg, #7c5cff 0%, #22d3ee 100%); + + /* SQL syntax accents (hero console + code theme) */ + --ph-sql-keyword: #a78bfa; + --ph-sql-string: #5eead4; + --ph-sql-number: #fbbf24; + --ph-sql-ident: #e6e9f0; + --ph-sql-punct: #7c879e; + + /* Light theme surfaces (toggle) */ + --ph-bg: #ffffff; + --ph-bg-alt: #f7f8fc; + --ph-bg-soft: #ffffff; + --ph-border: #e6e8f0; + --ph-text-1: #0b1220; + --ph-text-2: #4a5268; + --ph-text-3: #6b7488; + + /* In light mode the SQL identifiers should be dark, not near-white */ + --ph-sql-ident: #0b1220; + + /* Typography */ + --ph-font-display: "Space Grotesk", "Geist Sans", ui-sans-serif, system-ui, + -apple-system, "Segoe UI", sans-serif; + --ph-font-body: "Geist Sans", ui-sans-serif, system-ui, -apple-system, + "Segoe UI", Roboto, sans-serif; + --ph-font-mono: "Geist Mono", ui-monospace, "SF Mono", "Menlo", "Consolas", + monospace; + + /* Map onto VitePress vars (brand + fonts are theme-agnostic) */ + --vp-c-brand-1: var(--ph-violet); + --vp-c-brand-2: var(--ph-violet-hi); + --vp-c-brand-3: var(--ph-cyan); + --vp-c-brand-soft: rgba(124, 92, 255, 0.14); + + --vp-font-family-base: var(--ph-font-body); + --vp-font-family-mono: var(--ph-font-mono); + + /* Hero name uses the signature gradient */ + --vp-home-hero-name-color: transparent; + --vp-home-hero-name-background: var(--ph-grad); + + /* Brand button → gradient handled in base.css */ + --vp-button-brand-bg: var(--ph-violet); + --vp-button-brand-hover-bg: var(--ph-violet-hi); + --vp-button-brand-active-bg: var(--ph-violet); + --vp-button-brand-border: transparent; + --vp-button-brand-hover-border: transparent; + --vp-button-brand-active-border: transparent; + + /* Light-mode VitePress surfaces */ + --vp-c-bg: var(--ph-bg); + --vp-c-bg-alt: var(--ph-bg-alt); + --vp-c-bg-soft: var(--ph-bg-alt); + --vp-c-bg-elv: var(--ph-bg-soft); + --vp-c-divider: var(--ph-border); + --vp-c-border: var(--ph-border); + --vp-c-gutter: var(--ph-border); + --vp-c-text-1: var(--ph-text-1); + --vp-c-text-2: var(--ph-text-2); + --vp-c-text-3: var(--ph-text-3); +} + +/* Dark theme (default identity) */ +.dark { + --ph-bg: #0a0c10; + --ph-bg-alt: #0d1017; + --ph-bg-soft: #11151f; + --ph-border: #1c2230; + --ph-text-1: #e6e9f0; + --ph-text-2: #9aa4b8; + --ph-text-3: #7c879e; + --ph-sql-ident: #e6e9f0; + + --vp-c-bg: var(--ph-bg); + --vp-c-bg-alt: var(--ph-bg-alt); + --vp-c-bg-soft: var(--ph-bg-soft); + --vp-c-bg-elv: var(--ph-bg-soft); + --vp-c-divider: var(--ph-border); + --vp-c-border: var(--ph-border); + --vp-c-gutter: #05070b; + --vp-c-text-1: var(--ph-text-1); + --vp-c-text-2: var(--ph-text-2); + --vp-c-text-3: var(--ph-text-3); + + --vp-code-block-bg: var(--ph-bg-soft); + --vp-code-line-highlight-color: rgba(124, 92, 255, 0.12); + + --vp-home-hero-image-background-image: linear-gradient( + 135deg, + rgba(124, 92, 255, 0.35), + rgba(34, 211, 238, 0.3) + ); + --vp-home-hero-image-filter: blur(64px); +} diff --git a/site/docs/.vitepress/theme/utils/demoData.js b/site/docs/.vitepress/theme/utils/demoData.js new file mode 100644 index 0000000..f5e0ca6 --- /dev/null +++ b/site/docs/.vitepress/theme/utils/demoData.js @@ -0,0 +1,326 @@ +// Sample data + an in-browser evaluator for the interactive demo (PagiHelp only builds SQL). + +export const TABLES = { + products: { + name: "products", + columns: [ + { name: "id", type: "INT", key: "PK", align: "right" }, + { name: "sku", type: "VARCHAR(32)" }, + { name: "name", type: "VARCHAR(120)" }, + { name: "category", type: "VARCHAR(40)" }, + { name: "price", type: "DECIMAL(10,2)", align: "right" }, + { name: "stock", type: "INT", align: "right" }, + { name: "created_at", type: "TIMESTAMP" }, + ], + searchColumns: ["sku", "name"], + rows: [ + { id: 1, sku: "KB-100", name: "Mechanical Keyboard", category: "Peripherals", price: 3499, stock: 120, created_at: "2026-01-12" }, + { id: 2, sku: "MS-200", name: "Wireless Mouse", category: "Peripherals", price: 1299, stock: 340, created_at: "2026-02-03" }, + { id: 3, sku: "MN-270", name: "27-inch Monitor", category: "Displays", price: 13999, stock: 45, created_at: "2026-02-18" }, + { id: 4, sku: "DK-010", name: "Standing Desk", category: "Furniture", price: 24999, stock: 12, created_at: "2026-03-01" }, + { id: 5, sku: "CH-330", name: "Ergonomic Chair", category: "Furniture", price: 15999, stock: 30, created_at: "2026-03-22" }, + { id: 6, sku: "HS-040", name: "USB Headset", category: "Audio", price: 1999, stock: 0, created_at: "2025-12-30" }, + { id: 7, sku: "WC-050", name: "HD Webcam", category: "Audio", price: 2799, stock: 88, created_at: "2026-04-10" }, + { id: 8, sku: "DP-410", name: "USB-C Dock", category: "Accessories", price: 5499, stock: 60, created_at: "2026-04-25" }, + { id: 9, sku: "CB-500", name: "HDMI Cable", category: "Accessories", price: 349, stock: 500, created_at: "2026-05-09" }, + { id: 10, sku: "SP-220", name: "Bluetooth Speaker", category: "Audio", price: 2499, stock: 150, created_at: "2026-05-30" }, + ], + }, + + orders: { + name: "orders", + columns: [ + { name: "id", type: "INT", key: "PK", align: "right" }, + { name: "reference", type: "VARCHAR(24)" }, + { name: "status", type: "ENUM" }, + { name: "total", type: "DECIMAL(10,2)", align: "right" }, + { name: "created_at", type: "TIMESTAMP" }, + ], + searchColumns: ["reference", "status"], + rows: [ + { id: 1001, reference: "ORD-1001", status: "Paid", total: 12450.5, created_at: "2026-05-01" }, + { id: 1002, reference: "ORD-1002", status: "Pending", total: 2499, created_at: "2026-05-03" }, + { id: 1003, reference: "ORD-1003", status: "Paid", total: 30999, created_at: "2026-05-06" }, + { id: 1004, reference: "ORD-1004", status: "Refunded", total: 1499, created_at: "2026-05-09" }, + { id: 1005, reference: "ORD-1005", status: "Paid", total: 8999, created_at: "2026-05-12" }, + { id: 1006, reference: "ORD-1006", status: "Paid", total: 22999, created_at: "2026-05-15" }, + { id: 1007, reference: "ORD-1007", status: "Pending", total: 999, created_at: "2026-05-18" }, + { id: 1008, reference: "ORD-1008", status: "Paid", total: 49999, created_at: "2026-05-21" }, + { id: 1009, reference: "ORD-1009", status: "Refunded", total: 4599, created_at: "2026-05-24" }, + { id: 1010, reference: "ORD-1010", status: "Pending", total: 17999, created_at: "2026-05-27" }, + ], + }, +}; + +// PagiHelp option block for a table (alias === column name in this demo). +export function optionsFor(tableKey) { + const t = TABLES[tableKey]; + return [ + { + tableName: t.name, + columnList: t.columns.map((c) => ({ name: c.name, alias: c.name })), + searchColumnList: t.searchColumns.map((name) => ({ name })), + }, + ]; +} + +// Worked examples; `sql` is generated by scripts/gen-demo-sql.mjs from the real library. +export const EXAMPLES = [ + { + id: "in-stock-products", + table: "products", + title: "In-stock products, newest first", + desc: "Filter on a single column, sort descending, take the first page.", + request: { + filters: [["stock", ">", 0]], + sort: { attributes: ["created_at"], sorts: ["desc"] }, + pageNo: 1, + itemsPerPage: 5, + }, + sql: { + mysql: + "SELECT id AS id,sku AS sku,name AS name,category AS category,price AS price,stock AS stock,created_at AS created_at FROM `products` WHERE (stock > ?) ORDER BY `created_at`DESC,`id`DESC LIMIT ?,?", + postgres: + 'SELECT id AS "id",sku AS "sku",name AS "name",category AS "category",price AS "price",stock AS "stock",created_at AS "created_at" FROM "products" WHERE (stock > ?) ORDER BY "created_at"DESC,"id"DESC LIMIT ? OFFSET ?', + }, + }, + { + id: "search-category", + table: "products", + title: "Search catalog + filter by category", + desc: "Free-text search across searchable columns, combined with an IN filter.", + request: { + search: "USB", + filters: [["category", "IN", ["Audio", "Accessories"]]], + sort: { attributes: ["name"], sorts: ["asc"] }, + pageNo: 1, + itemsPerPage: 5, + }, + sql: { + mysql: + "SELECT id AS id,sku AS sku,name AS name,category AS category,price AS price,stock AS stock,created_at AS created_at FROM `products` WHERE (category IN (?,?)) AND ( sku LIKE ? OR name LIKE ? ) ORDER BY `name`ASC,`id`DESC LIMIT ?,?", + postgres: + 'SELECT id AS "id",sku AS "sku",name AS "name",category AS "category",price AS "price",stock AS "stock",created_at AS "created_at" FROM "products" WHERE (category IN (?,?)) AND ( sku LIKE ? OR name LIKE ? ) ORDER BY "name"ASC,"id"DESC LIMIT ? OFFSET ?', + }, + }, + { + id: "high-value-orders", + table: "orders", + title: "High-value paid orders", + desc: "Two filters combined with AND, sorted by amount.", + request: { + filters: [ + ["status", "=", "Paid"], + ["total", ">=", 10000], + ], + sort: { attributes: ["total"], sorts: ["desc"] }, + pageNo: 1, + itemsPerPage: 5, + }, + sql: { + mysql: + "SELECT id AS id,reference AS reference,status AS status,total AS total,created_at AS created_at FROM `orders` WHERE (status = ? AND total >= ?) ORDER BY `total`DESC,`id`DESC LIMIT ?,?", + postgres: + 'SELECT id AS "id",reference AS "reference",status AS "status",total AS "total",created_at AS "created_at" FROM "orders" WHERE (status = ? AND total >= ?) ORDER BY "total"DESC,"id"DESC LIMIT ? OFFSET ?', + }, + }, + { + id: "page-two", + table: "orders", + title: "Plain pagination — page 2", + desc: "No filters: sort by date and jump to the second page.", + request: { + sort: { attributes: ["created_at"], sorts: ["asc"] }, + pageNo: 2, + itemsPerPage: 4, + }, + sql: { + mysql: + "SELECT id AS id,reference AS reference,status AS status,total AS total,created_at AS created_at FROM `orders` ORDER BY `created_at`ASC,`id`DESC LIMIT ?,?", + postgres: + 'SELECT id AS "id",reference AS "reference",status AS "status",total AS "total",created_at AS "created_at" FROM "orders" ORDER BY "created_at"ASC,"id"DESC LIMIT ? OFFSET ?', + }, + }, +]; + +// In-browser evaluator: reproduces the query semantics over the demo rows. + +function likeToRegex(pattern, caseInsensitive) { + let out = "^"; + for (const ch of String(pattern)) { + if (ch === "%") out += ".*"; + else if (ch === "_") out += "."; + else out += ch.replace(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`); + } + out += "$"; + return new RegExp(out, caseInsensitive ? "i" : ""); +} + +function compare(a, b) { + if (typeof a === "number" && typeof b === "number") return a - b; + const sa = String(a); + const sb = String(b); + if (sa < sb) return -1; + return sa > sb ? 1 : 0; +} + +function matchFilter(row, [field, op, value]) { + const v = row[field]; + switch (String(op).toUpperCase()) { + case "=": + return v == value; + case "!=": + case "<>": + return v != value; + case ">": + return compare(v, value) > 0; + case ">=": + return compare(v, value) >= 0; + case "<": + return compare(v, value) < 0; + case "<=": + return compare(v, value) <= 0; + case "IN": + return Array.isArray(value) && value.some((x) => x == v); + case "NOT IN": + return Array.isArray(value) && !value.some((x) => x == v); + case "LIKE": + return likeToRegex(value, false).test(String(v)); + case "ILIKE": + return likeToRegex(value, true).test(String(v)); + default: + return true; + } +} + +// Operators the evaluator can reproduce; anything else still builds correct SQL but skips the row preview. +const EVALUABLE_OPS = new Set([ + "=", "!=", "<>", ">", ">=", "<", "<=", "IN", "NOT IN", "LIKE", "ILIKE", +]); + +function isConditionTuple(node) { + return ( + Array.isArray(node) && + node.length === 3 && + typeof node[0] === "string" && + typeof node[1] === "string" + ); +} + +// Evaluate the filter tree (even depth = AND, odd = OR); throws `.unsupportedOp` if an op can't be evaluated. +function evalFilterNode(node, row, depth) { + if (isConditionTuple(node)) { + const op = String(node[1]).toUpperCase(); + if (!EVALUABLE_OPS.has(op)) { + const err = new Error("unsupported operator"); + err.unsupportedOp = node[1]; + throw err; + } + return matchFilter(row, node); + } + if (!Array.isArray(node)) return true; + const results = node.map((child) => evalFilterNode(child, row, depth + 1)); + return depth % 2 === 0 ? results.every(Boolean) : results.some(Boolean); +} + +// Which columns search applies to; `complex` flags statement/prefixed columns the evaluator can't model. +function searchColumnsFor(opt, table) { + if (!Array.isArray(opt.searchColumnList)) { + return { cols: table.searchColumns, complex: false }; + } + const complex = opt.searchColumnList.some((c) => c?.statement || c?.prefix); + const cols = opt.searchColumnList + .filter((c) => c?.name && !c.prefix && !c.statement) + .map((c) => c.name); + return { cols, complex }; +} + +// AND-ed additionalWhereConditions, then free-text search, then the request filters. +function applyFilters(rows, request, searchCols, awc) { + let out = rows; + if (awc?.length) out = out.filter((r) => evalFilterNode(awc, r, 0)); + if (request.search) { + const term = String(request.search).toLowerCase(); + out = out.filter((r) => + searchCols.some((c) => String(r[c]).toLowerCase().includes(term)) + ); + } + if (request.filters?.length) { + out = out.filter((r) => evalFilterNode(request.filters, r, 0)); + } + return out; +} + +function sortRows(rows, sort) { + if (!sort?.attributes?.length) return rows; + return rows.sort((a, b) => { + for (let i = 0; i < sort.attributes.length; i += 1) { + const dir = String(sort.sorts[i]).toUpperCase() === "DESC" ? -1 : 1; + const c = compare(a[sort.attributes[i]], b[sort.attributes[i]]); + if (c !== 0) return c * dir; + } + return 0; + }); +} + +function paginateRows(rows, request) { + let offset = 0; + let limit = rows.length; + if (request.pageNo !== undefined && request.itemsPerPage !== undefined) { + offset = (request.pageNo - 1) * request.itemsPerPage; + limit = request.itemsPerPage; + } else if (request.offset !== undefined && request.limit !== undefined) { + offset = request.offset; + limit = request.limit; + } + return rows.slice(offset, offset + limit); +} + +// Run a request over the demo rows, honoring the options block; returns { computable, rows, total, reason? }. +export function runRequest(tableKey, request, options) { + const table = TABLES[tableKey]; + try { + const opt = (Array.isArray(options) && options[0]) || {}; + + if (opt.joinQuery && String(opt.joinQuery).trim()) { + return { + computable: false, + reason: "a custom joinQuery (joins aren’t modelled in the browser)", + rows: [], + total: 0, + }; + } + + const { cols: searchCols, complex } = searchColumnsFor(opt, table); + if (complex && request.search) { + return { + computable: false, + reason: "a statement/prefixed search column", + rows: [], + total: 0, + }; + } + + const filtered = applyFilters( + table.rows.slice(), + request, + searchCols, + opt.additionalWhereConditions + ); + const sorted = sortRows(filtered, request.sort); + return { + computable: true, + rows: paginateRows(sorted, request), + total: sorted.length, + }; + } catch (e) { + return { + computable: false, + reason: e?.unsupportedOp + ? `the “${e.unsupportedOp}” operator` + : "this request shape", + rows: [], + total: 0, + }; + } +} diff --git a/site/docs/.vitepress/theme/utils/sql.js b/site/docs/.vitepress/theme/utils/sql.js new file mode 100644 index 0000000..9e48a90 --- /dev/null +++ b/site/docs/.vitepress/theme/utils/sql.js @@ -0,0 +1,33 @@ +// Lightweight SQL tokenizer shared by the hero console and dialect showcase. +// Returns [{ t, cls }] where cls ∈ k(eyword) s(tring) n(umber) p(unct) w(hitespace) i(dent). + +const KEYWORDS = new Set( + ("select from where order by limit offset as and or in not like ilike asc " + + "desc union all is null on join left right inner count distinct group having") + .split(" ") +); + +// Punctuation includes JS object syntax ({ } [ ] :) so request objects render, plus SQL/Postgres operators. +const PUNCT = String.raw`[(),.?*;:=<>!~@|&+\-/{}\[\]]+`; +const RE = new RegExp( + `('[^']*'|\\$\\d+|\\b\\d+\\b|[A-Za-z_][\\w]*|\`[^\`]*\`|"[^"]*"|\\s+|${PUNCT})`, + "g" +); +const PUNCT_RE = new RegExp(`^${PUNCT}$`); + +export function tokenize(sql) { + const out = []; + let m; + RE.lastIndex = 0; + while ((m = RE.exec(sql))) { + const t = m[0]; + let cls = "i"; + if (/^\s+$/.test(t)) cls = "w"; + else if (/^'.*'$/.test(t) || /^".*"$/.test(t) || /^`.*`$/.test(t)) cls = "s"; + else if (/^\$\d+$/.test(t) || /^\d+$/.test(t)) cls = "n"; + else if (/^[A-Za-z_]/.test(t) && KEYWORDS.has(t.toLowerCase())) cls = "k"; + else if (PUNCT_RE.test(t)) cls = "p"; + out.push({ t, cls }); + } + return out; +} diff --git a/site/docs/api/reference.md b/site/docs/api/reference.md new file mode 100644 index 0000000..b73a623 --- /dev/null +++ b/site/docs/api/reference.md @@ -0,0 +1,199 @@ +# API Reference + +This page summarizes the public surface of the **v2** API. Full TypeScript types +ship with the package (`v2.d.ts` and `index.d.ts`). + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); +``` + +## Constructor + +```ts +new PagiHelpV2(options?: { + dialect?: "mysql" | "postgres"; // default "mysql" + columnNameConverter?: (name: string) => string; + safeOptions?: { validate?: boolean }; // only `validate` is supported +}) +``` + +See [Constructor](/v2/constructor). + +## Methods + +### `paginate(paginationObject, options)` + +Build offset/page-based pagination SQL. + +```ts +paginate( + paginationObject: PaginationInput, + options: PaginationOption[], + safeOptions?: { validate?: boolean } +): PaginationResult +``` + +→ [paginate()](/v2/paginate) + +### `paginateSafe(paginationObject, options, safeOptions?)` + +Validate the input and throw an `Error` on problems, then build SQL. Same return +shape as `paginate()`. + +### `paginateCursor(paginationObject, options)` + +Build keyset/cursor pagination SQL (single table, `after` only). + +```ts +paginateCursor( + paginationObject: CursorPaginationInput, // requires `sort` and `limit` + options: PaginationOption[], + safeOptions?: { validate?: boolean } +): CursorPaginationResult // PaginationResult + cursorPlan +``` + +→ [Cursor Pagination](/v2/cursor-pagination) + +### `resolveCursorPage(rows, cursorPlan)` + +Trim the extra fetched row and derive cursor metadata. + +```ts +resolveCursorPage(rows: Row[], cursorPlan: CursorPlan): { + rows: Row[]; + pageInfo: { + hasNextPage: boolean; + hasPreviousPage: boolean; + startCursor: string | null; + endCursor: string | null; + nextCursor: string | null; + }; +} +``` + +### `encodeCursorFromRow(row, cursorPlan)` + +```ts +encodeCursorFromRow(row: Record, cursorPlan: CursorPlan): string +``` + +Build an opaque `after` token from one query row. + +### `decodeCursor(token)` + +```ts +decodeCursor(cursorToken: string): { + v: 1; + d: "mysql" | "postgres"; + fp: string; + s: [attribute: string, direction: "ASC" | "DESC"][]; + values: unknown[]; + dir: "after"; +} +``` + +Decode and validate the cursor token envelope. + +### Validation helpers + +```ts +validatePaginationInput(paginationObject, options): ValidationResult +validatePaginationObject(paginationObject): ValidationResult +validateOptions(options): ValidationResult +validateCursorPaginationInput(paginationObject, options): ValidationResult +``` + +Each returns: + +```ts +interface ValidationResult { + valid: boolean; + errors: string[]; + warnings: string[]; +} +``` + +## Key types + +### `PaginationInput` + +```ts +interface PaginationInput { + search?: string; + filters?: ConditionInput; // [field, operator, value] tuples / nested groups + sort?: { attributes: string[]; sorts: ("ASC" | "DESC" | "asc" | "desc")[] }; + pageNo?: number; + itemsPerPage?: number; + offset?: number; + limit?: number; +} +``` + +### `CursorPaginationInput` + +```ts +interface CursorPaginationInput { + search?: string; + filters?: ConditionInput; + sort: { attributes: string[]; sorts: ("ASC" | "DESC" | "asc" | "desc")[] }; // required + limit: number; // required + after?: string; + // before, pageNo, itemsPerPage, offset are rejected +} +``` + +### `PaginationOption` + +```ts +interface PaginationOption { + tableName: string; + columnList: ColumnDescriptor[]; + searchColumnList?: SearchColumnDescriptor[]; // no `alias` on v2 + joinQuery?: string; + additionalWhereConditions?: ConditionInput; +} +``` + +### `ColumnDescriptor` + +```ts +interface ColumnDescriptor { + name?: string; // exactly one of name | statement + statement?: string; // raw SQL expression (trusted) + prefix?: string; // table alias, e.g. "l" -> l.column + alias?: string; // output alias +} +``` + +### `PaginationResult` + +```ts +interface PaginationResult { + query: string; + countQuery: string; + totalCountQuery: string; + replacements: unknown[]; +} +``` + +### `CursorPlan` (excerpt) + +```ts +interface CursorPlan { + version: 1; + dialect: "mysql" | "postgres"; + direction: "forward"; + requestedLimit: number; + fetchLimit: number; // requestedLimit + 1 + normalizedSort: { attribute: string; direction: "ASC" | "DESC" }[]; + cursorAliases: string[]; + queryFingerprint: string; + after: string | null; +} +``` + +::: tip +For the authoritative, always-current types, read `v2.d.ts` in the +[repository](https://github.com/Codebucket-Solutions/PagiHelp). +::: diff --git a/site/docs/dialects/mysql.md b/site/docs/dialects/mysql.md new file mode 100644 index 0000000..e105da1 --- /dev/null +++ b/site/docs/dialects/mysql.md @@ -0,0 +1,75 @@ +# MySQL Dialect + +MySQL is the default dialect. You can set it explicitly: + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); +``` + +## Identifier quoting + +MySQL quotes generated table names and `ORDER BY` identifiers with **backticks**: + +```sql +FROM `events` +ORDER BY `created_at`DESC +``` + +## Pagination clause + +MySQL uses a single `LIMIT` with offset and count: + +```sql +LIMIT ?,? +``` + +Replacements are `[offset, limit]`. + +For [cursor pagination](/v2/cursor-pagination), MySQL uses `LIMIT ?,?` with +replacements `[0, limit + 1]`. + +## Operators + +MySQL keeps its native operator set, including: + +- `JSON_CONTAINS`, `JSON_OVERLAPS` +- `FIND_IN_SET` +- `RLIKE` +- `MEMBER OF` + +plus the shared comparison/`IN`/`LIKE` operators. See +[Filters & Operators](/v2/filters-and-operators#shared-operators-both-dialects). + +## Example + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); + +const result = pagiHelp.paginate( + { + search: "Active", + filters: [["status", "IN", ["Active", "Paused"]]], + sort: { attributes: ["created_at"], sorts: ["desc"] }, + pageNo: 1, + itemsPerPage: 10, + }, + [ + { + tableName: "events", + columnList: [ + { name: "id", alias: "id" }, + { name: "status", alias: "status" }, + { name: "created_at", alias: "created_at" }, + ], + searchColumnList: [{ name: "status" }], + }, + ] +); +``` + +::: tip +Use MySQL-only functions (e.g. `IF()`) only inside trusted-input fields like +`statement`, `joinQuery`, and raw `additionalWhereConditions`. +::: diff --git a/site/docs/dialects/postgres.md b/site/docs/dialects/postgres.md new file mode 100644 index 0000000..b5abaaf --- /dev/null +++ b/site/docs/dialects/postgres.md @@ -0,0 +1,121 @@ +# PostgreSQL Dialect + +Opt into PostgreSQL on the constructor: + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "postgres" }); +``` + +## Identifier quoting + +PostgreSQL quotes generated table names and `ORDER BY` identifiers with +**double quotes**: + +```sql +FROM "audit"."licenses" +ORDER BY "created_at"DESC +``` + +## Schema-qualified names + +Schema-qualified table names are supported on v2: + +- `tableName: "audit.licenses"` renders `FROM "audit"."licenses"`. +- If you want a table alias, keep it in `joinQuery`, **not** inside `tableName`. +- Raw `additionalWhereConditions` can use fully-qualified fields like + `"audit.licenses.organization_id"`. +- Regular `filters` still resolve by `alias` or `prefix.column`, **not** by + `schema.table.column`. + +## Pagination clause + +PostgreSQL uses separate `LIMIT` and `OFFSET`: + +```sql +LIMIT ? OFFSET ? +``` + +Replacements are `[limit, offset]`. + +For [cursor pagination](/v2/cursor-pagination), PostgreSQL uses +`LIMIT ? OFFSET ?` with replacements `[limit + 1, 0]`. + +## Native operators + +PostgreSQL has its own native operator set on v2: + +| Operator | Meaning | +| --- | --- | +| `ILIKE` | case-insensitive `LIKE` | +| `~` `~*` `!~` `!~*` | regex (match / case-insensitive / negated) | +| `@>` `<@` | contains / contained-by | +| `?` `?|` `?&` | jsonb key exists / any / all | +| `&&` | array overlap | + +```js +filters: [ + ["metaInfo", "@>", { priority: "high" }], + ["tags", "?|", ["featured", "priority"]], + ["reference", "~*", "^lic-2026-"], +]; +``` + +## Operator compatibility + +To ease migration of shared MySQL-style code, PostgreSQL keeps compatibility +aliases that translate to native SQL: + +| MySQL-style | PostgreSQL rendering | +| --- | --- | +| `JSON_CONTAINS` | `@>` | +| `JSON_OVERLAPS` | emulated `jsonb` overlap SQL | +| `FIND_IN_SET` | `array_position(string_to_array(...), ?::text) IS NOT NULL` | +| `RLIKE` | `~` | +| `MEMBER OF` | `?::jsonb @> to_jsonb(field)` | +| `! IN` | `NOT IN` | + +## Example + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "postgres" }); + +const result = pagiHelp.paginate( + { + search: "LIC", + filters: [ + ["metaInfo", "@>", { priority: "high" }], + ["tags", "?|", ["featured", "priority"]], + ["reference", "~*", "^lic-2026-"], + ], + sort: { attributes: ["createdAt"], sorts: ["desc"] }, + pageNo: 2, + itemsPerPage: 10, + }, + [ + { + tableName: "audit.licenses", + columnList: [ + { name: "license_id", alias: "id" }, + { name: "created_at", alias: "createdAt" }, + { name: "meta_info", alias: "metaInfo" }, + { name: "tags", alias: "tags" }, + { name: "reference", alias: "reference" }, + { + statement: + "(CASE WHEN audit.licenses.tier = '1' THEN 'Premium' ELSE 'Standard' END)", + alias: "tierLabel", + }, + ], + searchColumnList: [{ name: "reference" }], + additionalWhereConditions: [["audit.licenses.organization_id", "=", 42]], + }, + ] +); +``` + +::: warning +Use PostgreSQL SQL inside `statement`, `joinQuery`, and raw +`additionalWhereConditions`. Do not reuse MySQL-only functions like `IF()` there. +::: diff --git a/site/docs/examples/cursor.md b/site/docs/examples/cursor.md new file mode 100644 index 0000000..636c988 --- /dev/null +++ b/site/docs/examples/cursor.md @@ -0,0 +1,96 @@ +# Cursor Pagination + +A full round-trip: build the first page, resolve it, then fetch the next page +using the returned cursor. See [Cursor Pagination](/v2/cursor-pagination) for the +API details and rules (single-table, `after` only, `sort` + `limit` required). + +```js +const PagiHelpV2 = require("pagi-help/v2"); + +const pagiHelp = new PagiHelpV2({ dialect: "postgres" }); + +// 1. First page +const initialQueries = pagiHelp.paginateCursor( + { + search: "ORD", + filters: [["status", "=", "Paid"]], + sort: { attributes: ["createdAt"], sorts: ["desc"] }, + limit: 20, + }, + [ + { + tableName: "orders", + columnList: [ + { name: "order_id", alias: "id" }, + { name: "reference", alias: "reference" }, + { name: "status", alias: "status" }, + { name: "created_at", alias: "createdAt" }, + ], + searchColumnList: [{ name: "reference" }, { name: "status" }], + }, + ] +); +``` + +Generated first-page query: + +```sql +SELECT order_id AS "id",reference AS "reference",status AS "status",created_at AS "createdAt" +FROM "orders" +WHERE (status = ?) AND ( reference LIKE ? OR status LIKE ? ) +ORDER BY "createdAt"DESC,"id"DESC +LIMIT ? OFFSET ? +-- replacements: ["Paid", "%ORD%", "%ORD%", 21, 0] (fetches limit + 1 = 21) +``` + +```js +// 2. Run initialQueries.query against your DB, then resolve the page. +// (rows below are illustrative) +const fetchedRows = [ + { id: 1006, reference: "ORD-1006", status: "Paid", createdAt: "2026-05-15" }, + { id: 1003, reference: "ORD-1003", status: "Paid", createdAt: "2026-05-06" }, +]; + +const resolvedPage = pagiHelp.resolveCursorPage( + fetchedRows, + initialQueries.cursorPlan +); +// resolvedPage.pageInfo => { hasNextPage, hasPreviousPage, startCursor, endCursor, nextCursor } + +// 3. Next page — pass the endCursor back in as `after`. +// Repeat the SAME search/filters/sort that produced the cursor — only +// `after` changes — or the token is rejected ("Cursor token does not +// match the current query"). +const nextQueries = pagiHelp.paginateCursor( + { + search: "ORD", + filters: [["status", "=", "Paid"]], + sort: { attributes: ["createdAt"], sorts: ["desc"] }, + limit: 20, + after: resolvedPage.pageInfo.endCursor, + }, + [ + { + tableName: "orders", + columnList: [ + { name: "order_id", alias: "id" }, + { name: "reference", alias: "reference" }, + { name: "status", alias: "status" }, + { name: "created_at", alias: "createdAt" }, + ], + searchColumnList: [{ name: "reference" }, { name: "status" }], + }, + ] +); +``` + +## Key points + +- `query` fetches `limit + 1` rows; `resolveCursorPage()` trims the extra one and + computes `pageInfo`. +- `pageInfo.endCursor` (or `nextCursor`) becomes the `after` value for the next + call. +- The cursor token is opaque and self-describing (it encodes the dialect, sort, + a query fingerprint, and the keyset values). +- `countQuery` / `totalCountQuery` stay aggregate, and include the cursor + predicate when `after` is present. diff --git a/site/docs/examples/filtering.md b/site/docs/examples/filtering.md new file mode 100644 index 0000000..2dd3560 --- /dev/null +++ b/site/docs/examples/filtering.md @@ -0,0 +1,225 @@ +# Filtering Examples + +Filters are `[field, operator, value]` tuples. Top-level tuples are joined with +**AND**; a nested array of tuples becomes an **OR** group. Values are always +parameterized into `replacements` (never concatenated). This page covers **every +operator** the library supports, for both dialects. + +All examples build against this neutral `orders` table: + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); // or "postgres" + +const options = [ + { + tableName: "orders", + columnList: [ + { name: "order_id", alias: "id" }, + { name: "reference", alias: "reference" }, + { name: "status", alias: "status" }, + { name: "total", alias: "total" }, + { name: "created_at", alias: "createdAt" }, + // extra columns used by the JSON / array / regex examples below: + { name: "meta_info", alias: "meta" }, + { name: "tags", alias: "tags" }, + { name: "note", alias: "note" }, + ], + searchColumnList: [{ name: "reference" }, { name: "status" }], + }, +]; +``` + +::: tip Try it live +Every scenario here is reproducible in the [interactive Playground](/playground). +::: + +## Combining conditions + +Top-level tuples → **AND**: + +```js +filters: [ + ["status", "=", "Paid"], + ["total", ">=", 10000], +]; +// WHERE (status = ? AND total >= ?) replacements: ["Paid", 10000] +``` + +A nested array of tuples → **OR** group: + +```js +filters: [[["status", "=", "Paid"], ["status", "=", "Pending"]]]; +// WHERE (( status = ? OR status = ?)) replacements: ["Paid", "Pending"] +``` + +Mix them — AND of an OR group: + +```js +filters: [ + ["total", ">=", 10000], + [["status", "=", "Paid"], ["status", "=", "Pending"]], +]; +// WHERE (total >= ? AND ( status = ? OR status = ?)) +``` + +::: tip Single-condition shorthand +A bare tuple like `filters: ["status", "IN", ["Paid"]]` is normalized to +`[["status", "IN", ["Paid"]]]` automatically. +::: + +## A complete query, both dialects + +The same request renders per dialect — note the identifier quoting and the +pagination clause (and the reversed replacement order): + +::: code-group + +```sql [MySQL] +SELECT order_id AS id,reference AS reference,status AS status,total AS total,created_at AS createdAt,meta_info AS meta,tags AS tags,note AS note +FROM `orders` +WHERE (status = ? AND total >= ?) AND ( reference LIKE ? OR status LIKE ? ) +ORDER BY `total`DESC,`id`DESC +LIMIT ?,? +-- replacements: ["Paid", 10000, "%ORD%", "%ORD%", 0, 10] +``` + +```sql [PostgreSQL] +SELECT order_id AS "id",reference AS "reference",status AS "status",total AS "total",created_at AS "createdAt",meta_info AS "meta",tags AS "tags",note AS "note" +FROM "orders" +WHERE (status = ? AND total >= ?) AND ( reference LIKE ? OR status LIKE ? ) +ORDER BY "total"DESC,"id"DESC +LIMIT ? OFFSET ? +-- replacements: ["Paid", 10000, "%ORD%", "%ORD%", 10, 0] +``` + +::: + +```js +// request that produced the SQL above +{ + search: "ORD", + filters: [["status", "=", "Paid"], ["total", ">=", 10000]], + sort: { attributes: ["total"], sorts: ["desc"] }, + pageNo: 1, + itemsPerPage: 10, +} +``` + +## Shared operators (both dialects) + +These render the same `WHERE` fragment on MySQL and PostgreSQL: + +| Filter | Generated `WHERE` | Replacements | +| --- | --- | --- | +| `["status", "=", "Paid"]` | `(status = ?)` | `["Paid"]` | +| `["status", "!=", "Paid"]` | `(status != ?)` | `["Paid"]` | +| `["total", ">=", 10000]` | `(total >= ?)` | `[10000]` | +| `["status", "IN", ["Paid", "Pending"]]` | `(status IN (?,?))` | `["Paid", "Pending"]` | +| `["status", "NOT IN", ["Refunded"]]` | `(status NOT IN (?))` | `["Refunded"]` | +| `["reference", "LIKE", "ORD-10%"]` | `(reference LIKE ?)` | `["ORD-10%"]` | + +Also available: `>`, `<`, `<=`, `<>`. + +### `IS NULL` differs by dialect + +::: code-group + +```sql [MySQL] +-- filters: [["note", "IS", null]] +WHERE (note IS ?) -- replacements: [null] +-- filters: [["note", "IS NOT", null]] +WHERE (note IS NOT ?) -- replacements: [null] +``` + +```sql [PostgreSQL] +-- filters: [["note", "IS", null]] +WHERE (note IS NULL) -- replacements: [] +-- filters: [["note", "IS NOT", null]] +WHERE (note IS NOT NULL) -- replacements: [] +``` + +::: + +::: warning Empty `IN` is rejected +An empty `IN` / `NOT IN` array throws on v2 — it would otherwise produce invalid +SQL. +::: + +## MySQL-only operators + +Use these only with `dialect: "mysql"`: + +| Filter | Generated `WHERE` | Replacements | +| --- | --- | --- | +| `["reference", "RLIKE", "^ORD-10"]` | `(reference RLIKE ?)` | `["^ORD-10"]` | +| `["meta", "JSON_CONTAINS", { featured: true }]` | `(JSON_CONTAINS(meta_info, ?))` | `['{"featured":true}']` | +| `["tags", "JSON_OVERLAPS", ["featured", "gold"]]` | `(JSON_OVERLAPS(tags, ?))` | `['["featured","gold"]']` | +| `["tags", "FIND_IN_SET", "featured"]` | `(FIND_IN_SET(?, tags))` | `["featured"]` | +| `["tags", "MEMBER OF", "featured"]` | `(tags MEMBER OF ?)` | `["featured"]` | + +Object values are JSON-stringified automatically for `JSON_CONTAINS` / +`JSON_OVERLAPS`. + +## PostgreSQL native operators + +Use these only with `dialect: "postgres"`: + +| Filter | Generated `WHERE` | Replacements | +| --- | --- | --- | +| `["reference", "ILIKE", "ord-10%"]` | `(reference ILIKE ?)` | `["ord-10%"]` | +| `["reference", "~", "^ORD"]` | `(reference ~ ?)` | `["^ORD"]` | +| `["note", "~*", "urgent"]` | `(note ~* ?)` | `["urgent"]` | +| `["reference", "!~", "^TMP"]` | `(reference !~ ?)` | `["^TMP"]` | +| `["meta", "@>", { priority: "high" }]` | `((meta_info)::jsonb @> (?::jsonb))` | `['{"priority":"high"}']` | +| `["meta", "<@", { a: 1, b: 2 }]` | `((meta_info)::jsonb <@ (?::jsonb))` | `['{"a":1,"b":2}']` | +| `["tags", "?|", ["featured", "gold"]]` | `((tags)::jsonb ?\| ARRAY[?,?])` | `["featured", "gold"]` | +| `["tags", "?&", ["featured", "gold"]]` | `((tags)::jsonb ?& ARRAY[?,?])` | `["featured", "gold"]` | +| `["tags", "&&", ["featured", "gold"]]` | `(tags && ARRAY[?,?])` | `["featured", "gold"]` | + +Also available: `!~*`, and `?` (single key exists). + +## PostgreSQL compatibility aliases + +So shared MySQL-style code keeps working under `dialect: "postgres"`, these +MySQL-style operators are translated to native PostgreSQL SQL: + +| MySQL-style filter | Rendered on PostgreSQL | +| --- | --- | +| `["meta", "JSON_CONTAINS", { featured: true }]` | `((meta_info)::jsonb @> (?::jsonb))` | +| `["tags", "FIND_IN_SET", "featured"]` | `(array_position(string_to_array(COALESCE(tags::text, ''), ','), ?::text) IS NOT NULL)` | +| `["reference", "RLIKE", "^ORD"]` | `(reference ~ ?)` | +| `["status", "! IN", ["Refunded"]]` | `(status NOT IN (?))` | + +::: warning `! IN` is Postgres-only +On PostgreSQL `! IN` is translated to `NOT IN`. On **MySQL** it renders literally +as `! IN`, which is **not valid MySQL** — use `NOT IN` for MySQL. +::: + +## Raw conditions: `additionalWhereConditions` + +Per option block, these are AND-ed into every query as **raw** SQL (trusted +input, not validated): + +```js +const options = [ + { + tableName: "orders", + columnList: [/* ... */], + searchColumnList: [/* ... */], + additionalWhereConditions: [["orders.org_id", "=", 42]], + }, +]; + +// with paginationObject.filters: [["status", "=", "Paid"]] +// WHERE (orders.org_id = ?) AND (status = ?) replacements: [42, "Paid"] +``` + +::: warning Trusted input only +Never build `additionalWhereConditions`, `statement`, or `joinQuery` from +untrusted user input. Use regular `filters` for anything user-controlled — those +are parameterized and operator-validated. +::: + +See [Filters & Operators](/v2/filters-and-operators) for the reference, and the +[PostgreSQL dialect](/dialects/postgres) page for native-operator details. diff --git a/site/docs/examples/joined-table.md b/site/docs/examples/joined-table.md new file mode 100644 index 0000000..51ea4ad --- /dev/null +++ b/site/docs/examples/joined-table.md @@ -0,0 +1,84 @@ +# Joined Table + +Use `joinQuery` to add a JOIN, `prefix` to qualify columns, and `statement` for +computed columns. This example joins `orders` to `shipments` and uses a +`columnNameConverter` to map camelCase to snake_case. + +```js +const PagiHelpV2 = require("pagi-help/v2"); + +const pagiHelp = new PagiHelpV2({ + dialect: "mysql", + columnNameConverter: (name) => + name.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`), +}); + +const paginationObject = { + search: "NEW", + filters: [ + ["expedited", "=", "Yes"], + ["o.status", "IN", ["NEW", "PACKING"]], + ], + sort: { + attributes: ["createdDate"], + sorts: ["desc"], + }, + pageNo: 1, + itemsPerPage: 20, +}; + +const options = [ + { + tableName: "orders", + columnList: [ + { name: "order_id", prefix: "o", alias: "id" }, + { name: "created_date", prefix: "o", alias: "createdDate" }, + { name: "status", prefix: "o", alias: "status" }, + { + statement: '(SELECT IF(o.priority="1","Yes","No"))', + alias: "expedited", + }, + { name: "carrier", prefix: "s", alias: "carrier" }, + ], + searchColumnList: [ + { name: "carrier", prefix: "s" }, + { name: "status", prefix: "o" }, + ], + joinQuery: " o left join shipments s on o.order_id = s.order_id ", + additionalWhereConditions: [["o.archived", "=", 0]], + }, +]; + +const result = pagiHelp.paginate(paginationObject, options); +``` + +Generated MySQL query: + +```sql +SELECT o.order_id AS id,o.created_date AS createdDate,o.status AS status, + (SELECT IF(o.priority="1","Yes","No")) AS expedited,s.carrier AS carrier +FROM `orders` o left join shipments s on o.order_id = s.order_id +WHERE (o.archived = ?) + AND ((SELECT IF(o.priority="1","Yes","No")) = ? AND o.status IN (?,?)) + AND ( s.carrier LIKE ? OR o.status LIKE ? ) +ORDER BY `created_date`DESC,`id`DESC +LIMIT ?,? +-- replacements: [0, "Yes", "NEW", "PACKING", "%NEW%", "%NEW%", 0, 20] +``` + +## Notes + +- **`prefix`** renders `o.created_date`, `s.carrier`, etc. +- **`statement`** is raw SQL for computed/derived columns (trusted input). The + MySQL `IF()` above is fine in a MySQL context; use Postgres SQL for the + Postgres dialect. +- **Filters can reference** an `alias` (`expedited` → matches the `expedited` + computed column), or a `prefix.column` (`o.status`). +- **`joinQuery`** is concatenated verbatim after `FROM \`orders\``; include the + leading space and table alias yourself. +- **`additionalWhereConditions`** are raw conditions AND-ed into every query. + +::: warning +`joinQuery`, `statement`, and `additionalWhereConditions` are trusted-input SQL. +Never build them from untrusted user input. +::: diff --git a/site/docs/examples/multi-table-union.md b/site/docs/examples/multi-table-union.md new file mode 100644 index 0000000..6d6e93c --- /dev/null +++ b/site/docs/examples/multi-table-union.md @@ -0,0 +1,57 @@ +# Multi-Table Union + +Pass more than one block in `options` to combine several tables with `UNION ALL` +into one paginated result set. PagiHelp aligns the column lists by `alias`, +filling gaps with `NULL` so every block selects the same columns. + +```js +const PagiHelpV2 = require("pagi-help/v2"); + +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); + +const paginationObject = { + search: "", + sort: { + attributes: ["id"], + sorts: ["asc"], + }, + offset: 10, + limit: 20, +}; + +const options = [ + { + tableName: "campaigns", + columnList: [ + { name: "campaign_id", alias: "id" }, + { name: "campaign_name", alias: "name" }, + ], + searchColumnList: [{ name: "campaign_name" }], + additionalWhereConditions: [["status", "=", "Active"]], + }, + { + tableName: "licenses", + columnList: [{ name: "license_id", alias: "id" }], + searchColumnList: [], + additionalWhereConditions: [["status", "=", "Active"]], + }, +]; + +const result = pagiHelp.paginate(paginationObject, options); + +console.log(JSON.stringify(result, null, 2)); +``` + +## How the union is built + +- Each option block becomes one `SELECT ... FROM ...`, joined with `UNION ALL`. +- Column lists are aligned by `alias`. Missing columns are filled with + `(NULL) AS alias` so each `SELECT` is union-compatible. +- `totalCountQuery` sums the per-table aggregate counts: + `SELECT SUM(countValue) AS countValue FROM ( ... UNION ALL ... ) AS totalCounts`. +- The shared `sort` and pagination window apply to the combined set. + +::: tip +Give tables that should not participate in search a `searchColumnList: []`, as the +`licenses` block does above. +::: diff --git a/site/docs/examples/pagination.md b/site/docs/examples/pagination.md new file mode 100644 index 0000000..11fb542 --- /dev/null +++ b/site/docs/examples/pagination.md @@ -0,0 +1,120 @@ +# Pagination Examples + +PagiHelp supports two windowing styles — **page-based** +(`pageNo` + `itemsPerPage`) and **offset-based** (`offset` + `limit`) — and emits +a dialect-specific `LIMIT` clause. For unbounded forward paging, see +[Cursor Pagination](/examples/cursor). + +All examples use this demo table: + +```js +const PagiHelpV2 = require("pagi-help/v2"); + +const options = [ + { + tableName: "orders", + columnList: [ + { name: "order_id", alias: "id" }, + { name: "reference", alias: "reference" }, + { name: "status", alias: "status" }, + { name: "total", alias: "total" }, + { name: "created_at", alias: "createdAt" }, + ], + searchColumnList: [{ name: "reference" }, { name: "status" }], + }, +]; +``` + +::: tip Try it live +Toggle page-based vs offset-based and watch the clause change in the +[interactive Playground](/playground). +::: + +## Page-based (MySQL) + +```js +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); +const result = pagiHelp.paginate({ pageNo: 2, itemsPerPage: 25 }, options); +``` + +```sql +... LIMIT ?,? -- [25, 25] +``` + +The offset is computed as `(pageNo - 1) * itemsPerPage` → `25`, with `25` rows. + +## Offset-based (MySQL) + +```js +const result = pagiHelp.paginate({ offset: 40, limit: 20 }, options); +``` + +```sql +... LIMIT ?,? -- [40, 20] +``` + +MySQL replacements are `[offset, limit]`. + +## Page-based (PostgreSQL) + +```js +const pagiHelp = new PagiHelpV2({ dialect: "postgres" }); +const result = pagiHelp.paginate({ pageNo: 2, itemsPerPage: 25 }, options); +``` + +```sql +... LIMIT ? OFFSET ? -- [25, 25] +``` + +## Offset-based (PostgreSQL) + +```js +const result = pagiHelp.paginate({ offset: 40, limit: 20 }, options); +``` + +```sql +... LIMIT ? OFFSET ? -- [20, 40] +``` + +PostgreSQL replacements are `[limit, offset]` — the reverse order of MySQL. + +## Clause & order cheat-sheet + +| Dialect | Clause | Replacements | +| --- | --- | --- | +| MySQL | `LIMIT ?,?` | `[offset, limit]` | +| PostgreSQL | `LIMIT ? OFFSET ?` | `[limit, offset]` | + +## Rules + +- Provide **either** `pageNo` + `itemsPerPage` **or** `offset` + `limit`. +- Each pair must be supplied together; supplying only one half is rejected. +- If both pairs are given, page-based values win. + +## Counting total rows + +Use the aggregate `totalCountQuery` (returns a `countValue` column) to compute the +total number of pages: + +::: code-group + +```js [mysql2] +const [[{ countValue }]] = await connection.query( + result.totalCountQuery, + result.replacements +); +const totalPages = Math.ceil(countValue / itemsPerPage); +``` + +```js [Sequelize] +const [{ countValue }] = await sequelize.query(result.totalCountQuery, { + replacements: result.replacements, + type: QueryTypes.SELECT, +}); +const totalPages = Math.ceil(countValue / itemsPerPage); +``` + +::: + +On v2, `countQuery` and `totalCountQuery` are the same aggregate query — run +either one, once. See [Return Shape](/v2/return-shape). diff --git a/site/docs/examples/sample-data.md b/site/docs/examples/sample-data.md new file mode 100644 index 0000000..a3d146b --- /dev/null +++ b/site/docs/examples/sample-data.md @@ -0,0 +1,10 @@ +--- +title: Interactive Demo +--- + +# Interactive Demo + +The fastest way to understand PagiHelp is to watch it work against a real table — +everything below runs the real engine in your browser. + + diff --git a/site/docs/examples/searching.md b/site/docs/examples/searching.md new file mode 100644 index 0000000..8fe30b0 --- /dev/null +++ b/site/docs/examples/searching.md @@ -0,0 +1,114 @@ +# Searching Examples + +Free-text search matches one `search` string against every column in +`searchColumnList` using `LIKE` with `%value%` wrapping — on **both** dialects. +The columns are OR-ed together and the whole group is AND-ed with your filters. +See [Search](/v2/search) for the rules. + +All examples use this neutral `orders` table: + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); + +const options = [ + { + tableName: "orders", + columnList: [ + { name: "order_id", alias: "id" }, + { name: "reference", alias: "reference" }, + { name: "status", alias: "status" }, + { name: "total", alias: "total" }, + { name: "created_at", alias: "createdAt" }, + ], + searchColumnList: [{ name: "reference" }, { name: "status" }], + }, +]; +``` + +::: tip Try it live +Tweak the `search` box in the [interactive Playground](/playground). +::: + +## Search across multiple columns + +```js +const result = pagiHelp.paginate( + { search: "ORD", pageNo: 1, itemsPerPage: 10 }, + options +); +``` + +```sql +WHERE ( reference LIKE ? OR status LIKE ? ) -- ["%ORD%", "%ORD%"] +``` + +One replacement is pushed per search column. + +## Search a single column + +With `searchColumnList: [{ name: "reference" }]`: + +```sql +WHERE ( reference LIKE ? ) -- ["%ORD%"] +``` + +## Empty / missing search + +An empty string (or omitted `search`) produces **no** search predicate: + +```js +const result = pagiHelp.paginate( + { search: "", pageNo: 1, itemsPerPage: 10 }, + options +); +``` + +```sql +-- no WHERE from search +SELECT ... FROM `orders` LIMIT ?,? -- [0, 10] +``` + +## Search combined with filters + +The search group is AND-ed after your filters: + +```js +const result = pagiHelp.paginate( + { + search: "ORD", + filters: [["status", "=", "Paid"]], + pageNo: 1, + itemsPerPage: 10, + }, + options +); +``` + +```sql +WHERE (status = ?) AND ( reference LIKE ? OR status LIKE ? ) +-- ["Paid", "%ORD%", "%ORD%"] +``` + +## Excluding a table from search + +Give a table `searchColumnList: []` so it contributes no search predicate — +useful in [multi-table unions](/examples/multi-table-union). + +## Case-insensitive search on PostgreSQL + +`LIKE` is case-sensitive on PostgreSQL. For case-insensitive matching, express it +as a filter with the native `ILIKE` operator instead of relying on `search`: + +```js +const pg = new PagiHelpV2({ dialect: "postgres" }); + +const result = pg.paginate( + { filters: [["reference", "ILIKE", "ord-%"]], pageNo: 1, itemsPerPage: 10 }, + options +); +``` + +```sql +WHERE (reference ILIKE ?) -- ["ord-%"] +``` diff --git a/site/docs/examples/single-table.md b/site/docs/examples/single-table.md new file mode 100644 index 0000000..bc2980b --- /dev/null +++ b/site/docs/examples/single-table.md @@ -0,0 +1,58 @@ +# Single Table + +The simplest case: paginate one table with search, filters, and sorting. + +```js +const PagiHelpV2 = require("pagi-help/v2"); + +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); + +const paginationObject = { + search: "campaign", + filters: [ + ["status", "=", "Active"], + ["created_date", ">=", "2026-01-01"], + ], + sort: { + attributes: ["created_date"], + sorts: ["desc"], + }, + pageNo: 1, + itemsPerPage: 10, +}; + +const options = [ + { + tableName: "campaigns", + columnList: [ + { name: "campaign_id", alias: "id" }, + { name: "campaign_name", alias: "campaign_name" }, + { name: "created_date", alias: "created_date" }, + { name: "status", alias: "status" }, + ], + searchColumnList: [{ name: "campaign_name" }, { name: "status" }], + }, +]; + +const result = pagiHelp.paginate(paginationObject, options); + +console.log(JSON.stringify(result, null, 2)); +``` + +## What you get back + +```js +{ + query, // SELECT ... FROM `campaigns` WHERE ... ORDER BY ... LIMIT ?,? + countQuery, // aggregate COUNT(*) returning countValue + totalCountQuery, // identical to countQuery on v2 + replacements, // positional values +} +``` + +- Top-level filters are AND-ed: `status = ? AND created_date >= ?`. +- `search` adds `( campaign_name LIKE ? OR status LIKE ? )`. +- A trailing `id DESC` tie-breaker is appended to the sort. + +See [Filters & Operators](/v2/filters-and-operators) and +[Return Shape](/v2/return-shape) for the details. diff --git a/site/docs/examples/sorting.md b/site/docs/examples/sorting.md new file mode 100644 index 0000000..4a1a5a5 --- /dev/null +++ b/site/docs/examples/sorting.md @@ -0,0 +1,95 @@ +# Sorting Examples + +Sorting uses two parallel arrays — `attributes` and `sorts` — of equal length. +A deterministic `id DESC` tie-breaker is appended automatically. See +[Sorting](/v2/sorting) for the rules. + +All examples use this demo table: + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); + +const options = [ + { + tableName: "orders", + columnList: [ + { name: "order_id", alias: "id" }, + { name: "reference", alias: "reference" }, + { name: "status", alias: "status" }, + { name: "total", alias: "total" }, + { name: "created_at", alias: "createdAt" }, + ], + searchColumnList: [{ name: "reference" }, { name: "status" }], + }, +]; +``` + +::: tip Try it live +Add and reorder sort fields in the [interactive Playground](/playground). +::: + +## Single column, descending + +```js +sort: { attributes: ["createdAt"], sorts: ["desc"] } +``` + +```sql +ORDER BY `createdAt`DESC,`id`DESC +``` + +The trailing `` `id`DESC `` is the automatic tie-breaker. + +## Single column, ascending + +```js +sort: { attributes: ["total"], sorts: ["asc"] } +``` + +```sql +ORDER BY `total`ASC,`id`DESC +``` + +## Multiple columns + +Sort by status ascending, then total descending: + +```js +sort: { attributes: ["status", "total"], sorts: ["asc", "desc"] } +``` + +```sql +ORDER BY `status`ASC,`total`DESC,`id`DESC +``` + +::: warning +`attributes` and `sorts` must be the **same length**, and each `sorts` entry must +be `"asc"` or `"desc"` (case-insensitive). Anything else is rejected. +::: + +## PostgreSQL identifier quoting + +The same sort under `dialect: "postgres"` uses double quotes: + +```js +const pg = new PagiHelpV2({ dialect: "postgres" }); +// sort: { attributes: ["createdAt"], sorts: ["desc"] } +``` + +```sql +ORDER BY "createdAt"DESC,"id"DESC +``` + +## Your input is never mutated + +PagiHelp does not append `id` to the `sort` arrays you pass in — the tie-breaker +is added only to the generated SQL. Your original `paginationObject.sort` stays +untouched, so you can safely reuse it. + +```js +const sort = { attributes: ["createdAt"], sorts: ["desc"] }; +pagiHelp.paginate({ sort, pageNo: 1, itemsPerPage: 10 }, options); + +console.log(sort.attributes); // still ["createdAt"] — not mutated +``` diff --git a/site/docs/guide/getting-started.md b/site/docs/guide/getting-started.md new file mode 100644 index 0000000..0537f72 --- /dev/null +++ b/site/docs/guide/getting-started.md @@ -0,0 +1,111 @@ +# Getting Started + +PagiHelp (`pagi-help`) is a small helper that turns a pagination request into +safe, parameterized SQL. You describe **what** you want — filters, search, +sorting, page size — and PagiHelp builds the `SELECT`, `WHERE`, `ORDER BY`, and +`LIMIT` clauses plus a `replacements` array you hand to your database driver. + +It does **not** connect to a database. It only generates SQL strings and +replacements, so it works with any MySQL or PostgreSQL client (`mysql2`, `pg`, +Sequelize raw queries, etc.). + +## Install + +```bash +npm install pagi-help +``` + +The only runtime dependency is [`sqlstring`](https://www.npmjs.com/package/sqlstring). + +## Import + +Import the API (MySQL + PostgreSQL): + +```js +const PagiHelpV2 = require("pagi-help/v2"); +``` + +::: tip Existing v1 code? +A legacy **v1** API still ships from the package root (`require("pagi-help")`) for +existing MySQL codebases. New code should use the above — see +[Migrating to v2](/guide/migration). +::: + +## Your first query + +```js +const PagiHelpV2 = require("pagi-help/v2"); + +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); + +const result = pagiHelp.paginate( + { + search: "Active", + filters: [["status", "IN", ["Active", "Paused"]]], + sort: { attributes: ["created_at"], sorts: ["desc"] }, + pageNo: 1, + itemsPerPage: 10, + }, + [ + { + tableName: "events", + columnList: [ + { name: "id", alias: "id" }, + { name: "status", alias: "status" }, + { name: "created_at", alias: "created_at" }, + ], + searchColumnList: [{ name: "status" }], + }, + ] +); + +console.log(result); +// { query, countQuery, totalCountQuery, replacements } +``` + +## Running the generated SQL + +PagiHelp gives you parameterized SQL. Pass `query` and `replacements` to your +driver: + +::: code-group + +```js [mysql2] +const [rows] = await connection.query(result.query, result.replacements); +const [[{ countValue }]] = await connection.query( + result.totalCountQuery, + result.replacements +); +``` + +```js [Sequelize] +const { QueryTypes } = require("sequelize"); + +const rows = await sequelize.query(result.query, { + replacements: result.replacements, + type: QueryTypes.SELECT, +}); +const [{ countValue }] = await sequelize.query(result.totalCountQuery, { + replacements: result.replacements, + type: QueryTypes.SELECT, +}); +``` + +::: + +Note the destructuring difference: mysql2 resolves to a `[rows, fields]` pair, +while Sequelize with `type: QueryTypes.SELECT` returns the rows directly — using +the mysql2 double-destructure there throws `object is not iterable`. + +::: tip +The `replacements` array is positional and already in the right order for the +generated SQL. Always pass it alongside the query — never interpolate values +into the SQL string yourself. +::: + +## How it fits together + +| You provide | PagiHelp builds | +| ----------- | --------------- | +| `paginationObject` (search, filters, sort, page/limit) | the dynamic `WHERE`, `ORDER BY`, and `LIMIT` | +| `options[]` (tableName, columnList, searchColumnList, joins) | the `SELECT ... FROM` and column list | diff --git a/site/docs/guide/installation.md b/site/docs/guide/installation.md new file mode 100644 index 0000000..f7f255b --- /dev/null +++ b/site/docs/guide/installation.md @@ -0,0 +1,52 @@ +# Installation + +## Requirements + +- Node.js (CommonJS `require` is used throughout the examples) +- A MySQL or PostgreSQL database and a client driver of your choice +- One runtime dependency, [`sqlstring`](https://www.npmjs.com/package/sqlstring), is installed automatically + +## Install the package + +::: code-group + +```bash [npm] +npm install pagi-help +``` + +```bash [yarn] +yarn add pagi-help +``` + +```bash [pnpm] +pnpm add pagi-help +``` + +::: + +## Import + +The API (MySQL + PostgreSQL): + +```js +const PagiHelpV2 = require("pagi-help/v2"); +``` + +Named exports are also available from the package root: + +```js +const { + PagiHelpV2, // the current API + PagiHelpV210, // compatibility alias for PagiHelpV2 + PagiHelpLegacy, // the legacy v1 API — see Migrating to v2 +} = require("pagi-help"); +``` + +`PagiHelpV210` is a compatibility alias for `PagiHelpV2`. The legacy v1 export +(`require("pagi-help")`) is covered in [Migrating to v2](/guide/migration). + +## TypeScript + +Type definitions ship with the package (`index.d.ts` and `v2.d.ts`), so no +`@types/*` install is needed. See the [API Reference](/api/reference) for the +exported types. diff --git a/site/docs/guide/migration.md b/site/docs/guide/migration.md new file mode 100644 index 0000000..b8e2aab --- /dev/null +++ b/site/docs/guide/migration.md @@ -0,0 +1,79 @@ +# Migrating to v2 + +v2 (`require("pagi-help/v2")`) is the current, actively developed API. The default +export `require("pagi-help")` is the legacy **v1** class — MySQL-only, its +`paginate()` behavior preserved unchanged for existing consumers. Both take the same +`paginationObject` and `options` shape, so migration is mostly an import swap plus a +few behavior changes. + +## What's new in v2 + +- **PostgreSQL** dialect alongside MySQL (native `@>`, `ILIKE`, `~*`, …) — v1 is MySQL-only +- **Cursor pagination** — [`paginateCursor()`](/v2/cursor-pagination) with opaque, fingerprinted tokens +- **Aggregate `countQuery`** returning `countValue` (v1's was a row-select) +- **Validation on by default** — `paginate()` rejects malformed input; helpers return `{ valid, errors, warnings }` reports +- **Cleaner behavior** — empty `search` handled safely (v1 turned it into `%undefined%`), no dangling `WHERE`, no caller `sort` mutation, no `console.log`, and `Error` objects instead of string throws + +## How to migrate + +### 1. Bump the version, then change the import + +If your `package.json` pins `"pagi-help": "^1.x"`, npm will **never** install 2.x +automatically — a caret stays within the same major. Move the range up first: + +```bash +npm install pagi-help@^2 +``` + +This is safe: `require("pagi-help")` still returns the legacy class with unchanged +`paginate()` behavior, so existing code keeps working. You opt into v2 per file by +importing the v2 entry: + +```js +// Before +const PagiHelp = require("pagi-help"); +const pagiHelp = new PagiHelp(); + +// After +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); +``` + +The constructor now accepts a [`dialect`](/v2/constructor) (`"mysql"` default or +`"postgres"`). + +### 2. Account for behavior changes + +Everything in [What's new in v2](#what-s-new-in-v2) applies automatically. The one +change that usually needs code is `countQuery` — it is now an aggregate returning +`countValue`, identical to `totalCountQuery` (run either one), so read that instead +of counting result rows: + +::: code-group + +```js [mysql2] +const [[{ countValue }]] = await connection.query( + result.countQuery, + result.replacements +); +``` + +```js [Sequelize] +const [{ countValue }] = await sequelize.query(result.countQuery, { + replacements: result.replacements, + type: QueryTypes.SELECT, +}); +``` + +::: + +Everything else — clean `search`, no `WHERE` dangling, no mutation or logging, +stricter validation, `Error` objects — needs no action and is strictly safer. + +### 3. (Optional) adopt new features + +Once on v2 you can use: + +- [PostgreSQL dialect](/dialects/postgres) +- [Cursor pagination](/v2/cursor-pagination) +- Built-in [validation helpers](/v2/overview#validation) diff --git a/site/docs/index.md b/site/docs/index.md new file mode 100644 index 0000000..bd9fb33 --- /dev/null +++ b/site/docs/index.md @@ -0,0 +1,9 @@ +--- +layout: home +title: Pagination that compiles to SQL +titleTemplate: Pagination query builder for MySQL & PostgreSQL + +# No `hero`/`features` frontmatter: the default home hero/features render +# nothing, and the bespoke "Query Console" landing is injected via the +# theme's `home-hero-before` slot (see .vitepress/theme/index.js). +--- diff --git a/site/docs/playground.md b/site/docs/playground.md new file mode 100644 index 0000000..31b42e4 --- /dev/null +++ b/site/docs/playground.md @@ -0,0 +1,35 @@ +--- +title: Playground +outline: false +--- + +# Interactive Playground + +Build a pagination request and watch PagiHelp generate the SQL **live** — this +runs the real `pagi-help/v2` engine in your browser. Switch dialects, add +filters, sorts, search, and a page window, then copy the output. + + + +## Demo schema + +The playground queries this table: + +```js +const options = [ + { + tableName: "orders", + columnList: [ + { name: "order_id", alias: "id" }, + { name: "reference", alias: "reference" }, + { name: "status", alias: "status" }, + { name: "total", alias: "total" }, + { name: "created_at", alias: "createdAt" }, + ], + searchColumnList: [{ name: "reference" }, { name: "status" }], + }, +]; +``` + +Want more recipes? See the worked [examples](/examples/filtering) for filtering, +searching, sorting, and pagination. diff --git a/site/docs/public/favicon.svg b/site/docs/public/favicon.svg new file mode 100644 index 0000000..7a8e428 --- /dev/null +++ b/site/docs/public/favicon.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/site/docs/public/logo.svg b/site/docs/public/logo.svg new file mode 100644 index 0000000..918b13f --- /dev/null +++ b/site/docs/public/logo.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/site/docs/v2/constructor.md b/site/docs/v2/constructor.md new file mode 100644 index 0000000..8824850 --- /dev/null +++ b/site/docs/v2/constructor.md @@ -0,0 +1,57 @@ +# Constructor + +```js +const PagiHelpV2 = require("pagi-help/v2"); + +const pagiHelp = new PagiHelpV2({ + dialect: "mysql", // default + columnNameConverter: (name) => + name.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`), + safeOptions: { + validate: true, + }, +}); +``` + +## Options + +### `dialect` + +- May be `"mysql"` or `"postgres"`. +- Omitted `dialect` defaults to `"mysql"`. +- Controls identifier quoting, the operator set, and the pagination clause. See + [MySQL](/dialects/mysql) and [PostgreSQL](/dialects/postgres). + +### `columnNameConverter` + +A function applied to column `name` values (and `ORDER BY` identifiers) before +they are rendered. Use it to map your code-side naming (e.g. camelCase) to your +database column naming (e.g. snake_case): + +```js +columnNameConverter: (name) => + name.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); +// createdAt -> created_at +``` + +If omitted, names are passed through unchanged (identity function). It does +**not** apply to `statement` expressions — those are raw SQL. + +### `safeOptions` + +```js +safeOptions: { + validate: true, // the only supported safeOptions key +} +``` + +- `safeOptions.validate` is the **only** supported `safeOptions` key. +- It defaults to `true`, so `paginate()` and the cursor builders validate input + and throw on errors. +- Any other `safeOptions` key is rejected. + +## Rules summary + +- `dialect` may be `"mysql"` or `"postgres"`; omitted defaults to `"mysql"`. +- `safeOptions.validate` is the only supported `safeOptions` key; any other key + is rejected. diff --git a/site/docs/v2/cursor-pagination.md b/site/docs/v2/cursor-pagination.md new file mode 100644 index 0000000..a534215 --- /dev/null +++ b/site/docs/v2/cursor-pagination.md @@ -0,0 +1,172 @@ +# Cursor Pagination + +`paginateCursor()` generates keyset (cursor) pagination SQL. It is available on +**v2 only** and uses opaque, self-describing cursor tokens. + +```js +const cursorQueries = pagiHelp.paginateCursor(paginationObject, options); +// { query, countQuery, totalCountQuery, replacements, cursorPlan } +``` + +## Phase-1 rules + +The current implementation is phase 1 and enforces: + +- **single-table only** (exactly one option block) +- **`after` only** (forward paging) +- `sort` is **required** +- `limit` is **required** +- `pageNo`, `itemsPerPage`, `offset`, and `before` are **rejected** +- the selected columns must include alias `id` + +## Building the first page + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "postgres" }); + +const cursorQueries = pagiHelp.paginateCursor( + { + search: "mail", + filters: [["stage", "=", "OPEN"]], + sort: { attributes: ["createdAt"], sorts: ["desc"] }, + limit: 20, + // after: existingCursorToken // omit for the first page + }, + [ + { + tableName: "audit.licenses", + columnList: [ + { name: "license_id", alias: "id" }, + { name: "created_at", alias: "createdAt" }, + { name: "stage", alias: "stage" }, + ], + searchColumnList: [{ name: "stage" }], + }, + ] +); +``` + +## Return shape + +```js +{ + countQuery, + totalCountQuery, + query, + replacements, + cursorPlan, // metadata consumed by the helpers below +} +``` + +- `query` fetches `limit + 1` rows (the extra row detects `hasNextPage`). +- `cursorPlan` carries the normalized sort, dialect, fetch size, and a query + fingerprint that ties a cursor to the query that produced it. + +## Resolving a page + +After running `query`, hand the rows and the `cursorPlan` to +`resolveCursorPage()`. It trims the extra row and returns `pageInfo`: + +::: code-group + +```js [mysql2] +const [rows] = await connection.query( + cursorQueries.query, + cursorQueries.replacements +); +``` + +```js [Sequelize] +const rows = await sequelize.query(cursorQueries.query, { + replacements: cursorQueries.replacements, + type: QueryTypes.SELECT, +}); +``` + +::: + +```js +const page = pagiHelp.resolveCursorPage(rows, cursorQueries.cursorPlan); +// { +// rows: [...], // trimmed to `limit` +// pageInfo: { +// hasNextPage, +// hasPreviousPage, +// startCursor, +// endCursor, +// nextCursor, +// } +// } +``` + +## Fetching the next page + +Feed `page.pageInfo.endCursor` (or `nextCursor`) back in as `after`. The cursor +token is bound to the query that produced it — dialect, `search`, `filters`, and +`sort` are baked into its fingerprint — so the next call **must repeat the same +`search`, `filters`, and `sort`**; only `after` changes. A mismatch is rejected +at runtime with `Cursor token does not match the current query`. + +```js +const next = pagiHelp.paginateCursor( + { + search: "mail", + filters: [["stage", "=", "OPEN"]], + sort: { attributes: ["createdAt"], sorts: ["desc"] }, + limit: 20, + after: page.pageInfo.endCursor, + }, + [ + { + tableName: "audit.licenses", + columnList: [ + { name: "license_id", alias: "id" }, + { name: "created_at", alias: "createdAt" }, + { name: "stage", alias: "stage" }, + ], + searchColumnList: [{ name: "stage" }], + }, + ] +); +``` + +## Cursor token helpers + +| Helper | Purpose | +| --- | --- | +| `encodeCursorFromRow(row, cursorPlan)` | Build an opaque `after` token from one row | +| `decodeCursor(token)` | Decode and validate the token envelope | +| `resolveCursorPage(rows, cursorPlan)` | Trim the extra row and derive `pageInfo` | + +The decoded token envelope looks like: + +```js +{ + v: 1, // version + d: "postgres", // dialect + fp: "...", // query fingerprint + s: [["createdAt", "DESC"], ["id", "DESC"]], // sort + appended id tie-breaker (directions upper-cased) + values: [...], // the keyset values + dir: "after", +} +``` + +## Count semantics + +- `countQuery` and `totalCountQuery` remain **aggregate** on v2. +- When `after` is present, **both** count queries include the cursor predicate. + +## Pagination clause + +- **MySQL** cursor pagination uses `LIMIT ?,?` with replacements `[0, limit + 1]`. +- **PostgreSQL** cursor pagination uses `LIMIT ? OFFSET ?` with replacements + `[limit + 1, 0]`. + +::: tip Roadmap +Phase 1 covers single-table forward (`after`) paging. Backward (`before`) paging +and multi-table cursors are reserved for future phases — `before` is currently +rejected at runtime. +::: + +See the full runnable script in [Examples → Cursor Pagination](/examples/cursor). diff --git a/site/docs/v2/filters-and-operators.md b/site/docs/v2/filters-and-operators.md new file mode 100644 index 0000000..7ab009e --- /dev/null +++ b/site/docs/v2/filters-and-operators.md @@ -0,0 +1,114 @@ +# Filters & Operators + +Filters are how you express `WHERE` conditions. Each condition is a tuple: + +```js +[field, operator, value] +``` + +- `field` resolves against your `columnList` — by `alias`, by camelCase form of + the alias, or by `prefix.column`. +- `operator` must be one of the supported operators (validated). +- `value` is parameterized into `replacements` (never concatenated). + +`filters` is optional — omit it, or pass an empty `filters: []` (allowed since +2.5.1), and no conditions are added. + +## Combining conditions: AND / OR + +Top-level filters are joined with **AND**. Nesting an array of tuples creates an +**OR** group. + +```js +// status = 'Active' AND created_at >= '2026-01-01' +filters: [ + ["status", "=", "Active"], + ["created_at", ">=", "2026-01-01"], +]; +``` + +```js +// status = 'Active' AND ( type = 'A' OR type = 'B' ) +filters: [ + ["status", "=", "Active"], + [ + ["type", "=", "A"], + ["type", "=", "B"], + ], +]; +``` + +::: tip Single condition shorthand +A bare tuple like `filters: ["status", "IN", ["Active"]]` is normalized to +`[["status", "IN", ["Active"]]]` automatically. +::: + +## Values and `IN` + +- Array values render an `IN (?, ?, ...)` list with one replacement per element. +- On v2, an **empty** `IN [ ]` array is rejected (it would otherwise produce + invalid SQL). + +```js +filters: [["status", "IN", ["Active", "Paused"]]]; +// status IN (?,?) replacements: ["Active", "Paused"] +``` + +## Shared operators (both dialects) + +``` +> >= < <= = != <> +IN NOT IN ! IN +IS IS NOT +LIKE RLIKE +MEMBER OF +JSON_CONTAINS JSON_OVERLAPS FIND_IN_SET +``` + +- `! IN` is rewritten to `NOT IN` on **PostgreSQL only**. On MySQL it is emitted + verbatim (`field ! IN (...)`), which is not valid SQL — use `NOT IN` for MySQL. +- `JSON_CONTAINS(field, ?)` and `JSON_OVERLAPS(field, ?)` JSON-stringify object + values automatically. +- `FIND_IN_SET` renders as `FIND_IN_SET(?, field)`. +- Renderings shown are the **MySQL** forms; on Postgres these compat aliases + compile to jsonb/array expressions — see + [operator compatibility](/dialects/postgres#operator-compatibility). + +## PostgreSQL native operators + +When the dialect is `postgres`, these additional native operators are available: + +| Operator | Meaning | +| --- | --- | +| `ILIKE` | case-insensitive `LIKE` | +| `~` `~*` `!~` `!~*` | regex match / case-insensitive / negated | +| `@>` `<@` | contains / contained-by (jsonb, arrays) | +| `?` `?|` `?&` | key exists / any key / all keys (jsonb) | +| `&&` | array overlap | + +```js +filters: [ + ["metaInfo", "@>", { priority: "high" }], + ["tags", "?|", ["featured", "priority"]], + ["reference", "~*", "^ord-2026-"], +]; +``` + +Postgres also keeps compatibility aliases so shared MySQL-style code keeps +working — see [PostgreSQL dialect](/dialects/postgres#operator-compatibility). + +## Raw conditions: `additionalWhereConditions` + +`additionalWhereConditions` (per option block) are **raw** conditions AND-ed into +every generated query. They are trusted input — operators are not validated and +the SQL is concatenated as written: + +```js +additionalWhereConditions: [["audit.licenses.organization_id", "=", 42]]; +``` + +::: warning +Never build `additionalWhereConditions`, `statement`, or `joinQuery` from +untrusted user input. Use regular `filters` for anything user-controlled — those +are parameterized and operator-validated. +::: diff --git a/site/docs/v2/overview.md b/site/docs/v2/overview.md new file mode 100644 index 0000000..851f02b --- /dev/null +++ b/site/docs/v2/overview.md @@ -0,0 +1,76 @@ +# Overview + +PagiHelp turns a pagination request into safe, parameterized SQL. Import it +directly: + +```js +const PagiHelpV2 = require("pagi-help/v2"); +``` + +It supports both MySQL and PostgreSQL, generates aggregate count queries, never +mutates your input, and is where features like cursor pagination live. + +## The shape of a request + +Every paginate call takes two arguments: + +1. A **`paginationObject`** — the dynamic request: `search`, `filters`, `sort`, + and the page window (`pageNo`/`itemsPerPage` or `offset`/`limit`). +2. An **`options`** array — one block per table describing `tableName`, + `columnList`, `searchColumnList`, optional `joinQuery`, and optional + `additionalWhereConditions`. + +```js +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); + +const result = pagiHelp.paginate(paginationObject, options); +// { query, countQuery, totalCountQuery, replacements } +``` + +## Methods + +| Method | Purpose | +| --- | --- | +| [`paginate(paginationObject, options)`](/v2/paginate) | Build offset/page-based SQL | +| [`paginateCursor(paginationObject, options)`](/v2/cursor-pagination) | Build keyset/cursor SQL (single table) | +| `paginateSafe(paginationObject, options, safeOptions)` | Validate then build, throwing on invalid input | +| [`resolveCursorPage(rows, cursorPlan)`](/v2/cursor-pagination#resolving-a-page) | Trim the extra row and derive `pageInfo` | +| [`encodeCursorFromRow(row, cursorPlan)`](/v2/cursor-pagination) | Build an opaque `after` token | +| [`decodeCursor(token)`](/v2/cursor-pagination) | Decode and validate a cursor token | +| `validatePaginationInput(paginationObject, options)` | Return a `{ valid, errors, warnings }` report | +| `validateCursorPaginationInput(...)` | Validation report for cursor input | + +## Column descriptors + +Columns in `columnList` are described by objects. A descriptor must define +**exactly one** of `name` or `statement`: + +```js +{ name: "created_at", alias: "createdAt" } // plain column +{ name: "id", prefix: "l", alias: "id" } // prefixed: l.id +{ statement: "(CASE WHEN ... END)", alias: "flag" } // raw SQL expression +``` + +- `name` — the column name (passed through `columnNameConverter`) +- `prefix` — optional table alias/prefix, e.g. `l` → `l.column` +- `statement` — raw SQL expression (trusted input), mutually exclusive with `name` +- `alias` — the output alias; recommended for filters, sorts, and unions + +`searchColumnList` uses the same descriptor shape **without** `alias` (v2 rejects +aliases there). + +## Validation + +v2 ships validation helpers that return a structured report instead of throwing: + +```js +const report = pagiHelp.validatePaginationInput(paginationObject, options); +// { valid: boolean, errors: string[], warnings: string[] } + +if (!report.valid) { + throw new Error(report.errors.join("\n")); +} +``` + +`paginateSafe()` runs this validation for you and throws an `Error` if the input +is invalid (controlled by `safeOptions.validate`, which defaults to `true`). diff --git a/site/docs/v2/paginate.md b/site/docs/v2/paginate.md new file mode 100644 index 0000000..2f9e379 --- /dev/null +++ b/site/docs/v2/paginate.md @@ -0,0 +1,124 @@ +# paginate() + +```js +const result = pagiHelp.paginate(paginationObject, options); +``` + +Builds offset/page-based pagination SQL. Returns `{ query, countQuery, +totalCountQuery, replacements }` — see [Return Shape](/v2/return-shape). + +## `paginationObject` + +```js +{ + search: "Active", // optional free-text search + filters: [["status", "IN", ["Active", "Paused"]]], // optional conditions + sort: { // optional ordering + attributes: ["created_at"], + sorts: ["desc"], + }, + pageNo: 1, // page-based window ... + itemsPerPage: 10, + // ... or offset-based window: + // offset: 10, + // limit: 20, +} +``` + +| Field | Description | +| --- | --- | +| `search` | Free-text value matched with `LIKE` against `searchColumnList`. See [Search](/v2/search). | +| `filters` | `[field, operator, value]` tuples, nestable into AND/OR groups. See [Filters & Operators](/v2/filters-and-operators). | +| `sort` | `{ attributes, sorts }` parallel arrays. See [Sorting](/v2/sorting). | +| `pageNo` + `itemsPerPage` | Page-based window. Both required together. | +| `offset` + `limit` | Offset-based window. Both required together. | + +::: tip Page window rules +Provide **either** `pageNo`/`itemsPerPage` **or** `offset`/`limit`. Each pair +must be supplied together. If both pairs are given, page-based values win. +Note: `offset: 0` emits **no** pagination clause — use `pageNo: 1` + +`itemsPerPage` for the first page. +::: + +## `options` + +An array of one or more table blocks. Multiple blocks are combined with +`UNION ALL` — see [Multi-Table Union](/examples/multi-table-union). + +```js +[ + { + tableName: "events", + columnList: [ + { name: "id", alias: "id" }, + { name: "status", alias: "status" }, + { name: "created_at", alias: "created_at" }, + ], + searchColumnList: [{ name: "status" }], + joinQuery: "", // optional raw JOIN SQL + additionalWhereConditions: [], // optional raw conditions (AND-ed in) + }, +] +``` + +| Field | Description | +| --- | --- | +| `tableName` | Table name. Postgres supports `schema.table`. | +| `columnList` | Column descriptors to select. Should include alias `id`. | +| `searchColumnList` | Columns matched by `search` (no `alias` allowed on v2). | +| `joinQuery` | Raw JOIN SQL appended after `FROM`. Trusted input. | +| `additionalWhereConditions` | Raw conditions AND-ed into every query. Trusted input. | + +::: warning Trusted-input fields +`joinQuery`, `statement`, and `additionalWhereConditions` are concatenated as raw +SQL (operators are not validated for these). Never build them from untrusted user +input. Regular `filters` and `search` values **are** parameterized. +::: + +## Full example + +```js +const PagiHelpV2 = require("pagi-help/v2"); +const pagiHelp = new PagiHelpV2({ dialect: "mysql" }); + +const result = pagiHelp.paginate( + { + search: "Active", + filters: [["status", "IN", ["Active", "Paused"]]], + sort: { attributes: ["created_at"], sorts: ["desc"] }, + pageNo: 1, + itemsPerPage: 10, + }, + [ + { + tableName: "events", + columnList: [ + { name: "id", alias: "id" }, + { name: "status", alias: "status" }, + { name: "created_at", alias: "created_at" }, + ], + searchColumnList: [{ name: "status" }], + }, + ] +); +``` + +## paginateSafe() + +`paginateSafe(paginationObject, options, safeOptions)` validates the input first +(via `validatePaginationInput`) and throws an `Error` listing all problems before +building SQL. With v2 defaults (`validate: true`), `paginate()` already runs in a +hardened mode; `paginateSafe()` makes the validate-then-build step explicit. + +```js +const report = pagiHelp.validatePaginationInput(paginationObject, options); +if (!report.valid) throw new Error(report.errors.join("\n")); + +const result = pagiHelp.paginateSafe(paginationObject, options); +``` + +## Related + +- [Return Shape](/v2/return-shape) +- [Cursor Pagination](/v2/cursor-pagination) for keyset-based paging +- [Examples](/examples/single-table) diff --git a/site/docs/v2/return-shape.md b/site/docs/v2/return-shape.md new file mode 100644 index 0000000..fd6a1ce --- /dev/null +++ b/site/docs/v2/return-shape.md @@ -0,0 +1,60 @@ +# Return Shape + +Both `paginate()` and `paginateSafe()` return the same object: + +```js +{ + query, // the row SELECT with WHERE, ORDER BY, and LIMIT + countQuery, // aggregate COUNT(*) returning countValue + totalCountQuery, // identical to countQuery on v2 + replacements, // positional values for query / count queries +} +``` + +On v2, `countQuery` and `totalCountQuery` are the **same aggregate query** — both +fields exist because they differed in [v1](/guide/migration) (row-select vs +aggregate). Run either one, once. + +[`paginateCursor()`](/v2/cursor-pagination) returns the same fields **plus** a +`cursorPlan`. + +## Using the result + +::: code-group + +```js [mysql2] +const [rows] = await connection.query(result.query, result.replacements); +const [[{ countValue }]] = await connection.query( + result.totalCountQuery, + result.replacements +); +``` + +```js [Sequelize] +const { QueryTypes } = require("sequelize"); + +const rows = await sequelize.query(result.query, { + replacements: result.replacements, + type: QueryTypes.SELECT, +}); +const [{ countValue }] = await sequelize.query(result.totalCountQuery, { + replacements: result.replacements, + type: QueryTypes.SELECT, +}); +``` + +::: + +The aggregate count queries return a single column named `countValue`. mysql2 +resolves to a `[rows, fields]` pair (hence the double destructure); Sequelize +with `type: QueryTypes.SELECT` returns the rows directly. + +## Pagination clause by dialect + +When a page window is given, the `query` ends with a dialect-specific pagination +clause: + +- **MySQL**: `LIMIT ?,?` with replacements `[offset, limit]` +- **PostgreSQL**: `LIMIT ? OFFSET ?` with replacements `[limit, offset]` + +See [MySQL](/dialects/mysql) and [PostgreSQL](/dialects/postgres) for details. diff --git a/site/docs/v2/search.md b/site/docs/v2/search.md new file mode 100644 index 0000000..bbec770 --- /dev/null +++ b/site/docs/v2/search.md @@ -0,0 +1,52 @@ +# Search + +Free-text search matches a single `search` string against the columns listed in +each option block's `searchColumnList`, using `LIKE` with `%value%` wrapping — +on **both** dialects. (For case-insensitive search on PostgreSQL, use the native +`ILIKE` operator via [filters](/v2/filters-and-operators#postgresql-native-operators).) + +```js +const result = pagiHelp.paginate( + { + search: "campaign", + // ... + }, + [ + { + tableName: "campaigns", + columnList: [ + { name: "campaign_id", alias: "id" }, + { name: "campaign_name", alias: "name" }, + ], + searchColumnList: [{ name: "campaign_name" }, { name: "status" }], + }, + ] +); +// ... WHERE ( campaign_name LIKE ? OR status LIKE ? ) +// replacements include "%campaign%" once per search column +``` + +## How it works + +- Each column in `searchColumnList` produces `column LIKE ?`, joined with `OR`. +- The value `%${search}%` is pushed into `replacements` once per search column. +- The whole search group is AND-ed with your filters and additional conditions. + +## Rules + +- `searchColumnList` descriptors use the same shape as `columnList` **without** + `alias`. On v2, an `alias` in `searchColumnList` is **rejected**. +- A missing or empty `search` produces **no** search predicate. +- A missing `searchColumnList` is treated as `[]` (no search). + +::: tip +Provide `searchColumnList: []` for tables that should not participate in search — +useful in [multi-table unions](/examples/multi-table-union) where only some +tables are searchable. +::: + +## Dialect note + +`LIKE` is shared across dialects. For case-insensitive search on PostgreSQL you +can express conditions with the native `ILIKE` operator via +[filters](/v2/filters-and-operators#postgresql-native-operators). diff --git a/site/docs/v2/sorting.md b/site/docs/v2/sorting.md new file mode 100644 index 0000000..982d060 --- /dev/null +++ b/site/docs/v2/sorting.md @@ -0,0 +1,50 @@ +# Sorting + +Sorting is described by two parallel arrays inside `paginationObject.sort`: + +```js +sort: { + attributes: ["created_at", "name"], + sorts: ["desc", "asc"], +} +// your sort → ORDER BY `created_at`DESC,`name`ASC +// (plus an automatic `id`DESC tie-breaker — see below) +``` + +## Rules + +- `attributes` and `sorts` must be arrays of the **same length**. +- Each entry in `sorts` must be `"asc"` or `"desc"` (case-insensitive; normalized + to uppercase). Any other value is rejected. +- Sort identifiers are escaped and passed through your `columnNameConverter`. + +## Automatic `id` tie-breaker + +PagiHelp appends `id` (descending) as a final tie-breaker so ordering is +deterministic and stable across pages: + +```js +sort: { attributes: ["created_at"], sorts: ["desc"] } +// effective: ORDER BY `created_at`DESC,`id`DESC +``` + +For this to work, your `columnList` should expose a column with `alias: "id"`. + +::: tip Your input is not mutated +PagiHelp does not push `id` onto the `sort` arrays you pass in — your original +object is left untouched. +::: + +## Dialect quoting + +The `ORDER BY` identifiers are quoted per dialect: + +- **MySQL** uses backticks: `` ORDER BY `created_at`DESC `` +- **PostgreSQL** uses double quotes: `ORDER BY "created_at"DESC` + +## Cursor mode + +For [cursor pagination](/v2/cursor-pagination), `sort` is **required** and forms +the keyset ordering. An `id` tie-breaker is appended here too, but it follows the +direction of your **last** sort entry (not always `DESC`) so the keyset stays +consistent. diff --git a/site/package-lock.json b/site/package-lock.json new file mode 100644 index 0000000..5ef3d9e --- /dev/null +++ b/site/package-lock.json @@ -0,0 +1,2359 @@ +{ + "name": "pagi-help-site", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pagi-help-site", + "version": "1.0.0", + "dependencies": { + "pagi-help": "^2.5.4" + }, + "devDependencies": { + "vitepress": "^1.6.3" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.21.1", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.21.1.tgz", + "integrity": "sha512-Wia5/mNTfiU0PIUN25UMfAGGdASkkwuCS9nBAdmhqrNPY/ff7U/6MgBVdwFDPsa3sA1msutPtO50gvOzx6MOXA==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.7.tgz", + "integrity": "sha512-BjiPOW6ks90UKl7TwMv7oNQMnzU+t/wk9mgIDi6b1tXpUek7MW0lbNOUHpvam9pe3lVCf4xPFT+lK7s+e+fs7Q==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.17.7", + "@algolia/autocomplete-shared": "1.17.7" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.7.tgz", + "integrity": "sha512-Jca5Ude6yUOuyzjnz57og7Et3aXjbwCSDf/8onLHSQgw1qW3ALl9mrMWaXb5FmPVkV3EtkD2F/+NkT6VHyPu9A==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.7.tgz", + "integrity": "sha512-ggOQ950+nwbWROq2MOCIL71RE0DdQZsceqrg32UqnhDz8FlO9rL8ONHNsI2R1MH0tkgVIDKI/D0sMiUchsFdWA==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-shared": "1.17.7" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.17.7", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.7.tgz", + "integrity": "sha512-o/1Vurr42U/qskRSuhBH+VKxMvkkUVTLU6WZQr+L5lGZZLYWyhdzWjW0iGXY7EkwRTjBqvN2EsR81yCTGV/kmg==", + "dev": true, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.55.1.tgz", + "integrity": "sha512-miW8RzAtBgNiEJ9fGEhsOPgWUpekAe64YcVufqXrlykj0Jjmo5nj0a5f/HAzRVX5ZuU1GAVd7BkzFDx7q50P3A==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.55.1.tgz", + "integrity": "sha512-eR3J3kB9JX6DdCvDRi3I4KPfwO6fR9HWYRXhVke2TXIoOQafMKCRAneg33JRmIrb+DnnJ/eWApJLF1O1CLPERg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.55.1.tgz", + "integrity": "sha512-P5ak7EurwYqgAiDyb95mgA3WRR/Zu8CPMv36lWTISvL2AmlPyqQPy2nX/KEJRTcwaeTWwrk6wJV4/M93GfjOWw==", + "dev": true, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.55.1.tgz", + "integrity": "sha512-OVtj9uA//+pjvKQI5INnzbyLrf3ClNv3XRbWswwJ2kHIStQNHtBfHo+LofNB/WhM9xjuXlW5ANn2aMj65UGx7w==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.55.1.tgz", + "integrity": "sha512-oKlVFlp+qbIEe4p7E54zSiP2gEV/vDu972Ykv8VDMFwEvreS7m0YKA3a8hGGHwc7yiBUGGiR3LlwzMLfnJmy6Q==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.55.1.tgz", + "integrity": "sha512-BOVrld6vdtsFmotVDMTVQfYXwrVplJ+DUvy60JFi+tkWV698q2J9NNPKEO3dr5qxtSLKQP4vHF8n+3U5PDWhOQ==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.55.1.tgz", + "integrity": "sha512-GAqHl9zERhC3bbBfubwUu07G3UXO06gORvOcsiTBZB3et0s3auNUbHlYdYNp4VKa3sUZqH5AcD3OKzU/KDGXjQ==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.55.1.tgz", + "integrity": "sha512-BXZw+C+gsWL7pZvbnhJUnCXASiDLGcQxVV7h55Pyh2DmSzwdZIVccE5xc9RVD2trtrhIqk5smuODTxtaZqd0IA==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.55.1", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.55.1.tgz", + "integrity": "sha512-9g/ceZrZTqA62FA3588Xj0onRPjDNfu0pVQqefK0rrHp9H6Wblph/YmzGjZ2g8uqbTh0ZGIvAGCzErU8f7MHpA==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.55.1.tgz", + "integrity": "sha512-cZTIrGyAP+W4A6jDVwvWM/JOaoJKQkD/2a5eLUEeNdKAD45jN7BCpsMDONyhZlosLa4UwL8uiINQzj4iFy9nqg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.55.1.tgz", + "integrity": "sha512-N6I3leW0UO8Y9Zv90yo2UHgYGuxZO0mjbvzNxDIJDjO0qECEF7Z9XMvSNeUWXQh/iNDA9lr8MfEy3rmZGIcclw==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.55.1.tgz", + "integrity": "sha512-ukU5zeeFs44rQkzv+TRdYard+d+3lmPGs8lPZhHtWE8rfz+LlBSF6s9kP3VQ7LeOYL8Dz0u6tZfnyTrqrumbHQ==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.55.1.tgz", + "integrity": "sha512-lCwXyijwPm3vbYHpBXPRomMcD6mgiptmps27gnMCf4HK+u/AOeFPBnIFh4V3l4A5SnP9VRiKBZqwGBpUH0vaTg==", + "dev": true, + "dependencies": { + "@algolia/client-common": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@docsearch/css": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.8.2.tgz", + "integrity": "sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==", + "dev": true + }, + "node_modules/@docsearch/js": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.8.2.tgz", + "integrity": "sha512-Q5wY66qHn0SwA7Taa0aDbHiJvaFJLOJyHmooQ7y8hlwwQLQ/5WwCcoX0g7ii04Qi2DJlHsd0XXzJ8Ypw9+9YmQ==", + "dev": true, + "dependencies": { + "@docsearch/react": "3.8.2", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.8.2.tgz", + "integrity": "sha512-xCRrJQlTt8N9GU0DG4ptwHRkfnSnD/YpdeaXe02iKfqs97TkZJv60yE+1eq/tjPcVnTW8dP5qLP7itifFVV5eg==", + "dev": true, + "dependencies": { + "@algolia/autocomplete-core": "1.17.7", + "@algolia/autocomplete-preset-algolia": "1.17.7", + "@docsearch/css": "3.8.2", + "algoliasearch": "^5.14.2" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@iconify-json/simple-icons": { + "version": "1.2.88", + "resolved": "https://registry.npmjs.org/@iconify-json/simple-icons/-/simple-icons-1.2.88.tgz", + "integrity": "sha512-+cvi1qCuvReL29ehi6t62L4fb7GDXe+UlGHFcsJcV7I2l9wtqn9XE2IBKcDr3CI5iGUGS5ISnXv699pSGpyx1Q==", + "dev": true, + "dependencies": { + "@iconify/types": "*" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-2.5.0.tgz", + "integrity": "sha512-uu/8RExTKtavlpH7XqnVYBrfBkUc20ngXiX9NSrBhOVZYv/7XQRKUyhtkeflY5QsxC0GbJThCerruZfsUaSldg==", + "dev": true, + "dependencies": { + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4", + "hast-util-to-html": "^9.0.4" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-2.5.0.tgz", + "integrity": "sha512-VjnOpnQf8WuCEZtNUdjjwGUbtAVKuZkVQ/5cHy/tojVVRIRtlWMYVjyWhxOmIq05AlSOv72z7hRNRGVBgQOl0w==", + "dev": true, + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^3.1.0" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-2.5.0.tgz", + "integrity": "sha512-pGd1wRATzbo/uatrCIILlAdFVKdxImWJGQ5rFiB5VZi2ve5xj3Ax9jny8QvkaV93btQEwR/rSz5ERFpC5mKNIw==", + "dev": true, + "dependencies": { + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-2.5.0.tgz", + "integrity": "sha512-Qfrrt5OsNH5R+5tJ/3uYBBZv3SuGmnRPejV9IlIbFH3HTGLDlkqgHymAlzklVmKBjAaVmkPkyikAV/sQ1wSL+w==", + "dev": true, + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-2.5.0.tgz", + "integrity": "sha512-wGrk+R8tJnO0VMzmUExHR+QdSaPUl/NKs+a4cQQRWyoc3YFbUzuLEi/KWK1hj+8BfHRKm2jNhhJck1dfstJpiw==", + "dev": true, + "dependencies": { + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/transformers": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-2.5.0.tgz", + "integrity": "sha512-SI494W5X60CaUwgi8u4q4m4s3YAFSxln3tzNjOSYqq54wlVgz0/NbbXEb3mdLbqMBztcmS7bVTaEd2w0qMmfeg==", + "dev": true, + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/types": "2.5.0" + } + }, + "node_modules/@shikijs/types": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-2.5.0.tgz", + "integrity": "sha512-ygl5yhxki9ZLNuNpPitBWvcy9fsSKKaRuO4BAlMyagszQidxcpLAr0qiW/q43DtSIDxO6hEbtYLiFZNXO/hdGw==", + "dev": true, + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==", + "dev": true + }, + "node_modules/@types/markdown-it": { + "version": "14.1.2", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-14.1.2.tgz", + "integrity": "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==", + "dev": true, + "dependencies": { + "@types/linkify-it": "^5", + "@types/mdurl": "^2" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==", + "dev": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.21", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz", + "integrity": "sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==", + "dev": true + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.2.tgz", + "integrity": "sha512-5jsZFwgR5rTdKwidH9Qmat75RKwqfpKlWWB1frDkljN127mwqBu8K0PYo7/hFpF03IEJpfVPpCQDY/eDx3iHvA==", + "dev": true + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.4.tgz", + "integrity": "sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0 || ^6.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.39.tgz", + "integrity": "sha512-16KBTEXAJCpDr0mwlw+AZyhu8iyC7R3S2vBwsI7QnWJU6X3WKc9VKeNEZpiMdZ569qWhz9574L3vV55qRL0Vtw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/shared": "3.5.39", + "entities": "^7.0.1", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.39.tgz", + "integrity": "sha512-oQPigALqYbNxTNPvNgSOe+czwVExfbVF02lz8jP0S3AXJiu3jxYDygNUiqSep4ezzW8XgnubqH63My2A7JR/vg==", + "dev": true, + "dependencies": { + "@vue/compiler-core": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.39.tgz", + "integrity": "sha512-d0ki86iOyN8LoZPBmk5SJWNwHP19CnDDCfuo//+2WJa2g5Ke0Jay983PIBIcSSzldC68I8DrD5GrHV3OSDfodg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.7", + "@vue/compiler-core": "3.5.39", + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.21", + "postcss": "^8.5.15", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.39.tgz", + "integrity": "sha512-Ce7/wvwMHai74bdszfXExdazFigYnlF9zgCmEQUcM1j0fOymlouZ7XilTYNo8oUjhlnjYOZbGrcYKuqjz89Ucw==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/devtools-api": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-7.7.10.tgz", + "integrity": "sha512-KxtEpUOOpFz/qOGRrAwA36QF7DqIA+FXgCYit9mk9wjbaZt0sXOFz81ElOZtKA4HbWHUdwNjZHBFsFFyp5BZiA==", + "dev": true, + "dependencies": { + "@vue/devtools-kit": "^7.7.10" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.7.10.tgz", + "integrity": "sha512-3WNi2Kq4tbpVbmhml7RiphmAt0279oh3fKNeWMQIrltfX8Q91b4i5PL8DtyNKdwmcsGrV4fg+erwWOmD05CLIw==", + "dev": true, + "dependencies": { + "@vue/devtools-shared": "^7.7.10", + "birpc": "^2.3.0", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.2" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.7.10", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.7.10.tgz", + "integrity": "sha512-wOPslzB8vTvpxwdaOcR2qAbwmuSP0L+rhpoC6Cf56V3Jip+HWb7PQQXOUPgBNQARpXsbQX/+mvi8kKucmBGRwQ==", + "dev": true, + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.39.tgz", + "integrity": "sha512-TpsuBJ9gGlZa5d23XcM2y8EXanz9dZeVDQBXRwzy46ItgvM+rWpzs+UVM0wcRLxGvcav0HE5jz2gNL53xlRAog==", + "dev": true, + "dependencies": { + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.39.tgz", + "integrity": "sha512-9GLtNyRvPAUMbX+7ono0RC2j0guo2LXVi8LvcmAooImACUKm0oFf0jjwbX8/H0AE/t1nxhAkn8RSl9PMCzzxZw==", + "dev": true, + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/shared": "3.5.39" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.39.tgz", + "integrity": "sha512-7Y6aAGboKcXAZ3ECuUy7RrS5yy2r47dhTp2SKaJmYxjopImaVFaNa5Ne66NwGovsrxVAl5S5rwc7m22UG7Lmww==", + "dev": true, + "dependencies": { + "@vue/reactivity": "3.5.39", + "@vue/runtime-core": "3.5.39", + "@vue/shared": "3.5.39", + "csstype": "^3.2.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.39.tgz", + "integrity": "sha512-yZSakiAGw85rZfG7UM8akMnIF+FmeiNk47uvHf2nVBBSe+dIKUhZuZq9+XgJhbV3nS5Z4ALH23/MpXofW+mbcw==", + "dev": true, + "dependencies": { + "@vue/compiler-ssr": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "vue": "3.5.39" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.39.tgz", + "integrity": "sha512-l1rrBtBfTnmxvtsvdQDXltUUy8S1Y+ZaqdfUzmAnJkTd8Z8rv5v/ytW+TKiqEOWyHPoqtPlNFSs0lhRmYVSHVA==", + "dev": true + }, + "node_modules/@vueuse/core": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-12.8.2.tgz", + "integrity": "sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==", + "dev": true, + "dependencies": { + "@types/web-bluetooth": "^0.0.21", + "@vueuse/metadata": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-12.8.2.tgz", + "integrity": "sha512-fbGYivgK5uBTRt7p5F3zy6VrETlV9RtZjBqd1/HxGdjdckBgBM4ugP8LHpjolqTj14TXTxSK1ZfgPbHYyGuH7g==", + "dev": true, + "dependencies": { + "@vueuse/core": "12.8.2", + "@vueuse/shared": "12.8.2", + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^5", + "drauu": "^0.4", + "focus-trap": "^7", + "fuse.js": "^7", + "idb-keyval": "^6", + "jwt-decode": "^4", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^7" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-12.8.2.tgz", + "integrity": "sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "12.8.2", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-12.8.2.tgz", + "integrity": "sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==", + "dev": true, + "dependencies": { + "vue": "^3.5.13" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/algoliasearch": { + "version": "5.55.1", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.55.1.tgz", + "integrity": "sha512-FyaFnnsbVPtevQwqSj/SdxE3jAsSsY0BEH8IVLf9rXxEBdAhAmT6VKCVSMWoaPIHVN1Eufh/1w8q6k8URpIkWw==", + "dev": true, + "dependencies": { + "@algolia/abtesting": "1.21.1", + "@algolia/client-abtesting": "5.55.1", + "@algolia/client-analytics": "5.55.1", + "@algolia/client-common": "5.55.1", + "@algolia/client-insights": "5.55.1", + "@algolia/client-personalization": "5.55.1", + "@algolia/client-query-suggestions": "5.55.1", + "@algolia/client-search": "5.55.1", + "@algolia/ingestion": "1.55.1", + "@algolia/monitoring": "1.55.1", + "@algolia/recommend": "5.55.1", + "@algolia/requester-browser-xhr": "5.55.1", + "@algolia/requester-fetch": "5.55.1", + "@algolia/requester-node-http": "5.55.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/birpc": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", + "integrity": "sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/copy-anything": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-4.0.5.tgz", + "integrity": "sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA==", + "dev": true, + "dependencies": { + "is-what": "^5.2.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/emoji-regex-xs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz", + "integrity": "sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==", + "dev": true + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/focus-trap": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.8.0.tgz", + "integrity": "sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==", + "dev": true, + "dependencies": { + "tabbable": "^6.4.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-what": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-5.5.0.tgz", + "integrity": "sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mark.js": { + "version": "8.11.1", + "resolved": "https://registry.npmjs.org/mark.js/-/mark.js-8.11.1.tgz", + "integrity": "sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==", + "dev": true + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "dev": true, + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/minisearch": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.2.0.tgz", + "integrity": "sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg==", + "dev": true + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/oniguruma-to-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", + "integrity": "sha512-bUH8SDvPkH3ho3dvwJwfonjlQ4R80vjyvrU8YpxuROddv55vAEJrTuCuCVUhhsHbtlD9tGGbaNApGQckXhS8iQ==", + "dev": true, + "dependencies": { + "emoji-regex-xs": "^1.0.0", + "regex": "^6.0.1", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/pagi-help": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/pagi-help/-/pagi-help-2.5.4.tgz", + "integrity": "sha512-pYA5HWZEvm3IEb4XRpJ/kTcIZclr2Mdb9yi7PD5Tq8lNH4XWij6F56LphwO/AkFS1WC26rzcs5Vj+JJv+ffJZQ==", + "dependencies": { + "sqlstring": "^2.3.3" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.29.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.29.3.tgz", + "integrity": "sha512-D9NL1GAnJZhc3RndVs4gDdxEeU9TcHgywMrhhOsnpdlvFjdbx0gAsLUnH6JEhlJH5giL7Tx5biWPUSEXE/HPzw==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "dev": true, + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "dev": true, + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "dev": true + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/search-insights": { + "version": "2.17.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", + "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", + "dev": true, + "peer": true + }, + "node_modules/shiki": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-2.5.0.tgz", + "integrity": "sha512-mI//trrsaiCIPsja5CNfsyNOqgAZUb6VpJA+340toL42UpzQlXpwRV9nch69X6gaUxrr9kaOOa6e3y3uAkGFxQ==", + "dev": true, + "dependencies": { + "@shikijs/core": "2.5.0", + "@shikijs/engine-javascript": "2.5.0", + "@shikijs/engine-oniguruma": "2.5.0", + "@shikijs/langs": "2.5.0", + "@shikijs/themes": "2.5.0", + "@shikijs/types": "2.5.0", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/superjson": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.6.tgz", + "integrity": "sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==", + "dev": true, + "dependencies": { + "copy-anything": "^4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tabbable": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.5.0.tgz", + "integrity": "sha512-wieBHXygIm7OyQOu5hQlkk62/WyCFYGlWg7L6/ZCUZwx0o398Zkn4pVmMyfYhfMG8kGrj/Krt8eIk6UKC6VzwA==", + "dev": true + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "dev": true, + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vitepress": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", + "integrity": "sha512-+2ym1/+0VVrbhNyRoFFesVvBvHAVMZMK0rw60E3X/5349M1GuVdKeazuksqopEdvkKwKGs21Q729jX81/bkBJg==", + "dev": true, + "dependencies": { + "@docsearch/css": "3.8.2", + "@docsearch/js": "3.8.2", + "@iconify-json/simple-icons": "^1.2.21", + "@shikijs/core": "^2.1.0", + "@shikijs/transformers": "^2.1.0", + "@shikijs/types": "^2.1.0", + "@types/markdown-it": "^14.1.2", + "@vitejs/plugin-vue": "^5.2.1", + "@vue/devtools-api": "^7.7.0", + "@vue/shared": "^3.5.13", + "@vueuse/core": "^12.4.0", + "@vueuse/integrations": "^12.4.0", + "focus-trap": "^7.6.4", + "mark.js": "8.11.1", + "minisearch": "^7.1.1", + "shiki": "^2.1.0", + "vite": "^5.4.14", + "vue": "^3.5.13" + }, + "bin": { + "vitepress": "bin/vitepress.js" + }, + "peerDependencies": { + "markdown-it-mathjax3": "^4", + "postcss": "^8" + }, + "peerDependenciesMeta": { + "markdown-it-mathjax3": { + "optional": true + }, + "postcss": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.39", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.39.tgz", + "integrity": "sha512-xmZCYabFGcirU8r0fTuvl/LICc1OU620rnqepaJDL/a141ZigkG7AyaxQLdqJ02ZRYzWe6YPaDHeQx7MfknQfA==", + "dev": true, + "dependencies": { + "@vue/compiler-dom": "3.5.39", + "@vue/compiler-sfc": "3.5.39", + "@vue/runtime-dom": "3.5.39", + "@vue/server-renderer": "3.5.39", + "@vue/shared": "3.5.39" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/site/package.json b/site/package.json new file mode 100644 index 0000000..2048ab7 --- /dev/null +++ b/site/package.json @@ -0,0 +1,19 @@ +{ + "name": "pagi-help-site", + "version": "1.0.0", + "private": true, + "description": "Documentation website for the pagi-help library", + "type": "module", + "scripts": { + "build:lib": "node scripts/build-lib.mjs", + "docs:dev": "node scripts/build-lib.mjs && vitepress dev docs", + "docs:build": "node scripts/build-lib.mjs && vitepress build docs", + "docs:preview": "vitepress preview docs" + }, + "devDependencies": { + "vitepress": "^1.6.3" + }, + "dependencies": { + "pagi-help": "^2.5.4" + } +} diff --git a/site/scripts/build-lib.mjs b/site/scripts/build-lib.mjs new file mode 100644 index 0000000..3e5390c --- /dev/null +++ b/site/scripts/build-lib.mjs @@ -0,0 +1,33 @@ +// Pre-bundle the published pagi-help package into a browser global for the demo/playground. +// Output is a classic IIFE (sloppy mode), NOT an ES module: the library writes to a +// getter-only inherited static — a no-op in sloppy CJS but a throw under ESM strict mode. +import { build } from "esbuild"; +import { fileURLToPath } from "node:url"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); + +const entry = require.resolve("pagi-help/v2"); +const outfile = fileURLToPath( + new URL("../docs/public/pagihelp.global.js", import.meta.url) +); +const cryptoShim = fileURLToPath( + new URL("../docs/.vitepress/shims/crypto.js", import.meta.url) +); + +await build({ + entryPoints: [entry], + outfile, + bundle: true, + format: "iife", + globalName: "__PagiHelp", + platform: "browser", + target: "es2019", + legalComments: "none", + alias: { + // Node's crypto -> tiny browser shim (only used by cursor fingerprints). + crypto: cryptoShim, + }, +}); + +console.log(`[build-lib] wrote ${outfile}`); diff --git a/site/scripts/gen-demo-sql.mjs b/site/scripts/gen-demo-sql.mjs new file mode 100644 index 0000000..6295b29 --- /dev/null +++ b/site/scripts/gen-demo-sql.mjs @@ -0,0 +1,19 @@ +// Dev helper: prints the exact SQL the installed pagi-help produces for each demo +// example, so the strings baked into utils/demoData.js stay accurate. +// Run from the site/ dir: node scripts/gen-demo-sql.mjs +import { createRequire } from "node:module"; +import { EXAMPLES, optionsFor } from "../docs/.vitepress/theme/utils/demoData.js"; + +const require = createRequire(import.meta.url); +const PagiHelpV2 = require("pagi-help/v2"); + +for (const ex of EXAMPLES) { + const options = optionsFor(ex.table); + const out = {}; + for (const dialect of ["mysql", "postgres"]) { + out[dialect] = new PagiHelpV2({ dialect }).paginate(ex.request, options).query; + } + console.log(`\n# ${ex.id}`); + console.log("mysql :", out.mysql); + console.log("postgres:", out.postgres); +} From 988686830a9c5405b4856883fa2fe9a154d1bc7c Mon Sep 17 00:00:00 2001 From: cB-Guru-Sharan-Kumar-Ram Date: Fri, 3 Jul 2026 17:40:40 +0530 Subject: [PATCH 2/2] docs: simplify empty-filters note in filters guide --- site/docs/v2/filters-and-operators.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/site/docs/v2/filters-and-operators.md b/site/docs/v2/filters-and-operators.md index 7ab009e..918f7d1 100644 --- a/site/docs/v2/filters-and-operators.md +++ b/site/docs/v2/filters-and-operators.md @@ -11,8 +11,8 @@ Filters are how you express `WHERE` conditions. Each condition is a tuple: - `operator` must be one of the supported operators (validated). - `value` is parameterized into `replacements` (never concatenated). -`filters` is optional — omit it, or pass an empty `filters: []` (allowed since -2.5.1), and no conditions are added. +`filters` is optional — omit it, or pass an empty `filters: []`, and no +conditions are added. ## Combining conditions: AND / OR