Compare commits
11 Commits
9f9bc03d03
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6799586f4 | ||
|
|
68abe3ce79 | ||
|
|
3c475ab5d6 | ||
|
|
c7f9270ef2 | ||
|
|
904b0f783e | ||
|
|
3641e86da5 | ||
|
|
1255639058 | ||
|
|
25f614a730 | ||
|
|
0646390d52 | ||
|
|
d02102977a | ||
|
|
f9494c88e2 |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
packages/*/dist/
|
||||
48
.github/release-drafter-config.yml
vendored
Normal file
48
.github/release-drafter-config.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
||||
name-template: "$RESOLVED_VERSION 🌈"
|
||||
tag-template: "$RESOLVED_VERSION"
|
||||
categories:
|
||||
- title: "🚀 Features"
|
||||
labels:
|
||||
- "feature"
|
||||
- "enhancement"
|
||||
- title: "🐛 Bug Fixes"
|
||||
labels:
|
||||
- "fix"
|
||||
- "bugfix"
|
||||
- "bug"
|
||||
- title: "🧰 Maintenance"
|
||||
label: "chore"
|
||||
change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
|
||||
change-title-escapes: '\<*_&' # You can add # and @ to disable mentions, and add ` to disable code blocks.
|
||||
version-resolver:
|
||||
major:
|
||||
labels:
|
||||
- "major"
|
||||
minor:
|
||||
labels:
|
||||
- "minor"
|
||||
patch:
|
||||
labels:
|
||||
- "patch"
|
||||
default: patch
|
||||
autolabeler:
|
||||
- label: "chore"
|
||||
files:
|
||||
- "*.md"
|
||||
branch:
|
||||
- '/docs{0,1}\/.+/'
|
||||
- label: "bug"
|
||||
branch:
|
||||
- '/fix\/.+/'
|
||||
title:
|
||||
- "/fix/i"
|
||||
- label: "enhancement"
|
||||
branch:
|
||||
- '/feature\/.+/'
|
||||
- '/feat\/.+/'
|
||||
title:
|
||||
- "/feat:.+/"
|
||||
template: |
|
||||
## Changes
|
||||
|
||||
$CHANGES
|
||||
21
.github/workflows/auto-labeler.yaml
auto-labeler.yaml
auto-labeler.yaml
auto-labeler.yaml
vendored
Normal file
21
.github/workflows/auto-labeler.yaml
auto-labeler.yaml
auto-labeler.yaml
auto-labeler.yaml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Auto Labeler
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, reopened, synchronize]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
auto-labeler:
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v6
|
||||
with:
|
||||
config-name: release-drafter-config.yml
|
||||
disable-releaser: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
55
.github/workflows/job-build.yaml
vendored
Normal file
55
.github/workflows/job-build.yaml
vendored
Normal file
@@ -0,0 +1,55 @@
|
||||
name: Build
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: "${{ env.NODE_VERSION }}"
|
||||
registry-url: "${{ env.NODE_REGISTRY }}"
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
run_install: false
|
||||
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
|
||||
- uses: actions/cache@v4
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Run tests
|
||||
run: pnpm test
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: lib
|
||||
retention-days: 5
|
||||
path: |
|
||||
packages/*/dist
|
||||
extensions/*/dist
|
||||
server/*/dist
|
||||
package.json
|
||||
README.md
|
||||
18
.github/workflows/job-draft-release.yaml
vendored
Normal file
18
.github/workflows/job-draft-release.yaml
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
name: Draft release
|
||||
on:
|
||||
workflow_call:
|
||||
jobs:
|
||||
draft-release:
|
||||
name: Update release drafter
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
environment: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v6
|
||||
with:
|
||||
config-name: release-drafter-config.yml
|
||||
publish: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
115
.github/workflows/pipeline-default.yaml
vendored
Normal file
115
.github/workflows/pipeline-default.yaml
vendored
Normal file
@@ -0,0 +1,115 @@
|
||||
name: Build and release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
|
||||
env:
|
||||
environment: test
|
||||
release_channel: latest
|
||||
DO_NOT_TRACK: "1"
|
||||
NODE_VERSION: "23.x"
|
||||
NODE_REGISTRY: "https://registry.npmjs.org"
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
DOCKER_REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
PNPM_VERSION: 10.6.0
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
packages: read
|
||||
pull-requests: write
|
||||
id-token: write
|
||||
actions: read
|
||||
security-events: write
|
||||
jobs:
|
||||
build:
|
||||
uses: ./.github/workflows/job-build.yaml
|
||||
name: Build
|
||||
|
||||
update-release-draft:
|
||||
needs: build
|
||||
if: github.ref == 'refs/heads/main'
|
||||
uses: ./.github/workflows/job-draft-release.yaml
|
||||
|
||||
release:
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
attestations: write
|
||||
id-token: write
|
||||
pages: write
|
||||
name: Release
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
needs: update-release-draft
|
||||
environment: release
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
# - uses: actions/setup-node@v4
|
||||
# with:
|
||||
# node-version: '${{ env.NODE_VERSION }}'
|
||||
# registry-url: '${{ env.NODE_REGISTRY }}'
|
||||
#
|
||||
# - uses: pnpm/action-setup@v4
|
||||
# name: Install pnpm
|
||||
# with:
|
||||
# version: ${{ env.PNPM_VERSION }}
|
||||
# run_install: false
|
||||
#
|
||||
# - name: Install dependencies
|
||||
# run: pnpm install
|
||||
# env:
|
||||
# NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
#
|
||||
# - uses: actions/download-artifact@v4
|
||||
# with:
|
||||
# name: lib
|
||||
# path: ./
|
||||
#
|
||||
# - name: Publish to npm
|
||||
# run: |
|
||||
# git config user.name "Github Actions Bot"
|
||||
# git config user.email "<>"
|
||||
# node ./scripts/set-version.mjs $(git describe --tag --abbrev=0)
|
||||
# pnpm publish -r --no-git-checks --access public
|
||||
# env:
|
||||
# NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1
|
||||
with:
|
||||
registry: ${{ env.DOCKER_REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||
|
||||
- name: Build and push Docker image
|
||||
id: push
|
||||
uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
|
||||
with:
|
||||
context: .
|
||||
file: ./packages/server/Dockerfile
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
# - name: Generate artifact attestation
|
||||
# uses: actions/attest-build-provenance@v2
|
||||
# with:
|
||||
# subject-name: ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME}}
|
||||
# subject-digest: ${{ steps.push.outputs.digest }}
|
||||
# push-to-registry: true
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
/node_modules
|
||||
/node_modules/
|
||||
/packages/*/dist/
|
||||
.turbo/
|
||||
/.env
|
||||
/coverage/
|
||||
30
.u8.json
30
.u8.json
@@ -36,6 +36,36 @@
|
||||
"packageVersion": "1.0.0",
|
||||
"packageName": "server"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-12-10T07:50:20.652Z",
|
||||
"template": "pkg",
|
||||
"values": {
|
||||
"monoRepo": true,
|
||||
"packagePrefix": "@morten-olsen/stash-",
|
||||
"packageVersion": "1.0.0",
|
||||
"packageName": "query-dsl"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-12-10T08:07:44.756Z",
|
||||
"template": "pkg",
|
||||
"values": {
|
||||
"monoRepo": true,
|
||||
"packagePrefix": "@morten-olsen/stash-",
|
||||
"packageVersion": "1.0.0",
|
||||
"packageName": "runtime"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-12-10T09:46:52.130Z",
|
||||
"template": "pkg",
|
||||
"values": {
|
||||
"monoRepo": true,
|
||||
"packagePrefix": "@morten-olsen/stash-",
|
||||
"packageVersion": "1.0.0",
|
||||
"packageName": "client"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
9
docker-compose.yaml
Normal file
9
docker-compose.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
name: stash
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./packages/server/Dockerfile
|
||||
read_only: true
|
||||
ports:
|
||||
- 3400:3400
|
||||
@@ -46,6 +46,6 @@ export default tseslint.config(
|
||||
},
|
||||
...compat.extends('plugin:prettier/recommended'),
|
||||
{
|
||||
ignores: ['**/node_modules/', '**/dist/', '**/.turbo/', '**/generated/'],
|
||||
ignores: ['**/node_modules/', '**/dist/', '**/.turbo/', '**/__generated__/'],
|
||||
},
|
||||
);
|
||||
|
||||
4
packages/client/.gitignore
vendored
Normal file
4
packages/client/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/node_modules/
|
||||
/dist/
|
||||
/coverage/
|
||||
/.env
|
||||
31
packages/client/package.json
Normal file
31
packages/client/package.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"type": "module",
|
||||
"main": "dist/exports.js",
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"test:unit": "vitest --run --passWithNoTests",
|
||||
"test": "pnpm run \"/^test:/\"",
|
||||
"generate:client": "openapi-typescript http://localhost:3400/docs/openapi.json -o src/__generated__/schema.ts"
|
||||
},
|
||||
"packageManager": "pnpm@10.6.0",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
".": "./dist/exports.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@morten-olsen/stash-configs": "workspace:*",
|
||||
"@morten-olsen/stash-tests": "workspace:*",
|
||||
"@types/node": "24.10.2",
|
||||
"@vitest/coverage-v8": "4.0.15",
|
||||
"openapi-typescript": "^7.10.1",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "4.0.15"
|
||||
},
|
||||
"name": "@morten-olsen/stash-client",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"openapi-fetch": "^0.15.0"
|
||||
}
|
||||
}
|
||||
515
packages/client/src/__generated__/schema.ts
generated
Normal file
515
packages/client/src/__generated__/schema.ts
generated
Normal file
@@ -0,0 +1,515 @@
|
||||
/**
|
||||
* This file was auto-generated by openapi-typescript.
|
||||
* Do not make direct changes to the file.
|
||||
*/
|
||||
|
||||
export interface paths {
|
||||
"/system/ready": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Get system ready state */
|
||||
get: operations["GET/system/ready"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/documents": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Upsert document */
|
||||
post: operations["POST/documents"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/document-filters": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Find documents */
|
||||
post: operations["POST/documents-filters"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/document-chunk-filters": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/** Find document chunks */
|
||||
post: operations["POST/documents-chunk-filters"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
schemas: never;
|
||||
responses: never;
|
||||
parameters: never;
|
||||
requestBodies: never;
|
||||
headers: never;
|
||||
pathItems: never;
|
||||
}
|
||||
export type $defs = Record<string, never>;
|
||||
export interface operations {
|
||||
"GET/system/ready": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Default Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
/** @enum {string} */
|
||||
status: "ok";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
"POST/documents": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: {
|
||||
content: {
|
||||
"application/json": {
|
||||
id?: string | null;
|
||||
owner?: string | null;
|
||||
contentType?: string | null;
|
||||
content?: string | null;
|
||||
source?: string | null;
|
||||
sourceId?: string | null;
|
||||
type?: string;
|
||||
typeVersion?: number | null;
|
||||
searchText?: string | null;
|
||||
metadata?: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Default Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
/** @enum {string} */
|
||||
action: "inserted" | "updated" | "skipped";
|
||||
id: string;
|
||||
document: {
|
||||
id: string;
|
||||
owner: string | null;
|
||||
/** Format: date-time */
|
||||
createdAt: string;
|
||||
/** Format: date-time */
|
||||
updatedAt: string;
|
||||
/** Format: date-time */
|
||||
deletedAt: string | null;
|
||||
contentType: string | null;
|
||||
content: string | null;
|
||||
source: string | null;
|
||||
sourceId: string | null;
|
||||
type: string;
|
||||
typeVersion: number | null;
|
||||
searchText: string | null;
|
||||
metadata: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
"POST/documents-filters": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": {
|
||||
/** @default 0 */
|
||||
offset?: number;
|
||||
/** @default 20 */
|
||||
limit?: number;
|
||||
condition: (({
|
||||
/** @enum {string} */
|
||||
type: "text";
|
||||
tableName?: string;
|
||||
field: string[];
|
||||
conditions: {
|
||||
equal?: string | null;
|
||||
notEqual?: string;
|
||||
like?: string;
|
||||
notLike?: string;
|
||||
in?: string[];
|
||||
notIn?: string[];
|
||||
};
|
||||
} | {
|
||||
/** @enum {string} */
|
||||
type: "number";
|
||||
tableName?: string;
|
||||
field: string[];
|
||||
conditions: {
|
||||
equals?: number | null;
|
||||
notEquals?: number | null;
|
||||
greaterThan?: number;
|
||||
greaterThanOrEqual?: number;
|
||||
lessThan?: number;
|
||||
lessThanOrEqual?: number;
|
||||
in?: number[];
|
||||
notIn?: number[];
|
||||
};
|
||||
}) | {
|
||||
/** @enum {string} */
|
||||
type: "operator";
|
||||
/** @enum {string} */
|
||||
operator: "and" | "or";
|
||||
conditions: (({
|
||||
/** @enum {string} */
|
||||
type: "text";
|
||||
tableName?: string;
|
||||
field: string[];
|
||||
conditions: {
|
||||
equal?: string | null;
|
||||
notEqual?: string;
|
||||
like?: string;
|
||||
notLike?: string;
|
||||
in?: string[];
|
||||
notIn?: string[];
|
||||
};
|
||||
} | {
|
||||
/** @enum {string} */
|
||||
type: "number";
|
||||
tableName?: string;
|
||||
field: string[];
|
||||
conditions: {
|
||||
equals?: number | null;
|
||||
notEquals?: number | null;
|
||||
greaterThan?: number;
|
||||
greaterThanOrEqual?: number;
|
||||
lessThan?: number;
|
||||
lessThanOrEqual?: number;
|
||||
in?: number[];
|
||||
notIn?: number[];
|
||||
};
|
||||
}) | {
|
||||
/** @enum {string} */
|
||||
type: "operator";
|
||||
/** @enum {string} */
|
||||
operator: "and" | "or";
|
||||
conditions: (({
|
||||
/** @enum {string} */
|
||||
type: "text";
|
||||
tableName?: string;
|
||||
field: string[];
|
||||
conditions: {
|
||||
equal?: string | null;
|
||||
notEqual?: string;
|
||||
like?: string;
|
||||
notLike?: string;
|
||||
in?: string[];
|
||||
notIn?: string[];
|
||||
};
|
||||
} | {
|
||||
/** @enum {string} */
|
||||
type: "number";
|
||||
tableName?: string;
|
||||
field: string[];
|
||||
conditions: {
|
||||
equals?: number | null;
|
||||
notEquals?: number | null;
|
||||
greaterThan?: number;
|
||||
greaterThanOrEqual?: number;
|
||||
lessThan?: number;
|
||||
lessThanOrEqual?: number;
|
||||
in?: number[];
|
||||
notIn?: number[];
|
||||
};
|
||||
}) | {
|
||||
/** @enum {string} */
|
||||
type: "operator";
|
||||
/** @enum {string} */
|
||||
operator: "and" | "or";
|
||||
conditions: ({
|
||||
/** @enum {string} */
|
||||
type: "text";
|
||||
tableName?: string;
|
||||
field: string[];
|
||||
conditions: {
|
||||
equal?: string | null;
|
||||
notEqual?: string;
|
||||
like?: string;
|
||||
notLike?: string;
|
||||
in?: string[];
|
||||
notIn?: string[];
|
||||
};
|
||||
} | {
|
||||
/** @enum {string} */
|
||||
type: "number";
|
||||
tableName?: string;
|
||||
field: string[];
|
||||
conditions: {
|
||||
equals?: number | null;
|
||||
notEquals?: number | null;
|
||||
greaterThan?: number;
|
||||
greaterThanOrEqual?: number;
|
||||
lessThan?: number;
|
||||
lessThanOrEqual?: number;
|
||||
in?: number[];
|
||||
notIn?: number[];
|
||||
};
|
||||
})[];
|
||||
})[];
|
||||
})[];
|
||||
}) | string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Default Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
items: {
|
||||
id: string;
|
||||
owner: string | null;
|
||||
/** Format: date-time */
|
||||
createdAt: string;
|
||||
/** Format: date-time */
|
||||
updatedAt: string;
|
||||
/** Format: date-time */
|
||||
deletedAt: string | null;
|
||||
contentType: string | null;
|
||||
content: string | null;
|
||||
source: string | null;
|
||||
sourceId: string | null;
|
||||
type: string;
|
||||
typeVersion: number | null;
|
||||
searchText: string | null;
|
||||
metadata: unknown;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
"POST/documents-chunk-filters": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: {
|
||||
content: {
|
||||
"application/json": {
|
||||
/** @default 20 */
|
||||
limit?: number;
|
||||
/** @default 0 */
|
||||
offset?: number;
|
||||
semanticText?: string;
|
||||
conditions?: (({
|
||||
/** @enum {string} */
|
||||
type: "text";
|
||||
tableName?: string;
|
||||
field: string[];
|
||||
conditions: {
|
||||
equal?: string | null;
|
||||
notEqual?: string;
|
||||
like?: string;
|
||||
notLike?: string;
|
||||
in?: string[];
|
||||
notIn?: string[];
|
||||
};
|
||||
} | {
|
||||
/** @enum {string} */
|
||||
type: "number";
|
||||
tableName?: string;
|
||||
field: string[];
|
||||
conditions: {
|
||||
equals?: number | null;
|
||||
notEquals?: number | null;
|
||||
greaterThan?: number;
|
||||
greaterThanOrEqual?: number;
|
||||
lessThan?: number;
|
||||
lessThanOrEqual?: number;
|
||||
in?: number[];
|
||||
notIn?: number[];
|
||||
};
|
||||
}) | {
|
||||
/** @enum {string} */
|
||||
type: "operator";
|
||||
/** @enum {string} */
|
||||
operator: "and" | "or";
|
||||
conditions: (({
|
||||
/** @enum {string} */
|
||||
type: "text";
|
||||
tableName?: string;
|
||||
field: string[];
|
||||
conditions: {
|
||||
equal?: string | null;
|
||||
notEqual?: string;
|
||||
like?: string;
|
||||
notLike?: string;
|
||||
in?: string[];
|
||||
notIn?: string[];
|
||||
};
|
||||
} | {
|
||||
/** @enum {string} */
|
||||
type: "number";
|
||||
tableName?: string;
|
||||
field: string[];
|
||||
conditions: {
|
||||
equals?: number | null;
|
||||
notEquals?: number | null;
|
||||
greaterThan?: number;
|
||||
greaterThanOrEqual?: number;
|
||||
lessThan?: number;
|
||||
lessThanOrEqual?: number;
|
||||
in?: number[];
|
||||
notIn?: number[];
|
||||
};
|
||||
}) | {
|
||||
/** @enum {string} */
|
||||
type: "operator";
|
||||
/** @enum {string} */
|
||||
operator: "and" | "or";
|
||||
conditions: (({
|
||||
/** @enum {string} */
|
||||
type: "text";
|
||||
tableName?: string;
|
||||
field: string[];
|
||||
conditions: {
|
||||
equal?: string | null;
|
||||
notEqual?: string;
|
||||
like?: string;
|
||||
notLike?: string;
|
||||
in?: string[];
|
||||
notIn?: string[];
|
||||
};
|
||||
} | {
|
||||
/** @enum {string} */
|
||||
type: "number";
|
||||
tableName?: string;
|
||||
field: string[];
|
||||
conditions: {
|
||||
equals?: number | null;
|
||||
notEquals?: number | null;
|
||||
greaterThan?: number;
|
||||
greaterThanOrEqual?: number;
|
||||
lessThan?: number;
|
||||
lessThanOrEqual?: number;
|
||||
in?: number[];
|
||||
notIn?: number[];
|
||||
};
|
||||
}) | {
|
||||
/** @enum {string} */
|
||||
type: "operator";
|
||||
/** @enum {string} */
|
||||
operator: "and" | "or";
|
||||
conditions: ({
|
||||
/** @enum {string} */
|
||||
type: "text";
|
||||
tableName?: string;
|
||||
field: string[];
|
||||
conditions: {
|
||||
equal?: string | null;
|
||||
notEqual?: string;
|
||||
like?: string;
|
||||
notLike?: string;
|
||||
in?: string[];
|
||||
notIn?: string[];
|
||||
};
|
||||
} | {
|
||||
/** @enum {string} */
|
||||
type: "number";
|
||||
tableName?: string;
|
||||
field: string[];
|
||||
conditions: {
|
||||
equals?: number | null;
|
||||
notEquals?: number | null;
|
||||
greaterThan?: number;
|
||||
greaterThanOrEqual?: number;
|
||||
lessThan?: number;
|
||||
lessThanOrEqual?: number;
|
||||
in?: number[];
|
||||
notIn?: number[];
|
||||
};
|
||||
})[];
|
||||
})[];
|
||||
})[];
|
||||
}) | string;
|
||||
};
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Default Response */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": {
|
||||
items: {
|
||||
id: string;
|
||||
owner: string;
|
||||
content: string;
|
||||
metadata: unknown;
|
||||
distance?: number;
|
||||
}[];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
19
packages/client/src/exports.ts
Normal file
19
packages/client/src/exports.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import createApiClient from 'openapi-fetch';
|
||||
|
||||
import type { paths } from './__generated__/schema.js';
|
||||
|
||||
type CreateStashClientOptions = {
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
type StashClient = ReturnType<typeof createApiClient<paths>>;
|
||||
|
||||
const createStashClient = (options: CreateStashClientOptions): StashClient => {
|
||||
const client = createApiClient<paths>({
|
||||
baseUrl: options.baseUrl,
|
||||
});
|
||||
return client;
|
||||
};
|
||||
|
||||
export type { StashClient };
|
||||
export { createStashClient };
|
||||
9
packages/client/tsconfig.json
Normal file
9
packages/client/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"extends": "@morten-olsen/stash-configs/tsconfig.json"
|
||||
}
|
||||
12
packages/client/vitest.config.ts
Normal file
12
packages/client/vitest.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { getAliases } from '@morten-olsen/stash-tests/vitest';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default defineConfig(async () => {
|
||||
const aliases = await getAliases();
|
||||
return {
|
||||
resolve: {
|
||||
alias: aliases,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -10,11 +10,9 @@
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"allowImportingTsExtensions": true
|
||||
"erasableSyntaxOnly": true
|
||||
}
|
||||
}
|
||||
|
||||
4
packages/query-dsl/.gitignore
vendored
Normal file
4
packages/query-dsl/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/node_modules/
|
||||
/dist/
|
||||
/coverage/
|
||||
/.env
|
||||
626
packages/query-dsl/docs/diagram/index.html
Normal file
626
packages/query-dsl/docs/diagram/index.html
Normal file
@@ -0,0 +1,626 @@
|
||||
|
||||
<!-- This is a generated file -->
|
||||
<!DOCTYPE html>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
background-color: hsl(30, 20%, 95%)
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<link rel='stylesheet' href='https://unpkg.com/chevrotain@11.0.3/diagrams/diagrams.css'>
|
||||
|
||||
<script src='https://unpkg.com/chevrotain@11.0.3/diagrams/vendor/railroad-diagrams.js'></script>
|
||||
<script src='https://unpkg.com/chevrotain@11.0.3/diagrams/src/diagrams_builder.js'></script>
|
||||
<script src='https://unpkg.com/chevrotain@11.0.3/diagrams/src/diagrams_behavior.js'></script>
|
||||
<script src='https://unpkg.com/chevrotain@11.0.3/diagrams/src/main.js'></script>
|
||||
|
||||
<div id="diagrams" align="center"></div>
|
||||
|
||||
<script>
|
||||
window.serializedGrammar = [
|
||||
{
|
||||
"type": "Rule",
|
||||
"name": "orExpression",
|
||||
"orgText": "",
|
||||
"definition": [
|
||||
{
|
||||
"type": "NonTerminal",
|
||||
"name": "andExpression",
|
||||
"idx": 0
|
||||
},
|
||||
{
|
||||
"type": "Repetition",
|
||||
"idx": 0,
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "Or",
|
||||
"label": "Or",
|
||||
"idx": 0,
|
||||
"pattern": "OR"
|
||||
},
|
||||
{
|
||||
"type": "NonTerminal",
|
||||
"name": "andExpression",
|
||||
"idx": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Rule",
|
||||
"name": "andExpression",
|
||||
"orgText": "",
|
||||
"definition": [
|
||||
{
|
||||
"type": "NonTerminal",
|
||||
"name": "primaryExpression",
|
||||
"idx": 0
|
||||
},
|
||||
{
|
||||
"type": "Repetition",
|
||||
"idx": 0,
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "And",
|
||||
"label": "And",
|
||||
"idx": 0,
|
||||
"pattern": "AND"
|
||||
},
|
||||
{
|
||||
"type": "NonTerminal",
|
||||
"name": "primaryExpression",
|
||||
"idx": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Rule",
|
||||
"name": "primaryExpression",
|
||||
"orgText": "",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Alternation",
|
||||
"idx": 0,
|
||||
"definition": [
|
||||
{
|
||||
"type": "Alternative",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "LParen",
|
||||
"label": "LParen",
|
||||
"idx": 0,
|
||||
"pattern": "\\("
|
||||
},
|
||||
{
|
||||
"type": "NonTerminal",
|
||||
"name": "orExpression",
|
||||
"idx": 0
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "RParen",
|
||||
"label": "RParen",
|
||||
"idx": 0,
|
||||
"pattern": "\\)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Alternative",
|
||||
"definition": [
|
||||
{
|
||||
"type": "NonTerminal",
|
||||
"name": "condition",
|
||||
"idx": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Rule",
|
||||
"name": "condition",
|
||||
"orgText": "",
|
||||
"definition": [
|
||||
{
|
||||
"type": "NonTerminal",
|
||||
"name": "fieldReference",
|
||||
"idx": 0
|
||||
},
|
||||
{
|
||||
"type": "Alternation",
|
||||
"idx": 0,
|
||||
"definition": [
|
||||
{
|
||||
"type": "Alternative",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "Is",
|
||||
"label": "Is",
|
||||
"idx": 0,
|
||||
"pattern": "IS"
|
||||
},
|
||||
{
|
||||
"type": "Option",
|
||||
"idx": 0,
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "Not",
|
||||
"label": "Not",
|
||||
"idx": 0,
|
||||
"pattern": "NOT"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "Null",
|
||||
"label": "Null",
|
||||
"idx": 0,
|
||||
"pattern": "NULL"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Alternative",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "Not",
|
||||
"label": "Not",
|
||||
"idx": 2,
|
||||
"pattern": "NOT"
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "In",
|
||||
"label": "In",
|
||||
"idx": 0,
|
||||
"pattern": "IN"
|
||||
},
|
||||
{
|
||||
"type": "NonTerminal",
|
||||
"name": "stringInList",
|
||||
"idx": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Alternative",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "Not",
|
||||
"label": "Not",
|
||||
"idx": 3,
|
||||
"pattern": "NOT"
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "In",
|
||||
"label": "In",
|
||||
"idx": 2,
|
||||
"pattern": "IN"
|
||||
},
|
||||
{
|
||||
"type": "NonTerminal",
|
||||
"name": "numberInList",
|
||||
"idx": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Alternative",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "Not",
|
||||
"label": "Not",
|
||||
"idx": 4,
|
||||
"pattern": "NOT"
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "Like",
|
||||
"label": "Like",
|
||||
"idx": 0,
|
||||
"pattern": "LIKE"
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "StringLiteral",
|
||||
"label": "StringLiteral",
|
||||
"idx": 0,
|
||||
"pattern": "'(?:''|[^'])*'"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Alternative",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "In",
|
||||
"label": "In",
|
||||
"idx": 3,
|
||||
"pattern": "IN"
|
||||
},
|
||||
{
|
||||
"type": "NonTerminal",
|
||||
"name": "stringInList",
|
||||
"idx": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Alternative",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "In",
|
||||
"label": "In",
|
||||
"idx": 4,
|
||||
"pattern": "IN"
|
||||
},
|
||||
{
|
||||
"type": "NonTerminal",
|
||||
"name": "numberInList",
|
||||
"idx": 2
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Alternative",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "Like",
|
||||
"label": "Like",
|
||||
"idx": 2,
|
||||
"pattern": "LIKE"
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "StringLiteral",
|
||||
"label": "StringLiteral",
|
||||
"idx": 2,
|
||||
"pattern": "'(?:''|[^'])*'"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Alternative",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "Equals",
|
||||
"label": "Equals",
|
||||
"idx": 0,
|
||||
"pattern": "="
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "StringLiteral",
|
||||
"label": "StringLiteral",
|
||||
"idx": 3,
|
||||
"pattern": "'(?:''|[^'])*'"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Alternative",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "Equals",
|
||||
"label": "Equals",
|
||||
"idx": 2,
|
||||
"pattern": "="
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "NumberLiteral",
|
||||
"label": "NumberLiteral",
|
||||
"idx": 0,
|
||||
"pattern": "-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Alternative",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "Equals",
|
||||
"label": "Equals",
|
||||
"idx": 3,
|
||||
"pattern": "="
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "Null",
|
||||
"label": "Null",
|
||||
"idx": 2,
|
||||
"pattern": "NULL"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Alternative",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "NotEquals",
|
||||
"label": "NotEquals",
|
||||
"idx": 0,
|
||||
"pattern": "!="
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "StringLiteral",
|
||||
"label": "StringLiteral",
|
||||
"idx": 4,
|
||||
"pattern": "'(?:''|[^'])*'"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Alternative",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "NotEquals",
|
||||
"label": "NotEquals",
|
||||
"idx": 2,
|
||||
"pattern": "!="
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "NumberLiteral",
|
||||
"label": "NumberLiteral",
|
||||
"idx": 2,
|
||||
"pattern": "-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Alternative",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "GreaterThan",
|
||||
"label": "GreaterThan",
|
||||
"idx": 0,
|
||||
"pattern": ">"
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "NumberLiteral",
|
||||
"label": "NumberLiteral",
|
||||
"idx": 3,
|
||||
"pattern": "-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Alternative",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "GreaterThanOrEqual",
|
||||
"label": "GreaterThanOrEqual",
|
||||
"idx": 0,
|
||||
"pattern": ">="
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "NumberLiteral",
|
||||
"label": "NumberLiteral",
|
||||
"idx": 4,
|
||||
"pattern": "-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Alternative",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "LessThan",
|
||||
"label": "LessThan",
|
||||
"idx": 0,
|
||||
"pattern": "<"
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "NumberLiteral",
|
||||
"label": "NumberLiteral",
|
||||
"idx": 5,
|
||||
"pattern": "-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Alternative",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "LessThanOrEqual",
|
||||
"label": "LessThanOrEqual",
|
||||
"idx": 0,
|
||||
"pattern": "<="
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "NumberLiteral",
|
||||
"label": "NumberLiteral",
|
||||
"idx": 6,
|
||||
"pattern": "-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Rule",
|
||||
"name": "fieldReference",
|
||||
"orgText": "",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "Identifier",
|
||||
"label": "Identifier",
|
||||
"idx": 0,
|
||||
"pattern": "[a-zA-Z_][a-zA-Z0-9_]*"
|
||||
},
|
||||
{
|
||||
"type": "Repetition",
|
||||
"idx": 0,
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "Dot",
|
||||
"label": "Dot",
|
||||
"idx": 0,
|
||||
"pattern": "\\."
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "Identifier",
|
||||
"label": "Identifier",
|
||||
"idx": 2,
|
||||
"pattern": "[a-zA-Z_][a-zA-Z0-9_]*"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Rule",
|
||||
"name": "stringInList",
|
||||
"orgText": "",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "LParen",
|
||||
"label": "LParen",
|
||||
"idx": 0,
|
||||
"pattern": "\\("
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "StringLiteral",
|
||||
"label": "StringLiteral",
|
||||
"idx": 0,
|
||||
"pattern": "'(?:''|[^'])*'"
|
||||
},
|
||||
{
|
||||
"type": "Repetition",
|
||||
"idx": 0,
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "Comma",
|
||||
"label": "Comma",
|
||||
"idx": 0,
|
||||
"pattern": ","
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "StringLiteral",
|
||||
"label": "StringLiteral",
|
||||
"idx": 2,
|
||||
"pattern": "'(?:''|[^'])*'"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "RParen",
|
||||
"label": "RParen",
|
||||
"idx": 0,
|
||||
"pattern": "\\)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Rule",
|
||||
"name": "numberInList",
|
||||
"orgText": "",
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "LParen",
|
||||
"label": "LParen",
|
||||
"idx": 2,
|
||||
"pattern": "\\("
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "NumberLiteral",
|
||||
"label": "NumberLiteral",
|
||||
"idx": 0,
|
||||
"pattern": "-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?"
|
||||
},
|
||||
{
|
||||
"type": "Repetition",
|
||||
"idx": 0,
|
||||
"definition": [
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "Comma",
|
||||
"label": "Comma",
|
||||
"idx": 2,
|
||||
"pattern": ","
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "NumberLiteral",
|
||||
"label": "NumberLiteral",
|
||||
"idx": 2,
|
||||
"pattern": "-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Terminal",
|
||||
"name": "RParen",
|
||||
"label": "RParen",
|
||||
"idx": 2,
|
||||
"pattern": "\\)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "Rule",
|
||||
"name": "query",
|
||||
"orgText": "",
|
||||
"definition": [
|
||||
{
|
||||
"type": "NonTerminal",
|
||||
"name": "orExpression",
|
||||
"idx": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<script>
|
||||
var diagramsDiv = document.getElementById("diagrams");
|
||||
main.drawDiagramsFromSerializedGrammar(serializedGrammar, diagramsDiv);
|
||||
</script>
|
||||
30
packages/query-dsl/package.json
Normal file
30
packages/query-dsl/package.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"type": "module",
|
||||
"main": "dist/exports.js",
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"test:unit": "vitest --run --passWithNoTests",
|
||||
"test": "pnpm run \"/^test:/\""
|
||||
},
|
||||
"packageManager": "pnpm@10.6.0",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
".": "./dist/exports.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@morten-olsen/stash-configs": "workspace:*",
|
||||
"@morten-olsen/stash-tests": "workspace:*",
|
||||
"@types/node": "24.10.2",
|
||||
"@vitest/coverage-v8": "4.0.15",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "4.0.15"
|
||||
},
|
||||
"name": "@morten-olsen/stash-query-dsl",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"chevrotain": "^11.0.3",
|
||||
"zod": "4.1.13"
|
||||
}
|
||||
}
|
||||
13
packages/query-dsl/scripts/generate-diagram.mjs
Normal file
13
packages/query-dsl/scripts/generate-diagram.mjs
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createSyntaxDiagramsCode } from 'chevrotain';
|
||||
|
||||
import { QueryParser } from '../dist/exports.js';
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
const rootPath = resolve('./docs/diagram');
|
||||
const parser = new QueryParser();
|
||||
const diagram = createSyntaxDiagramsCode(parser.getSerializedGastProductions());
|
||||
|
||||
await mkdir(rootPath, { recursive: true });
|
||||
|
||||
await writeFile(resolve(rootPath, 'index.html'), diagram);
|
||||
4
packages/query-dsl/src/exports.ts
Normal file
4
packages/query-dsl/src/exports.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './query-parser.schemas.js';
|
||||
export { QueryParser, queryParser } from './query-parser.js';
|
||||
export * from './utils.filter.js';
|
||||
export * from './query-parser.codec.js';
|
||||
20
packages/query-dsl/src/query-parser.codec.ts
Normal file
20
packages/query-dsl/src/query-parser.codec.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { queryFilterSchema } from './query-parser.schemas.js';
|
||||
import { queryParser } from './query-parser.js';
|
||||
|
||||
const queryStringSchema: typeof queryFilterSchema = z
|
||||
.codec(z.string(), queryFilterSchema, {
|
||||
encode: (filter) => {
|
||||
return queryParser.stringify(filter);
|
||||
},
|
||||
decode: (input) => {
|
||||
return queryParser.parse(input);
|
||||
},
|
||||
// eslint-disable-next-line
|
||||
}).meta({ id: 'QueryString', examples: ["metadata.foo = 'bar'"] }) as any;
|
||||
|
||||
// eslint-disable-next-line
|
||||
const querySchema: typeof queryFilterSchema = z.union([queryStringSchema, queryFilterSchema]) as any
|
||||
|
||||
export { querySchema };
|
||||
460
packages/query-dsl/src/query-parser.parser.ts
Normal file
460
packages/query-dsl/src/query-parser.parser.ts
Normal file
@@ -0,0 +1,460 @@
|
||||
import { createToken, Lexer, EmbeddedActionsParser } from 'chevrotain';
|
||||
|
||||
import { type QueryFilter, type QueryCondition, queryFilterSchema } from './query-parser.schemas.js';
|
||||
|
||||
// ----------------- Lexer -----------------
|
||||
|
||||
// Whitespace (skipped)
|
||||
const WhiteSpace = createToken({ name: 'WhiteSpace', pattern: /\s+/, group: Lexer.SKIPPED });
|
||||
|
||||
// Keywords (must be defined before Identifier to take precedence)
|
||||
const And = createToken({ name: 'And', pattern: /AND/i, longer_alt: undefined });
|
||||
const Or = createToken({ name: 'Or', pattern: /OR/i, longer_alt: undefined });
|
||||
const Like = createToken({ name: 'Like', pattern: /LIKE/i, longer_alt: undefined });
|
||||
const Not = createToken({ name: 'Not', pattern: /NOT/i, longer_alt: undefined });
|
||||
const In = createToken({ name: 'In', pattern: /IN/i, longer_alt: undefined });
|
||||
const Is = createToken({ name: 'Is', pattern: /IS/i, longer_alt: undefined });
|
||||
const Null = createToken({ name: 'Null', pattern: /NULL/i, longer_alt: undefined });
|
||||
|
||||
// Identifier (must come after keywords)
|
||||
const Identifier = createToken({ name: 'Identifier', pattern: /[a-zA-Z_][a-zA-Z0-9_]*/ });
|
||||
|
||||
// Set longer_alt for keywords to handle cases like "ANDROID" not matching "AND"
|
||||
And.LONGER_ALT = Identifier;
|
||||
Or.LONGER_ALT = Identifier;
|
||||
Like.LONGER_ALT = Identifier;
|
||||
Not.LONGER_ALT = Identifier;
|
||||
In.LONGER_ALT = Identifier;
|
||||
Is.LONGER_ALT = Identifier;
|
||||
Null.LONGER_ALT = Identifier;
|
||||
|
||||
// Literals
|
||||
const StringLiteral = createToken({
|
||||
name: 'StringLiteral',
|
||||
pattern: /'(?:''|[^'])*'/,
|
||||
});
|
||||
|
||||
const NumberLiteral = createToken({
|
||||
name: 'NumberLiteral',
|
||||
pattern: /-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/,
|
||||
});
|
||||
|
||||
// Operators
|
||||
const NotEquals = createToken({ name: 'NotEquals', pattern: /!=/ });
|
||||
const GreaterThanOrEqual = createToken({ name: 'GreaterThanOrEqual', pattern: />=/ });
|
||||
const LessThanOrEqual = createToken({ name: 'LessThanOrEqual', pattern: /<=/ });
|
||||
const Equals = createToken({ name: 'Equals', pattern: /=/ });
|
||||
const GreaterThan = createToken({ name: 'GreaterThan', pattern: />/ });
|
||||
const LessThan = createToken({ name: 'LessThan', pattern: /</ });
|
||||
|
||||
// Punctuation
|
||||
const LParen = createToken({ name: 'LParen', pattern: /\(/ });
|
||||
const RParen = createToken({ name: 'RParen', pattern: /\)/ });
|
||||
const Comma = createToken({ name: 'Comma', pattern: /,/ });
|
||||
const Dot = createToken({ name: 'Dot', pattern: /\./ });
|
||||
|
||||
// Token order matters! More specific patterns first.
|
||||
const allTokens = [
|
||||
WhiteSpace,
|
||||
// Multi-char operators first
|
||||
NotEquals,
|
||||
GreaterThanOrEqual,
|
||||
LessThanOrEqual,
|
||||
// Single-char operators
|
||||
Equals,
|
||||
GreaterThan,
|
||||
LessThan,
|
||||
// Punctuation
|
||||
LParen,
|
||||
RParen,
|
||||
Comma,
|
||||
Dot,
|
||||
// Keywords (before Identifier)
|
||||
And,
|
||||
Or,
|
||||
Like,
|
||||
Not,
|
||||
In,
|
||||
Is,
|
||||
Null,
|
||||
// Literals
|
||||
StringLiteral,
|
||||
NumberLiteral,
|
||||
// Identifier last
|
||||
Identifier,
|
||||
];
|
||||
|
||||
const QueryLexer = new Lexer(allTokens);
|
||||
|
||||
// ----------------- Parser -----------------
|
||||
|
||||
class QueryParserParser extends EmbeddedActionsParser {
|
||||
constructor() {
|
||||
super(allTokens);
|
||||
this.performSelfAnalysis();
|
||||
}
|
||||
|
||||
// OR has lowest precedence
|
||||
#orExpression = this.RULE('orExpression', (): QueryFilter => {
|
||||
let left = this.SUBRULE(this.#andExpression);
|
||||
|
||||
this.MANY(() => {
|
||||
this.CONSUME(Or);
|
||||
const right = this.SUBRULE2(this.#andExpression);
|
||||
left = this.ACTION(() => this.#combineWithOperator(left, right, 'or'));
|
||||
});
|
||||
|
||||
return left;
|
||||
});
|
||||
|
||||
// AND has higher precedence than OR
|
||||
#andExpression = this.RULE('andExpression', (): QueryFilter => {
|
||||
let left = this.SUBRULE(this.#primaryExpression);
|
||||
|
||||
this.MANY(() => {
|
||||
this.CONSUME(And);
|
||||
const right = this.SUBRULE2(this.#primaryExpression);
|
||||
left = this.ACTION(() => this.#combineWithOperator(left, right, 'and'));
|
||||
});
|
||||
|
||||
return left;
|
||||
});
|
||||
|
||||
// Primary: parenthesized expression or condition
|
||||
#primaryExpression = this.RULE('primaryExpression', (): QueryFilter => {
|
||||
return this.OR([
|
||||
{
|
||||
ALT: () => {
|
||||
this.CONSUME(LParen);
|
||||
const expr = this.SUBRULE(this.#orExpression);
|
||||
this.CONSUME(RParen);
|
||||
return expr;
|
||||
},
|
||||
},
|
||||
{ ALT: () => this.SUBRULE(this.#condition) },
|
||||
]);
|
||||
});
|
||||
|
||||
// Condition: field followed by operator and value(s)
|
||||
#condition = this.RULE('condition', (): QueryCondition => {
|
||||
const field = this.SUBRULE(this.#fieldReference);
|
||||
|
||||
return this.OR([
|
||||
// IS NULL / IS NOT NULL
|
||||
{
|
||||
ALT: () => {
|
||||
this.CONSUME(Is);
|
||||
const isNot = this.OPTION(() => this.CONSUME(Not)) !== undefined;
|
||||
this.CONSUME(Null);
|
||||
|
||||
return this.ACTION(() => ({
|
||||
type: 'text' as const,
|
||||
field,
|
||||
conditions: isNot ? { notEqual: undefined, equal: undefined } : { equal: null },
|
||||
}));
|
||||
},
|
||||
},
|
||||
// NOT IN (strings) - LA(1)=NOT, LA(2)=IN, LA(3)=(, LA(4)=value
|
||||
{
|
||||
GATE: () => this.LA(4).tokenType === StringLiteral,
|
||||
ALT: () => {
|
||||
this.CONSUME2(Not);
|
||||
this.CONSUME(In);
|
||||
const values = this.SUBRULE(this.#stringInList);
|
||||
return this.ACTION(() => ({
|
||||
type: 'text' as const,
|
||||
field,
|
||||
conditions: { notIn: values },
|
||||
}));
|
||||
},
|
||||
},
|
||||
// NOT IN (numbers)
|
||||
{
|
||||
GATE: () => this.LA(4).tokenType === NumberLiteral,
|
||||
ALT: () => {
|
||||
this.CONSUME3(Not);
|
||||
this.CONSUME2(In);
|
||||
const values = this.SUBRULE(this.#numberInList);
|
||||
return this.ACTION(() => ({
|
||||
type: 'number' as const,
|
||||
field,
|
||||
conditions: { notIn: values },
|
||||
}));
|
||||
},
|
||||
},
|
||||
// NOT LIKE
|
||||
{
|
||||
ALT: () => {
|
||||
this.CONSUME4(Not);
|
||||
this.CONSUME(Like);
|
||||
const pattern = this.CONSUME(StringLiteral);
|
||||
return this.ACTION(() => ({
|
||||
type: 'text' as const,
|
||||
field,
|
||||
conditions: { notLike: this.#extractStringValue(pattern.image) },
|
||||
}));
|
||||
},
|
||||
},
|
||||
// IN (strings) - LA(1)=IN, LA(2)=(, LA(3)=value
|
||||
{
|
||||
GATE: () => this.LA(3).tokenType === StringLiteral,
|
||||
ALT: () => {
|
||||
this.CONSUME3(In);
|
||||
const values = this.SUBRULE2(this.#stringInList);
|
||||
return this.ACTION(() => ({
|
||||
type: 'text' as const,
|
||||
field,
|
||||
conditions: { in: values },
|
||||
}));
|
||||
},
|
||||
},
|
||||
// IN (numbers)
|
||||
{
|
||||
GATE: () => this.LA(3).tokenType === NumberLiteral,
|
||||
ALT: () => {
|
||||
this.CONSUME4(In);
|
||||
const values = this.SUBRULE2(this.#numberInList);
|
||||
return this.ACTION(() => ({
|
||||
type: 'number' as const,
|
||||
field,
|
||||
conditions: { in: values },
|
||||
}));
|
||||
},
|
||||
},
|
||||
// LIKE
|
||||
{
|
||||
ALT: () => {
|
||||
this.CONSUME2(Like);
|
||||
const pattern = this.CONSUME2(StringLiteral);
|
||||
return this.ACTION(() => ({
|
||||
type: 'text' as const,
|
||||
field,
|
||||
conditions: { like: this.#extractStringValue(pattern.image) },
|
||||
}));
|
||||
},
|
||||
},
|
||||
// = string
|
||||
{
|
||||
GATE: () => this.LA(2).tokenType === StringLiteral,
|
||||
ALT: () => {
|
||||
this.CONSUME(Equals);
|
||||
const token = this.CONSUME3(StringLiteral);
|
||||
return this.ACTION(() => ({
|
||||
type: 'text' as const,
|
||||
field,
|
||||
conditions: { equal: this.#extractStringValue(token.image) },
|
||||
}));
|
||||
},
|
||||
},
|
||||
// = number
|
||||
{
|
||||
GATE: () => this.LA(2).tokenType === NumberLiteral,
|
||||
ALT: () => {
|
||||
this.CONSUME2(Equals);
|
||||
const token = this.CONSUME(NumberLiteral);
|
||||
return this.ACTION(() => ({
|
||||
type: 'number' as const,
|
||||
field,
|
||||
conditions: { equals: parseFloat(token.image) },
|
||||
}));
|
||||
},
|
||||
},
|
||||
// = NULL
|
||||
{
|
||||
ALT: () => {
|
||||
this.CONSUME3(Equals);
|
||||
this.CONSUME2(Null);
|
||||
return this.ACTION(() => ({
|
||||
type: 'text' as const,
|
||||
field,
|
||||
conditions: { equal: null },
|
||||
}));
|
||||
},
|
||||
},
|
||||
// != string
|
||||
{
|
||||
GATE: () => this.LA(2).tokenType === StringLiteral,
|
||||
ALT: () => {
|
||||
this.CONSUME(NotEquals);
|
||||
const token = this.CONSUME4(StringLiteral);
|
||||
return this.ACTION(() => ({
|
||||
type: 'text' as const,
|
||||
field,
|
||||
conditions: { notEqual: this.#extractStringValue(token.image) },
|
||||
}));
|
||||
},
|
||||
},
|
||||
// != number
|
||||
{
|
||||
ALT: () => {
|
||||
this.CONSUME2(NotEquals);
|
||||
const token = this.CONSUME2(NumberLiteral);
|
||||
return this.ACTION(() => ({
|
||||
type: 'number' as const,
|
||||
field,
|
||||
conditions: { notEquals: parseFloat(token.image) },
|
||||
}));
|
||||
},
|
||||
},
|
||||
// > number
|
||||
{
|
||||
ALT: () => {
|
||||
this.CONSUME(GreaterThan);
|
||||
const token = this.CONSUME3(NumberLiteral);
|
||||
return this.ACTION(() => ({
|
||||
type: 'number' as const,
|
||||
field,
|
||||
conditions: { greaterThan: parseFloat(token.image) },
|
||||
}));
|
||||
},
|
||||
},
|
||||
// >= number
|
||||
{
|
||||
ALT: () => {
|
||||
this.CONSUME(GreaterThanOrEqual);
|
||||
const token = this.CONSUME4(NumberLiteral);
|
||||
return this.ACTION(() => ({
|
||||
type: 'number' as const,
|
||||
field,
|
||||
conditions: { greaterThanOrEqual: parseFloat(token.image) },
|
||||
}));
|
||||
},
|
||||
},
|
||||
// < number
|
||||
{
|
||||
ALT: () => {
|
||||
this.CONSUME(LessThan);
|
||||
const token = this.CONSUME5(NumberLiteral);
|
||||
return this.ACTION(() => ({
|
||||
type: 'number' as const,
|
||||
field,
|
||||
conditions: { lessThan: parseFloat(token.image) },
|
||||
}));
|
||||
},
|
||||
},
|
||||
// <= number
|
||||
{
|
||||
ALT: () => {
|
||||
this.CONSUME(LessThanOrEqual);
|
||||
const token = this.CONSUME6(NumberLiteral);
|
||||
return this.ACTION(() => ({
|
||||
type: 'number' as const,
|
||||
field,
|
||||
conditions: { lessThanOrEqual: parseFloat(token.image) },
|
||||
}));
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
// Field reference: identifier.identifier.identifier...
|
||||
#fieldReference = this.RULE('fieldReference', (): string[] => {
|
||||
const parts: string[] = [];
|
||||
const first = this.CONSUME(Identifier);
|
||||
this.ACTION(() => parts.push(first.image));
|
||||
|
||||
this.MANY(() => {
|
||||
this.CONSUME(Dot);
|
||||
const next = this.CONSUME2(Identifier);
|
||||
this.ACTION(() => parts.push(next.image));
|
||||
});
|
||||
|
||||
return parts;
|
||||
});
|
||||
|
||||
// String IN list: ('val1', 'val2', ...)
|
||||
#stringInList = this.RULE('stringInList', (): string[] => {
|
||||
const values: string[] = [];
|
||||
|
||||
this.CONSUME(LParen);
|
||||
const first = this.CONSUME(StringLiteral);
|
||||
this.ACTION(() => values.push(this.#extractStringValue(first.image)));
|
||||
|
||||
this.MANY(() => {
|
||||
this.CONSUME(Comma);
|
||||
const next = this.CONSUME2(StringLiteral);
|
||||
this.ACTION(() => values.push(this.#extractStringValue(next.image)));
|
||||
});
|
||||
|
||||
this.CONSUME(RParen);
|
||||
return values;
|
||||
});
|
||||
|
||||
// Number IN list: (1, 2, 3, ...)
|
||||
#numberInList = this.RULE('numberInList', (): number[] => {
|
||||
const values: number[] = [];
|
||||
|
||||
this.CONSUME2(LParen);
|
||||
const first = this.CONSUME(NumberLiteral);
|
||||
this.ACTION(() => values.push(parseFloat(first.image)));
|
||||
|
||||
this.MANY(() => {
|
||||
this.CONSUME2(Comma);
|
||||
const next = this.CONSUME2(NumberLiteral);
|
||||
this.ACTION(() => values.push(parseFloat(next.image)));
|
||||
});
|
||||
|
||||
this.CONSUME2(RParen);
|
||||
return values;
|
||||
});
|
||||
|
||||
// Extract string value from quoted literal, handling escaped quotes
|
||||
#extractStringValue(image: string): string {
|
||||
// Remove surrounding quotes and unescape doubled quotes
|
||||
return image.slice(1, -1).replace(/''/g, "'");
|
||||
}
|
||||
|
||||
// Combine two filters with an operator, flattening if possible
|
||||
#combineWithOperator(left: QueryFilter, right: QueryFilter, operator: 'and' | 'or'): QueryFilter {
|
||||
if (left.type === 'operator' && left.operator === operator) {
|
||||
return {
|
||||
type: 'operator',
|
||||
operator,
|
||||
conditions: [...left.conditions, right],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'operator',
|
||||
operator,
|
||||
conditions: [left, right],
|
||||
};
|
||||
}
|
||||
|
||||
// Entry point
|
||||
#query = this.RULE('query', (): QueryFilter => {
|
||||
return this.SUBRULE(this.#orExpression);
|
||||
});
|
||||
|
||||
public parse = <T extends typeof queryFilterSchema>(
|
||||
input: string,
|
||||
schema: T = queryFilterSchema as unknown as T,
|
||||
): QueryFilter => {
|
||||
const lexResult = QueryLexer.tokenize(input);
|
||||
|
||||
if (lexResult.errors.length > 0) {
|
||||
const error = lexResult.errors[0];
|
||||
// Check if this looks like an unterminated string (starts with ' but lexer failed)
|
||||
if (error.message.includes("'") || input.slice(error.offset).startsWith("'")) {
|
||||
// Count unescaped single quotes
|
||||
const unescapedQuotes = input.replace(/''/g, '').match(/'/g);
|
||||
if (unescapedQuotes && unescapedQuotes.length % 2 !== 0) {
|
||||
throw new Error(`Unterminated string starting at position ${error.offset}`);
|
||||
}
|
||||
}
|
||||
throw new Error(`Lexer error at position ${error.offset}: ${error.message}`);
|
||||
}
|
||||
|
||||
this.input = lexResult.tokens;
|
||||
const result = this.#query();
|
||||
|
||||
if (this.errors.length > 0) {
|
||||
const error = this.errors[0];
|
||||
throw new Error(`Parse error: ${error.message}`);
|
||||
}
|
||||
|
||||
return schema.parse(result);
|
||||
};
|
||||
}
|
||||
|
||||
export { QueryParserParser, QueryLexer };
|
||||
65
packages/query-dsl/src/query-parser.schemas.ts
Normal file
65
packages/query-dsl/src/query-parser.schemas.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { z, ZodArray } from 'zod';
|
||||
|
||||
const queryConditionTextSchema = z
|
||||
.object({
|
||||
type: z.literal('text'),
|
||||
tableName: z.string().optional(),
|
||||
field: z.array(z.string()),
|
||||
conditions: z.object({
|
||||
equal: z.string().nullish(),
|
||||
notEqual: z.string().optional(),
|
||||
like: z.string().optional(),
|
||||
notLike: z.string().optional(),
|
||||
in: z.array(z.string()).optional(),
|
||||
notIn: z.array(z.string()).optional(),
|
||||
}),
|
||||
})
|
||||
.meta({ id: 'QueryConditionText' });
|
||||
|
||||
type QueryConditionText = z.infer<typeof queryConditionTextSchema>;
|
||||
|
||||
const queryConditionNumberSchema = z
|
||||
.object({
|
||||
type: z.literal('number'),
|
||||
tableName: z.string().optional(),
|
||||
field: z.array(z.string()),
|
||||
conditions: z.object({
|
||||
equals: z.number().nullish(),
|
||||
notEquals: z.number().nullish(),
|
||||
greaterThan: z.number().optional(),
|
||||
greaterThanOrEqual: z.number().optional(),
|
||||
lessThan: z.number().optional(),
|
||||
lessThanOrEqual: z.number().optional(),
|
||||
in: z.array(z.number()).optional(),
|
||||
notIn: z.array(z.number()).optional(),
|
||||
}),
|
||||
})
|
||||
.meta({ id: 'QueryConditionNumber' });
|
||||
|
||||
type QueryConditionNumber = z.infer<typeof queryConditionNumberSchema>;
|
||||
|
||||
const queryConditionSchema = z
|
||||
.discriminatedUnion('type', [queryConditionTextSchema, queryConditionNumberSchema])
|
||||
.meta({ id: 'QueryCondition' });
|
||||
|
||||
type QueryCondition = z.infer<typeof queryConditionSchema>;
|
||||
|
||||
const queryOperatorSchema = z
|
||||
.object({
|
||||
type: z.literal('operator'),
|
||||
operator: z.enum(['and', 'or']),
|
||||
get conditions(): ZodArray<typeof queryOperatorSchema | typeof queryConditionSchema> {
|
||||
// eslint-disable-next-line
|
||||
return z.array(queryFilterSchema) as any;
|
||||
},
|
||||
})
|
||||
.meta({ id: 'QueryOperator' });
|
||||
|
||||
type QueryOperator = z.infer<typeof queryOperatorSchema>;
|
||||
|
||||
const queryFilterSchema = z.union([queryOperatorSchema, queryConditionSchema]).meta({ id: 'QueryFilter' });
|
||||
|
||||
type QueryFilter = z.infer<typeof queryFilterSchema>;
|
||||
|
||||
export type { QueryConditionText, QueryConditionNumber, QueryOperator, QueryCondition, QueryFilter };
|
||||
export { queryConditionSchema, queryFilterSchema };
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
QueryCondition,
|
||||
QueryConditionText,
|
||||
QueryConditionNumber,
|
||||
} from '#root/utils/utils.query.ts';
|
||||
} from './query-parser.schemas.js';
|
||||
|
||||
class Stringifier {
|
||||
#stringifyFilter = (filter: QueryFilter, needsParens: boolean): string => {
|
||||
@@ -1,8 +1,7 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { QueryParser } from './query-parser.ts';
|
||||
|
||||
import type { QueryConditionNumber, QueryConditionText, QueryFilter, QueryOperator } from '#root/utils/utils.query.ts';
|
||||
import { QueryParser } from './query-parser.js';
|
||||
import type { QueryConditionNumber, QueryConditionText, QueryFilter, QueryOperator } from './query-parser.schemas.js';
|
||||
|
||||
describe('QueryParser', () => {
|
||||
const parser = new QueryParser();
|
||||
24
packages/query-dsl/src/query-parser.ts
Normal file
24
packages/query-dsl/src/query-parser.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { Stringifier } from './query-parser.stringifier.js';
|
||||
import { QueryParserParser } from './query-parser.parser.js';
|
||||
import type { QueryFilter } from './query-parser.schemas.js';
|
||||
|
||||
class QueryParser {
|
||||
#stringifier = new Stringifier();
|
||||
#parser = new QueryParserParser();
|
||||
|
||||
public getSerializedGastProductions() {
|
||||
return this.#parser.getSerializedGastProductions();
|
||||
}
|
||||
|
||||
public parse = (input: string): QueryFilter => {
|
||||
return this.#parser.parse(input);
|
||||
};
|
||||
|
||||
public stringify = (filter: QueryFilter): string => {
|
||||
return this.#stringifier.stringify(filter);
|
||||
};
|
||||
}
|
||||
|
||||
const queryParser = new QueryParser();
|
||||
|
||||
export { QueryParser, queryParser };
|
||||
171
packages/query-dsl/src/utils.filter.ts
Normal file
171
packages/query-dsl/src/utils.filter.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import type { QueryCondition, QueryConditionNumber, QueryConditionText, QueryFilter } from './query-parser.schemas.js';
|
||||
|
||||
const getFieldValue = <T extends Record<string, unknown>>(obj: T, field: string[]): unknown => {
|
||||
let current: unknown = obj;
|
||||
|
||||
for (const key of field) {
|
||||
if (current === null || current === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof current !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
current = (current as Record<string, unknown>)[key];
|
||||
}
|
||||
|
||||
return current;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a SQL LIKE pattern to a RegExp.
|
||||
* Handles % (any characters) and _ (single character) wildcards.
|
||||
*/
|
||||
const likeToRegex = (pattern: string): RegExp => {
|
||||
const escaped = pattern
|
||||
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
|
||||
.replace(/%/g, '.*') // % matches any characters
|
||||
.replace(/_/g, '.'); // _ matches single character
|
||||
return new RegExp(`^${escaped}$`, 'i');
|
||||
};
|
||||
|
||||
const applyQueryConditionText = <T extends Record<string, unknown>>(
|
||||
obj: T,
|
||||
{ field, conditions }: QueryConditionText,
|
||||
): boolean => {
|
||||
const value = getFieldValue(obj, field);
|
||||
|
||||
if (conditions.equal !== undefined) {
|
||||
if (conditions.equal === null) {
|
||||
if (value !== null && value !== undefined) return false;
|
||||
} else {
|
||||
if (value !== conditions.equal) return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (conditions.notEqual !== undefined) {
|
||||
if (conditions.notEqual === null) {
|
||||
if (value === null || value === undefined) return false;
|
||||
} else {
|
||||
if (value === conditions.notEqual) return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (conditions.like !== undefined) {
|
||||
if (typeof value !== 'string') return false;
|
||||
const regex = likeToRegex(conditions.like);
|
||||
if (!regex.test(value)) return false;
|
||||
}
|
||||
|
||||
if (conditions.notLike !== undefined) {
|
||||
if (typeof value !== 'string') return false;
|
||||
const regex = likeToRegex(conditions.notLike);
|
||||
if (regex.test(value)) return false;
|
||||
}
|
||||
|
||||
if (conditions.in !== undefined) {
|
||||
if (!conditions.in.includes(value as string)) return false;
|
||||
}
|
||||
|
||||
if (conditions.notIn !== undefined) {
|
||||
if (conditions.notIn.includes(value as string)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const applyQueryConditionNumber = <T extends Record<string, unknown>>(
|
||||
obj: T,
|
||||
{ field, conditions }: QueryConditionNumber,
|
||||
): boolean => {
|
||||
const value = getFieldValue(obj, field);
|
||||
|
||||
if (conditions.equals !== undefined) {
|
||||
if (conditions.equals === null) {
|
||||
if (value !== null && value !== undefined) return false;
|
||||
} else {
|
||||
if (value !== conditions.equals) return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (conditions.notEquals !== undefined) {
|
||||
if (conditions.notEquals === null) {
|
||||
if (value === null || value === undefined) return false;
|
||||
} else {
|
||||
if (value === conditions.notEquals) return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (conditions.greaterThan !== undefined) {
|
||||
if (typeof value !== 'number' || value <= conditions.greaterThan) return false;
|
||||
}
|
||||
|
||||
if (conditions.greaterThanOrEqual !== undefined) {
|
||||
if (typeof value !== 'number' || value < conditions.greaterThanOrEqual) return false;
|
||||
}
|
||||
|
||||
if (conditions.lessThan !== undefined) {
|
||||
if (typeof value !== 'number' || value >= conditions.lessThan) return false;
|
||||
}
|
||||
|
||||
if (conditions.lessThanOrEqual !== undefined) {
|
||||
if (typeof value !== 'number' || value > conditions.lessThanOrEqual) return false;
|
||||
}
|
||||
|
||||
if (conditions.in !== undefined) {
|
||||
if (!conditions.in.includes(value as number)) return false;
|
||||
}
|
||||
|
||||
if (conditions.notIn !== undefined) {
|
||||
if (conditions.notIn.includes(value as number)) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const applyQueryCondition = <T extends Record<string, unknown>>(obj: T, options: QueryCondition): boolean => {
|
||||
switch (options.type) {
|
||||
case 'text': {
|
||||
return applyQueryConditionText(obj, options);
|
||||
}
|
||||
case 'number': {
|
||||
return applyQueryConditionNumber(obj, options);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown filter type`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const applyQueryFilter = <T extends Record<string, unknown>>(obj: T, filter: QueryFilter): boolean => {
|
||||
if (filter.type === 'operator') {
|
||||
if (filter.conditions.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'or': {
|
||||
return filter.conditions.some((condition) => applyQueryFilter(obj, condition));
|
||||
}
|
||||
case 'and': {
|
||||
return filter.conditions.every((condition) => applyQueryFilter(obj, condition));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return applyQueryCondition(obj, filter);
|
||||
}
|
||||
};
|
||||
|
||||
const createFilterFunction = <T extends Record<string, unknown>>(filter: QueryFilter): ((obj: T) => boolean) => {
|
||||
return (obj: T) => applyQueryFilter(obj, filter);
|
||||
};
|
||||
|
||||
const filterObjects = <T extends Record<string, unknown>>(objects: T[], filter: QueryFilter): T[] => {
|
||||
return objects.filter(createFilterFunction(filter));
|
||||
};
|
||||
|
||||
const isMatch = <T extends Record<string, unknown>>(input: T, filter: QueryFilter) => {
|
||||
const fn = createFilterFunction(filter);
|
||||
return fn(input);
|
||||
};
|
||||
|
||||
export { createFilterFunction, filterObjects, isMatch };
|
||||
10
packages/query-dsl/tsconfig.json
Normal file
10
packages/query-dsl/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"extends": "@morten-olsen/stash-configs/tsconfig.json"
|
||||
}
|
||||
12
packages/query-dsl/vitest.config.ts
Normal file
12
packages/query-dsl/vitest.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { getAliases } from '@morten-olsen/stash-tests/vitest';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default defineConfig(async () => {
|
||||
const aliases = await getAliases();
|
||||
return {
|
||||
resolve: {
|
||||
alias: aliases,
|
||||
},
|
||||
};
|
||||
});
|
||||
4
packages/runtime/.gitignore
vendored
Normal file
4
packages/runtime/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/node_modules/
|
||||
/dist/
|
||||
/coverage/
|
||||
/.env
|
||||
40
packages/runtime/package.json
Normal file
40
packages/runtime/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"type": "module",
|
||||
"main": "dist/exports.js",
|
||||
"scripts": {
|
||||
"build": "tsc --build",
|
||||
"test:unit": "vitest --run --passWithNoTests",
|
||||
"test": "pnpm run \"/^test:/\""
|
||||
},
|
||||
"packageManager": "pnpm@10.6.0",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
".": "./dist/exports.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@morten-olsen/stash-configs": "workspace:*",
|
||||
"@morten-olsen/stash-tests": "workspace:*",
|
||||
"@types/deep-equal": "^1.0.4",
|
||||
"@types/node": "24.10.2",
|
||||
"@vitest/coverage-v8": "4.0.15",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "4.0.15"
|
||||
},
|
||||
"name": "@morten-olsen/stash-runtime",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@electric-sql/pglite": "^0.3.14",
|
||||
"@huggingface/transformers": "^3.8.1",
|
||||
"@langchain/textsplitters": "^1.0.1",
|
||||
"@morten-olsen/stash-query-dsl": "workspace:*",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"deep-equal": "^2.2.3",
|
||||
"knex": "^3.1.0",
|
||||
"knex-pglite": "^0.13.0",
|
||||
"pg": "^8.16.3",
|
||||
"pgvector": "^0.2.1",
|
||||
"zod": "4.1.13"
|
||||
}
|
||||
}
|
||||
17
packages/runtime/src/exports.ts
Normal file
17
packages/runtime/src/exports.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { env, pipeline } from '@huggingface/transformers';
|
||||
|
||||
import { EMBEDDING_MODEL } from './utils/utils.consts.js';
|
||||
|
||||
const setModelLocation = (location: string) => {
|
||||
env.cacheDir = location;
|
||||
};
|
||||
|
||||
const preloadModel = async () => {
|
||||
await pipeline('feature-extraction', EMBEDDING_MODEL);
|
||||
};
|
||||
|
||||
export { Services } from './utils/utils.services.js';
|
||||
export { StashRuntime } from './runtime.js';
|
||||
export * from './services/documents/documents.js';
|
||||
export * from './services/document-chunks/document-chunks.js';
|
||||
export { setModelLocation, preloadModel };
|
||||
12
packages/runtime/src/global.d.ts
vendored
Normal file
12
packages/runtime/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,12 @@
|
||||
import 'fastify';
|
||||
import type { Services } from './utils/utils.services.ts';
|
||||
|
||||
// eslint-disable-next-line
|
||||
declare type ExplicitAny = any;
|
||||
|
||||
declare module 'fastify' {
|
||||
// eslint-disable-next-line
|
||||
export interface FastifyInstance {
|
||||
services: Services;
|
||||
}
|
||||
}
|
||||
32
packages/runtime/src/runtime.ts
Normal file
32
packages/runtime/src/runtime.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { DocumentChunksService } from './exports.js';
|
||||
import { DatabaseService } from './services/database/database.js';
|
||||
import { DocumentsService } from './services/documents/documents.js';
|
||||
import { WarmupService } from './services/warmup/warmup.js';
|
||||
import { Services } from './utils/utils.services.js';
|
||||
|
||||
class StashRuntime {
|
||||
#services: Services;
|
||||
|
||||
constructor(services = new Services()) {
|
||||
this.#services = services;
|
||||
services.set(StashRuntime, this);
|
||||
}
|
||||
|
||||
public get database() {
|
||||
return this.#services.get(DatabaseService);
|
||||
}
|
||||
|
||||
public get documents() {
|
||||
return this.#services.get(DocumentsService);
|
||||
}
|
||||
|
||||
public get documentChunks() {
|
||||
return this.#services.get(DocumentChunksService);
|
||||
}
|
||||
|
||||
public get warmup() {
|
||||
return this.#services.get(WarmupService);
|
||||
}
|
||||
}
|
||||
|
||||
export { StashRuntime };
|
||||
40
packages/runtime/src/services/database/database.schemas.ts
Normal file
40
packages/runtime/src/services/database/database.schemas.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const insertedRecordEventSchema = z.object({
|
||||
operation: z.literal('INSERT'),
|
||||
table: z.string(),
|
||||
schema: z.string(),
|
||||
newRecord: z.record(z.string(), z.unknown()),
|
||||
});
|
||||
|
||||
type InsertedRecordEvent = z.infer<typeof insertedRecordEventSchema>;
|
||||
|
||||
const deletedRecordEventSchema = z.object({
|
||||
operation: z.literal('DELETE'),
|
||||
table: z.string(),
|
||||
schema: z.string(),
|
||||
oldRecord: z.record(z.string(), z.unknown()),
|
||||
});
|
||||
|
||||
type DeletedRecordEvent = z.infer<typeof deletedRecordEventSchema>;
|
||||
|
||||
const updatedRecordEventSchema = z.object({
|
||||
operation: z.literal('UPDATE'),
|
||||
table: z.string(),
|
||||
schema: z.string(),
|
||||
newRecord: z.record(z.string(), z.unknown()),
|
||||
oldRecord: z.record(z.string(), z.unknown()),
|
||||
});
|
||||
|
||||
type UpdatedRecordEvent = z.infer<typeof updatedRecordEventSchema>;
|
||||
|
||||
const changedRecordEventSchema = z.discriminatedUnion('operation', [
|
||||
insertedRecordEventSchema,
|
||||
deletedRecordEventSchema,
|
||||
updatedRecordEventSchema,
|
||||
]);
|
||||
|
||||
type ChangedRecordEvent = z.infer<typeof changedRecordEventSchema>;
|
||||
|
||||
export type { InsertedRecordEvent, DeletedRecordEvent, UpdatedRecordEvent, ChangedRecordEvent };
|
||||
export { insertedRecordEventSchema, deletedRecordEventSchema, updatedRecordEventSchema, changedRecordEventSchema };
|
||||
62
packages/runtime/src/services/database/database.ts
Normal file
62
packages/runtime/src/services/database/database.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { destroy, type Services } from '../../utils/utils.services.js';
|
||||
import { EventEmitter } from '../../utils/utils.event-emitter.js';
|
||||
|
||||
import { migrationSource } from './migrations/migrations.js';
|
||||
import { type ChangedRecordEvent } from './database.schemas.js';
|
||||
import type { GeneratorOutput } from './generators/generators.types.js';
|
||||
import { pgLiteGenerator } from './generators/generators.pglite.js';
|
||||
import { createEmitter } from './generators/generators.js';
|
||||
|
||||
type DatabaseServiceEvents = {
|
||||
changedRecord: (event: ChangedRecordEvent) => void;
|
||||
};
|
||||
|
||||
class DatabaseService extends EventEmitter<DatabaseServiceEvents> {
|
||||
#services: Services;
|
||||
#generated?: Promise<GeneratorOutput>;
|
||||
|
||||
constructor(services: Services) {
|
||||
super();
|
||||
this.#services = services;
|
||||
}
|
||||
|
||||
#setup = async () => {
|
||||
const emitter = createEmitter();
|
||||
const output = await pgLiteGenerator({ emitter, config: {} });
|
||||
const { instance } = output;
|
||||
await instance.migrate.latest({
|
||||
migrationSource: migrationSource({ services: this.#services }),
|
||||
});
|
||||
emitter.on('changed', this.emit.bind(this, 'changedRecord'));
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
#getGenerated = async () => {
|
||||
if (!this.#generated) {
|
||||
this.#generated = this.#setup();
|
||||
}
|
||||
return this.#generated;
|
||||
};
|
||||
|
||||
public listen = async () => {
|
||||
const { subscribe } = await this.#getGenerated();
|
||||
await subscribe();
|
||||
};
|
||||
|
||||
public getInstance = async () => {
|
||||
const { instance } = await this.#getGenerated();
|
||||
return instance;
|
||||
};
|
||||
|
||||
[destroy] = async () => {
|
||||
if (!this.#generated) {
|
||||
return;
|
||||
}
|
||||
const { instance } = await this.#generated;
|
||||
await instance.destroy();
|
||||
};
|
||||
}
|
||||
|
||||
export { type TableRows, tableNames } from './migrations/migrations.js';
|
||||
export { DatabaseService };
|
||||
@@ -0,0 +1,38 @@
|
||||
import { PGlite } from '@electric-sql/pglite';
|
||||
import knex from 'knex';
|
||||
import ClientPGLite from 'knex-pglite';
|
||||
import { vector } from '@electric-sql/pglite/vector';
|
||||
|
||||
import { changedRecordEventSchema } from '../database.schemas.js';
|
||||
|
||||
import type { Generator } from './generators.types.js';
|
||||
|
||||
type PGLiteGeneratorOptions = {
|
||||
dataLocation?: string;
|
||||
};
|
||||
const pgLiteGenerator: Generator<PGLiteGeneratorOptions> = async ({ emitter }) => {
|
||||
const pglite = new PGlite({
|
||||
extensions: { vector },
|
||||
});
|
||||
|
||||
const instance = knex({
|
||||
client: ClientPGLite,
|
||||
dialect: 'postgres',
|
||||
connection: () => ({ pglite }) as object,
|
||||
});
|
||||
await instance.raw(`CREATE EXTENSION IF NOT EXISTS vector`);
|
||||
const subscribe = async () => {
|
||||
pglite.onNotification((channel, data) => {
|
||||
if (channel !== 'row_changed') {
|
||||
return;
|
||||
}
|
||||
const payload = changedRecordEventSchema.parse(JSON.parse(data));
|
||||
emitter.emit('changed', payload);
|
||||
});
|
||||
await instance.raw('LISTEN row_changed');
|
||||
};
|
||||
|
||||
return { instance, subscribe };
|
||||
};
|
||||
|
||||
export { pgLiteGenerator };
|
||||
@@ -0,0 +1,7 @@
|
||||
import { EventEmitter } from '../../../utils/utils.event-emitter.js';
|
||||
|
||||
import type { GeneratorEvents } from './generators.types.js';
|
||||
|
||||
const createEmitter = () => new EventEmitter<GeneratorEvents>();
|
||||
|
||||
export { createEmitter };
|
||||
@@ -0,0 +1,21 @@
|
||||
import type { Knex } from 'knex';
|
||||
|
||||
import type { ChangedRecordEvent } from '../database.schemas.js';
|
||||
import type { EventEmitter } from '../../../utils/utils.event-emitter.js';
|
||||
|
||||
type GeneratorEvents = {
|
||||
changed: (event: ChangedRecordEvent) => void;
|
||||
};
|
||||
type GeneratorOutput = {
|
||||
instance: Knex;
|
||||
subscribe: () => Promise<void>;
|
||||
};
|
||||
|
||||
type GeneratorOptions<T> = {
|
||||
config: T;
|
||||
emitter: EventEmitter<GeneratorEvents>;
|
||||
};
|
||||
|
||||
type Generator<T> = (options: GeneratorOptions<T>) => Promise<GeneratorOutput>;
|
||||
|
||||
export type { GeneratorEvents, GeneratorOutput, Generator };
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Migration } from './migrations.types.ts';
|
||||
import { EMBEDDING_MODEL } from '../../../utils/utils.consts.js';
|
||||
import { EmbeddingsService } from '../../embeddings/embeddings.js';
|
||||
|
||||
import { EmbeddingsService } from '#root/services/embeddings/embeddings.ts';
|
||||
import { EMBEDDING_MODEL } from '#root/utils/utils.consts.ts';
|
||||
import type { Migration } from './migrations.types.js';
|
||||
|
||||
const tableNames = {
|
||||
documents: 'documents',
|
||||
@@ -15,6 +15,45 @@ const init: Migration = {
|
||||
const embedding = services.get(EmbeddingsService);
|
||||
const embeddingField = await embedding.getFieldType(EMBEDDING_MODEL);
|
||||
|
||||
await knex.raw(`
|
||||
CREATE OR REPLACE FUNCTION notify_changes()
|
||||
RETURNS trigger AS $$
|
||||
DECLARE
|
||||
payload TEXT;
|
||||
BEGIN
|
||||
-- Build the JSON payload based on the operation type
|
||||
IF (TG_OP = 'DELETE') THEN
|
||||
payload := json_build_object(
|
||||
'operation', TG_OP,
|
||||
'table', TG_TABLE_NAME,
|
||||
'schema', TG_TABLE_SCHEMA,
|
||||
'oldRecord', row_to_json(OLD)
|
||||
)::text;
|
||||
ELSIF (TG_OP = 'INSERT') THEN
|
||||
payload := json_build_object(
|
||||
'operation', TG_OP,
|
||||
'table', TG_TABLE_NAME,
|
||||
'schema', TG_TABLE_SCHEMA,
|
||||
'newRecord', row_to_json(NEW)
|
||||
)::text;
|
||||
ELSIF (TG_OP = 'UPDATE') THEN
|
||||
payload := json_build_object(
|
||||
'operation', TG_OP,
|
||||
'table', TG_TABLE_NAME,
|
||||
'schema', TG_TABLE_SCHEMA,
|
||||
'oldRecord', row_to_json(OLD),
|
||||
'newRecord', row_to_json(NEW)
|
||||
)::text;
|
||||
END IF;
|
||||
|
||||
-- Send the notification to the channel
|
||||
PERFORM pg_notify('row_changed', payload);
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
`);
|
||||
|
||||
await knex.schema.createTable(tableNames.documents, (table) => {
|
||||
table.uuid('id').primary();
|
||||
table.uuid('owner').nullable().references('id').inTable(tableNames.documents).onDelete('CASCADE');
|
||||
@@ -22,18 +61,25 @@ const init: Migration = {
|
||||
table.datetime('createdAt').notNullable();
|
||||
table.datetime('deletedAt').nullable();
|
||||
table.string('contentType').nullable();
|
||||
table.text('content').nullable();
|
||||
table.binary('content').nullable();
|
||||
table.text('text').nullable();
|
||||
table.string('source').nullable();
|
||||
table.string('sourceId').nullable();
|
||||
table.string('type').notNullable();
|
||||
table.string('type').nullable();
|
||||
table.integer('typeVersion').nullable();
|
||||
table.text('searchText').nullable();
|
||||
table.jsonb('metadata').nullable();
|
||||
|
||||
table.index(['source', 'sourceId']);
|
||||
table.index(['owner']);
|
||||
});
|
||||
|
||||
await knex.raw(`
|
||||
CREATE TRIGGER document_changes_trigger
|
||||
AFTER INSERT OR UPDATE OR DELETE ON documents
|
||||
FOR EACH ROW
|
||||
EXECUTE PROCEDURE notify_changes();
|
||||
`);
|
||||
|
||||
await knex.schema.createTable(tableNames.documentChunks, (table) => {
|
||||
table.uuid('id').primary();
|
||||
table.uuid('owner').nullable().references('id').inTable(tableNames.documents).onDelete('CASCADE');
|
||||
@@ -61,6 +107,8 @@ const init: Migration = {
|
||||
});
|
||||
},
|
||||
down: async ({ knex }) => {
|
||||
await knex.raw('DROP TRIGGER IF EXISTS document_changes_trigger ON documents;');
|
||||
await knex.raw('DROP FUNCTION IF EXISTS notify_changes();');
|
||||
await knex.schema.dropTableIfExists(tableNames.relations);
|
||||
await knex.schema.dropTableIfExists(tableNames.documentChunks);
|
||||
await knex.schema.dropTableIfExists(tableNames.documents);
|
||||
@@ -74,13 +122,13 @@ type DocumentRow = {
|
||||
createdAt: Date;
|
||||
deletedAt: Date | null;
|
||||
contentType: string | null;
|
||||
content: string | null;
|
||||
content: Buffer | null;
|
||||
text: string | null;
|
||||
source: string | null;
|
||||
sourceId: string | null;
|
||||
type: string;
|
||||
type: string | null;
|
||||
typeVersion: number | null;
|
||||
searchText: string | null;
|
||||
metadata: unknown;
|
||||
metadata: unknown | null;
|
||||
};
|
||||
|
||||
type DocumentChunkRow = {
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Knex } from 'knex';
|
||||
|
||||
import type { Migration } from './migrations.types.ts';
|
||||
import { init } from './migrations.001-init.ts';
|
||||
import type { Services } from '../../../utils/utils.services.js';
|
||||
|
||||
import type { Services } from '#root/utils/utils.services.ts';
|
||||
import type { Migration } from './migrations.types.js';
|
||||
import { init } from './migrations.001-init.js';
|
||||
|
||||
const migrations = [init] satisfies Migration[];
|
||||
|
||||
@@ -21,5 +21,5 @@ const migrationSource = (options: MigrationSourceOptions): Knex.MigrationSource<
|
||||
getMigrations: async () => migrations,
|
||||
});
|
||||
|
||||
export { type TableRows, tableNames } from './migrations.001-init.ts';
|
||||
export { type TableRows, tableNames } from './migrations.001-init.js';
|
||||
export { migrationSource };
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Knex } from 'knex';
|
||||
|
||||
import type { Services } from '#root/utils/utils.services.ts';
|
||||
import type { Services } from '../../../utils/utils.services.js';
|
||||
|
||||
type MigrationOptions = {
|
||||
knex: Knex;
|
||||
@@ -0,0 +1,11 @@
|
||||
import type { TableRows } from '../database/database.js';
|
||||
|
||||
import { documentChunkWithDistanceSchema } from './document-chunks.schemas.js';
|
||||
|
||||
const mapFromDocumentChunkRow = (
|
||||
row: TableRows['documentChunks'] & {
|
||||
metadata?: unknown;
|
||||
},
|
||||
) => documentChunkWithDistanceSchema.parse(row);
|
||||
|
||||
export { mapFromDocumentChunkRow };
|
||||
@@ -0,0 +1,46 @@
|
||||
import { z } from 'zod';
|
||||
import { querySchema } from '@morten-olsen/stash-query-dsl';
|
||||
|
||||
import { createListResultSchema } from '../../utils/utils.schema.js';
|
||||
|
||||
const documentChunkSchema = z
|
||||
.object({
|
||||
id: z.guid(),
|
||||
owner: z.string(),
|
||||
content: z.string(),
|
||||
metadata: z.unknown(),
|
||||
})
|
||||
.meta({ id: 'DocumentChunk' });
|
||||
|
||||
type DocumentChunk = z.infer<typeof documentChunkSchema>;
|
||||
|
||||
const documentChunkFilterSchema = z
|
||||
.object({
|
||||
limit: z.number().default(20),
|
||||
offset: z.number().default(0),
|
||||
semanticText: z.string().optional(),
|
||||
conditions: querySchema.optional(),
|
||||
})
|
||||
.meta({ id: 'DocumentChunkFilter' });
|
||||
|
||||
type DocumentChunkFilter = z.infer<typeof documentChunkFilterSchema>;
|
||||
|
||||
const documentChunkWithDistanceSchema = documentChunkSchema
|
||||
.extend({
|
||||
distance: z.number().optional(),
|
||||
})
|
||||
.meta({ id: 'DocumentChunkWithDistance' });
|
||||
|
||||
const documentChunksFindResultSchema = createListResultSchema(documentChunkWithDistanceSchema).meta({
|
||||
id: 'DocumentChunkFindResult',
|
||||
});
|
||||
|
||||
type DocumentChunksFindResult = z.infer<typeof documentChunksFindResultSchema>;
|
||||
|
||||
export type { DocumentChunk, DocumentChunkFilter, DocumentChunksFindResult };
|
||||
export {
|
||||
documentChunkSchema,
|
||||
documentChunkFilterSchema,
|
||||
documentChunksFindResultSchema,
|
||||
documentChunkWithDistanceSchema,
|
||||
};
|
||||
@@ -1,14 +1,11 @@
|
||||
import { DatabaseService, tableNames, type TableRows } from '../database/database.ts';
|
||||
import { EmbeddingsService } from '../embeddings/embeddings.ts';
|
||||
import { DatabaseService, tableNames, type TableRows } from '../database/database.js';
|
||||
import { EmbeddingsService } from '../embeddings/embeddings.js';
|
||||
import type { Services } from '../../utils/utils.services.js';
|
||||
import { EMBEDDING_MODEL } from '../../utils/utils.consts.js';
|
||||
import { applyQueryFilter } from '../../utils/utils.query.js';
|
||||
|
||||
import type { DocumentChunkFilter, DocumentChunksFindResult } from './document-chunks.schemas.ts';
|
||||
import { mapFromDocumentChunkRow } from './document.mappings.ts';
|
||||
|
||||
import type { Services } from '#root/utils/utils.services.ts';
|
||||
import { EMBEDDING_MODEL } from '#root/utils/utils.consts.ts';
|
||||
import type { ExplicitAny } from '#root/global.js';
|
||||
import { applyQueryFilter } from '#root/utils/utils.query.ts';
|
||||
import { QueryParser } from '#root/query-parser/query-parser.ts';
|
||||
import type { DocumentChunkFilter, DocumentChunksFindResult } from './document-chunks.schemas.js';
|
||||
import { mapFromDocumentChunkRow } from './document-chunks.mappings.js';
|
||||
|
||||
const baseFields = [
|
||||
`${tableNames.documentChunks}.*`,
|
||||
@@ -44,11 +41,7 @@ class DocumentChunksService {
|
||||
query = query.orderBy('createdAt', 'desc');
|
||||
}
|
||||
if (filter.conditions) {
|
||||
const parser = this.#services.get(QueryParser);
|
||||
query = applyQueryFilter(
|
||||
query,
|
||||
typeof filter.conditions === 'string' ? parser.parse(filter.conditions) : filter.conditions,
|
||||
);
|
||||
query = applyQueryFilter(query, filter.conditions);
|
||||
}
|
||||
|
||||
query = query.limit(filter.limit).offset(filter.offset);
|
||||
@@ -56,10 +49,10 @@ class DocumentChunksService {
|
||||
const items = await query;
|
||||
|
||||
return {
|
||||
items: items.map(mapFromDocumentChunkRow as ExplicitAny),
|
||||
items: items.map(mapFromDocumentChunkRow),
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export * from './document-chunks.schemas.ts';
|
||||
export * from './document-chunks.schemas.js';
|
||||
export { DocumentChunksService };
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { TableRows } from '../database/database.js';
|
||||
|
||||
import { documentSchema, type Document } from './documents.schemas.js';
|
||||
|
||||
const mapFromDocumentRow = (row: TableRows['documents']): Document => documentSchema.parse(row);
|
||||
|
||||
export { mapFromDocumentRow };
|
||||
@@ -1,42 +1,44 @@
|
||||
import { z } from 'zod';
|
||||
import { queryFilterSchema } from '@morten-olsen/stash-query-dsl';
|
||||
|
||||
import { createListResultSchema } from '#root/utils/utils.schema.ts';
|
||||
import { queryFilterSchema } from '#root/utils/utils.query.ts';
|
||||
import { createListResultSchema, queryDSLSchema } from '../../utils/utils.schema.js';
|
||||
|
||||
const documentSchema = z.object({
|
||||
id: z.string(),
|
||||
owner: z.string().nullable(),
|
||||
createdAt: z.iso.datetime(),
|
||||
updatedAt: z.iso.datetime(),
|
||||
deletedAt: z.iso.datetime().nullable(),
|
||||
const documentSchema = z
|
||||
.object({
|
||||
id: z.guid(),
|
||||
owner: z.guid().nullable(),
|
||||
createdAt: z.coerce.date(),
|
||||
updatedAt: z.coerce.date(),
|
||||
deletedAt: z.coerce.date().nullable(),
|
||||
contentType: z.string().nullable(),
|
||||
content: z.string().nullable(),
|
||||
text: z.string().nullable(),
|
||||
source: z.string().nullable(),
|
||||
sourceId: z.string().nullable(),
|
||||
type: z.string(),
|
||||
type: z.string().nullable(),
|
||||
typeVersion: z.int().nullable(),
|
||||
searchText: z.string().nullable(),
|
||||
metadata: z.unknown(),
|
||||
});
|
||||
metadata: z.unknown().nullable(),
|
||||
})
|
||||
.meta({ id: 'Document' });
|
||||
|
||||
type Document = z.infer<typeof documentSchema>;
|
||||
|
||||
const documentUpsertSchema = z
|
||||
.object({
|
||||
id: z.string().nullish(),
|
||||
id: z.guid().optional(),
|
||||
owner: z.string().nullish(),
|
||||
contentType: z.string().nullish(),
|
||||
content: z.string().nullish(),
|
||||
text: z.string().nullish(),
|
||||
source: z.string().nullish(),
|
||||
sourceId: z.string().nullish(),
|
||||
type: z.string().optional(),
|
||||
type: z.string().nullish(),
|
||||
typeVersion: z.int().nullish(),
|
||||
searchText: z.string().nullish(),
|
||||
metadata: z.unknown().nullish(),
|
||||
})
|
||||
.meta({
|
||||
id: 'DocumentUpsert',
|
||||
example: {
|
||||
content: 'the cat is yellow',
|
||||
text: 'the cat is yellow',
|
||||
contentType: 'text/plain',
|
||||
source: 'test',
|
||||
sourceId: 'test',
|
||||
@@ -61,7 +63,7 @@ type DocumentUpsertResult = z.infer<typeof documentUpsertResultSchema>;
|
||||
const documentFilterSchema = z.object({
|
||||
offset: z.number().default(0),
|
||||
limit: z.number().default(20),
|
||||
condition: z.union([queryFilterSchema, z.string()]),
|
||||
condition: z.union([queryDSLSchema, queryFilterSchema]),
|
||||
});
|
||||
|
||||
type DocumentFilter = z.infer<typeof documentFilterSchema>;
|
||||
@@ -70,11 +72,26 @@ const documentFindResultSchema = createListResultSchema(documentSchema);
|
||||
|
||||
type DocumentFindResult = z.infer<typeof documentFindResultSchema>;
|
||||
|
||||
export type { Document, DocumentUpsert, DocumentUpsertResult, DocumentFilter, DocumentFindResult };
|
||||
const documentFilterChangedEventSchema = z.object({
|
||||
action: z.enum(['add', 'remove', 'update']),
|
||||
document: documentSchema,
|
||||
});
|
||||
|
||||
type DocumentFilterChangedEvent = z.infer<typeof documentFilterChangedEventSchema>;
|
||||
|
||||
export type {
|
||||
Document,
|
||||
DocumentUpsert,
|
||||
DocumentUpsertResult,
|
||||
DocumentFilter,
|
||||
DocumentFindResult,
|
||||
DocumentFilterChangedEvent,
|
||||
};
|
||||
export {
|
||||
documentSchema,
|
||||
documentUpsertSchema,
|
||||
documentUpsertResultSchema,
|
||||
documentFilterSchema,
|
||||
documentFindResultSchema,
|
||||
documentFilterChangedEventSchema,
|
||||
};
|
||||
308
packages/runtime/src/services/documents/documents.ts
Normal file
308
packages/runtime/src/services/documents/documents.ts
Normal file
@@ -0,0 +1,308 @@
|
||||
import { isMatch, QueryParser, type QueryFilter } from '@morten-olsen/stash-query-dsl';
|
||||
|
||||
import { DatabaseService, tableNames, type TableRows } from '../database/database.js';
|
||||
import { SplittingService } from '../splitter/splitter.js';
|
||||
import { EventEmitter } from '../../utils/utils.event-emitter.js';
|
||||
import { destroy, type Services } from '../../utils/utils.services.js';
|
||||
import { compareObjectKeys } from '../../utils/utils.compare.js';
|
||||
import { applyQueryFilter } from '../../utils/utils.query.js';
|
||||
import { base64ToMaybeBuffer } from '../../utils/utils.binary.js';
|
||||
|
||||
import { mapFromDocumentRow } from './documents.mapping.js';
|
||||
import {
|
||||
type Document,
|
||||
type DocumentFilter,
|
||||
type DocumentFilterChangedEvent,
|
||||
type DocumentFindResult,
|
||||
type DocumentUpsert,
|
||||
type DocumentUpsertResult,
|
||||
} from './documents.schemas.js';
|
||||
|
||||
type DocumentsServiceEvents = {
|
||||
upserted: (document: Document) => void;
|
||||
inserted: (document: Document) => void;
|
||||
updated: (next: Document, prev: Document) => void;
|
||||
deleted: (document: Document) => void;
|
||||
};
|
||||
|
||||
type DocumentServiceFilterSubscriber = {
|
||||
filter?: QueryFilter | string;
|
||||
fn: (event: DocumentFilterChangedEvent) => void;
|
||||
abortSignal?: AbortSignal;
|
||||
};
|
||||
|
||||
class DocumentsService extends EventEmitter<DocumentsServiceEvents> {
|
||||
#services: Services;
|
||||
#subscribeListenAbortController: AbortController;
|
||||
#databaseListenAbortController?: Promise<AbortController>;
|
||||
|
||||
constructor(services: Services) {
|
||||
super();
|
||||
this.#subscribeListenAbortController = new AbortController();
|
||||
this.onSubscribe(
|
||||
async () => {
|
||||
await this.#listen();
|
||||
},
|
||||
{
|
||||
abortSignal: this.#subscribeListenAbortController.signal,
|
||||
},
|
||||
);
|
||||
this.#services = services;
|
||||
}
|
||||
|
||||
#setupListen = async () => {
|
||||
const abortController = new AbortController();
|
||||
const databaseService = this.#services.get(DatabaseService);
|
||||
await databaseService.listen();
|
||||
databaseService.on(
|
||||
'changedRecord',
|
||||
(evt) => {
|
||||
if (evt.table !== tableNames.documents) {
|
||||
return;
|
||||
}
|
||||
if (evt.operation === 'INSERT') {
|
||||
const newDocument = mapFromDocumentRow(evt.newRecord as TableRows['documents']);
|
||||
this.emit('inserted', newDocument);
|
||||
this.emit('upserted', newDocument);
|
||||
}
|
||||
if (evt.operation === 'UPDATE') {
|
||||
const newDocument = mapFromDocumentRow(evt.newRecord as TableRows['documents']);
|
||||
const oldDocument = mapFromDocumentRow(evt.oldRecord as TableRows['documents']);
|
||||
this.emit('updated', newDocument, oldDocument);
|
||||
this.emit('upserted', newDocument);
|
||||
}
|
||||
if (evt.operation === 'DELETE') {
|
||||
const oldDocument = mapFromDocumentRow(evt.oldRecord as TableRows['documents']);
|
||||
this.emit('deleted', oldDocument);
|
||||
}
|
||||
},
|
||||
{ abortSignal: abortController.signal },
|
||||
);
|
||||
return abortController;
|
||||
};
|
||||
|
||||
#listen = async () => {
|
||||
if (!this.#databaseListenAbortController) {
|
||||
this.#databaseListenAbortController = this.#setupListen();
|
||||
}
|
||||
return this.#databaseListenAbortController;
|
||||
};
|
||||
|
||||
public subscribe = async (options: DocumentServiceFilterSubscriber) => {
|
||||
const abortController = new AbortController();
|
||||
const queryParser = this.#services.get(QueryParser);
|
||||
const filter = typeof options.filter === 'string' ? queryParser.parse(options.filter) : options.filter;
|
||||
|
||||
this.on(
|
||||
'inserted',
|
||||
(next) => {
|
||||
const nextIncluded = !filter || isMatch(next, filter);
|
||||
if (!nextIncluded) {
|
||||
return;
|
||||
}
|
||||
options.fn({
|
||||
action: 'add',
|
||||
document: next,
|
||||
});
|
||||
},
|
||||
{ abortSignal: abortController.signal },
|
||||
);
|
||||
|
||||
this.on(
|
||||
'updated',
|
||||
(next, prev) => {
|
||||
const nextIncluded = !filter || isMatch(next, filter);
|
||||
const prevIncluded = !filter || isMatch(prev, filter);
|
||||
if (nextIncluded && prevIncluded) {
|
||||
options.fn({
|
||||
action: 'update',
|
||||
document: next,
|
||||
});
|
||||
} else if (nextIncluded && !prevIncluded) {
|
||||
options.fn({
|
||||
action: 'add',
|
||||
document: next,
|
||||
});
|
||||
} else if (!nextIncluded && prevIncluded) {
|
||||
options.fn({
|
||||
action: 'remove',
|
||||
document: next,
|
||||
});
|
||||
}
|
||||
},
|
||||
{ abortSignal: abortController.signal },
|
||||
);
|
||||
|
||||
this.on(
|
||||
'deleted',
|
||||
(prev) => {
|
||||
const prevIncluded = !filter || isMatch(prev, filter);
|
||||
if (!prevIncluded) {
|
||||
return;
|
||||
}
|
||||
options.fn({
|
||||
action: 'remove',
|
||||
document: prev,
|
||||
});
|
||||
},
|
||||
{ abortSignal: abortController.signal },
|
||||
);
|
||||
|
||||
options.abortSignal?.addEventListener('abort', () => abortController.abort());
|
||||
|
||||
await this.#listen();
|
||||
return () => abortController.abort();
|
||||
};
|
||||
|
||||
public find = async (filter: DocumentFilter): Promise<DocumentFindResult> => {
|
||||
const databaseService = this.#services.get(DatabaseService);
|
||||
const db = await databaseService.getInstance();
|
||||
let query = db<TableRows['documents']>(tableNames.documents);
|
||||
if (filter) {
|
||||
const parser = this.#services.get(QueryParser);
|
||||
query = applyQueryFilter(
|
||||
query,
|
||||
typeof filter.condition === 'string' ? parser.parse(filter.condition) : filter.condition,
|
||||
);
|
||||
}
|
||||
query = query.limit(filter.limit).offset(filter.offset);
|
||||
const items = await query;
|
||||
return {
|
||||
items: items.map(mapFromDocumentRow),
|
||||
};
|
||||
};
|
||||
|
||||
public get = async (id: string): Promise<Document> => {
|
||||
const databaseService = this.#services.get(DatabaseService);
|
||||
const db = await databaseService.getInstance();
|
||||
const [item] = await db<TableRows['documents']>(tableNames.documents).where('id', id).limit(1);
|
||||
return mapFromDocumentRow(item);
|
||||
};
|
||||
|
||||
public remove = async (id: string): Promise<void> => {
|
||||
const databaseService = this.#services.get(DatabaseService);
|
||||
const db = await databaseService.getInstance();
|
||||
await db<TableRows['documents']>(tableNames.documents).where('id', id).delete();
|
||||
};
|
||||
|
||||
public upsert = async (document: DocumentUpsert): Promise<DocumentUpsertResult> => {
|
||||
const databaseService = this.#services.get(DatabaseService);
|
||||
const db = await databaseService.getInstance();
|
||||
|
||||
const result = await db.transaction(async (trx) => {
|
||||
let id = document.id || crypto.randomUUID();
|
||||
if (!document.id && document.source && document.sourceId) {
|
||||
const [currentSourceDocument] = await trx<TableRows['documents']>(tableNames.documents)
|
||||
.where('source', document.source)
|
||||
.andWhere('sourceId', document.sourceId)
|
||||
.limit(1);
|
||||
if (currentSourceDocument) {
|
||||
id = currentSourceDocument.id;
|
||||
}
|
||||
}
|
||||
const now = new Date();
|
||||
const [current] = await trx<TableRows['documents']>(tableNames.documents).where('id', id).limit(1);
|
||||
if (current) {
|
||||
id = current.id;
|
||||
document.id = id;
|
||||
if (
|
||||
compareObjectKeys(current, document, [
|
||||
'sourceId',
|
||||
'source',
|
||||
'content',
|
||||
'contentType',
|
||||
'type',
|
||||
'typeVersion',
|
||||
'metadata',
|
||||
'text',
|
||||
])
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
action: 'skipped',
|
||||
document: mapFromDocumentRow(current),
|
||||
} as const;
|
||||
}
|
||||
await trx<TableRows['documents']>(tableNames.documents)
|
||||
.update({
|
||||
...document,
|
||||
content: base64ToMaybeBuffer(document.content),
|
||||
id,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where('id', id);
|
||||
const resultDocument: Document = mapFromDocumentRow({
|
||||
...current,
|
||||
...document,
|
||||
content: base64ToMaybeBuffer(document.content ?? current.content) || null,
|
||||
id,
|
||||
});
|
||||
return {
|
||||
id,
|
||||
action: 'updated',
|
||||
document: resultDocument,
|
||||
} as const;
|
||||
} else {
|
||||
await trx<TableRows['documents']>(tableNames.documents).insert({
|
||||
...document,
|
||||
content: base64ToMaybeBuffer(document.content),
|
||||
id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
const resultDocument: Document = mapFromDocumentRow({
|
||||
type: null,
|
||||
text: null,
|
||||
owner: null,
|
||||
contentType: null,
|
||||
source: null,
|
||||
sourceId: null,
|
||||
typeVersion: null,
|
||||
metadata: null,
|
||||
...document,
|
||||
content: base64ToMaybeBuffer(document.content) || null,
|
||||
deletedAt: null,
|
||||
id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
return {
|
||||
id,
|
||||
action: 'inserted',
|
||||
document: resultDocument,
|
||||
} as const;
|
||||
}
|
||||
});
|
||||
|
||||
if (result.action !== 'skipped') {
|
||||
await db.transaction(async (trx) => {
|
||||
await trx<TableRows['documentChunks']>(tableNames.documentChunks).delete().where('owner', result.id);
|
||||
const splittingService = this.#services.get(SplittingService);
|
||||
const chunks = await splittingService.chunk(result.document);
|
||||
if (chunks.length > 0) {
|
||||
await trx<TableRows['documentChunks']>(tableNames.documentChunks).insert(
|
||||
chunks.map((chunk) => ({
|
||||
id: crypto.randomUUID(),
|
||||
owner: result.id,
|
||||
content: chunk.content,
|
||||
embedding: chunk.vector.toSql(),
|
||||
embeddingModel: chunk.model,
|
||||
})),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
[destroy] = async () => {
|
||||
this.#subscribeListenAbortController.abort();
|
||||
if (this.#databaseListenAbortController) {
|
||||
(await this.#databaseListenAbortController).abort();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export * from './documents.schemas.js';
|
||||
export { DocumentsService };
|
||||
@@ -1,6 +1,8 @@
|
||||
import { pipeline, type FeatureExtractionPipeline } from '@huggingface/transformers';
|
||||
|
||||
import { Vector } from './embeddings.vector.ts';
|
||||
import type { ExplicitAny } from '../../global.js';
|
||||
|
||||
import { Vector } from './embeddings.vector.js';
|
||||
|
||||
type ExtractOptions = {
|
||||
input: string[];
|
||||
@@ -57,4 +59,4 @@ class EmbeddingsService {
|
||||
};
|
||||
}
|
||||
|
||||
export { EmbeddingsService };
|
||||
export { EmbeddingsService, Vector };
|
||||
@@ -1,11 +1,10 @@
|
||||
import { EmbeddingsService } from '../embeddings/embeddings.ts';
|
||||
import type { Document } from '../documents/documents.schemas.ts';
|
||||
import { EmbeddingsService } from '../embeddings/embeddings.js';
|
||||
import type { Document } from '../documents/documents.schemas.js';
|
||||
import type { Services } from '../../utils/utils.services.js';
|
||||
import { EMBEDDING_MODEL } from '../../utils/utils.consts.js';
|
||||
|
||||
import type { Chunk, Splitter } from './splitter.types.ts';
|
||||
import { textSplitter } from './splitters/splitters.text.ts';
|
||||
|
||||
import type { Services } from '#root/utils/utils.services.ts';
|
||||
import { EMBEDDING_MODEL } from '#root/utils/utils.consts.ts';
|
||||
import type { Chunk, Splitter } from './splitter.types.js';
|
||||
import { textSplitter } from './splitters/splitters.text.js';
|
||||
|
||||
class SplittingService {
|
||||
#services: Services;
|
||||
@@ -40,5 +39,5 @@ class SplittingService {
|
||||
};
|
||||
}
|
||||
|
||||
export * from './splitter.types.ts';
|
||||
export * from './splitter.types.js';
|
||||
export { SplittingService };
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Document } from '../documents/documents.schemas.ts';
|
||||
import type { Vector } from '../embeddings/embeddings.vector.ts';
|
||||
import type { Document } from '../documents/documents.schemas.js';
|
||||
import type { Vector } from '../embeddings/embeddings.vector.js';
|
||||
|
||||
type Chunk = {
|
||||
content: string;
|
||||
@@ -1,15 +1,15 @@
|
||||
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';
|
||||
|
||||
import type { Splitter } from '../splitter.types.ts';
|
||||
import type { Splitter } from '../splitter.types.js';
|
||||
|
||||
const textSplitter: Splitter = {
|
||||
match: (document) => !!document.content,
|
||||
match: (document) => !!document.text,
|
||||
chunk: async (document) => {
|
||||
if (!document.content) {
|
||||
if (!document.text) {
|
||||
return [];
|
||||
}
|
||||
const splitter = new RecursiveCharacterTextSplitter({ chunkSize: 100, chunkOverlap: 0 });
|
||||
const texts = await splitter.splitText(document.content);
|
||||
const texts = await splitter.splitText(document.text);
|
||||
return texts;
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DatabaseService } from '../database/database.ts';
|
||||
|
||||
import { Services } from '#root/utils/utils.services.ts';
|
||||
import { DatabaseService } from '../database/database.js';
|
||||
import { Services } from '../../utils/utils.services.js';
|
||||
|
||||
class WarmupService {
|
||||
#services: Services;
|
||||
14
packages/runtime/src/utils/utils.binary.ts
Normal file
14
packages/runtime/src/utils/utils.binary.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
const base64ToMaybeBuffer = (input?: string | null | Buffer) => {
|
||||
if (input === null) {
|
||||
return input;
|
||||
}
|
||||
if (!input) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof input === 'object') {
|
||||
return input;
|
||||
}
|
||||
return Buffer.from(input, 'base64');
|
||||
};
|
||||
|
||||
export { base64ToMaybeBuffer };
|
||||
21
packages/runtime/src/utils/utils.compare.ts
Normal file
21
packages/runtime/src/utils/utils.compare.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import deepEqual from 'deep-equal';
|
||||
|
||||
const compareObjectKeys = <A extends Record<string, unknown>, B extends Record<string, unknown>>(
|
||||
a: A,
|
||||
b: B,
|
||||
keys: (keyof (A & B))[],
|
||||
) => {
|
||||
for (const key of keys) {
|
||||
const avalue = a[key as keyof A];
|
||||
const bvalue = b[key as keyof B];
|
||||
if (bvalue === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (!deepEqual(avalue, bvalue)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export { compareObjectKeys };
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { ExplicitAny } from '../global.js';
|
||||
|
||||
type EventListener<T extends unknown[]> = (...args: T) => void | Promise<void>;
|
||||
type SubscribeListener<T> = (type: T) => void | Promise<void>;
|
||||
|
||||
type OnOptions = {
|
||||
abortSignal?: AbortSignal;
|
||||
@@ -6,8 +9,25 @@ type OnOptions = {
|
||||
|
||||
class EventEmitter<T extends Record<string, (...args: ExplicitAny[]) => void | Promise<void>>> {
|
||||
#listeners = new Map<keyof T, Set<EventListener<ExplicitAny>>>();
|
||||
#subscribeListeners = new Set<SubscribeListener<keyof T>>();
|
||||
|
||||
onSubscribe = (callback: SubscribeListener<keyof T>, options: OnOptions = {}) => {
|
||||
const { abortSignal } = options;
|
||||
const callbackClone = (type: keyof T) => callback(type);
|
||||
this.#subscribeListeners.add(callbackClone);
|
||||
const abortController = new AbortController();
|
||||
abortSignal?.addEventListener('abort', abortController.abort);
|
||||
|
||||
abortController.signal.addEventListener('abort', () => {
|
||||
this.#subscribeListeners.difference(new Set([callbackClone]));
|
||||
});
|
||||
return abortController.abort;
|
||||
};
|
||||
|
||||
on = <K extends keyof T>(event: K, callback: EventListener<Parameters<T[K]>>, options: OnOptions = {}) => {
|
||||
for (const subscribeListener of this.#subscribeListeners) {
|
||||
subscribeListener(event);
|
||||
}
|
||||
const { abortSignal } = options;
|
||||
if (!this.#listeners.has(event)) {
|
||||
this.#listeners.set(event, new Set());
|
||||
161
packages/runtime/src/utils/utils.query.ts
Normal file
161
packages/runtime/src/utils/utils.query.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import type {
|
||||
QueryCondition,
|
||||
QueryConditionNumber,
|
||||
QueryConditionText,
|
||||
QueryFilter,
|
||||
} from '@morten-olsen/stash-query-dsl';
|
||||
import { type Knex } from 'knex';
|
||||
/**
|
||||
* Escapes a JSON key for use in PostgreSQL JSON operators.
|
||||
* Escapes single quotes by doubling them, which is the PostgreSQL standard.
|
||||
*/
|
||||
const escapeJsonKey = (key: string): string => {
|
||||
return key.replace(/'/g, "''");
|
||||
};
|
||||
|
||||
const getFieldSelector = (query: Knex.QueryBuilder, field: string[], tableName?: string) => {
|
||||
const baseColumn = field[0];
|
||||
if (field.length === 1) {
|
||||
return tableName ? `${tableName}.${baseColumn}` : baseColumn;
|
||||
}
|
||||
|
||||
const baseFieldRef = tableName ? query.client.ref(baseColumn).withSchema(tableName) : query.client.ref(baseColumn);
|
||||
const jsonPath = field.slice(1);
|
||||
let sqlExpression = baseFieldRef.toString();
|
||||
|
||||
for (let i = 0; i < jsonPath.length - 1; i++) {
|
||||
const escapedKey = escapeJsonKey(jsonPath[i]);
|
||||
sqlExpression += ` -> '${escapedKey}'`;
|
||||
}
|
||||
|
||||
const finalElement = jsonPath[jsonPath.length - 1];
|
||||
const escapedFinalKey = escapeJsonKey(finalElement);
|
||||
sqlExpression += ` ->> '${escapedFinalKey}'`;
|
||||
return query.client.raw(sqlExpression);
|
||||
};
|
||||
|
||||
const applyQueryConditionText = (query: Knex.QueryBuilder, { field, tableName, conditions }: QueryConditionText) => {
|
||||
const selector = getFieldSelector(query, field, tableName);
|
||||
if (conditions.equal) {
|
||||
query = query.where(selector, '=', conditions.equal);
|
||||
}
|
||||
if (conditions.notEqual) {
|
||||
query = query.where(selector, '<>', conditions.notEqual);
|
||||
}
|
||||
if (conditions.like) {
|
||||
query = query.whereLike(selector, conditions.like);
|
||||
}
|
||||
if (conditions.notLike) {
|
||||
query = query.not.whereLike(selector, conditions.notLike);
|
||||
}
|
||||
if (conditions.equal === null) {
|
||||
query = query.whereNull(selector);
|
||||
}
|
||||
if (conditions.notEqual === null) {
|
||||
query = query.whereNotNull(selector);
|
||||
}
|
||||
if (conditions.in) {
|
||||
query = query.whereIn(selector, conditions.in);
|
||||
}
|
||||
if (conditions.notIn) {
|
||||
query = query.whereNotIn(selector, conditions.notIn);
|
||||
}
|
||||
return query;
|
||||
};
|
||||
|
||||
const applyQueryConditionNumber = (
|
||||
query: Knex.QueryBuilder,
|
||||
{ field, tableName, conditions }: QueryConditionNumber,
|
||||
) => {
|
||||
const selector = getFieldSelector(query, field, tableName);
|
||||
if (conditions.equals !== undefined && conditions.equals !== null) {
|
||||
query = query.where(selector, '=', conditions.equals);
|
||||
}
|
||||
if (conditions.notEquals !== undefined && conditions.notEquals !== null) {
|
||||
query = query.where(selector, '<>', conditions.notEquals);
|
||||
}
|
||||
if (conditions.equals === null) {
|
||||
query = query.whereNull(selector);
|
||||
}
|
||||
if (conditions.notEquals === null) {
|
||||
query = query.whereNotNull(selector);
|
||||
}
|
||||
if (conditions.greaterThan) {
|
||||
query = query.where(selector, '>', conditions.greaterThan);
|
||||
}
|
||||
if (conditions.greaterThanOrEqual) {
|
||||
query = query.where(selector, '>=', conditions.greaterThanOrEqual);
|
||||
}
|
||||
if (conditions.lessThan) {
|
||||
query = query.where(selector, '<', conditions.lessThan);
|
||||
}
|
||||
if (conditions.lessThanOrEqual) {
|
||||
query = query.where(selector, '<=', conditions.lessThanOrEqual);
|
||||
}
|
||||
if (conditions.in) {
|
||||
query = query.whereIn(selector, conditions.in);
|
||||
}
|
||||
if (conditions.notIn) {
|
||||
query = query.whereNotIn(selector, conditions.notIn);
|
||||
}
|
||||
return query;
|
||||
};
|
||||
|
||||
const applyQueryCondition = (query: Knex.QueryBuilder, options: QueryCondition) => {
|
||||
switch (options.type) {
|
||||
case 'text': {
|
||||
return applyQueryConditionText(query, options);
|
||||
}
|
||||
case 'number': {
|
||||
return applyQueryConditionNumber(query, options);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown filter type`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const applyQueryFilter = (query: Knex.QueryBuilder, filter: QueryFilter) => {
|
||||
if (filter.type === 'operator') {
|
||||
if (filter.conditions.length === 0) {
|
||||
return query;
|
||||
}
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'or': {
|
||||
return query.where((subquery) => {
|
||||
let isFirst = true;
|
||||
for (const condition of filter.conditions) {
|
||||
if (isFirst) {
|
||||
applyQueryFilter(subquery, condition);
|
||||
isFirst = false;
|
||||
} else {
|
||||
subquery.orWhere((subSubquery) => {
|
||||
applyQueryFilter(subSubquery, condition);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
case 'and': {
|
||||
return query.where((subquery) => {
|
||||
let isFirst = true;
|
||||
for (const condition of filter.conditions) {
|
||||
if (isFirst) {
|
||||
applyQueryFilter(subquery, condition);
|
||||
isFirst = false;
|
||||
} else {
|
||||
subquery.andWhere((subSubquery) => {
|
||||
applyQueryFilter(subSubquery, condition);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return applyQueryCondition(query, filter);
|
||||
}
|
||||
};
|
||||
|
||||
export { applyQueryCondition, applyQueryFilter };
|
||||
26
packages/runtime/src/utils/utils.schema.ts
Normal file
26
packages/runtime/src/utils/utils.schema.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { QueryParser } from '@morten-olsen/stash-query-dsl';
|
||||
import { z, type ZodType } from 'zod';
|
||||
|
||||
const parser = new QueryParser();
|
||||
|
||||
const createListResultSchema = <T extends ZodType>(schema: T) =>
|
||||
z.object({
|
||||
items: z.array(schema),
|
||||
});
|
||||
|
||||
const queryDSLSchema = z
|
||||
.string()
|
||||
.describe('Query DSL based filter')
|
||||
.superRefine((value, context) => {
|
||||
try {
|
||||
parser.parse(value);
|
||||
} catch (err) {
|
||||
context.addIssue(String(err));
|
||||
}
|
||||
})
|
||||
.meta({
|
||||
id: 'QueryDQLString',
|
||||
examples: ["metadata.foo = 'bar'"],
|
||||
});
|
||||
|
||||
export { createListResultSchema, queryDSLSchema };
|
||||
10
packages/runtime/tsconfig.json
Normal file
10
packages/runtime/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"extends": "@morten-olsen/stash-configs/tsconfig.json"
|
||||
}
|
||||
12
packages/runtime/vitest.config.ts
Normal file
12
packages/runtime/vitest.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { getAliases } from '@morten-olsen/stash-tests/vitest';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default defineConfig(async () => {
|
||||
const aliases = await getAliases();
|
||||
return {
|
||||
resolve: {
|
||||
alias: aliases,
|
||||
},
|
||||
};
|
||||
});
|
||||
32
packages/server/Dockerfile
Normal file
32
packages/server/Dockerfile
Normal file
@@ -0,0 +1,32 @@
|
||||
FROM node:23-slim AS base
|
||||
ENV \
|
||||
MODEL_DIR=/models
|
||||
RUN corepack enable
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS builder
|
||||
RUN npm i -g turbo
|
||||
COPY . .
|
||||
RUN turbo prune @morten-olsen/stash-server --docker
|
||||
|
||||
FROM base AS installer
|
||||
COPY --from=builder /app/out/json/ .
|
||||
RUN pnpm install --frozen-lockfile
|
||||
COPY --from=builder /app/out/full/ .
|
||||
RUN \
|
||||
pnpm build \
|
||||
&& node /app/packages/server/dist/preload-data.js
|
||||
|
||||
FROM base AS runner
|
||||
ENV \
|
||||
SERVER_HOST=0.0.0.0
|
||||
RUN \
|
||||
addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 nodejs \
|
||||
&& mkdir /data \
|
||||
&& chown nodejs:nodejs /data
|
||||
COPY --from=installer /models /models
|
||||
COPY --from=installer /app /app
|
||||
USER nodejs
|
||||
|
||||
CMD ["node", "/app/packages/server/dist/start.js"]
|
||||
@@ -27,25 +27,15 @@
|
||||
},
|
||||
"name": "@morten-olsen/stash-server",
|
||||
"version": "1.0.0",
|
||||
"imports": {
|
||||
"#root/*": "./src/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@electric-sql/pglite": "^0.3.14",
|
||||
"@fastify/cors": "11.1.0",
|
||||
"@fastify/swagger": "9.6.1",
|
||||
"@fastify/websocket": "11.2.0",
|
||||
"@huggingface/transformers": "^3.8.1",
|
||||
"@langchain/textsplitters": "^1.0.1",
|
||||
"@morten-olsen/stash-query-dsl": "workspace:*",
|
||||
"@morten-olsen/stash-runtime": "workspace:*",
|
||||
"@scalar/fastify-api-reference": "1.40.2",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"deep-equal": "^2.2.3",
|
||||
"fastify": "5.6.2",
|
||||
"fastify-type-provider-zod": "6.1.0",
|
||||
"knex": "^3.1.0",
|
||||
"knex-pglite": "^0.13.0",
|
||||
"pg": "^8.16.3",
|
||||
"pgvector": "^0.2.1",
|
||||
"zod": "4.1.13",
|
||||
"zod-to-json-schema": "3.25.0"
|
||||
}
|
||||
|
||||
@@ -5,17 +5,18 @@ import {
|
||||
hasZodFastifySchemaValidationErrors,
|
||||
isResponseSerializationError,
|
||||
jsonSchemaTransform,
|
||||
jsonSchemaTransformObject,
|
||||
serializerCompiler,
|
||||
validatorCompiler,
|
||||
type ZodTypeProvider,
|
||||
} from 'fastify-type-provider-zod';
|
||||
import scalar from '@scalar/fastify-api-reference';
|
||||
import { StashRuntime } from '@morten-olsen/stash-runtime';
|
||||
|
||||
import { Services } from './utils/utils.services.ts';
|
||||
import { systemEndpoints } from './endpoints/system/system.ts';
|
||||
import { WarmupService } from './services/warmup/warmup.ts';
|
||||
import { documentEndpoints } from './endpoints/documents/documents.ts';
|
||||
import { documentFilterEndpoints } from './endpoints/document-filters/document-filters.ts';
|
||||
import { documentChunkFilterEndpoints } from './endpoints/document-chunk-filters/document-chunk-filters.ts';
|
||||
import { systemEndpoints } from './endpoints/system/system.js';
|
||||
import { documentEndpoints } from './endpoints/documents/documents.js';
|
||||
import { documentFilterEndpoints } from './endpoints/document-filters/document-filters.js';
|
||||
import { documentChunkFilterEndpoints } from './endpoints/document-chunk-filters/document-chunk-filters.js';
|
||||
|
||||
class BaseError extends Error {
|
||||
public statusCode: number;
|
||||
@@ -26,12 +27,18 @@ class BaseError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
const createApi = async (services: Services = new Services()) => {
|
||||
const createApi = async (runtime: StashRuntime = new StashRuntime()) => {
|
||||
runtime.documents.subscribe({
|
||||
filter: "metadata.foo = 'bar'",
|
||||
fn: (document) => {
|
||||
// console.log(document);
|
||||
},
|
||||
});
|
||||
const app = fastify().withTypeProvider<ZodTypeProvider>();
|
||||
app.setValidatorCompiler(validatorCompiler);
|
||||
app.setSerializerCompiler(serializerCompiler);
|
||||
|
||||
app.decorate('services', services);
|
||||
app.decorate('runtime', runtime);
|
||||
|
||||
app.register(fastifyCors);
|
||||
app.register(fastifySwagger, {
|
||||
@@ -42,10 +49,20 @@ const createApi = async (services: Services = new Services()) => {
|
||||
},
|
||||
},
|
||||
transform: jsonSchemaTransform,
|
||||
transformObject: jsonSchemaTransformObject,
|
||||
});
|
||||
|
||||
await app.register(import('@scalar/fastify-api-reference'), {
|
||||
await app.register(scalar, {
|
||||
routePrefix: '/docs',
|
||||
configuration: {
|
||||
pageTitle: 'Foo',
|
||||
title: 'Hello World!',
|
||||
telemetry: false,
|
||||
hideClientButton: true,
|
||||
theme: 'laserwave',
|
||||
persistAuth: true,
|
||||
orderRequiredPropertiesFirst: false,
|
||||
},
|
||||
});
|
||||
|
||||
app.setErrorHandler((err, req, reply) => {
|
||||
@@ -92,8 +109,7 @@ const createApi = async (services: Services = new Services()) => {
|
||||
});
|
||||
|
||||
app.addHook('onReady', async () => {
|
||||
const warmupService = app.services.get(WarmupService);
|
||||
await warmupService.ensure();
|
||||
app.runtime.warmup.ensure();
|
||||
});
|
||||
|
||||
await app.register(systemEndpoints, { prefix: '/system' });
|
||||
|
||||
@@ -1,37 +1,36 @@
|
||||
import { StashRuntime, type DocumentUpsert } from '@morten-olsen/stash-runtime';
|
||||
|
||||
import { createApi } from './api.js';
|
||||
import { DocumentsService, type DocumentUpsert } from './services/documents/documents.ts';
|
||||
import { Services } from './utils/utils.services.ts';
|
||||
|
||||
const services = new Services();
|
||||
const server = await createApi(services);
|
||||
const runtime = new StashRuntime();
|
||||
const server = await createApi(runtime);
|
||||
|
||||
const documentsService = services.get(DocumentsService);
|
||||
const documents: DocumentUpsert[] = [
|
||||
{
|
||||
metadata: {
|
||||
foo: 'bar',
|
||||
},
|
||||
type: 'demo',
|
||||
content: 'the cat is yellow',
|
||||
text: 'the cat is yellow',
|
||||
},
|
||||
{
|
||||
metadata: {
|
||||
foo: 'bar',
|
||||
},
|
||||
type: 'demo',
|
||||
content: 'the dog is blue',
|
||||
text: 'the dog is blue',
|
||||
},
|
||||
{
|
||||
metadata: {
|
||||
foo: 'baz',
|
||||
},
|
||||
source: 'test',
|
||||
content: 'the pig says hi',
|
||||
text: 'the pig says hi',
|
||||
type: 'demo',
|
||||
},
|
||||
];
|
||||
|
||||
await Promise.all(documents.map((document) => documentsService.upsert(document)));
|
||||
await Promise.all(documents.map((document) => runtime.documents.upsert(document)));
|
||||
|
||||
await server.listen({
|
||||
port: 3400,
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { documentChunkFilterSchema, documentChunksFindResultSchema } from '@morten-olsen/stash-runtime';
|
||||
import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
|
||||
|
||||
import {
|
||||
documentChunkFilterSchema,
|
||||
documentChunksFindResultSchema,
|
||||
DocumentChunksService,
|
||||
} from '#root/services/document-chunks/document-chunks.ts';
|
||||
|
||||
const documentChunkFilterEndpoints: FastifyPluginAsyncZod = async (instance) => {
|
||||
instance.route({
|
||||
method: 'POST',
|
||||
@@ -20,9 +15,8 @@ const documentChunkFilterEndpoints: FastifyPluginAsyncZod = async (instance) =>
|
||||
},
|
||||
},
|
||||
handler: async (req, reply) => {
|
||||
const { services } = instance;
|
||||
const documentChunksService = services.get(DocumentChunksService);
|
||||
const response = await documentChunksService.find(req.body);
|
||||
const { runtime } = instance;
|
||||
const response = await runtime.documentChunks.find(req.body);
|
||||
await reply.send(response);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { documentFilterSchema, documentFindResultSchema } from '@morten-olsen/stash-runtime';
|
||||
import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
|
||||
|
||||
import {
|
||||
documentFilterSchema,
|
||||
documentFindResultSchema,
|
||||
DocumentsService,
|
||||
} from '#root/services/documents/documents.ts';
|
||||
|
||||
const documentFilterEndpoints: FastifyPluginAsyncZod = async (instance) => {
|
||||
instance.route({
|
||||
method: 'POST',
|
||||
@@ -20,9 +15,8 @@ const documentFilterEndpoints: FastifyPluginAsyncZod = async (instance) => {
|
||||
},
|
||||
},
|
||||
handler: async (req, reply) => {
|
||||
const { services } = instance;
|
||||
const documentsService = services.get(DocumentsService);
|
||||
const response = await documentsService.find(req.body);
|
||||
const { runtime } = instance;
|
||||
const response = await runtime.documents.find(req.body);
|
||||
await reply.send(response);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { documentUpsertResultSchema, documentUpsertSchema } from '@morten-olsen/stash-runtime';
|
||||
import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
|
||||
|
||||
import {
|
||||
DocumentsService,
|
||||
documentUpsertResultSchema,
|
||||
documentUpsertSchema,
|
||||
} from '#root/services/documents/documents.ts';
|
||||
|
||||
const documentEndpoints: FastifyPluginAsyncZod = async (instance) => {
|
||||
instance.route({
|
||||
method: 'POST',
|
||||
@@ -20,9 +15,8 @@ const documentEndpoints: FastifyPluginAsyncZod = async (instance) => {
|
||||
},
|
||||
},
|
||||
handler: async (req, reply) => {
|
||||
const { services } = instance;
|
||||
const documentsService = services.get(DocumentsService);
|
||||
const response = await documentsService.upsert(req.body);
|
||||
const { runtime } = instance;
|
||||
const response = await runtime.documents.upsert(req.body);
|
||||
await reply.send(response);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { DatabaseService } from '#root/services/database/database.ts';
|
||||
|
||||
const systemEndpoints: FastifyPluginAsyncZod = async (instance) => {
|
||||
instance.route({
|
||||
method: 'GET',
|
||||
@@ -18,9 +16,8 @@ const systemEndpoints: FastifyPluginAsyncZod = async (instance) => {
|
||||
},
|
||||
},
|
||||
handler: async (_, reply) => {
|
||||
const { services } = instance;
|
||||
const databaseService = services.get(DatabaseService);
|
||||
const db = await databaseService.getInstance();
|
||||
const { runtime } = instance;
|
||||
const db = await runtime.database.getInstance();
|
||||
await db.raw('SELECT 1=1');
|
||||
await reply.send({
|
||||
status: 'ok',
|
||||
|
||||
4
packages/server/src/global.d.ts
vendored
4
packages/server/src/global.d.ts
vendored
@@ -1,5 +1,5 @@
|
||||
import 'fastify';
|
||||
import type { Services } from './utils/utils.services.ts';
|
||||
import type { StashRuntime } from '@morten-olsen/stash-runtime';
|
||||
|
||||
// eslint-disable-next-line
|
||||
declare type ExplicitAny = any;
|
||||
@@ -7,6 +7,6 @@ declare type ExplicitAny = any;
|
||||
declare module 'fastify' {
|
||||
// eslint-disable-next-line
|
||||
export interface FastifyInstance {
|
||||
services: Services;
|
||||
runtime: StashRuntime;
|
||||
}
|
||||
}
|
||||
|
||||
11
packages/server/src/preload-data.ts
Normal file
11
packages/server/src/preload-data.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { mkdir } from 'fs/promises';
|
||||
import { resolve } from 'path';
|
||||
|
||||
import { preloadModel, setModelLocation } from '@morten-olsen/stash-runtime';
|
||||
|
||||
if (process.env.MODEL_DIR) {
|
||||
const modelDir = resolve(process.env.MODEL_DIR);
|
||||
await mkdir(modelDir, { recursive: true });
|
||||
setModelLocation(modelDir);
|
||||
await preloadModel();
|
||||
}
|
||||
@@ -1,202 +0,0 @@
|
||||
import type { Token } from './query-parser.types.ts';
|
||||
|
||||
class Lexer {
|
||||
#input: string;
|
||||
#position = 0;
|
||||
#tokens: Token[] = [];
|
||||
|
||||
constructor(input: string) {
|
||||
this.#input = input;
|
||||
}
|
||||
|
||||
#skipWhitespace = (): void => {
|
||||
while (this.#position < this.#input.length && /\s/.test(this.#input[this.#position])) {
|
||||
this.#position++;
|
||||
}
|
||||
};
|
||||
|
||||
#nextToken = (): Token | null => {
|
||||
const char = this.#input[this.#position];
|
||||
const startPosition = this.#position;
|
||||
|
||||
// Single character tokens
|
||||
if (char === '(') {
|
||||
this.#position++;
|
||||
return { type: 'LPAREN', value: '(', position: startPosition };
|
||||
}
|
||||
if (char === ')') {
|
||||
this.#position++;
|
||||
return { type: 'RPAREN', value: ')', position: startPosition };
|
||||
}
|
||||
if (char === ',') {
|
||||
this.#position++;
|
||||
return { type: 'COMMA', value: ',', position: startPosition };
|
||||
}
|
||||
if (char === '.') {
|
||||
this.#position++;
|
||||
return { type: 'DOT', value: '.', position: startPosition };
|
||||
}
|
||||
|
||||
// Two-character operators
|
||||
if (char === '!' && this.#input[this.#position + 1] === '=') {
|
||||
this.#position += 2;
|
||||
return { type: 'NOT_EQUALS', value: '!=', position: startPosition };
|
||||
}
|
||||
if (char === '>' && this.#input[this.#position + 1] === '=') {
|
||||
this.#position += 2;
|
||||
return { type: 'GREATER_THAN_OR_EQUAL', value: '>=', position: startPosition };
|
||||
}
|
||||
if (char === '<' && this.#input[this.#position + 1] === '=') {
|
||||
this.#position += 2;
|
||||
return { type: 'LESS_THAN_OR_EQUAL', value: '<=', position: startPosition };
|
||||
}
|
||||
|
||||
// Single character operators
|
||||
if (char === '=') {
|
||||
this.#position++;
|
||||
return { type: 'EQUALS', value: '=', position: startPosition };
|
||||
}
|
||||
if (char === '>') {
|
||||
this.#position++;
|
||||
return { type: 'GREATER_THAN', value: '>', position: startPosition };
|
||||
}
|
||||
if (char === '<') {
|
||||
this.#position++;
|
||||
return { type: 'LESS_THAN', value: '<', position: startPosition };
|
||||
}
|
||||
|
||||
// String literal
|
||||
if (char === "'") {
|
||||
return this.#readString();
|
||||
}
|
||||
|
||||
// Number
|
||||
if (/[0-9]/.test(char) || (char === '-' && /[0-9]/.test(this.#input[this.#position + 1]))) {
|
||||
return this.#readNumber();
|
||||
}
|
||||
|
||||
// Identifier or keyword
|
||||
if (/[a-zA-Z_]/.test(char)) {
|
||||
return this.#readIdentifierOrKeyword();
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected character '${char}' at position ${this.#position}`);
|
||||
};
|
||||
|
||||
#readString = (): Token => {
|
||||
const startPosition = this.#position;
|
||||
this.#position++; // Skip opening quote
|
||||
let value = '';
|
||||
|
||||
while (this.#position < this.#input.length) {
|
||||
const char = this.#input[this.#position];
|
||||
if (char === "'") {
|
||||
// Check for escaped quote
|
||||
if (this.#input[this.#position + 1] === "'") {
|
||||
value += "'";
|
||||
this.#position += 2;
|
||||
} else {
|
||||
this.#position++; // Skip closing quote
|
||||
return { type: 'STRING', value, position: startPosition };
|
||||
}
|
||||
} else {
|
||||
value += char;
|
||||
this.#position++;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Unterminated string starting at position ${startPosition}`);
|
||||
};
|
||||
|
||||
#readNumber = (): Token => {
|
||||
const startPosition = this.#position;
|
||||
let value = '';
|
||||
|
||||
// Optional minus sign
|
||||
if (this.#input[this.#position] === '-') {
|
||||
value += '-';
|
||||
this.#position++;
|
||||
}
|
||||
|
||||
// Integer part
|
||||
while (this.#position < this.#input.length && /[0-9]/.test(this.#input[this.#position])) {
|
||||
value += this.#input[this.#position];
|
||||
this.#position++;
|
||||
}
|
||||
|
||||
// Decimal part
|
||||
if (this.#input[this.#position] === '.' && /[0-9]/.test(this.#input[this.#position + 1])) {
|
||||
value += '.';
|
||||
this.#position++;
|
||||
while (this.#position < this.#input.length && /[0-9]/.test(this.#input[this.#position])) {
|
||||
value += this.#input[this.#position];
|
||||
this.#position++;
|
||||
}
|
||||
}
|
||||
|
||||
// Scientific notation
|
||||
if (this.#input[this.#position] === 'e' || this.#input[this.#position] === 'E') {
|
||||
value += this.#input[this.#position];
|
||||
this.#position++;
|
||||
if (this.#input[this.#position] === '+' || this.#input[this.#position] === '-') {
|
||||
value += this.#input[this.#position];
|
||||
this.#position++;
|
||||
}
|
||||
while (this.#position < this.#input.length && /[0-9]/.test(this.#input[this.#position])) {
|
||||
value += this.#input[this.#position];
|
||||
this.#position++;
|
||||
}
|
||||
}
|
||||
|
||||
return { type: 'NUMBER', value, position: startPosition };
|
||||
};
|
||||
|
||||
#readIdentifierOrKeyword = (): Token => {
|
||||
const startPosition = this.#position;
|
||||
let value = '';
|
||||
|
||||
while (this.#position < this.#input.length && /[a-zA-Z0-9_]/.test(this.#input[this.#position])) {
|
||||
value += this.#input[this.#position];
|
||||
this.#position++;
|
||||
}
|
||||
|
||||
const upperValue = value.toUpperCase();
|
||||
|
||||
// Keywords
|
||||
switch (upperValue) {
|
||||
case 'AND':
|
||||
return { type: 'AND', value, position: startPosition };
|
||||
case 'OR':
|
||||
return { type: 'OR', value, position: startPosition };
|
||||
case 'LIKE':
|
||||
return { type: 'LIKE', value, position: startPosition };
|
||||
case 'NOT':
|
||||
return { type: 'NOT', value, position: startPosition };
|
||||
case 'IN':
|
||||
return { type: 'IN', value, position: startPosition };
|
||||
case 'IS':
|
||||
return { type: 'IS', value, position: startPosition };
|
||||
case 'NULL':
|
||||
return { type: 'NULL', value, position: startPosition };
|
||||
default:
|
||||
return { type: 'IDENTIFIER', value, position: startPosition };
|
||||
}
|
||||
};
|
||||
|
||||
public tokenize = (): Token[] => {
|
||||
while (this.#position < this.#input.length) {
|
||||
this.#skipWhitespace();
|
||||
if (this.#position >= this.#input.length) break;
|
||||
|
||||
const token = this.#nextToken();
|
||||
if (token) {
|
||||
this.#tokens.push(token);
|
||||
}
|
||||
}
|
||||
|
||||
this.#tokens.push({ type: 'EOF', value: '', position: this.#position });
|
||||
return this.#tokens;
|
||||
};
|
||||
}
|
||||
|
||||
export { Lexer };
|
||||
@@ -1,317 +0,0 @@
|
||||
import { Lexer } from './query-parser.lexer.ts';
|
||||
import type { Token, TokenType } from './query-parser.types.ts';
|
||||
|
||||
import type { QueryConditionText, QueryConditionNumber, QueryFilter, QueryCondition } from '#root/utils/utils.query.ts';
|
||||
|
||||
class Parser {
|
||||
#tokens: Token[] = [];
|
||||
#position = 0;
|
||||
|
||||
#current = (): Token => {
|
||||
return this.#tokens[this.#position];
|
||||
};
|
||||
|
||||
#advance = (): Token => {
|
||||
const token = this.#current();
|
||||
this.#position++;
|
||||
return token;
|
||||
};
|
||||
|
||||
#expect = (type: TokenType): Token => {
|
||||
const token = this.#current();
|
||||
if (token.type !== type) {
|
||||
throw new Error(`Expected ${type} but got ${token.type} at position ${token.position}`);
|
||||
}
|
||||
return this.#advance();
|
||||
};
|
||||
|
||||
#parseExpression = (): QueryFilter => {
|
||||
return this.#parseOr();
|
||||
};
|
||||
|
||||
#parseOr = (): QueryFilter => {
|
||||
let left = this.#parseAnd();
|
||||
|
||||
while (this.#current().type === 'OR') {
|
||||
this.#advance();
|
||||
const right = this.#parseAnd();
|
||||
left = this.#combineWithOperator(left, right, 'or');
|
||||
}
|
||||
|
||||
return left;
|
||||
};
|
||||
|
||||
#parseAnd = (): QueryFilter => {
|
||||
let left = this.#parsePrimary();
|
||||
|
||||
while (this.#current().type === 'AND') {
|
||||
this.#advance();
|
||||
const right = this.#parsePrimary();
|
||||
left = this.#combineWithOperator(left, right, 'and');
|
||||
}
|
||||
|
||||
return left;
|
||||
};
|
||||
|
||||
#combineWithOperator = (left: QueryFilter, right: QueryFilter, operator: 'and' | 'or'): QueryFilter => {
|
||||
// If left is already an operator of the same type, add to its conditions
|
||||
if (left.type === 'operator' && left.operator === operator) {
|
||||
return {
|
||||
type: 'operator',
|
||||
operator,
|
||||
conditions: [...left.conditions, right],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'operator',
|
||||
operator,
|
||||
conditions: [left, right],
|
||||
};
|
||||
};
|
||||
|
||||
#parsePrimary = (): QueryFilter => {
|
||||
// Handle parenthesized expressions
|
||||
if (this.#current().type === 'LPAREN') {
|
||||
this.#advance();
|
||||
const expr = this.#parseExpression();
|
||||
this.#expect('RPAREN');
|
||||
return expr;
|
||||
}
|
||||
|
||||
// Must be a condition
|
||||
return this.#parseCondition();
|
||||
};
|
||||
|
||||
#parseCondition = (): QueryCondition => {
|
||||
const field = this.#parseField();
|
||||
|
||||
const token = this.#current();
|
||||
|
||||
// IS NULL / IS NOT NULL
|
||||
if (token.type === 'IS') {
|
||||
this.#advance();
|
||||
const isNot = this.#current().type === 'NOT';
|
||||
if (isNot) {
|
||||
this.#advance();
|
||||
}
|
||||
this.#expect('NULL');
|
||||
|
||||
// IS NULL / IS NOT NULL could be either text or number - default to text
|
||||
return {
|
||||
type: 'text',
|
||||
field,
|
||||
conditions: isNot ? { notEqual: undefined, equal: undefined } : { equal: null },
|
||||
} satisfies QueryConditionText;
|
||||
}
|
||||
|
||||
// NOT IN / NOT LIKE
|
||||
if (token.type === 'NOT') {
|
||||
this.#advance();
|
||||
const nextToken = this.#current();
|
||||
|
||||
if (nextToken.type === 'IN') {
|
||||
this.#advance();
|
||||
return this.#parseInCondition(field, true);
|
||||
}
|
||||
|
||||
if (nextToken.type === 'LIKE') {
|
||||
this.#advance();
|
||||
const pattern = this.#expect('STRING').value;
|
||||
return {
|
||||
type: 'text',
|
||||
field,
|
||||
conditions: { notLike: pattern },
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Expected IN or LIKE after NOT at position ${nextToken.position}`);
|
||||
}
|
||||
|
||||
// IN
|
||||
if (token.type === 'IN') {
|
||||
this.#advance();
|
||||
return this.#parseInCondition(field, false);
|
||||
}
|
||||
|
||||
// LIKE
|
||||
if (token.type === 'LIKE') {
|
||||
this.#advance();
|
||||
const pattern = this.#expect('STRING').value;
|
||||
return {
|
||||
type: 'text',
|
||||
field,
|
||||
conditions: { like: pattern },
|
||||
};
|
||||
}
|
||||
|
||||
// Comparison operators
|
||||
if (token.type === 'EQUALS') {
|
||||
this.#advance();
|
||||
return this.#parseValueCondition(field, 'equals');
|
||||
}
|
||||
|
||||
if (token.type === 'NOT_EQUALS') {
|
||||
this.#advance();
|
||||
return this.#parseValueCondition(field, 'notEquals');
|
||||
}
|
||||
|
||||
if (token.type === 'GREATER_THAN') {
|
||||
this.#advance();
|
||||
const value = this.#parseNumber();
|
||||
return {
|
||||
type: 'number',
|
||||
field,
|
||||
conditions: { greaterThan: value },
|
||||
};
|
||||
}
|
||||
|
||||
if (token.type === 'GREATER_THAN_OR_EQUAL') {
|
||||
this.#advance();
|
||||
const value = this.#parseNumber();
|
||||
return {
|
||||
type: 'number',
|
||||
field,
|
||||
conditions: { greaterThanOrEqual: value },
|
||||
};
|
||||
}
|
||||
|
||||
if (token.type === 'LESS_THAN') {
|
||||
this.#advance();
|
||||
const value = this.#parseNumber();
|
||||
return {
|
||||
type: 'number',
|
||||
field,
|
||||
conditions: { lessThan: value },
|
||||
};
|
||||
}
|
||||
|
||||
if (token.type === 'LESS_THAN_OR_EQUAL') {
|
||||
this.#advance();
|
||||
const value = this.#parseNumber();
|
||||
return {
|
||||
type: 'number',
|
||||
field,
|
||||
conditions: { lessThanOrEqual: value },
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected token '${token.value}' at position ${token.position}`);
|
||||
};
|
||||
|
||||
#parseField = (): string[] => {
|
||||
const parts: string[] = [];
|
||||
parts.push(this.#expect('IDENTIFIER').value);
|
||||
|
||||
while (this.#current().type === 'DOT') {
|
||||
this.#advance();
|
||||
parts.push(this.#expect('IDENTIFIER').value);
|
||||
}
|
||||
|
||||
return parts;
|
||||
};
|
||||
|
||||
#parseValueCondition = (field: string[], operator: 'equals' | 'notEquals'): QueryCondition => {
|
||||
const token = this.#current();
|
||||
|
||||
if (token.type === 'STRING') {
|
||||
this.#advance();
|
||||
const textCondition: QueryConditionText = {
|
||||
type: 'text',
|
||||
field,
|
||||
conditions: operator === 'equals' ? { equal: token.value } : { notEqual: token.value },
|
||||
};
|
||||
return textCondition;
|
||||
}
|
||||
|
||||
if (token.type === 'NUMBER') {
|
||||
this.#advance();
|
||||
const value = parseFloat(token.value);
|
||||
const numCondition: QueryConditionNumber = {
|
||||
type: 'number',
|
||||
field,
|
||||
conditions: operator === 'equals' ? { equals: value } : { notEquals: value },
|
||||
};
|
||||
return numCondition;
|
||||
}
|
||||
|
||||
if (token.type === 'NULL') {
|
||||
this.#advance();
|
||||
// NULL equality - default to text type
|
||||
return {
|
||||
type: 'text',
|
||||
field,
|
||||
conditions: operator === 'equals' ? { equal: null } : {},
|
||||
} as QueryConditionText;
|
||||
}
|
||||
|
||||
throw new Error(`Expected value but got ${token.type} at position ${token.position}`);
|
||||
};
|
||||
|
||||
#parseNumber = (): number => {
|
||||
const token = this.#expect('NUMBER');
|
||||
return parseFloat(token.value);
|
||||
};
|
||||
|
||||
#parseInCondition = (field: string[], isNot: boolean): QueryCondition => {
|
||||
this.#expect('LPAREN');
|
||||
|
||||
const firstToken = this.#current();
|
||||
|
||||
if (firstToken.type === 'STRING') {
|
||||
// Text IN
|
||||
const values: string[] = [];
|
||||
values.push(this.#advance().value);
|
||||
|
||||
while (this.#current().type === 'COMMA') {
|
||||
this.#advance();
|
||||
values.push(this.#expect('STRING').value);
|
||||
}
|
||||
|
||||
this.#expect('RPAREN');
|
||||
|
||||
return {
|
||||
type: 'text',
|
||||
field,
|
||||
conditions: isNot ? { notIn: values } : { in: values },
|
||||
};
|
||||
}
|
||||
|
||||
if (firstToken.type === 'NUMBER') {
|
||||
// Numeric IN
|
||||
const values: number[] = [];
|
||||
values.push(parseFloat(this.#advance().value));
|
||||
|
||||
while (this.#current().type === 'COMMA') {
|
||||
this.#advance();
|
||||
values.push(parseFloat(this.#expect('NUMBER').value));
|
||||
}
|
||||
|
||||
this.#expect('RPAREN');
|
||||
|
||||
return {
|
||||
type: 'number',
|
||||
field,
|
||||
conditions: isNot ? { notIn: values } : { in: values },
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Expected STRING or NUMBER in IN list at position ${firstToken.position}`);
|
||||
};
|
||||
|
||||
public parse(input: string): QueryFilter {
|
||||
const lexer = new Lexer(input);
|
||||
this.#tokens = lexer.tokenize();
|
||||
this.#position = 0;
|
||||
|
||||
const result = this.#parseExpression();
|
||||
|
||||
if (this.#current().type !== 'EOF') {
|
||||
throw new Error(`Unexpected token '${this.#current().value}' at position ${this.#current().position}`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export { Parser };
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Stringifier } from './query-parser.stringifier.ts';
|
||||
import { Parser } from './query-parser.parser.ts';
|
||||
|
||||
import type { QueryFilter } from '#root/utils/utils.query.ts';
|
||||
|
||||
class QueryParser {
|
||||
private parser = new Parser();
|
||||
private stringifier = new Stringifier();
|
||||
|
||||
public parse = (input: string): QueryFilter => {
|
||||
return this.parser.parse(input);
|
||||
};
|
||||
|
||||
public stringify = (filter: QueryFilter): string => {
|
||||
return this.stringifier.stringify(filter);
|
||||
};
|
||||
}
|
||||
|
||||
export { QueryParser };
|
||||
@@ -1,30 +0,0 @@
|
||||
type TokenType =
|
||||
| 'IDENTIFIER'
|
||||
| 'STRING'
|
||||
| 'NUMBER'
|
||||
| 'AND'
|
||||
| 'OR'
|
||||
| 'LIKE'
|
||||
| 'NOT'
|
||||
| 'IN'
|
||||
| 'IS'
|
||||
| 'NULL'
|
||||
| 'EQUALS'
|
||||
| 'NOT_EQUALS'
|
||||
| 'GREATER_THAN'
|
||||
| 'GREATER_THAN_OR_EQUAL'
|
||||
| 'LESS_THAN'
|
||||
| 'LESS_THAN_OR_EQUAL'
|
||||
| 'LPAREN'
|
||||
| 'RPAREN'
|
||||
| 'COMMA'
|
||||
| 'DOT'
|
||||
| 'EOF';
|
||||
|
||||
type Token = {
|
||||
type: TokenType;
|
||||
value: string;
|
||||
position: number;
|
||||
};
|
||||
|
||||
export type { TokenType, Token };
|
||||
@@ -1,54 +0,0 @@
|
||||
import knex, { type Knex } from 'knex';
|
||||
import ClientPgLite from 'knex-pglite';
|
||||
import { PGlite } from '@electric-sql/pglite';
|
||||
import { vector } from '@electric-sql/pglite/vector';
|
||||
|
||||
import { migrationSource } from './migrations/migrations.ts';
|
||||
|
||||
import { destroy, Services } from '#root/utils/utils.services.ts';
|
||||
|
||||
class DatabaseService {
|
||||
#services: Services;
|
||||
#instance?: Promise<Knex>;
|
||||
|
||||
constructor(services: Services) {
|
||||
this.#services = services;
|
||||
}
|
||||
|
||||
#setup = async () => {
|
||||
const pglite = new PGlite({
|
||||
extensions: { vector },
|
||||
});
|
||||
|
||||
const instance = knex({
|
||||
client: ClientPgLite,
|
||||
dialect: 'postgres',
|
||||
connection: () => ({ pglite }) as object,
|
||||
});
|
||||
await instance.raw(`CREATE EXTENSION IF NOT EXISTS vector`);
|
||||
|
||||
await instance.migrate.latest({
|
||||
migrationSource: migrationSource({ services: this.#services }),
|
||||
});
|
||||
|
||||
return instance;
|
||||
};
|
||||
|
||||
public getInstance = () => {
|
||||
if (!this.#instance) {
|
||||
this.#instance = this.#setup();
|
||||
}
|
||||
return this.#instance;
|
||||
};
|
||||
|
||||
[destroy] = async () => {
|
||||
if (!this.#instance) {
|
||||
return;
|
||||
}
|
||||
const instance = await this.#instance;
|
||||
await instance.destroy();
|
||||
};
|
||||
}
|
||||
|
||||
export { type TableRows, tableNames } from './migrations/migrations.ts';
|
||||
export { DatabaseService };
|
||||
@@ -1,33 +0,0 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { createListResultSchema } from '#root/utils/utils.schema.ts';
|
||||
import { queryFilterSchema } from '#root/utils/utils.query.ts';
|
||||
|
||||
const documentChunkSchema = z.object({
|
||||
id: z.string(),
|
||||
owner: z.string(),
|
||||
content: z.string(),
|
||||
metadata: z.unknown(),
|
||||
});
|
||||
|
||||
type DocumentChunk = z.infer<typeof documentChunkSchema>;
|
||||
|
||||
const documentChunkFilterSchema = z.object({
|
||||
limit: z.number().default(20),
|
||||
offset: z.number().default(0),
|
||||
semanticText: z.string().optional(),
|
||||
conditions: z.union([queryFilterSchema, z.string()]).optional(),
|
||||
});
|
||||
|
||||
type DocumentChunkFilter = z.infer<typeof documentChunkFilterSchema>;
|
||||
|
||||
const documentChunksFindResultSchema = createListResultSchema(
|
||||
documentChunkSchema.extend({
|
||||
distance: z.number().optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
type DocumentChunksFindResult = z.infer<typeof documentChunksFindResultSchema>;
|
||||
|
||||
export type { DocumentChunk, DocumentChunkFilter, DocumentChunksFindResult };
|
||||
export { documentChunkSchema, documentChunkFilterSchema, documentChunksFindResultSchema };
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { TableRows } from '../database/database.ts';
|
||||
|
||||
import type { DocumentChunk } from './document-chunks.schemas.ts';
|
||||
|
||||
const mapFromDocumentChunkRow = (
|
||||
row: TableRows['documentChunks'] & {
|
||||
metadata: unknown;
|
||||
},
|
||||
): DocumentChunk => ({
|
||||
...row,
|
||||
});
|
||||
|
||||
export { mapFromDocumentChunkRow };
|
||||
@@ -1,12 +0,0 @@
|
||||
import type { TableRows } from '../database/database.ts';
|
||||
|
||||
import type { Document } from './documents.schemas.ts';
|
||||
|
||||
const mapFromDocumentRow = (row: TableRows['documents']): Document => ({
|
||||
...row,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
deletedAt: row.deletedAt?.toISOString() || null,
|
||||
});
|
||||
|
||||
export { mapFromDocumentRow };
|
||||
@@ -1,178 +0,0 @@
|
||||
import { DatabaseService, tableNames, type TableRows } from '../database/database.ts';
|
||||
import { SplittingService } from '../splitter/splitter.ts';
|
||||
|
||||
import type {
|
||||
Document,
|
||||
DocumentFilter,
|
||||
DocumentFindResult,
|
||||
DocumentUpsert,
|
||||
DocumentUpsertResult,
|
||||
} from './documents.schemas.ts';
|
||||
import { mapFromDocumentRow } from './documents.mapping.ts';
|
||||
|
||||
import { EventEmitter } from '#root/utils/utils.event-emitter.ts';
|
||||
import type { Services } from '#root/utils/utils.services.ts';
|
||||
import { compareObjectKeys } from '#root/utils/utils.compare.ts';
|
||||
import { applyQueryFilter } from '#root/utils/utils.query.ts';
|
||||
import { QueryParser } from '#root/query-parser/query-parser.ts';
|
||||
|
||||
type DocumentsServiceEvents = {
|
||||
upserted: (document: Document) => void;
|
||||
inserted: (document: Document) => void;
|
||||
updated: (document: Document) => void;
|
||||
};
|
||||
|
||||
class DocumentsService extends EventEmitter<DocumentsServiceEvents> {
|
||||
#services: Services;
|
||||
|
||||
constructor(services: Services) {
|
||||
super();
|
||||
this.#services = services;
|
||||
}
|
||||
|
||||
public find = async (filter: DocumentFilter): Promise<DocumentFindResult> => {
|
||||
const databaseService = this.#services.get(DatabaseService);
|
||||
const db = await databaseService.getInstance();
|
||||
let query = db<TableRows['documents']>(tableNames.documents);
|
||||
if (filter) {
|
||||
const parser = this.#services.get(QueryParser);
|
||||
query = applyQueryFilter(
|
||||
query,
|
||||
typeof filter.condition === 'string' ? parser.parse(filter.condition) : filter.condition,
|
||||
);
|
||||
}
|
||||
query = query.limit(filter.limit).offset(filter.offset);
|
||||
const items = await query;
|
||||
return {
|
||||
items: items.map(mapFromDocumentRow),
|
||||
};
|
||||
};
|
||||
|
||||
public get = async (id: string): Promise<Document> => {
|
||||
const databaseService = this.#services.get(DatabaseService);
|
||||
const db = await databaseService.getInstance();
|
||||
const [item] = await db<TableRows['documents']>(tableNames.documents).where('id', id).limit(1);
|
||||
return mapFromDocumentRow(item);
|
||||
};
|
||||
|
||||
public remove = async (id: string): Promise<void> => {
|
||||
const databaseService = this.#services.get(DatabaseService);
|
||||
const db = await databaseService.getInstance();
|
||||
await db<TableRows['documents']>(tableNames.documents).where('id', id).delete();
|
||||
};
|
||||
|
||||
public upsert = async (document: DocumentUpsert): Promise<DocumentUpsertResult> => {
|
||||
const databaseService = this.#services.get(DatabaseService);
|
||||
const db = await databaseService.getInstance();
|
||||
|
||||
const result = await db.transaction(async (trx) => {
|
||||
let id = document.id || crypto.randomUUID();
|
||||
if (document.source && document.sourceId) {
|
||||
const [currentSourceDocument] = await trx<TableRows['documents']>(tableNames.documents)
|
||||
.where('source', document.source)
|
||||
.andWhere('sourceId', document.sourceId)
|
||||
.limit(1);
|
||||
if (currentSourceDocument) {
|
||||
id = currentSourceDocument.id;
|
||||
}
|
||||
}
|
||||
const now = new Date();
|
||||
const [current] = await trx<TableRows['documents']>(tableNames.documents).where('id', id).limit(1);
|
||||
if (current) {
|
||||
if (
|
||||
compareObjectKeys(current, document, [
|
||||
'sourceId',
|
||||
'source',
|
||||
'content',
|
||||
'contentType',
|
||||
'searchText',
|
||||
'type',
|
||||
'typeVersion',
|
||||
'metadata',
|
||||
])
|
||||
) {
|
||||
return {
|
||||
id,
|
||||
action: 'skipped',
|
||||
document: mapFromDocumentRow(current),
|
||||
} as const;
|
||||
}
|
||||
await trx<TableRows['documents']>(tableNames.documents)
|
||||
.update({
|
||||
...document,
|
||||
id,
|
||||
updatedAt: now,
|
||||
})
|
||||
.where('id', id);
|
||||
const resultDocument: Document = mapFromDocumentRow({
|
||||
...current,
|
||||
...document,
|
||||
id,
|
||||
});
|
||||
this.emit('updated', resultDocument);
|
||||
this.emit('upserted', resultDocument);
|
||||
return {
|
||||
id,
|
||||
action: 'updated',
|
||||
document: resultDocument,
|
||||
} as const;
|
||||
} else {
|
||||
await trx<TableRows['documents']>(tableNames.documents).insert({
|
||||
metadata: {},
|
||||
type: 'raw',
|
||||
...document,
|
||||
id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
const resultDocument: Document = mapFromDocumentRow({
|
||||
type: 'raw',
|
||||
owner: null,
|
||||
contentType: null,
|
||||
content: null,
|
||||
source: null,
|
||||
sourceId: null,
|
||||
typeVersion: null,
|
||||
searchText: null,
|
||||
metadata: {},
|
||||
...document,
|
||||
deletedAt: null,
|
||||
id,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
this.emit('inserted', resultDocument);
|
||||
this.emit('upserted', resultDocument);
|
||||
return {
|
||||
id,
|
||||
action: 'inserted',
|
||||
document: resultDocument,
|
||||
} as const;
|
||||
}
|
||||
});
|
||||
|
||||
if (result.action !== 'skipped') {
|
||||
await db.transaction(async (trx) => {
|
||||
await trx<TableRows['documentChunks']>(tableNames.documentChunks).delete().where('owner', result.id);
|
||||
const splittingService = this.#services.get(SplittingService);
|
||||
const chunks = await splittingService.chunk(result.document);
|
||||
if (chunks.length > 0) {
|
||||
await trx<TableRows['documentChunks']>(tableNames.documentChunks).insert(
|
||||
chunks.map((chunk) => ({
|
||||
id: crypto.randomUUID(),
|
||||
owner: result.id,
|
||||
content: chunk.content,
|
||||
embedding: chunk.vector.toSql(),
|
||||
embeddingModel: chunk.model,
|
||||
})),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
export * from './documents.schemas.ts';
|
||||
export { DocumentsService };
|
||||
@@ -1,7 +1,19 @@
|
||||
import { resolve } from 'path';
|
||||
import { mkdir } from 'fs/promises';
|
||||
|
||||
import { setModelLocation } from '@morten-olsen/stash-runtime';
|
||||
|
||||
import { createApi } from './api.js';
|
||||
|
||||
const server = await createApi();
|
||||
if (process.env.MODEL_DIR) {
|
||||
const modelDir = resolve(process.env.MODEL_DIR);
|
||||
await mkdir(modelDir, { recursive: true });
|
||||
setModelLocation(modelDir);
|
||||
}
|
||||
|
||||
const server = await createApi();
|
||||
await server.listen({
|
||||
port: 3400,
|
||||
host: process.env.SERVER_HOST,
|
||||
});
|
||||
console.log('Server started');
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import deepEqual from 'deep-equal';
|
||||
|
||||
const compareObjectKeys = <T extends Record<string, unknown>>(a: T, b: T, keys: (keyof T)[]) => {
|
||||
for (const key of keys) {
|
||||
const avalue = a[key];
|
||||
const bvalue = b[key];
|
||||
if (!deepEqual(avalue, bvalue)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export { compareObjectKeys };
|
||||
@@ -1,549 +0,0 @@
|
||||
import { type Knex } from 'knex';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Escapes a JSON key for use in PostgreSQL JSON operators.
|
||||
* Escapes single quotes by doubling them, which is the PostgreSQL standard.
|
||||
*/
|
||||
const escapeJsonKey = (key: string): string => {
|
||||
return key.replace(/'/g, "''");
|
||||
};
|
||||
|
||||
const getFieldSelector = (query: Knex.QueryBuilder, field: string[], tableName?: string) => {
|
||||
const baseColumn = field[0];
|
||||
if (field.length === 1) {
|
||||
return tableName ? `${tableName}.${baseColumn}` : baseColumn;
|
||||
}
|
||||
|
||||
const baseFieldRef = tableName ? query.client.ref(baseColumn).withSchema(tableName) : query.client.ref(baseColumn);
|
||||
const jsonPath = field.slice(1);
|
||||
let sqlExpression = baseFieldRef.toString();
|
||||
|
||||
for (let i = 0; i < jsonPath.length - 1; i++) {
|
||||
const escapedKey = escapeJsonKey(jsonPath[i]);
|
||||
sqlExpression += ` -> '${escapedKey}'`;
|
||||
}
|
||||
|
||||
const finalElement = jsonPath[jsonPath.length - 1];
|
||||
const escapedFinalKey = escapeJsonKey(finalElement);
|
||||
sqlExpression += ` ->> '${escapedFinalKey}'`;
|
||||
return query.client.raw(sqlExpression);
|
||||
};
|
||||
|
||||
const queryConditionTextSchema = z
|
||||
.object({
|
||||
type: z.literal('text'),
|
||||
tableName: z.string().optional(),
|
||||
field: z.array(z.string()),
|
||||
conditions: z.object({
|
||||
equal: z.string().nullish(),
|
||||
notEqual: z.string().optional(),
|
||||
like: z.string().optional(),
|
||||
notLike: z.string().optional(),
|
||||
in: z.array(z.string()).optional(),
|
||||
notIn: z.array(z.string()).optional(),
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
example: {
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'bar',
|
||||
},
|
||||
},
|
||||
examples: [
|
||||
{
|
||||
summary: 'Equal condition',
|
||||
value: {
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'bar',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
summary: 'Like condition',
|
||||
value: {
|
||||
type: 'text',
|
||||
field: ['content'],
|
||||
conditions: {
|
||||
like: '%cat%',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
summary: 'In condition',
|
||||
value: {
|
||||
type: 'text',
|
||||
field: ['type'],
|
||||
conditions: {
|
||||
in: ['demo', 'article', 'post'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
summary: 'Null check',
|
||||
value: {
|
||||
type: 'text',
|
||||
field: ['source'],
|
||||
conditions: {
|
||||
equal: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
type QueryConditionText = z.infer<typeof queryConditionTextSchema>;
|
||||
|
||||
const applyQueryConditionText = (query: Knex.QueryBuilder, { field, tableName, conditions }: QueryConditionText) => {
|
||||
const selector = getFieldSelector(query, field, tableName);
|
||||
if (conditions.equal) {
|
||||
query = query.where(selector, '=', conditions.equal);
|
||||
}
|
||||
if (conditions.notEqual) {
|
||||
query = query.where(selector, '<>', conditions.notEqual);
|
||||
}
|
||||
if (conditions.like) {
|
||||
query = query.whereLike(selector, conditions.like);
|
||||
}
|
||||
if (conditions.notLike) {
|
||||
query = query.not.whereLike(selector, conditions.notLike);
|
||||
}
|
||||
if (conditions.equal === null) {
|
||||
query = query.whereNull(selector);
|
||||
}
|
||||
if (conditions.notEqual === null) {
|
||||
query = query.whereNotNull(selector);
|
||||
}
|
||||
if (conditions.in) {
|
||||
query = query.whereIn(selector, conditions.in);
|
||||
}
|
||||
if (conditions.notIn) {
|
||||
query = query.whereNotIn(selector, conditions.notIn);
|
||||
}
|
||||
return query;
|
||||
};
|
||||
|
||||
const queryConditionNumberSchema = z
|
||||
.object({
|
||||
type: z.literal('number'),
|
||||
tableName: z.string().optional(),
|
||||
field: z.array(z.string()),
|
||||
conditions: z.object({
|
||||
equals: z.number().nullish(),
|
||||
notEquals: z.number().nullish(),
|
||||
greaterThan: z.number().optional(),
|
||||
greaterThanOrEqual: z.number().optional(),
|
||||
lessThan: z.number().optional(),
|
||||
lessThanOrEqual: z.number().optional(),
|
||||
in: z.array(z.number()).optional(),
|
||||
notIn: z.array(z.number()).optional(),
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
example: {
|
||||
type: 'number',
|
||||
field: ['typeVersion'],
|
||||
conditions: {
|
||||
equals: 1,
|
||||
},
|
||||
},
|
||||
examples: [
|
||||
{
|
||||
summary: 'Equals condition',
|
||||
value: {
|
||||
type: 'number',
|
||||
field: ['typeVersion'],
|
||||
conditions: {
|
||||
equals: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
summary: 'Greater than condition',
|
||||
value: {
|
||||
type: 'number',
|
||||
field: ['typeVersion'],
|
||||
conditions: {
|
||||
greaterThan: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
summary: 'Range condition',
|
||||
value: {
|
||||
type: 'number',
|
||||
field: ['typeVersion'],
|
||||
conditions: {
|
||||
greaterThanOrEqual: 1,
|
||||
lessThanOrEqual: 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
summary: 'In condition',
|
||||
value: {
|
||||
type: 'number',
|
||||
field: ['typeVersion'],
|
||||
conditions: {
|
||||
in: [1, 2, 3],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
type QueryConditionNumber = z.infer<typeof queryConditionNumberSchema>;
|
||||
|
||||
const applyQueryConditionNumber = (
|
||||
query: Knex.QueryBuilder,
|
||||
{ field, tableName, conditions }: QueryConditionNumber,
|
||||
) => {
|
||||
const selector = getFieldSelector(query, field, tableName);
|
||||
if (conditions.equals !== undefined && conditions.equals !== null) {
|
||||
query = query.where(selector, '=', conditions.equals);
|
||||
}
|
||||
if (conditions.notEquals !== undefined && conditions.notEquals !== null) {
|
||||
query = query.where(selector, '<>', conditions.notEquals);
|
||||
}
|
||||
if (conditions.equals === null) {
|
||||
query = query.whereNull(selector);
|
||||
}
|
||||
if (conditions.notEquals === null) {
|
||||
query = query.whereNotNull(selector);
|
||||
}
|
||||
if (conditions.greaterThan) {
|
||||
query = query.where(selector, '>', conditions.greaterThan);
|
||||
}
|
||||
if (conditions.greaterThanOrEqual) {
|
||||
query = query.where(selector, '>=', conditions.greaterThanOrEqual);
|
||||
}
|
||||
if (conditions.lessThan) {
|
||||
query = query.where(selector, '<', conditions.lessThan);
|
||||
}
|
||||
if (conditions.lessThanOrEqual) {
|
||||
query = query.where(selector, '<=', conditions.lessThanOrEqual);
|
||||
}
|
||||
if (conditions.in) {
|
||||
query = query.whereIn(selector, conditions.in);
|
||||
}
|
||||
if (conditions.notIn) {
|
||||
query = query.whereNotIn(selector, conditions.notIn);
|
||||
}
|
||||
return query;
|
||||
};
|
||||
|
||||
const queryConditionSchema = z.discriminatedUnion('type', [queryConditionTextSchema, queryConditionNumberSchema]);
|
||||
|
||||
type QueryCondition = z.infer<typeof queryConditionSchema>;
|
||||
|
||||
const applyQueryCondition = (query: Knex.QueryBuilder, options: QueryCondition) => {
|
||||
switch (options.type) {
|
||||
case 'text': {
|
||||
return applyQueryConditionText(query, options);
|
||||
}
|
||||
case 'number': {
|
||||
return applyQueryConditionNumber(query, options);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown filter type`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
type QueryFilter = QueryCondition | QueryOperator;
|
||||
|
||||
type QueryOperator = {
|
||||
type: 'operator';
|
||||
operator: 'and' | 'or';
|
||||
conditions: QueryFilter[];
|
||||
};
|
||||
|
||||
// Create a depth-limited recursive schema for OpenAPI compatibility
|
||||
// This supports up to 3 levels of nesting, which should be sufficient for most use cases
|
||||
// OpenAPI cannot handle z.lazy(), so we manually define the nesting
|
||||
// If you need deeper nesting, you can add more levels (Level3, Level4, etc.)
|
||||
const queryFilterSchemaLevel0: z.ZodType<QueryFilter> = z.union([
|
||||
queryConditionSchema,
|
||||
z
|
||||
.object({
|
||||
type: z.literal('operator'),
|
||||
operator: z.enum(['and', 'or']),
|
||||
conditions: z.array(queryConditionSchema),
|
||||
})
|
||||
.meta({
|
||||
example: {
|
||||
type: 'operator',
|
||||
operator: 'and',
|
||||
conditions: [
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
examples: [
|
||||
{
|
||||
summary: 'AND operator',
|
||||
value: {
|
||||
type: 'operator',
|
||||
operator: 'and',
|
||||
conditions: [
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
field: ['type'],
|
||||
conditions: {
|
||||
equal: 'demo',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
summary: 'OR operator',
|
||||
value: {
|
||||
type: 'operator',
|
||||
operator: 'or',
|
||||
conditions: [
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'baz',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
const queryFilterSchemaLevel1: z.ZodType<QueryFilter> = z.union([
|
||||
queryConditionSchema,
|
||||
z
|
||||
.object({
|
||||
type: z.literal('operator'),
|
||||
operator: z.enum(['and', 'or']),
|
||||
conditions: z.array(queryFilterSchemaLevel0),
|
||||
})
|
||||
.meta({
|
||||
example: {
|
||||
type: 'operator',
|
||||
operator: 'or',
|
||||
conditions: [
|
||||
{
|
||||
type: 'operator',
|
||||
operator: 'and',
|
||||
conditions: [
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'bar',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'baz',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
examples: [
|
||||
{
|
||||
summary: 'Nested AND within OR',
|
||||
value: {
|
||||
type: 'operator',
|
||||
operator: 'or',
|
||||
conditions: [
|
||||
{
|
||||
type: 'operator',
|
||||
operator: 'and',
|
||||
conditions: [
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
field: ['type'],
|
||||
conditions: {
|
||||
equal: 'demo',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'baz',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
const queryFilterSchemaLevel2: z.ZodType<QueryFilter> = z.union([
|
||||
queryConditionSchema,
|
||||
z
|
||||
.object({
|
||||
type: z.literal('operator'),
|
||||
operator: z.enum(['and', 'or']),
|
||||
conditions: z.array(queryFilterSchemaLevel1),
|
||||
})
|
||||
.meta({
|
||||
example: {
|
||||
type: 'operator',
|
||||
operator: 'and',
|
||||
conditions: [
|
||||
{
|
||||
type: 'operator',
|
||||
operator: 'or',
|
||||
conditions: [
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'baz',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
field: ['type'],
|
||||
conditions: {
|
||||
equal: 'demo',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
examples: [
|
||||
{
|
||||
summary: 'Complex nested query',
|
||||
value: {
|
||||
type: 'operator',
|
||||
operator: 'and',
|
||||
conditions: [
|
||||
{
|
||||
type: 'operator',
|
||||
operator: 'or',
|
||||
conditions: [
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'bar',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
field: ['metadata', 'foo'],
|
||||
conditions: {
|
||||
equal: 'baz',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
field: ['type'],
|
||||
conditions: {
|
||||
equal: 'demo',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
// Export the depth-limited schema (supports 3 levels of nesting)
|
||||
// This works with OpenAPI schema generation
|
||||
const queryFilterSchema = queryFilterSchemaLevel2;
|
||||
|
||||
const applyQueryFilter = (query: Knex.QueryBuilder, filter: QueryFilter) => {
|
||||
if (filter.type === 'operator') {
|
||||
if (filter.conditions.length === 0) {
|
||||
return query;
|
||||
}
|
||||
|
||||
switch (filter.operator) {
|
||||
case 'or': {
|
||||
return query.where((subquery) => {
|
||||
let isFirst = true;
|
||||
for (const condition of filter.conditions) {
|
||||
if (isFirst) {
|
||||
applyQueryFilter(subquery, condition);
|
||||
isFirst = false;
|
||||
} else {
|
||||
subquery.orWhere((subSubquery) => {
|
||||
applyQueryFilter(subSubquery, condition);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
case 'and': {
|
||||
return query.where((subquery) => {
|
||||
let isFirst = true;
|
||||
for (const condition of filter.conditions) {
|
||||
if (isFirst) {
|
||||
applyQueryFilter(subquery, condition);
|
||||
isFirst = false;
|
||||
} else {
|
||||
subquery.andWhere((subSubquery) => {
|
||||
applyQueryFilter(subSubquery, condition);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return applyQueryCondition(query, filter);
|
||||
}
|
||||
};
|
||||
|
||||
export type { QueryConditionText, QueryConditionNumber, QueryOperator, QueryCondition, QueryFilter };
|
||||
export { applyQueryCondition, queryConditionSchema, queryFilterSchema, applyQueryFilter };
|
||||
@@ -1,8 +0,0 @@
|
||||
import { z, type ZodType } from 'zod';
|
||||
|
||||
const createListResultSchema = <T extends ZodType>(schema: T) =>
|
||||
z.object({
|
||||
items: z.array(schema),
|
||||
});
|
||||
|
||||
export { createListResultSchema };
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"paths": {
|
||||
"#root/*": [
|
||||
"./src/*"
|
||||
|
||||
355
pnpm-lock.yaml
generated
355
pnpm-lock.yaml
generated
@@ -48,43 +48,84 @@ importers:
|
||||
specifier: 4.0.15
|
||||
version: 4.0.15(@types/node@24.10.2)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
packages/client:
|
||||
dependencies:
|
||||
openapi-fetch:
|
||||
specifier: ^0.15.0
|
||||
version: 0.15.0
|
||||
devDependencies:
|
||||
'@morten-olsen/stash-configs':
|
||||
specifier: workspace:*
|
||||
version: link:../configs
|
||||
'@morten-olsen/stash-tests':
|
||||
specifier: workspace:*
|
||||
version: link:../tests
|
||||
'@types/node':
|
||||
specifier: 24.10.2
|
||||
version: 24.10.2
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 4.0.15
|
||||
version: 4.0.15(vitest@4.0.15(@types/node@24.10.2)(tsx@4.21.0)(yaml@2.8.2))
|
||||
openapi-typescript:
|
||||
specifier: ^7.10.1
|
||||
version: 7.10.1(typescript@5.9.3)
|
||||
typescript:
|
||||
specifier: 5.9.3
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: 4.0.15
|
||||
version: 4.0.15(@types/node@24.10.2)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
packages/configs: {}
|
||||
|
||||
packages/server:
|
||||
packages/query-dsl:
|
||||
dependencies:
|
||||
chevrotain:
|
||||
specifier: ^11.0.3
|
||||
version: 11.0.3
|
||||
zod:
|
||||
specifier: 4.1.13
|
||||
version: 4.1.13
|
||||
devDependencies:
|
||||
'@morten-olsen/stash-configs':
|
||||
specifier: workspace:*
|
||||
version: link:../configs
|
||||
'@morten-olsen/stash-tests':
|
||||
specifier: workspace:*
|
||||
version: link:../tests
|
||||
'@types/node':
|
||||
specifier: 24.10.2
|
||||
version: 24.10.2
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 4.0.15
|
||||
version: 4.0.15(vitest@4.0.15(@types/node@24.10.2)(tsx@4.21.0)(yaml@2.8.2))
|
||||
typescript:
|
||||
specifier: 5.9.3
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: 4.0.15
|
||||
version: 4.0.15(@types/node@24.10.2)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
packages/runtime:
|
||||
dependencies:
|
||||
'@electric-sql/pglite':
|
||||
specifier: ^0.3.14
|
||||
version: 0.3.14
|
||||
'@fastify/cors':
|
||||
specifier: 11.1.0
|
||||
version: 11.1.0
|
||||
'@fastify/swagger':
|
||||
specifier: 9.6.1
|
||||
version: 9.6.1
|
||||
'@fastify/websocket':
|
||||
specifier: 11.2.0
|
||||
version: 11.2.0
|
||||
'@huggingface/transformers':
|
||||
specifier: ^3.8.1
|
||||
version: 3.8.1
|
||||
'@langchain/textsplitters':
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1(@langchain/core@1.1.4)
|
||||
'@scalar/fastify-api-reference':
|
||||
specifier: 1.40.2
|
||||
version: 1.40.2
|
||||
'@morten-olsen/stash-query-dsl':
|
||||
specifier: workspace:*
|
||||
version: link:../query-dsl
|
||||
better-sqlite3:
|
||||
specifier: ^12.5.0
|
||||
version: 12.5.0
|
||||
deep-equal:
|
||||
specifier: ^2.2.3
|
||||
version: 2.2.3
|
||||
fastify:
|
||||
specifier: 5.6.2
|
||||
version: 5.6.2
|
||||
fastify-type-provider-zod:
|
||||
specifier: 6.1.0
|
||||
version: 6.1.0(@fastify/swagger@9.6.1)(fastify@5.6.2)(openapi-types@12.1.3)(zod@4.1.13)
|
||||
knex:
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0(better-sqlite3@12.5.0)(pg@8.16.3)
|
||||
@@ -100,6 +141,58 @@ importers:
|
||||
zod:
|
||||
specifier: 4.1.13
|
||||
version: 4.1.13
|
||||
devDependencies:
|
||||
'@morten-olsen/stash-configs':
|
||||
specifier: workspace:*
|
||||
version: link:../configs
|
||||
'@morten-olsen/stash-tests':
|
||||
specifier: workspace:*
|
||||
version: link:../tests
|
||||
'@types/deep-equal':
|
||||
specifier: ^1.0.4
|
||||
version: 1.0.4
|
||||
'@types/node':
|
||||
specifier: 24.10.2
|
||||
version: 24.10.2
|
||||
'@vitest/coverage-v8':
|
||||
specifier: 4.0.15
|
||||
version: 4.0.15(vitest@4.0.15(@types/node@24.10.2)(tsx@4.21.0)(yaml@2.8.2))
|
||||
typescript:
|
||||
specifier: 5.9.3
|
||||
version: 5.9.3
|
||||
vitest:
|
||||
specifier: 4.0.15
|
||||
version: 4.0.15(@types/node@24.10.2)(tsx@4.21.0)(yaml@2.8.2)
|
||||
|
||||
packages/server:
|
||||
dependencies:
|
||||
'@fastify/cors':
|
||||
specifier: 11.1.0
|
||||
version: 11.1.0
|
||||
'@fastify/swagger':
|
||||
specifier: 9.6.1
|
||||
version: 9.6.1
|
||||
'@fastify/websocket':
|
||||
specifier: 11.2.0
|
||||
version: 11.2.0
|
||||
'@morten-olsen/stash-query-dsl':
|
||||
specifier: workspace:*
|
||||
version: link:../query-dsl
|
||||
'@morten-olsen/stash-runtime':
|
||||
specifier: workspace:*
|
||||
version: link:../runtime
|
||||
'@scalar/fastify-api-reference':
|
||||
specifier: 1.40.2
|
||||
version: 1.40.2
|
||||
fastify:
|
||||
specifier: 5.6.2
|
||||
version: 5.6.2
|
||||
fastify-type-provider-zod:
|
||||
specifier: 6.1.0
|
||||
version: 6.1.0(@fastify/swagger@9.6.1)(fastify@5.6.2)(openapi-types@12.1.3)(zod@4.1.13)
|
||||
zod:
|
||||
specifier: 4.1.13
|
||||
version: 4.1.13
|
||||
zod-to-json-schema:
|
||||
specifier: 3.25.0
|
||||
version: 3.25.0(zod@4.1.13)
|
||||
@@ -181,6 +274,21 @@ packages:
|
||||
'@cfworker/json-schema@4.1.1':
|
||||
resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==}
|
||||
|
||||
'@chevrotain/cst-dts-gen@11.0.3':
|
||||
resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==}
|
||||
|
||||
'@chevrotain/gast@11.0.3':
|
||||
resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==}
|
||||
|
||||
'@chevrotain/regexp-to-ast@11.0.3':
|
||||
resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==}
|
||||
|
||||
'@chevrotain/types@11.0.3':
|
||||
resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==}
|
||||
|
||||
'@chevrotain/utils@11.0.3':
|
||||
resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==}
|
||||
|
||||
'@electric-sql/pglite@0.3.14':
|
||||
resolution: {integrity: sha512-3DB258dhqdsArOI1fIt7cb9RpUOgcDg5hXWVgVHAeqVQ/qxtFy605QKs4gx6mFq3jWsSPqDN8TgSEsqC3OfV9Q==}
|
||||
|
||||
@@ -942,6 +1050,16 @@ packages:
|
||||
'@protobufjs/utf8@1.1.0':
|
||||
resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
|
||||
|
||||
'@redocly/ajv@8.17.1':
|
||||
resolution: {integrity: sha512-EDtsGZS964mf9zAUXAl9Ew16eYbeyAFWhsPr0fX6oaJxgd8rApYlPBf0joyhnUHz88WxrigyFtTaqqzXNzPgqw==}
|
||||
|
||||
'@redocly/config@0.22.2':
|
||||
resolution: {integrity: sha512-roRDai8/zr2S9YfmzUfNhKjOF0NdcOIqF7bhf4MVC5UxpjIysDjyudvlAiVbpPHp3eDRWbdzUgtkK1a7YiDNyQ==}
|
||||
|
||||
'@redocly/openapi-core@1.34.6':
|
||||
resolution: {integrity: sha512-2+O+riuIUgVSuLl3Lyh5AplWZyVMNuG2F98/o6NrutKJfW4/GTZdPpZlIphS0HGgcOHgmWcCSHj+dWFlZaGSHw==}
|
||||
engines: {node: '>=18.17.0', npm: '>=9.5.0'}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.53.3':
|
||||
resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==}
|
||||
cpu: [arm]
|
||||
@@ -1232,6 +1350,10 @@ packages:
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
agent-base@7.1.4:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ajv-draft-04@1.0.0:
|
||||
resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==}
|
||||
peerDependencies:
|
||||
@@ -1257,6 +1379,10 @@ packages:
|
||||
ansi-align@3.0.1:
|
||||
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
|
||||
|
||||
ansi-colors@4.1.3:
|
||||
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
ansi-diff@1.2.0:
|
||||
resolution: {integrity: sha512-BIXwHKpjzghBjcwEV10Y4b17tjHfK4nhEqK3LqyQ3JgcMcjmi3DIevozNgrOpfvBMmrq9dfvrPJSu5/5vNUBQg==}
|
||||
|
||||
@@ -1418,10 +1544,16 @@ packages:
|
||||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
change-case@5.4.4:
|
||||
resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
|
||||
|
||||
char-regex@1.0.2:
|
||||
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
chevrotain@11.0.3:
|
||||
resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==}
|
||||
|
||||
chownr@1.1.4:
|
||||
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||
|
||||
@@ -1448,6 +1580,9 @@ packages:
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
colorette@1.4.0:
|
||||
resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
|
||||
|
||||
colorette@2.0.19:
|
||||
resolution: {integrity: sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==}
|
||||
|
||||
@@ -1964,6 +2099,10 @@ packages:
|
||||
html-escaper@2.0.2:
|
||||
resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==}
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
human-signals@2.1.0:
|
||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||
engines: {node: '>=10.17.0'}
|
||||
@@ -1987,6 +2126,10 @@ packages:
|
||||
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||
engines: {node: '>=0.8.19'}
|
||||
|
||||
index-to-position@1.2.0:
|
||||
resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
individual@3.0.0:
|
||||
resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==}
|
||||
|
||||
@@ -2161,6 +2304,10 @@ packages:
|
||||
resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
js-levenshtein@1.1.6:
|
||||
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
js-tiktoken@1.0.21:
|
||||
resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==}
|
||||
|
||||
@@ -2288,6 +2435,9 @@ packages:
|
||||
resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
lodash-es@4.17.21:
|
||||
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
||||
|
||||
lodash.merge@4.6.2:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
|
||||
@@ -2353,6 +2503,10 @@ packages:
|
||||
minimatch@3.1.2:
|
||||
resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
|
||||
|
||||
minimatch@5.1.6:
|
||||
resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
minimatch@9.0.5:
|
||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
@@ -2472,9 +2626,21 @@ packages:
|
||||
onnxruntime-web@1.22.0-dev.20250409-89f8206ba4:
|
||||
resolution: {integrity: sha512-0uS76OPgH0hWCPrFKlL8kYVV7ckM7t/36HfbgoFw6Nd0CZVVbQC4PkrR8mBX8LtNUFZO25IQBqV2Hx2ho3FlbQ==}
|
||||
|
||||
openapi-fetch@0.15.0:
|
||||
resolution: {integrity: sha512-OjQUdi61WO4HYhr9+byCPMj0+bgste/LtSBEcV6FzDdONTs7x0fWn8/ndoYwzqCsKWIxEZwo4FN/TG1c1rI8IQ==}
|
||||
|
||||
openapi-types@12.1.3:
|
||||
resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==}
|
||||
|
||||
openapi-typescript-helpers@0.0.15:
|
||||
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
|
||||
|
||||
openapi-typescript@7.10.1:
|
||||
resolution: {integrity: sha512-rBcU8bjKGGZQT4K2ekSTY2Q5veOQbVG/lTKZ49DeCyT9z62hM2Vj/LLHjDHC9W7LJG8YMHcdXpRZDqC1ojB/lw==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: ^5.x
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -2523,6 +2689,10 @@ packages:
|
||||
resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
parse-json@8.3.0:
|
||||
resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
parse-ms@2.1.0:
|
||||
resolution: {integrity: sha512-kHt7kzLoS9VBZfUsiKjv43mr91ea+U05EyKkEtqp7vNbHxmaVuEqN7XxeEVnGrMtYOAxGrDElSi96K7EgO1zCA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -2617,6 +2787,10 @@ packages:
|
||||
platform@1.3.6:
|
||||
resolution: {integrity: sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==}
|
||||
|
||||
pluralize@8.0.0:
|
||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
possible-typed-array-names@1.1.0:
|
||||
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -2985,6 +3159,10 @@ packages:
|
||||
resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
supports-color@10.2.2:
|
||||
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -3119,6 +3297,10 @@ packages:
|
||||
resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
type-fest@4.41.0:
|
||||
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
type-fest@5.0.0:
|
||||
resolution: {integrity: sha512-GeJop7+u7BYlQ6yQCAY1nBQiRSHR+6OdCEtd8Bwp9a3NK3+fWAVjOaPKJDteB9f6cIJ0wt4IfnScjLG450EpXA==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -3323,6 +3505,9 @@ packages:
|
||||
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
yaml-ast-parser@0.0.43:
|
||||
resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==}
|
||||
|
||||
yaml@2.8.0:
|
||||
resolution: {integrity: sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==}
|
||||
engines: {node: '>= 14.6'}
|
||||
@@ -3333,6 +3518,10 @@ packages:
|
||||
engines: {node: '>= 14.6'}
|
||||
hasBin: true
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yocto-queue@0.1.0:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -3373,6 +3562,23 @@ snapshots:
|
||||
|
||||
'@cfworker/json-schema@4.1.1': {}
|
||||
|
||||
'@chevrotain/cst-dts-gen@11.0.3':
|
||||
dependencies:
|
||||
'@chevrotain/gast': 11.0.3
|
||||
'@chevrotain/types': 11.0.3
|
||||
lodash-es: 4.17.21
|
||||
|
||||
'@chevrotain/gast@11.0.3':
|
||||
dependencies:
|
||||
'@chevrotain/types': 11.0.3
|
||||
lodash-es: 4.17.21
|
||||
|
||||
'@chevrotain/regexp-to-ast@11.0.3': {}
|
||||
|
||||
'@chevrotain/types@11.0.3': {}
|
||||
|
||||
'@chevrotain/utils@11.0.3': {}
|
||||
|
||||
'@electric-sql/pglite@0.3.14': {}
|
||||
|
||||
'@emnapi/runtime@1.7.1':
|
||||
@@ -3546,7 +3752,7 @@ snapshots:
|
||||
'@eslint/config-array@0.21.1':
|
||||
dependencies:
|
||||
'@eslint/object-schema': 2.1.7
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
minimatch: 3.1.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -3562,7 +3768,7 @@ snapshots:
|
||||
'@eslint/eslintrc@3.3.3':
|
||||
dependencies:
|
||||
ajv: 6.12.6
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
espree: 10.4.0
|
||||
globals: 14.0.0
|
||||
ignore: 5.3.2
|
||||
@@ -4056,6 +4262,29 @@ snapshots:
|
||||
|
||||
'@protobufjs/utf8@1.1.0': {}
|
||||
|
||||
'@redocly/ajv@8.17.1':
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
fast-uri: 3.1.0
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
|
||||
'@redocly/config@0.22.2': {}
|
||||
|
||||
'@redocly/openapi-core@1.34.6(supports-color@10.2.2)':
|
||||
dependencies:
|
||||
'@redocly/ajv': 8.17.1
|
||||
'@redocly/config': 0.22.2
|
||||
colorette: 1.4.0
|
||||
https-proxy-agent: 7.0.6(supports-color@10.2.2)
|
||||
js-levenshtein: 1.1.6
|
||||
js-yaml: 4.1.1
|
||||
minimatch: 5.1.6
|
||||
pluralize: 8.0.0
|
||||
yaml-ast-parser: 0.0.43
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.53.3':
|
||||
optional: true
|
||||
|
||||
@@ -4218,7 +4447,7 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.49.0
|
||||
'@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 8.49.0
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
eslint: 9.39.1
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
@@ -4228,7 +4457,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.49.0
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
typescript: 5.9.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -4247,7 +4476,7 @@ snapshots:
|
||||
'@typescript-eslint/types': 8.49.0
|
||||
'@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 8.49.0(eslint@9.39.1)(typescript@5.9.3)
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
eslint: 9.39.1
|
||||
ts-api-utils: 2.1.0(typescript@5.9.3)
|
||||
typescript: 5.9.3
|
||||
@@ -4262,7 +4491,7 @@ snapshots:
|
||||
'@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3)
|
||||
'@typescript-eslint/types': 8.49.0
|
||||
'@typescript-eslint/visitor-keys': 8.49.0
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
minimatch: 9.0.5
|
||||
semver: 7.7.3
|
||||
tinyglobby: 0.2.15
|
||||
@@ -4355,6 +4584,8 @@ snapshots:
|
||||
|
||||
acorn@8.15.0: {}
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
ajv-draft-04@1.0.0(ajv@8.17.1):
|
||||
optionalDependencies:
|
||||
ajv: 8.17.1
|
||||
@@ -4381,6 +4612,8 @@ snapshots:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
|
||||
ansi-colors@4.1.3: {}
|
||||
|
||||
ansi-diff@1.2.0:
|
||||
dependencies:
|
||||
ansi-split: 1.0.1
|
||||
@@ -4578,8 +4811,19 @@ snapshots:
|
||||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
change-case@5.4.4: {}
|
||||
|
||||
char-regex@1.0.2: {}
|
||||
|
||||
chevrotain@11.0.3:
|
||||
dependencies:
|
||||
'@chevrotain/cst-dts-gen': 11.0.3
|
||||
'@chevrotain/gast': 11.0.3
|
||||
'@chevrotain/regexp-to-ast': 11.0.3
|
||||
'@chevrotain/types': 11.0.3
|
||||
'@chevrotain/utils': 11.0.3
|
||||
lodash-es: 4.17.21
|
||||
|
||||
chownr@1.1.4: {}
|
||||
|
||||
chownr@3.0.0: {}
|
||||
@@ -4599,6 +4843,8 @@ snapshots:
|
||||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
colorette@1.4.0: {}
|
||||
|
||||
colorette@2.0.19: {}
|
||||
|
||||
commander@10.0.1: {}
|
||||
@@ -4652,9 +4898,11 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
|
||||
debug@4.4.3:
|
||||
debug@4.4.3(supports-color@10.2.2):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 10.2.2
|
||||
|
||||
decamelize@1.2.0: {}
|
||||
|
||||
@@ -4982,7 +5230,7 @@ snapshots:
|
||||
ajv: 6.12.6
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 8.4.0
|
||||
eslint-visitor-keys: 4.2.1
|
||||
@@ -5272,6 +5520,13 @@ snapshots:
|
||||
|
||||
html-escaper@2.0.2: {}
|
||||
|
||||
https-proxy-agent@7.0.6(supports-color@10.2.2):
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
human-signals@2.1.0: {}
|
||||
|
||||
ieee754@1.2.1: {}
|
||||
@@ -5287,6 +5542,8 @@ snapshots:
|
||||
|
||||
imurmurhash@0.1.4: {}
|
||||
|
||||
index-to-position@1.2.0: {}
|
||||
|
||||
individual@3.0.0: {}
|
||||
|
||||
inherits@2.0.4: {}
|
||||
@@ -5447,7 +5704,7 @@ snapshots:
|
||||
istanbul-lib-source-maps@5.0.6:
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
istanbul-lib-coverage: 3.2.2
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -5457,6 +5714,8 @@ snapshots:
|
||||
html-escaper: 2.0.2
|
||||
istanbul-lib-report: 3.0.1
|
||||
|
||||
js-levenshtein@1.1.6: {}
|
||||
|
||||
js-tiktoken@1.0.21:
|
||||
dependencies:
|
||||
base64-js: 1.5.1
|
||||
@@ -5479,7 +5738,7 @@ snapshots:
|
||||
|
||||
json-schema-resolver@3.0.0:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
fast-uri: 3.1.0
|
||||
rfdc: 1.4.1
|
||||
transitivePeerDependencies:
|
||||
@@ -5567,6 +5826,8 @@ snapshots:
|
||||
dependencies:
|
||||
p-locate: 5.0.0
|
||||
|
||||
lodash-es@4.17.21: {}
|
||||
|
||||
lodash.merge@4.6.2: {}
|
||||
|
||||
lodash@4.17.21: {}
|
||||
@@ -5623,6 +5884,10 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
|
||||
minimatch@5.1.6:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minimatch@9.0.5:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
@@ -5740,8 +6005,24 @@ snapshots:
|
||||
platform: 1.3.6
|
||||
protobufjs: 7.5.4
|
||||
|
||||
openapi-fetch@0.15.0:
|
||||
dependencies:
|
||||
openapi-typescript-helpers: 0.0.15
|
||||
|
||||
openapi-types@12.1.3: {}
|
||||
|
||||
openapi-typescript-helpers@0.0.15: {}
|
||||
|
||||
openapi-typescript@7.10.1(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@redocly/openapi-core': 1.34.6(supports-color@10.2.2)
|
||||
ansi-colors: 4.1.3
|
||||
change-case: 5.4.4
|
||||
parse-json: 8.3.0
|
||||
supports-color: 10.2.2
|
||||
typescript: 5.9.3
|
||||
yargs-parser: 21.1.1
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
@@ -5795,6 +6076,12 @@ snapshots:
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
lines-and-columns: 1.2.4
|
||||
|
||||
parse-json@8.3.0:
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.27.1
|
||||
index-to-position: 1.2.0
|
||||
type-fest: 4.41.0
|
||||
|
||||
parse-ms@2.1.0: {}
|
||||
|
||||
path-absolute@1.0.1: {}
|
||||
@@ -5880,6 +6167,8 @@ snapshots:
|
||||
|
||||
platform@1.3.6: {}
|
||||
|
||||
pluralize@8.0.0: {}
|
||||
|
||||
possible-typed-array-names@1.1.0: {}
|
||||
|
||||
postcss@8.5.6:
|
||||
@@ -6321,6 +6610,8 @@ snapshots:
|
||||
|
||||
strip-json-comments@3.1.1: {}
|
||||
|
||||
supports-color@10.2.2: {}
|
||||
|
||||
supports-color@7.2.0:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
@@ -6446,6 +6737,8 @@ snapshots:
|
||||
|
||||
type-fest@0.6.0: {}
|
||||
|
||||
type-fest@4.41.0: {}
|
||||
|
||||
type-fest@5.0.0:
|
||||
dependencies:
|
||||
tagged-tag: 1.0.0
|
||||
@@ -6656,10 +6949,14 @@ snapshots:
|
||||
|
||||
yallist@5.0.0: {}
|
||||
|
||||
yaml-ast-parser@0.0.43: {}
|
||||
|
||||
yaml@2.8.0: {}
|
||||
|
||||
yaml@2.8.2: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
zod-to-json-schema@3.25.0(zod@4.1.13):
|
||||
|
||||
17
tsconfig.json
Normal file
17
tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./packages/query-dsl/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "./packages/runtime/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "./packages/server/tsconfig.json"
|
||||
},
|
||||
{
|
||||
"path": "./packages/client/tsconfig.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import { defineConfig, type UserConfigExport } from 'vitest/config';
|
||||
import { defineConfig, type ViteUserConfigExport } from 'vitest/config';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default defineConfig(async () => {
|
||||
const config: UserConfigExport = {
|
||||
const config: ViteUserConfigExport = {
|
||||
test: {
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
|
||||
Reference in New Issue
Block a user