feat(api): expose preview/import/profile/domain endpoints
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
8
src/app.d.ts
vendored
8
src/app.d.ts
vendored
@@ -1,4 +1,10 @@
|
||||
declare global {
|
||||
namespace App {}
|
||||
namespace App {
|
||||
interface Error {
|
||||
message: string;
|
||||
code?: string;
|
||||
issues?: unknown;
|
||||
}
|
||||
}
|
||||
}
|
||||
export {};
|
||||
|
||||
17
src/lib/server/errors.ts
Normal file
17
src/lib/server/errors.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { error } from '@sveltejs/kit';
|
||||
import { ImporterError } from './recipes/importer';
|
||||
|
||||
export function mapImporterError(e: unknown): never {
|
||||
if (e instanceof ImporterError) {
|
||||
const status =
|
||||
e.code === 'INVALID_URL' || e.code === 'DOMAIN_BLOCKED'
|
||||
? e.code === 'DOMAIN_BLOCKED'
|
||||
? 403
|
||||
: 400
|
||||
: e.code === 'NO_RECIPE_FOUND'
|
||||
? 422
|
||||
: 502; // FETCH_FAILED
|
||||
error(status, { message: e.message, code: e.code });
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
32
src/routes/api/domains/+server.ts
Normal file
32
src/routes/api/domains/+server.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import { z } from 'zod';
|
||||
import { getDb } from '$lib/server/db';
|
||||
import { addDomain, listDomains } from '$lib/server/domains/repository';
|
||||
|
||||
const CreateSchema = z.object({
|
||||
domain: z.string().min(3).max(253),
|
||||
display_name: z.string().max(100).nullable().optional(),
|
||||
added_by_profile_id: z.number().int().positive().nullable().optional()
|
||||
});
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
return json(listDomains(getDb()));
|
||||
};
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
const body = await request.json().catch(() => null);
|
||||
const parsed = CreateSchema.safeParse(body);
|
||||
if (!parsed.success) error(400, { message: 'Invalid body' });
|
||||
try {
|
||||
const d = addDomain(
|
||||
getDb(),
|
||||
parsed.data.domain,
|
||||
parsed.data.display_name ?? null,
|
||||
parsed.data.added_by_profile_id ?? null
|
||||
);
|
||||
return json(d, { status: 201 });
|
||||
} catch (e) {
|
||||
error(409, { message: (e as Error).message });
|
||||
}
|
||||
};
|
||||
11
src/routes/api/domains/[id]/+server.ts
Normal file
11
src/routes/api/domains/[id]/+server.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb } from '$lib/server/db';
|
||||
import { removeDomain } from '$lib/server/domains/repository';
|
||||
|
||||
export const DELETE: RequestHandler = async ({ params }) => {
|
||||
const id = Number(params.id);
|
||||
if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid id' });
|
||||
removeDomain(getDb(), id);
|
||||
return json({ ok: true });
|
||||
};
|
||||
28
src/routes/api/profiles/+server.ts
Normal file
28
src/routes/api/profiles/+server.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import { z } from 'zod';
|
||||
import { getDb } from '$lib/server/db';
|
||||
import { createProfile, listProfiles } from '$lib/server/profiles/repository';
|
||||
|
||||
const CreateSchema = z.object({
|
||||
name: z.string().min(1).max(50),
|
||||
avatar_emoji: z.string().max(16).nullable().optional()
|
||||
});
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
return json(listProfiles(getDb()));
|
||||
};
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
const body = await request.json().catch(() => null);
|
||||
const parsed = CreateSchema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
error(400, { message: 'Invalid body', issues: parsed.error.issues });
|
||||
}
|
||||
try {
|
||||
const p = createProfile(getDb(), parsed.data.name, parsed.data.avatar_emoji ?? null);
|
||||
return json(p, { status: 201 });
|
||||
} catch (e) {
|
||||
error(409, { message: (e as Error).message });
|
||||
}
|
||||
};
|
||||
28
src/routes/api/profiles/[id]/+server.ts
Normal file
28
src/routes/api/profiles/[id]/+server.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import { z } from 'zod';
|
||||
import { getDb } from '$lib/server/db';
|
||||
import { deleteProfile, renameProfile } from '$lib/server/profiles/repository';
|
||||
|
||||
const RenameSchema = z.object({ name: z.string().min(1).max(50) });
|
||||
|
||||
function parseId(raw: string): number {
|
||||
const id = Number(raw);
|
||||
if (!Number.isInteger(id) || id <= 0) error(400, { message: 'Invalid id' });
|
||||
return id;
|
||||
}
|
||||
|
||||
export const PATCH: RequestHandler = async ({ params, request }) => {
|
||||
const id = parseId(params.id!);
|
||||
const body = await request.json().catch(() => null);
|
||||
const parsed = RenameSchema.safeParse(body);
|
||||
if (!parsed.success) error(400, { message: 'Invalid body' });
|
||||
renameProfile(getDb(), id, parsed.data.name);
|
||||
return json({ ok: true });
|
||||
};
|
||||
|
||||
export const DELETE: RequestHandler = async ({ params }) => {
|
||||
const id = parseId(params.id!);
|
||||
deleteProfile(getDb(), id);
|
||||
return json({ ok: true });
|
||||
};
|
||||
22
src/routes/api/recipes/import/+server.ts
Normal file
22
src/routes/api/recipes/import/+server.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import { z } from 'zod';
|
||||
import { getDb } from '$lib/server/db';
|
||||
import { importRecipe } from '$lib/server/recipes/importer';
|
||||
import { mapImporterError } from '$lib/server/errors';
|
||||
|
||||
const ImportSchema = z.object({ url: z.string().url() });
|
||||
|
||||
const IMAGE_DIR = process.env.IMAGE_DIR ?? './data/images';
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
const body = await request.json().catch(() => null);
|
||||
const parsed = ImportSchema.safeParse(body);
|
||||
if (!parsed.success) error(400, { message: 'Invalid body' });
|
||||
try {
|
||||
const result = await importRecipe(getDb(), IMAGE_DIR, parsed.data.url);
|
||||
return json({ id: result.id, duplicate: result.duplicate });
|
||||
} catch (e) {
|
||||
mapImporterError(e);
|
||||
}
|
||||
};
|
||||
16
src/routes/api/recipes/preview/+server.ts
Normal file
16
src/routes/api/recipes/preview/+server.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { json, error } from '@sveltejs/kit';
|
||||
import { getDb } from '$lib/server/db';
|
||||
import { previewRecipe } from '$lib/server/recipes/importer';
|
||||
import { mapImporterError } from '$lib/server/errors';
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
const target = url.searchParams.get('url');
|
||||
if (!target) error(400, { message: 'Missing ?url=' });
|
||||
try {
|
||||
const recipe = await previewRecipe(getDb(), target);
|
||||
return json(recipe);
|
||||
} catch (e) {
|
||||
mapImporterError(e);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user