Projektstart

This commit is contained in:
2026-01-22 15:49:12 +01:00
parent 7212eb6f7a
commit 57e5f652f8
10637 changed files with 2598792 additions and 64 deletions

View File

@@ -0,0 +1,82 @@
'use strict'
const Ref = require('json-schema-resolver')
const cloner = require('rfdc')({ proto: true, circles: false })
function addHook (fastify, pluginOptions) {
const routes = []
const sharedSchemasMap = new Map()
let hookRun = false
fastify.addHook('onRoute', (routeOptions) => {
const routeConfig = routeOptions.config || {}
const swaggerConfig = routeConfig.swagger || {}
if (routeOptions.method === 'HEAD' && pluginOptions.exposeHeadRoutes !== true && swaggerConfig.exposeHeadRoute !== true) {
return
}
if (
routeOptions.method === 'HEAD' &&
routeOptions.schema !== undefined &&
routeOptions.schema.operationId !== undefined
) {
routes.push(
// If two routes with operationId are added to the swagger
// object, it is no longer valid.
// therefore we suffix the operationId with `-head`.
Object.assign({}, routeOptions, {
schema: Object.assign({}, routeOptions.schema, {
operationId: `${routeOptions.schema.operationId}-head`
})
})
)
return
}
routes.push(routeOptions)
})
fastify.addHook('onRegister', async (instance) => {
// we need to wait the ready event to get all the .getSchemas()
// otherwise it will be empty
// TODO: better handle for schemaId
// when schemaId is the same in difference instance
// the latter will lost
instance.addHook('onReady', (done) => {
const allSchemas = instance.getSchemas()
for (const schemaId of Object.keys(allSchemas)) {
sharedSchemasMap.set(schemaId, allSchemas[schemaId])
}
done()
})
})
fastify.addHook('onReady', (done) => {
hookRun = true
const allSchemas = fastify.getSchemas()
for (const schemaId of Object.keys(allSchemas)) {
// it is the top-level, we do not expect to have duplicate id
sharedSchemasMap.set(schemaId, allSchemas[schemaId])
}
done()
})
return {
routes,
Ref () {
if (hookRun === false) {
throw new Error('.swagger() must be called after .ready()')
}
const externalSchemas = cloner(Array.from(sharedSchemasMap.values()))
return Ref(Object.assign(
{ applicationUri: 'todo.com' },
pluginOptions.refResolver,
{ clone: true, externalSchemas })
)
}
}
}
module.exports = {
addHook
}

View File

@@ -0,0 +1,86 @@
'use strict'
// The swagger standard does not accept the url param with ':'
// so '/user/:id' is not valid.
// This function converts the url in a swagger compliant url string
// => '/user/{id}'
// custom verbs at the end of a url are okay => /user::watch but should be rendered as /user:watch in swagger
const COLON = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_'
function formatParamUrl (str) {
let i, char
let state = 'skip'
let path = ''
let param = ''
let level = 0
// count for regex if no param exist
let regexp = 0
for (i = 0; i < str.length; i++) {
char = str[i]
switch (state) {
case 'colon': {
// we only accept a-zA-Z0-9_ in param
if (COLON.indexOf(char) !== -1) {
param += char
} else if (char === '(') {
state = 'regexp'
level++
} else {
// end
state = 'skip'
path += '{' + param + '}'
path += char
param = ''
}
break
}
case 'regexp': {
if (char === '(') {
level++
} else if (char === ')') {
level--
}
// we end if the level reach zero
if (level === 0) {
state = 'skip'
if (param === '') {
regexp++
param = 'regexp' + String(regexp)
}
path += '{' + param + '}'
param = ''
}
break
}
default: {
// we check if we need to change state
if (char === ':' && str[i + 1] === ':') {
// double colon -> single colon
path += char
// skip one more
i++
} else if (char === ':') {
// single colon -> state colon
state = 'colon'
} else if (char === '(') {
state = 'regexp'
level++
} else if (char === '*') {
// * -> {*}
// should be exist once only
path += '{*}'
} else {
path += char
}
}
}
}
// clean up
if (state === 'colon' && param !== '') {
path += '{' + param + '}'
}
return path
}
module.exports = {
formatParamUrl
}

View File

