feat(openapi): Started working on Openapi generation
- Updated to Typebox 1.0.0 to better support Openapi type generation - Changed dockerfile to fetch depedencies only once - Fixed Routes to properly handle openapi - Fixed Routes to respond with multiples status code (no more only 200) - Fixed Schemas so the auth-gated endpoint properly reflect that - Added Makefile rule to generate openapi client (none working due to missing files)
This commit is contained in:
parent
1bd2b4594b
commit
b7c2a3dff9
36 changed files with 5472 additions and 833 deletions
178
src/user/openapi.json
Normal file
178
src/user/openapi.json
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"version": "9.6.0",
|
||||
"title": "@fastify/swagger"
|
||||
},
|
||||
"components": {
|
||||
"schemas": {}
|
||||
},
|
||||
"paths": {
|
||||
"/api/user/info/{user}": {
|
||||
"get": {
|
||||
"operationId": "getUser",
|
||||
"parameters": [
|
||||
{
|
||||
"schema": {
|
||||
"anyOf": [
|
||||
{
|
||||
"enum": [
|
||||
"me"
|
||||
],
|
||||
"description": "the current logged in user"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"format": "uuid",
|
||||
"description": "A user uuid"
|
||||
}
|
||||
]
|
||||
},
|
||||
"in": "path",
|
||||
"name": "user",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg",
|
||||
"payload"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"success"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"userinfo.success"
|
||||
]
|
||||
},
|
||||
"payload": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"name",
|
||||
"id",
|
||||
"guest"
|
||||
],
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"guest": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"notLoggedIn"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"auth.noCookie",
|
||||
"auth.invalidKind",
|
||||
"auth.noUser",
|
||||
"auth.invalid"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"failure"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"userinfo.failure.notLoggedIn"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "Default Response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"kind",
|
||||
"msg"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"enum": [
|
||||
"failure"
|
||||
]
|
||||
},
|
||||
"msg": {
|
||||
"enum": [
|
||||
"userinfo.failure.unknownUser"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"servers": [
|
||||
{
|
||||
"url": "https://local.maix.me:8888",
|
||||
"description": "direct from docker"
|
||||
},
|
||||
{
|
||||
"url": "https://local.maix.me:8000",
|
||||
"description": "using fnginx"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -11,7 +11,8 @@
|
|||
"scripts": {
|
||||
"start": "npm run build && node dist/run.js",
|
||||
"build": "vite build",
|
||||
"build:prod": "vite build --outDir=/dist --minify=true --sourcemap=false"
|
||||
"build:prod": "vite build --outDir=/dist --minify=true --sourcemap=false",
|
||||
"build:openapi": "VITE_ENTRYPOINT=src/openapi.ts vite build && node dist/openapi.cjs >openapi.json"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
|
|
@ -22,15 +23,15 @@
|
|||
"@fastify/multipart": "^9.3.0",
|
||||
"@fastify/sensible": "^6.0.3",
|
||||
"@fastify/static": "^8.3.0",
|
||||
"@sinclair/typebox": "^0.34.41",
|
||||
"typebox": "^1.0.51",
|
||||
"fastify": "^5.6.1",
|
||||
"fastify-cli": "^7.4.0",
|
||||
"fastify-cli": "^7.4.1",
|
||||
"fastify-plugin": "^5.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.18.13",
|
||||
"rollup-plugin-node-externals": "^8.1.1",
|
||||
"vite": "^7.1.12",
|
||||
"@types/node": "^22.19.0",
|
||||
"rollup-plugin-node-externals": "^8.1.2",
|
||||
"vite": "^7.2.2",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,21 +3,20 @@ import fastifyFormBody from '@fastify/formbody';
|
|||
import fastifyMultipart from '@fastify/multipart';
|
||||
import * as db from '@shared/database';
|
||||
import * as auth from '@shared/auth';
|
||||
import * as swagger from '@shared/swagger';
|
||||
import * as utils from '@shared/utils';
|
||||
|
||||
declare const __SERVICE_NAME: string;
|
||||
|
||||
// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this...
|
||||
const plugins = import.meta.glob('./plugins/**/*.ts', { eager: true });
|
||||
// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this...
|
||||
const routes = import.meta.glob('./routes/**/*.ts', { eager: true });
|
||||
|
||||
// When using .decorate you have to specify added properties for Typescript
|
||||
declare module 'fastify' {
|
||||
export interface FastifyInstance {
|
||||
image_store: string;
|
||||
}
|
||||
}
|
||||
|
||||
const app: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
|
||||
void opts;
|
||||
await fastify.register(utils.useMakeResponse);
|
||||
await fastify.register(swagger.useSwagger, { service: __SERVICE_NAME });
|
||||
await fastify.register(db.useDatabase as FastifyPluginAsync, {});
|
||||
await fastify.register(auth.jwtPlugin as FastifyPluginAsync, {});
|
||||
await fastify.register(auth.authPlugin as FastifyPluginAsync, {});
|
||||
|
|
|
|||
21
src/user/src/openapi.ts
Normal file
21
src/user/src/openapi.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import f, { FastifyPluginAsync } from 'fastify';
|
||||
import * as swagger from '@shared/swagger';
|
||||
import * as auth from '@shared/auth';
|
||||
|
||||
declare const __SERVICE_NAME: string;
|
||||
|
||||
// @ts-expect-error: import.meta.glob is a vite thing. Typescript doesn't know this...
|
||||
const routes = import.meta.glob('./routes/**/*.ts', { eager: true });
|
||||
|
||||
async function start() {
|
||||
const fastify = f({ logger: false });
|
||||
await fastify.register(auth.authPlugin, { onlySchema: true });
|
||||
await fastify.register(swagger.useSwagger, { service: __SERVICE_NAME });
|
||||
|
||||
for (const route of Object.values(routes)) {
|
||||
await fastify.register(route as FastifyPluginAsync, {});
|
||||
}
|
||||
await fastify.ready();
|
||||
console.log(JSON.stringify(fastify.swagger(), undefined, 4));
|
||||
}
|
||||
start();
|
||||
|
|
@ -1,26 +1,37 @@
|
|||
import { FastifyPluginAsync } from 'fastify';
|
||||
|
||||
import { Static, Type } from '@sinclair/typebox';
|
||||
import { isNullish, makeResponse, typeResponse } from '@shared/utils';
|
||||
import { Static, Type } from 'typebox';
|
||||
import { isNullish, MakeStaticResponse, typeResponse } from '@shared/utils';
|
||||
|
||||
|
||||
export const UserInfoRes = Type.Union([
|
||||
typeResponse('success', 'userinfo.success', { name: Type.String(), id: Type.String(), guest: Type.Boolean() }),
|
||||
typeResponse('failure', ['userinfo.failure.generic', 'userinfo.failure.unknownUser', 'userinfo.failure.notLoggedIn']),
|
||||
]);
|
||||
export const UserInfoRes = {
|
||||
'200': typeResponse('success', 'userinfo.success', {
|
||||
name: Type.String(), id: Type.String(), guest: Type.Boolean(),
|
||||
}),
|
||||
'403': typeResponse('failure', 'userinfo.failure.notLoggedIn'),
|
||||
'404': typeResponse('failure', 'userinfo.failure.unknownUser'),
|
||||
};
|
||||
|
||||
export type UserInfoRes = Static<typeof UserInfoRes>;
|
||||
export type UserInfoRes = MakeStaticResponse<typeof UserInfoRes>;
|
||||
|
||||
export const UserInfoParams = Type.Object({
|
||||
user: Type.Union([
|
||||
Type.Enum(['me'], { description: 'the current logged in user' }),
|
||||
Type.String({ format: 'uuid', description: 'A user uuid' }),
|
||||
]),
|
||||
});
|
||||
|
||||
export type UserInfoParams = Static<typeof UserInfoParams>;
|
||||
|
||||
const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
||||
void _opts;
|
||||
fastify.get<{ Params: { user: string } }>(
|
||||
fastify.get<{ Params: UserInfoParams }>(
|
||||
'/api/user/info/:user',
|
||||
{ schema: { response: { '2xx': UserInfoRes } }, config: { requireAuth: true } },
|
||||
async function(req, _res) {
|
||||
void _res;
|
||||
if (isNullish(req.authUser)) { return makeResponse('failure', 'userinfo.failure.notLoggedIn'); }
|
||||
{ schema: { params: UserInfoParams, response: UserInfoRes, operationId: 'getUser' }, config: { requireAuth: true } },
|
||||
async function(req, res) {
|
||||
if (isNullish(req.authUser)) { return res.makeResponse(403, 'failure', 'userinfo.failure.notLoggedIn'); }
|
||||
if (isNullish(req.params.user) || req.params.user.length === 0) {
|
||||
return makeResponse('failure', 'userinfo.failure.unknownUser');
|
||||
return res.makeResponse(404, 'failure', 'userinfo.failure.unknownUser');
|
||||
}
|
||||
|
||||
// if the param is the special value `me`, then just get the id from the currently auth'ed user
|
||||
|
|
@ -30,7 +41,7 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
|||
|
||||
const user = this.db.getUser(req.params.user);
|
||||
if (isNullish(user)) {
|
||||
return makeResponse('failure', 'userinfo.failure.unknownUser');
|
||||
return res.makeResponse(404, 'failure', 'userinfo.failure.unknownUser');
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -48,7 +59,7 @@ const route: FastifyPluginAsync = async (fastify, _opts): Promise<void> => {
|
|||
guest: !!user.guest,
|
||||
};
|
||||
|
||||
return makeResponse('success', 'userinfo.success', payload);
|
||||
return res.makeResponse(200, 'success', 'userinfo.success', payload);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ const externals = collectDeps(
|
|||
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
define: {
|
||||
__SERVICE_NAME: '"user"',
|
||||
},
|
||||
// service root
|
||||
plugins: [tsconfigPaths(), nodeExternals()],
|
||||
build: {
|
||||
|
|
@ -33,7 +36,7 @@ export default defineConfig({
|
|||
outDir: 'dist',
|
||||
emptyOutDir: true,
|
||||
lib: {
|
||||
entry: path.resolve(__dirname, 'src/run.ts'),
|
||||
entry: path.resolve(__dirname, process.env.VITE_ENTRYPOINT ?? 'src/run.ts'),
|
||||
// adjust main entry
|
||||
formats: ['cjs'],
|
||||
// CommonJS for Node.js
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue