├── .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 |
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 | [](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 | [](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 | [](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   
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 |
40 |
41 | Search movies by title, genre, or description...
42 |
43 | {
46 | onChangeQuery("an epic space adventure");
47 | setTimeout(() => onSubmit(), 100);
48 | }}
49 | >
50 | an epic space adventure
51 |
52 |
53 |
54 |
55 |
56 | Find movies by plot, characters, or themes...
57 |
58 | {
61 | onChangeQuery("a crime saga about a powerful mafia family");
62 | setTimeout(() => onSubmit(), 100);
63 | }}
64 | >
65 | a crime saga about a powerful mafia family
66 |
67 |
68 |
69 |
70 |
71 | Type a movie’s storyline, genre, or cast...
72 |
73 | {
76 | onChangeQuery("a sci-fi film where reality is questioned by a computer hacker");
77 | setTimeout(() => onSubmit(), 100);
78 | }}
79 | >
80 | a sci-fi film where reality is questioned by a computer hacker
81 |
82 |
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 |
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 |
160 | All Content
161 | Blog Posts
162 |
163 | Changelog Entries
164 |
165 |
166 |
167 |
168 |
169 | }
175 | className="bg-black hover:bg-gray-800 border-black hover:border-gray-800"
176 | >
177 | Search
178 |
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 |
--------------------------------------------------------------------------------