@@ -0,0 +1,35 @@
'use strict'
const { matchParams } = require('./match-params')
const namePattern = /\{([^{}]+)\}/u
function paramName (param) {
return param.replace(namePattern, (_, captured) => captured)
}
// Generates default parameters schema from the given URL. (ex: /example/{userId})
function generateParamsSchema (url) {
const params = matchParams(url)
const schema = {
params: {
type: 'object',
properties: {}
}
}
schema.params.properties = params.reduce((acc, param) => {
const name = paramName(param)
acc[name] = {
type: 'string'
}
return acc
}, {})
return schema
}
module.exports = {
generateParamsSchema,
paramName
}

View File

@@ -0,0 +1,18 @@
'use strict'
const paramPattern = /\{[^{}]+\}/gu
function hasParams (url) {
if (!url) return false
return paramPattern.test(url)
}
function matchParams (url) {
if (!url) return []
return url.match(paramPattern) || []
}
module.exports = {
hasParams,
matchParams
}

View File

@@ -0,0 +1,16 @@
'use strict'
const fs = require('node:fs')
const path = require('node:path')
function readPackageJson () {
try {
return JSON.parse(fs.readFileSync(path.join(__dirname, '..', '..', 'package.json')))
} catch {
return {}
}
}
module.exports = {
readPackageJson
}

View File

@@ -0,0 +1,42 @@
'use strict'
const { rawRequired } = require('../symbols')
const { xConsume } = require('../constants')
function resolveLocalRef (jsonSchema, externalSchemas) {
if (jsonSchema.type !== undefined && jsonSchema.properties !== undefined) {
// for the shorthand querystring/params/headers declaration
const propertiesMap = Object.keys(jsonSchema.properties).reduce((acc, headers) => {
const rewriteProps = {}
rewriteProps.required = (Array.isArray(jsonSchema.required) && jsonSchema.required.indexOf(headers) >= 0) || false
// save raw required for next restore in the content/<media-type>
if (jsonSchema.properties[headers][xConsume]) {
rewriteProps[rawRequired] = jsonSchema.properties[headers].required
}
const newProps = Object.assign({}, jsonSchema.properties[headers], rewriteProps)
return Object.assign({}, acc, { [headers]: newProps })
}, {})
return propertiesMap
}
// for oneOf, anyOf, allOf support in querystring/params/headers
if (jsonSchema.oneOf || jsonSchema.anyOf || jsonSchema.allOf) {
const schemas = jsonSchema.oneOf || jsonSchema.anyOf || jsonSchema.allOf
return schemas.reduce((acc, schema) => Object.assign(acc, resolveLocalRef(schema, externalSchemas)), {})
}
// $ref is in the format: #/definitions/<resolved definition>/<optional fragment>
if (jsonSchema.$ref) {
const localRef = jsonSchema.$ref.split('/', 3)[2]
if (externalSchemas[localRef]) return resolveLocalRef(externalSchemas[localRef], externalSchemas)
// $ref is in the format: #/components/schemas/<resolved definition>
return resolveLocalRef(externalSchemas[jsonSchema.$ref.split('/', 4)[3]], externalSchemas)
}
return jsonSchema
}
module.exports = {
resolveLocalRef
}

View File

@@ -0,0 +1,18 @@
'use strict'
function resolveSchemaReference (rawSchema, ref) {
const resolvedReference = ref.resolve(rawSchema, { externalSchemas: [ref.definitions().definitions] })
// Ref has format `#/definitions/id`
const schemaId = resolvedReference?.$ref?.split('/', 3)[2]
if (schemaId === undefined) {
return undefined
}
return resolvedReference.definitions?.[schemaId]
}
module.exports = {
resolveSchemaReference
}

View File

@@ -0,0 +1,13 @@
'use strict'
function resolveSwaggerFunction (opts, cache, routes, Ref) {
if (opts.openapi === undefined || opts.openapi === null) {
return require('../spec/swagger')(opts, cache, routes, Ref)
} else {
return require('../spec/openapi')(opts, cache, routes, Ref)
}
}
module.exports = {
resolveSwaggerFunction
}

View File

@@ -0,0 +1,25 @@
'use strict'
function shouldRouteHide (schema, opts) {
const { hiddenTag, hideUntagged } = opts
if (schema?.hide) {
return true
}
const tags = schema?.tags || []
if (tags.length === 0 && hideUntagged) {
return true
}
if (tags.includes(hiddenTag)) {
return true
}
return false
}
module.exports = {
shouldRouteHide
}