Merge pull request #98 from SELab-2/docs/swagger-autogen

docs: API docs met Swagger UI
This commit is contained in:
Tibo De Peuter 2025-03-13 21:59:27 +01:00 committed by GitHub
commit a70d7dc1bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 1112 additions and 44 deletions

View file

@ -1,3 +1,7 @@
#
# Basic configuration
#
DWENGO_PORT=3000 # The port the backend will listen on
DWENGO_DB_HOST=domain-or-ip-of-database
DWENGO_DB_PORT=5431
@ -19,4 +23,5 @@ DWENGO_AUTH_TEACHER_URL=http://localhost:7080/realms/teacher
DWENGO_AUTH_TEACHER_CLIENT_ID=dwengo
DWENGO_AUTH_TEACHER_JWKS_ENDPOINT=http://localhost:7080/realms/teacher/protocol/openid-connect/certs
# LOKI_HOST=http://localhost:3102 # The address of the Loki instance, used for logging
# The address of the Lokiinstance, used for logging
# LOKI_HOST=http://localhost:3102

View file

@ -14,6 +14,12 @@ RUN npm install --silent
# Root tsconfig.json
COPY tsconfig.json ./
WORKDIR /app/docs
COPY docs ./
RUN npm run swagger
WORKDIR /app/backend
COPY backend ./
@ -29,6 +35,7 @@ COPY package-lock.json backend/package.json ./
RUN npm install --silent --only=production
COPY --from=build-stage /app/backend/dist ./dist/
COPY --from=build-stage /app/docs/api ./docs/swagger
EXPOSE 3000

View file

@ -34,6 +34,7 @@
"loki-logger-ts": "^1.0.2",
"marked": "^15.0.7",
"response-time": "^2.3.3",
"swagger-ui-express": "^5.0.1",
"uuid": "^11.1.0",
"winston": "^3.17.0",
"winston-loki": "^6.1.3"
@ -45,6 +46,7 @@
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.13.4",
"@types/response-time": "^2.3.8",
"@types/swagger-ui-express": "^4.1.8",
"globals": "^15.15.0",
"ts-node": "^10.9.2",
"tsx": "^4.19.3",

View file

@ -1,6 +1,5 @@
import express, { Express } from 'express';
import { initORM } from './orm.js';
import { authenticateUser } from './middleware/auth/auth.js';
import cors from './middleware/cors.js';
import { getLogger, Logger } from './logging/initalize.js';
@ -8,19 +7,25 @@ import { responseTimeLogger } from './logging/responseTimeLogger.js';
import responseTime from 'response-time';
import { EnvVars, getNumericEnvVar } from './util/envvars.js';
import apiRouter from './routes/router.js';
import swaggerMiddleware from './swagger.js';
import swaggerUi from 'swagger-ui-express';
const logger: Logger = getLogger();
const app: Express = express();
const port: string | number = getNumericEnvVar(EnvVars.Port);
app.use(cors);
app.use(express.json());
app.use(responseTime(responseTimeLogger));
app.use(cors);
app.use(authenticateUser);
// Add response time logging
app.use(responseTime(responseTimeLogger));
// Swagger
app.get('/api', apiRouter);
app.use('/api-docs', swaggerUi.serve, swaggerMiddleware);
async function startServer() {
await initORM();

View file

@ -9,14 +9,17 @@ router.get('/config', (req, res) => {
});
router.get('/testAuthenticatedOnly', authenticatedOnly, (req, res) => {
/* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */
res.json({ message: 'If you see this, you should be authenticated!' });
});
router.get('/testStudentsOnly', studentsOnly, (req, res) => {
/* #swagger.security = [{ "student": [ ] }] */
res.json({ message: 'If you see this, you should be a student!' });
});
router.get('/testTeachersOnly', teachersOnly, (req, res) => {
/* #swagger.security = [{ "teacher": [ ] }] */
res.json({ message: 'If you see this, you should be a teacher!' });
});

View file

@ -1,15 +1,15 @@
import { Response, Router } from 'express';
import studentRouter from './student';
import groupRouter from './group';
import assignmentRouter from './assignment';
import submissionRouter from './submission';
import classRouter from './class';
import questionRouter from './question';
import authRouter from './auth';
import themeRoutes from './themes';
import learningPathRoutes from './learning-paths';
import learningObjectRoutes from './learning-objects';
import { getLogger, Logger } from '../logging/initalize';
import studentRouter from './student.js';
import groupRouter from './group.js';
import assignmentRouter from './assignment.js';
import submissionRouter from './submission.js';
import classRouter from './class.js';
import questionRouter from './question.js';
import authRouter from './auth.js';
import themeRoutes from './themes.js';
import learningPathRoutes from './learning-paths.js';
import learningObjectRoutes from './learning-objects.js';
import { getLogger, Logger } from '../logging/initalize.js';
const router = Router();
const logger: Logger = getLogger();
@ -21,15 +21,15 @@ router.get('/', (_, res: Response) => {
});
});
router.use('/student', studentRouter);
router.use('/group', groupRouter);
router.use('/assignment', assignmentRouter);
router.use('/submission', submissionRouter);
router.use('/class', classRouter);
router.use('/question', questionRouter);
router.use('/auth', authRouter);
router.use('/theme', themeRoutes);
router.use('/learningPath', learningPathRoutes);
router.use('/learningObject', learningObjectRoutes);
router.use('/student', studentRouter /* #swagger.tags = ['Student'] */);
router.use('/group', groupRouter /* #swagger.tags = ['Group'] */);
router.use('/assignment', assignmentRouter /* #swagger.tags = ['Assignment'] */);
router.use('/submission', submissionRouter /* #swagger.tags = ['Submission'] */);
router.use('/class', classRouter /* #swagger.tags = ['Class'] */);
router.use('/question', questionRouter /* #swagger.tags = ['Question'] */);
router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */);
router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */);
router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */);
router.use('/learningObject', learningObjectRoutes /* #swagger.tags = ['Learning Object'] */);
export default router;

7
backend/src/swagger.ts Normal file
View file

@ -0,0 +1,7 @@
import { RequestHandler } from 'express';
import swaggerUi from 'swagger-ui-express';
import swaggerDocument from '../../docs/api/swagger.json';
const swaggerMiddleware: RequestHandler = swaggerUi.setup(swaggerDocument);
export default swaggerMiddleware;

View file

@ -3,6 +3,7 @@
"include": ["src/**/*.ts"],
"compilerOptions": {
"rootDir": "./src",
"outDir": "./dist"
"outDir": "./dist",
"resolveJsonModule": true
}
}

58
docs/api/generate.ts Normal file
View file

@ -0,0 +1,58 @@
import swaggerAutogen from 'swagger-autogen';
const doc = {
info: {
version: '0.1.0',
title: 'Dwengo-1 Backend API',
description: 'Dwengo-1 Backend API using Express, based on VZW Dwengo',
license: {
name: 'MIT',
url: 'https://github.com/SELab-2/Dwengo-1/blob/336496ab6352ee3f8bf47490c90b5cf81526cef6/LICENSE',
},
},
servers: [
{
url: 'http://localhost:3000/',
description: 'Development server',
},
{
url: 'https://sel2-1.ugent.be/api',
description: 'Production server',
},
],
components: {
securitySchemes: {
student: {
type: 'oauth2',
flows: {
implicit: {
authorizationUrl: 'http://localhost:7080/realms/student/protocol/openid-connect/auth',
scopes: {
openid: 'openid',
profile: 'profile',
email: 'email',
},
},
},
},
teacher: {
type: 'oauth2',
flows: {
implicit: {
authorizationUrl: 'http://localhost:7080/realms/teacher/protocol/openid-connect/auth',
scopes: {
openid: 'openid',
profile: 'profile',
email: 'email',
},
},
},
},
},
},
};
const outputFile = './swagger.json';
const routes = ['../../backend/src/app.ts'];
swaggerAutogen({ openapi: '3.1.0' })(outputFile, routes, doc);

735
docs/api/swagger.json Normal file
View file

