diff --git a/example/index.js b/example/index.js new file mode 100644 index 0000000..7f0026b --- /dev/null +++ b/example/index.js @@ -0,0 +1,34 @@ +const path = require('node:path') + +const Fastify = require('fastify') +const fastifyApiReference = require('@scalar/fastify-api-reference') + +const fastifyOpenapiMerge = require('..') + +const fastify = Fastify() + +fastify.register(fastifyOpenapiMerge, { + specDir: path.join(__dirname, 'specs'), + specDefinition: { + openapi: '3.0.0', + info: { + title: 'CRUD API Example', + version: '1.0.0', + description: 'A sample CRUD API for managing items.' + }, + } +}) + +fastify.register(fastifyApiReference, { + routePrefix: '/docs', + configuration: { + spec: { + url: '/openapi/json' + } + } +}) + +fastify.listen({ port: 3000 }, (err) => { + if (err) throw err + console.log('Documentation available on http://localhost:3000/docs') +}) diff --git a/example/specs/api.yaml b/example/specs/api.yaml new file mode 100644 index 0000000..2faa0ca --- /dev/null +++ b/example/specs/api.yaml @@ -0,0 +1,83 @@ +paths: + /items: + get: + summary: Get all items + operationId: getItems + responses: + '200': + description: A list of items + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Item' + post: + summary: Create a new item + operationId: createItem + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ItemInput' + responses: + '201': + description: Item created + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + /items/{id}: + get: + summary: Get an item by ID + operationId: getItemById + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '200': + description: Item details + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + '404': + description: Item not found + put: + summary: Update an item by ID + operationId: updateItem + parameters: + - name: id + in: path + required: true + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ItemInput' + responses: + '200': + description: Item updated + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + delete: + summary: Delete an item by ID + operationId: deleteItem + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + '204': + description: Item deleted diff --git a/example/specs/schemas.yaml b/example/specs/schemas.yaml new file mode 100644 index 0000000..9e1401c --- /dev/null +++ b/example/specs/schemas.yaml @@ -0,0 +1,23 @@ +components: + schemas: + Item: + type: object + properties: + id: + type: string + name: + type: string + description: + type: string + required: + - id + - name + ItemInput: + type: object + properties: + name: + type: string + description: + type: string + required: + - name diff --git a/index.js b/index.js index d02cb13..39775d4 100644 --- a/index.js +++ b/index.js @@ -4,7 +4,6 @@ const path = require('node:path') const fp = require('fastify-plugin') const { glob } = require('glob') -const YAML = require('yaml') const { checkSpecDir } = require('./lib/spec-dir') const { mergeSpec } = require('./lib/merge-spec') @@ -20,11 +19,11 @@ async function fastifyOpenapiMerge (fastify, opts) { for (const specPath of rootsSpec) { const files = await glob('**/*.{yaml,yml,json}', { - cwd: specPath, absolute: false, follow: true, nodir: true, + cwd: specPath, absolute: true, follow: true, nodir: true, }) specFiles.push(...files.map((file) => { - return path.join(specPath, file).split(path.win32.sep).join(path.posix.sep) + return file.split(path.win32.sep).join(path.posix.sep) })) } @@ -48,13 +47,12 @@ async function fastifyOpenapiMerge (fastify, opts) { const mergedSpec = await mergeSpec(specFiles, { customMerge: opts.merge, specDefinition: opts.specDefinition, + yaml: true }) - const yaml = YAML.stringify(mergedSpec) - return reply .type('application/x-yaml') - .send(yaml) + .send(mergedSpec) }, }) } diff --git a/lib/merge-spec.js b/lib/merge-spec.js index 0b299f7..651b321 100644 --- a/lib/merge-spec.js +++ b/lib/merge-spec.js @@ -24,17 +24,17 @@ async function defaultMerge (specs) { return result.output } -async function mergeSpec (specPaths, { specDefinition, customMerge } = {}) { +async function mergeSpec (specPaths, opts = {}) { if (!specPaths.length) { - throw new Error('"specPaths" option array requires one or more paths') + return } - const _merge = customMerge ?? defaultMerge + const _merge = opts.customMerge ?? defaultMerge if (typeof _merge !== 'function') { throw new Error('"customMerge" must be a function') } - specDefinition = specDefinition || defaultSpecDefinition + opts.specDefinition = opts.specDefinition || defaultSpecDefinition const specs = [] @@ -42,7 +42,7 @@ async function mergeSpec (specPaths, { specDefinition, customMerge } = {}) { const content = fs.readFileSync(specPath, 'utf-8') const parse = YAML.parse(content) ?? {} - if (!customMerge) { + if (!opts.customMerge) { specs.push({ oas: parse }) } else { specs.push(parse) @@ -51,10 +51,16 @@ async function mergeSpec (specPaths, { specDefinition, customMerge } = {}) { const result = await _merge(specs) - return { + const merged = { ...result, - ...specDefinition, + ...opts.specDefinition, } + + if (opts.yaml === true) { + return YAML.stringify(merged) + } + + return merged } module.exports = { mergeSpec } diff --git a/package.json b/package.json index 7f139ec..06f4a33 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "yaml": "^2.6.1" }, "devDependencies": { + "@scalar/fastify-api-reference": "^1.25.76", "@types/node": "^22.10.1", "c8": "^10.1.2", "eslint": "^9.16.0", diff --git a/test/merge-spec.test.js b/test/merge-spec.test.js index 7579ecd..4bbdc09 100644 --- a/test/merge-spec.test.js +++ b/test/merge-spec.test.js @@ -83,13 +83,10 @@ test('custom merge not a function', async (t) => { }) test('merge with empty spec', async (t) => { - t.plan(2) - try { - await mergeSpec([]) - } catch (e) { - t.assert.ok(e) - t.assert.strictEqual(e.message, '"specPaths" option array requires one or more paths') - } + t.plan(1) + + const mergedSpec = await mergeSpec([]) + t.assert.ifError(mergedSpec) }) test('merge with YAML file', async (t) => {