mirror of
https://github.com/morten-olsen/reservoir.git
synced 2026-02-08 01:46:24 +01:00
init
This commit is contained in:
3
.dockerignore
Normal file
3
.dockerignore
Normal file
@@ -0,0 +1,3 @@
|
||||
node_modules/
|
||||
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
vendored
Normal file
21
.github/workflows/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 }}
|
||||
114
.github/workflows/pipeline-default.yaml
vendored
Normal file
114
.github/workflows/pipeline-default.yaml
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
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: .
|
||||
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
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/node_modules
|
||||
.turbo/
|
||||
/.env
|
||||
/coverage/
|
||||
18
.prettierrc.json
Normal file
18
.prettierrc.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"bracketSameLine": false,
|
||||
"jsxSingleQuote": false,
|
||||
"printWidth": 120,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false,
|
||||
"singleAttributePerLine": false
|
||||
}
|
||||
41
.u8.json
Normal file
41
.u8.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"values": {
|
||||
"monoRepo": true,
|
||||
"packagePrefix": "@morten-olsen/reservoir-",
|
||||
"packageVersion": "1.0.0"
|
||||
},
|
||||
"entries": [
|
||||
{
|
||||
"timestamp": "2025-11-03T09:33:18.994Z",
|
||||
"template": "monorepo",
|
||||
"values": {
|
||||
"monoRepo": true,
|
||||
"packagePrefix": "@morten-olsen/reservoir-",
|
||||
"packageVersion": "1.0.0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-11-03T09:33:32.155Z",
|
||||
"template": "eslint",
|
||||
"values": {
|
||||
"monoRepo": true,
|
||||
"packagePrefix": "@morten-olsen/reservoir-",
|
||||
"packageVersion": "1.0.0",
|
||||
"target-pkg": {
|
||||
"name": "@morten-olsen/reservoir-repo",
|
||||
"dir": "/Users/alice/Projects/private/incubator/reservoir"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"timestamp": "2025-11-03T09:33:49.149Z",
|
||||
"template": "pkg",
|
||||
"values": {
|
||||
"monoRepo": true,
|
||||
"packagePrefix": "@morten-olsen/reservoir-",
|
||||
"packageVersion": "1.0.0",
|
||||
"packageName": "server"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
9
docker-compose.yaml
Normal file
9
docker-compose.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
name: reservoir
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./packages/server/Dockerfile
|
||||
|
||||
ports:
|
||||
- 9111:9111
|
||||
52
eslint.config.mjs
Normal file
52
eslint.config.mjs
Normal file
@@ -0,0 +1,52 @@
|
||||
import { defineConfig } from 'eslint/config';
|
||||
import { FlatCompat } from '@eslint/eslintrc';
|
||||
import importPlugin from 'eslint-plugin-import';
|
||||
import eslint from '@eslint/js';
|
||||
import eslintConfigPrettier from 'eslint-config-prettier';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
const compat = new FlatCompat({
|
||||
baseDirectory: import.meta.__dirname,
|
||||
resolvePluginsRelativeTo: import.meta.__dirname,
|
||||
});
|
||||
|
||||
export default defineConfig(
|
||||
eslint.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
...tseslint.configs.stylistic,
|
||||
eslintConfigPrettier,
|
||||
{
|
||||
files: ['**/*.{ts,tsxx}'],
|
||||
extends: [importPlugin.flatConfigs.recommended, importPlugin.flatConfigs.typescript],
|
||||
rules: {
|
||||
'import/no-unresolved': 'off',
|
||||
'import/extensions': ['error', 'ignorePackages'],
|
||||
'import/exports-last': 'error',
|
||||
'import/no-default-export': 'error',
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
|
||||
'newlines-between': 'always',
|
||||
},
|
||||
],
|
||||
'import/no-duplicates': 'error',
|
||||
},
|
||||
},
|
||||
{
|
||||
rules: {
|
||||
'@typescript-eslint/consistent-type-definitions': ['error', 'type'],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**.d.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/triple-slash-reference': 'off',
|
||||
'@typescript-eslint/consistent-type-definitions': 'off',
|
||||
},
|
||||
},
|
||||
...compat.extends('plugin:prettier/recommended'),
|
||||
{
|
||||
ignores: ['**/node_modules/', '**/dist/', '**/.turbo/', '**/generated/'],
|
||||
},
|
||||
);
|
||||
33
package.json
Normal file
33
package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"version": "1.0.0",
|
||||
"name": "@morten-olsen/reservoir-repo",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"test:lint": "eslint",
|
||||
"build": "turbo build",
|
||||
"build:dev": "tsc --build --watch",
|
||||
"test:unit": "vitest --run --coverage --passWithNoTests",
|
||||
"test": "pnpm run \"/^test:.+/\""
|
||||
},
|
||||
"packageManager": "pnpm@10.6.0",
|
||||
"workspaces": [
|
||||
"packages/*",
|
||||
"apps/*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"turbo": "2.6.0",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "4.0.6",
|
||||
"@vitest/coverage-v8": "4.0.6",
|
||||
"@eslint/eslintrc": "3.3.1",
|
||||
"@eslint/js": "9.39.0",
|
||||
"@pnpm/find-workspace-packages": "6.0.9",
|
||||
"eslint": "9.39.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-import": "2.32.0",
|
||||
"eslint-plugin-prettier": "5.5.4",
|
||||
"prettier": "3.6.2",
|
||||
"typescript-eslint": "8.46.2"
|
||||
}
|
||||
}
|
||||
4
packages/configs/package.json
Normal file
4
packages/configs/package.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "@morten-olsen/reservoir-configs",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
20
packages/configs/tsconfig.json
Normal file
20
packages/configs/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"strict": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"isolatedModules": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"allowImportingTsExtensions": true
|
||||
}
|
||||
}
|
||||
4
packages/server/.gitignore
vendored
Normal file
4
packages/server/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/node_modules/
|
||||
/dist/
|
||||
/coverage/
|
||||
/.env
|
||||
27
packages/server/Dockerfile
Normal file
27
packages/server/Dockerfile
Normal file
@@ -0,0 +1,27 @@
|
||||
FROM node:23-slim AS base
|
||||
RUN corepack enable
|
||||
WORKDIR /app
|
||||
|
||||
FROM base AS builder
|
||||
RUN npm i -g turbo
|
||||
COPY . .
|
||||
RUN turbo prune @morten-olsen/reservoir-server --docker
|
||||
|
||||
FROM base AS installer
|
||||
COPY --from=builder /app/out/json/ .
|
||||
RUN pnpm install --prod --frozen-lockfile
|
||||
COPY --from=builder /app/out/full/ .
|
||||
|
||||
FROM base AS runner
|
||||
ENV \
|
||||
SERVER_HOST=0.0.0.0 \
|
||||
DB_URL=/data/db.sqlite
|
||||
RUN \
|
||||
addgroup --system --gid 1001 nodejs \
|
||||
&& adduser --system --uid 1001 nodejs \
|
||||
&& mkdir /data \
|
||||
&& chown nodejs:nodejs /data
|
||||
USER nodejs
|
||||
|
||||
COPY --from=installer /app /app
|
||||
CMD ["node", "/app/packages/server/src/start.ts"]
|
||||
42
packages/server/package.json
Normal file
42
packages/server/package.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"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/reservoir-configs": "workspace:*",
|
||||
"@morten-olsen/reservoir-tests": "workspace:*",
|
||||
"@types/node": "24.10.0",
|
||||
"@vitest/coverage-v8": "4.0.6",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "4.0.6"
|
||||
},
|
||||
"name": "@morten-olsen/reservoir-server",
|
||||
"version": "1.0.0",
|
||||
"imports": {
|
||||
"#root/*": "./src/*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/swagger": "^9.5.2",
|
||||
"@scalar/fastify-api-reference": "^1.38.1",
|
||||
"better-sqlite3": "^12.4.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fastify": "^5.6.1",
|
||||
"fastify-type-provider-zod": "^6.1.0",
|
||||
"knex": "^3.1.0",
|
||||
"pg": "^8.16.3",
|
||||
"pino": "^10.1.0",
|
||||
"pino-pretty": "^13.1.2",
|
||||
"zod": "^4.1.12"
|
||||
}
|
||||
}
|
||||
36
packages/server/src/api/api.documents.ts
Normal file
36
packages/server/src/api/api.documents.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { z } from 'zod';
|
||||
import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
|
||||
|
||||
import { DocumentsService } from '#root/services/documents/documents.ts';
|
||||
import {
|
||||
upsertDocumentRequestSchema,
|
||||
upsertDocumentResponseSchema,
|
||||
} from '#root/services/documents/documents.schemas.ts';
|
||||
|
||||
const documentsPlugin: FastifyPluginAsyncZod = async (app) => {
|
||||
app.route({
|
||||
method: 'POST',
|
||||
url: '',
|
||||
schema: {
|
||||
operationId: 'v1.documents.put',
|
||||
tags: ['documents'],
|
||||
summary: 'Upsert documents',
|
||||
body: z.object({
|
||||
items: z.array(upsertDocumentRequestSchema),
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
items: z.array(upsertDocumentResponseSchema),
|
||||
}),
|
||||
},
|
||||
},
|
||||
handler: async (req, reply) => {
|
||||
const documentsService = app.services.get(DocumentsService);
|
||||
const { items } = req.body;
|
||||
const results = await Promise.all(items.map((item) => documentsService.upsert(item)));
|
||||
return reply.send({ items: results });
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export { documentsPlugin };
|
||||
52
packages/server/src/api/api.ts
Normal file
52
packages/server/src/api/api.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import fastify from 'fastify';
|
||||
import fastifySwagger from '@fastify/swagger';
|
||||
import fastifyScalar from '@scalar/fastify-api-reference';
|
||||
import { jsonSchemaTransform, serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod';
|
||||
|
||||
import { documentsPlugin } from './api.documents.ts';
|
||||
|
||||
import { Services } from '#root/utils/utils.services.ts';
|
||||
import { DatabaseService } from '#root/database/database.ts';
|
||||
|
||||
const createApi = async (services: Services = new Services()) => {
|
||||
const db = services.get(DatabaseService);
|
||||
await db.ready();
|
||||
const app = fastify({
|
||||
logger: {
|
||||
level: 'warn',
|
||||
transport: {
|
||||
target: 'pino-pretty',
|
||||
},
|
||||
},
|
||||
});
|
||||
app.setValidatorCompiler(validatorCompiler);
|
||||
app.setSerializerCompiler(serializerCompiler);
|
||||
app.decorate('services', services);
|
||||
|
||||
await app.register(fastifySwagger, {
|
||||
openapi: {
|
||||
info: {
|
||||
title: 'Reservoir',
|
||||
version: '1.0.0',
|
||||
},
|
||||
servers: [],
|
||||
},
|
||||
transform: jsonSchemaTransform,
|
||||
});
|
||||
|
||||
await app.register(fastifyScalar, {
|
||||
routePrefix: '/docs',
|
||||
});
|
||||
|
||||
await app.register(documentsPlugin, {
|
||||
prefix: '/api/v1/documents',
|
||||
});
|
||||
|
||||
app.addHook('onReady', async () => {
|
||||
app.swagger();
|
||||
});
|
||||
await app.ready();
|
||||
return app;
|
||||
};
|
||||
|
||||
export { createApi };
|
||||
10
packages/server/src/config/config.ts
Normal file
10
packages/server/src/config/config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
class ConfigService {
|
||||
public get database() {
|
||||
return {
|
||||
client: process.env.DB_CLIENT || 'better-sqlite3',
|
||||
connection: process.env.DB_URL || ':memory:',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { ConfigService };
|
||||
52
packages/server/src/database/database.ts
Normal file
52
packages/server/src/database/database.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import knex, { type Knex } from 'knex';
|
||||
|
||||
import { migrationSource } from './migrations/migrations.ts';
|
||||
|
||||
import { destroy, Services } from '#root/utils/utils.services.ts';
|
||||
import { ConfigService } from '#root/config/config.ts';
|
||||
|
||||
class DatabaseService {
|
||||
#services: Services;
|
||||
#instance?: Promise<Knex>;
|
||||
|
||||
constructor(services: Services) {
|
||||
this.#services = services;
|
||||
}
|
||||
|
||||
#setup = async () => {
|
||||
const configService = this.#services.get(ConfigService);
|
||||
const db = knex({
|
||||
client: configService.database.client,
|
||||
connection: configService.database.connection,
|
||||
useNullAsDefault: true,
|
||||
});
|
||||
|
||||
await db.migrate.latest({
|
||||
migrationSource,
|
||||
});
|
||||
|
||||
return db;
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
public ready = async () => {
|
||||
await this.getInstance();
|
||||
};
|
||||
}
|
||||
|
||||
export { tableNames, type Tables } from './migrations/migrations.ts';
|
||||
export { DatabaseService };
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { Migration } from './migrations.types.ts';
|
||||
|
||||
const tableNames = {
|
||||
documents: 'documents',
|
||||
};
|
||||
|
||||
const init: Migration = {
|
||||
name: 'init',
|
||||
up: async (knex) => {
|
||||
await knex.schema.createTable(tableNames.documents, (table) => {
|
||||
table.string('id').notNullable();
|
||||
table.string('type').notNullable();
|
||||
table.string('source').nullable();
|
||||
table.jsonb('data').notNullable();
|
||||
table.datetime('createdAt').notNullable();
|
||||
table.datetime('updatedAt').notNullable();
|
||||
table.datetime('deletedAt').nullable();
|
||||
});
|
||||
},
|
||||
down: async (knex) => {
|
||||
await knex.schema.dropTableIfExists(tableNames.documents);
|
||||
},
|
||||
};
|
||||
|
||||
type DocumentRow = {
|
||||
id: string;
|
||||
type: string;
|
||||
source: string | null;
|
||||
data: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
};
|
||||
|
||||
type Tables = {
|
||||
document: DocumentRow;
|
||||
};
|
||||
|
||||
export type { Tables };
|
||||
export { init, tableNames };
|
||||
15
packages/server/src/database/migrations/migrations.ts
Normal file
15
packages/server/src/database/migrations/migrations.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { Knex } from 'knex';
|
||||
|
||||
import { init, tableNames, type Tables } from './migrations.001-init.ts';
|
||||
import type { Migration } from './migrations.types.ts';
|
||||
|
||||
const migrations = [init];
|
||||
|
||||
const migrationSource: Knex.MigrationSource<Migration> = {
|
||||
getMigration: async (migration) => migration,
|
||||
getMigrationName: (migration: Migration) => migration.name,
|
||||
getMigrations: async () => migrations,
|
||||
};
|
||||
|
||||
export { tableNames, type Tables };
|
||||
export { migrationSource };
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { Knex } from 'knex';
|
||||
|
||||
type Migration = {
|
||||
name: string;
|
||||
up: (knex: Knex) => Promise<void>;
|
||||
down: (knex: Knex) => Promise<void>;
|
||||
};
|
||||
|
||||
export type { Migration };
|
||||
9
packages/server/src/global.d.ts
vendored
Normal file
9
packages/server/src/global.d.ts
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
import 'fastify';
|
||||
import type { Services } from './utils/utils.services.ts';
|
||||
|
||||
declare module 'fastify' {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
interface FastifyInstance {
|
||||
services: Services;
|
||||
}
|
||||
}
|
||||
22
packages/server/src/services/documents/documents.schemas.ts
Normal file
22
packages/server/src/services/documents/documents.schemas.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const upsertDocumentRequestSchema = z.object({
|
||||
id: z.string().min(1).optional(),
|
||||
type: z.string().min(1),
|
||||
source: z.string().min(1).nullable(),
|
||||
data: z.unknown(),
|
||||
});
|
||||
|
||||
type UpsertDocumentRequest = z.infer<typeof upsertDocumentRequestSchema>;
|
||||
|
||||
const upsertDocumentResponseSchema = upsertDocumentRequestSchema.extend({
|
||||
createdAt: z.iso.datetime(),
|
||||
updatedAt: z.iso.datetime(),
|
||||
deletedAt: z.iso.datetime().nullable(),
|
||||
action: z.enum(['inserted', 'updated', 'skipped']),
|
||||
});
|
||||
|
||||
type UpsertDocumentResponse = z.input<typeof upsertDocumentResponseSchema>;
|
||||
|
||||
export type { UpsertDocumentRequest, UpsertDocumentResponse };
|
||||
export { upsertDocumentRequestSchema, upsertDocumentResponseSchema };
|
||||
77
packages/server/src/services/documents/documents.ts
Normal file
77
packages/server/src/services/documents/documents.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import equal from 'fast-deep-equal';
|
||||
|
||||
import type { UpsertDocumentRequest, UpsertDocumentResponse } from './documents.schemas.ts';
|
||||
|
||||
import { DatabaseService, tableNames, type Tables } from '#root/database/database.ts';
|
||||
import type { Services } from '#root/utils/utils.services.ts';
|
||||
|
||||
class DocumentsService {
|
||||
#services: Services;
|
||||
|
||||
constructor(services: Services) {
|
||||
this.#services = services;
|
||||
}
|
||||
|
||||
public upsert = async (document: UpsertDocumentRequest): Promise<UpsertDocumentResponse> => {
|
||||
const dbService = this.#services.get(DatabaseService);
|
||||
const db = await dbService.getInstance();
|
||||
|
||||
const id = document.id || crypto.randomUUID();
|
||||
|
||||
const [current] = await db<Tables['document']>(tableNames).where({
|
||||
id,
|
||||
type: document.type,
|
||||
});
|
||||
const now = new Date();
|
||||
|
||||
if (!current) {
|
||||
await db<Tables['document']>(tableNames.documents).insert({
|
||||
id,
|
||||
type: document.type,
|
||||
createdAt: now.toISOString(),
|
||||
updatedAt: now.toISOString(),
|
||||
data: JSON.stringify(document.data),
|
||||
});
|
||||
return {
|
||||
data: document.data,
|
||||
id,
|
||||
type: document.type,
|
||||
source: document.source || null,
|
||||
createdAt: now.toISOString(),
|
||||
updatedAt: now.toISOString(),
|
||||
deletedAt: null,
|
||||
action: 'inserted',
|
||||
};
|
||||
}
|
||||
const currentData = JSON.parse(current.data);
|
||||
if (equal(currentData, document.data)) {
|
||||
return {
|
||||
...current,
|
||||
data: currentData,
|
||||
id,
|
||||
createdAt: current.createdAt,
|
||||
updatedAt: current.updatedAt,
|
||||
deletedAt: current.deletedAt || null,
|
||||
action: 'skipped',
|
||||
};
|
||||
}
|
||||
await db<Tables['document']>(tableNames.documents)
|
||||
.update({
|
||||
source: document.source,
|
||||
data: JSON.stringify(document.data),
|
||||
updatedAt: now.toISOString(),
|
||||
})
|
||||
.where({ id, type: document.type });
|
||||
return {
|
||||
...current,
|
||||
id,
|
||||
data: document.data,
|
||||
createdAt: current.createdAt,
|
||||
updatedAt: now.toISOString(),
|
||||
deletedAt: current.deletedAt || null,
|
||||
action: 'updated',
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export { DocumentsService };
|
||||
7
packages/server/src/start.ts
Normal file
7
packages/server/src/start.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { createApi } from './api/api.ts';
|
||||
|
||||
const app = await createApi();
|
||||
await app.listen({
|
||||
port: 9111,
|
||||
host: process.env.SERVER_HOST,
|
||||
});
|
||||
51
packages/server/src/utils/utils.services.ts
Normal file
51
packages/server/src/utils/utils.services.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
const destroy = Symbol('destroy');
|
||||
const instanceKey = Symbol('instances');
|
||||
|
||||
type ServiceDependency<T> = new (services: Services) => T & {
|
||||
[destroy]?: () => Promise<void> | void;
|
||||
};
|
||||
|
||||
class Services {
|
||||
[instanceKey]: Map<ServiceDependency<unknown>, unknown>;
|
||||
|
||||
constructor() {
|
||||
this[instanceKey] = new Map();
|
||||
}
|
||||
|
||||
public get = <T>(service: ServiceDependency<T>) => {
|
||||
if (!this[instanceKey].has(service)) {
|
||||
this[instanceKey].set(service, new service(this));
|
||||
}
|
||||
const instance = this[instanceKey].get(service);
|
||||
if (!instance) {
|
||||
throw new Error('Could not generate instance');
|
||||
}
|
||||
return instance as T;
|
||||
};
|
||||
|
||||
public set = <T>(service: ServiceDependency<T>, instance: Partial<T>) => {
|
||||
this[instanceKey].set(service, instance);
|
||||
};
|
||||
|
||||
public clone = () => {
|
||||
const services = new Services();
|
||||
services[instanceKey] = Object.fromEntries(this[instanceKey].entries());
|
||||
};
|
||||
|
||||
public destroy = async () => {
|
||||
await Promise.all(
|
||||
this[instanceKey].values().map(async (instance) => {
|
||||
if (
|
||||
typeof instance === 'object' &&
|
||||
instance &&
|
||||
destroy in instance &&
|
||||
typeof instance[destroy] === 'function'
|
||||
) {
|
||||
await instance[destroy]();
|
||||
}
|
||||
}),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export { Services, destroy };
|
||||
10
packages/server/tsconfig.json
Normal file
10
packages/server/tsconfig.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"extends": "@morten-olsen/reservoir-configs/tsconfig.json"
|
||||
}
|
||||
12
packages/server/vitest.config.ts
Normal file
12
packages/server/vitest.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
import { getAliases } from '@morten-olsen/reservoir-tests/vitest';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default defineConfig(async () => {
|
||||
const aliases = await getAliases();
|
||||
return {
|
||||
resolve: {
|
||||
alias: aliases,
|
||||
},
|
||||
};
|
||||
});
|
||||
4
packages/tests/.gitignore
vendored
Normal file
4
packages/tests/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/node_modules
|
||||
/dist
|
||||
/coverage
|
||||
/.env
|
||||
27
packages/tests/package.json
Normal file
27
packages/tests/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"type": "module",
|
||||
"main": "dist/exports.js",
|
||||
"scripts": {
|
||||
"build": "tsc --build"
|
||||
},
|
||||
"packageManager": "pnpm@10.6.0",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"exports": {
|
||||
".": "./dist/exports.js",
|
||||
"./vitest": "./dist/vitest.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "24.10.0",
|
||||
"@vitest/coverage-v8": "4.0.6",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "4.0.6",
|
||||
"@morten-olsen/reservoir-configs": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pnpm/find-workspace-packages": "6.0.9"
|
||||
},
|
||||
"name": "@morten-olsen/reservoir-tests",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
1
packages/tests/src/exports.ts
Normal file
1
packages/tests/src/exports.ts
Normal file
@@ -0,0 +1 @@
|
||||
console.log('Hello World');
|
||||
10
packages/tests/src/vitest.ts
Normal file
10
packages/tests/src/vitest.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { resolve } from 'node:path';
|
||||
|
||||
import { findWorkspacePackages } from '@pnpm/find-workspace-packages';
|
||||
|
||||
const getAliases = async () => {
|
||||
const packages = await findWorkspacePackages(process.cwd());
|
||||
return Object.fromEntries(packages.map((pkg) => [pkg.manifest.name, resolve(pkg.dir, 'src', 'exports.ts')]));
|
||||
};
|
||||
|
||||
export { getAliases };
|
||||
9
packages/tests/tsconfig.json
Normal file
9
packages/tests/tsconfig.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"extends": "@morten-olsen/reservoir-configs/tsconfig.json"
|
||||
}
|
||||
5603
pnpm-lock.yaml
generated
Normal file
5603
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
5
pnpm-workspace.yaml
Normal file
5
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,5 @@
|
||||
packages:
|
||||
- ./packages/*
|
||||
- ./apps/*
|
||||
onlyBuiltDependencies:
|
||||
- better-sqlite3
|
||||
16
scripts/set-version.mjs
Normal file
16
scripts/set-version.mjs
Normal file
@@ -0,0 +1,16 @@
|
||||
import { readFile, writeFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import process from 'process';
|
||||
|
||||
import { findWorkspacePackages } from '@pnpm/find-workspace-packages';
|
||||
|
||||
const packages = await findWorkspacePackages(process.cwd());
|
||||
|
||||
for (const pkg of packages) {
|
||||
const pkgPath = join(pkg.dir, 'package.json');
|
||||
const pkgJson = JSON.parse(await readFile(pkgPath, 'utf-8'));
|
||||
|
||||
pkgJson.version = process.argv[2];
|
||||
|
||||
await writeFile(pkgPath, JSON.stringify(pkgJson, null, 2) + '\n');
|
||||
}
|
||||
37
turbo.json
Normal file
37
turbo.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "https://turbo.build/schema.json",
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"outputs": [
|
||||
"dist/**",
|
||||
"public/**"
|
||||
],
|
||||
"inputs": [
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.ts",
|
||||
"./tsconfig.*",
|
||||
"../../pnpm-lock.yaml"
|
||||
]
|
||||
},
|
||||
"test": {
|
||||
"cache": false
|
||||
},
|
||||
"clean": {},
|
||||
"dev": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
},
|
||||
"demo": {
|
||||
"dependsOn": [
|
||||
"^build"
|
||||
],
|
||||
"persistent": true
|
||||
}
|
||||
}
|
||||
}
|
||||
14
vitest.config.ts
Normal file
14
vitest.config.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { defineConfig, type UserConfigExport } from 'vitest/config';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default defineConfig(async () => {
|
||||
const config: UserConfigExport = {
|
||||
test: {
|
||||
coverage: {
|
||||
provider: 'v8',
|
||||
include: ['packages/**/src/**/*.ts'],
|
||||
},
|
||||
},
|
||||
};
|
||||
return config;
|
||||
});
|
||||
Reference in New Issue
Block a user