mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-22 14:51:45 +00:00
c130ed41be
* chore: fix monorepo build pipeline and migrate shared to built package
- Root package.json: add workspace scripts (dev, build, test, test:cov, test:e2e)
that delegate to actual scripts in shared/server/client workspaces
- shared: add tsup build step (CJS + ESM dual output, .d.ts); consumers now import
from the built dist instead of raw TS source via path aliases
- server: replace tsc-alias with tsconfig-paths (tsc-alias mangled node_modules
paths); fix MCP SDK path aliases to point to root node_modules (../node_modules)
- server/scripts/dev.mjs: delay node --watch until tsc -w signals first-pass done,
eliminating the spurious restart on every dev startup
- client/vite.config.js + vitest.config.ts: remove @trek/shared path alias (no longer
needed now that shared is a proper package)
- Consolidate package-lock.json at the workspace root; drop per-workspace lock files
* chore: fix test script to reflect root package.json
* chore: add missing lint and prettier script in root package.json
* fix(ci): build shared before tests; fix vitest MCP SDK alias paths
vitest.config.ts aliases pointed at ./node_modules/ (server-local) but
packages are hoisted to the root node_modules/ in the npm workspace —
changed to ../node_modules/.
CI jobs now install and build shared before running server/client tests
so that @trek/shared's dist/ exists when vitest resolves the package.
* fix(docker): update Dockerfile and CI for monorepo workspace structure
Dockerfile:
- Add shared-builder stage that produces @trek/shared dist before
client and server stages need it
- Each build stage carries root package.json + package-lock.json so npm
can resolve @trek/shared as a workspace dependency
- Production stage installs via workspace context (npm ci --workspace=server
--omit=dev) so node_modules/@trek/shared symlinks to shared/dist correctly
- Copy server/tsconfig.json into the image so tsconfig-paths/register can
find the MCP SDK path aliases at runtime
- CMD cds into /app/server before starting node so tsconfig-paths baseUrl
resolves and ../node_modules points to /app/node_modules
- Remove mkdir for /app/server (now a real dir); keep symlinks for uploads/data
docker.yml version-bump:
- Replace manual per-workspace cd+npm-version calls with single:
npm version --workspaces --include-workspace-root --no-git-tag-version
(mirrors the version:* scripts in root package.json)
- git add now references root package-lock.json; adds shared/package.json
.dockerignore: add shared/dist
package.json: fix version:prerelease preid (alpha → pre)
* fix(tests): use in-memory SQLite per worker in test mode
vitest pool:forks spawns parallel worker processes that all called
initDb() on the same data/travel.db, causing SQLite "database is locked"
and "duplicate column name" races.
When NODE_ENV=test each fork now gets an isolated :memory: DB so migrations
run independently with no file contention.
* chore(ci): add ACT guards to skip DockerHub steps in local act runs
act sets ACT=true automatically. Guards added:
- docker login: if: ${{ !env.ACT }}
- build outputs: type=docker (local load) when ACT, push-by-digest when CI
- digest export/upload: if: ${{ !env.ACT }}
- merge job: if: ${{ !env.ACT }}
- release-helm job (docker.yml): if: ${{ !env.ACT }}
- version-bump git push (docker.yml): wrapped in [ -z "$ACT" ] shell guard
Run locally with:
./bin/act -j build -W .github/workflows/docker.yml \
-P ubuntu-latest=catthehacker/ubuntu:act-latest
* fix(ci): move ACT guards to step level; add guards to security.yml
env context is invalid in job-level if conditions — moved all ACT
guards down to individual steps. Also guards docker login + scout
in security.yml so act can run the build-only part of that workflow.
* fix(ci): skip git fetch and tag logic in act (no remote access in local containers)
* Revert "fix(ci): skip git fetch and tag logic in act (no remote access in local containers)"
This reverts commit 67cf290cda.
* Revert "fix(ci): move ACT guards to step level; add guards to security.yml"
This reverts commit f92b95e054.
* Revert "chore(ci): add ACT guards to skip DockerHub steps in local act runs"
This reverts commit 797183de08.
* fix(docker): add musl optional deps so alpine builds find native rollup/sharp binaries
npm prunes libc-constrained optional deps to the host libc (glibc) when
generating the lockfile, leaving no musl entry for Alpine containers.
Declaring the x64/arm64 musl variants as explicit root optionalDependencies
forces them into the lockfile so npm ci on Alpine can install them.
Covers shared-builder (tsup/rollup) and client-builder (vite/rollup + sharp
icon generation) for both linux/amd64 and linux/arm64 CI targets.
* fix(docker): copy client dist into server/public so the server resolves static files correctly
The server runs from /app/server and serves static files relative to that
directory, so the client build output must land at /app/server/public, not /app/public.
147 lines
4.1 KiB
TypeScript
147 lines
4.1 KiB
TypeScript
import Database from 'better-sqlite3';
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
import { createTables } from './schema';
|
|
import { runMigrations } from './migrations';
|
|
import { runSeeds } from './seeds';
|
|
import { Place, Tag } from '../types';
|
|
|
|
// In test mode each vitest worker gets an isolated in-memory DB so that
|
|
// parallel forks can't race on the same file or share migration state.
|
|
const isTest = process.env.NODE_ENV === 'test';
|
|
|
|
let dbPath: string;
|
|
if (isTest) {
|
|
dbPath = ':memory:';
|
|
} else {
|
|
const dataDir = path.join(__dirname, '../../data');
|
|
if (!fs.existsSync(dataDir)) {
|
|
fs.mkdirSync(dataDir, { recursive: true });
|
|
}
|
|
dbPath = path.join(dataDir, 'travel.db');
|
|
}
|
|
|
|
let _db: Database.Database | null = null;
|
|
|
|
function initDb(): void {
|
|
if (_db) {
|
|
try { _db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); } catch (e) {}
|
|
try { _db.close(); } catch (e) {}
|
|
_db = null;
|
|
}
|
|
|
|
_db = new Database(dbPath);
|
|
_db.exec('PRAGMA journal_mode = WAL');
|
|
_db.exec('PRAGMA busy_timeout = 5000');
|
|
_db.exec('PRAGMA foreign_keys = ON');
|
|
|
|
createTables(_db);
|
|
runMigrations(_db);
|
|
|
|
runSeeds(_db);
|
|
}
|
|
|
|
initDb();
|
|
|
|
const db = new Proxy({} as Database.Database, {
|
|
get(_, prop: string | symbol) {
|
|
if (!_db) throw new Error('Database connection is not available (restore in progress?)');
|
|
const val = (_db as unknown as Record<string | symbol, unknown>)[prop];
|
|
return typeof val === 'function' ? val.bind(_db) : val;
|
|
},
|
|
set(_, prop: string | symbol, val: unknown) {
|
|
(_db as unknown as Record<string | symbol, unknown>)[prop] = val;
|
|
return true;
|
|
},
|
|
});
|
|
|
|
if (process.env.DEMO_MODE?.toLowerCase() === 'true') {
|
|
try {
|
|
const { seedDemoData } = require('../demo/demo-seed');
|
|
seedDemoData(_db);
|
|
} catch (err: unknown) {
|
|
console.error('[Demo] Seed error:', err instanceof Error ? err.message : err);
|
|
}
|
|
}
|
|
|
|
function closeDb(): void {
|
|
if (_db) {
|
|
try { _db.exec('PRAGMA wal_checkpoint(TRUNCATE)'); } catch (e) {}
|
|
try { _db.close(); } catch (e) {}
|
|
_db = null;
|
|
console.log('[DB] Database connection closed');
|
|
}
|
|
}
|
|
|
|
function reinitialize(): void {
|
|
console.log('[DB] Reinitializing database connection after restore...');
|
|
if (_db) closeDb();
|
|
initDb();
|
|
console.log('[DB] Database reinitialized successfully');
|
|
}
|
|
|
|
interface PlaceWithCategory extends Place {
|
|
category_name: string | null;
|
|
category_color: string | null;
|
|
category_icon: string | null;
|
|
}
|
|
|
|
interface PlaceWithTags extends Place {
|
|
category: { id: number; name: string; color: string; icon: string } | null;
|
|
tags: Tag[];
|
|
}
|
|
|
|
function getPlaceWithTags(placeId: number | string): PlaceWithTags | null {
|
|
const place = db.prepare(`
|
|
SELECT p.*, c.name as category_name, c.color as category_color, c.icon as category_icon
|
|
FROM places p
|
|
LEFT JOIN categories c ON p.category_id = c.id
|
|
WHERE p.id = ?
|
|
`).get(placeId) as PlaceWithCategory | undefined;
|
|
|
|
if (!place) return null;
|
|
|
|
const tags = db.prepare(`
|
|
SELECT t.* FROM tags t
|
|
JOIN place_tags pt ON t.id = pt.tag_id
|
|
WHERE pt.place_id = ?
|
|
`).all(placeId) as Tag[];
|
|
|
|
return {
|
|
...place,
|
|
category: place.category_id ? {
|
|
id: place.category_id,
|
|
name: place.category_name!,
|
|
color: place.category_color!,
|
|
icon: place.category_icon!,
|
|
} : null,
|
|
tags,
|
|
};
|
|
}
|
|
|
|
interface TripAccess {
|
|
id: number;
|
|
user_id: number;
|
|
}
|
|
|
|
function canAccessTrip(tripId: number | string, userId: number): TripAccess | undefined {
|
|
return db.prepare(`
|
|
SELECT t.id, t.user_id FROM trips t
|
|
LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ?
|
|
WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)
|
|
`).get(userId, tripId, userId) as TripAccess | undefined;
|
|
}
|
|
|
|
function isOwner(tripId: number | string, userId: number): boolean {
|
|
return !!db.prepare('SELECT id FROM trips WHERE id = ? AND user_id = ?').get(tripId, userId);
|
|
}
|
|
|
|
try {
|
|
const { backfillFlightEndpoints } = require('../services/airportService');
|
|
backfillFlightEndpoints();
|
|
} catch (err) {
|
|
console.error('[DB] Flight endpoint backfill failed:', err);
|
|
}
|
|
|
|
export { db, closeDb, reinitialize, getPlaceWithTags, canAccessTrip, isOwner };
|