├── .prettierignore ├── examples ├── search-docs │ ├── app │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── postcss.config.mjs │ ├── .env.example │ ├── next.config.ts │ ├── utils │ │ └── colors.ts │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ ├── pages │ │ └── api │ │ │ └── crawl.ts │ ├── README.md │ └── components │ │ ├── SearchComponent.tsx │ │ └── RecentUpdates.tsx ├── upstash-search-vercel-changelog │ ├── .vercelignore │ ├── src │ │ ├── lib │ │ │ ├── constants.ts │ │ │ ├── dateUtils.ts │ │ │ └── types.ts │ │ └── app │ │ │ ├── favicon.ico │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── api │ │ │ └── search │ │ │ │ └── route.ts │ │ │ └── page.tsx │ ├── postcss.config.mjs │ ├── thumbnail.png │ ├── public │ │ ├── vercel.svg │ │ ├── window.svg │ │ ├── file.svg │ │ ├── globe.svg │ │ └── next.svg │ ├── next.config.ts │ ├── eslint.config.mjs │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ ├── scripts │ │ ├── uploadData.ts │ │ └── parser.ts │ └── README.md └── nextjs-movies │ ├── lib │ ├── constants.ts │ ├── utils.ts │ └── types.ts │ ├── demo-image.png │ ├── .env.example │ ├── app │ ├── favicon-32x32.png │ ├── globals.css │ ├── providers.tsx │ ├── layout.tsx │ ├── page.tsx │ └── actions.ts │ ├── postcss.config.mjs │ ├── tailwind.config.ts │ ├── components │ ├── tag.tsx │ ├── info.tsx │ ├── search-form.tsx │ └── result-data.tsx │ ├── next.config.mjs │ ├── .gitignore │ ├── tsconfig.json │ ├── package.json │ ├── LICENSE │ ├── scripts │ └── upsert-data.ts │ ├── README.md │ └── pnpm-lock.yaml ├── bun.lockb ├── src ├── client │ ├── error.ts │ ├── telemetry.ts │ ├── search-client.ts │ ├── metadata.ts │ └── metadata.test.ts ├── types.ts ├── search.test.ts ├── search.ts ├── platforms │ ├── cloudflare.ts │ └── nodejs.ts ├── search-index.test.ts └── search-index.ts ├── tsup.config.ts ├── prettier.config.mjs ├── tsconfig.json ├── .github ├── workflows │ ├── tests.yaml │ ├── npm_retention.yaml │ └── release.yaml └── scripts │ └── npm_retention.py ├── LICENSE ├── commitlint.config.js ├── package.json ├── eslint.config.mjs ├── .gitignore └── README.md /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | examples 3 | node_modules -------------------------------------------------------------------------------- /examples/search-docs/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/search-js/main/bun.lockb -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/.vercelignore: -------------------------------------------------------------------------------- 1 | scripts 2 | node_modules -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const NAMESPACE = "vercel-changelog"; -------------------------------------------------------------------------------- /examples/nextjs-movies/lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const INDEX_NAME = 'movies'; 2 | export const BATCH_SIZE = 100; 3 | -------------------------------------------------------------------------------- /examples/nextjs-movies/demo-image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/search-js/main/examples/nextjs-movies/demo-image.png -------------------------------------------------------------------------------- /examples/nextjs-movies/.env.example: -------------------------------------------------------------------------------- 1 | UPSTASH_SEARCH_REST_URL="***" 2 | UPSTASH_SEARCH_REST_TOKEN="***" 3 | RERANKING_ENABLED="" 4 | -------------------------------------------------------------------------------- /examples/nextjs-movies/app/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/search-js/main/examples/nextjs-movies/app/favicon-32x32.png -------------------------------------------------------------------------------- /examples/search-docs/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /examples/search-docs/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_UPSTASH_SEARCH_URL="***" 2 | NEXT_PUBLIC_UPSTASH_SEARCH_READONLY_TOKEN="***" 3 | UPSTASH_SEARCH_REST_TOKEN="***" 4 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/search-js/main/examples/upstash-search-vercel-changelog/thumbnail.png -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/upstash/search-js/main/examples/upstash-search-vercel-changelog/src/app/favicon.ico -------------------------------------------------------------------------------- /src/client/error.ts: -------------------------------------------------------------------------------- 1 | export class UpstashError extends Error { 2 | constructor(message: string) { 3 | super(message); 4 | this.name = "UpstashError"; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/search-docs/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /examples/nextjs-movies/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | export default defineConfig({ 4 | entry: ["./src/platforms/nodejs.ts", "./src/platforms/cloudflare.ts"], 5 | format: ["cjs", "esm"], 6 | clean: true, 7 | dts: true, 8 | }); 9 | -------------------------------------------------------------------------------- /examples/nextjs-movies/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer components { 6 | b, 7 | strong { 8 | @apply font-bold; 9 | } 10 | 11 | input::placeholder { 12 | @apply text-indigo-900/40; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @type {import('prettier').Config} 3 | */ 4 | const config = { 5 | endOfLine: "lf", 6 | singleQuote: false, 7 | tabWidth: 2, 8 | trailingComma: "es5", 9 | printWidth: 100, 10 | arrowParens: "always", 11 | }; 12 | 13 | export default config; 14 | -------------------------------------------------------------------------------- /examples/nextjs-movies/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | }; 10 | export default config; 11 | -------------------------------------------------------------------------------- /examples/nextjs-movies/components/tag.tsx: -------------------------------------------------------------------------------- 1 | export default function KeyValue({ 2 | label, 3 | value, 4 | }: { 5 | label: string; 6 | value: number | string; 7 | }) { 8 | return ( 9 |

10 | {label}: {value} 11 |

12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /examples/nextjs-movies/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | remotePatterns: [ 5 | { 6 | protocol: "https", 7 | hostname: "image.tmdb.org", 8 | }, 9 | ], 10 | }, 11 | }; 12 | 13 | export default nextConfig; 14 | -------------------------------------------------------------------------------- /examples/nextjs-movies/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | 8 | export const normalize = (value: number, min: number, max: number) => 9 | (value - min) / (max - min); 10 | 11 | export const formatter = Intl.NumberFormat("en", { notation: "compact" }); 12 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/src/lib/dateUtils.ts: -------------------------------------------------------------------------------- 1 | export const dateToInt = (date: Date): number => { 2 | const epoch = new Date(1970, 0, 1); 3 | const diff = date.getTime() - epoch.getTime(); 4 | return Math.floor(diff / (1000 * 60 * 60 * 24)); 5 | }; 6 | 7 | export const intToDate = (int: number): Date => { 8 | const epoch = new Date(1970, 0, 1); 9 | const date = new Date(epoch.getTime() + int * (1000 * 60 * 60 * 24)); 10 | return date; 11 | }; 12 | -------------------------------------------------------------------------------- /examples/nextjs-movies/app/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | import { PropsWithChildren } from "react"; 5 | 6 | export const Providers = ({ children }: PropsWithChildren) => { 7 | return ( 8 | {children} 9 | ); 10 | }; 11 | 12 | const queryClient = new QueryClient({ 13 | defaultOptions: { 14 | queries: { 15 | refetchOnWindowFocus: false, 16 | }, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /src/client/telemetry.ts: -------------------------------------------------------------------------------- 1 | export const VERSION = "0.1.0"; 2 | 3 | export function getRuntime() { 4 | if (typeof process === "object" && typeof process.versions == "object" && process.versions.bun) 5 | return `bun@${process.versions.bun}`; 6 | 7 | if (typeof process === "object" && typeof process.version === "string") { 8 | return `node@${process.version}`; 9 | } 10 | 11 | // @ts-expect-error EdgeRuntime not recognized 12 | if (typeof EdgeRuntime === "string") { 13 | return "edge-light"; 14 | } 15 | 16 | return "undetermined"; 17 | } 18 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export type VercelContent = { 4 | title: string, 5 | content: string, 6 | authors: string 7 | } 8 | 9 | export type VercelMetadata = { 10 | dateInt: number, 11 | url: string, 12 | updated: string, 13 | kind: "blog" | "changelog" 14 | } 15 | 16 | export type SearchAPIResponse = { 17 | results: { 18 | content: VercelContent; 19 | metadata?: VercelMetadata; 20 | score: number; 21 | id: string; 22 | }[]; 23 | query: string; 24 | filters: { 25 | dateFrom?: string; 26 | dateUntil?: string; 27 | contentType?: string; 28 | }; 29 | } -------------------------------------------------------------------------------- /examples/search-docs/utils/colors.ts: -------------------------------------------------------------------------------- 1 | const colorPalette = [ 2 | "bg-blue-50 text-blue-700", 3 | "bg-purple-50 text-purple-700", 4 | "bg-yellow-50 text-yellow-700", 5 | "bg-pink-50 text-pink-700", 6 | "bg-indigo-50 text-indigo-700", 7 | "bg-red-50 text-red-700", 8 | "bg-green-50 text-green-700", 9 | ]; 10 | 11 | export function getIndexColor(indexName: string) { 12 | let hash = 0; 13 | for (let i = 0; i < indexName.length; i++) { 14 | hash = indexName.charCodeAt(i) + ((hash << 5) - hash); 15 | } 16 | const colorIndex = Math.abs(hash) % colorPalette.length; 17 | return colorPalette[colorIndex]; 18 | } -------------------------------------------------------------------------------- /examples/search-docs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | .env.local 4 | .env 5 | 6 | 7 | **/node_modules 8 | **/.idea 9 | 10 | # dependencies 11 | /node_modules 12 | /.pnp 13 | .pnp.js 14 | .yarn/install-state.gz 15 | 16 | # testing 17 | /coverage 18 | 19 | # next.js 20 | /.next/ 21 | /out/ 22 | 23 | # production 24 | /build 25 | 26 | # misc 27 | .DS_Store 28 | *.pem 29 | 30 | # debug 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | 35 | # local env files 36 | .env*.local 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | next-env.d.ts 44 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/src/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | :root { 4 | --background: #ffffff; 5 | --foreground: #171717; 6 | } 7 | 8 | @theme inline { 9 | --color-background: var(--background); 10 | --color-foreground: var(--foreground); 11 | --font-sans: var(--font-geist-sans); 12 | --font-mono: var(--font-geist-mono); 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | :root { 17 | --background: #0a0a0a; 18 | --foreground: #ededed; 19 | } 20 | } 21 | 22 | body { 23 | background: var(--background); 24 | color: var(--foreground); 25 | font-family: Arial, Helvetica, sans-serif; 26 | } 27 | -------------------------------------------------------------------------------- /examples/nextjs-movies/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | .env.local 4 | .env 5 | 6 | 7 | **/node_modules 8 | **/.idea 9 | 10 | # dependencies 11 | /node_modules 12 | /.pnp 13 | .pnp.js 14 | .yarn/install-state.gz 15 | 16 | # testing 17 | /coverage 18 | 19 | # next.js 20 | /.next/ 21 | /out/ 22 | 23 | # production 24 | /build 25 | 26 | # misc 27 | .DS_Store 28 | *.pem 29 | 30 | # debug 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | 35 | # local env files 36 | .env*.local 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | next-env.d.ts 44 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | { 15 | ignores: [ 16 | "node_modules/**", 17 | ".next/**", 18 | "out/**", 19 | "build/**", 20 | "next-env.d.ts", 21 | ], 22 | }, 23 | ]; 24 | 25 | export default eslintConfig; 26 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /examples/nextjs-movies/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /examples/nextjs-movies/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import { Metadata } from "next"; 3 | import { Providers } from "@/app/providers"; 4 | 5 | export const metadata: Metadata = { 6 | title: "Movies AI Search", 7 | description: "AI Search for movies and TV series using TMDB data", 8 | icons: { 9 | icon: "/favicon-32x32.png", 10 | }, 11 | }; 12 | 13 | export default function RootLayout({ 14 | children, 15 | }: { 16 | children: React.ReactNode; 17 | }) { 18 | return ( 19 | 20 | 21 | {children} 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /examples/search-docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./src/*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ESNext"], 4 | "module": "esnext", 5 | "target": "esnext", 6 | "moduleResolution": "bundler", 7 | "moduleDetection": "force", 8 | "allowImportingTsExtensions": true, 9 | "noEmit": true, 10 | "strict": true, 11 | "downlevelIteration": true, 12 | "skipLibCheck": true, 13 | "allowSyntheticDefaultImports": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "allowJs": true, 16 | "types": [ 17 | "bun-types" // add Bun global 18 | ], 19 | "paths": { 20 | "@commands/*": ["./src/commands/*"], 21 | "@http": ["./src/http/index.ts"], 22 | "@utils/*": ["./src/utils/*"], 23 | "@error/*": ["./src/error/*"] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export type { QueryResult, Index as VectorIndex } from "@upstash/vector"; 2 | 3 | export type Dict = Record; 4 | 5 | export type UpsertParameters = { 6 | id: string; 7 | content: TContent; 8 | metadata?: TIndexMetadata; 9 | }; 10 | 11 | export type Document< 12 | TContent extends Dict, 13 | TMetadata extends Dict, 14 | TWithScore extends boolean = false, 15 | > = { 16 | id: string; 17 | content: TContent; 18 | metadata?: TMetadata; 19 | // eslint-disable-next-line @typescript-eslint/no-empty-object-type 20 | } & (TWithScore extends true ? { score: number } : {}); 21 | 22 | export type SearchResult = Document< 23 | TContent, 24 | TMetadata, 25 | true 26 | >[]; 27 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | 8 | env: 9 | UPSTASH_SEARCH_REST_URL: ${{ secrets.UPSTASH_SEARCH_REST_URL }} 10 | UPSTASH_SEARCH_REST_TOKEN: ${{ secrets.UPSTASH_SEARCH_REST_TOKEN }} 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | concurrency: test 15 | 16 | name: Tests 17 | steps: 18 | - name: Setup repo 19 | uses: actions/checkout@v3 20 | 21 | - name: Setup Bun 22 | uses: oven-sh/setup-bun@v1 23 | with: 24 | bun-version: latest 25 | 26 | - name: Install Dependencies 27 | run: bun install 28 | 29 | - name: Run Lint 30 | run: bun run fmt 31 | 32 | - name: Run tests 33 | run: bun run test 34 | 35 | - name: Run Build 36 | run: bun run build 37 | -------------------------------------------------------------------------------- /examples/search-docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "search-docs", 3 | "version": "0.1.0", 4 | "description": "A modern documentation library to search and track the docs.", 5 | "private": true, 6 | "scripts": { 7 | "dev": "next dev --turbopack", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint" 11 | }, 12 | "dependencies": { 13 | "@upstash/search": "^0.1.3", 14 | "@upstash/search-crawler": "^0.1.8", 15 | "@upstash/search-ui": "^0.1.4", 16 | "lucide-react": "^0.525.0", 17 | "next": "15.3.8", 18 | "react": "^19.0.0", 19 | "react-dom": "^19.0.0" 20 | }, 21 | "devDependencies": { 22 | "@tailwindcss/postcss": "^4", 23 | "@types/node": "^20", 24 | "@types/react": "^19", 25 | "@types/react-dom": "^19", 26 | "tailwindcss": "^4", 27 | "tw-animate-css": "^1.3.5", 28 | "typescript": "^5" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/search-docs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }); 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 12 | subsets: ["latin"], 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: "Upstash Docs Library", 17 | description: "A modern documentation library to search and track the docs you like.", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 30 | {children} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /examples/nextjs-movies/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "movies-semantic-search", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "upsert-data": "tsx scripts/upsert-data.ts" 11 | }, 12 | "dependencies": { 13 | "@tanstack/react-query": "^5.53.2", 14 | "@upstash/search": "^0.1.2", 15 | "clsx": "^2.1.1", 16 | "dotenv": "^16.5.0", 17 | "next": "14.2.35", 18 | "react": "^18", 19 | "react-dom": "^18", 20 | "zod": "^3.23.8" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "^20", 24 | "@types/react": "^18", 25 | "@types/react-dom": "^18", 26 | "postcss": "^8", 27 | "prettier": "^3.3.3", 28 | "tailwind-merge": "^2.5.2", 29 | "tailwindcss": "^3.4.10", 30 | "tsx": "^4.20.3", 31 | "typescript": "^5" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /examples/search-docs/pages/api/crawl.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import { crawlAndIndex } from "@upstash/search-crawler" 3 | 4 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 5 | if (req.method === 'POST') { 6 | const { docsUrl, index } = req.body; 7 | const upstashUrl = process.env.NEXT_PUBLIC_UPSTASH_SEARCH_URL! 8 | const upstashRestToken = process.env.UPSTASH_SEARCH_REST_TOKEN! 9 | 10 | if (!docsUrl || !upstashUrl || !upstashRestToken) { 11 | return res.status(500).json({ error: "Missing Upstash Search configuration" }); 12 | } 13 | 14 | const result = await crawlAndIndex({ 15 | upstashUrl, 16 | upstashToken: upstashRestToken, 17 | indexName: index || "default", 18 | docUrl: docsUrl, 19 | }) 20 | 21 | return res.status(200).json(result) 22 | } else { 23 | return res.status(405).json({ error: "Method not allowed" }); 24 | } 25 | } -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import { AntdRegistry } from "@ant-design/nextjs-registry"; 4 | import "./globals.css"; 5 | 6 | const geistSans = Geist({ 7 | variable: "--font-geist-sans", 8 | subsets: ["latin"], 9 | }); 10 | 11 | const geistMono = Geist_Mono({ 12 | variable: "--font-geist-mono", 13 | subsets: ["latin"], 14 | }); 15 | 16 | export const metadata: Metadata = { 17 | title: "Vercel & Upstash Search", 18 | description: "Search through Vercel's changelog and blog entries", 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: Readonly<{ 24 | children: React.ReactNode; 25 | }>) { 26 | return ( 27 | 28 | 31 | {children} 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /examples/nextjs-movies/lib/types.ts: -------------------------------------------------------------------------------- 1 | 2 | export type IndexContent = { 3 | title?: string; 4 | release_date: string; 5 | overview: string; 6 | genres: string; 7 | director: string; 8 | cast: string; 9 | name?: string 10 | } 11 | 12 | export type IndexMetadata = { 13 | movie_id: number; 14 | name: string; 15 | release_year: string; 16 | vote_average: number; 17 | vote_count: number; 18 | imdb_link: string; 19 | poster_link: string; 20 | popularity: number; 21 | }; 22 | 23 | export type Dataset = { 24 | id: string 25 | data: object 26 | metadata: IndexMetadata 27 | vector: number[] 28 | sparseVector: { 29 | indices: number[] 30 | values: number[] 31 | } 32 | }[] 33 | 34 | export enum ResultCode { 35 | Empty = "EMPTY", 36 | Success = "SUCCESS", 37 | UnknownError = "UNKNOWN_ERROR", 38 | MinLengthError = "MIN_LENGTH_ERROR", 39 | } 40 | 41 | export interface Result { 42 | code: ResultCode; 43 | movies: { content: IndexContent, metadata: IndexMetadata, score: number }[]; 44 | } 45 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs-movies/components/info.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { cn } from "@/lib/utils"; 3 | 4 | export const Info = ({ className }: React.ComponentProps<"div">) => { 5 | return ( 6 |
12 |

13 | This project is an experiment to demonstrate the search quality of 14 | Upstash Search using a movie dataset. With this app, you can upsert 15 | a dataset to your search database and search for movies them across 16 | multiple dimensions. 17 |

18 | 19 |

20 | 21 | 👉 Check out the{" "} 22 | 27 | Github Repo 28 | 29 | 30 |

31 |
32 | ); 33 | }; 34 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "upstash-search-vercel-changelog", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build --turbopack", 8 | "start": "next start", 9 | "lint": "eslint", 10 | "upload-data": "bun run scripts/uploadData.ts" 11 | }, 12 | "dependencies": { 13 | "@ant-design/icons": "^6.0.1", 14 | "@ant-design/nextjs-registry": "^1.1.0", 15 | "@upstash/search": "^0.1.5", 16 | "antd": "^5.27.3", 17 | "dayjs": "^1.11.18", 18 | "next": "15.5.9", 19 | "react": "19.1.0", 20 | "react-dom": "19.1.0", 21 | "xmldom": "^0.6.0" 22 | }, 23 | "devDependencies": { 24 | "@eslint/eslintrc": "^3", 25 | "@tailwindcss/postcss": "^4", 26 | "@types/node": "^20", 27 | "@types/react": "^19", 28 | "@types/react-dom": "^19", 29 | "@types/xmldom": "^0.1.34", 30 | "eslint": "^9", 31 | "eslint-config-next": "15.5.2", 32 | "tailwindcss": "^4", 33 | "typescript": "^5" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /.github/workflows/npm_retention.yaml: -------------------------------------------------------------------------------- 1 | name: NPM Package Retention 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * 0" # Run weekly on Sunday at midnight 6 | 7 | jobs: 8 | cleanup: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | 13 | - name: Set up Node.js 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: "14" 17 | 18 | - name: Install npm 19 | run: | 20 | npm install -g npm@latest 21 | 22 | - name: Configure npm 23 | run: | 24 | echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc 25 | 26 | - name: Set up Python 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: "3.x" 30 | 31 | - name: Install Python dependencies 32 | run: | 33 | python -m pip install --upgrade pip 34 | pip install requests 35 | 36 | - name: Run retention script 37 | env: 38 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 39 | run: python .github/scripts/npm_retention.py 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Upstash 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/nextjs-movies/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Upstash 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/search-docs/app/page.tsx: -------------------------------------------------------------------------------- 1 | import SearchComponent from "../components/SearchComponent" 2 | import RecentUpdates from "../components/RecentUpdates" 3 | import { BookOpen } from "lucide-react" 4 | 5 | export default function Page() { 6 | return ( 7 |
8 |
9 |
10 |
11 |
12 | 13 |

Documentation Library

14 |
15 |

16 | Search across all your documentation sources and discover the latest updates 17 |

18 |
19 |
20 |
21 | 22 |
23 | 24 | 25 |
26 |
27 | ) 28 | } -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout Repo 14 | uses: actions/checkout@v3 15 | 16 | - name: Set env 17 | run: echo "VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 18 | 19 | - name: Setup Node 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 18 23 | 24 | - name: Set package version 25 | run: echo $(jq --arg v "${{ env.VERSION }}" '(.version) = $v' package.json) > package.json 26 | 27 | - name: Setup Bun 28 | uses: oven-sh/setup-bun@v1 29 | with: 30 | bun-version: latest 31 | 32 | - name: Install dependencies 33 | run: bun install 34 | 35 | - name: Build 36 | run: bun run build 37 | 38 | - name: Add npm token 39 | run: echo "//registry.npmjs.org/:_authToken=${{secrets.NPM_TOKEN}}" > .npmrc 40 | 41 | - name: Publish release candidate 42 | if: "github.event.release.prerelease" 43 | run: npm publish --access public --tag=canary 44 | 45 | - name: Publish 46 | if: "!github.event.release.prerelease" 47 | run: npm publish --access public 48 | -------------------------------------------------------------------------------- /examples/nextjs-movies/scripts/upsert-data.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv'; 2 | import movies from '../app/movies.json'; 3 | import { Dataset } from '@/lib/types'; 4 | import { BATCH_SIZE, INDEX_NAME } from '@/lib/constants'; 5 | 6 | // Load environment variables 7 | config(); 8 | 9 | 10 | async function main() { 11 | console.log('Starting data upsert...'); 12 | 13 | try { 14 | const dataset = movies as Dataset; 15 | 16 | for (let index_ = 0; index_ < dataset.length; index_ += BATCH_SIZE) { 17 | const batch = dataset.slice(index_, index_ + BATCH_SIZE).map((data) => { 18 | const { data: content, ...rest } = data; 19 | return { 20 | ...rest, 21 | content, 22 | }; 23 | }); 24 | 25 | await fetch( 26 | `${process.env.UPSTASH_SEARCH_REST_URL}/upsert/${INDEX_NAME}`, 27 | { 28 | headers: { 29 | authorization: `Bearer ${process.env.UPSTASH_SEARCH_REST_TOKEN}`, 30 | 'content-type': 'application/json', 31 | }, 32 | body: JSON.stringify(batch), 33 | method: 'POST', 34 | keepalive: false, 35 | } 36 | ); 37 | } 38 | console.log('✅ Data upserted successfully!'); 39 | } catch (error) { 40 | console.error('❌ Error upserting data:', error); 41 | process.exit(1); 42 | } 43 | } 44 | 45 | main(); -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs-movies/components/search-form.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import type { DefinedUseQueryResult } from "@tanstack/react-query"; 3 | import { Result } from "@/lib/types"; 4 | 5 | export default function SearchForm({ 6 | state, 7 | query, 8 | onChangeQuery = () => {}, 9 | onSubmit = () => {}, 10 | }: { 11 | state: DefinedUseQueryResult; 12 | query: string; 13 | onChangeQuery: (q: string) => void; 14 | onSubmit: () => void; 15 | }) { 16 | return ( 17 |
{ 20 | e.preventDefault(); 21 | return onSubmit(); 22 | }} 23 | > 24 | onChangeQuery(e.target.value)} 29 | placeholder="Search for a movie..." 30 | className="grow w-full sm:w-auto h-12 rounded-lg border border-indigo-300 px-4 text-xl" 31 | disabled={state.isFetching} 32 | /> 33 | 43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/scripts/uploadData.ts: -------------------------------------------------------------------------------- 1 | import { Search } from "@upstash/search"; 2 | import { getEntries } from './parser'; 3 | import { dateToInt } from '@/lib/dateUtils'; 4 | import { VercelContent, VercelMetadata } from '@/lib/types'; 5 | import { NAMESPACE } from '@/lib/constants'; 6 | 7 | const entries = await getEntries() 8 | 9 | const formatedEntries = entries.map((entry, index) => { 10 | const dateObj = new Date(entry.updated); 11 | const dateInt = dateToInt(dateObj) 12 | const kind = entry.link.includes("/blog/") ? "blog" : "changelog"; 13 | 14 | return { 15 | id: `${index}-${entry.id}`, 16 | content: { 17 | title: entry.title, 18 | content: entry.content, 19 | authors: entry.author.join(', ') 20 | } satisfies VercelContent, 21 | metadata: { 22 | dateInt, 23 | url: entry.link, 24 | updated: entry.updated, 25 | kind 26 | } satisfies VercelMetadata 27 | } 28 | }) 29 | 30 | const client = new Search({ 31 | url: process.env.UPSTASH_SEARCH_REST_URL!, 32 | token: process.env.UPSTASH_SEARCH_REST_TOKEN!, 33 | }); 34 | 35 | const index = client.index(NAMESPACE); 36 | 37 | // upsert 100 entries at a time 38 | const BATCH_SIZE = 100; 39 | 40 | for (let i = 0; i < formatedEntries.length; i += BATCH_SIZE) { 41 | const batch = formatedEntries.slice(i, i + BATCH_SIZE); 42 | console.log(`Upserting entries ${i} to ${i + batch.length}...`); 43 | 44 | await index.upsert(batch); 45 | } 46 | 47 | console.log("All entries upserted."); -------------------------------------------------------------------------------- /src/search.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, beforeAll, afterAll, expect } from "bun:test"; 2 | import { Search } from "./platforms/nodejs"; 3 | import { Index } from "@upstash/vector"; 4 | 5 | const client = Search.fromEnv(); 6 | const NAMESPACE = "test-namespace"; 7 | const searchIndex = client.index<{ text: string }, { key: string }>(NAMESPACE); 8 | const vectorIndex = new Index({ 9 | url: process.env.UPSTASH_SEARCH_REST_URL, 10 | token: process.env.UPSTASH_SEARCH_REST_TOKEN, 11 | }); 12 | 13 | describe("Search (Real Index)", () => { 14 | beforeAll(async () => { 15 | await searchIndex.reset(); // Clean namespace 16 | await searchIndex.upsert({ 17 | id: "2", 18 | content: { text: "test-data-2" }, 19 | metadata: { key: "value2" }, 20 | }); 21 | await searchIndex.upsert({ 22 | id: "1", 23 | content: { text: "test-data-1" }, 24 | metadata: { key: "value1" }, 25 | }); 26 | }); 27 | 28 | afterAll(async () => { 29 | await searchIndex.deleteIndex(); // Clean up 30 | await vectorIndex.reset({ all: true }); 31 | }); 32 | 33 | test("should get overall index info", async () => { 34 | const info = await client.info(); 35 | 36 | expect(info).toMatchObject({ 37 | diskSize: expect.any(Number), 38 | pendingDocumentCount: expect.any(Number), 39 | documentCount: expect.any(Number), 40 | indexes: { 41 | [NAMESPACE]: { 42 | pendingDocumentCount: expect.any(Number), 43 | documentCount: expect.any(Number), 44 | }, 45 | }, 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | // build: Changes that affect the build system or external dependencies (example scopes: gulp, broccoli, npm) 2 | // ci: Changes to our CI configuration files and scripts (example scopes: Travis, Circle, BrowserStack, SauceLabs) 3 | // docs: Documentation only changes 4 | // feat: A new feature 5 | // fix: A bug fix 6 | // perf: A code change that improves performance 7 | // refactor: A code change that neither fixes a bug nor adds a feature 8 | // style: Changes that do not affect the meaning of the code (white-space, formatting, missing semi-colons, etc) 9 | // test: Adding missing tests or correcting existing tests 10 | 11 | module.exports = { 12 | extends: ["@commitlint/config-conventional"], 13 | rules: { 14 | "body-leading-blank": [1, "always"], 15 | "body-max-line-length": [2, "always", 100], 16 | "footer-leading-blank": [1, "always"], 17 | "footer-max-line-length": [2, "always", 100], 18 | "header-max-length": [2, "always", 100], 19 | "scope-case": [2, "always", "lower-case"], 20 | "subject-case": [2, "never", ["sentence-case", "start-case", "pascal-case", "upper-case"]], 21 | "subject-empty": [2, "never"], 22 | "subject-full-stop": [2, "never", "."], 23 | "type-case": [2, "always", "lower-case"], 24 | "type-empty": [2, "never"], 25 | "type-enum": [ 26 | 2, 27 | "always", 28 | [ 29 | "build", 30 | "chore", 31 | "ci", 32 | "docs", 33 | "feat", 34 | "fix", 35 | "perf", 36 | "refactor", 37 | "revert", 38 | "style", 39 | "test", 40 | "translation", 41 | "security", 42 | "changeset", 43 | ], 44 | ], 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /examples/nextjs-movies/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ResultCode } from "@/lib/types"; 4 | import SearchForm from "@/components/search-form"; 5 | import ResultData from "@/components/result-data"; 6 | import { useState } from "react"; 7 | import { useQuery } from "@tanstack/react-query"; 8 | import { fetchSimilarMovies } from "@/app/actions"; 9 | import { Info } from "@/components/info"; 10 | 11 | export default function Page() { 12 | const [query, setQuery] = useState(""); 13 | 14 | const state = useQuery({ 15 | queryKey: ["movies", query], 16 | queryFn: async () => await fetchSimilarMovies({query, limit: 10}), 17 | initialData: { 18 | movies: [], 19 | code: ResultCode.Empty, 20 | }, 21 | enabled: false, 22 | }); 23 | 24 | const onChangeQuery = (q: string) => { 25 | setQuery(q); 26 | }; 27 | 28 | const onSubmit = () => { 29 | return state.refetch(); 30 | }; 31 | 32 | return ( 33 |
34 |
35 |

onChangeQuery("")} 37 | className="cursor-pointer text-3xl md:text-5xl tracking-tight font-bold text-indigo-900" 38 | > 39 | Movies AI Search 40 |

