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 {
|
declare global {
|
||||||
namespace App {}
|
namespace App {
|
||||||
|
interface Error {
|
||||||
|
message: string;
|
||||||
|
code?: string;
|
||||||
|
issues?: unknown;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
export {};
|
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