feat(api): seed reference activities and enable CORS for the worker SPA
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
45
apps/api/src/db/seed.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
29
apps/api/test/cors.test.ts
Normal file
29
apps/api/test/cors.test.ts
Normal 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');
|
||||
});
|
||||
});
|
||||
34
apps/api/test/seed.test.ts
Normal file
34
apps/api/test/seed.test.ts
Normal 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']);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user