added Swagger documentation

This commit is contained in:
Dmitri 2026-07-02 13:03:05 +02:00
parent a3dfdccd0d
commit 6085443549
27 changed files with 2133 additions and 338 deletions

View File

@ -35,7 +35,6 @@
"pino": "^9.0.0",
"pino-pretty": "^11.0.0",
"sequelize": "^6.37.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0",
"uuid": "^14.0.1",
"validator": "^13.15.35"
@ -58,7 +57,6 @@
"@types/passport-google-oauth2": "^0.1.10",
"@types/passport-jwt": "^4.0.1",
"@types/passport-microsoft": "^2.1.1",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8",
"@types/validator": "^13.15.10",
"@typescript-eslint/eslint-plugin": "^8.62.1",
@ -76,50 +74,6 @@
"node": ">=24 <25"
}
},
"node_modules/@apidevtools/json-schema-ref-parser": {
"version": "9.1.2",
"resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-9.1.2.tgz",
"integrity": "sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==",
"license": "MIT",
"dependencies": {
"@jsdevtools/ono": "^7.1.3",
"@types/json-schema": "^7.0.6",
"call-me-maybe": "^1.0.1",
"js-yaml": "^4.1.0"
}
},
"node_modules/@apidevtools/openapi-schemas": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@apidevtools/openapi-schemas/-/openapi-schemas-2.1.0.tgz",
"integrity": "sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@apidevtools/swagger-methods": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-methods/-/swagger-methods-3.0.2.tgz",
"integrity": "sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==",
"license": "MIT"
},
"node_modules/@apidevtools/swagger-parser": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/@apidevtools/swagger-parser/-/swagger-parser-10.0.3.tgz",
"integrity": "sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==",
"license": "MIT",
"dependencies": {
"@apidevtools/json-schema-ref-parser": "^9.0.6",
"@apidevtools/openapi-schemas": "^2.0.4",
"@apidevtools/swagger-methods": "^3.0.2",
"@jsdevtools/ono": "^7.1.3",
"call-me-maybe": "^1.0.1",
"z-schema": "^5.0.1"
},
"peerDependencies": {
"openapi-types": ">=7"
}
},
"node_modules/@aws-crypto/crc32": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz",
@ -1255,12 +1209,6 @@
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@jsdevtools/ono": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz",
"integrity": "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==",
"license": "MIT"
},
"node_modules/@napi-rs/wasm-runtime": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz",
@ -2369,12 +2317,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"license": "MIT"
},
"node_modules/@types/json2csv": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/@types/json2csv/-/json2csv-5.0.7.tgz",
@ -2566,13 +2508,6 @@
"@types/node": "*"
}
},
"node_modules/@types/swagger-jsdoc": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/@types/swagger-jsdoc/-/swagger-jsdoc-6.0.4.tgz",
"integrity": "sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==",
"dev": true,
"license": "MIT"
},
"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",
@ -3321,6 +3256,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/array-buffer-byte-length": {
@ -3640,6 +3576,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/base64-js": {
@ -3806,6 +3743,7 @@
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
"integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
@ -3930,12 +3868,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/call-me-maybe": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz",
"integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==",
"license": "MIT"
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@ -4039,6 +3971,7 @@
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
"dev": true,
"license": "MIT",
"optional": true,
"engines": {
@ -4049,6 +3982,7 @@
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/concat-stream": {
@ -4353,6 +4287,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
"integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"esutils": "^2.0.2"
@ -5254,6 +5189,7 @@
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
@ -5664,6 +5600,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"dev": true,
"license": "ISC"
},
"node_modules/fsevents": {
@ -5895,26 +5832,6 @@
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
}
},
"node_modules/glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"license": "ISC",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@ -6292,6 +6209,7 @@
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dev": true,
"license": "ISC",
"dependencies": {
"once": "^1.3.0",
@ -6784,6 +6702,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
"dev": true,
"funding": [
{
"type": "github",
@ -6992,6 +6911,7 @@
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.isinteger": {
@ -7025,12 +6945,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/lodash.mergewith": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
"integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
@ -7123,6 +7037,7 @@
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
@ -7632,13 +7547,6 @@
"wrappy": "1"
}
},
"node_modules/openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
"license": "MIT",
"peer": true
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -7838,6 +7746,7 @@
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -9251,47 +9160,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/swagger-jsdoc": {
"version": "6.2.8",
"resolved": "https://registry.npmjs.org/swagger-jsdoc/-/swagger-jsdoc-6.2.8.tgz",
"integrity": "sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==",
"license": "MIT",
"dependencies": {
"commander": "6.2.0",
"doctrine": "3.0.0",
"glob": "7.1.6",
"lodash.mergewith": "^4.6.2",
"swagger-parser": "^10.0.3",
"yaml": "2.0.0-1"
},
"bin": {
"swagger-jsdoc": "bin/swagger-jsdoc.js"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/swagger-jsdoc/node_modules/commander": {
"version": "6.2.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-6.2.0.tgz",
"integrity": "sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==",
"license": "MIT",
"engines": {
"node": ">= 6"
}
},
"node_modules/swagger-parser": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/swagger-parser/-/swagger-parser-10.0.3.tgz",
"integrity": "sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==",
"license": "MIT",
"dependencies": {
"@apidevtools/swagger-parser": "10.0.3"
},
"engines": {
"node": ">=10"
}
},
"node_modules/swagger-ui-dist": {
"version": "5.17.14",
"resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.17.14.tgz",
@ -10199,15 +10067,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.0.0-1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.0.0-1.tgz",
"integrity": "sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==",
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@ -10224,6 +10083,7 @@
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/z-schema/-/z-schema-5.0.6.tgz",
"integrity": "sha512-+XR1GhnWklYdfr8YaZv/iu+vY+ux7V5DS5zH1DQf6bO5ufrt/5cgNhVO5qyhsjFXvsqQb/f08DWE9b6uPscyAg==",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash.get": "^4.4.2",

View File

@ -55,7 +55,6 @@
"pino": "^9.0.0",
"pino-pretty": "^11.0.0",
"sequelize": "^6.37.0",
"swagger-jsdoc": "^6.2.8",
"swagger-ui-express": "^5.0.0",
"uuid": "^14.0.1",
"validator": "^13.15.35"
@ -93,7 +92,6 @@
"@types/passport-google-oauth2": "^0.1.10",
"@types/passport-jwt": "^4.0.1",
"@types/passport-microsoft": "^2.1.1",
"@types/swagger-jsdoc": "^6.0.4",
"@types/swagger-ui-express": "^4.1.8",
"@types/validator": "^13.15.10",
"@typescript-eslint/eslint-plugin": "^8.62.1",

View File

@ -4,7 +4,6 @@ import express from 'express';
import fs from 'node:fs';
import path from 'node:path';
import helmet from 'helmet';
import swaggerJsDoc from 'swagger-jsdoc';
import * as swaggerUI from 'swagger-ui-express';
import './auth/auth.ts';
@ -23,6 +22,7 @@ import {
downloadLimiter,
searchLimiter,
} from './middlewares/rateLimiter.ts';
import { createOpenApiDocument } from './openapi/document.ts';
import accessLogsRoutesModule from './routes/access_logs.ts';
import assetVariantsRoutesModule from './routes/asset_variants.ts';
import assetsRoutesModule from './routes/assets.ts';
@ -55,9 +55,6 @@ import type {
HealthResponse,
MountRuntimeEntityRoute,
RuntimeReadOrAuthMiddleware,
SwaggerDocumentOptions,
SwaggerHostMiddleware,
SwaggerUiModuleWithHost,
} from './types/index.ts';
import {
exitAfterLogging,
@ -73,7 +70,6 @@ import {
} from './utils/request-context.ts';
const app = express();
const swaggerUiWithHost: SwaggerUiModuleWithHost = swaggerUI;
registerProcessErrorHandlers();
initializePermissionsMiddleware();
@ -154,54 +150,12 @@ const getBaseUrl = (url: string | undefined): string => {
return url.endsWith('/api') ? url.slice(0, -4) : url;
};
const options: SwaggerDocumentOptions = {
definition: {
openapi: '3.0.0',
info: {
version: '1.0.0',
title: 'Tour Builder Platform',
description:
'Tour Builder Platform Online REST API for Testing and Prototyping application. You can perform all major operations with your entities - create, delete and etc.',
},
servers: [
{
url: getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || config.swaggerUrl,
description: 'Development server',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
},
},
responses: {
UnauthorizedError: {
description: 'Access token is missing or invalid',
},
},
},
security: [
{
bearerAuth: [],
},
],
},
apis: ['./src/routes/*.{js,ts}'],
};
const specs = swaggerJsDoc(options);
const swaggerHostMiddleware: SwaggerHostMiddleware = (req, _res, next) => {
swaggerUiWithHost.host =
getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || req.get('host') || '';
next();
};
const specs = createOpenApiDocument({
serverUrl: getBaseUrl(process.env.NEXT_PUBLIC_BACK_API) || config.swaggerUrl,
});
app.use(
'/api-docs',
swaggerHostMiddleware,
swaggerUI.serve,
swaggerUI.setup(specs),
);

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,7 @@
import type { ErrorRequestHandler, NextFunction, Request, RequestHandler, Response, Router } from 'express';
import type swaggerJSDoc from 'swagger-jsdoc';
import type * as swaggerUiExpress from 'swagger-ui-express';
import type { ErrorRequestHandler, RequestHandler, Router } from 'express';
import type { CurrentUser } from './auth.ts';
export type SwaggerDocumentOptions = swaggerJSDoc.Options;
export type SwaggerUiModuleWithHost = typeof swaggerUiExpress & {
host?: string;
};
export type ExpressRouter = Router;
export interface HealthResponse {
@ -33,10 +25,6 @@ export interface MountRuntimeEntityRoute {
export type AppErrorHandler = ErrorRequestHandler;
export interface SwaggerHostMiddleware {
(req: Request, res: Response, next: NextFunction): void;
}
export interface PublicAccessHardeningSummary {
publicRolePermissions: number;
publicUsersWithCustomPermissions: number;

View File

@ -131,9 +131,6 @@ export type {
PublicAccessHardeningSummary,
RuntimeJwtVerifyHandler,
RuntimeReadOrAuthMiddleware,
SwaggerDocumentOptions,
SwaggerHostMiddleware,
SwaggerUiModuleWithHost,
} from './app.ts';
export type {
EmailSendResult,

View File

@ -0,0 +1,114 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import type { NextFunction } from 'express';
import { createRequest, createResponse } from 'node-mocks-http';
import * as swaggerUI from 'swagger-ui-express';
import { createOpenApiDocument } from '../src/openapi/document.ts';
import type { OpenApiDocument } from '../src/openapi/document.ts';
function createTestDocument(): OpenApiDocument {
return createOpenApiDocument({
serverUrl: 'http://localhost:3000',
});
}
function collectRefs(value: unknown): Set<string> {
const refs = new Set<string>();
const serialized = JSON.stringify(value);
const refPattern = /"\$ref"\s*:\s*"([^"]+)"/g;
for (const match of serialized.matchAll(refPattern)) {
const ref = match[1];
if (ref) {
refs.add(ref);
}
}
return refs;
}
function hasComponentRef(document: OpenApiDocument, ref: string): boolean {
const match = /^#\/components\/(schemas|responses|parameters|securitySchemes)\/(.+)$/.exec(ref);
if (!match) return false;
const section = match[1];
const name = match[2];
if (!section || !name) return false;
switch (section) {
case 'schemas':
return name in document.components.schemas;
case 'responses':
return name in document.components.responses;
case 'parameters':
return name in document.components.parameters;
case 'securitySchemes':
return name in document.components.securitySchemes;
default:
return false;
}
}
void test('OpenAPI document exposes comprehensive route coverage', () => {
const document = createTestDocument();
const requiredPaths = [
'/api/health',
'/api/auth/signin/local',
'/api/users',
'/api/users/count',
'/api/users/autocomplete',
'/api/projects/{id}/clone',
'/api/file/presign',
'/api/file/upload-sessions/{sessionId}/chunks/{chunkIndex}',
'/api/runtime-access/me',
'/api/project-ui-control-settings/project/{projectId}/env/{environment}',
'/api/tour_pages/reverse-video-status',
];
assert.equal(document.openapi, '3.0.0');
for (const path of requiredPaths) {
assert.ok(document.paths[path], `Missing OpenAPI path: ${path}`);
}
});
void test('OpenAPI document resolves all internal refs', () => {
const document = createTestDocument();
const refs = collectRefs(document);
const missingRefs = [...refs].filter((ref) => !hasComponentRef(document, ref));
assert.equal(missingRefs.length, 0, `Missing refs: ${missingRefs.join(', ')}`);
});
void test('OpenAPI factory CRUD paths are generated consistently', () => {
const document = createTestDocument();
const resourcePath = '/api/assets';
assert.ok(document.paths[resourcePath]?.post);
assert.ok(document.paths[resourcePath]?.get);
assert.ok(document.paths[`${resourcePath}/bulk-import`]?.post);
assert.ok(document.paths[`${resourcePath}/deleteByIds`]?.post);
assert.ok(document.paths[`${resourcePath}/count`]?.get);
assert.ok(document.paths[`${resourcePath}/autocomplete`]?.get);
assert.ok(document.paths[`${resourcePath}/{id}`]?.get);
assert.ok(document.paths[`${resourcePath}/{id}`]?.put);
assert.ok(document.paths[`${resourcePath}/{id}`]?.delete);
});
void test('Swagger UI setup serves documentation HTML without throwing', () => {
const document = createTestDocument();
const req = createRequest({ url: '/api-docs/' });
const res = createResponse();
const handler = swaggerUI.setup(document);
let nextError: unknown = null;
const next: NextFunction = (error?: unknown) => {
nextError = error;
};
handler(req, res, next);
assert.equal(nextError, null);
assert.equal(res.statusCode, 200);
assert.match(String(res._getData()), /swagger-ui/);
});

View File

@ -434,9 +434,7 @@ const ConstructorToolbar = forwardRef<HTMLDivElement, ConstructorToolbarProps>(
className='h-10 w-[86px]'
label={isSaving ? 'Saving...' : 'Save'}
subtitle={
lastSavedAt
? dataFormatter.relativeTimestamp(lastSavedAt)
: ' '
lastSavedAt ? dataFormatter.relativeTimestamp(lastSavedAt) : ' '
}
onClick={onSave}
disabled={isSaving}

View File

@ -642,8 +642,7 @@ export function ElementEditorPanel({
<NavigationSettingsSectionCompact
type={
selectedElement.type as
| 'navigation_next'
| 'navigation_prev'
'navigation_next' | 'navigation_prev'
}
navType={selectedElement.navType}
navLabel={selectedElement.navLabel || ''}
@ -1310,10 +1309,7 @@ export function ElementEditorPanel({
}
onChange={(e) => {
const val = e.target.value as
| 'left'
| 'center'
| 'right'
| '';
'left' | 'center' | 'right' | '';
updateSelectedElement({
infoPanelTitleTextAlign: val || undefined,
});
@ -1465,10 +1461,7 @@ export function ElementEditorPanel({
}
onChange={(e) => {
const val = e.target.value as
| 'left'
| 'center'
| 'right'
| '';
'left' | 'center' | 'right' | '';
updateSelectedElement({
infoPanelTextTextAlign: val || undefined,
});
@ -1639,10 +1632,7 @@ export function ElementEditorPanel({
}
onChange={(e) => {
const val = e.target.value as
| 'left'
| 'center'
| 'right'
| '';
'left' | 'center' | 'right' | '';
updateSelectedElement({
infoPanelSpanTextAlign: val || undefined,
});
@ -2093,10 +2083,7 @@ export function ElementEditorPanel({
// Handle slide transition properties with proper prefixes
if (prop === 'slideTransitionType') {
const typedValue = (value || undefined) as
| 'fade'
| 'none'
| ''
| undefined;
'fade' | 'none' | '' | undefined;
if (selectedElement.type === 'gallery') {
updateSelectedElement({
gallerySlideTransitionType: typedValue,

View File

@ -148,7 +148,10 @@ const StyleSettingsSection: React.FC<StyleSettingsSectionProps> = ({
max='100'
value={opacityToPercentInput(values.opacity)}
onChange={(event) =>
onChange('opacity', percentInputToOpacityValue(event.target.value))
onChange(
'opacity',
percentInputToOpacityValue(event.target.value),
)
}
placeholder='50'
/>

View File

@ -8,6 +8,8 @@ export const portApi =
: '';
export const baseURLApi = `${hostApi}${portApi ? `:${portApi}` : ``}/api`;
export const swaggerDocsUrl = baseURLApi.replace(/\/api\/?$/, '/api-docs');
export const localStorageDarkModeKey = 'darkMode';
export const localStorageStyleKey = 'style';

View File

@ -36,8 +36,7 @@ const PageNavigationContext =
// Provider
// ============================================================================
export interface PageNavigationProviderProps
extends UsePageNavigationStateOptions {
export interface PageNavigationProviderProps extends UsePageNavigationStateOptions {
children: ReactNode;
}

View File

@ -48,10 +48,7 @@ interface PreloadQueueItem {
}
export type PreloadPhase =
| 'idle'
| 'phase1_current_page'
| 'phase2_transitions'
| 'complete';
'idle' | 'phase1_current_page' | 'phase2_transitions' | 'complete';
interface UsePreloadOrchestratorResult {
isPreloading: boolean;

View File

@ -47,8 +47,7 @@ export interface UseTransitionCreationOptions {
}
export interface UseTransitionCreationResult
extends TransitionCreationState,
TransitionCreationActions {}
extends TransitionCreationState, TransitionCreationActions {}
/**
* Hook to manage transition creation form state.

View File

@ -60,11 +60,7 @@ export interface UseTransitionPlaybackOptions {
}
export type PlaybackPhase =
| 'idle'
| 'preparing'
| 'playing'
| 'finishing'
| 'completed';
'idle' | 'preparing' | 'playing' | 'finishing' | 'completed';
export interface UseTransitionPlaybackResult {
phase: PlaybackPhase;

View File

@ -404,11 +404,7 @@ export function toPreloadAssetInfo(asset: AssetToCache): PreloadAssetInfo {
url: asset.originalUrl,
pageId: asset.pageId,
assetType: assetType as
| 'image'
| 'video'
| 'audio'
| 'transition'
| 'other',
'image' | 'video' | 'audio' | 'transition' | 'other',
priority: asset.priority,
};
}

View File

@ -131,12 +131,7 @@ export function remToDesignPx(value: number): number {
export function normalizeToCanvasUnits(
value: string | number | undefined,
property:
| 'width'
| 'height'
| 'fontSize'
| 'padding'
| 'borderRadius'
| 'gap',
'width' | 'height' | 'fontSize' | 'padding' | 'borderRadius' | 'gap',
designWidth: number = CANVAS_CONFIG.defaults.width,
designHeight: number = CANVAS_CONFIG.defaults.height,
): string {

View File

@ -266,10 +266,10 @@ export function hasHoverEffects(
): boolean {
return Boolean(
effects.hoverScale ||
effects.hoverOpacity ||
effects.hoverBackgroundColor ||
effects.hoverColor ||
effects.hoverBoxShadow,
effects.hoverOpacity ||
effects.hoverBackgroundColor ||
effects.hoverColor ||
effects.hoverBoxShadow,
);
}
@ -281,9 +281,9 @@ export function hasFocusEffects(
): boolean {
return Boolean(
effects.focusScale ||
effects.focusOpacity ||
effects.focusOutline ||
effects.focusBoxShadow,
effects.focusOpacity ||
effects.focusOutline ||
effects.focusBoxShadow,
);
}
@ -295,8 +295,8 @@ export function hasActiveEffects(
): boolean {
return Boolean(
effects.activeScale ||
effects.activeOpacity ||
effects.activeBackgroundColor,
effects.activeOpacity ||
effects.activeBackgroundColor,
);
}

View File

@ -18,11 +18,7 @@ import { toCU } from './canvasScale';
* Gallery section names for styling
*/
export type GallerySectionName =
| 'header'
| 'title'
| 'span'
| 'card'
| 'wrapper';
'header' | 'title' | 'span' | 'card' | 'wrapper';
/**
* Default values for gallery sections using canvas units.

View File

@ -1,4 +1,5 @@
import * as icon from '@mdi/js';
import { swaggerDocsUrl } from './config';
import { MenuAsideItem } from './types/menu';
const menuAside: MenuAsideItem[] = [
@ -31,7 +32,7 @@ const menuAside: MenuAsideItem[] = [
icon: icon.mdiAccountCircle,
},
{
href: '/api-docs',
href: swaggerDocsUrl,
target: '_blank',
label: 'Swagger',
icon: icon.mdiFileCode,

View File

@ -23,9 +23,7 @@ const ProjectsListPage = () => {
const dispatch = useAppDispatch();
const projectsRaw = useAppSelector((state) => state.projects.data) as
| Project[]
| Project
| undefined;
Project[] | Project | undefined;
// Handle both array (from list fetch) and single object (after edit fetch)
const projects: Project[] = Array.isArray(projectsRaw)
? projectsRaw

View File

@ -114,10 +114,7 @@ export interface CarouselSlide {
export type InfoPanelItemType = 'image' | 'video' | '360';
export type InfoPanelMediaOpenMode = 'panel' | 'fullscreen';
export type InfoPanelImageClickAction =
| 'panel'
| 'fullscreen'
| 'target_page'
| 'external_url';
'panel' | 'fullscreen' | 'target_page' | 'external_url';
export type InfoPanelLinkClickAction = 'target_page' | 'external_url';
/**
@ -165,12 +162,7 @@ export interface InfoPanelInfoSpan {
* - 'images': Media viewer with preview + thumbnail strip
*/
export type InfoPanelSectionType =
| 'header'
| 'title'
| 'text'
| 'spans'
| 'cards'
| 'images';
'header' | 'title' | 'text' | 'spans' | 'cards' | 'images';
/**
* Section instance with unique ID, settings, AND data.
@ -269,8 +261,7 @@ export function generateItemId(): string {
* Extends ElementStyleProperties for CSS styling and ElementEffectProperties for effects.
*/
export interface BaseCanvasElement
extends ElementStyleProperties,
ElementEffectProperties {
extends ElementStyleProperties, ElementEffectProperties {
id: string;
type: CanvasElementType | string;
label?: string;

View File

@ -50,13 +50,7 @@ export interface TableColumnConfig {
valueOptions?: string[];
// Custom render function name
renderType?:
| 'link'
| 'boolean'
| 'date'
| 'datetime'
| 'image'
| 'actions'
| 'relation';
'link' | 'boolean' | 'date' | 'datetime' | 'image' | 'actions' | 'relation';
// For relation columns
relationField?: string;
}

View File

@ -6,22 +6,13 @@
// Asset variant types matching backend model
export type AssetVariantType =
| 'thumbnail'
| 'preview'
| 'webp'
| 'mp4_low'
| 'mp4_high'
| 'original';
'thumbnail' | 'preview' | 'webp' | 'mp4_low' | 'mp4_high' | 'original';
export type AssetType = 'image' | 'video' | 'audio' | 'transition' | 'other';
// Preload job status
export type PreloadJobStatus =
| 'queued'
| 'downloading'
| 'completed'
| 'error'
| 'paused';
'queued' | 'downloading' | 'completed' | 'error' | 'paused';
// Download job for tracking individual asset downloads
export interface PreloadJob {
@ -44,11 +35,7 @@ export interface PreloadJob {
// Project offline status
export type ProjectOfflineStatus =
| 'not_downloaded'
| 'downloading'
| 'downloaded'
| 'outdated'
| 'error';
'not_downloaded' | 'downloading' | 'downloaded' | 'outdated' | 'error';
// Offline project metadata
export interface OfflineProject {

View File

@ -92,11 +92,7 @@ export interface NavigableElement {
* Transition phase from useTransitionPlayback
*/
export type TransitionPhase =
| 'idle'
| 'preparing'
| 'playing'
| 'finishing'
| 'completed';
'idle' | 'preparing' | 'playing' | 'finishing' | 'completed';
/**
* Background transition options

View File

@ -8,13 +8,7 @@
* Color keys for general UI elements
*/
export type ColorKey =
| 'white'
| 'light'
| 'contrast'
| 'success'
| 'danger'
| 'warning'
| 'info';
'white' | 'light' | 'contrast' | 'success' | 'danger' | 'warning' | 'info';
/**
* Color keys for buttons

View File

@ -2,11 +2,7 @@ import type { BaseEntity } from './entities';
export type SystemUiControlType = 'fullscreen' | 'sound' | 'offline';
export type SystemUiControlAnchor =
| 'center'
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right';
'center' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
export interface SystemUiControlSettings {
enabled?: boolean;