Scaffold pnpm monorepo with lint, format, and test tooling
pnpm workspace with apps/web, apps/api, apps/collab, and packages/shared; strict TypeScript base config, repo-wide ESLint (flat) + Prettier, Vitest per package, and root scripts lint/typecheck/test/ build. @dorfteich/shared ships a first health-response helper consumed by apps/api to prove workspace linking. Existing markdown docs are reformatted once by the new Prettier setup. Closes #1 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
6517c8fb71
commit
b16d23297e
9
.editorconfig
Normal file
9
.editorconfig
Normal file
@ -0,0 +1,9 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
trim_trailing_whitespace = true
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
*.log
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.DS_Store
|
||||
4
.prettierignore
Normal file
4
.prettierignore
Normal file
@ -0,0 +1,4 @@
|
||||
dist/
|
||||
node_modules/
|
||||
coverage/
|
||||
pnpm-lock.yaml
|
||||
5
.prettierrc.json
Normal file
5
.prettierrc.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 100
|
||||
}
|
||||
28
README.md
28
README.md
@ -1,7 +1,7 @@
|
||||
# Dorfteich
|
||||
|
||||
Dorfteich is an open-source wiki system built around **ponds** (German:
|
||||
*Teiche*) — self-contained wiki spaces that people and teams organize freely
|
||||
_Teiche_) — self-contained wiki spaces that people and teams organize freely
|
||||
with hierarchical labels, directories, and Obsidian-style page relations.
|
||||
Pages are edited in a collaborative WYSIWYG editor with live cursors and
|
||||
offline support.
|
||||
@ -33,12 +33,28 @@ offline support.
|
||||
|
||||
## Repository layout
|
||||
|
||||
| Path | Contents |
|
||||
| --- | --- |
|
||||
| Path | Contents |
|
||||
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `docs/architecture/` | Architecture documentation: ADRs, data model, permission model, collaboration and plugin concepts, deployment and operations |
|
||||
| `apps/` | Application packages (web frontend, API server, collaboration server) — created as implementation proceeds |
|
||||
| `packages/` | Shared packages (types, permission logic, plugin SDK) |
|
||||
| `deploy/` | Docker Compose stacks and deployment tooling |
|
||||
| `apps/` | Application packages (web frontend, API server, collaboration server) — created as implementation proceeds |
|
||||
| `packages/` | Shared packages (types, permission logic, plugin SDK) |
|
||||
| `deploy/` | Docker Compose stacks and deployment tooling |
|
||||
|
||||
## Development
|
||||
|
||||
Requirements: Node.js ≥ 22 and [pnpm](https://pnpm.io) (`npm install -g pnpm`).
|
||||
|
||||
```sh
|
||||
pnpm install # install all workspace dependencies
|
||||
pnpm lint # ESLint + Prettier check across the repo
|
||||
pnpm typecheck # TypeScript --noEmit in every package
|
||||
pnpm test # Vitest in every package
|
||||
pnpm build # build every package (dependency order)
|
||||
```
|
||||
|
||||
The workspace packages live under `apps/` (web, api, collab) and
|
||||
`packages/` (shared). Shared logic goes into `packages/shared` and is
|
||||
imported as `@dorfteich/shared` — never copy code between apps.
|
||||
|
||||
## Status
|
||||
|
||||
|
||||
18
apps/api/package.json
Normal file
18
apps/api/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "@dorfteich/api",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "Dorfteich REST API server",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dorfteich/shared": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
9
apps/api/src/index.test.ts
Normal file
9
apps/api/src/index.test.ts
Normal file
@ -0,0 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { apiHealth } from './index';
|
||||
|
||||
describe('workspace linking', () => {
|
||||
it('api consumes @dorfteich/shared', () => {
|
||||
expect(apiHealth().service).toBe('api');
|
||||
});
|
||||
});
|
||||
7
apps/api/src/index.ts
Normal file
7
apps/api/src/index.ts
Normal file
@ -0,0 +1,7 @@
|
||||
// Placeholder entry point; replaced by the NestJS bootstrap in issue #2.
|
||||
// It already imports from @dorfteich/shared to prove workspace linking.
|
||||
import { healthResponse } from '@dorfteich/shared';
|
||||
|
||||
export function apiHealth(): ReturnType<typeof healthResponse> {
|
||||
return healthResponse('api', '0.0.0');
|
||||
}
|
||||
10
apps/api/tsconfig.json
Normal file
10
apps/api/tsconfig.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["src/**/*.test.ts"]
|
||||
}
|
||||
15
apps/collab/package.json
Normal file
15
apps/collab/package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@dorfteich/collab",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "Dorfteich collaboration server (Yjs/Hocuspocus)",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
3
apps/collab/src/index.ts
Normal file
3
apps/collab/src/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
// Placeholder entry point; the Hocuspocus server arrives with milestone M3
|
||||
// (issue #33). The package exists so workspace tooling covers it from day one.
|
||||
export const SERVICE_NAME = 'collab';
|
||||
9
apps/collab/tsconfig.json
Normal file
9
apps/collab/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
15
apps/web/package.json
Normal file
15
apps/web/package.json
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "@dorfteich/web",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "Dorfteich single-page application",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
2
apps/web/src/index.ts
Normal file
2
apps/web/src/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
// Placeholder entry point; replaced by the Vite + React app in issue #4.
|
||||
export const APP_NAME = 'Dorfteich';
|
||||
9
apps/web/tsconfig.json
Normal file
9
apps/web/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@ -52,48 +52,48 @@ flowchart LR
|
||||
|
||||
### Architecture Decision Records (`adr/`)
|
||||
|
||||
| ADR | Decision |
|
||||
| --- | --- |
|
||||
| [0001](adr/0001-typescript-monorepo.md) | TypeScript everywhere, pnpm monorepo |
|
||||
| [0002](adr/0002-postgresql.md) | PostgreSQL as the only database |
|
||||
| [0003](adr/0003-yjs-crdt-collaboration.md) | Yjs CRDT + Hocuspocus for real-time and offline collaboration |
|
||||
| [0004](adr/0004-tiptap-editor.md) | TipTap (ProseMirror) as the WYSIWYG editor |
|
||||
| [0005](adr/0005-react-vite-frontend.md) | React + Vite single-page application |
|
||||
| [0006](adr/0006-nestjs-prisma-backend.md) | NestJS + Prisma for the API server |
|
||||
| [0007](adr/0007-auth-sessions-oidc-ready.md) | Cookie sessions, Argon2id, OIDC-ready identity model |
|
||||
| [0008](adr/0008-plugin-sandbox.md) | Sandboxed iframe plugins with a message-based API |
|
||||
| [0009](adr/0009-import-export-converters.md) | Pandoc + Gotenberg sidecars for import/export |
|
||||
| [0010](adr/0010-postgres-fulltext-search.md) | PostgreSQL full-text search behind a search interface |
|
||||
| [0011](adr/0011-file-storage-quotas.md) | Filesystem volume for uploads, DB-tracked quotas |
|
||||
| [0012](adr/0012-i18n.md) | i18next with German and English from the start |
|
||||
| [0013](adr/0013-versioning-and-trash.md) | Page version history via Yjs snapshots, soft-delete trash |
|
||||
| [0014](adr/0014-gitea-actions-cicd.md) | CI/CD with Gitea Actions, staged promotion |
|
||||
| [0015](adr/0015-backup-strategy.md) | Nightly pg_dump + uploads sync, 30-day retention, off-host mirror |
|
||||
| [0016](adr/0016-self-hosted-fonts.md) | Self-hosted Google Fonts, per-pond font configuration |
|
||||
| ADR | Decision |
|
||||
| -------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| [0001](adr/0001-typescript-monorepo.md) | TypeScript everywhere, pnpm monorepo |
|
||||
| [0002](adr/0002-postgresql.md) | PostgreSQL as the only database |
|
||||
| [0003](adr/0003-yjs-crdt-collaboration.md) | Yjs CRDT + Hocuspocus for real-time and offline collaboration |
|
||||
| [0004](adr/0004-tiptap-editor.md) | TipTap (ProseMirror) as the WYSIWYG editor |
|
||||
| [0005](adr/0005-react-vite-frontend.md) | React + Vite single-page application |
|
||||
| [0006](adr/0006-nestjs-prisma-backend.md) | NestJS + Prisma for the API server |
|
||||
| [0007](adr/0007-auth-sessions-oidc-ready.md) | Cookie sessions, Argon2id, OIDC-ready identity model |
|
||||
| [0008](adr/0008-plugin-sandbox.md) | Sandboxed iframe plugins with a message-based API |
|
||||
| [0009](adr/0009-import-export-converters.md) | Pandoc + Gotenberg sidecars for import/export |
|
||||
| [0010](adr/0010-postgres-fulltext-search.md) | PostgreSQL full-text search behind a search interface |
|
||||
| [0011](adr/0011-file-storage-quotas.md) | Filesystem volume for uploads, DB-tracked quotas |
|
||||
| [0012](adr/0012-i18n.md) | i18next with German and English from the start |
|
||||
| [0013](adr/0013-versioning-and-trash.md) | Page version history via Yjs snapshots, soft-delete trash |
|
||||
| [0014](adr/0014-gitea-actions-cicd.md) | CI/CD with Gitea Actions, staged promotion |
|
||||
| [0015](adr/0015-backup-strategy.md) | Nightly pg_dump + uploads sync, 30-day retention, off-host mirror |
|
||||
| [0016](adr/0016-self-hosted-fonts.md) | Self-hosted Google Fonts, per-pond font configuration |
|
||||
|
||||
### Concept documents
|
||||
|
||||
| Document | Contents |
|
||||
| --- | --- |
|
||||
| [data-model.md](data-model.md) | Entities and relations: users, ponds, pages, labels, grants, quotas, plugins |
|
||||
| [permissions.md](permissions.md) | Role model and the "most specific setting wins" resolution algorithm |
|
||||
| [realtime-collaboration.md](realtime-collaboration.md) | CRDT document lifecycle, cursor sync, offline behavior, versioning hooks |
|
||||
| [plugin-architecture.md](plugin-architecture.md) | Plugin manifest, packaging, sandbox runtime, extension points, admin flows |
|
||||
| [deployment.md](deployment.md) | Compose stacks for Dev/Test/Int/Prod, environments, promotion pipeline |
|
||||
| [operations.md](operations.md) | Monitoring, logging, backup/restore, update strategy for self-hosters |
|
||||
| [security.md](security.md) | Threat-driven security concept: authn/authz, sandboxing, uploads, secrets |
|
||||
| [roadmap.md](roadmap.md) | Epics and milestones; the order stories are implemented in |
|
||||
| Document | Contents |
|
||||
| ------------------------------------------------------ | ---------------------------------------------------------------------------- |
|
||||
| [data-model.md](data-model.md) | Entities and relations: users, ponds, pages, labels, grants, quotas, plugins |
|
||||
| [permissions.md](permissions.md) | Role model and the "most specific setting wins" resolution algorithm |
|
||||
| [realtime-collaboration.md](realtime-collaboration.md) | CRDT document lifecycle, cursor sync, offline behavior, versioning hooks |
|
||||
| [plugin-architecture.md](plugin-architecture.md) | Plugin manifest, packaging, sandbox runtime, extension points, admin flows |
|
||||
| [deployment.md](deployment.md) | Compose stacks for Dev/Test/Int/Prod, environments, promotion pipeline |
|
||||
| [operations.md](operations.md) | Monitoring, logging, backup/restore, update strategy for self-hosters |
|
||||
| [security.md](security.md) | Threat-driven security concept: authn/authz, sandboxing, uploads, secrets |
|
||||
| [roadmap.md](roadmap.md) | Epics and milestones; the order stories are implemented in |
|
||||
|
||||
## Terminology
|
||||
|
||||
| German (product vision) | English (code, docs, issues) |
|
||||
| --- | --- |
|
||||
| Teich | pond |
|
||||
| Seite | page |
|
||||
| Teich-Admin | Pond Admin |
|
||||
| Bearbeitende | Editor |
|
||||
| Lesende | Reader |
|
||||
| Öffentlichkeit | Public |
|
||||
| ----------------------- | ---------------------------- |
|
||||
| Teich | pond |
|
||||
| Seite | page |
|
||||
| Teich-Admin | Pond Admin |
|
||||
| Bearbeitende | Editor |
|
||||
| Lesende | Reader |
|
||||
| Öffentlichkeit | Public |
|
||||
|
||||
## Conventions for implementers
|
||||
|
||||
|
||||
@ -32,9 +32,9 @@ confirmed).
|
||||
- editors per pond (default 5) and readers per pond (default 50) for
|
||||
self-signup personal ponds,
|
||||
- additional ponds a user may create (default 0).
|
||||
Current usage is tracked in the database (`pond_usage`), updated
|
||||
transactionally with upload/delete; quota exceedance yields a clear,
|
||||
i18n-ed error.
|
||||
Current usage is tracked in the database (`pond_usage`), updated
|
||||
transactionally with upload/delete; quota exceedance yields a clear,
|
||||
i18n-ed error.
|
||||
- **Orphan cleanup**: deleting a page moves it to trash (ADR 0013); its
|
||||
files are removed when the trash entry is purged. A nightly job reconciles
|
||||
volume contents against the database and reports drift.
|
||||
|
||||
@ -29,7 +29,7 @@ natural hooks for both.
|
||||
the current version (diff computed on the derived plain/Markdown
|
||||
representation — good enough for "what changed", no structural diff UI in
|
||||
v1).
|
||||
- **Restore**: restoring creates a *new* state on top of history (the
|
||||
- **Restore**: restoring creates a _new_ state on top of history (the
|
||||
restored content is applied as a regular update, preceded by an automatic
|
||||
"pre-restore" snapshot). History is append-only; nothing is rewritten.
|
||||
- **Compaction**: to bound Yjs update-log growth, the persistence layer
|
||||
@ -64,5 +64,5 @@ natural hooks for both.
|
||||
rejected.
|
||||
- **Storing Markdown snapshots instead of Yjs states**: smaller, but restore
|
||||
would lose structure/plugin blocks and break the CRDT continuity;
|
||||
rejected (Markdown diffs are still used for the *display* layer).
|
||||
rejected (Markdown diffs are still used for the _display_ layer).
|
||||
- **Hard delete only**: data-loss risk contradicts kickoff decision.
|
||||
|
||||
@ -35,7 +35,7 @@ automated and objective wherever possible.
|
||||
start (ADR 0006); releases with breaking migrations must say so in the
|
||||
release notes.
|
||||
- **Deploy mechanism**: the runner executes `docker compose pull && docker
|
||||
compose up -d` in the stage's directory (`/home/DOCKER/dorfteich-<stage>/`)
|
||||
compose up -d` in the stage's directory (`/home/DOCKER/dorfteich-<stage>/`)
|
||||
over SSH (deploy key per stage). Moving Prod to a dedicated host later
|
||||
changes only that SSH target.
|
||||
- **Self-hosters** consume the same release images via a published
|
||||
|
||||
@ -32,42 +32,47 @@ erDiagram
|
||||
## Identity and access
|
||||
|
||||
### `users`
|
||||
| Column | Notes |
|
||||
| --- | --- |
|
||||
| `id` (uuid) | |
|
||||
| `username` | unique, URL-safe |
|
||||
| `email` | unique, stored verified/unverified with `email_verified_at` |
|
||||
| `display_name` | shown at cursors, comments |
|
||||
| `locale` | UI language (ADR 0012) |
|
||||
| `is_site_admin` | boolean; Site Admin is a user flag, not a grant |
|
||||
| `status` | `active` / `disabled` / `pending_verification` |
|
||||
| `created_at`, `last_login_at` | |
|
||||
|
||||
| Column | Notes |
|
||||
| ----------------------------- | ----------------------------------------------------------- |
|
||||
| `id` (uuid) | |
|
||||
| `username` | unique, URL-safe |
|
||||
| `email` | unique, stored verified/unverified with `email_verified_at` |
|
||||
| `display_name` | shown at cursors, comments |
|
||||
| `locale` | UI language (ADR 0012) |
|
||||
| `is_site_admin` | boolean; Site Admin is a user flag, not a grant |
|
||||
| `status` | `active` / `disabled` / `pending_verification` |
|
||||
| `created_at`, `last_login_at` | |
|
||||
|
||||
### `user_identities` (ADR 0007)
|
||||
|
||||
`user_id`, `provider` (`password` now; `oidc:<issuer>` later), `subject`,
|
||||
`credential` (Argon2id hash for `password`), unique on
|
||||
(`provider`, `subject`).
|
||||
|
||||
### `sessions`
|
||||
|
||||
Opaque id (hashed), `user_id`, `created_at`, `expires_at`, `last_seen_at`,
|
||||
user-agent summary (for "active sessions" UI).
|
||||
|
||||
### `auth_tokens`
|
||||
|
||||
Single-use tokens for e-mail verification and password reset: hashed token,
|
||||
`purpose`, `expires_at`, `consumed_at`.
|
||||
|
||||
### `role_grants` — the permission table (see `permissions.md`)
|
||||
| Column | Notes |
|
||||
| --- | --- |
|
||||
| `id` | |
|
||||
| `pond_id` | every grant belongs to exactly one pond |
|
||||
| `subject_type` | `user` / `authenticated` / `public` |
|
||||
| `subject_id` | user id when `subject_type = user`, else null |
|
||||
| `role` | `pond_admin` / `editor` / `reader` |
|
||||
| `scope_type` | `pond` / `label` / `page` |
|
||||
| `scope_id` | label id or page id when scoped, else null |
|
||||
| `effect` | `allow` / `deny` |
|
||||
| `created_by`, `created_at` | audit |
|
||||
|
||||
| Column | Notes |
|
||||
| -------------------------- | --------------------------------------------- |
|
||||
| `id` | |
|
||||
| `pond_id` | every grant belongs to exactly one pond |
|
||||
| `subject_type` | `user` / `authenticated` / `public` |
|
||||
| `subject_id` | user id when `subject_type = user`, else null |
|
||||
| `role` | `pond_admin` / `editor` / `reader` |
|
||||
| `scope_type` | `pond` / `label` / `page` |
|
||||
| `scope_id` | label id or page id when scoped, else null |
|
||||
| `effect` | `allow` / `deny` |
|
||||
| `created_by`, `created_at` | audit |
|
||||
|
||||
Unique on (`pond_id`, `subject_type`, `subject_id`, `role`, `scope_type`,
|
||||
`scope_id`). `pond_admin` grants are only valid with `scope_type = pond`
|
||||
@ -76,48 +81,55 @@ and `subject_type = user`.
|
||||
## Content
|
||||
|
||||
### `ponds`
|
||||
| Column | Notes |
|
||||
| --- | --- |
|
||||
| `id`, `slug` (unique), `name`, `description` | |
|
||||
| `type` | `personal` (one per user, from self-signup) / `shared` |
|
||||
| `owner_id` | creator; personal ponds: the user it belongs to |
|
||||
| `settings` (jsonb) | fonts (ADR 0016), sidebar sort mode (`alpha` / `created` / `manual`), default page permissions |
|
||||
| `deleted_at`, `deleted_by` | pond-level trash (ADR 0013) |
|
||||
|
||||
| Column | Notes |
|
||||
| -------------------------------------------- | ---------------------------------------------------------------------------------------------- |
|
||||
| `id`, `slug` (unique), `name`, `description` | |
|
||||
| `type` | `personal` (one per user, from self-signup) / `shared` |
|
||||
| `owner_id` | creator; personal ponds: the user it belongs to |
|
||||
| `settings` (jsonb) | fonts (ADR 0016), sidebar sort mode (`alpha` / `created` / `manual`), default page permissions |
|
||||
| `deleted_at`, `deleted_by` | pond-level trash (ADR 0013) |
|
||||
|
||||
### `pages`
|
||||
| Column | Notes |
|
||||
| --- | --- |
|
||||
| `id`, `pond_id` | |
|
||||
| `title` | also indexed for search weight A |
|
||||
| `slug` | unique per pond, for stable URLs and wikilink resolution |
|
||||
| `ydoc_state` (bytea) | current merged Yjs state (ADR 0003) |
|
||||
| `ydoc_updates` | append log table `page_updates(page_id, seq, update bytea)`, compacted periodically |
|
||||
| `sort_key` | manual sidebar ordering (fractional indexing) |
|
||||
| `created_by`, `created_at`, `updated_at` | |
|
||||
| `deleted_at`, `deleted_by` | trash |
|
||||
|
||||
| Column | Notes |
|
||||
| ---------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| `id`, `pond_id` | |
|
||||
| `title` | also indexed for search weight A |
|
||||
| `slug` | unique per pond, for stable URLs and wikilink resolution |
|
||||
| `ydoc_state` (bytea) | current merged Yjs state (ADR 0003) |
|
||||
| `ydoc_updates` | append log table `page_updates(page_id, seq, update bytea)`, compacted periodically |
|
||||
| `sort_key` | manual sidebar ordering (fractional indexing) |
|
||||
| `created_by`, `created_at`, `updated_at` | |
|
||||
| `deleted_at`, `deleted_by` | trash |
|
||||
|
||||
### `page_content_cache`
|
||||
|
||||
One row per page, refreshed on persistence: `plain_text`, `markdown`,
|
||||
`html`, `outline` (headings JSON, for TOC plugins), generated `tsvector`
|
||||
column with GIN index (ADR 0010).
|
||||
|
||||
### `page_versions` (ADR 0013)
|
||||
|
||||
`page_id`, `ydoc_snapshot` (bytea, self-contained), `trigger`
|
||||
(`auto` / `manual` / `pre_restore`), `label`, `contributor_ids`,
|
||||
`created_at`.
|
||||
|
||||
### `labels`
|
||||
|
||||
`pond_id`, `name`, `parent_id` (nullable — hierarchy), `color`, unique on
|
||||
(`pond_id`, `parent_id`, `name`). Cycles are rejected at write time.
|
||||
`page_labels(page_id, label_id)` is the assignment table.
|
||||
|
||||
### `page_links`
|
||||
|
||||
Wikilink index maintained on persistence: `from_page_id`, `to_page_id`
|
||||
(nullable when target does not exist yet — "phantom" links), `target_slug`.
|
||||
Backlinks = query by `to_page_id`. Creating a page with a phantom-linked
|
||||
slug resolves those rows.
|
||||
|
||||
### `attachments` (ADR 0011)
|
||||
|
||||
`id`, `pond_id`, `page_id` (nullable — pond-level files), `file_name`,
|
||||
`mime_type`, `size_bytes`, `storage_path`, `uploaded_by`, `created_at`,
|
||||
`deleted_at`.
|
||||
@ -125,23 +137,27 @@ slug resolves those rows.
|
||||
## Plugins (ADR 0008)
|
||||
|
||||
### `plugins`
|
||||
|
||||
`id` (manifest id), `version`, `manifest` (jsonb), `storage_path`,
|
||||
`kind` (`code` / `section_style`), `instance_mode`
|
||||
(`disabled` / `optional` / `required`), `installed_by`, `installed_at`.
|
||||
|
||||
### `pond_plugins`
|
||||
|
||||
(`pond_id`, `plugin_id`, `enabled`) — only meaningful for `optional`
|
||||
plugins; `required` plugins are active everywhere.
|
||||
|
||||
## Quotas and settings
|
||||
|
||||
### `instance_settings`
|
||||
|
||||
Key-value (typed JSON) singleton set: registration mode, SMTP config
|
||||
(secrets referenced from env, not stored plaintext — see `security.md`),
|
||||
default quotas, upload allowlist, legal pages content (imprint, privacy),
|
||||
instance default locale.
|
||||
|
||||
### `quota_overrides`
|
||||
|
||||
`subject_type` (`user` / `pond`), `subject_id`, `quota_key`
|
||||
(`editors_per_pond`, `readers_per_pond`, `additional_ponds`,
|
||||
`storage_bytes`, `max_file_bytes`), `value`. Resolution: pond override →
|
||||
@ -149,6 +165,7 @@ user override → instance default (most specific wins, mirroring the
|
||||
permission philosophy).
|
||||
|
||||
### `pond_usage`
|
||||
|
||||
Cached counters per pond: `storage_bytes_used`, `editor_count`,
|
||||
`reader_count` — updated transactionally, reconciled nightly.
|
||||
|
||||
|
||||
@ -11,15 +11,15 @@ choice and any later move cheap). DNS status: `*.dorfteich.online` and
|
||||
|
||||
Every stage (and every self-hosted instance) runs the same services:
|
||||
|
||||
| Service | Image | Notes |
|
||||
| --- | --- | --- |
|
||||
| `web` | `dorfteich-web` | nginx: SPA assets, fonts; SPA fallback routing |
|
||||
| `api` | `dorfteich-api` | NestJS; runs `prisma migrate deploy` on start |
|
||||
| `collab` | `dorfteich-collab` | Hocuspocus WebSocket server |
|
||||
| `db` | `postgres:<pinned>` | volume `db-data` |
|
||||
| `pandoc` | `pandoc/core:<pinned>` (server mode) | internal only |
|
||||
| `gotenberg` | `gotenberg/gotenberg:<pinned>` | internal only |
|
||||
| `backup` | `dorfteich-backup` | cron sidecar: pg_dump, volume archive, prune, mirror (ADR 0015) |
|
||||
| Service | Image | Notes |
|
||||
| ----------- | ------------------------------------ | --------------------------------------------------------------- |
|
||||
| `web` | `dorfteich-web` | nginx: SPA assets, fonts; SPA fallback routing |
|
||||
| `api` | `dorfteich-api` | NestJS; runs `prisma migrate deploy` on start |
|
||||
| `collab` | `dorfteich-collab` | Hocuspocus WebSocket server |
|
||||
| `db` | `postgres:<pinned>` | volume `db-data` |
|
||||
| `pandoc` | `pandoc/core:<pinned>` (server mode) | internal only |
|
||||
| `gotenberg` | `gotenberg/gotenberg:<pinned>` | internal only |
|
||||
| `backup` | `dorfteich-backup` | cron sidecar: pg_dump, volume archive, prune, mirror (ADR 0015) |
|
||||
|
||||
Volumes: `db-data`, `uploads` (uploads + installed plugins), `backups`.
|
||||
Networks: `frontend` (reverse proxy ↔ web/api/collab) and `internal`
|
||||
@ -40,12 +40,12 @@ Self-hosters without a proxy can enable the optional `caddy` Compose profile
|
||||
|
||||
## Stages
|
||||
|
||||
| Stage | Where | Domain | Purpose | Data |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| **Dev** | contributor machine (e.g. the operator's MacBook), Docker Desktop | `localhost` | feature work; hot reload via `compose.dev.yml` overlay (source mounts, vite dev server) | fixtures/seed script |
|
||||
| **Test** | VPS `188.245.116.44`, `/home/DOCKER/dorfteich-test/` | `test.dorfteich.cloud` | auto-deploy target of `main`; e2e suite runs here | reset-able; seeded |
|
||||
| **Int** | VPS `188.245.116.44`, `/home/DOCKER/dorfteich-int/` | `int.dorfteich.cloud` | stable preview; manual/exploratory testing; release candidates | persistent test data |
|
||||
| **Prod** | host decided at go-live (M8): the VPS or a dedicated host | `dorfteich.online` | public flagship instance | real data; full backup + mirror |
|
||||
| Stage | Where | Domain | Purpose | Data |
|
||||
| -------- | ----------------------------------------------------------------- | ---------------------- | --------------------------------------------------------------------------------------- | ------------------------------- |
|
||||
| **Dev** | contributor machine (e.g. the operator's MacBook), Docker Desktop | `localhost` | feature work; hot reload via `compose.dev.yml` overlay (source mounts, vite dev server) | fixtures/seed script |
|
||||
| **Test** | VPS `188.245.116.44`, `/home/DOCKER/dorfteich-test/` | `test.dorfteich.cloud` | auto-deploy target of `main`; e2e suite runs here | reset-able; seeded |
|
||||
| **Int** | VPS `188.245.116.44`, `/home/DOCKER/dorfteich-int/` | `int.dorfteich.cloud` | stable preview; manual/exploratory testing; release candidates | persistent test data |
|
||||
| **Prod** | host decided at go-live (M8): the VPS or a dedicated host | `dorfteich.online` | public flagship instance | real data; full backup + mirror |
|
||||
|
||||
Stage layout follows the operator's Docker host convention:
|
||||
compose file + `.env` under `/home/DOCKER/dorfteich-<stage>/`, bulk data
|
||||
|
||||
@ -53,14 +53,14 @@ monitoring, structured logs, backup alerting — no dedicated metrics stack.
|
||||
|
||||
## Maintenance jobs (in-app scheduler, `jobs` table)
|
||||
|
||||
| Job | Cadence | Purpose |
|
||||
| --- | --- | --- |
|
||||
| trash purge | daily | delete pages/ponds past trash retention (ADR 0013) |
|
||||
| version thinning | daily | auto-version retention policy (ADR 0013) |
|
||||
| update-log compaction | hourly, idle pages only | bound Yjs log growth |
|
||||
| quota reconciliation | nightly | recompute `pond_usage`, report drift |
|
||||
| orphan file sweep | nightly | volume ↔ DB consistency (ADR 0011) |
|
||||
| mail outbox retry | every minute | e-mail delivery with backoff |
|
||||
| Job | Cadence | Purpose |
|
||||
| --------------------- | ----------------------- | -------------------------------------------------- |
|
||||
| trash purge | daily | delete pages/ponds past trash retention (ADR 0013) |
|
||||
| version thinning | daily | auto-version retention policy (ADR 0013) |
|
||||
| update-log compaction | hourly, idle pages only | bound Yjs log growth |
|
||||
| quota reconciliation | nightly | recompute `pond_usage`, report drift |
|
||||
| orphan file sweep | nightly | volume ↔ DB consistency (ADR 0011) |
|
||||
| mail outbox retry | every minute | e-mail delivery with backoff |
|
||||
|
||||
Job outcomes are visible in the Site Admin UI (last run, status) — that
|
||||
panel is the operator's single glance for instance health.
|
||||
@ -78,9 +78,9 @@ panel is the operator's single glance for instance health.
|
||||
|
||||
## Capacity & limits (initial values, instance-tunable)
|
||||
|
||||
| Limit | Default |
|
||||
| --- | --- |
|
||||
| max page document size | 5 MiB Yjs state |
|
||||
| max upload size | 25 MiB (quota ladder, ADR 0011) |
|
||||
| collab connections per instance | 500 concurrent |
|
||||
| rate limits | login 10/min/IP, signup 5/h/IP, API 100/min/user |
|
||||
| Limit | Default |
|
||||
| ------------------------------- | ------------------------------------------------ |
|
||||
| max page document size | 5 MiB Yjs state |
|
||||
| max upload size | 25 MiB (quota ladder, ADR 0011) |
|
||||
| collab connections per instance | 500 concurrent |
|
||||
| rate limits | login 10/min/IP, signup 5/h/IP, API 100/min/user |
|
||||
|
||||
@ -8,13 +8,13 @@ never enforces security).
|
||||
|
||||
## Roles
|
||||
|
||||
| Role | Scope of existence | Capabilities |
|
||||
| --- | --- | --- |
|
||||
| **Site Admin** | instance (user flag) | everything: instance settings, users, quotas, plugins (install + set `disabled`/`optional`/`required`), all ponds' administration, legal pages, backups |
|
||||
| **Pond Admin** | per pond (grant) | manage the pond: settings/fonts, labels, members and their grants, enable/disable optional plugins, trash, quotas view; implies Editor everywhere in the pond |
|
||||
| **Editor** | per pond/label/page (grant) | create/edit/delete pages within the granted scope; view history, restore versions, use trash for pages they may edit |
|
||||
| **Reader** | per pond/label/page (grant) | read pages within the granted scope; no history access |
|
||||
| **Public** | pseudo-subject | what non-authenticated visitors may read (never write) |
|
||||
| Role | Scope of existence | Capabilities |
|
||||
| -------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **Site Admin** | instance (user flag) | everything: instance settings, users, quotas, plugins (install + set `disabled`/`optional`/`required`), all ponds' administration, legal pages, backups |
|
||||
| **Pond Admin** | per pond (grant) | manage the pond: settings/fonts, labels, members and their grants, enable/disable optional plugins, trash, quotas view; implies Editor everywhere in the pond |
|
||||
| **Editor** | per pond/label/page (grant) | create/edit/delete pages within the granted scope; view history, restore versions, use trash for pages they may edit |
|
||||
| **Reader** | per pond/label/page (grant) | read pages within the granted scope; no history access |
|
||||
| **Public** | pseudo-subject | what non-authenticated visitors may read (never write) |
|
||||
|
||||
Additional structural rules:
|
||||
|
||||
@ -34,11 +34,11 @@ A grant is `(subject, role, scope, effect)` inside one pond
|
||||
`public` (everyone, including anonymous visitors).
|
||||
- **scope**: the whole pond, one label, or one page.
|
||||
- **effect**: `allow` or `deny`. `deny` expresses the vision's "all pages
|
||||
*except* label X" (pond-scope allow + label-scope deny).
|
||||
_except_ label X" (pond-scope allow + label-scope deny).
|
||||
|
||||
## Resolution algorithm
|
||||
|
||||
Question: *may user U perform action A (read / write) on page P?*
|
||||
Question: _may user U perform action A (read / write) on page P?_
|
||||
|
||||
1. If U is Site Admin → **allow**.
|
||||
2. If the page or its pond is in trash → only roles that could edit it may
|
||||
@ -64,12 +64,12 @@ Question: *may user U perform action A (read / write) on page P?*
|
||||
|
||||
Pond "Handbook", user Uma has pond-scope `editor` (allow):
|
||||
|
||||
| Extra grants | Uma edits page "Salaries"? | Why |
|
||||
| --- | --- | --- |
|
||||
| — | yes | pond-scope allow |
|
||||
| label `confidential` on the page + label-scope `editor` **deny** for Uma | no | label level is more specific than pond level |
|
||||
| additionally page-scope `editor` **allow** for Uma on "Salaries" | yes | page level beats label level |
|
||||
| page has labels `confidential` (deny Uma) and `hr` (allow Uma) | no | same level → deny wins |
|
||||
| Extra grants | Uma edits page "Salaries"? | Why |
|
||||
| ------------------------------------------------------------------------ | -------------------------- | -------------------------------------------- |
|
||||
| — | yes | pond-scope allow |
|
||||
| label `confidential` on the page + label-scope `editor` **deny** for Uma | no | label level is more specific than pond level |
|
||||
| additionally page-scope `editor` **allow** for Uma on "Salaries" | yes | page level beats label level |
|
||||
| page has labels `confidential` (deny Uma) and `hr` (allow Uma) | no | same level → deny wins |
|
||||
|
||||
"Only pages with label Y" (vision) = no pond-scope editor grant + label-Y
|
||||
`editor` allow. "All except label X" = pond-scope allow + label-X deny.
|
||||
|
||||
@ -26,7 +26,11 @@ my-plugin.zip
|
||||
"apiVersion": "1",
|
||||
"kind": "code",
|
||||
"extensionPoints": [
|
||||
{ "type": "pageTool", "id": "toc", "title": { "de": "Inhaltsverzeichnis", "en": "Table of contents" } }
|
||||
{
|
||||
"type": "pageTool",
|
||||
"id": "toc",
|
||||
"title": { "de": "Inhaltsverzeichnis", "en": "Table of contents" }
|
||||
}
|
||||
],
|
||||
"permissions": ["readCurrentPage"],
|
||||
"fallback": { "type": "text", "value": "[Table of contents]" },
|
||||
@ -44,11 +48,11 @@ my-plugin.zip
|
||||
|
||||
## Kinds and extension points
|
||||
|
||||
| Kind | Extension point | What it does | Sandbox |
|
||||
| --- | --- | --- | --- |
|
||||
| `section_style` | `sectionStyle` | declares named styles (name, i18n label, CSS class body) applicable to container blocks — e.g. colored background boxes | none needed: CSS is sanitized (no `@import`, no `url()` to external hosts) and scoped under `.dt-style-<pluginId>-<styleId>` |
|
||||
| `code` | `block` | a custom editor block (diagram, embed, …); host registers a ProseMirror node `plugin_block` instance with `pluginId`, `blockType`, `data` attrs | sandboxed iframe per block |
|
||||
| `code` | `pageTool` | read-only widget rendered in the page tools panel or embedded as a block (TOC, page index, cross-page block embed) | sandboxed iframe |
|
||||
| Kind | Extension point | What it does | Sandbox |
|
||||
| --------------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `section_style` | `sectionStyle` | declares named styles (name, i18n label, CSS class body) applicable to container blocks — e.g. colored background boxes | none needed: CSS is sanitized (no `@import`, no `url()` to external hosts) and scoped under `.dt-style-<pluginId>-<styleId>` |
|
||||
| `code` | `block` | a custom editor block (diagram, embed, …); host registers a ProseMirror node `plugin_block` instance with `pluginId`, `blockType`, `data` attrs | sandboxed iframe per block |
|
||||
| `code` | `pageTool` | read-only widget rendered in the page tools panel or embedded as a block (TOC, page index, cross-page block embed) | sandboxed iframe |
|
||||
|
||||
## Sandbox runtime
|
||||
|
||||
@ -57,7 +61,7 @@ my-plugin.zip
|
||||
parent DOM. The iframe document is generated by the host and loads only
|
||||
the plugin bundle + its assets from the plugin's static path.
|
||||
- CSP on plugin frames: `default-src 'none'; script-src <plugin path>;
|
||||
img-src <plugin path> blob: data:; style-src <plugin path> 'unsafe-inline'`.
|
||||
img-src <plugin path> blob: data:; style-src <plugin path> 'unsafe-inline'`.
|
||||
No network access (`connect-src 'none'`) in v1.
|
||||
- Host ↔ plugin communication: `postMessage` RPC with structured-clone
|
||||
payloads. `packages/plugin-sdk` provides both sides:
|
||||
@ -72,13 +76,13 @@ All calls are mediated by the host and executed against the REST API with
|
||||
the **viewing user's** session — a plugin can never read more than the
|
||||
person looking at it could.
|
||||
|
||||
| Capability | Methods |
|
||||
| --- | --- |
|
||||
| `readCurrentPage` | `getOutline()`, `getContent()` (Markdown), `getMeta()` |
|
||||
| `readPond` | `listPages()`, `getPageOutline(pageId)`, `getPageContent(pageId)` |
|
||||
| `readBlock` | `getBlock(pageId, blockId)` — cross-page block embedding |
|
||||
| `blockData` | `getData()` / `setData(data)` for the plugin's own block instance (writes go through the editor as a normal document change — requires the viewer to have write permission) |
|
||||
| `ui` | `resize(height)`, `openPage(pageId)` (host navigates), `toast(msgKey)` |
|
||||
| Capability | Methods |
|
||||
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `readCurrentPage` | `getOutline()`, `getContent()` (Markdown), `getMeta()` |
|
||||
| `readPond` | `listPages()`, `getPageOutline(pageId)`, `getPageContent(pageId)` |
|
||||
| `readBlock` | `getBlock(pageId, blockId)` — cross-page block embedding |
|
||||
| `blockData` | `getData()` / `setData(data)` for the plugin's own block instance (writes go through the editor as a normal document change — requires the viewer to have write permission) |
|
||||
| `ui` | `resize(height)`, `openPage(pageId)` (host navigates), `toast(msgKey)` |
|
||||
|
||||
## Lifecycle & administration
|
||||
|
||||
|
||||
@ -88,9 +88,9 @@ sequenceDiagram
|
||||
|
||||
## Failure modes
|
||||
|
||||
| Failure | Behavior |
|
||||
| --- | --- |
|
||||
| collab container down | editing degrades to offline mode (local persistence); banner "reconnecting…"; REST reads unaffected |
|
||||
| WebSocket blocked (proxy) | same as above; deployment docs require WebSocket pass-through for `/collab` |
|
||||
| stale collab token | client transparently re-fetches and reconnects |
|
||||
| document too large | server rejects updates beyond a size ceiling (instance setting) with a user-visible error; prevents runaway documents |
|
||||
| Failure | Behavior |
|
||||
| ------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| collab container down | editing degrades to offline mode (local persistence); banner "reconnecting…"; REST reads unaffected |
|
||||
| WebSocket blocked (proxy) | same as above; deployment docs require WebSocket pass-through for `/collab` |
|
||||
| stale collab token | client transparently re-fetches and reconnects |
|
||||
| document too large | server rejects updates beyond a size ceiling (instance setting) with a user-visible error; prevents runaway documents |
|
||||
|
||||
@ -72,7 +72,7 @@ or sloppy plugin authors, compromised dependencies.
|
||||
entered in the setup wizard is written to the env-backed secret store,
|
||||
not to a DB row).
|
||||
- Key rotation: collab signing key and session pepper rotate via env change
|
||||
+ rolling restart; procedure documented in `operations.md` runbooks.
|
||||
plus rolling restart; procedure documented in `operations.md` runbooks.
|
||||
- Dependencies: lockfile-pinned; monthly update batch; images pinned to
|
||||
digests in Prod.
|
||||
|
||||
|
||||
26
eslint.config.mjs
Normal file
26
eslint.config.mjs
Normal file
@ -0,0 +1,26 @@
|
||||
// Repo-wide ESLint flat config. Packages inherit these rules; add
|
||||
// package-specific overrides here (scoped by `files`) rather than with
|
||||
// per-package config files, so rules stay consistent across the monorepo.
|
||||
import js from '@eslint/js';
|
||||
import tseslint from 'typescript-eslint';
|
||||
import prettier from 'eslint-config-prettier';
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: ['**/dist/**', '**/node_modules/**', '**/coverage/**', '**/*.gen.ts'],
|
||||
},
|
||||
js.configs.recommended,
|
||||
...tseslint.configs.recommended,
|
||||
prettier,
|
||||
{
|
||||
rules: {
|
||||
// Unused values are usually bugs; underscore-prefix marks intentional ones.
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
|
||||
],
|
||||
// `any` defeats the shared Zod/type contracts between client and server.
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
},
|
||||
},
|
||||
);
|
||||
27
package.json
Normal file
27
package.json
Normal file
@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "dorfteich",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "Dorfteich — an open-source wiki system with real-time collaboration",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"packageManager": "pnpm@11.9.0",
|
||||
"scripts": {
|
||||
"lint": "eslint . && prettier --check .",
|
||||
"format": "prettier --write .",
|
||||
"typecheck": "pnpm -r run typecheck",
|
||||
"test": "pnpm -r run test",
|
||||
"build": "pnpm -r run build",
|
||||
"i18n:check": "node scripts/i18n-check.mjs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.20.0",
|
||||
"eslint": "^9.20.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"prettier": "^3.5.0",
|
||||
"typescript": "^5.7.0",
|
||||
"typescript-eslint": "^8.24.0"
|
||||
}
|
||||
}
|
||||
32
packages/shared/package.json
Normal file
32
packages/shared/package.json
Normal file
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "@dorfteich/shared",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"description": "Types, schemas, and logic shared between web, api, and collab",
|
||||
"license": "MIT",
|
||||
"main": "./dist/index.cjs",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist/index.cjs"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsup": "^8.3.0",
|
||||
"vitest": "^3.0.0"
|
||||
}
|
||||
}
|
||||
13
packages/shared/src/health.test.ts
Normal file
13
packages/shared/src/health.test.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { healthResponse } from './health';
|
||||
|
||||
describe('healthResponse', () => {
|
||||
it('reports ok with service, version, and a valid timestamp', () => {
|
||||
const res = healthResponse('api', '1.2.3');
|
||||
expect(res.status).toBe('ok');
|
||||
expect(res.service).toBe('api');
|
||||
expect(res.version).toBe('1.2.3');
|
||||
expect(Number.isNaN(Date.parse(res.time))).toBe(false);
|
||||
});
|
||||
});
|
||||
23
packages/shared/src/health.ts
Normal file
23
packages/shared/src/health.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* Shape of the liveness responses served by every Dorfteich service
|
||||
* (`/healthz` on web, api, and collab). Readiness (`/readyz`) has a richer,
|
||||
* service-specific shape and lives with the api.
|
||||
*/
|
||||
export interface HealthResponse {
|
||||
status: 'ok';
|
||||
service: 'web' | 'api' | 'collab';
|
||||
version: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export function healthResponse(
|
||||
service: HealthResponse['service'],
|
||||
version: string,
|
||||
): HealthResponse {
|
||||
return {
|
||||
status: 'ok',
|
||||
service,
|
||||
version,
|
||||
time: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
1
packages/shared/src/index.ts
Normal file
1
packages/shared/src/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './health';
|
||||
9
packages/shared/tsconfig.json
Normal file
9
packages/shared/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"outDir": "dist"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
2472
pnpm-lock.yaml
generated
Normal file
2472
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
6
pnpm-workspace.yaml
Normal file
6
pnpm-workspace.yaml
Normal file
@ -0,0 +1,6 @@
|
||||
packages:
|
||||
- apps/*
|
||||
- packages/*
|
||||
# Postinstall scripts are opt-in with pnpm; esbuild needs its binary install.
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
19
tsconfig.base.json
Normal file
19
tsconfig.base.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"lib": ["ES2022"],
|
||||
"strict": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"isolatedModules": true
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user