feat: add ESLint and Prettier configuration for code quality

- Added ESLint with TypeScript support and custom rules.
- Introduced Prettier for consistent code formatting.
- Updated package.json to include lint and format scripts.
- Modified db.ts to ensure proper parsing of BIGINT types from PostgreSQL.
- Enhanced webRoutes to coerce timestamp fields to numbers and format isDeleted as boolean.
This commit is contained in:
2025-10-25 17:33:48 +02:00
parent 09a2b09913
commit 14c80197cc
13 changed files with 1620 additions and 44 deletions
+4
View File
@@ -4,6 +4,10 @@ dist
.env
.git
.gitignore
.vscode
.idea
README.md
docker-compose.yml
Dockerfile
data/
collabtable.db
+22
View File
@@ -0,0 +1,22 @@
module.exports = {
root: true,
env: {
es2021: true,
node: true,
},
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
project: null,
},
plugins: ['@typescript-eslint'],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
'prettier',
],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
},
};
+6
View File
@@ -0,0 +1,6 @@
{
"singleQuote": true,
"semi": true,
"trailingComma": "all",
"printWidth": 100
}
+36 -12
View File
@@ -1,12 +1,12 @@
# CollabTable Server
A collaborative list management server built with Node.js, Express, TypeScript, and SQLite.
A collaborative table management server built with Node.js, Express, TypeScript, and a pluggable database (SQLite or PostgreSQL).
## Features
- RESTful API for managing lists, fields, items, and values
- Real-time synchronization support
- SQLite database with Sequelize ORM
- Real-time synchronization via WebSocket (primary) with HTTP fallback
- SQLite (better-sqlite3) or PostgreSQL (pg) database backends
- Dockerized deployment with persistent storage
- TypeScript for type safety
@@ -35,7 +35,7 @@ A collaborative list management server built with Node.js, Express, TypeScript,
The server will be available at `http://localhost:3000`
**Important:** The SQLite database is stored in a Docker volume named `sqlite_data`, which persists across container restarts and rebuilds.
**Important:** The default backend is SQLite with a Docker volume `sqlite_data` for persistence. You can also switch to PostgreSQL by setting environment variables.
## Local Development
@@ -49,9 +49,21 @@ The server will be available at `http://localhost:3000`
cp .env.example .env
```
3. Update the database path in `.env` if needed:
3. Choose your database in `.env` (SQLite by default):
```
# Backend: sqlite | postgres (default sqlite)
DB_CLIENT=sqlite
# SQLite
DB_PATH=./data/collabtable.db
# OR PostgreSQL
# DATABASE_URL=postgres://user:password@localhost:5432/collabtable
# PGHOST=localhost
# PGPORT=5432
# PGUSER=postgres
# PGPASSWORD=yourpassword
# PGDATABASE=collabtable
```
4. Start the development server:
@@ -85,6 +97,13 @@ The server will be available at `http://localhost:3000`
### Sync
- `POST /api/sync` - Synchronize data between client and server
### WebSocket
- `GET /api/ws` - WebSocket endpoint. Send JSON messages:
- `{"type":"ping"}` → `{"type":"pong"}`
- `{"type":"sync", "id":"<uuid>", "payload": { ...SyncRequest }}` → `{"type":"syncResponse", "id":"<uuid>", "payload": { ...SyncResponse }}`
Authentication: If `SERVER_PASSWORD` is set, include header `Authorization: Bearer <password>` on HTTP and WebSocket requests.
### Health Check
- `GET /health` - Check server health
@@ -205,27 +224,32 @@ docker-compose up -d --build
## Environment Variables
- `PORT` - Server port (default: 3000)
- `DB_PATH` - Path to SQLite database file (default: /data/collabtable.db)
- `NODE_ENV` - Environment (development/production)
- `SERVER_PASSWORD` - Optional shared password for API and WebSocket (recommended)
- `DB_CLIENT` - `sqlite` (default) or `postgres`
- `DB_PATH` - Path to SQLite database file (default: ./data/collabtable.db for local, /data/collabtable.db in Docker)
- `DATABASE_URL` - PostgreSQL connection URL (optional alternative to PG* variables)
- `PGHOST`, `PGPORT`, `PGUSER`, `PGPASSWORD`, `PGDATABASE` - PostgreSQL connection parameters
## Data Persistence
The SQLite database file is stored in a Docker volume, ensuring data persists across:
- Container restarts
- Container recreation
- Docker Compose down/up cycles
### SQLite
The SQLite database file is stored in a Docker volume, ensuring data persists across container lifecycle events.
To backup your data:
Backup:
```bash
docker cp collabtable-server:/data/collabtable.db ./backup.db
```
To restore data:
Restore:
```bash
docker cp ./backup.db collabtable-server:/data/collabtable.db
docker-compose restart
```
### PostgreSQL
Use your normal PostgreSQL backup/restore procedures (e.g., `pg_dump`, `pg_restore`). If using the optional Dockerized Postgres service, the data is stored in a named volume (see `docker-compose.yml`).
## License
MIT
+30 -1
View File
@@ -7,8 +7,20 @@ services:
restart: unless-stopped
environment:
- PORT=3000
- DB_PATH=/data/collabtable.db
- NODE_ENV=production
# Authentication (optional but recommended)
- SERVER_PASSWORD=change_me
# Database selection: sqlite | postgres (default sqlite)
- DB_CLIENT=sqlite
# SQLite path (used when DB_CLIENT=sqlite)
- DB_PATH=/data/collabtable.db
# PostgreSQL (used when DB_CLIENT=postgres) - choose either DATABASE_URL or the PG* vars
# - DATABASE_URL=postgres://user:password@postgres:5432/collabtable
# - PGHOST=postgres
# - PGPORT=5432
# - PGUSER=postgres
# - PGPASSWORD=yourpassword
# - PGDATABASE=collabtable
ports:
- "3000:3000"
volumes:
@@ -16,8 +28,25 @@ services:
networks:
- collabtable-network
# Optional PostgreSQL database
# postgres:
# image: postgres:16-alpine
# container_name: collabtable-postgres
# restart: unless-stopped
# environment:
# - POSTGRES_DB=collabtable
# - POSTGRES_USER=postgres
# - POSTGRES_PASSWORD=yourpassword
# ports:
# - "5432:5432"
# volumes:
# - pg_data:/var/lib/postgresql/data
# networks:
# - collabtable-network
volumes:
sqlite_data:
# pg_data:
networks:
collabtable-network:
+1458 -18
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -7,6 +7,8 @@
"start": "node dist/index.js",
"dev": "ts-node-dev --respawn --transpile-only src/index.ts",
"build": "tsc",
"lint": "eslint src --ext .ts",
"format": "prettier --write \"**/*.{ts,js,json,md,yml,yaml}\"",
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": ["collaboration", "api", "express"],
@@ -18,6 +20,11 @@
"better-sqlite3": "^11.0.0",
"dotenv": "^16.3.1",
"ws": "^8.16.0",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"@typescript-eslint/parser": "^7.0.2",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.3.3",
"pg": "^8.11.3"
},
"devDependencies": {
+4 -1
View File
@@ -1,6 +1,6 @@
import dotenv from 'dotenv';
import BetterSqlite3, { Database as SqliteDB } from 'better-sqlite3';
import { Pool, PoolClient, QueryResult } from 'pg';
import { Pool, PoolClient, QueryResult, types as pgTypes } from 'pg';
import path from 'path';
import fs from 'fs';
@@ -129,6 +129,9 @@ class PostgresAdapter implements DBAdapter {
private pool: Pool;
constructor() {
// Ensure BIGINT (int8) is parsed as number to avoid string timestamps
// OID 20 = INT8, OID 1700 = NUMERIC (keep default for now)
pgTypes.setTypeParser(20, (val: string) => parseInt(val, 10));
const connectionString = process.env.DATABASE_URL;
if (connectionString) {
this.pool = new Pool({ connectionString });
+20 -3
View File
@@ -313,9 +313,26 @@ router.get('/web/data', async (req: Request, res: Response) => {
const values = await dbAdapter.queryAll('SELECT * FROM item_values');
// Convert isDeleted from INTEGER to BOOLEAN
const formattedLists = (lists as any[]).map(list => ({ ...list, isDeleted: !!list.isDeleted }));
const formattedFields = (fields as any[]).map(field => ({ ...field, isDeleted: !!field.isDeleted }));
const formattedItems = (items as any[]).map(item => ({ ...item, isDeleted: !!item.isDeleted }));
// Coerce timestamps to numbers (pg may return strings)
const toNum = (v: any) => (typeof v === 'string' ? Number(v) : v);
const formattedLists = (lists as any[]).map(list => ({
...list,
isDeleted: !!list.isDeleted,
createdAt: toNum(list.createdAt),
updatedAt: toNum(list.updatedAt)
}));
const formattedFields = (fields as any[]).map(field => ({
...field,
isDeleted: !!field.isDeleted,
createdAt: toNum(field.createdAt),
updatedAt: toNum(field.updatedAt)
}));
const formattedItems = (items as any[]).map(item => ({
...item,
isDeleted: !!item.isDeleted,
createdAt: toNum(item.createdAt),
updatedAt: toNum(item.updatedAt)
}));
res.json({
lists: formattedLists,