Files
stash/packages/server/src/api.ts
2025-12-10 21:12:17 +01:00

120 lines
3.3 KiB
TypeScript

import fastifyCors from '@fastify/cors';
import fastifySwagger from '@fastify/swagger';
import fastify from 'fastify';
import {
hasZodFastifySchemaValidationErrors,
isResponseSerializationError,
jsonSchemaTransform,
jsonSchemaTransformObject,
serializerCompiler,
validatorCompiler,
type ZodTypeProvider,
} from 'fastify-type-provider-zod';
import scalar from '@scalar/fastify-api-reference';
import { StashRuntime } from '@morten-olsen/stash-runtime';
import { systemEndpoints } from './endpoints/system/system.js';
import { documentEndpoints } from './endpoints/documents/documents.js';
import { documentFilterEndpoints } from './endpoints/document-filters/document-filters.js';
import { documentChunkFilterEndpoints } from './endpoints/document-chunk-filters/document-chunk-filters.js';
class BaseError extends Error {
public statusCode: number;
constructor(message: string, statusCode = 500) {
super(message);
this.name = this.constructor.name;
this.statusCode = statusCode;
}
}
const createApi = async (runtime: StashRuntime = new StashRuntime()) => {
const app = fastify().withTypeProvider<ZodTypeProvider>();
app.setValidatorCompiler(validatorCompiler);
app.setSerializerCompiler(serializerCompiler);
app.decorate('runtime', runtime);
app.register(fastifyCors);
app.register(fastifySwagger, {
openapi: {
info: {
title: 'My API',
version: '1.0.0',
},
},
transform: jsonSchemaTransform,
transformObject: jsonSchemaTransformObject,
});
await app.register(scalar, {
routePrefix: '/docs',
configuration: {
pageTitle: 'Foo',
title: 'Hello World!',
telemetry: false,
hideClientButton: true,
theme: 'laserwave',
persistAuth: true,
orderRequiredPropertiesFirst: false,
},
});
app.setErrorHandler((err, req, reply) => {
console.error(err);
if (hasZodFastifySchemaValidationErrors(err)) {
return reply.code(400).send({
error: 'Response Validation Error',
message: "Request doesn't match the schema",
statusCode: 400,
details: {
issues: err.validation,
method: req.method,
url: req.url,
},
});
}
if (isResponseSerializationError(err)) {
return reply.code(500).send({
error: 'Internal Server Error',
message: "Response doesn't match the schema",
statusCode: 500,
details: {
issues: err.cause.issues,
method: err.method,
url: err.url,
},
});
}
if (err instanceof BaseError) {
return reply.code(err.statusCode ?? 500).send({
error: err.name,
message: err.message,
statusCode: err.statusCode,
});
}
return reply.code(500).send({
error: 'Internal Server Error',
message: err instanceof Error ? err.message : 'An unknown error occurred',
statusCode: 500,
});
});
app.addHook('onReady', async () => {
app.runtime.warmup.ensure();
});
await app.register(systemEndpoints, { prefix: '/system' });
await app.register(documentEndpoints, { prefix: '/documents' });
await app.register(documentFilterEndpoints, { prefix: '/document-filters' });
await app.register(documentChunkFilterEndpoints, { prefix: '/document-chunk-filters' });
await app.ready();
app.swagger();
return app;
};
export { createApi };