41 |
42 | 43 |
44 | 50 |
51 | 52 |
53 | 58 |
59 | 60 | 61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@upstash/search", 3 | "version": "0.1.1", 4 | "author": "Cahid Arda Oz", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/upstash/search-js" 8 | }, 9 | "exports": { 10 | ".": { 11 | "import": "./dist/nodejs.mjs", 12 | "require": "./dist/nodejs.js" 13 | }, 14 | "./cloudflare": { 15 | "import": "./dist/cloudflare.mjs", 16 | "require": "./dist/cloudflare.js" 17 | }, 18 | "./nodejs": { 19 | "import": "./dist/nodejs.mjs", 20 | "require": "./dist/nodejs.js" 21 | } 22 | }, 23 | "main": "./dist/nodejs.js", 24 | "module": "./dist/nodejs.mjs", 25 | "types": "./dist/nodejs.d.ts", 26 | "devDependencies": { 27 | "@commitlint/cli": "^18.6.0", 28 | "@commitlint/config-conventional": "^18.6.0", 29 | "@typescript-eslint/eslint-plugin": "^8.4.0", 30 | "bun-types": "latest", 31 | "eslint": "9.10.0", 32 | "eslint-plugin-unicorn": "^55.0.0", 33 | "husky": "^8.0.3", 34 | "prettier": "^3.3.3", 35 | "tsup": "latest", 36 | "typescript": "^5.0.0", 37 | "vitest": "^3.0.9" 38 | }, 39 | "bugs": { 40 | "url": "https://github.com/upstash/search/issues" 41 | }, 42 | "description": "An HTTP/REST based AI Search client built on top of Upstash REST API.", 43 | "files": [ 44 | "dist" 45 | ], 46 | "homepage": "https://upstash.com/search", 47 | "keywords": [ 48 | "search", 49 | "vector", 50 | "upstash", 51 | "db" 52 | ], 53 | "license": "MIT", 54 | "scripts": { 55 | "test": "bun test src --coverage --bail --coverageSkipTestFiles=[test-utils.ts] --timeout 20000", 56 | "fmt": "prettier --write .", 57 | "lint": "tsc && eslint \"src/**/*.{js,ts,tsx}\" --quiet --fix", 58 | "build": "tsup ", 59 | "prepare": "husky install" 60 | }, 61 | "dependencies": { 62 | "@upstash/vector": "^1.2.1" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/src/app/api/search/route.ts: -------------------------------------------------------------------------------- 1 | import { Search } from "@upstash/search"; 2 | import { NextRequest } from "next/server"; 3 | import { dateToInt } from "@/lib/dateUtils"; 4 | import { SearchAPIResponse, VercelContent, VercelMetadata } from "@/lib/types"; 5 | import { NAMESPACE } from "@/lib/constants"; 6 | 7 | // Initialize Search client 8 | const client = new Search({ 9 | url: process.env.UPSTASH_SEARCH_REST_URL!, 10 | token: process.env.UPSTASH_SEARCH_REST_TOKEN!, 11 | }); 12 | 13 | // Create or access a index 14 | const index = client.index(NAMESPACE); 15 | 16 | export async function POST(request: NextRequest) { 17 | try { 18 | const { query, dateFrom, dateUntil, contentType } = await request.json(); 19 | 20 | if (!query) { 21 | return Response.json({ error: "Query is required" }, { status: 400 }); 22 | } 23 | 24 | const fromInt = dateFrom ? dateToInt(new Date(dateFrom)) : undefined; 25 | const untilInt = dateUntil ? dateToInt(new Date(dateUntil)) : undefined; 26 | 27 | const filters = [] 28 | if (fromInt !== undefined) { 29 | filters.push(`@metadata.dateInt >= ${fromInt}`); 30 | } 31 | if (untilInt !== undefined) { 32 | filters.push(`@metadata.dateInt <= ${untilInt}`); 33 | } 34 | if (contentType && contentType !== "all") { 35 | filters.push(`@metadata.kind = "${contentType}"`); 36 | } 37 | 38 | const searchResults = await index.search({ 39 | query, 40 | limit: 20, 41 | reranking: false, 42 | filter: filters.length > 0 ? filters.join(" AND ") : undefined, 43 | }); 44 | 45 | return Response.json({ 46 | results: searchResults, 47 | query, 48 | filters: { 49 | dateFrom, 50 | dateUntil, 51 | contentType, 52 | }, 53 | } satisfies SearchAPIResponse); 54 | } catch (error) { 55 | console.error("Search error:", error); 56 | return Response.json( 57 | { error: "Failed to perform search" }, 58 | { status: 500 } 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/README.md: -------------------------------------------------------------------------------- 1 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fupstash%2Fsearch-js%2Ftree%2Fmain%2Fexamples%2Fupstash-search-vercel-changelog&project-name=upstash-search-vercel-changelog&repository-name=upstash-search-vercel-changelog&products=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22upstash%22%2C%22productSlug%22%3A%22upstash-search%22%2C%22protocol%22%3A%22storage%22%7D%5D) 2 | 3 | # Vercel Changelog Search 4 | 5 | A Next.js application that provides search functionality for Vercel's changelog using Upstash Search. 6 | 7 | ## Features 8 | 9 | - **Full-text & Semantic Search**: Search through Vercel changelog entries with full text and semantic search capabilities 10 | - **Input Enrichment & Reranking**: Enriches search queries and reranks search results 11 | - **Date & Content Type Filtering**: Filter results by date range (From/Until dates) and content type (Blog/Changelog) 12 | - **Search Scoring**: Display relevance scores for search results 13 | 14 | ## Setup 15 | 16 | ### 1. Install Dependencies 17 | 18 | ```bash 19 | bun install 20 | ``` 21 | 22 | ### 2. Environment Configuration 23 | 24 | > [!TIP] 25 | > If you created the project with the `Deploy with Vercel` button, you can skip this section. 26 | 27 | Copy the example environment file and configure your Upstash Search credentials: 28 | 29 | ```bash 30 | cp .env.example .env.local 31 | ``` 32 | 33 | To create an Upstash Search database: 34 | 35 | 1. Go to [Upstash Console](https://console.upstash.com/) 36 | 2. Create a new Search index named `vercel-changelog` 37 | 3. Copy the REST URL and Token to your `.env.local` file 38 | 39 | ### 3. Load the Database 40 | 41 | Upload the data from `https://vercel.com/atom` to Upstash Search: 42 | 43 | ```bash 44 | bun upload-data 45 | ``` 46 | 47 | ## Development 48 | 49 | ```bash 50 | bun dev 51 | ``` 52 | 53 | Open [http://localhost:3000](http://localhost:3000) to view the application. 54 | 55 | ## Tech Stack 56 | 57 | - **Next.js 15** - React framework with App Router 58 | - **TypeScript** - Type safety 59 | - **Ant Design** - UI component library 60 | - **Tailwind CSS** - Utility-first CSS framework 61 | - **Upstash Search** - Semantic and full-text search 62 | -------------------------------------------------------------------------------- /src/search.ts: -------------------------------------------------------------------------------- 1 | import { Index as VectorIndex } from "@upstash/vector"; 2 | import { SearchIndex } from "./search-index"; 3 | import type { Dict } from "./types"; 4 | import type { HttpClient } from "./client/search-client"; 5 | 6 | /** 7 | * Provides search capabilities over indexes. 8 | */ 9 | export class Search { 10 | protected vectorIndex: VectorIndex; 11 | 12 | /** 13 | * Creates a new Search instance. 14 | * 15 | * @param vectorIndex - The underlying index used for search operations. 16 | */ 17 | constructor(private client: HttpClient) { 18 | this.vectorIndex = new VectorIndex(client); 19 | } 20 | 21 | /** 22 | * Returns a SearchIndex instance for a given index. 23 | * 24 | * Each index is an isolated collection where documents can be added, 25 | * retrieved, searched, and deleted. 26 | * 27 | * @param indexName - The name to use as an index. 28 | * @returns A SearchIndex instance for managing documents within the index. 29 | */ 30 | index = ( 31 | indexName: string 32 | ): SearchIndex => { 33 | return new SearchIndex(this.client, this.vectorIndex, indexName); 34 | }; 35 | 36 | /** 37 | * Retrieves a list of all available indexes. 38 | * 39 | * @returns An array of strings representing the names of available indexes. 40 | */ 41 | listIndexes = async () => { 42 | return await this.vectorIndex.listNamespaces(); 43 | }; 44 | 45 | /** 46 | * Retrieves overall search index statistics. 47 | * 48 | * This includes disk usage, total document count, pending document count, 49 | * and details about each available index. 50 | * 51 | * @returns An object containing search system metrics and index details. 52 | */ 53 | info = async () => { 54 | const { indexSize, namespaces, pendingVectorCount, vectorCount } = 55 | await this.vectorIndex.info(); 56 | 57 | const indexes = Object.fromEntries( 58 | Object.entries(namespaces).map((namespace) => [ 59 | namespace[0], 60 | { 61 | pendingDocumentCount: namespace[1].pendingVectorCount, 62 | documentCount: namespace[1].vectorCount, 63 | }, 64 | ]) 65 | ); 66 | 67 | return { 68 | diskSize: indexSize, 69 | pendingDocumentCount: pendingVectorCount, 70 | documentCount: vectorCount, 71 | indexes, 72 | }; 73 | }; 74 | } 75 | -------------------------------------------------------------------------------- /examples/nextjs-movies/app/actions.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { z } from "zod"; 4 | import { Search } from '@upstash/search'; 5 | import movies from './movies.json'; 6 | import { ResultCode, Dataset, IndexContent, IndexMetadata, Result } from '@/lib/types'; 7 | import { BATCH_SIZE, INDEX_NAME } from "@/lib/constants"; 8 | 9 | const client = new Search({ 10 | url: process.env.UPSTASH_SEARCH_REST_URL!, 11 | token: process.env.UPSTASH_SEARCH_REST_TOKEN!, 12 | }); 13 | 14 | const index = client.index(INDEX_NAME); 15 | 16 | const rerankingEnabled = process.env.RERANKING_ENABLED === 'true'; 17 | 18 | export async function fetchMovie(movie_id: string) { 19 | const response = await index.fetch({ ids: [movie_id] }) 20 | 21 | const movie = response[0]; 22 | 23 | if (!movie) { 24 | throw new Error(`Movie with ID ${movie_id} not found`); 25 | } 26 | 27 | return movie 28 | } 29 | 30 | export async function fetchSimilarMovies(options: { query: string, limit: number, filter?: string }): Promise { 31 | const { query, limit, filter } = options; 32 | 33 | try { 34 | const parsedCredentials = z 35 | .object({ 36 | query: z.string().min(2), 37 | }) 38 | .safeParse({ 39 | query, 40 | }); 41 | 42 | if (parsedCredentials.error) { 43 | return { 44 | code: ResultCode.MinLengthError, 45 | movies: [], 46 | }; 47 | } 48 | 49 | const response = await index.search({ 50 | query, 51 | limit, 52 | filter, 53 | reranking: rerankingEnabled, 54 | }); 55 | 56 | return { 57 | code: ResultCode.Success, 58 | movies: response as Result['movies'], 59 | }; 60 | } catch (error) { 61 | return { 62 | code: ResultCode.UnknownError, 63 | movies: [], 64 | }; 65 | } 66 | } 67 | 68 | 69 | 70 | export async function upsertData() { 71 | const dataset = movies as Dataset; 72 | 73 | for (let index_ = 0; index_ < dataset.length; index_ += BATCH_SIZE) { 74 | const batch = dataset.slice(index_, index_ + BATCH_SIZE).map((data) => { 75 | const { data: content, ...rest } = data; 76 | return { 77 | ...rest, 78 | content, 79 | }; 80 | }); 81 | 82 | await fetch( 83 | `${process.env.UPSTASH_SEARCH_REST_URL}/upsert/${INDEX_NAME}`, 84 | { 85 | headers: { 86 | authorization: `Bearer ${process.env.UPSTASH_SEARCH_REST_TOKEN}`, 87 | 'content-type': 'application/json', 88 | }, 89 | body: JSON.stringify(batch), 90 | method: 'POST', 91 | keepalive: false, 92 | } 93 | ); 94 | } 95 | } -------------------------------------------------------------------------------- /examples/search-docs/README.md: -------------------------------------------------------------------------------- 1 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fupstash%2Fupstash%2Fsearch-js%2Ftree%2Fmain%2Fexamples%2Fsearch-docs&env=NEXT_PUBLIC_UPSTASH_SEARCH_URL,NEXT_PUBLIC_UPSTASH_SEARCH_READONLY_TOKEN,UPSTASH_SEARCH_REST_TOKEN&envDescription=Credentials%20needed%20for%20Upstash%20Search%20Component%20use&envLink=https%3A%2F%2Fconsole.upstash.com%2Fsearch&project-name=search-docs&repository-name=search-docs&demo-title=Documentation%20Library&demo-description=Search%20across%20all%20your%20documentation%20sources%20and%20discover%20the%20latest%20updates&demo-url=https%3A%2F%2Fsearch-docs.vercel.app%2F) 2 | ## Description 3 | 4 | A modern documentation library to search and track the docs. 5 | 6 | ## How it Works 7 | - Search: Uses Upstash Search UI to query multiple indexes in parallel, sorts and groups results, and displays them with section headers. 8 | - Recent Updates: Upstash Qstash fetches all documents from multiple indexes in batches, filters for those crawled in the last week. 9 | 10 | 11 | ## Setup 12 | 13 | Follow these steps to get the application running: 14 | 15 | 2. **Create Upstash Search Database:** 16 | - Go to the [Upstash Console](https://console.upstash.com/search) and create a new Search database. 17 | - Once created, copy your `UPSTASH_SEARCH_REST_URL`, `UPSTASH_SEARCH_REST_TOKEN` and `UPSTASH_SEARCH_READONLY_TOKEN`. 18 | 19 | 3. **Deploy Crawler Script:** 20 | Click on the vercel deploy button above and provide the requested Upstash Search credentials. 21 | 22 | ```env 23 | NEXT_PUBLIC_UPSTASH_SEARCH_URL="YOUR_UPSTASH_SEARCH_REST_URL" 24 | NEXT_PUBLIC_UPSTASH_SEARCH_READONLY_TOKEN="YOUR_UPSTASH_SEARCH_READONLY_TOKEN" 25 | UPSTASH_SEARCH_REST_TOKEN="YOUR_UPSTASH_SEARCH_REST_TOKEN" 26 | ``` 27 | 28 | 4. **Populate the Database:** 29 | Now that we deployed the project, our crawler resides at `/api/crawl`. 30 | 31 | - Upstash Qstash can call the `/api/crawl` endpoint with a schedule to crawl the relevant data and upsert it to the specified Search Database 32 | - Providing the URL and the index name in the body, you may manage the crawler from [Upstash Console](https://console.upstash.com/qstash/request-builder), 33 | e.g. 34 | 35 | ``` 36 | { 37 | "docsUrl": "https://upstash.com/docs", 38 | "index": "upstash" 39 | } 40 | ``` 41 | 42 | 6. **Search for Docs:** 43 | The UI also resides in the same deployment, so now using the UI, you can track your docs and do search accross all the docs you have added as scheduled. 44 | 45 | 46 | ## Final Remarks 47 | 48 | The crawler operates incrementally, automatically discarding outdated content and keeping your Search database synchronized with the latest website updates each time it runs on schedule. 49 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import unicorn from "eslint-plugin-unicorn"; 3 | import path from "node:path"; 4 | import { fileURLToPath } from "node:url"; 5 | import js from "@eslint/js"; 6 | import { FlatCompat } from "@eslint/eslintrc"; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | const compat = new FlatCompat({ 11 | baseDirectory: __dirname, 12 | recommendedConfig: js.configs.recommended, 13 | allConfig: js.configs.all, 14 | }); 15 | 16 | export default [ 17 | { 18 | ignores: ["**/*.config.*", "**/examples", "**/dist"], 19 | }, 20 | ...compat.extends( 21 | "eslint:recommended", 22 | "plugin:unicorn/recommended", 23 | "plugin:@typescript-eslint/recommended" 24 | ), 25 | { 26 | plugins: { 27 | "@typescript-eslint": typescriptEslint, 28 | unicorn, 29 | }, 30 | 31 | languageOptions: { 32 | globals: {}, 33 | ecmaVersion: 5, 34 | sourceType: "script", 35 | 36 | parserOptions: { 37 | project: "./tsconfig.json", 38 | }, 39 | }, 40 | 41 | rules: { 42 | "no-console": [ 43 | "error", 44 | { 45 | allow: ["warn", "error"], 46 | }, 47 | ], 48 | 49 | "@typescript-eslint/no-magic-numbers": "off", 50 | "@typescript-eslint/unbound-method": "off", 51 | "@typescript-eslint/prefer-as-const": "error", 52 | "@typescript-eslint/consistent-type-imports": "error", 53 | "@typescript-eslint/no-explicit-any": "off", 54 | "@typescript-eslint/restrict-template-expressions": "off", 55 | "@typescript-eslint/consistent-type-definitions": ["error", "type"], 56 | 57 | "@typescript-eslint/no-unused-vars": [ 58 | "error", 59 | { 60 | varsIgnorePattern: "^_", 61 | argsIgnorePattern: "^_", 62 | }, 63 | ], 64 | 65 | "@typescript-eslint/prefer-ts-expect-error": "off", 66 | 67 | "@typescript-eslint/no-misused-promises": [ 68 | "error", 69 | { 70 | checksVoidReturn: false, 71 | }, 72 | ], 73 | 74 | "unicorn/prevent-abbreviations": "off", 75 | 76 | "no-implicit-coercion": [ 77 | "error", 78 | { 79 | boolean: true, 80 | }, 81 | ], 82 | 83 | "no-extra-boolean-cast": [ 84 | "error", 85 | { 86 | enforceForLogicalOperands: true, 87 | }, 88 | ], 89 | 90 | "no-unneeded-ternary": [ 91 | "error", 92 | { 93 | defaultAssignment: true, 94 | }, 95 | ], 96 | 97 | "unicorn/no-array-reduce": ["off"], 98 | "unicorn/no-nested-ternary": "off", 99 | "unicorn/no-null": "off", 100 | "unicorn/filename-case": "off", 101 | }, 102 | }, 103 | ]; 104 | -------------------------------------------------------------------------------- /src/platforms/cloudflare.ts: -------------------------------------------------------------------------------- 1 | import * as core from "./../search"; 2 | import { HttpClient, type RequesterConfig } from "../client/search-client"; 3 | import { UpstashError } from "../client/error"; 4 | import { VERSION } from "../client/telemetry"; 5 | 6 | /** 7 | * Connection credentials for upstash vector. 8 | * Get them from https://console.upstash.com/vector/ 9 | */ 10 | export type ClientConfig = { 11 | /** 12 | * UPSTASH_SEARCH_REST_URL 13 | */ 14 | url?: string; 15 | /** 16 | * UPSTASH_SEARCH_REST_TOKEN 17 | */ 18 | token?: string; 19 | 20 | /** 21 | * Enable telemetry to help us improve the SDK. 22 | * The sdk will send the sdk version, platform and node version as telemetry headers. 23 | * 24 | * @default true 25 | */ 26 | enableTelemetry?: boolean; 27 | } & RequesterConfig; 28 | 29 | /** 30 | * Provides search capabilities over indexes. 31 | */ 32 | export class Search extends core.Search { 33 | /** 34 | * Creates a new Search instance. 35 | * 36 | * @param vectorIndex - The underlying index used for search operations. 37 | */ 38 | constructor(params: ClientConfig) { 39 | const token = params?.token; 40 | const url = params?.url; 41 | 42 | if (!token) { 43 | throw new UpstashError("UPSTASH_SEARCH_REST_TOKEN is missing!"); 44 | } 45 | if (!url) { 46 | throw new UpstashError("UPSTASH_SEARCH_REST_URL is missing!"); 47 | } 48 | 49 | if (url.startsWith(" ") || url.endsWith(" ") || /\r|\n/.test(url)) { 50 | console.warn("The vector url contains whitespace or newline, which can cause errors!"); 51 | } 52 | if (token.startsWith(" ") || token.endsWith(" ") || /\r|\n/.test(token)) { 53 | console.warn("The vector token contains whitespace or newline, which can cause errors!"); 54 | } 55 | 56 | const telemetryHeaders: Record = 57 | (params.enableTelemetry ?? true) 58 | ? { 59 | "Upstash-Telemetry-Sdk": `upstash-search-js@${VERSION}`, 60 | "Upstash-Telemetry-Platform": "cloudflare", 61 | } 62 | : {}; 63 | 64 | const client = new HttpClient({ 65 | baseUrl: url, 66 | retry: params?.retry, 67 | headers: { authorization: `Bearer ${token}`, ...telemetryHeaders }, 68 | cache: params?.cache === false ? undefined : params?.cache, 69 | }); 70 | 71 | super(client); 72 | } 73 | 74 | /** 75 | * Creates a new Search instance using env variables 76 | * `UPSTASH_SEARCH_REST_URL` and 77 | * `UPSTASH_SEARCH_REST_TOKEN` 78 | * 79 | * @param env 80 | * @returns 81 | */ 82 | static fromEnv = ( 83 | env?: { 84 | UPSTASH_SEARCH_REST_URL: string; 85 | UPSTASH_SEARCH_REST_TOKEN: string; 86 | }, 87 | config?: Omit 88 | ) => { 89 | const url = env?.UPSTASH_SEARCH_REST_URL; 90 | const token = env?.UPSTASH_SEARCH_REST_TOKEN; 91 | 92 | return new Search({ url, token, ...config }); 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Diagnostic reports (https://nodejs.org/api/report.html) 14 | 15 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 16 | 17 | # Runtime data 18 | 19 | pids 20 | _.pid 21 | _.seed 22 | \*.pid.lock 23 | 24 | # Directory for instrumented libs generated by jscoverage/JSCover 25 | 26 | lib-cov 27 | 28 | # Coverage directory used by tools like istanbul 29 | 30 | coverage 31 | \*.lcov 32 | 33 | # nyc test coverage 34 | 35 | .nyc_output 36 | 37 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 38 | 39 | .grunt 40 | 41 | # Bower dependency directory (https://bower.io/) 42 | 43 | bower_components 44 | 45 | # node-waf configuration 46 | 47 | .lock-wscript 48 | 49 | # Compiled binary addons (https://nodejs.org/api/addons.html) 50 | 51 | build/Release 52 | 53 | # Dependency directories 54 | 55 | node_modules/ 56 | jspm_packages/ 57 | 58 | # Snowpack dependency directory (https://snowpack.dev/) 59 | 60 | web_modules/ 61 | 62 | # TypeScript cache 63 | 64 | \*.tsbuildinfo 65 | 66 | # Optional npm cache directory 67 | 68 | .npm 69 | 70 | # Optional eslint cache 71 | 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | 76 | .stylelintcache 77 | 78 | # Microbundle cache 79 | 80 | .rpt2_cache/ 81 | .rts2_cache_cjs/ 82 | .rts2_cache_es/ 83 | .rts2_cache_umd/ 84 | 85 | # Optional REPL history 86 | 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | 91 | \*.tgz 92 | 93 | # Yarn Integrity file 94 | 95 | .yarn-integrity 96 | 97 | # dotenv environment variable files 98 | 99 | .env 100 | .env.development.local 101 | .env.test.local 102 | .env.production.local 103 | .env.local 104 | 105 | # parcel-bundler cache (https://parceljs.org/) 106 | 107 | .cache 108 | .parcel-cache 109 | 110 | # Next.js build output 111 | 112 | .next 113 | out 114 | 115 | # Nuxt.js build / generate output 116 | 117 | .nuxt 118 | dist 119 | 120 | # Gatsby files 121 | 122 | .cache/ 123 | 124 | # Comment in the public line in if your project uses Gatsby and not Next.js 125 | 126 | # https://nextjs.org/blog/next-9-1#public-directory-support 127 | 128 | # public 129 | 130 | # vuepress build output 131 | 132 | .vuepress/dist 133 | 134 | # vuepress v2.x temp and cache directory 135 | 136 | .temp 137 | .cache 138 | 139 | # Docusaurus cache and generated files 140 | 141 | .docusaurus 142 | 143 | # Serverless directories 144 | 145 | .serverless/ 146 | 147 | # FuseBox cache 148 | 149 | .fusebox/ 150 | 151 | # DynamoDB Local files 152 | 153 | .dynamodb/ 154 | 155 | # TernJS port file 156 | 157 | .tern-port 158 | 159 | # Stores VSCode versions used for testing VSCode extensions 160 | 161 | .vscode-test 162 | 163 | # yarn v2 164 | 165 | .yarn/cache 166 | .yarn/unplugged 167 | .yarn/build-state.yml 168 | .yarn/install-state.gz 169 | .pnp.\* 170 | 171 | # IntelliJ based IDEs 172 | .idea 173 | 174 | # Finder (MacOS) folder config 175 | .DS_Store 176 | 177 | -------------------------------------------------------------------------------- /examples/nextjs-movies/README.md: -------------------------------------------------------------------------------- 1 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fupstash%2Fsearch-js%2Ftree%2Fmain%2Fexamples%2Fnextjs-movies&project-name=upstash-search&repository-name=upstash-search&demo-title=Movies%20AI%20Search%20-%20Upstash%20Search&demo-url=https%3A%2F%2Fupstash-search-movies.vercel.app%2F&products=%5B%7B%22type%22%3A%22integration%22%2C%22integrationSlug%22%3A%22upstash%22%2C%22productSlug%22%3A%22upstash-search%22%2C%22protocol%22%3A%22storage%22%2C%22group%22%3A%22%22%7D%5D) 2 | 3 | # Upstash Search - Movies Example 4 | 5 | This is a [Next.js](https://nextjs.org) application demonstrating the semantic search capabilities of [Upstash Search](https://upstash.com/docs/search). It allows you to search for movies using natural language queries and find semantically similar content from a movie dataset. 6 | 7 | ## Features 8 | 9 | - **Semantic Search:** Utilizes Upstash Search's AI-powered semantic search to find movies based on meaning and context, not just keywords. 10 | - **Movie Details:** View detailed information about specific movies by clicking on search results. 11 | - **Smart Filtering:** Advanced search capabilities that understand the intent behind your queries. 12 | - **Command-line Data Import:** Easy setup with a dedicated script to populate your database. 13 | 14 | ## Setup 15 | 16 | Follow these steps to get the application running: 17 | 18 | 1. **Install Dependencies:** 19 | ```bash 20 | npm install 21 | ``` 22 | 23 | 2. **Create Upstash Search Database:** 24 | - Go to the [Upstash Console](https://console.upstash.com/search) and create a new Search database. 25 | - Once created, copy your `UPSTASH_SEARCH_REST_URL` and `UPSTASH_SEARCH_REST_TOKEN`. 26 | 27 | 3. **Set Environment Variables:** 28 | Create a `.env` file in the project root and add your credentials: 29 | ```env 30 | UPSTASH_SEARCH_REST_URL="YOUR_UPSTASH_SEARCH_REST_URL" 31 | UPSTASH_SEARCH_REST_TOKEN="YOUR_UPSTASH_SEARCH_REST_TOKEN" 32 | ``` 33 | Replace the placeholder values with your actual Upstash Search credentials. 34 | 35 | You can also control [reranking](https://upstash.com/docs/search/features/reranking) through env variables. If you want to enable reranking, set env variable `RERANKING_ENABLED` to `true`. Reranking is disabled by default. 36 | 37 | 4. **Populate the Database:** 38 | Run the data upsert script to populate your Upstash Search database with the movie dataset: 39 | ```bash 40 | npm run upsert-data 41 | ``` 42 | This script will process and upload all movie data to your database. You'll see progress logs as batches are uploaded. 43 | 44 | 5. **Start the Development Server:** 45 | ```bash 46 | npm run dev 47 | ``` 48 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the application. 49 | 50 | 6. **Search for Movies:** 51 | Try semantic search queries like: 52 | - "space adventure with robots" 53 | - "romantic comedy in Paris" 54 | - "thriller about artificial intelligence" 55 | - "superhero movie with humor" 56 | 57 | ## Learn More About Upstash Search 58 | 59 | - [Upstash Search Documentation](https://upstash.com/docs/search) 60 | - [Upstash Search GitHub Repository](https://github.com/upstash/search-js) 61 | -------------------------------------------------------------------------------- /src/client/search-client.ts: -------------------------------------------------------------------------------- 1 | import type { Requester, UpstashRequest, UpstashResponse } from "@upstash/vector"; 2 | import { UpstashError } from "./error"; 3 | 4 | type CacheSetting = 5 | | "default" 6 | | "force-cache" 7 | | "no-cache" 8 | | "no-store" 9 | | "only-if-cached" 10 | | "reload" 11 | | false; 12 | 13 | export type RetryConfig = 14 | | false 15 | | { 16 | /** 17 | * The number of retries to attempt before giving up. 18 | * 19 | * @default 5 20 | */ 21 | retries?: number; 22 | /** 23 | * A backoff function receives the current retry cound and returns a number in milliseconds to wait before retrying. 24 | * 25 | * @default 26 | * ```ts 27 | * Math.exp(retryCount) * 50 28 | * ``` 29 | */ 30 | backoff?: (retryCount: number) => number; 31 | }; 32 | 33 | export type RequesterConfig = { 34 | /** 35 | * Configure the retry behaviour in case of network errors 36 | */ 37 | retry?: RetryConfig; 38 | 39 | /** 40 | * Configure the cache behaviour 41 | * @default "no-store" 42 | */ 43 | cache?: CacheSetting; 44 | }; 45 | 46 | export type HttpClientConfig = { 47 | headers?: Record; 48 | baseUrl: string; 49 | retry?: RetryConfig; 50 | } & RequesterConfig; 51 | 52 | export class HttpClient implements Requester { 53 | public baseUrl: string; 54 | public headers: Record; 55 | public readonly options: { 56 | cache?: CacheSetting; 57 | }; 58 | 59 | public readonly retry: { 60 | attempts: number; 61 | backoff: (retryCount: number) => number; 62 | }; 63 | 64 | public constructor(config: HttpClientConfig) { 65 | this.options = { 66 | cache: config.cache, 67 | }; 68 | 69 | this.baseUrl = config.baseUrl.replace(/\/$/, ""); 70 | 71 | this.headers = { 72 | "Content-Type": "application/json", 73 | ...config.headers, 74 | }; 75 | 76 | this.retry = 77 | typeof config?.retry === "boolean" && config?.retry === false 78 | ? { 79 | attempts: 1, 80 | backoff: () => 0, 81 | } 82 | : { 83 | attempts: config?.retry?.retries ?? 5, 84 | backoff: config?.retry?.backoff ?? ((retryCount) => Math.exp(retryCount) * 50), 85 | }; 86 | } 87 | 88 | public async request(req: UpstashRequest): Promise> { 89 | const requestOptions = { 90 | cache: this.options.cache, 91 | method: "POST", 92 | headers: this.headers, 93 | body: JSON.stringify(req.body), 94 | keepalive: true, 95 | }; 96 | 97 | let res: Response | null = null; 98 | let error: Error | null = null; 99 | for (let i = 0; i <= this.retry.attempts; i++) { 100 | try { 101 | res = await fetch([this.baseUrl, ...(req.path ?? [])].join("/"), requestOptions); 102 | break; 103 | } catch (error_) { 104 | error = error_ as Error; 105 | if (i < this.retry.attempts) { 106 | await new Promise((r) => setTimeout(r, this.retry.backoff(i))); 107 | } 108 | } 109 | } 110 | if (!res) { 111 | throw error ?? new Error("Exhausted all retries"); 112 | } 113 | 114 | const body = (await res.json()) as UpstashResponse; 115 | if (!res.ok) { 116 | throw new UpstashError(`${body.error}`); 117 | } 118 | 119 | return { result: body.result, error: body.error }; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/platforms/nodejs.ts: -------------------------------------------------------------------------------- 1 | import * as core from "./../search"; 2 | import { HttpClient, type RequesterConfig } from "../client/search-client"; 3 | import { UpstashError } from "../client/error"; 4 | import { getRuntime, VERSION } from "../client/telemetry"; 5 | 6 | /** 7 | * Connection credentials for upstash vector. 8 | * Get them from https://console.upstash.com/vector/ 9 | */ 10 | export type ClientConfig = { 11 | /** 12 | * UPSTASH_SEARCH_REST_URL 13 | */ 14 | url?: string; 15 | /** 16 | * UPSTASH_SEARCH_REST_TOKEN 17 | */ 18 | token?: string; 19 | 20 | /** 21 | * Enable telemetry to help us improve the SDK. 22 | * The sdk will send the sdk version, platform and node version as telemetry headers. 23 | * 24 | * @default true 25 | */ 26 | enableTelemetry?: boolean; 27 | } & RequesterConfig; 28 | 29 | /** 30 | * Provides search capabilities over indexes. 31 | */ 32 | export class Search extends core.Search { 33 | /** 34 | * Creates a new Search instance. 35 | * 36 | * @param vectorIndex - The underlying index used for search operations. 37 | */ 38 | constructor(params: ClientConfig) { 39 | const environment = 40 | typeof process === "undefined" ? ({} as Record) : process.env; 41 | 42 | const token = 43 | params?.token ?? 44 | environment.NEXT_PUBLIC_UPSTASH_SEARCH_REST_TOKEN ?? 45 | environment.UPSTASH_SEARCH_REST_TOKEN; 46 | const url = 47 | params?.url ?? 48 | environment.NEXT_PUBLIC_UPSTASH_SEARCH_REST_URL ?? 49 | environment.UPSTASH_SEARCH_REST_URL; 50 | 51 | if (!token) { 52 | throw new UpstashError("UPSTASH_SEARCH_REST_TOKEN is missing!"); 53 | } 54 | if (!url) { 55 | throw new UpstashError("UPSTASH_SEARCH_REST_URL is missing!"); 56 | } 57 | 58 | if (url.startsWith(" ") || url.endsWith(" ") || /\r|\n/.test(url)) { 59 | console.warn("The vector url contains whitespace or newline, which can cause errors!"); 60 | } 61 | if (token.startsWith(" ") || token.endsWith(" ") || /\r|\n/.test(token)) { 62 | console.warn("The vector token contains whitespace or newline, which can cause errors!"); 63 | } 64 | 65 | const enableTelemetry = environment.UPSTASH_DISABLE_TELEMETRY 66 | ? false 67 | : (params?.enableTelemetry ?? true); 68 | 69 | const telemetryHeaders: Record = enableTelemetry 70 | ? { 71 | "Upstash-Telemetry-Sdk": `upstash-search-js@${VERSION}`, 72 | "Upstash-Telemetry-Platform": environment.VERCEL 73 | ? "vercel" 74 | : environment.AWS_REGION 75 | ? "aws" 76 | : "unknown", 77 | "Upstash-Telemetry-Runtime": getRuntime(), 78 | } 79 | : {}; 80 | 81 | const client = new HttpClient({ 82 | baseUrl: url, 83 | retry: params?.retry, 84 | headers: { authorization: `Bearer ${token}`, ...telemetryHeaders }, 85 | cache: params?.cache === false ? undefined : params?.cache || "no-store", 86 | }); 87 | 88 | super(client); 89 | } 90 | 91 | /** 92 | * Creates a new Search instance using env variables 93 | * `UPSTASH_SEARCH_REST_URL` and 94 | * `UPSTASH_SEARCH_REST_TOKEN` 95 | * 96 | * @param env 97 | * @returns 98 | */ 99 | static fromEnv = ( 100 | env?: { 101 | UPSTASH_SEARCH_REST_URL: string; 102 | UPSTASH_SEARCH_REST_TOKEN: string; 103 | }, 104 | config?: Omit 105 | ) => { 106 | const url = env?.UPSTASH_SEARCH_REST_URL; 107 | const token = env?.UPSTASH_SEARCH_REST_TOKEN; 108 | 109 | return new Search({ url, token, ...config }); 110 | }; 111 | } 112 | -------------------------------------------------------------------------------- /examples/search-docs/components/SearchComponent.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { SearchBar } from "@upstash/search-ui" 4 | import "@upstash/search-ui/dist/index.css" 5 | import { Search } from "@upstash/search" 6 | import { FileText } from "lucide-react" 7 | import { getIndexColor } from "@/utils/colors" 8 | 9 | // Initialize Upstash Search client 10 | const client = new Search({ 11 | url: process.env.NEXT_PUBLIC_UPSTASH_SEARCH_URL || "", 12 | token: process.env.NEXT_PUBLIC_UPSTASH_SEARCH_READONLY_TOKEN || "", 13 | }) 14 | 15 | interface SearchResult { 16 | id: string 17 | content: { 18 | title: string 19 | fullContent: string 20 | } 21 | metadata: { 22 | url: string 23 | path: string 24 | contentLength: number 25 | crawledAt: string 26 | } 27 | score: number 28 | indexName?: string 29 | } 30 | 31 | async function searchDocs(query: string): Promise { 32 | if (!query.trim()) return [] 33 | 34 | try { 35 | const indexes = await client.listIndexes() 36 | 37 | const searchPromises = indexes.map(async (indexName) => { 38 | try { 39 | const index = client.index(indexName) 40 | const searchParams: any = { 41 | query, 42 | limit: 10, 43 | reranking: true 44 | } 45 | 46 | const results = await index.search(searchParams) 47 | 48 | return (results as any[]).map((result, i) => ({ 49 | ...result, 50 | id: `${indexName}-${result.id}`, 51 | indexName 52 | })) 53 | } catch (error) { 54 | console.error(`Error searching ${indexName}:`, error) 55 | return [] 56 | } 57 | }) 58 | 59 | const resultArrays = await Promise.all(searchPromises) 60 | 61 | const allResults = resultArrays.flat() as SearchResult[] 62 | 63 | const topResults = allResults 64 | .sort((a, b) => (b.score || 0) - (a.score || 0)) 65 | .slice(0, 10) 66 | 67 | return topResults 68 | 69 | } catch (error) { 70 | console.error('Search error:', error) 71 | return [] 72 | } 73 | } 74 | 75 | 76 | export default function SearchComponent() { 77 | 78 | return ( 79 |
80 | {/* Search Bar */} 81 |
82 | 83 | 84 | 85 | 86 | 87 | 90 | {(result) => ( 91 | 92 | 93 | 94 | 95 | 96 | 97 | { 98 | window.open(result.metadata?.url, "_blank") 99 | }}> 100 | 101 | {result.content?.title} {result.indexName} 102 | 103 | 104 |

{`${result.content.fullContent.slice(0, 100)}...`}

105 |
106 |
107 | )} 108 |
109 |
110 |
111 |
112 |
113 | ) 114 | } 115 | -------------------------------------------------------------------------------- /.github/scripts/npm_retention.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | from datetime import datetime, timedelta 4 | import json 5 | import subprocess 6 | import re 7 | 8 | PACKAGE_NAME = "@upstash/vector" 9 | DAYS_TO_KEEP = 7 10 | CI_VERSIONS_TO_KEEP = 5 11 | NPM_TOKEN = os.environ.get("NPM_TOKEN") 12 | 13 | 14 | def run_npm_command(command): 15 | try: 16 | result = subprocess.run(command, capture_output=True, text=True, check=True) 17 | return result.stdout.strip() 18 | except subprocess.CalledProcessError as e: 19 | print(f"Error running command: {e}") 20 | print(e.stderr) 21 | return None 22 | 23 | 24 | def get_package_versions(): 25 | output = run_npm_command(["npm", "view", PACKAGE_NAME, "versions", "--json"]) 26 | if output: 27 | return json.loads(output) 28 | print("Warning: No package version returned.") 29 | return [] 30 | 31 | 32 | def get_version_details(version): 33 | output = run_npm_command(["npm", "view", f"{PACKAGE_NAME}@{version}", "--json"]) 34 | if output: 35 | return json.loads(output) 36 | print("Warning: No version detail returned.") 37 | return {} 38 | 39 | 40 | def parse_ci_version_date(version): 41 | match = re.search(r"-(\d{14})$", version) 42 | if match: 43 | date_str = match.group(1) 44 | return datetime.strptime(date_str, "%Y%m%d%H%M%S") 45 | return None 46 | 47 | 48 | def is_ci_version(version): 49 | return bool(re.search(r"-ci\.", version)) 50 | 51 | 52 | def deprecate_package_version(version): 53 | result = run_npm_command( 54 | [ 55 | "npm", 56 | "deprecate", 57 | f"{PACKAGE_NAME}@{version}", 58 | "This CI version has been deprecated due to retention policy", 59 | ] 60 | ) 61 | if result is not None: 62 | print(f"Successfully deprecated version: {version}") 63 | return True 64 | else: 65 | print(f"Failed to deprecate version: {version}") 66 | return False 67 | 68 | 69 | def apply_retention_policy(): 70 | versions = get_package_versions() 71 | 72 | now = datetime.utcnow() 73 | retention_date = now - timedelta(days=DAYS_TO_KEEP) 74 | 75 | ci_versions = [] 76 | 77 | for version in versions: 78 | if is_ci_version(version): 79 | ci_date = parse_ci_version_date(version) 80 | if ci_date: 81 | version_details = get_version_details(version) 82 | if version_details.get("deprecated"): 83 | print(f"Skipping deprecated version: {version}") 84 | continue 85 | ci_versions.append((version, ci_date)) 86 | else: 87 | print(f"Warning: Could not parse date from CI version: {version}") 88 | 89 | ci_versions.sort(key=lambda x: x[1], reverse=True) 90 | 91 | versions_to_keep = [] 92 | versions_to_deprecate = [] 93 | 94 | for version, date in ci_versions: 95 | if len(versions_to_keep) < CI_VERSIONS_TO_KEEP or date > retention_date: 96 | versions_to_keep.append(version) 97 | else: 98 | versions_to_deprecate.append(version) 99 | print(f"Deprecating version: {version}") 100 | 101 | for version in versions_to_deprecate: 102 | version_deprecated = deprecate_package_version(version) 103 | if not version_deprecated: 104 | print(f"Failed to delete or deprecate version: {version}") 105 | 106 | print(f"Keeping {len(versions_to_keep)} CI versions:") 107 | for version in versions_to_keep: 108 | print(f" {version}") 109 | 110 | 111 | if __name__ == "__main__": 112 | apply_retention_policy() 113 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/scripts/parser.ts: -------------------------------------------------------------------------------- 1 | import { DOMParser } from 'xmldom'; 2 | 3 | // Define the interface for our entry data 4 | interface FeedEntry { 5 | id: string; 6 | title: string; 7 | link: string; 8 | updated: string; 9 | content: string; 10 | author: string[]; 11 | } 12 | 13 | // Function to extract text content from XML elements 14 | function getTextContent(element: Element | null): string { 15 | return element?.textContent?.trim() || ''; 16 | } 17 | 18 | // Function to get href attribute from link elements 19 | function getLinkHref(element: Element | null): string { 20 | return element?.getAttribute('href') || ''; 21 | } 22 | 23 | // Function to extract authors from entry 24 | function getAuthors(entryElement: Element): string[] { 25 | const authors: string[] = []; 26 | const authorElements = entryElement.getElementsByTagName('author'); 27 | 28 | for (let i = 0; i < authorElements.length; i++) { 29 | const nameElement = authorElements[i].getElementsByTagName('name')[0]; 30 | const authorName = getTextContent(nameElement); 31 | if (authorName) { 32 | authors.push(authorName); 33 | } 34 | } 35 | 36 | return authors; 37 | } 38 | 39 | // Function to extract content from the content element 40 | function getContent(entryElement: Element): string { 41 | const contentElement = entryElement.getElementsByTagName('content')[0]; 42 | if (!contentElement) return ''; 43 | 44 | // Get the inner content, handling XHTML content 45 | const divElement = contentElement.getElementsByTagName('div')[0]; 46 | if (divElement) { 47 | // Extract text content from all paragraphs and elements 48 | const textContent = divElement.textContent || ''; 49 | return textContent.trim(); 50 | } 51 | 52 | return getTextContent(contentElement); 53 | } 54 | 55 | // Main function to parse the XML file and extract entries 56 | async function parseXMLFeed(xmlUrl: string): Promise { 57 | try { 58 | // Fetch the XML content 59 | const response = await fetch(xmlUrl); 60 | const xmlContent = await response.text(); 61 | 62 | // Parse the XML 63 | const parser = new DOMParser(); 64 | const xmlDoc = parser.parseFromString(xmlContent, 'text/xml'); 65 | 66 | // Get all entry elements 67 | const entries = xmlDoc.getElementsByTagName('entry'); 68 | const feedEntries: FeedEntry[] = []; 69 | 70 | // Process each entry 71 | for (let i = 0; i < entries.length; i++) { 72 | const entry = entries[i]; 73 | 74 | const feedEntry: FeedEntry = { 75 | id: getTextContent(entry.getElementsByTagName('id')[0]), 76 | title: getTextContent(entry.getElementsByTagName('title')[0]), 77 | link: getLinkHref(entry.getElementsByTagName('link')[0]), 78 | updated: getTextContent(entry.getElementsByTagName('updated')[0]), 79 | content: getContent(entry), 80 | author: getAuthors(entry) 81 | }; 82 | 83 | feedEntries.push(feedEntry); 84 | } 85 | 86 | return feedEntries; 87 | 88 | } catch (error) { 89 | console.error('Error parsing XML file:', error); 90 | throw error; 91 | } 92 | } 93 | 94 | async function getEntries() { 95 | const xmlUrl = 'https://vercel.com/atom'; // Update this path to your XML file location 96 | 97 | try { 98 | // Parse the XML and get entries 99 | const entries = await parseXMLFeed(xmlUrl); 100 | 101 | return entries; // Return the array for further use 102 | 103 | } catch (error) { 104 | console.error('Failed to process XML feed:', error); 105 | return []; 106 | } 107 | } 108 | 109 | // Export the functions for use in other modules 110 | export { parseXMLFeed, type FeedEntry, getEntries }; 111 | -------------------------------------------------------------------------------- /src/client/metadata.ts: -------------------------------------------------------------------------------- 1 | type MutuallyExclusives = { 2 | [P in TFields]: { [Q in P]: Q extends "in" | "notIn" ? TParameter[] : TParameter } & { 3 | [R in Exclude]?: never; 4 | }; 5 | }[TFields]; 6 | 7 | type StringOperation = "equals" | "notEquals" | "glob" | "notGlob" | "in" | "notIn"; 8 | 9 | type NumberOperation = 10 | | "equals" 11 | | "notEquals" 12 | | "lessThan" 13 | | "lessThanOrEquals" 14 | | "greaterThan" 15 | | "greaterThanOrEquals" 16 | | "in" 17 | | "notIn"; 18 | 19 | type BooleanOperation = "equals" | "notEquals" | "in" | "notIn"; 20 | 21 | type ArrayOperation = "contains" | "notContains"; 22 | 23 | // Map operations to their string representations 24 | const operationMap: Record = { 25 | equals: "=", 26 | notEquals: "!=", 27 | lessThan: "<", 28 | lessThanOrEquals: "<=", 29 | greaterThan: ">", 30 | greaterThanOrEquals: ">=", 31 | glob: "GLOB", 32 | notGlob: "NOT GLOB", 33 | in: "IN", 34 | notIn: "NOT IN", 35 | contains: "CONTAINS", 36 | notContains: "NOT CONTAINS", 37 | }; 38 | 39 | type ValidOperations = T extends number 40 | ? MutuallyExclusives 41 | : T extends string 42 | ? MutuallyExclusives 43 | : T extends boolean 44 | ? MutuallyExclusives 45 | : T extends any[] 46 | ? MutuallyExclusives 47 | : never; 48 | 49 | // Merge TContent and TMetadata, prefixing metadata keys with @metadata. 50 | type MergedFields = TContent & { 51 | [K in keyof TMetadata as `@metadata.${string & K}`]: TMetadata[K]; 52 | }; 53 | 54 | // Type definitions for FilterTree with strict operations 55 | // A leaf must have exactly one field with exactly one operation 56 | type Leaf = { 57 | [Field in keyof TFields]: { 58 | [K in Field]: ValidOperations; 59 | } & { 60 | [K in Exclude]?: never; 61 | }; 62 | }[keyof TFields]; 63 | 64 | export type TreeNode = 65 | | Leaf> 66 | | { OR: TreeNode[] } 67 | | { AND: TreeNode[] }; 68 | 69 | const valueFormatter = (value: string | boolean | number | any[]): string | number | boolean => { 70 | return Array.isArray(value) 71 | ? `(${value.map((v) => (typeof v === "string" ? `'${v}'` : v)).join(", ")})` 72 | : typeof value === "string" 73 | ? `'${value}'` 74 | : value; 75 | }; 76 | 77 | // Recursive function to construct filter string from FilterTree 78 | export function constructFilterString( 79 | filterTree: TreeNode 80 | ): string { 81 | if ("OR" in filterTree) { 82 | return `(${filterTree.OR.map((node: TreeNode) => constructFilterString(node)).join(" OR ")})`; 83 | } 84 | if ("AND" in filterTree) { 85 | return `(${filterTree.AND.map((node: TreeNode) => constructFilterString(node)).join(" AND ")})`; 86 | } 87 | 88 | const field = Object.keys(filterTree)[0]; 89 | const operationObj = (filterTree as Record)[field]; 90 | const operation = Object.keys(operationObj)[0]; 91 | const value = operationObj[operation as keyof typeof operationObj]; 92 | 93 | if (!operation || value === undefined) { 94 | throw new Error( 95 | `Invalid filter operation for field ${String(field)}: ${JSON.stringify(operationObj)}` 96 | ); 97 | } 98 | 99 | const mappedOperation = operationMap[operation]; 100 | if (!mappedOperation) { 101 | throw new Error(`Invalid filter operation for field ${String(field)}: ${operation}`); 102 | } 103 | 104 | const formattedValue = valueFormatter(value); 105 | 106 | return `${String(field)} ${mappedOperation} ${formattedValue}`; 107 | } 108 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Upstash AI Search ![npm (scoped)](https://img.shields.io/npm/v/@upstash/search) ![npm bundle size](https://img.shields.io/bundlephobia/minzip/@upstash/search) ![npm weekly download](https://img.shields.io/npm/dw/%40upstash%2Fsearch) 2 | 3 | > [!NOTE] 4 | > **This project is in GA Stage.** 5 | > 6 | > The Upstash Professional Support fully covers this project. It receives regular updates, and bug fixes. 7 | > The Upstash team is committed to maintaining and improving its functionality. 8 | 9 | It is a connectionless (HTTP based) AI Search client and designed for: 10 | 11 | - Serverless functions (AWS Lambda ...) 12 | - Cloudflare Workers 13 | - Next.js, Jamstack ... 14 | - Client side web/mobile applications 15 | - WebAssembly 16 | - and other environments where HTTP is preferred over TCP. 17 | 18 | ## Quick Start 19 | 20 | ### Install 21 | 22 | #### Node.js 23 | 24 | ```bash 25 | npm install @upstash/search 26 | ``` 27 | 28 | ### Create Database 29 | 30 | Create a new database on [Upstash](https://console.upstash.com/search) 31 | 32 | ## Basic Usage: 33 | 34 | ```ts 35 | import { Search } from "@upstash/search"; 36 | 37 | type Content = { 38 | title: string; 39 | genre: "sci-fi" | "fantasy" | "horror" | "action"; 40 | category: "classic" | "modern"; 41 | }; 42 | 43 | type Metadata = { 44 | director: string; 45 | }; 46 | 47 | // Initialize Search client 48 | const client = new Search({ 49 | url: "", 50 | token: "", 51 | }); 52 | 53 | // Create or access a index 54 | const index = client.index("movies"); 55 | 56 | // Upsert data into the index 57 | await index.upsert([ 58 | { 59 | id: "star-wars", 60 | content: { title: "Star Wars", genre: "sci-fi", category: "classic" }, 61 | metadata: { director: "George Lucas" }, 62 | }, 63 | { 64 | id: "inception", 65 | content: { title: "Inception", genre: "action", category: "modern" }, 66 | metadata: { director: "Christopher Nolan" }, 67 | }, 68 | ]); 69 | 70 | // Fetch documents by IDs 71 | const documents = await index.fetch({ 72 | ids: ["star-wars", "inception"], 73 | }); 74 | console.log(documents); 75 | 76 | // AI search with reranking: 77 | const searchResults = await index.search({ 78 | query: "space opera", 79 | limit: 2, 80 | reranking: true, 81 | }); 82 | console.log(searchResults); 83 | 84 | // AI search without reranking: 85 | const searchResults = await index.search({ 86 | query: "space opera", 87 | limit: 2, 88 | }); 89 | console.log(searchResults); 90 | 91 | // AI search with only semantic search 92 | const searchResults = await index.search({ 93 | query: "space opera", 94 | limit: 2, 95 | semanticWeight: 1, 96 | }); 97 | 98 | // AI search with only full-text search 99 | const searchResults = await index.search({ 100 | query: "space opera", 101 | limit: 2, 102 | semanticWeight: 0, 103 | }); 104 | 105 | // AI search with full-text search and sematic search 106 | // combined with equal weights 107 | const searchResults = await index.search({ 108 | query: "space opera", 109 | limit: 2, 110 | semanticWeight: 0.5, 111 | }); 112 | 113 | // AI search without input enrichment 114 | const searchResults = await index.search({ 115 | query: "space opera", 116 | limit: 2, 117 | inputEnrichment: false, 118 | }); 119 | 120 | // AI search without reranking: 121 | const searchResults = await index.search({ 122 | query: "space opera", 123 | limit: 2, 124 | }); 125 | console.log(searchResults); 126 | 127 | // AI search with filter: 128 | const searchResults = await index.search({ 129 | query: "space", 130 | limit: 2, 131 | filter: "category = 'classic'", 132 | }); 133 | 134 | // Delete a document by ID 135 | await index.delete({ 136 | ids: ["star-wars"], 137 | }); 138 | 139 | // Search within a document range 140 | const { nextCursor, documents: rangeDocuments } = await index.range({ 141 | cursor: 0, 142 | limit: 1, 143 | prefix: "in", 144 | }); 145 | console.log(rangeDocuments); 146 | 147 | // Reset the index (delete all documents) 148 | await index.reset(); 149 | 150 | // Get index and namespace info 151 | const info = await search.info(); 152 | console.log(info); 153 | ``` 154 | -------------------------------------------------------------------------------- /examples/nextjs-movies/components/result-data.tsx: -------------------------------------------------------------------------------- 1 | import { Result, ResultCode } from "@/lib/types"; 2 | import KeyValue from "@/components/tag"; 3 | import type { DefinedUseQueryResult } from "@tanstack/react-query"; 4 | 5 | export default function ResultData({ 6 | state, 7 | onChangeQuery = () => { }, 8 | onSubmit = () => { }, 9 | }: { 10 | state: DefinedUseQueryResult; 11 | onChangeQuery: (q: string) => void; 12 | onSubmit: () => void; 13 | }) { 14 | if (state.isFetching) { 15 | return
Loading...
; 16 | } 17 | 18 | if (state.data?.code === ResultCode.UnknownError) { 19 | return ( 20 |
21 |

An error occurred, please try again.

22 |
23 | ); 24 | } 25 | 26 | if (state.data?.code === ResultCode.MinLengthError) { 27 | return ( 28 |
29 |

30 | Please enter at least 2 characters to start searching for movies. 31 |

32 |
33 | ); 34 | } 35 | 36 | if (state.data?.code === ResultCode.Empty) { 37 | return ( 38 |
    39 |
  1. 40 |

    41 | Search movies by title, genre, or description... 42 |

    43 | 52 |
  2. 53 | 54 |
  3. 55 |

    56 | Find movies by plot, characters, or themes... 57 |

    58 | 67 |
  4. 68 | 69 |
  5. 70 |

    71 | Type a movie’s storyline, genre, or cast... 72 |

    73 | 82 |
  6. 83 |
84 | ); 85 | } 86 | 87 | return ( 88 | 120 | ); 121 | } 122 | -------------------------------------------------------------------------------- /src/search-index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect, beforeEach, afterEach } from "bun:test"; 2 | import { Search } from "./platforms/nodejs"; 3 | 4 | const client = Search.fromEnv(); 5 | const indexName = "test-index-name"; 6 | const searchIndex = client.index<{ text: string }, { key: string; count: number }>(indexName); 7 | 8 | describe("SearchIndex", () => { 9 | beforeEach(async () => { 10 | // Ensure the namespace is empty before the tests 11 | await searchIndex.reset(); 12 | 13 | // Insert test data 14 | await searchIndex.upsert([ 15 | { id: "id1", content: { text: "test-data-1" }, metadata: { key: "value1", count: 1 } }, 16 | { id: "id2", content: { text: "test-data-2" }, metadata: { key: "value2", count: 2 } }, 17 | { 18 | id: "different-id3", 19 | content: { text: "different-test-data-3" }, 20 | metadata: { key: "value3", count: 3 }, 21 | }, 22 | ]); 23 | 24 | let info = await searchIndex.info(); 25 | let counter = 0; 26 | while (info.pendingDocumentCount > 0) { 27 | await new Promise((r) => setTimeout(r, 500)); 28 | info = await searchIndex.info(); 29 | counter++; 30 | if (counter > 10) { 31 | throw new Error("Timeout waiting for pendingDocumentCount to be 0"); 32 | } 33 | } 34 | }); 35 | 36 | afterEach(async () => { 37 | // Clean up after tests 38 | await searchIndex.deleteIndex(); 39 | }); 40 | 41 | test("should upsert and retrieve data", async () => { 42 | const results = await searchIndex.fetch({ ids: ["id1", "id2"] }); 43 | 44 | expect(results).toEqual([ 45 | { id: "id1", content: { text: "test-data-1" }, metadata: { key: "value1", count: 1 } }, 46 | { id: "id2", content: { text: "test-data-2" }, metadata: { key: "value2", count: 2 } }, 47 | ]); 48 | }); 49 | 50 | test("should search and return results", async () => { 51 | const results = await searchIndex.search({ 52 | query: "test-data-1", 53 | limit: 2, 54 | filter: "text GLOB 'test*'", 55 | keepOriginalQueryAfterEnrichment: true, 56 | }); 57 | 58 | expect(results).toEqual([ 59 | { 60 | id: "id1", 61 | content: { text: "test-data-1" }, 62 | metadata: { key: "value1", count: 1 }, 63 | score: expect.any(Number), 64 | }, 65 | 66 | { 67 | content: { text: "test-data-2" }, 68 | metadata: { 69 | key: "value2", 70 | count: 2, 71 | }, 72 | id: "id2", 73 | score: expect.any(Number), 74 | }, 75 | ]); 76 | }); 77 | 78 | test("should search with a filter", async () => { 79 | const results = await searchIndex.search({ 80 | query: "test-data", 81 | limit: 2, 82 | filter: { AND: [{ text: { glob: "test-data-1" } }] }, 83 | semanticWeight: 0.5, 84 | inputEnrichment: false, 85 | }); 86 | 87 | expect(results).toEqual([ 88 | { 89 | id: "id1", 90 | content: { text: "test-data-1" }, 91 | metadata: { key: "value1", count: 1 }, 92 | score: expect.any(Number), 93 | }, 94 | ]); 95 | }); 96 | 97 | test("should search with a metadata filter", async () => { 98 | const results = await searchIndex.search({ 99 | query: "test-data", 100 | limit: 2, 101 | filter: { 102 | AND: [{ text: { glob: "*test-data*" } }, { "@metadata.count": { greaterThanOrEquals: 3 } }], 103 | }, 104 | semanticWeight: 0.5, 105 | inputEnrichment: false, 106 | }); 107 | 108 | expect(results).toEqual([ 109 | { 110 | id: "different-id3", 111 | content: { text: "different-test-data-3" }, 112 | metadata: { key: "value3", count: 3 }, 113 | score: expect.any(Number), 114 | }, 115 | ]); 116 | }); 117 | 118 | test("should delete a document", async () => { 119 | const deleteResult = await searchIndex.delete({ ids: ["id1"] }); 120 | expect(deleteResult).toEqual({ deleted: 1 }); 121 | 122 | const results = await searchIndex.fetch({ ids: ["id1"] }); 123 | expect(results).toEqual([null]); // Ensure it's deleted 124 | }); 125 | 126 | test("should get namespace info", async () => { 127 | const info = await searchIndex.info(); 128 | 129 | expect(info).toMatchObject({ 130 | documentCount: expect.any(Number), 131 | pendingDocumentCount: expect.any(Number), 132 | }); 133 | }); 134 | 135 | test("should reset the index", async () => { 136 | await searchIndex.reset(); 137 | const results = await searchIndex.fetch({ ids: ["id2"] }); 138 | 139 | expect(results).toEqual([null]); // Ensure it's cleared 140 | }); 141 | 142 | test("should search within a range", async () => { 143 | const { nextCursor, documents } = await searchIndex.range({ 144 | cursor: "0", 145 | limit: 1, 146 | prefix: "id", 147 | }); 148 | 149 | expect(documents).toEqual([ 150 | { id: "id1", content: { text: "test-data-1" }, metadata: { key: "value1", count: 1 } }, 151 | ]); 152 | 153 | const { documents: nextDocuments } = await searchIndex.range({ 154 | cursor: nextCursor, 155 | limit: 5, 156 | prefix: "id", 157 | }); 158 | 159 | expect(nextDocuments).toEqual([ 160 | { 161 | content: { text: "test-data-2" }, 162 | metadata: { 163 | key: "value2", 164 | count: 2, 165 | }, 166 | id: "id2", 167 | }, 168 | ]); 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /examples/search-docs/components/RecentUpdates.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { getIndexColor } from "@/utils/colors" 4 | import { Search } from "@upstash/search" 5 | import { FileText, Clock } from "lucide-react" 6 | import { useState, useEffect } from "react" 7 | 8 | // Initialize Upstash Search client 9 | const client = new Search({ 10 | url: process.env.NEXT_PUBLIC_UPSTASH_SEARCH_URL || "", 11 | token: process.env.NEXT_PUBLIC_UPSTASH_SEARCH_READONLY_TOKEN || "", 12 | }) 13 | 14 | interface SearchResult { 15 | id: string 16 | content: { 17 | title: string 18 | fullContent: string 19 | } 20 | metadata: { 21 | url: string 22 | path: string 23 | contentLength: number 24 | crawledAt: string 25 | } 26 | score: number 27 | indexName?: string 28 | } 29 | 30 | async function getLatestDocs(): Promise { 31 | try { 32 | const indexes = await client.listIndexes() 33 | 34 | const currentDate = new Date() 35 | currentDate.setDate(currentDate.getDate() - 7) 36 | const oneWeekAgo = currentDate.getTime() 37 | 38 | const rangePromises = indexes.map(async (indexName) => { 39 | try { 40 | const index = client.index(indexName) 41 | let recentDocuments: SearchResult[] = [] 42 | let cursor = "" 43 | 44 | while (true) { 45 | const results = await index.range({ 46 | cursor: cursor, 47 | limit: 100 48 | }) 49 | //TODO: use metadata filter instead as param 50 | const recentBatch = results.documents 51 | .filter(result => { 52 | if (!result.metadata?.crawledAt) return false 53 | const crawledTime = new Date(result.metadata.crawledAt as string).getTime() 54 | return crawledTime >= oneWeekAgo 55 | }) 56 | .map((result) => ({ 57 | ...result, 58 | id: `${indexName}-${result.id}`, 59 | indexName 60 | })) as SearchResult[] 61 | 62 | recentDocuments = recentDocuments.concat(recentBatch) 63 | 64 | if (!results.nextCursor || results.nextCursor === cursor || recentDocuments.length >= 10) { 65 | break 66 | } 67 | cursor = results.nextCursor 68 | } 69 | 70 | return recentDocuments.slice(0, 10) 71 | } catch (error) { 72 | console.error(`Error getting documents from ${indexName}:`, error) 73 | return [] 74 | } 75 | }) 76 | 77 | const allResultArrays = await Promise.all(rangePromises) 78 | 79 | const allResults = allResultArrays.flat() as SearchResult[] 80 | 81 | return allResults 82 | 83 | } catch (error) { 84 | console.error('Error getting latest docs:', error) 85 | return [] 86 | } 87 | } 88 | 89 | export default function RecentUpdates() { 90 | const [latestDocs, setLatestDocs] = useState([]) 91 | const [loadingLatest, setLoadingLatest] = useState(true) 92 | 93 | useEffect(() => { 94 | const loadLatestDocs = async () => { 95 | const latest = await getLatestDocs() 96 | setLatestDocs(latest) 97 | setLoadingLatest(false) 98 | } 99 | 100 | loadingLatest && loadLatestDocs() 101 | }, [latestDocs]) 102 | 103 | const formatDate = (dateString: string) => { 104 | const date = new Date(dateString) 105 | return date.toLocaleDateString() + " " + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) 106 | } 107 | 108 | return ( 109 |
110 |
111 |

112 | 113 | Recent Updates 114 |

115 |
116 | 117 |
118 | {loadingLatest ? ( 119 |
120 |
121 | Loading... 122 |
123 | ) : ( 124 |
125 |
126 | {latestDocs.map((doc, index) => ( 127 |
128 |
window.open(doc.metadata?.url, "_blank")} 131 | > 132 |
133 | 134 |
135 |
136 |

137 | {doc.content?.title || 'Documentation'} 138 | {doc.indexName} 139 | 140 |

141 |
142 |

143 | {`${doc.content.fullContent.slice(0, 100)}...`} 144 |

145 | 146 | {formatDate(doc.metadata.crawledAt)} 147 | 148 |
149 |
150 |
151 | {index < latestDocs.length - 1 && ( 152 |
153 | )} 154 |
155 | ))} 156 | 157 | {latestDocs.length === 0 && ( 158 |
159 | 160 |

No recent updates in the last week

161 |
162 | )} 163 |
164 |
165 | )} 166 |
167 |
168 | ) 169 | } 170 | -------------------------------------------------------------------------------- /src/search-index.ts: -------------------------------------------------------------------------------- 1 | import { UpstashError } from "./client/error"; 2 | import { constructFilterString, type TreeNode } from "./client/metadata"; 3 | import type { HttpClient } from "./client/search-client"; 4 | import type { Dict, VectorIndex, UpsertParameters, SearchResult, Document } from "./types"; 5 | 6 | /** 7 | * Represents a search index for managing and querying documents. 8 | * 9 | * Each SearchIndex instance operates within a specific index, allowing for 10 | * isolated document storage and retrieval. It provides methods to upsert, search, 11 | * fetch, delete, and manage documents within the index. 12 | * 13 | * @template TContent - Content shape associated with each document. 14 | * @template TIndexMetadata - Metadata shape associated with each document. 15 | */ 16 | export class SearchIndex { 17 | /** 18 | * Initializes a new SearchIndex instance for the specified index. 19 | * 20 | * @param vectorIndex - The underlying vector index used for search operations. 21 | * @param indexName - The name to use for this index. Must be a non-empty string. 22 | * @throws Will throw an error if the indexn name is not provided. 23 | */ 24 | constructor( 25 | private httpClient: HttpClient, 26 | private vectorIndex: VectorIndex, 27 | private indexName: string 28 | ) { 29 | if (!indexName) { 30 | throw new Error("indexName is required when defining a SearchIndex"); 31 | } 32 | } 33 | 34 | /** 35 | * Inserts or updates documents in the index. 36 | * 37 | * Documents are identified by their unique IDs. If a document with the same ID exists, it will be updated. 38 | * 39 | * @param params - A document or array of documents to upsert, including `id`, `content`, and optional `metadata`. 40 | * @returns A promise resolving to the result of the upsert operation. 41 | */ 42 | upsert = async ( 43 | params: 44 | | UpsertParameters 45 | | UpsertParameters[] 46 | ) => { 47 | const upsertParams = Array.isArray(params) ? params : [params]; 48 | 49 | const path = ["upsert-data", this.indexName]; 50 | const { result } = (await this.httpClient.request({ 51 | path, 52 | body: upsertParams, 53 | })) as { result: string; error: Error | undefined }; 54 | 55 | return result; 56 | }; 57 | 58 | /** 59 | * Searches for documents matching a query string. 60 | * 61 | * Returns documents that best match the provided query, optionally filtered and limited in number. 62 | * 63 | * @param query - Text string used to find matching documents within the index. 64 | * @param limit - Maximum number of results to retrieve (defaults to 5 documents). 65 | * @param filter - Optional search constraint using either a string expression or structured filter object. 66 | * @param reranking - Optional boolean to use enhanced search result reranking. It will have additional 67 | * cost when enabled. See [Search Pricing](https://upstash.com/pricing/search) for more details 68 | * (False by default) 69 | * @param semanticWeight - Optional relevance balance between semantic and keyword search (0-1 range, defaults to 0.75). 70 | * For instance, 0.2 applies 20% semantic matching with 80% full-text matching. 71 | * You can learn more about how Upstash Search works from [our docs](https://upstash.com/docs/search/features/algorithm). 72 | * @param inputEnrichment - Optional boolean to enhance queries before searching (enabled by default). 73 | * @param keepOriginalQueryAfterEnrichment - Optional boolean to keep the original query alongside the enriched one (false by default). 74 | * @returns Promise that resolves to an array of documents matching the 75 | */ 76 | search = async (params: { 77 | query: string; 78 | limit?: number; 79 | filter?: string | TreeNode; 80 | reranking?: boolean; 81 | semanticWeight?: number; 82 | inputEnrichment?: boolean; 83 | keepOriginalQueryAfterEnrichment?: boolean; 84 | }): Promise> => { 85 | const { 86 | query, 87 | limit = 5, 88 | filter, 89 | reranking, 90 | semanticWeight, 91 | inputEnrichment, 92 | keepOriginalQueryAfterEnrichment, 93 | } = params; 94 | 95 | if (semanticWeight && (semanticWeight < 0 || semanticWeight > 1)) { 96 | throw new UpstashError("semanticWeight must be between 0 and 1"); 97 | } 98 | 99 | const path = ["search", this.indexName]; 100 | const { result } = (await this.httpClient.request({ 101 | path, 102 | body: { 103 | query, 104 | topK: limit, 105 | includeData: true, 106 | includeMetadata: true, 107 | filter: 108 | typeof filter === "string" || filter === undefined 109 | ? filter 110 | : constructFilterString(filter), 111 | reranking, 112 | semanticWeight, 113 | inputEnrichment, 114 | _appendOriginalInputToEnrichmentResult: keepOriginalQueryAfterEnrichment, 115 | }, 116 | })) as { result: SearchResult }; 117 | 118 | return result.map(({ id, content, metadata, score }) => ({ 119 | id, 120 | content, 121 | metadata, 122 | score, 123 | })); 124 | }; 125 | 126 | /** 127 | * Fetches documents by their IDs from the index. 128 | * 129 | * @param params - An array of document IDs to retrieve. 130 | * @returns A promise resolving to an array of documents or `null` if a document is not found. 131 | */ 132 | fetch = async (params: Parameters[0]) => { 133 | const result = await this.vectorIndex.fetch(params, { 134 | namespace: this.indexName, 135 | includeData: true, 136 | includeMetadata: true, 137 | }); 138 | 139 | return result.map((fetchResult) => { 140 | if (!fetchResult) return fetchResult; 141 | 142 | return { 143 | id: fetchResult.id, 144 | content: (fetchResult as unknown as { content: TContent }).content, 145 | metadata: fetchResult.metadata as TIndexMetadata, 146 | }; 147 | }); 148 | }; 149 | 150 | /** 151 | * Deletes documents by their IDs from the index. 152 | * 153 | * @param params - An array of document IDs to delete. 154 | * @returns A promise resolving to the result of the deletion operation. 155 | */ 156 | delete = async (params: Parameters[0]) => { 157 | return await this.vectorIndex.delete(params, { namespace: this.indexName }); 158 | }; 159 | 160 | /** 161 | * Retrieves documents within a specific range, with pagination support. 162 | * 163 | * Useful for paginating through large result sets by providing a `cursor`. 164 | * 165 | * @param params - Range parameters including `cursor`, `limit`, and ID `prefix`. 166 | * @returns A promise resolving to the next cursor and documents in the range. 167 | */ 168 | range = async (params: { cursor: string; limit: number; prefix?: string }) => { 169 | const { nextCursor, vectors } = await this.vectorIndex.range( 170 | { ...params, includeData: true, includeMetadata: true }, 171 | { namespace: this.indexName } 172 | ); 173 | 174 | return { 175 | nextCursor, 176 | documents: (vectors as unknown as Document[]).map( 177 | ({ id, content, metadata }) => ({ 178 | id, 179 | content, 180 | metadata, 181 | }) 182 | ), 183 | }; 184 | }; 185 | 186 | /** 187 | * Clears all documents in the current index. 188 | * 189 | * Useful for resetting the index before or after tests, or when a clean state is needed. 190 | * 191 | * @returns A promise resolving to the result of the reset operation. 192 | */ 193 | reset = async () => { 194 | return await this.vectorIndex.reset({ namespace: this.indexName }); 195 | }; 196 | 197 | /** 198 | * Deletes the entire index and all its documents. 199 | * 200 | * Use with caution, as this operation is irreversible. 201 | * 202 | * @returns A promise resolving to the result of the delete operation. 203 | */ 204 | deleteIndex = async () => { 205 | return await this.vectorIndex.deleteNamespace(this.indexName); 206 | }; 207 | 208 | /** 209 | * Retrieves information about the current index. 210 | * 211 | * Provides document count and pending document count, indicating documents that are awaiting indexing. 212 | * 213 | * @returns A promise resolving to index information with document counts. 214 | */ 215 | info = async () => { 216 | const info = await this.vectorIndex.info(); 217 | const { pendingVectorCount, vectorCount } = info.namespaces[this.indexName] ?? { 218 | pendingVectorCount: 0, 219 | vectorCount: 0, 220 | }; 221 | 222 | return { 223 | pendingDocumentCount: pendingVectorCount, 224 | documentCount: vectorCount, 225 | }; 226 | }; 227 | } 228 | -------------------------------------------------------------------------------- /examples/upstash-search-vercel-changelog/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { 5 | Input, 6 | Button, 7 | DatePicker, 8 | Card, 9 | List, 10 | Space, 11 | Typography, 12 | Tag, 13 | message, 14 | Row, 15 | Col, 16 | Empty, 17 | Select, 18 | } from "antd"; 19 | import { SearchOutlined } from "@ant-design/icons"; 20 | import dayjs, { Dayjs } from "dayjs"; 21 | import { intToDate } from "@/lib/dateUtils"; 22 | import { SearchAPIResponse } from "@/lib/types"; 23 | 24 | const { Title, Text, Link } = Typography; 25 | const { RangePicker } = DatePicker; 26 | 27 | export default function Home() { 28 | const [searchQuery, setSearchQuery] = useState(""); 29 | const [dateRange, setDateRange] = useState< 30 | [Dayjs | null, Dayjs | null] | null 31 | >(null); 32 | const [contentType, setContentType] = useState<"all" | "blog" | "changelog">( 33 | "all" 34 | ); 35 | const [loading, setLoading] = useState(false); 36 | const [searchResponse, setSearchResponse] = useState(null); 37 | 38 | const handleSearch = async () => { 39 | if (!searchQuery.trim()) { 40 | message.warning("Please enter a search query"); 41 | return; 42 | } 43 | 44 | setLoading(true); 45 | try { 46 | const payload: { 47 | query: string; 48 | dateFrom?: string; 49 | dateUntil?: string; 50 | contentType?: string; 51 | } = { 52 | query: searchQuery, 53 | }; 54 | 55 | if (dateRange) { 56 | if (dateRange[0]) { 57 | payload.dateFrom = dateRange[0].toISOString(); 58 | } 59 | if (dateRange[1]) { 60 | payload.dateUntil = dateRange[1].toISOString(); 61 | } 62 | } 63 | 64 | if (contentType !== "all") { 65 | payload.contentType = contentType; 66 | } 67 | 68 | const response = await fetch("/api/search", { 69 | method: "POST", 70 | headers: { 71 | "Content-Type": "application/json", 72 | }, 73 | body: JSON.stringify(payload), 74 | }); 75 | 76 | if (!response.ok) { 77 | throw new Error("Search failed"); 78 | } 79 | 80 | const data = (await response.json()) as SearchAPIResponse; 81 | setSearchResponse(data); 82 | } catch (error) { 83 | console.error("Search error:", error); 84 | message.error("Failed to perform search"); 85 | } finally { 86 | setLoading(false); 87 | } 88 | }; 89 | 90 | const formatDate = (dateInt: number) => { 91 | const date = intToDate(dateInt); 92 | return dayjs(date).format("MMM DD, YYYY"); 93 | }; 94 | 95 | return ( 96 |
97 | {/* Header */} 98 |
99 |
100 | 101 | Vercel & Upstash Search Demo 102 | 103 | 104 | Search through Vercel's changelog and blog entries (parsed from: 105 | https://vercel.com/atom 106 | ). Source code and quickstart available on 107 | @upstash/search Repository. 108 | 109 | 110 |
111 |
112 | 113 | {/* Search Section */} 114 |
115 | 116 | 117 |
118 | 119 | Search Query 120 | 121 | setSearchQuery(e.target.value)} 126 | onPressEnter={handleSearch} 127 | prefix={} 128 | /> 129 |
130 | 131 |
132 |
133 | 134 | Date Range (Optional) 135 | 136 | 144 | 145 | You can select just one date or a range of dates 146 | 147 |
148 | 149 |
150 | 151 | Content Type 152 | 153 | 166 |
167 |
168 | 169 | 179 |
180 |
181 | 182 | {/* Results Section */} 183 | {searchResponse && ( 184 |
185 |
186 | 187 | Search Results 188 | 189 | 190 | Showing results for "{searchResponse.query}" 191 | {(searchResponse.filters.dateFrom || searchResponse.filters.dateUntil) && ( 192 | <> 193 | {" "} 194 | {searchResponse.filters.dateFrom && searchResponse.filters.dateUntil ? ( 195 | <> 196 | from {dayjs(searchResponse.filters.dateFrom).format("MMM DD, YYYY")} to{" "} 197 | {dayjs(searchResponse.filters.dateUntil).format("MMM DD, YYYY")} 198 | 199 | ) : searchResponse.filters.dateFrom ? ( 200 | <>on {dayjs(searchResponse.filters.dateFrom).format("MMM DD, YYYY")} 201 | ) : ( 202 | <>until {dayjs(searchResponse.filters.dateUntil).format("MMM DD, YYYY")} 203 | )} 204 | 205 | )} 206 | {searchResponse.filters.contentType && searchResponse.filters.contentType !== "all" && ( 207 | <> 208 | {" "} 209 | in{" "} 210 | {searchResponse.filters.contentType === "blog" 211 | ? "blog posts" 212 | : "changelog entries"} 213 | 214 | )} 215 | 216 |
217 | 218 | {searchResponse.results.length > 0 ? ( 219 | ( 222 | 223 | 224 | 225 | 230 | 235 | {item.content.title} 236 | 237 | 238 | {item.content.content} 239 | 240 | 241 | 242 | 243 | 244 |
245 | 246 | Score: {item.score.toFixed(2)} 247 | 248 | 256 | {item.metadata?.kind === "blog" 257 | ? "Blog" 258 | : "Changelog"} 259 | 260 |
261 | 262 | {formatDate(item.metadata!.dateInt)} 263 | 264 |
265 | 266 |
267 |
268 | )} 269 | /> 270 | ) : ( 271 | 272 | 276 | 277 | )} 278 |
279 | )} 280 |
281 |
282 | ); 283 | } 284 | -------------------------------------------------------------------------------- /src/client/metadata.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, test, expect } from "bun:test"; 2 | import { constructFilterString } from "./metadata"; 3 | 4 | describe("constructFilterString", () => { 5 | // String operations tests 6 | describe("String operations", () => { 7 | test("should handle string equals operation", () => { 8 | const filter = { name: { equals: "John" } }; 9 | expect(constructFilterString(filter)).toBe("name = 'John'"); 10 | }); 11 | 12 | test("should handle string notEquals operation", () => { 13 | const filter = { name: { notEquals: "John" } }; 14 | expect(constructFilterString(filter)).toBe("name != 'John'"); 15 | }); 16 | 17 | test("should handle string glob operation", () => { 18 | const filter = { name: { glob: "John*" } }; 19 | expect(constructFilterString(filter)).toBe("name GLOB 'John*'"); 20 | }); 21 | 22 | test("should handle string notGlob operation", () => { 23 | const filter = { name: { notGlob: "John*" } }; 24 | expect(constructFilterString(filter)).toBe("name NOT GLOB 'John*'"); 25 | }); 26 | 27 | test("should handle string in operation", () => { 28 | const filter = { name: { in: ["John", "Jane", "Bob"] } }; 29 | expect(constructFilterString(filter)).toBe("name IN ('John', 'Jane', 'Bob')"); 30 | }); 31 | 32 | test("should handle string notIn operation", () => { 33 | const filter = { name: { notIn: ["John", "Jane"] } }; 34 | expect(constructFilterString(filter)).toBe("name NOT IN ('John', 'Jane')"); 35 | }); 36 | 37 | test("should handle empty string values", () => { 38 | const filter = { name: { equals: "" } }; 39 | expect(constructFilterString(filter)).toBe("name = ''"); 40 | }); 41 | 42 | test("should handle string with special characters", () => { 43 | const filter = { name: { equals: 'John\'s "data"' } }; 44 | expect(constructFilterString(filter)).toBe("name = 'John's \"data\"'"); 45 | }); 46 | }); 47 | 48 | // Number operations tests 49 | describe("Number operations", () => { 50 | test("should handle number equals operation", () => { 51 | const filter = { age: { equals: 25 } }; 52 | expect(constructFilterString(filter)).toBe("age = 25"); 53 | }); 54 | 55 | test("should handle number notEquals operation", () => { 56 | const filter = { age: { notEquals: 25 } }; 57 | expect(constructFilterString(filter)).toBe("age != 25"); 58 | }); 59 | 60 | test("should handle number lessThan operation", () => { 61 | const filter = { age: { lessThan: 30 } }; 62 | expect(constructFilterString(filter)).toBe("age < 30"); 63 | }); 64 | 65 | test("should handle number lessThanOrEquals operation", () => { 66 | const filter = { age: { lessThanOrEquals: 30 } }; 67 | expect(constructFilterString(filter)).toBe("age <= 30"); 68 | }); 69 | 70 | test("should handle number greaterThan operation", () => { 71 | const filter = { age: { greaterThan: 18 } }; 72 | expect(constructFilterString(filter)).toBe("age > 18"); 73 | }); 74 | 75 | test("should handle number greaterThanOrEquals operation", () => { 76 | const filter = { age: { greaterThanOrEquals: 18 } }; 77 | expect(constructFilterString(filter)).toBe("age >= 18"); 78 | }); 79 | 80 | test("should handle number in operation", () => { 81 | const filter = { age: { in: [18, 25, 30] } }; 82 | expect(constructFilterString(filter)).toBe("age IN (18, 25, 30)"); 83 | }); 84 | 85 | test("should handle number notIn operation", () => { 86 | const filter = { age: { notIn: [18, 25] } }; 87 | expect(constructFilterString(filter)).toBe("age NOT IN (18, 25)"); 88 | }); 89 | 90 | test("should handle negative numbers", () => { 91 | const filter = { temperature: { equals: -10 } }; 92 | expect(constructFilterString(filter)).toBe("temperature = -10"); 93 | }); 94 | 95 | test("should handle floating point numbers", () => { 96 | const filter = { score: { equals: 98.5 } }; 97 | expect(constructFilterString(filter)).toBe("score = 98.5"); 98 | }); 99 | 100 | test("should handle zero values", () => { 101 | const filter = { count: { equals: 0 } }; 102 | expect(constructFilterString(filter)).toBe("count = 0"); 103 | }); 104 | }); 105 | 106 | // Boolean operations tests 107 | describe("Boolean operations", () => { 108 | test("should handle boolean equals true operation", () => { 109 | const filter = { active: { equals: true } }; 110 | expect(constructFilterString(filter)).toBe("active = true"); 111 | }); 112 | 113 | test("should handle boolean equals false operation", () => { 114 | const filter = { active: { equals: false } }; 115 | expect(constructFilterString(filter)).toBe("active = false"); 116 | }); 117 | 118 | test("should handle boolean notEquals operation", () => { 119 | const filter = { active: { notEquals: true } }; 120 | expect(constructFilterString(filter)).toBe("active != true"); 121 | }); 122 | 123 | test("should handle boolean in operation", () => { 124 | const filter = { active: { in: [true, false] } } as any; 125 | expect(constructFilterString(filter)).toBe("active IN (true, false)"); 126 | }); 127 | 128 | test("should handle boolean notIn operation", () => { 129 | const filter = { active: { notIn: [true] } } as any; 130 | expect(constructFilterString(filter)).toBe("active NOT IN (true)"); 131 | }); 132 | }); 133 | 134 | // Array operations tests 135 | describe("Array operations", () => { 136 | test("should handle array contains operation", () => { 137 | const filter = { tags: { contains: "javascript" } } as any; 138 | expect(constructFilterString(filter)).toBe("tags CONTAINS 'javascript'"); 139 | }); 140 | 141 | test("should handle array notContains operation", () => { 142 | const filter = { tags: { notContains: "python" } } as any; 143 | expect(constructFilterString(filter)).toBe("tags NOT CONTAINS 'python'"); 144 | }); 145 | 146 | test("should handle array contains with number", () => { 147 | const filter = { scores: { contains: 95 } } as any; 148 | expect(constructFilterString(filter)).toBe("scores CONTAINS 95"); 149 | }); 150 | 151 | test("should handle array notContains with boolean", () => { 152 | const filter = { flags: { notContains: true } } as any; 153 | expect(constructFilterString(filter)).toBe("flags NOT CONTAINS true"); 154 | }); 155 | }); 156 | 157 | // OR operations tests 158 | describe("OR operations", () => { 159 | test("should handle simple OR operation", () => { 160 | const filter = { 161 | OR: [{ name: { equals: "John" } }, { name: { equals: "Jane" } }], 162 | } as any; 163 | expect(constructFilterString(filter)).toBe("(name = 'John' OR name = 'Jane')"); 164 | }); 165 | 166 | test("should handle OR with different fields", () => { 167 | const filter = { 168 | OR: [{ name: { equals: "John" } }, { age: { equals: 25 } }], 169 | } as any; 170 | expect(constructFilterString(filter)).toBe("(name = 'John' OR age = 25)"); 171 | }); 172 | 173 | test("should handle OR with multiple conditions", () => { 174 | const filter = { 175 | OR: [ 176 | { name: { equals: "John" } }, 177 | { name: { equals: "Jane" } }, 178 | { age: { greaterThan: 30 } }, 179 | ], 180 | } as any; 181 | expect(constructFilterString(filter)).toBe("(name = 'John' OR name = 'Jane' OR age > 30)"); 182 | }); 183 | }); 184 | 185 | // AND operations tests 186 | describe("AND operations", () => { 187 | test("should handle simple AND operation", () => { 188 | const filter = { 189 | AND: [{ name: { equals: "John" } }, { age: { greaterThan: 18 } }], 190 | } as any; 191 | expect(constructFilterString(filter)).toBe("(name = 'John' AND age > 18)"); 192 | }); 193 | 194 | test("should handle AND with multiple conditions", () => { 195 | const filter = { 196 | AND: [ 197 | { name: { equals: "John" } }, 198 | { age: { greaterThan: 18 } }, 199 | { active: { equals: true } }, 200 | ], 201 | } as any; 202 | expect(constructFilterString(filter)).toBe("(name = 'John' AND age > 18 AND active = true)"); 203 | }); 204 | 205 | test("should handle AND with same field different operations", () => { 206 | const filter = { 207 | AND: [{ age: { greaterThan: 18 } }, { age: { lessThan: 65 } }], 208 | } as any; 209 | expect(constructFilterString(filter)).toBe("(age > 18 AND age < 65)"); 210 | }); 211 | }); 212 | 213 | // Nested operations tests 214 | describe("Nested operations", () => { 215 | test("should handle nested OR within AND", () => { 216 | const filter = { 217 | AND: [ 218 | { active: { equals: true } }, 219 | { 220 | OR: [{ name: { equals: "John" } }, { name: { equals: "Jane" } }], 221 | }, 222 | ], 223 | } as any; 224 | expect(constructFilterString(filter)).toBe( 225 | "(active = true AND (name = 'John' OR name = 'Jane'))" 226 | ); 227 | }); 228 | 229 | test("should handle nested AND within OR", () => { 230 | const filter = { 231 | OR: [ 232 | { 233 | AND: [{ name: { equals: "John" } }, { age: { greaterThan: 18 } }], 234 | }, 235 | { active: { equals: false } }, 236 | ], 237 | } as any; 238 | expect(constructFilterString(filter)).toBe( 239 | "((name = 'John' AND age > 18) OR active = false)" 240 | ); 241 | }); 242 | 243 | test("should handle deeply nested operations", () => { 244 | const filter = { 245 | AND: [ 246 | { category: { equals: "tech" } }, 247 | { 248 | OR: [ 249 | { 250 | AND: [{ name: { equals: "John" } }, { age: { greaterThan: 25 } }], 251 | }, 252 | { priority: { equals: "high" } }, 253 | ], 254 | }, 255 | ], 256 | } as any; 257 | expect(constructFilterString(filter)).toBe( 258 | "(category = 'tech' AND ((name = 'John' AND age > 25) OR priority = 'high'))" 259 | ); 260 | }); 261 | }); 262 | 263 | // Edge cases and error handling 264 | describe("Edge cases and error handling", () => { 265 | test("should handle single item in array for IN operation", () => { 266 | const filter = { name: { in: ["John"] } } as any; 267 | expect(constructFilterString(filter)).toBe("name IN ('John')"); 268 | }); 269 | 270 | test("should handle empty array for IN operation", () => { 271 | const filter = { name: { in: [] } } as any; 272 | expect(constructFilterString(filter)).toBe("name IN ()"); 273 | }); 274 | 275 | test("should handle mixed types in array for IN operation", () => { 276 | const filter = { values: { in: ["string", 123, true] } } as any; 277 | expect(constructFilterString(filter)).toBe("values IN ('string', 123, true)"); 278 | }); 279 | 280 | test("should throw error for invalid operation", () => { 281 | const filter = { name: { invalidOp: "value" } } as any; 282 | expect(() => constructFilterString(filter)).toThrow(); 283 | }); 284 | 285 | test("should throw error for undefined value", () => { 286 | const filter = { name: { equals: undefined } } as any; 287 | expect(() => constructFilterString(filter)).toThrow(); 288 | }); 289 | 290 | test("should throw error for missing operation", () => { 291 | const filter = { name: {} } as any; 292 | expect(() => constructFilterString(filter)).toThrow(); 293 | }); 294 | }); 295 | 296 | // Complex real-world scenarios 297 | describe("Complex real-world scenarios", () => { 298 | test("should handle user search with multiple filters", () => { 299 | const filter = { 300 | AND: [ 301 | { status: { equals: "active" } }, 302 | { age: { greaterThanOrEquals: 18 } }, 303 | { 304 | OR: [ 305 | { department: { in: ["engineering", "design"] } }, 306 | { role: { glob: "*manager*" } }, 307 | ], 308 | }, 309 | ], 310 | } as any; 311 | expect(constructFilterString(filter)).toBe( 312 | "(status = 'active' AND age >= 18 AND (department IN ('engineering', 'design') OR role GLOB '*manager*'))" 313 | ); 314 | }); 315 | 316 | test("should handle product filtering scenario", () => { 317 | const filter = { 318 | AND: [ 319 | { category: { equals: "electronics" } }, 320 | { price: { lessThan: 1000 } }, 321 | { inStock: { equals: true } }, 322 | { 323 | OR: [{ brand: { in: ["Apple", "Samsung"] } }, { rating: { greaterThan: 4.5 } }], 324 | }, 325 | ], 326 | } as any; 327 | expect(constructFilterString(filter)).toBe( 328 | "(category = 'electronics' AND price < 1000 AND inStock = true AND (brand IN ('Apple', 'Samsung') OR rating > 4.5))" 329 | ); 330 | }); 331 | 332 | test("should handle content filtering with tags", () => { 333 | const filter = { 334 | OR: [ 335 | { tags: { contains: "javascript" } }, 336 | { 337 | AND: [ 338 | { author: { equals: "John Doe" } }, 339 | { publishDate: { greaterThan: "2023-01-01" } }, 340 | ], 341 | }, 342 | { featured: { equals: true } }, 343 | ], 344 | } as any; 345 | expect(constructFilterString(filter)).toBe( 346 | "(tags CONTAINS 'javascript' OR (author = 'John Doe' AND publishDate > '2023-01-01') OR featured = true)" 347 | ); 348 | }); 349 | 350 | test("should handle exclusion filters", () => { 351 | const filter = { 352 | AND: [ 353 | { status: { notEquals: "deleted" } }, 354 | { category: { notIn: ["spam", "test"] } }, 355 | { content: { notGlob: "*temp*" } }, 356 | { tags: { notContains: "deprecated" } }, 357 | ], 358 | } as any; 359 | expect(constructFilterString(filter)).toBe( 360 | "(status != 'deleted' AND category NOT IN ('spam', 'test') AND content NOT GLOB '*temp*' AND tags NOT CONTAINS 'deprecated')" 361 | ); 362 | }); 363 | 364 | test("should handle date range filtering", () => { 365 | const filter = { 366 | AND: [ 367 | { startDate: { greaterThanOrEquals: "2023-01-01" } }, 368 | { endDate: { lessThanOrEquals: "2023-12-31" } }, 369 | { status: { equals: "published" } }, 370 | ], 371 | } as any; 372 | expect(constructFilterString(filter)).toBe( 373 | "(startDate >= '2023-01-01' AND endDate <= '2023-12-31' AND status = 'published')" 374 | ); 375 | }); 376 | }); 377 | }); 378 | -------------------------------------------------------------------------------- /examples/nextjs-movies/pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '9.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | importers: 8 | 9 | .: 10 | dependencies: 11 | '@tanstack/react-query': 12 | specifier: ^5.53.2 13 | version: 5.53.2(react@18.3.1) 14 | '@upstash/search': 15 | specifier: ^0.1.2 16 | version: 0.1.2 17 | clsx: 18 | specifier: ^2.1.1 19 | version: 2.1.1 20 | dotenv: 21 | specifier: ^16.5.0 22 | version: 16.5.0 23 | next: 24 | specifier: 14.2.35 25 | version: 14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1) 26 | react: 27 | specifier: ^18 28 | version: 18.3.1 29 | react-dom: 30 | specifier: ^18 31 | version: 18.3.1(react@18.3.1) 32 | zod: 33 | specifier: ^3.23.8 34 | version: 3.23.8 35 | devDependencies: 36 | '@types/node': 37 | specifier: ^20 38 | version: 20.16.3 39 | '@types/react': 40 | specifier: ^18 41 | version: 18.3.5 42 | '@types/react-dom': 43 | specifier: ^18 44 | version: 18.3.0 45 | postcss: 46 | specifier: ^8 47 | version: 8.4.44 48 | prettier: 49 | specifier: ^3.3.3 50 | version: 3.3.3 51 | tailwind-merge: 52 | specifier: ^2.5.2 53 | version: 2.5.2 54 | tailwindcss: 55 | specifier: ^3.4.10 56 | version: 3.4.10 57 | tsx: 58 | specifier: ^4.20.3 59 | version: 4.20.3 60 | typescript: 61 | specifier: ^5 62 | version: 5.5.4 63 | 64 | packages: 65 | 66 | '@alloc/quick-lru@5.2.0': 67 | resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} 68 | engines: {node: '>=10'} 69 | 70 | '@esbuild/aix-ppc64@0.25.5': 71 | resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} 72 | engines: {node: '>=18'} 73 | cpu: [ppc64] 74 | os: [aix] 75 | 76 | '@esbuild/android-arm64@0.25.5': 77 | resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} 78 | engines: {node: '>=18'} 79 | cpu: [arm64] 80 | os: [android] 81 | 82 | '@esbuild/android-arm@0.25.5': 83 | resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} 84 | engines: {node: '>=18'} 85 | cpu: [arm] 86 | os: [android] 87 | 88 | '@esbuild/android-x64@0.25.5': 89 | resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} 90 | engines: {node: '>=18'} 91 | cpu: [x64] 92 | os: [android] 93 | 94 | '@esbuild/darwin-arm64@0.25.5': 95 | resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} 96 | engines: {node: '>=18'} 97 | cpu: [arm64] 98 | os: [darwin] 99 | 100 | '@esbuild/darwin-x64@0.25.5': 101 | resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} 102 | engines: {node: '>=18'} 103 | cpu: [x64] 104 | os: [darwin] 105 | 106 | '@esbuild/freebsd-arm64@0.25.5': 107 | resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} 108 | engines: {node: '>=18'} 109 | cpu: [arm64] 110 | os: [freebsd] 111 | 112 | '@esbuild/freebsd-x64@0.25.5': 113 | resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} 114 | engines: {node: '>=18'} 115 | cpu: [x64] 116 | os: [freebsd] 117 | 118 | '@esbuild/linux-arm64@0.25.5': 119 | resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} 120 | engines: {node: '>=18'} 121 | cpu: [arm64] 122 | os: [linux] 123 | 124 | '@esbuild/linux-arm@0.25.5': 125 | resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} 126 | engines: {node: '>=18'} 127 | cpu: [arm] 128 | os: [linux] 129 | 130 | '@esbuild/linux-ia32@0.25.5': 131 | resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} 132 | engines: {node: '>=18'} 133 | cpu: [ia32] 134 | os: [linux] 135 | 136 | '@esbuild/linux-loong64@0.25.5': 137 | resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} 138 | engines: {node: '>=18'} 139 | cpu: [loong64] 140 | os: [linux] 141 | 142 | '@esbuild/linux-mips64el@0.25.5': 143 | resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} 144 | engines: {node: '>=18'} 145 | cpu: [mips64el] 146 | os: [linux] 147 | 148 | '@esbuild/linux-ppc64@0.25.5': 149 | resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} 150 | engines: {node: '>=18'} 151 | cpu: [ppc64] 152 | os: [linux] 153 | 154 | '@esbuild/linux-riscv64@0.25.5': 155 | resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} 156 | engines: {node: '>=18'} 157 | cpu: [riscv64] 158 | os: [linux] 159 | 160 | '@esbuild/linux-s390x@0.25.5': 161 | resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} 162 | engines: {node: '>=18'} 163 | cpu: [s390x] 164 | os: [linux] 165 | 166 | '@esbuild/linux-x64@0.25.5': 167 | resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} 168 | engines: {node: '>=18'} 169 | cpu: [x64] 170 | os: [linux] 171 | 172 | '@esbuild/netbsd-arm64@0.25.5': 173 | resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} 174 | engines: {node: '>=18'} 175 | cpu: [arm64] 176 | os: [netbsd] 177 | 178 | '@esbuild/netbsd-x64@0.25.5': 179 | resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} 180 | engines: {node: '>=18'} 181 | cpu: [x64] 182 | os: [netbsd] 183 | 184 | '@esbuild/openbsd-arm64@0.25.5': 185 | resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} 186 | engines: {node: '>=18'} 187 | cpu: [arm64] 188 | os: [openbsd] 189 | 190 | '@esbuild/openbsd-x64@0.25.5': 191 | resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} 192 | engines: {node: '>=18'} 193 | cpu: [x64] 194 | os: [openbsd] 195 | 196 | '@esbuild/sunos-x64@0.25.5': 197 | resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} 198 | engines: {node: '>=18'} 199 | cpu: [x64] 200 | os: [sunos] 201 | 202 | '@esbuild/win32-arm64@0.25.5': 203 | resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} 204 | engines: {node: '>=18'} 205 | cpu: [arm64] 206 | os: [win32] 207 | 208 | '@esbuild/win32-ia32@0.25.5': 209 | resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} 210 | engines: {node: '>=18'} 211 | cpu: [ia32] 212 | os: [win32] 213 | 214 | '@esbuild/win32-x64@0.25.5': 215 | resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} 216 | engines: {node: '>=18'} 217 | cpu: [x64] 218 | os: [win32] 219 | 220 | '@isaacs/cliui@8.0.2': 221 | resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} 222 | engines: {node: '>=12'} 223 | 224 | '@jridgewell/gen-mapping@0.3.5': 225 | resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} 226 | engines: {node: '>=6.0.0'} 227 | 228 | '@jridgewell/resolve-uri@3.1.2': 229 | resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} 230 | engines: {node: '>=6.0.0'} 231 | 232 | '@jridgewell/set-array@1.2.1': 233 | resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} 234 | engines: {node: '>=6.0.0'} 235 | 236 | '@jridgewell/sourcemap-codec@1.5.0': 237 | resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} 238 | 239 | '@jridgewell/trace-mapping@0.3.25': 240 | resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} 241 | 242 | '@next/env@14.2.35': 243 | resolution: {integrity: sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==} 244 | 245 | '@next/swc-darwin-arm64@14.2.33': 246 | resolution: {integrity: sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==} 247 | engines: {node: '>= 10'} 248 | cpu: [arm64] 249 | os: [darwin] 250 | 251 | '@next/swc-darwin-x64@14.2.33': 252 | resolution: {integrity: sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==} 253 | engines: {node: '>= 10'} 254 | cpu: [x64] 255 | os: [darwin] 256 | 257 | '@next/swc-linux-arm64-gnu@14.2.33': 258 | resolution: {integrity: sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==} 259 | engines: {node: '>= 10'} 260 | cpu: [arm64] 261 | os: [linux] 262 | 263 | '@next/swc-linux-arm64-musl@14.2.33': 264 | resolution: {integrity: sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==} 265 | engines: {node: '>= 10'} 266 | cpu: [arm64] 267 | os: [linux] 268 | 269 | '@next/swc-linux-x64-gnu@14.2.33': 270 | resolution: {integrity: sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==} 271 | engines: {node: '>= 10'} 272 | cpu: [x64] 273 | os: [linux] 274 | 275 | '@next/swc-linux-x64-musl@14.2.33': 276 | resolution: {integrity: sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==} 277 | engines: {node: '>= 10'} 278 | cpu: [x64] 279 | os: [linux] 280 | 281 | '@next/swc-win32-arm64-msvc@14.2.33': 282 | resolution: {integrity: sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==} 283 | engines: {node: '>= 10'} 284 | cpu: [arm64] 285 | os: [win32] 286 | 287 | '@next/swc-win32-ia32-msvc@14.2.33': 288 | resolution: {integrity: sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==} 289 | engines: {node: '>= 10'} 290 | cpu: [ia32] 291 | os: [win32] 292 | 293 | '@next/swc-win32-x64-msvc@14.2.33': 294 | resolution: {integrity: sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==} 295 | engines: {node: '>= 10'} 296 | cpu: [x64] 297 | os: [win32] 298 | 299 | '@nodelib/fs.scandir@2.1.5': 300 | resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} 301 | engines: {node: '>= 8'} 302 | 303 | '@nodelib/fs.stat@2.0.5': 304 | resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 305 | engines: {node: '>= 8'} 306 | 307 | '@nodelib/fs.walk@1.2.8': 308 | resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} 309 | engines: {node: '>= 8'} 310 | 311 | '@pkgjs/parseargs@0.11.0': 312 | resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} 313 | engines: {node: '>=14'} 314 | 315 | '@swc/counter@0.1.3': 316 | resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} 317 | 318 | '@swc/helpers@0.5.5': 319 | resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} 320 | 321 | '@tanstack/query-core@5.53.2': 322 | resolution: {integrity: sha512-gCsABpRrYfLsmwcQ0JCE5I3LOQ9KYrDDSnseUDP3T7ukV8E7+lhlHDJS4Gegt1TSZCsxKhc1J5A7TkF5ePjDUQ==} 323 | 324 | '@tanstack/react-query@5.53.2': 325 | resolution: {integrity: sha512-ZxG/rspElkfqg2LElnNtsNgPtiCZ4Wl2XY43bATQqPvNgyrhzbCFzCjDwSQy9fJhSiDVALSlxYS8YOIiToqQmg==} 326 | peerDependencies: 327 | react: ^18 || ^19 328 | 329 | '@types/node@20.16.3': 330 | resolution: {integrity: sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==} 331 | 332 | '@types/prop-types@15.7.12': 333 | resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} 334 | 335 | '@types/react-dom@18.3.0': 336 | resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} 337 | 338 | '@types/react@18.3.5': 339 | resolution: {integrity: sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==} 340 | 341 | '@upstash/search@0.1.2': 342 | resolution: {integrity: sha512-NbeL61SlxmyghBVYxH0D8yLJ8kmJrx1I6MNK2ZIKB36ZGZC5F8Ck/g9MJA0aHhtA3MQM1LbvcaZghzZX4SNZ2w==} 343 | 344 | '@upstash/vector@1.2.1': 345 | resolution: {integrity: sha512-xKA9qTgbnPsxym/ymgwmaJbJHSTA6b5B1SNyTqJerV8322xn6eJM+p3xZPxKJYDPfSea7RgVxsTkhE9I/mOaOw==} 346 | 347 | ansi-regex@5.0.1: 348 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 349 | engines: {node: '>=8'} 350 | 351 | ansi-regex@6.0.1: 352 | resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} 353 | engines: {node: '>=12'} 354 | 355 | ansi-styles@4.3.0: 356 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 357 | engines: {node: '>=8'} 358 | 359 | ansi-styles@6.2.1: 360 | resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} 361 | engines: {node: '>=12'} 362 | 363 | any-promise@1.3.0: 364 | resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} 365 | 366 | anymatch@3.1.3: 367 | resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} 368 | engines: {node: '>= 8'} 369 | 370 | arg@5.0.2: 371 | resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} 372 | 373 | balanced-match@1.0.2: 374 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 375 | 376 | binary-extensions@2.3.0: 377 | resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} 378 | engines: {node: '>=8'} 379 | 380 | brace-expansion@2.0.1: 381 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} 382 | 383 | braces@3.0.3: 384 | resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} 385 | engines: {node: '>=8'} 386 | 387 | busboy@1.6.0: 388 | resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} 389 | engines: {node: '>=10.16.0'} 390 | 391 | camelcase-css@2.0.1: 392 | resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} 393 | engines: {node: '>= 6'} 394 | 395 | caniuse-lite@1.0.30001655: 396 | resolution: {integrity: sha512-jRGVy3iSGO5Uutn2owlb5gR6qsGngTw9ZTb4ali9f3glshcNmJ2noam4Mo9zia5P9Dk3jNNydy7vQjuE5dQmfg==} 397 | 398 | chokidar@3.6.0: 399 | resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} 400 | engines: {node: '>= 8.10.0'} 401 | 402 | client-only@0.0.1: 403 | resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} 404 | 405 | clsx@2.1.1: 406 | resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} 407 | engines: {node: '>=6'} 408 | 409 | color-convert@2.0.1: 410 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 411 | engines: {node: '>=7.0.0'} 412 | 413 | color-name@1.1.4: 414 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 415 | 416 | commander@4.1.1: 417 | resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} 418 | engines: {node: '>= 6'} 419 | 420 | cross-spawn@7.0.3: 421 | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 422 | engines: {node: '>= 8'} 423 | 424 | cssesc@3.0.0: 425 | resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} 426 | engines: {node: '>=4'} 427 | hasBin: true 428 | 429 | csstype@3.1.3: 430 | resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} 431 | 432 | didyoumean@1.2.2: 433 | resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} 434 | 435 | dlv@1.1.3: 436 | resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} 437 | 438 | dotenv@16.5.0: 439 | resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} 440 | engines: {node: '>=12'} 441 | 442 | eastasianwidth@0.2.0: 443 | resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} 444 | 445 | emoji-regex@8.0.0: 446 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 447 | 448 | emoji-regex@9.2.2: 449 | resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} 450 | 451 | esbuild@0.25.5: 452 | resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} 453 | engines: {node: '>=18'} 454 | hasBin: true 455 | 456 | fast-glob@3.3.2: 457 | resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} 458 | engines: {node: '>=8.6.0'} 459 | 460 | fastq@1.17.1: 461 | resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} 462 | 463 | fill-range@7.1.1: 464 | resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} 465 | engines: {node: '>=8'} 466 | 467 | foreground-child@3.3.0: 468 | resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==} 469 | engines: {node: '>=14'} 470 | 471 | fsevents@2.3.3: 472 | resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} 473 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 474 | os: [darwin] 475 | 476 | function-bind@1.1.2: 477 | resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} 478 | 479 | get-tsconfig@4.10.1: 480 | resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} 481 | 482 | glob-parent@5.1.2: 483 | resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} 484 | engines: {node: '>= 6'} 485 | 486 | glob-parent@6.0.2: 487 | resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} 488 | engines: {node: '>=10.13.0'} 489 | 490 | glob@10.4.5: 491 | resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} 492 | hasBin: true 493 | 494 | graceful-fs@4.2.11: 495 | resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} 496 | 497 | hasown@2.0.2: 498 | resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} 499 | engines: {node: '>= 0.4'} 500 | 501 | is-binary-path@2.1.0: 502 | resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} 503 | engines: {node: '>=8'} 504 | 505 | is-core-module@2.15.1: 506 | resolution: {integrity: sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==} 507 | engines: {node: '>= 0.4'} 508 | 509 | is-extglob@2.1.1: 510 | resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 511 | engines: {node: '>=0.10.0'} 512 | 513 | is-fullwidth-code-point@3.0.0: 514 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 515 | engines: {node: '>=8'} 516 | 517 | is-glob@4.0.3: 518 | resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} 519 | engines: {node: '>=0.10.0'} 520 | 521 | is-number@7.0.0: 522 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 523 | engines: {node: '>=0.12.0'} 524 | 525 | isexe@2.0.0: 526 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} 527 | 528 | jackspeak@3.4.3: 529 | resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} 530 | 531 | jiti@1.21.6: 532 | resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} 533 | hasBin: true 534 | 535 | js-tokens@4.0.0: 536 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 537 | 538 | lilconfig@2.1.0: 539 | resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} 540 | engines: {node: '>=10'} 541 | 542 | lilconfig@3.1.2: 543 | resolution: {integrity: sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==} 544 | engines: {node: '>=14'} 545 | 546 | lines-and-columns@1.2.4: 547 | resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} 548 | 549 | loose-envify@1.4.0: 550 | resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} 551 | hasBin: true 552 | 553 | lru-cache@10.4.3: 554 | resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} 555 | 556 | merge2@1.4.1: 557 | resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 558 | engines: {node: '>= 8'} 559 | 560 | micromatch@4.0.8: 561 | resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} 562 | engines: {node: '>=8.6'} 563 | 564 | minimatch@9.0.5: 565 | resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} 566 | engines: {node: '>=16 || 14 >=14.17'} 567 | 568 | minipass@7.1.2: 569 | resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} 570 | engines: {node: '>=16 || 14 >=14.17'} 571 | 572 | mz@2.7.0: 573 | resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} 574 | 575 | nanoid@3.3.7: 576 | resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} 577 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 578 | hasBin: true 579 | 580 | next@14.2.35: 581 | resolution: {integrity: sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==} 582 | engines: {node: '>=18.17.0'} 583 | hasBin: true 584 | peerDependencies: 585 | '@opentelemetry/api': ^1.1.0 586 | '@playwright/test': ^1.41.2 587 | react: ^18.2.0 588 | react-dom: ^18.2.0 589 | sass: ^1.3.0 590 | peerDependenciesMeta: 591 | '@opentelemetry/api': 592 | optional: true 593 | '@playwright/test': 594 | optional: true 595 | sass: 596 | optional: true 597 | 598 | normalize-path@3.0.0: 599 | resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} 600 | engines: {node: '>=0.10.0'} 601 | 602 | object-assign@4.1.1: 603 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 604 | engines: {node: '>=0.10.0'} 605 | 606 | object-hash@3.0.0: 607 | resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} 608 | engines: {node: '>= 6'} 609 | 610 | package-json-from-dist@1.0.0: 611 | resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} 612 | 613 | path-key@3.1.1: 614 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 615 | engines: {node: '>=8'} 616 | 617 | path-parse@1.0.7: 618 | resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} 619 | 620 | path-scurry@1.11.1: 621 | resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} 622 | engines: {node: '>=16 || 14 >=14.18'} 623 | 624 | picocolors@1.0.1: 625 | resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==} 626 | 627 | picomatch@2.3.1: 628 | resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} 629 | engines: {node: '>=8.6'} 630 | 631 | pify@2.3.0: 632 | resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} 633 | engines: {node: '>=0.10.0'} 634 | 635 | pirates@4.0.6: 636 | resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} 637 | engines: {node: '>= 6'} 638 | 639 | postcss-import@15.1.0: 640 | resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} 641 | engines: {node: '>=14.0.0'} 642 | peerDependencies: 643 | postcss: ^8.0.0 644 | 645 | postcss-js@4.0.1: 646 | resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} 647 | engines: {node: ^12 || ^14 || >= 16} 648 | peerDependencies: 649 | postcss: ^8.4.21 650 | 651 | postcss-load-config@4.0.2: 652 | resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} 653 | engines: {node: '>= 14'} 654 | peerDependencies: 655 | postcss: '>=8.0.9' 656 | ts-node: '>=9.0.0' 657 | peerDependenciesMeta: 658 | postcss: 659 | optional: true 660 | ts-node: 661 | optional: true 662 | 663 | postcss-nested@6.2.0: 664 | resolution: {integrity: sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==} 665 | engines: {node: '>=12.0'} 666 | peerDependencies: 667 | postcss: ^8.2.14 668 | 669 | postcss-selector-parser@6.1.2: 670 | resolution: {integrity: sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==} 671 | engines: {node: '>=4'} 672 | 673 | postcss-value-parser@4.2.0: 674 | resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} 675 | 676 | postcss@8.4.31: 677 | resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} 678 | engines: {node: ^10 || ^12 || >=14} 679 | 680 | postcss@8.4.44: 681 | resolution: {integrity: sha512-Aweb9unOEpQ3ezu4Q00DPvvM2ZTUitJdNKeP/+uQgr1IBIqu574IaZoURId7BKtWMREwzKa9OgzPzezWGPWFQw==} 682 | engines: {node: ^10 || ^12 || >=14} 683 | 684 | prettier@3.3.3: 685 | resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} 686 | engines: {node: '>=14'} 687 | hasBin: true 688 | 689 | queue-microtask@1.2.3: 690 | resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 691 | 692 | react-dom@18.3.1: 693 | resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} 694 | peerDependencies: 695 | react: ^18.3.1 696 | 697 | react@18.3.1: 698 | resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} 699 | engines: {node: '>=0.10.0'} 700 | 701 | read-cache@1.0.0: 702 | resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} 703 | 704 | readdirp@3.6.0: 705 | resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} 706 | engines: {node: '>=8.10.0'} 707 | 708 | resolve-pkg-maps@1.0.0: 709 | resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} 710 | 711 | resolve@1.22.8: 712 | resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} 713 | hasBin: true 714 | 715 | reusify@1.0.4: 716 | resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 717 | engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 718 | 719 | run-parallel@1.2.0: 720 | resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 721 | 722 | scheduler@0.23.2: 723 | resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} 724 | 725 | shebang-command@2.0.0: 726 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 727 | engines: {node: '>=8'} 728 | 729 | shebang-regex@3.0.0: 730 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 731 | engines: {node: '>=8'} 732 | 733 | signal-exit@4.1.0: 734 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} 735 | engines: {node: '>=14'} 736 | 737 | source-map-js@1.2.0: 738 | resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} 739 | engines: {node: '>=0.10.0'} 740 | 741 | streamsearch@1.1.0: 742 | resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} 743 | engines: {node: '>=10.0.0'} 744 | 745 | string-width@4.2.3: 746 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 747 | engines: {node: '>=8'} 748 | 749 | string-width@5.1.2: 750 | resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} 751 | engines: {node: '>=12'} 752 | 753 | strip-ansi@6.0.1: 754 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 755 | engines: {node: '>=8'} 756 | 757 | strip-ansi@7.1.0: 758 | resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} 759 | engines: {node: '>=12'} 760 | 761 | styled-jsx@5.1.1: 762 | resolution: {integrity: sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==} 763 | engines: {node: '>= 12.0.0'} 764 | peerDependencies: 765 | '@babel/core': '*' 766 | babel-plugin-macros: '*' 767 | react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' 768 | peerDependenciesMeta: 769 | '@babel/core': 770 | optional: true 771 | babel-plugin-macros: 772 | optional: true 773 | 774 | sucrase@3.35.0: 775 | resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} 776 | engines: {node: '>=16 || 14 >=14.17'} 777 | hasBin: true 778 | 779 | supports-preserve-symlinks-flag@1.0.0: 780 | resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} 781 | engines: {node: '>= 0.4'} 782 | 783 | tailwind-merge@2.5.2: 784 | resolution: {integrity: sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==} 785 | 786 | tailwindcss@3.4.10: 787 | resolution: {integrity: sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==} 788 | engines: {node: '>=14.0.0'} 789 | hasBin: true 790 | 791 | thenify-all@1.6.0: 792 | resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} 793 | engines: {node: '>=0.8'} 794 | 795 | thenify@3.3.1: 796 | resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} 797 | 798 | to-regex-range@5.0.1: 799 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 800 | engines: {node: '>=8.0'} 801 | 802 | ts-interface-checker@0.1.13: 803 | resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} 804 | 805 | tslib@2.7.0: 806 | resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} 807 | 808 | tsx@4.20.3: 809 | resolution: {integrity: sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==} 810 | engines: {node: '>=18.0.0'} 811 | hasBin: true 812 | 813 | typescript@5.5.4: 814 | resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} 815 | engines: {node: '>=14.17'} 816 | hasBin: true 817 | 818 | undici-types@6.19.8: 819 | resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} 820 | 821 | util-deprecate@1.0.2: 822 | resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} 823 | 824 | which@2.0.2: 825 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 826 | engines: {node: '>= 8'} 827 | hasBin: true 828 | 829 | wrap-ansi@7.0.0: 830 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 831 | engines: {node: '>=10'} 832 | 833 | wrap-ansi@8.1.0: 834 | resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} 835 | engines: {node: '>=12'} 836 | 837 | yaml@2.5.0: 838 | resolution: {integrity: sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==} 839 | engines: {node: '>= 14'} 840 | hasBin: true 841 | 842 | zod@3.23.8: 843 | resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} 844 | 845 | snapshots: 846 | 847 | '@alloc/quick-lru@5.2.0': {} 848 | 849 | '@esbuild/aix-ppc64@0.25.5': 850 | optional: true 851 | 852 | '@esbuild/android-arm64@0.25.5': 853 | optional: true 854 | 855 | '@esbuild/android-arm@0.25.5': 856 | optional: true 857 | 858 | '@esbuild/android-x64@0.25.5': 859 | optional: true 860 | 861 | '@esbuild/darwin-arm64@0.25.5': 862 | optional: true 863 | 864 | '@esbuild/darwin-x64@0.25.5': 865 | optional: true 866 | 867 | '@esbuild/freebsd-arm64@0.25.5': 868 | optional: true 869 | 870 | '@esbuild/freebsd-x64@0.25.5': 871 | optional: true 872 | 873 | '@esbuild/linux-arm64@0.25.5': 874 | optional: true 875 | 876 | '@esbuild/linux-arm@0.25.5': 877 | optional: true 878 | 879 | '@esbuild/linux-ia32@0.25.5': 880 | optional: true 881 | 882 | '@esbuild/linux-loong64@0.25.5': 883 | optional: true 884 | 885 | '@esbuild/linux-mips64el@0.25.5': 886 | optional: true 887 | 888 | '@esbuild/linux-ppc64@0.25.5': 889 | optional: true 890 | 891 | '@esbuild/linux-riscv64@0.25.5': 892 | optional: true 893 | 894 | '@esbuild/linux-s390x@0.25.5': 895 | optional: true 896 | 897 | '@esbuild/linux-x64@0.25.5': 898 | optional: true 899 | 900 | '@esbuild/netbsd-arm64@0.25.5': 901 | optional: true 902 | 903 | '@esbuild/netbsd-x64@0.25.5': 904 | optional: true 905 | 906 | '@esbuild/openbsd-arm64@0.25.5': 907 | optional: true 908 | 909 | '@esbuild/openbsd-x64@0.25.5': 910 | optional: true 911 | 912 | '@esbuild/sunos-x64@0.25.5': 913 | optional: true 914 | 915 | '@esbuild/win32-arm64@0.25.5': 916 | optional: true 917 | 918 | '@esbuild/win32-ia32@0.25.5': 919 | optional: true 920 | 921 | '@esbuild/win32-x64@0.25.5': 922 | optional: true 923 | 924 | '@isaacs/cliui@8.0.2': 925 | dependencies: 926 | string-width: 5.1.2 927 | string-width-cjs: string-width@4.2.3 928 | strip-ansi: 7.1.0 929 | strip-ansi-cjs: strip-ansi@6.0.1 930 | wrap-ansi: 8.1.0 931 | wrap-ansi-cjs: wrap-ansi@7.0.0 932 | 933 | '@jridgewell/gen-mapping@0.3.5': 934 | dependencies: 935 | '@jridgewell/set-array': 1.2.1 936 | '@jridgewell/sourcemap-codec': 1.5.0 937 | '@jridgewell/trace-mapping': 0.3.25 938 | 939 | '@jridgewell/resolve-uri@3.1.2': {} 940 | 941 | '@jridgewell/set-array@1.2.1': {} 942 | 943 | '@jridgewell/sourcemap-codec@1.5.0': {} 944 | 945 | '@jridgewell/trace-mapping@0.3.25': 946 | dependencies: 947 | '@jridgewell/resolve-uri': 3.1.2 948 | '@jridgewell/sourcemap-codec': 1.5.0 949 | 950 | '@next/env@14.2.35': {} 951 | 952 | '@next/swc-darwin-arm64@14.2.33': 953 | optional: true 954 | 955 | '@next/swc-darwin-x64@14.2.33': 956 | optional: true 957 | 958 | '@next/swc-linux-arm64-gnu@14.2.33': 959 | optional: true 960 | 961 | '@next/swc-linux-arm64-musl@14.2.33': 962 | optional: true 963 | 964 | '@next/swc-linux-x64-gnu@14.2.33': 965 | optional: true 966 | 967 | '@next/swc-linux-x64-musl@14.2.33': 968 | optional: true 969 | 970 | '@next/swc-win32-arm64-msvc@14.2.33': 971 | optional: true 972 | 973 | '@next/swc-win32-ia32-msvc@14.2.33': 974 | optional: true 975 | 976 | '@next/swc-win32-x64-msvc@14.2.33': 977 | optional: true 978 | 979 | '@nodelib/fs.scandir@2.1.5': 980 | dependencies: 981 | '@nodelib/fs.stat': 2.0.5 982 | run-parallel: 1.2.0 983 | 984 | '@nodelib/fs.stat@2.0.5': {} 985 | 986 | '@nodelib/fs.walk@1.2.8': 987 | dependencies: 988 | '@nodelib/fs.scandir': 2.1.5 989 | fastq: 1.17.1 990 | 991 | '@pkgjs/parseargs@0.11.0': 992 | optional: true 993 | 994 | '@swc/counter@0.1.3': {} 995 | 996 | '@swc/helpers@0.5.5': 997 | dependencies: 998 | '@swc/counter': 0.1.3 999 | tslib: 2.7.0 1000 | 1001 | '@tanstack/query-core@5.53.2': {} 1002 | 1003 | '@tanstack/react-query@5.53.2(react@18.3.1)': 1004 | dependencies: 1005 | '@tanstack/query-core': 5.53.2 1006 | react: 18.3.1 1007 | 1008 | '@types/node@20.16.3': 1009 | dependencies: 1010 | undici-types: 6.19.8 1011 | 1012 | '@types/prop-types@15.7.12': {} 1013 | 1014 | '@types/react-dom@18.3.0': 1015 | dependencies: 1016 | '@types/react': 18.3.5 1017 | 1018 | '@types/react@18.3.5': 1019 | dependencies: 1020 | '@types/prop-types': 15.7.12 1021 | csstype: 3.1.3 1022 | 1023 | '@upstash/search@0.1.2': 1024 | dependencies: 1025 | '@upstash/vector': 1.2.1 1026 | 1027 | '@upstash/vector@1.2.1': {} 1028 | 1029 | ansi-regex@5.0.1: {} 1030 | 1031 | ansi-regex@6.0.1: {} 1032 | 1033 | ansi-styles@4.3.0: 1034 | dependencies: 1035 | color-convert: 2.0.1 1036 | 1037 | ansi-styles@6.2.1: {} 1038 | 1039 | any-promise@1.3.0: {} 1040 | 1041 | anymatch@3.1.3: 1042 | dependencies: 1043 | normalize-path: 3.0.0 1044 | picomatch: 2.3.1 1045 | 1046 | arg@5.0.2: {} 1047 | 1048 | balanced-match@1.0.2: {} 1049 | 1050 | binary-extensions@2.3.0: {} 1051 | 1052 | brace-expansion@2.0.1: 1053 | dependencies: 1054 | balanced-match: 1.0.2 1055 | 1056 | braces@3.0.3: 1057 | dependencies: 1058 | fill-range: 7.1.1 1059 | 1060 | busboy@1.6.0: 1061 | dependencies: 1062 | streamsearch: 1.1.0 1063 | 1064 | camelcase-css@2.0.1: {} 1065 | 1066 | caniuse-lite@1.0.30001655: {} 1067 | 1068 | chokidar@3.6.0: 1069 | dependencies: 1070 | anymatch: 3.1.3 1071 | braces: 3.0.3 1072 | glob-parent: 5.1.2 1073 | is-binary-path: 2.1.0 1074 | is-glob: 4.0.3 1075 | normalize-path: 3.0.0 1076 | readdirp: 3.6.0 1077 | optionalDependencies: 1078 | fsevents: 2.3.3 1079 | 1080 | client-only@0.0.1: {} 1081 | 1082 | clsx@2.1.1: {} 1083 | 1084 | color-convert@2.0.1: 1085 | dependencies: 1086 | color-name: 1.1.4 1087 | 1088 | color-name@1.1.4: {} 1089 | 1090 | commander@4.1.1: {} 1091 | 1092 | cross-spawn@7.0.3: 1093 | dependencies: 1094 | path-key: 3.1.1 1095 | shebang-command: 2.0.0 1096 | which: 2.0.2 1097 | 1098 | cssesc@3.0.0: {} 1099 | 1100 | csstype@3.1.3: {} 1101 | 1102 | didyoumean@1.2.2: {} 1103 | 1104 | dlv@1.1.3: {} 1105 | 1106 | dotenv@16.5.0: {} 1107 | 1108 | eastasianwidth@0.2.0: {} 1109 | 1110 | emoji-regex@8.0.0: {} 1111 | 1112 | emoji-regex@9.2.2: {} 1113 | 1114 | esbuild@0.25.5: 1115 | optionalDependencies: 1116 | '@esbuild/aix-ppc64': 0.25.5 1117 | '@esbuild/android-arm': 0.25.5 1118 | '@esbuild/android-arm64': 0.25.5 1119 | '@esbuild/android-x64': 0.25.5 1120 | '@esbuild/darwin-arm64': 0.25.5 1121 | '@esbuild/darwin-x64': 0.25.5 1122 | '@esbuild/freebsd-arm64': 0.25.5 1123 | '@esbuild/freebsd-x64': 0.25.5 1124 | '@esbuild/linux-arm': 0.25.5 1125 | '@esbuild/linux-arm64': 0.25.5 1126 | '@esbuild/linux-ia32': 0.25.5 1127 | '@esbuild/linux-loong64': 0.25.5 1128 | '@esbuild/linux-mips64el': 0.25.5 1129 | '@esbuild/linux-ppc64': 0.25.5 1130 | '@esbuild/linux-riscv64': 0.25.5 1131 | '@esbuild/linux-s390x': 0.25.5 1132 | '@esbuild/linux-x64': 0.25.5 1133 | '@esbuild/netbsd-arm64': 0.25.5 1134 | '@esbuild/netbsd-x64': 0.25.5 1135 | '@esbuild/openbsd-arm64': 0.25.5 1136 | '@esbuild/openbsd-x64': 0.25.5 1137 | '@esbuild/sunos-x64': 0.25.5 1138 | '@esbuild/win32-arm64': 0.25.5 1139 | '@esbuild/win32-ia32': 0.25.5 1140 | '@esbuild/win32-x64': 0.25.5 1141 | 1142 | fast-glob@3.3.2: 1143 | dependencies: 1144 | '@nodelib/fs.stat': 2.0.5 1145 | '@nodelib/fs.walk': 1.2.8 1146 | glob-parent: 5.1.2 1147 | merge2: 1.4.1 1148 | micromatch: 4.0.8 1149 | 1150 | fastq@1.17.1: 1151 | dependencies: 1152 | reusify: 1.0.4 1153 | 1154 | fill-range@7.1.1: 1155 | dependencies: 1156 | to-regex-range: 5.0.1 1157 | 1158 | foreground-child@3.3.0: 1159 | dependencies: 1160 | cross-spawn: 7.0.3 1161 | signal-exit: 4.1.0 1162 | 1163 | fsevents@2.3.3: 1164 | optional: true 1165 | 1166 | function-bind@1.1.2: {} 1167 | 1168 | get-tsconfig@4.10.1: 1169 | dependencies: 1170 | resolve-pkg-maps: 1.0.0 1171 | 1172 | glob-parent@5.1.2: 1173 | dependencies: 1174 | is-glob: 4.0.3 1175 | 1176 | glob-parent@6.0.2: 1177 | dependencies: 1178 | is-glob: 4.0.3 1179 | 1180 | glob@10.4.5: 1181 | dependencies: 1182 | foreground-child: 3.3.0 1183 | jackspeak: 3.4.3 1184 | minimatch: 9.0.5 1185 | minipass: 7.1.2 1186 | package-json-from-dist: 1.0.0 1187 | path-scurry: 1.11.1 1188 | 1189 | graceful-fs@4.2.11: {} 1190 | 1191 | hasown@2.0.2: 1192 | dependencies: 1193 | function-bind: 1.1.2 1194 | 1195 | is-binary-path@2.1.0: 1196 | dependencies: 1197 | binary-extensions: 2.3.0 1198 | 1199 | is-core-module@2.15.1: 1200 | dependencies: 1201 | hasown: 2.0.2 1202 | 1203 | is-extglob@2.1.1: {} 1204 | 1205 | is-fullwidth-code-point@3.0.0: {} 1206 | 1207 | is-glob@4.0.3: 1208 | dependencies: 1209 | is-extglob: 2.1.1 1210 | 1211 | is-number@7.0.0: {} 1212 | 1213 | isexe@2.0.0: {} 1214 | 1215 | jackspeak@3.4.3: 1216 | dependencies: 1217 | '@isaacs/cliui': 8.0.2 1218 | optionalDependencies: 1219 | '@pkgjs/parseargs': 0.11.0 1220 | 1221 | jiti@1.21.6: {} 1222 | 1223 | js-tokens@4.0.0: {} 1224 | 1225 | lilconfig@2.1.0: {} 1226 | 1227 | lilconfig@3.1.2: {} 1228 | 1229 | lines-and-columns@1.2.4: {} 1230 | 1231 | loose-envify@1.4.0: 1232 | dependencies: 1233 | js-tokens: 4.0.0 1234 | 1235 | lru-cache@10.4.3: {} 1236 | 1237 | merge2@1.4.1: {} 1238 | 1239 | micromatch@4.0.8: 1240 | dependencies: 1241 | braces: 3.0.3 1242 | picomatch: 2.3.1 1243 | 1244 | minimatch@9.0.5: 1245 | dependencies: 1246 | brace-expansion: 2.0.1 1247 | 1248 | minipass@7.1.2: {} 1249 | 1250 | mz@2.7.0: 1251 | dependencies: 1252 | any-promise: 1.3.0 1253 | object-assign: 4.1.1 1254 | thenify-all: 1.6.0 1255 | 1256 | nanoid@3.3.7: {} 1257 | 1258 | next@14.2.35(react-dom@18.3.1(react@18.3.1))(react@18.3.1): 1259 | dependencies: 1260 | '@next/env': 14.2.35 1261 | '@swc/helpers': 0.5.5 1262 | busboy: 1.6.0 1263 | caniuse-lite: 1.0.30001655 1264 | graceful-fs: 4.2.11 1265 | postcss: 8.4.31 1266 | react: 18.3.1 1267 | react-dom: 18.3.1(react@18.3.1) 1268 | styled-jsx: 5.1.1(react@18.3.1) 1269 | optionalDependencies: 1270 | '@next/swc-darwin-arm64': 14.2.33 1271 | '@next/swc-darwin-x64': 14.2.33 1272 | '@next/swc-linux-arm64-gnu': 14.2.33 1273 | '@next/swc-linux-arm64-musl': 14.2.33 1274 | '@next/swc-linux-x64-gnu': 14.2.33 1275 | '@next/swc-linux-x64-musl': 14.2.33 1276 | '@next/swc-win32-arm64-msvc': 14.2.33 1277 | '@next/swc-win32-ia32-msvc': 14.2.33 1278 | '@next/swc-win32-x64-msvc': 14.2.33 1279 | transitivePeerDependencies: 1280 | - '@babel/core' 1281 | - babel-plugin-macros 1282 | 1283 | normalize-path@3.0.0: {} 1284 | 1285 | object-assign@4.1.1: {} 1286 | 1287 | object-hash@3.0.0: {} 1288 | 1289 | package-json-from-dist@1.0.0: {} 1290 | 1291 | path-key@3.1.1: {} 1292 | 1293 | path-parse@1.0.7: {} 1294 | 1295 | path-scurry@1.11.1: 1296 | dependencies: 1297 | lru-cache: 10.4.3 1298 | minipass: 7.1.2 1299 | 1300 | picocolors@1.0.1: {} 1301 | 1302 | picomatch@2.3.1: {} 1303 | 1304 | pify@2.3.0: {} 1305 | 1306 | pirates@4.0.6: {} 1307 | 1308 | postcss-import@15.1.0(postcss@8.4.44): 1309 | dependencies: 1310 | postcss: 8.4.44 1311 | postcss-value-parser: 4.2.0 1312 | read-cache: 1.0.0 1313 | resolve: 1.22.8 1314 | 1315 | postcss-js@4.0.1(postcss@8.4.44): 1316 | dependencies: 1317 | camelcase-css: 2.0.1 1318 | postcss: 8.4.44 1319 | 1320 | postcss-load-config@4.0.2(postcss@8.4.44): 1321 | dependencies: 1322 | lilconfig: 3.1.2 1323 | yaml: 2.5.0 1324 | optionalDependencies: 1325 | postcss: 8.4.44 1326 | 1327 | postcss-nested@6.2.0(postcss@8.4.44): 1328 | dependencies: 1329 | postcss: 8.4.44 1330 | postcss-selector-parser: 6.1.2 1331 | 1332 | postcss-selector-parser@6.1.2: 1333 | dependencies: 1334 | cssesc: 3.0.0 1335 | util-deprecate: 1.0.2 1336 | 1337 | postcss-value-parser@4.2.0: {} 1338 | 1339 | postcss@8.4.31: 1340 | dependencies: 1341 | nanoid: 3.3.7 1342 | picocolors: 1.0.1 1343 | source-map-js: 1.2.0 1344 | 1345 | postcss@8.4.44: 1346 | dependencies: 1347 | nanoid: 3.3.7 1348 | picocolors: 1.0.1 1349 | source-map-js: 1.2.0 1350 | 1351 | prettier@3.3.3: {} 1352 | 1353 | queue-microtask@1.2.3: {} 1354 | 1355 | react-dom@18.3.1(react@18.3.1): 1356 | dependencies: 1357 | loose-envify: 1.4.0 1358 | react: 18.3.1 1359 | scheduler: 0.23.2 1360 | 1361 | react@18.3.1: 1362 | dependencies: 1363 | loose-envify: 1.4.0 1364 | 1365 | read-cache@1.0.0: 1366 | dependencies: 1367 | pify: 2.3.0 1368 | 1369 | readdirp@3.6.0: 1370 | dependencies: 1371 | picomatch: 2.3.1 1372 | 1373 | resolve-pkg-maps@1.0.0: {} 1374 | 1375 | resolve@1.22.8: 1376 | dependencies: 1377 | is-core-module: 2.15.1 1378 | path-parse: 1.0.7 1379 | supports-preserve-symlinks-flag: 1.0.0 1380 | 1381 | reusify@1.0.4: {} 1382 | 1383 | run-parallel@1.2.0: 1384 | dependencies: 1385 | queue-microtask: 1.2.3 1386 | 1387 | scheduler@0.23.2: 1388 | dependencies: 1389 | loose-envify: 1.4.0 1390 | 1391 | shebang-command@2.0.0: 1392 | dependencies: 1393 | shebang-regex: 3.0.0 1394 | 1395 | shebang-regex@3.0.0: {} 1396 | 1397 | signal-exit@4.1.0: {} 1398 | 1399 | source-map-js@1.2.0: {} 1400 | 1401 | streamsearch@1.1.0: {} 1402 | 1403 | string-width@4.2.3: 1404 | dependencies: 1405 | emoji-regex: 8.0.0 1406 | is-fullwidth-code-point: 3.0.0 1407 | strip-ansi: 6.0.1 1408 | 1409 | string-width@5.1.2: 1410 | dependencies: 1411 | eastasianwidth: 0.2.0 1412 | emoji-regex: 9.2.2 1413 | strip-ansi: 7.1.0 1414 | 1415 | strip-ansi@6.0.1: 1416 | dependencies: 1417 | ansi-regex: 5.0.1 1418 | 1419 | strip-ansi@7.1.0: 1420 | dependencies: 1421 | ansi-regex: 6.0.1 1422 | 1423 | styled-jsx@5.1.1(react@18.3.1): 1424 | dependencies: 1425 | client-only: 0.0.1 1426 | react: 18.3.1 1427 | 1428 | sucrase@3.35.0: 1429 | dependencies: 1430 | '@jridgewell/gen-mapping': 0.3.5 1431 | commander: 4.1.1 1432 | glob: 10.4.5 1433 | lines-and-columns: 1.2.4 1434 | mz: 2.7.0 1435 | pirates: 4.0.6 1436 | ts-interface-checker: 0.1.13 1437 | 1438 | supports-preserve-symlinks-flag@1.0.0: {} 1439 | 1440 | tailwind-merge@2.5.2: {} 1441 | 1442 | tailwindcss@3.4.10: 1443 | dependencies: 1444 | '@alloc/quick-lru': 5.2.0 1445 | arg: 5.0.2 1446 | chokidar: 3.6.0 1447 | didyoumean: 1.2.2 1448 | dlv: 1.1.3 1449 | fast-glob: 3.3.2 1450 | glob-parent: 6.0.2 1451 | is-glob: 4.0.3 1452 | jiti: 1.21.6 1453 | lilconfig: 2.1.0 1454 | micromatch: 4.0.8 1455 | normalize-path: 3.0.0 1456 | object-hash: 3.0.0 1457 | picocolors: 1.0.1 1458 | postcss: 8.4.44 1459 | postcss-import: 15.1.0(postcss@8.4.44) 1460 | postcss-js: 4.0.1(postcss@8.4.44) 1461 | postcss-load-config: 4.0.2(postcss@8.4.44) 1462 | postcss-nested: 6.2.0(postcss@8.4.44) 1463 | postcss-selector-parser: 6.1.2 1464 | resolve: 1.22.8 1465 | sucrase: 3.35.0 1466 | transitivePeerDependencies: 1467 | - ts-node 1468 | 1469 | thenify-all@1.6.0: 1470 | dependencies: 1471 | thenify: 3.3.1 1472 | 1473 | thenify@3.3.1: 1474 | dependencies: 1475 | any-promise: 1.3.0 1476 | 1477 | to-regex-range@5.0.1: 1478 | dependencies: 1479 | is-number: 7.0.0 1480 | 1481 | ts-interface-checker@0.1.13: {} 1482 | 1483 | tslib@2.7.0: {} 1484 | 1485 | tsx@4.20.3: 1486 | dependencies: 1487 | esbuild: 0.25.5 1488 | get-tsconfig: 4.10.1 1489 | optionalDependencies: 1490 | fsevents: 2.3.3 1491 | 1492 | typescript@5.5.4: {} 1493 | 1494 | undici-types@6.19.8: {} 1495 | 1496 | util-deprecate@1.0.2: {} 1497 | 1498 | which@2.0.2: 1499 | dependencies: 1500 | isexe: 2.0.0 1501 | 1502 | wrap-ansi@7.0.0: 1503 | dependencies: 1504 | ansi-styles: 4.3.0 1505 | string-width: 4.2.3 1506 | strip-ansi: 6.0.1 1507 | 1508 | wrap-ansi@8.1.0: 1509 | dependencies: 1510 | ansi-styles: 6.2.1 1511 | string-width: 5.1.2 1512 | strip-ansi: 7.1.0 1513 | 1514 | yaml@2.5.0: {} 1515 | 1516 | zod@3.23.8: {} 1517 | --------------------------------------------------------------------------------