mirror of
https://github.com/ragestudio/comty.git
synced 2025-06-18 06:54:15 +00:00
Add OpenAPI plugin and post data/replies route specs
This commit is contained in:
parent
14f38b87c5
commit
9b56c04978
277
packages/server/lb-plugins/openapi/index.js
Normal file
277
packages/server/lb-plugins/openapi/index.js
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
import Endpoint from "../../../../linebridge/server/src/classes/Endpoint"
|
||||||
|
import fs from "node:fs"
|
||||||
|
|
||||||
|
// OpenAPI Extension for Linebridge
|
||||||
|
// Registers and generates OpenAPI specification from registered routes/specifications
|
||||||
|
|
||||||
|
const typeMap = {
|
||||||
|
string: { type: "string" },
|
||||||
|
number: { type: "number" },
|
||||||
|
boolean: { type: "boolean" },
|
||||||
|
date: { type: "string", format: "date-time" },
|
||||||
|
String: { type: "string" },
|
||||||
|
Number: { type: "number" },
|
||||||
|
Boolean: { type: "boolean" },
|
||||||
|
Date: { type: "string", format: "date-time" },
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractProperties(properties) {
|
||||||
|
return Object.entries(properties).reduce((acc, [key, value]) => {
|
||||||
|
let type = value.type?.name ? value.type.name.toLowerCase() : value.type
|
||||||
|
if (type === "array" || type === Array) {
|
||||||
|
acc[key] = { type: "array", items: { type: "string" } }
|
||||||
|
} else {
|
||||||
|
acc[key] = typeMap[type] || { type }
|
||||||
|
}
|
||||||
|
return acc
|
||||||
|
}, {})
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractRequiredKeys(properties) {
|
||||||
|
return Object.keys(properties).filter((key) => properties[key].required)
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSchemaRef(refs, ref) {
|
||||||
|
// Basic validation and check if already added
|
||||||
|
// Removed !ref.constructor check as it prevents Mongoose models (which are constructors)
|
||||||
|
if (!ref || !ref.name || refs.has(ref.name)) {
|
||||||
|
return // Return nothing as original didn't use return value
|
||||||
|
}
|
||||||
|
|
||||||
|
let schemaName
|
||||||
|
let schemaType
|
||||||
|
let rawProperties
|
||||||
|
|
||||||
|
// Check if it's a Mongoose model (simple check for .schema property and .name)
|
||||||
|
// Mongoose models typically have a 'schema' property and a 'name' property
|
||||||
|
if (ref.schema && ref.name) {
|
||||||
|
schemaName = ref.name
|
||||||
|
schemaType = "object" // Mongoose models represent objects
|
||||||
|
rawProperties = ref.schema.obj // Get the raw schema object structure
|
||||||
|
} else {
|
||||||
|
// Assume it's a standard specification ref object
|
||||||
|
// It must have type and properties
|
||||||
|
if (!ref.type || !ref.properties) {
|
||||||
|
console.warn(
|
||||||
|
`[OpenAPIPlugin] Invalid schema ref definition for ${ref.name}. Missing 'type' or 'properties'.`,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
schemaName = ref.name
|
||||||
|
schemaType = ref.type
|
||||||
|
rawProperties = ref.properties
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract required keys and process properties for OpenAPI format
|
||||||
|
const required = extractRequiredKeys(rawProperties)
|
||||||
|
const properties = extractProperties(rawProperties)
|
||||||
|
|
||||||
|
// Store the processed schema definition in the refs map
|
||||||
|
refs.set(schemaName, { type: schemaType, required, properties })
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildParameters(spec) {
|
||||||
|
const params = []
|
||||||
|
|
||||||
|
if (spec.parameters) {
|
||||||
|
for (const [key, value] of Object.entries(spec.parameters)) {
|
||||||
|
params.push({
|
||||||
|
name: key,
|
||||||
|
in: "path",
|
||||||
|
required: true,
|
||||||
|
description: value.description,
|
||||||
|
schema: { type: value.type },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spec.query) {
|
||||||
|
for (const [key, value] of Object.entries(spec.query)) {
|
||||||
|
params.push({
|
||||||
|
name: key,
|
||||||
|
in: "query",
|
||||||
|
required: value.required,
|
||||||
|
description: value.description,
|
||||||
|
schema: { type: value.type },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRequestBody(spec, refs) {
|
||||||
|
if (!spec.body) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
let refName = null
|
||||||
|
|
||||||
|
if (typeof spec.body.ref === "object") {
|
||||||
|
addSchemaRef(refs, spec.body.ref)
|
||||||
|
refName = spec.body.ref.name
|
||||||
|
} else {
|
||||||
|
refName = spec.body.ref.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
description: spec.body.description,
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
type: spec.body.type,
|
||||||
|
$ref: `#/components/schemas/${refName}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildResponses(spec, refs) {
|
||||||
|
const responses = {
|
||||||
|
default: { description: "Default response" },
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spec.returns) {
|
||||||
|
let refName = null
|
||||||
|
|
||||||
|
if (typeof spec.returns.ref === "object") {
|
||||||
|
addSchemaRef(refs, spec.returns.ref)
|
||||||
|
refName = spec.returns.ref.name
|
||||||
|
} else {
|
||||||
|
refName = spec.returns.ref.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
responses[200] = {
|
||||||
|
description: spec.returns.description,
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
type: spec.returns.type,
|
||||||
|
$ref: `#/components/schemas/${refName}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (spec.errors) {
|
||||||
|
for (const [code, err] of Object.entries(spec.errors)) {
|
||||||
|
responses[code] = { description: err.description }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return responses
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateOpenAPIJson(specifications) {
|
||||||
|
const paths = {}
|
||||||
|
const components = { schemas: {} }
|
||||||
|
const refs = new Map()
|
||||||
|
|
||||||
|
for (const spec of specifications) {
|
||||||
|
const path = spec.path.replace(/:([^/]+)/g, `{$1}`)
|
||||||
|
|
||||||
|
if (!paths[path]) {
|
||||||
|
paths[path] = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parameters = buildParameters(spec)
|
||||||
|
const requestBody = buildRequestBody(spec, refs)
|
||||||
|
const responses = buildResponses(spec, refs)
|
||||||
|
|
||||||
|
paths[path][spec.method] = {
|
||||||
|
description: spec.description,
|
||||||
|
parameters,
|
||||||
|
...(requestBody && { requestBody }),
|
||||||
|
responses,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [name, ref] of refs) {
|
||||||
|
components.schemas[name] = {
|
||||||
|
type: ref.type,
|
||||||
|
properties: ref.properties,
|
||||||
|
...(ref.required && ref.required.length > 0
|
||||||
|
? { required: ref.required }
|
||||||
|
: {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
openapi: "3.0.0",
|
||||||
|
info: {
|
||||||
|
title: "api",
|
||||||
|
version: "1.0.0",
|
||||||
|
},
|
||||||
|
paths,
|
||||||
|
components,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class OpenAPIPlugin {
|
||||||
|
constructor(server) {
|
||||||
|
this.server = server
|
||||||
|
}
|
||||||
|
|
||||||
|
specifications = new Set()
|
||||||
|
|
||||||
|
async initialize() {
|
||||||
|
for (const endpoint of this.server.engine.registers) {
|
||||||
|
if (!endpoint.filePath) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Searc for spec file with same name as route file but with '.spec.js' extension
|
||||||
|
const specFilePath = endpoint.filePath.replace(/\.js$/, ".spec.js")
|
||||||
|
|
||||||
|
// Check if the spec file exists
|
||||||
|
if (fs.existsSync(specFilePath)) {
|
||||||
|
try {
|
||||||
|
// Dynamically import the spec file
|
||||||
|
const specModule = await import(specFilePath)
|
||||||
|
|
||||||
|
// Get the specification export (assuming default or named 'specification')
|
||||||
|
const specification =
|
||||||
|
specModule.specification || specModule.default
|
||||||
|
|
||||||
|
if (specification) {
|
||||||
|
// Determine the actual spec details based on method
|
||||||
|
const methodSpec =
|
||||||
|
specification[endpoint.method.toLowerCase()] ??
|
||||||
|
specification // Use lowercase method for key lookup
|
||||||
|
|
||||||
|
// Construct the final spec object for the set
|
||||||
|
const spec = {
|
||||||
|
path: endpoint.route, // Assuming endpoint has route property
|
||||||
|
method: endpoint.method, // Assuming endpoint has method property
|
||||||
|
...methodSpec, // Include all details from the spec file (parameters, body, returns, errors, etc.)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.specifications.add(spec)
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
`[OpenAPIPlugin] Spec file found for ${endpoint.filePath} but no 'specification' or default export found.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`[OpenAPIPlugin] Failed to load spec file ${specFilePath}:`,
|
||||||
|
error,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getOpenApiEndpoint = new Endpoint(async () => {
|
||||||
|
return await generateOpenAPIJson(this.specifications)
|
||||||
|
})
|
||||||
|
|
||||||
|
this.server.register.http({
|
||||||
|
method: "GET",
|
||||||
|
route: "/openapi",
|
||||||
|
fn: getOpenApiEndpoint.handler,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
import Post from "@db_models/post"
|
||||||
|
|
||||||
|
const StagedPostRef = {
|
||||||
|
name: "StagedPostRef",
|
||||||
|
description: "A reference to a staged post",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
...Post.schema.obj,
|
||||||
|
countLikes: {
|
||||||
|
type: "number",
|
||||||
|
description: "The number of likes the post has",
|
||||||
|
},
|
||||||
|
hasReplies: {
|
||||||
|
type: "number",
|
||||||
|
description: "The number of replies the post has",
|
||||||
|
},
|
||||||
|
share_url: {
|
||||||
|
type: "string",
|
||||||
|
description: "The share url of the post",
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: "object",
|
||||||
|
description: "The user who created the post",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
description: "Get data of a post by its id",
|
||||||
|
parameters: {
|
||||||
|
post_id: {
|
||||||
|
type: "string",
|
||||||
|
description: "The id of the post",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
returns: {
|
||||||
|
type: "object",
|
||||||
|
description: "The requested post data",
|
||||||
|
ref: StagedPostRef,
|
||||||
|
},
|
||||||
|
}
|
@ -0,0 +1,40 @@
|
|||||||
|
import Post from "@db_models/post"
|
||||||
|
|
||||||
|
const StagedPostRef = {
|
||||||
|
name: "StagedPostRef",
|
||||||
|
description: "A reference to a staged post",
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
...Post.schema.obj,
|
||||||
|
countLikes: {
|
||||||
|
type: "number",
|
||||||
|
description: "The number of likes the post has",
|
||||||
|
},
|
||||||
|
hasReplies: {
|
||||||
|
type: "number",
|
||||||
|
description: "The number of replies the post has",
|
||||||
|
},
|
||||||
|
share_url: {
|
||||||
|
type: "string",
|
||||||
|
description: "The share url of the post",
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: "object",
|
||||||
|
description: "The user who created the post",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
description: "Get all replies of a post",
|
||||||
|
parameters: {
|
||||||
|
post_id: {
|
||||||
|
type: "string",
|
||||||
|
description: "The id of the post",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
returns: {
|
||||||
|
type: "array",
|
||||||
|
description: "Replies post data",
|
||||||
|
},
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user