@ -0,0 +1,735 @@
{
"openapi": "3.1.0",
"info": {
"version": "0.1.0",
"title": "Dwengo-1 Backend API",
"description": "Dwengo-1 Backend API using Express, based on VZW Dwengo",
"license": {
"name": "MIT",
"url": "https://github.com/SELab-2/Dwengo-1/blob/336496ab6352ee3f8bf47490c90b5cf81526cef6/LICENSE"
}
},
"servers": [
{
"url": "http://localhost:3000/",
"description": "Development server"
},
{
"url": "https://sel2-1.ugent.be/api",
"description": "Production server"
}
],
"paths": {
"/": {
"get": {
"description": "",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/student/": {
"get": {
"tags": ["Student"],
"description": "",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/student/{id}": {
"get": {
"tags": ["Student"],
"description": "",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/student/{id}/classes": {
"get": {
"tags": ["Student"],
"description": "",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/student/{id}/submissions": {
"get": {
"tags": ["Student"],
"description": "",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/student/{id}/assignments": {
"get": {
"tags": ["Student"],
"description": "",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/student/{id}/groups": {
"get": {
"tags": ["Student"],
"description": "",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/group/": {
"get": {
"tags": ["Group"],
"description": "",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/group/{id}": {
"get": {
"tags": ["Group"],
"description": "",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/group/{id}/question": {
"get": {
"tags": ["Group"],
"description": "",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/assignment/": {
"get": {
"tags": ["Assignment"],
"description": "",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/assignment/{id}": {
"get": {
"tags": ["Assignment"],
"description": "",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/assignment/{id}/submissions": {
"get": {
"tags": ["Assignment"],
"description": "",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/assignment/{id}/groups": {
"get": {
"tags": ["Assignment"],
"description": "",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/assignment/{id}/questions": {
"get": {
"tags": ["Assignment"],
"description": "",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/submission/": {
"get": {
"tags": ["Submission"],
"description": "",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/submission/{id}": {
"get": {
"tags": ["Submission"],
"description": "",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/class/": {
"get": {
"tags": ["Class"],
"description": "",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/class/{id}": {
"get": {
"tags": ["Class"],
"description": "",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/class/{id}/invitations": {
"get": {
"tags": ["Class"],
"description": "",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/class/{id}/assignments": {
"get": {
"tags": ["Class"],
"description": "",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/class/{id}/students": {
"get": {
"tags": ["Class"],
"description": "",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/question/": {
"get": {
"tags": ["Question"],
"description": "",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/question/{id}": {
"get": {
"tags": ["Question"],
"description": "",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/question/{id}/answers": {
"get": {
"tags": ["Question"],
"description": "",
"parameters": [
{
"name": "id",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/auth/config": {
"get": {
"tags": ["Auth"],
"description": "",
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/auth/testAuthenticatedOnly": {
"get": {
"tags": ["Auth"],
"description": "",
"responses": {
"200": {
"description": "OK"
}
},
"security": [
{
"student": []
},
{
"teacher": []
}
]
}
},
"/auth/testStudentsOnly": {
"get": {
"tags": ["Auth"],
"description": "",
"responses": {
"200": {
"description": "OK"
}
},
"security": [
{
"student": []
}
]
}
},
"/auth/testTeachersOnly": {
"get": {
"tags": ["Auth"],
"description": "",
"responses": {
"200": {
"description": "OK"
}
},
"security": [
{
"teacher": []
}
]
}
},
"/theme/": {
"get": {
"tags": ["Theme"],
"description": "",
"parameters": [
{
"name": "language",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/theme/{theme}": {
"get": {
"tags": ["Theme"],
"description": "",
"parameters": [
{
"name": "theme",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"404": {
"description": "Not Found"
}
}
}
},
"/learningPath/": {
"get": {
"tags": ["Learning Path"],
"description": "",
"parameters": [
{
"name": "hruid",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "theme",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "search",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "language",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"404": {
"description": "Not Found"
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/learningObject/": {
"get": {
"tags": ["Learning Object"],
"description": "",
"parameters": [
{
"name": "hruid",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "full",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "language",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request"
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/learningObject/{hruid}": {
"get": {
"tags": ["Learning Object"],
"description": "",
"parameters": [
{
"name": "hruid",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "language",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request"
},
"500": {
"description": "Internal Server Error"
}
}
}
}
},
"components": {
"securitySchemes": {
"student": {
"type": "oauth2",
"flows": {
"implicit": {
"authorizationUrl": "http://localhost:7080/realms/student/protocol/openid-connect/auth",
"scopes": {
"openid": "openid",
"profile": "profile",
"email": "email"
}
}
}
},
"teacher": {
"type": "oauth2",
"flows": {
"implicit": {
"authorizationUrl": "http://localhost:7080/realms/teacher/protocol/openid-connect/auth",
"scopes": {
"openid": "openid",
"profile": "profile",
"email": "email"
}
}
}
}
}
}
}

14
docs/package.json Normal file
View file

@ -0,0 +1,14 @@
{
"name": "dwengo-1-docs",
"version": "0.0.1",
"description": "Documentation for Dwengo-1",
"private": true,
"scripts": {
"build": "npm run architecture && npm run swagger",
"architecture": "python3 -m venv .venv && source .venv/bin/activate && pip install -r docs/requirements.txt && python docs/architecture/schema.py",
"swagger": "tsx api/generate.ts"
},
"devDependencies": {
"swagger-autogen": "^2.23.7"
}
}

View file

@ -17,6 +17,7 @@
},
"dependencies": {
"vue": "^3.5.13",
"vue-i18n": "^11.1.2",
"vue-router": "^4.5.0",
"vuetify": "^3.7.12",
"oidc-client-ts": "^3.1.0",

262
package-lock.json generated
View file

@ -10,7 +10,8 @@
"license": "MIT",
"workspaces": [
"backend",
"frontend"
"frontend",
"docs"
],
"devDependencies": {
"@eslint/compat": "^1.2.6",
@ -48,6 +49,7 @@
"loki-logger-ts": "^1.0.2",
"marked": "^15.0.7",
"response-time": "^2.3.3",
"swagger-ui-express": "^5.0.1",
"uuid": "^11.1.0",
"winston": "^3.17.0",
"winston-loki": "^6.1.3"
@ -59,6 +61,7 @@
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.13.4",
"@types/response-time": "^2.3.8",
"@types/swagger-ui-express": "^4.1.8",
"globals": "^15.15.0",
"ts-node": "^10.9.2",
"tsx": "^4.19.3",
@ -77,6 +80,13 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"docs": {
"name": "dwengo-1-docs",
"version": "0.0.1",
"devDependencies": {
"swagger-autogen": "^2.23.7"
}
},
"frontend": {
"name": "dwengo-1-frontend",
"version": "0.0.1",
@ -84,6 +94,7 @@
"axios": "^1.8.2",
"oidc-client-ts": "^3.1.0",
"vue": "^3.5.13",
"vue-i18n": "^11.1.2",
"vue-router": "^4.5.0",
"vuetify": "^3.7.12"
},
@ -947,6 +958,50 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@intlify/core-base": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.1.2.tgz",
"integrity": "sha512-nmG512G8QOABsserleechwHGZxzKSAlggGf9hQX0nltvSwyKNVuB/4o6iFeG2OnjXK253r8p8eSDOZf8PgFdWw==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "11.1.2",
"@intlify/shared": "11.1.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.1.2.tgz",
"integrity": "sha512-T/xbNDzi+Yv0Qn2Dfz2CWCAJiwNgU5d95EhhAEf4YmOgjCKktpfpiUSmLcBvK1CtLpPQ85AMMQk/2NCcXnNj1g==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "11.1.2",
"source-map-js": "^1.0.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.1.2.tgz",
"integrity": "sha512-dF2iMMy8P9uKVHV/20LA1ulFLL+MKSbfMiixSmn6fpwqzvix38OIc7ebgnFbBqElvghZCW9ACtzKTGKsTGTWGA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"dev": true,
@ -1028,6 +1083,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@jsep-plugin/assignment/-/assignment-1.3.0.tgz",
"integrity": "sha512-VVgV+CXrhbMI3aSusQyclHkenWSAm95WaiKrMxRFam3JSUiIaQjoMIw2sEs/OX4XifnqeQUN4DYbJjlA8EfktQ==",
"license": "MIT",
"engines": {
"node": ">= 10.16.0"
},
@ -1039,6 +1095,7 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/@jsep-plugin/regex/-/regex-1.0.4.tgz",
"integrity": "sha512-q7qL4Mgjs1vByCaTnDFcBnV9HS7GVPJX5vyVoCgZHNSC9rjwIlmbXG5sUuorR5ndfHAIlJ8pVStxvjXHbNvtUg==",
"license": "MIT",
"engines": {
"node": ">= 10.16.0"
},
@ -1048,8 +1105,6 @@
},
"node_modules/@mikro-orm/cli": {
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@mikro-orm/cli/-/cli-6.4.9.tgz",
"integrity": "sha512-LQzVsmar/0DoJkPGyz3OpB8pa9BCQtvYreEC71h0O+RcizppJjgBQNTkj5tJd2Iqvh4hSaMv6qTv0l5UK6F2Vw==",
"dev": true,
"dependencies": {
"@jercle/yargonaut": "1.1.5",
@ -1071,6 +1126,7 @@
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@mikro-orm/core/-/core-6.4.9.tgz",
"integrity": "sha512-osB2TbvSH4ZL1s62LCBQFAnxPqLycX5fakPHOoztudixqfbVD5QQydeGizJXMMh2zKP6vRCwIJy3MeSuFxPjHg==",
"license": "MIT",
"dependencies": {
"dataloader": "2.2.3",
"dotenv": "16.4.7",
@ -1091,6 +1147,7 @@
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@mikro-orm/knex/-/knex-6.4.9.tgz",
"integrity": "sha512-iGXJfe/TziVOQsWuxMIqkOpurysWzQA6kj3+FDtOkHJAijZhqhjSBnfUVHHY/JzU9o0M0rgLrDVJFry/uEaJEA==",
"license": "MIT",
"dependencies": {
"fs-extra": "11.3.0",
"knex": "3.1.0",
@ -1121,6 +1178,7 @@
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@mikro-orm/postgresql/-/postgresql-6.4.9.tgz",
"integrity": "sha512-ZdVVFAL/TSbzpEmChGdH0oUpy2KiHLjNIeItZHRQgInn1X9p0qx28VVDR78p8qgRGkQ3LquxGTkvmWI0w7qi3A==",
"license": "MIT",
"dependencies": {
"@mikro-orm/knex": "6.4.9",
"pg": "8.13.3",
@ -1139,6 +1197,7 @@
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@mikro-orm/reflection/-/reflection-6.4.9.tgz",
"integrity": "sha512-fgY7yLrcZm3J/8dv9reUC4PQo7C2muImU31jmzz1SxmNKPJFDJl7OzcDZlM5NOisXzsWUBrcNdCyuQiWViVc3A==",
"license": "MIT",
"dependencies": {
"globby": "11.1.0",
"ts-morph": "25.0.1"
@ -1154,6 +1213,7 @@
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/@mikro-orm/sqlite/-/sqlite-6.4.9.tgz",
"integrity": "sha512-O7Jy/5DrTWpJI/3qkhRJHl+OcECx1N625LHDODAAauOK3+MJB/bj80TrvQhe6d/CHZMmvxZ7m2GzaL1NulKxRw==",
"license": "MIT",
"dependencies": {
"@mikro-orm/knex": "6.4.9",
"fs-extra": "11.3.0",
@ -1566,6 +1626,13 @@
"linux"
]
},
"node_modules/@scarf/scarf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
"integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==",
"hasInstallScript": true,
"license": "Apache-2.0"
},
"node_modules/@sec-ant/readable-stream": {
"version": "0.4.1",
"dev": true,
@ -1764,6 +1831,17 @@
"@types/send": "*"
}
},
"node_modules/@types/swagger-ui-express": {
"version": "4.1.8",
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz",
"integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/express": "*",
"@types/serve-static": "*"
}
},
"node_modules/@types/tough-cookie": {
"version": "4.0.5",
"dev": true,
@ -1778,6 +1856,7 @@
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
@ -2583,6 +2662,7 @@
"version": "1.8.3",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
"integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@ -3228,12 +3308,12 @@
"node_modules/cross": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/cross/-/cross-1.0.0.tgz",
"integrity": "sha512-p6hXbCnjuIB4bhKWFeztQd7VwffgQP9zOBzUoiA8Lvi01RzQY0e7PbPFU/uqVPTM2stY7uCpVck1UTPpxhinMQ=="
"integrity": "sha512-p6hXbCnjuIB4bhKWFeztQd7VwffgQP9zOBzUoiA8Lvi01RzQY0e7PbPFU/uqVPTM2stY7uCpVck1UTPpxhinMQ==",
"license": "BSD"
},
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
"integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.1"
},
@ -3357,6 +3437,16 @@
"dev": true,
"license": "MIT"
},
"node_modules/deepmerge": {
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
"integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/default-browser": {
"version": "5.2.1",
"dev": true,
@ -3450,6 +3540,7 @@
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.4.tgz",
"integrity": "sha512-ysFSFEDVduQpyhzAob/kkuJjf5zWkZD8/A9ywSp1byueyuCfHamrCBa14/Oc2iiB0e51B+NpxSl5gmzn+Ms/mg==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
@ -3480,6 +3571,10 @@
"resolved": "backend",
"link": true
},
"node_modules/dwengo-1-docs": {
"resolved": "docs",
"link": true
},
"node_modules/dwengo-1-frontend": {
"resolved": "frontend",
"link": true
@ -4443,8 +4538,8 @@
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"license": "ISC",
"optional": true
"devOptional": true,
"license": "ISC"
},
"node_modules/function-bind": {
"version": "1.1.2",
@ -4603,6 +4698,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/gift-pegjs/-/gift-pegjs-1.0.2.tgz",
"integrity": "sha512-S/A2wBDdia2QWKpB5FtASx1gguep1wg5If5glDWJgUMiABICJT7ogArGfsdgozevhBdbdOiHhrykJP86hbgvRw==",
"license": "MIT",
"dependencies": {
"pegjs": "^0.10.x"
}
@ -4899,8 +4995,8 @@
},
"node_modules/inflight": {
"version": "1.0.6",
"devOptional": true,
"license": "ISC",
"optional": true,
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
@ -5100,6 +5196,7 @@
"version": "2.22.0",
"resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.22.0.tgz",
"integrity": "sha512-A2xsDNST1yB94rErEnwqlzSvGllCJ4e8lDMe1OWBH2hvpfc/2qzgMEiDshTO1HwO+PIDTiYeOc7ZDB7Ds49BOg==",
"license": "MIT",
"dependencies": {
"dompurify": "^3.2.4",
"jsdom": "^26.0.0"
@ -5235,6 +5332,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/jsep/-/jsep-1.4.0.tgz",
"integrity": "sha512-B7qPcEVE3NVkmSJbaYxvv4cHkVW7DQsZz13pUMrfS8z8Q/BuShN+gcTXrUlPiGqM2/t/EEaI030bpxMqY8gMlw==",
"license": "MIT",
"engines": {
"node": ">= 10.16.0"
}
@ -5298,6 +5396,7 @@
"version": "10.3.0",
"resolved": "https://registry.npmjs.org/jsonpath-plus/-/jsonpath-plus-10.3.0.tgz",
"integrity": "sha512-8TNmfeTCk2Le33A3vRRwtuworG/L5RrgMvdjhKZxvyShO+mBu2fP50OWUjRLNtvw344DdDarFh9buFAZs5ujeA==",
"license": "MIT",
"dependencies": {
"@jsep-plugin/assignment": "^1.3.0",
"@jsep-plugin/regex": "^1.0.4",
@ -5746,6 +5845,7 @@
"version": "15.0.7",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.7.tgz",
"integrity": "sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
@ -5813,6 +5913,7 @@
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/mikro-orm/-/mikro-orm-6.4.9.tgz",
"integrity": "sha512-XwVrWNT4NNwS6kHIKFNDfvy8L1eWcBBEHeTVzFFYcnb2ummATaLxqeVkNEmKA68jmdtfQdUmWBqGdbcIPwtL2Q==",
"license": "MIT",
"engines": {
"node": ">= 18.12.0"
}
@ -6362,6 +6463,7 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/oidc-client-ts/-/oidc-client-ts-3.2.0.tgz",
"integrity": "sha512-wUvVcG3SXzZDKHxi/VGQGaTUk9qguMKfYh26Y1zOVrQsu1zp85JWx/SjZzKSXK5j3NA1RcasgMoaHe6gt1WNtw==",
"license": "Apache-2.0",
"dependencies": {
"jwt-decode": "^4.0.0"
},
@ -6542,8 +6644,8 @@
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"devOptional": true,
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.10.0"
}
@ -6605,6 +6707,7 @@
"version": "0.10.0",
"resolved": "https://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz",
"integrity": "sha512-qI5+oFNEGi3L5HAxDwN2LA4Gg7irF70Zs25edhjld9QemOgp0CbvMtbFcMvFtEo1OityPrcCzkQFB8JP/hxgow==",
"license": "MIT",
"bin": {
"pegjs": "bin/pegjs"
},
@ -6621,6 +6724,7 @@
"version": "8.13.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.13.3.tgz",
"integrity": "sha512-P6tPt9jXbL9HVu/SSRERNYaYG++MjnscnegFh9pPHihfoBSujsrka0hyuymMzeJKFWrcG8wvCKy8rCe8e5nDUQ==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.7.0",
"pg-pool": "^3.7.1",
@ -6647,6 +6751,7 @@
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
"integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
@ -6657,6 +6762,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
@ -6665,6 +6771,7 @@
"version": "3.8.0",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.8.0.tgz",
"integrity": "sha512-VBw3jiVm6ZOdLBTIcXLNdSotb6Iy3uOCwDGFAksZCXmi10nyRvnP2v3jl4d+IsLYRyXf6o9hIm/ZtUzlByNUdw==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
@ -6672,12 +6779,12 @@
"node_modules/pg-protocol": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.8.0.tgz",
"integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g=="
"integrity": "sha512-jvuYlEkL03NRvOoyoRktBK7+qU5kOvlAwvmrH8sr3wbLrOdVWsRxQfz8mMy9sZFsqJ1hEWNfdWKI4SAmoL+j7g==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
@ -6693,6 +6800,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
@ -6701,6 +6809,7 @@
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@ -6709,6 +6818,7 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
@ -6719,12 +6829,12 @@
"node_modules/pg/node_modules/pg-connection-string": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.7.0.tgz",
"integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA=="
"integrity": "sha512-PI2W9mv53rXJQEOb8xNR8lH7Hr+EKa6oJa38zsK0S/ky2er16ios1wLKhZyxzD7jUReiWokc9WK5nxSnC7W1TA==",
"license": "MIT"
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
@ -6824,6 +6934,7 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.4.tgz",
"integrity": "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ==",
"license": "MIT",
"engines": {
"node": ">=12"
}
@ -6832,6 +6943,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@ -7716,6 +7828,7 @@
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
"license": "ISC",
"engines": {
"node": ">= 10.x"
}
@ -7974,6 +8087,102 @@
"version": "1.0.0",
"dev": true
},
"node_modules/swagger-autogen": {
"version": "2.23.7",
"resolved": "https://registry.npmjs.org/swagger-autogen/-/swagger-autogen-2.23.7.tgz",
"integrity": "sha512-vr7uRmuV0DCxWc0wokLJAwX3GwQFJ0jwN+AWk0hKxre2EZwusnkGSGdVFd82u7fQLgwSTnbWkxUL7HXuz5LTZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^7.4.1",
"deepmerge": "^4.2.2",
"glob": "^7.1.7",
"json5": "^2.2.3"
}
},
"node_modules/swagger-autogen/node_modules/acorn": {
"version": "7.4.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz",
"integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/swagger-autogen/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/swagger-autogen/node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"deprecated": "Glob versions prior to v9 are no longer supported",
"dev": true,
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/swagger-autogen/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/swagger-ui-dist": {
"version": "5.20.1",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.20.1.tgz",
"integrity": "sha512-qBPCis2w8nP4US7SvUxdJD3OwKcqiWeZmjN2VWhq2v+ESZEXOP/7n4DeiOiiZcGYTKMHAHUUrroHaTsjUWTEGw==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "=1.4.0"
}
},
"node_modules/swagger-ui-express": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz",
"integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==",
"license": "MIT",
"dependencies": {
"swagger-ui-dist": ">=5.0.0"
},
"engines": {
"node": ">= v0.10.32"
},
"peerDependencies": {
"express": ">=4.0.0 || >=5.0.0-beta"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"license": "MIT"
@ -8838,6 +9047,26 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/vue-i18n": {
"version": "11.1.2",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.1.2.tgz",
"integrity": "sha512-MfdkdKGUHN+jkkaMT5Zbl4FpRmN7kfelJIwKoUpJ32ONIxdFhzxZiLTVaAXkAwvH3y9GmWpoiwjDqbPIkPIMFA==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.1.2",
"@intlify/shared": "11.1.2",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-router": {
"version": "4.5.0",
"license": "MIT",
@ -9217,6 +9446,7 @@
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}

View file

@ -8,13 +8,13 @@
"build": "npm run build --ws",
"format": "npm run format --ws",
"format-check": "npm run format-check --ws",
"generate-docs": "python3 -m venv .venv && source .venv/bin/activate && pip install -r docs/requirements.txt && python docs/architecture/schema.py",
"lint": "npm run lint --ws",
"test:unit": "npm run test:unit --ws"
},
"workspaces": [
"backend",
"frontend"
"frontend",
"docs"
],
"repository": {
"type": "git",