feat(api): seed reference activities and enable CORS for the worker SPA

This commit is contained in:
Bas van Rossem
2026-06-17 15:54:52 +02:00
parent 85184d3287
commit 35f9aa5574
6 changed files with 122 additions and 2 deletions

View File

@@ -10,7 +10,8 @@
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"db:generate": "drizzle-kit generate",
"db:migrate": "tsx src/db/migrate.ts"
"db:migrate": "tsx src/db/migrate.ts",
"db:seed": "tsx src/db/seed.ts"
},
"dependencies": {
"@hono/node-server": "^1.13.7",

View File

@@ -1,4 +1,5 @@
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { health } from './routes/health';
import { me } from './routes/me';
import { activitiesRoutes } from './routes/activities';
@@ -7,6 +8,16 @@ import { auth } from './auth';
export function createApp(): Hono {
const app = new Hono();
app.use(
'/api/*',
cors({
origin: ['http://localhost:5173'],
allowMethods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
exposeHeaders: ['set-auth-token'], // so the SPA can read the bearer token on sign-in
credentials: true,
})
);
app.route('/', health);
app.on(['POST', 'GET'], '/api/auth/*', (c) => auth.handler(c.req.raw));
app.route('/', me);

View File

@@ -8,7 +8,7 @@ import { env } from './env';
export const auth = betterAuth({
secret: env.BETTER_AUTH_SECRET,
baseURL: env.BETTER_AUTH_URL,
trustedOrigins: [env.BETTER_AUTH_URL, 'http://localhost:3000'],
trustedOrigins: [env.BETTER_AUTH_URL, 'http://localhost:3000', 'http://localhost:5173'],
database: drizzleAdapter(db, { provider: 'sqlite', schema }),
emailAndPassword: {
enabled: true,

45
apps/api/src/db/seed.ts Normal file
View File

@@ -0,0 +1,45 @@
import { pathToFileURL } from 'node:url';
import { eq } from 'drizzle-orm';
import { db } from './client';
import { activities } from './schema';
// Reference activities (realistic Dutch handeling names) — see
// docs/reference/legacy-mobile-app.md §6.2 and the Phase 1 plan.
const REFERENCE_ACTIVITIES: { name: string; insoleTypes: string[] }[] = [
{ name: 'Leerrand', insoleTypes: ['Kurk', 'Berk', '3D'] },
{ name: 'Frezen', insoleTypes: ['Kurk', 'Berk'] },
{ name: 'Slijpen', insoleTypes: ['Kurk', 'Berk', '3D'] },
{ name: 'Bekleden', insoleTypes: ['Kurk', 'Berk', '3D'] },
{ name: 'Afwerken', insoleTypes: ['Kurk', 'Berk', '3D'] },
{ name: 'Printen', insoleTypes: ['3D'] },
];
// Idempotent: insert each reference activity only if no activity with that name exists.
export async function seed(): Promise<void> {
for (const activity of REFERENCE_ACTIVITIES) {
const existing = await db
.select()
.from(activities)
.where(eq(activities.name, activity.name));
if (existing.length === 0) {
await db.insert(activities).values({
name: activity.name,
insoleTypes: activity.insoleTypes,
});
}
}
}
// Allow running directly: `tsx src/db/seed.ts` (cross-platform — pathToFileURL
// handles Windows drive-letter/backslash paths, which a raw `file://` prefix does not).
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
seed()
.then(() => {
console.log('Seed complete.');
process.exit(0);
})
.catch((err) => {
console.error(err);
process.exit(1);
});
}

View File

@@ -0,0 +1,29 @@
import { describe, it, expect } from 'vitest';
import { createApp } from '../src/app';
const ORIGIN = 'http://localhost:5173';
describe('cors', () => {
it('answers a CORS preflight for the SPA origin', async () => {
const app = createApp();
const res = await app.request('/api/activities', {
method: 'OPTIONS',
headers: {
Origin: ORIGIN,
'Access-Control-Request-Method': 'GET',
},
});
expect(res.headers.get('access-control-allow-origin')).toBe(ORIGIN);
const allowMethods = res.headers.get('access-control-allow-methods') ?? '';
expect(allowMethods).toContain('GET');
});
it('exposes set-auth-token to the SPA origin', async () => {
const app = createApp();
const res = await app.request('/api/activities', {
headers: { Origin: ORIGIN },
});
const expose = (res.headers.get('access-control-expose-headers') ?? '').toLowerCase();
expect(expose).toContain('set-auth-token');
});
});

View File

@@ -0,0 +1,34 @@
import { describe, it, expect } from 'vitest';
import { inArray, eq } from 'drizzle-orm';
import { seed } from '../src/db/seed';
import { db } from '../src/db/client';
import { activities } from '../src/db/schema';
const SEED_NAMES = ['Leerrand', 'Frezen', 'Slijpen', 'Bekleden', 'Afwerken', 'Printen'];
describe('seed', () => {
it('seeds the reference activities idempotently', async () => {
await seed();
const first = await db
.select()
.from(activities)
.where(inArray(activities.name, SEED_NAMES));
const countFirst = first.length;
await seed();
const second = await db
.select()
.from(activities)
.where(inArray(activities.name, SEED_NAMES));
expect(second.length).toBe(countFirst);
expect(countFirst).toBe(SEED_NAMES.length);
const printen = await db
.select()
.from(activities)
.where(eq(activities.name, 'Printen'));
expect(printen).toHaveLength(1);
expect(printen[0]?.insoleTypes).toEqual(['3D']);
});
});