@adhd/apigen-plugin-openapi 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +57 -0
- package/README.md +131 -4
- package/index.js +1 -1
- package/index.mjs +15 -15
- package/package.json +3 -3
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
## 0.1.4 (2026-07-23)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### 🚀 Features
|
|
5
|
+
|
|
6
|
+
- session batch — dispatch tool-exec + provider routing, publish hygiene, agent/env/apigen fixes
|
|
7
|
+
|
|
8
|
+
- **release:** nx release independent versioning + verify-dist-load publish gate (Agent 2)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### 🩹 Fixes
|
|
12
|
+
|
|
13
|
+
- resolve build errors from workspace-cleanup merge — unterminated strings, path mappings, lint
|
|
14
|
+
|
|
15
|
+
- **apigen:** update remaining stale @adhd/apigen-errors, core, logical refs
|
|
16
|
+
|
|
17
|
+
- **environment:** correct agent-mcp plugins at: classification (build->runtime)
|
|
18
|
+
|
|
19
|
+
- **apigen:** complete FEAT-APIGEN-022 test coverage — primitive→GET + complex→POST
|
|
20
|
+
|
|
21
|
+
- **apigen:** externalize real npm deps in vite builds — 10 packages shipped broken dist bundles (__filename/timeOrigin crash)
|
|
22
|
+
|
|
23
|
+
- **apigen-plugin-openapi:** sync @adhd/apigen-codegen-openapi range to installed 0.1.2
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
### ❤️ Thank You
|
|
27
|
+
|
|
28
|
+
- pseudosky
|
|
29
|
+
|
|
30
|
+
## 0.1.3 (2026-07-23)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### 🚀 Features
|
|
34
|
+
|
|
35
|
+
- session batch — dispatch tool-exec + provider routing, publish hygiene, agent/env/apigen fixes
|
|
36
|
+
|
|
37
|
+
- **release:** nx release independent versioning + verify-dist-load publish gate (Agent 2)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
### 🩹 Fixes
|
|
41
|
+
|
|
42
|
+
- resolve build errors from workspace-cleanup merge — unterminated strings, path mappings, lint
|
|
43
|
+
|
|
44
|
+
- **apigen:** update remaining stale @adhd/apigen-errors, core, logical refs
|
|
45
|
+
|
|
46
|
+
- **environment:** correct agent-mcp plugins at: classification (build->runtime)
|
|
47
|
+
|
|
48
|
+
- **apigen:** complete FEAT-APIGEN-022 test coverage — primitive→GET + complex→POST
|
|
49
|
+
|
|
50
|
+
- **apigen:** externalize real npm deps in vite builds — 10 packages shipped broken dist bundles (__filename/timeOrigin crash)
|
|
51
|
+
|
|
52
|
+
- **apigen-plugin-openapi:** sync @adhd/apigen-codegen-openapi range to installed 0.1.2
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
### ❤️ Thank You
|
|
56
|
+
|
|
57
|
+
- pseudosky
|
package/README.md
CHANGED
|
@@ -1,14 +1,141 @@
|
|
|
1
1
|
# @adhd/apigen-plugin-openapi
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[`@adhd/apigen-codegen-openapi`](../../codegen/openapi).
|
|
3
|
+
**Mount plugin** — serves a **live OpenAPI 3.1 document** at `GET /_meta/openapi`. The document is derived from the extracted operations at request time, so it always reflects the current service definition.
|
|
5
4
|
|
|
6
|
-
Part of [apigen](
|
|
5
|
+
Part of [apigen](../README.md). Driven via [`@adhd/apigen-cli`](../../../entrypoint/apigen-cli/README.md).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What it does
|
|
10
|
+
|
|
11
|
+
When loaded via `--use` on any HTTP target plugin (`api-fastify`, `api-express`), the openapi plugin contributes a synthetic `_meta/openapi` operation. Hitting `GET /_meta/openapi` returns a full OpenAPI 3.1 spec derived from your operations at request time — every endpoint, parameter type, and response shape, all inferred from your TypeScript types.
|
|
12
|
+
|
|
13
|
+
**No code generation, no build step.** The spec is always current with your running service.
|
|
7
14
|
|
|
8
15
|
```bash
|
|
9
|
-
|
|
16
|
+
curl http://localhost:3000/_meta/openapi
|
|
17
|
+
# → {
|
|
18
|
+
# "openapi": "3.1.0",
|
|
19
|
+
# "info": { "title": "My API", "version": "1.0.0" },
|
|
20
|
+
# "paths": {
|
|
21
|
+
# "/api/getUser": {
|
|
22
|
+
# "get": {
|
|
23
|
+
# "parameters": [{ "name": "id", "in": "query", "schema": { "type": "string" } }],
|
|
24
|
+
# "responses": { "200": { "description": "success" } }
|
|
25
|
+
# }
|
|
26
|
+
# }
|
|
27
|
+
# }
|
|
28
|
+
# }
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## CLI usage
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
# Fastify with OpenAPI doc
|
|
37
|
+
npx @adhd/apigen-cli run --source api.ts --type api-fastify --opt port=3000 --use openapi
|
|
38
|
+
|
|
39
|
+
# With custom title and version
|
|
40
|
+
npx @adhd/apigen-cli run --source api.ts --type api-fastify --use openapi \
|
|
41
|
+
--opt 'useOptions={"openapi":{"title":"User API","version":"2.0.0"}}'
|
|
42
|
+
|
|
43
|
+
# Combined with other plugins
|
|
44
|
+
npx @adhd/apigen-cli run --source api.ts --type api-fastify --use openapi --use health --use logger
|
|
10
45
|
```
|
|
11
46
|
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Programmatic usage
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
import { extract, composeSchemas } from '@adhd/apigen-core-client';
|
|
53
|
+
import { apiFastifyPlugin } from '@adhd/apigen-plugin-api-fastify';
|
|
54
|
+
import { openapiPlugin } from '@adhd/apigen-plugin-openapi';
|
|
55
|
+
|
|
56
|
+
const ops = await extract({ sourceFile: './api.ts', namespace: 'api' });
|
|
57
|
+
const schemas = composeSchemas(
|
|
58
|
+
{
|
|
59
|
+
metadata: { namespace: 'api', phase: '' },
|
|
60
|
+
schemas: Object.fromEntries(
|
|
61
|
+
ops.filter(o => o.kind === 'action').map(op => [
|
|
62
|
+
op.path[op.path.length - 1].raw,
|
|
63
|
+
{ input: op.input, output: op.output, hasCtx: op.hasCtx, 'x-apigen-safe': op.safe },
|
|
64
|
+
])
|
|
65
|
+
),
|
|
66
|
+
},
|
|
67
|
+
[]
|
|
68
|
+
);
|
|
69
|
+
const mod = await import('./api.ts');
|
|
70
|
+
|
|
71
|
+
const abort = new AbortController();
|
|
72
|
+
process.on('SIGINT', () => abort.abort());
|
|
73
|
+
|
|
74
|
+
await apiFastifyPlugin.run({
|
|
75
|
+
packages: [{ id: 'api', schemas, importPath: './api.ts', fns: mod, createClient: async () => ({}) }],
|
|
76
|
+
outputDir: '',
|
|
77
|
+
options: {
|
|
78
|
+
port: 3000,
|
|
79
|
+
usePlugins: [
|
|
80
|
+
openapiPlugin, // ← mounts GET /_meta/openapi
|
|
81
|
+
],
|
|
82
|
+
// Custom title and version:
|
|
83
|
+
// useOptions: { openapi: { title: 'User API', version: '2.0.0' } },
|
|
84
|
+
},
|
|
85
|
+
signal: abort.signal,
|
|
86
|
+
operations: ops,
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Options
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
import { type OpenapiOptions } from '@adhd/apigen-plugin-openapi';
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
| Option | Type | Default | Description |
|
|
99
|
+
|--------|------|---------|-------------|
|
|
100
|
+
| `title` | `string` | `'API'` | `info.title` in the generated OpenAPI document |
|
|
101
|
+
| `version` | `string` | `'0.0.0'` | `info.version` in the generated OpenAPI document |
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## What gets documented
|
|
106
|
+
|
|
107
|
+
Every operation extracted from your source file appears in the `paths` section:
|
|
108
|
+
|
|
109
|
+
- **Safe operations** (`x-apigen-safe: true`) → listed as `GET` with query parameters
|
|
110
|
+
- **Unsafe operations** → listed as `POST` with `requestBody`
|
|
111
|
+
|
|
112
|
+
Rich types (Date, Decimal, bigint, UUID) use their canonical JSON Schema representations with `x-apigen-*` hints so codegen tools can reconstruct the correct types.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## How it works
|
|
117
|
+
|
|
118
|
+
The openapi plugin implements the **mount capability** (SPEC §7.2b). At compose time, it registers a synthetic `_meta/openapi` operation. At request time, the handler calls `toOpenApi()` from `@adhd/apigen-codegen-openapi` with the full `Operation[]` descriptor — the same one used to serve requests — and returns the live document. No caching, no staleness.
|
|
119
|
+
|
|
120
|
+
This is the same function you can use independently:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { toOpenApi } from '@adhd/apigen-codegen-openapi';
|
|
124
|
+
import type { Operation } from '@adhd/apigen-core-client';
|
|
125
|
+
|
|
126
|
+
const doc = toOpenApi(operations, { title: 'My API', version: '1.0.0' });
|
|
127
|
+
// doc satisfies the OpenAPI 3.1 schema
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Exports
|
|
133
|
+
|
|
134
|
+
| Export | Kind |
|
|
135
|
+
|--------|------|
|
|
136
|
+
| `openapiPlugin` | v2 `Plugin<OpenapiOptions>` — mount capability only |
|
|
137
|
+
| `OpenapiOptions` | TypeScript interface |
|
|
138
|
+
|
|
12
139
|
```ts
|
|
13
140
|
import { openapiPlugin, type OpenapiOptions } from '@adhd/apigen-plugin-openapi';
|
|
14
141
|
```
|
package/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});require("ts-morph");require("node:fs");require("node:path");require("ts-json-schema-generator");const d=new Set(["string","number","boolean","integer"]);function v(e){if(typeof e!="object"||e===null)return!1;const t=e;if(t.$ref!==void 0||t.oneOf||t.anyOf||t.allOf)return!1;const{type:r}=t;return typeof r=="string"?d.has(r):Array.isArray(r)?r.every(o=>d.has(o)||o==="null"):Array.isArray(t.enum)&&t.enum.length>0?t.enum.every(o=>["string","number","boolean"].includes(typeof o)):!1}function O(e){if(!e||typeof e!="object")return!1;const t=e.properties;return t===void 0||typeof t!="object"||t===null?!1:Object.values(t).every(v)}const q={"decimal.js":"Decimal"};function A(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}Object.entries(q).map(([e,t])=>({pattern:new RegExp(`import\\(["'][^"']*${A(e)}[^"']*["']\\)\\.\\w+`,"g"),key:t}));function m(e){return e.words.join("-")}function y(e){return e.words.map(
|
|
1
|
+
"use strict";Object.defineProperties(exports,{__esModule:{value:!0},[Symbol.toStringTag]:{value:"Module"}});require("ts-morph");require("node:fs");require("node:path");require("ts-json-schema-generator");const d=new Set(["string","number","boolean","integer"]);function v(e){if(typeof e!="object"||e===null)return!1;const t=e;if(t.$ref!==void 0||t.oneOf||t.anyOf||t.allOf)return!1;const{type:r}=t;return typeof r=="string"?d.has(r):Array.isArray(r)?r.every(o=>d.has(o)||o==="null"):Array.isArray(t.enum)&&t.enum.length>0?t.enum.every(o=>["string","number","boolean"].includes(typeof o)):!1}function O(e){if(!e||typeof e!="object")return!1;const t=e.properties;return t===void 0||typeof t!="object"||t===null?!1:Object.values(t).every(v)}const q={"decimal.js":"Decimal"};function A(e){return e.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")}Object.entries(q).map(([e,t])=>({pattern:new RegExp(`import\\(["'][^"']*${A(e)}[^"']*["']\\)\\.\\w+`,"g"),key:t}));function m(e){return e.words.join("-")}function y(e){return e.words.map(P).join("")}function g(e){return e.words.join("_")}function w(e,t={}){var r,o;const n=[e.namespace,...e.path],i=e.safe||O(e.input)?"GET":"POST",c=((o=(r=t.http)==null?void 0:r.verb)==null?void 0:o[e.id])??i,l="/"+n.map(s=>m(s)).join("/"),a=n.map(s=>g(s)).join("_"),f=n.map(s=>g(s)).slice(0,-1).join("."),u=y(n[n.length-2]??n[0]),p=y(n[n.length-1]),j=n.map(s=>m(s));return{http:{verb:c,route:l},mcp:{name:a},grpc:{package:f,service:u,method:p},cli:{path:j}}}function P(e){return e.length===0?e:e[0].toUpperCase()+e.slice(1)}function S(e,t={}){const r=t.title??"API",o=t.version??"0.0.0",n={};for(const i of e){const c=w(i),{verb:l,route:a}=c.http,f=l.toLowerCase(),u={operationId:i.id,summary:i.id,responses:{200:_(i.output)}};if(l==="GET")u.parameters=T(i.input);else{const p=i.input;typeof p=="object"&&p!==null&&Object.keys(p).length>0&&(u.requestBody={required:!0,content:{"application/json":{schema:p}}})}n[a]===void 0&&(n[a]={}),n[a][f]=u}return{openapi:"3.1.0",info:{title:r,version:o},paths:n}}function _(e){return typeof e=="object"&&e!==null&&Object.keys(e).length>0?{description:"Success",content:{"application/json":{schema:e}}}:{description:"Success"}}function T(e){if(typeof e!="object"||e===null||typeof e.properties!="object"||e.properties===null)return[];const t=Array.isArray(e.required)?e.required:[],r=e.properties;return Object.entries(r).map(([o,n])=>({name:o,in:"query",required:t.includes(o),schema:n}))}function b(e){return{raw:e,words:e.split(/(?=[A-Z])|[-_]/).map(t=>t.toLowerCase()).filter(Boolean)}}function k(e,t={}){return[{id:"_meta/openapi",host:e.host,namespace:b("meta"),path:[b("openapi")],kind:"query",async:!1,streaming:!1,safe:!0,input:{},output:{type:"object"},envelope:{},typeText:null,transports:["http"],handler:r=>S(e.operations,{title:t.title,version:t.version})}]}const h={id:"openapi",description:"Mount plugin: serves the OpenAPI 3.1 doc at GET /meta/openapi",language:"ts",optionsSchema:{type:"object",properties:{title:{type:"string",description:"OpenAPI info.title"},version:{type:"string",description:"OpenAPI info.version"}},additionalProperties:!1},capabilities:{mount:{operations(e,t){return k(e,t)}}}};exports.default=h;exports.openapiPlugin=h;
|
package/index.mjs
CHANGED
|
@@ -43,43 +43,43 @@ function d(e) {
|
|
|
43
43
|
return e.words.join("-");
|
|
44
44
|
}
|
|
45
45
|
function y(e) {
|
|
46
|
-
return e.words.map(
|
|
46
|
+
return e.words.map(q).join("");
|
|
47
47
|
}
|
|
48
48
|
function h(e) {
|
|
49
49
|
return e.words.join("_");
|
|
50
50
|
}
|
|
51
|
-
function
|
|
51
|
+
function w(e, t = {}) {
|
|
52
52
|
var o, r;
|
|
53
|
-
const n = [e.namespace, ...e.path], i = e.safe || v(e.input) ? "GET" : "POST", l = ((r = (o = t.http) == null ? void 0 : o.verb) == null ? void 0 : r[e.id]) ?? i, c = "/" + n.map((
|
|
53
|
+
const n = [e.namespace, ...e.path], i = e.safe || v(e.input) ? "GET" : "POST", l = ((r = (o = t.http) == null ? void 0 : o.verb) == null ? void 0 : r[e.id]) ?? i, c = "/" + n.map((p) => d(p)).join("/"), a = n.map((p) => h(p)).join("_"), f = n.map((p) => h(p)).slice(0, -1).join("."), u = y(n[n.length - 2] ?? n[0]), s = y(n[n.length - 1]), g = n.map((p) => d(p));
|
|
54
54
|
return {
|
|
55
55
|
http: { verb: l, route: c },
|
|
56
56
|
mcp: { name: a },
|
|
57
|
-
grpc: { package: f, service: u, method:
|
|
57
|
+
grpc: { package: f, service: u, method: s },
|
|
58
58
|
cli: { path: g }
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
|
-
function
|
|
61
|
+
function q(e) {
|
|
62
62
|
return e.length === 0 ? e : e[0].toUpperCase() + e.slice(1);
|
|
63
63
|
}
|
|
64
64
|
function P(e, t = {}) {
|
|
65
65
|
const o = t.title ?? "API", r = t.version ?? "0.0.0", n = {};
|
|
66
66
|
for (const i of e) {
|
|
67
|
-
const l =
|
|
67
|
+
const l = w(i), { verb: c, route: a } = l.http, f = c.toLowerCase(), u = {
|
|
68
68
|
operationId: i.id,
|
|
69
69
|
summary: i.id,
|
|
70
70
|
responses: {
|
|
71
|
-
200:
|
|
71
|
+
200: S(i.output)
|
|
72
72
|
}
|
|
73
73
|
};
|
|
74
74
|
if (c === "GET")
|
|
75
|
-
u.parameters =
|
|
75
|
+
u.parameters = k(i.input);
|
|
76
76
|
else {
|
|
77
|
-
const
|
|
78
|
-
typeof
|
|
77
|
+
const s = i.input;
|
|
78
|
+
typeof s == "object" && s !== null && Object.keys(s).length > 0 && (u.requestBody = {
|
|
79
79
|
required: !0,
|
|
80
80
|
content: {
|
|
81
81
|
"application/json": {
|
|
82
|
-
schema:
|
|
82
|
+
schema: s
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
85
|
});
|
|
@@ -92,7 +92,7 @@ function P(e, t = {}) {
|
|
|
92
92
|
paths: n
|
|
93
93
|
};
|
|
94
94
|
}
|
|
95
|
-
function
|
|
95
|
+
function S(e) {
|
|
96
96
|
return typeof e == "object" && e !== null && Object.keys(e).length > 0 ? {
|
|
97
97
|
description: "Success",
|
|
98
98
|
content: {
|
|
@@ -100,7 +100,7 @@ function k(e) {
|
|
|
100
100
|
}
|
|
101
101
|
} : { description: "Success" };
|
|
102
102
|
}
|
|
103
|
-
function
|
|
103
|
+
function k(e) {
|
|
104
104
|
if (typeof e != "object" || e === null || typeof e.properties != "object" || e.properties === null)
|
|
105
105
|
return [];
|
|
106
106
|
const t = Array.isArray(e.required) ? e.required : [], o = e.properties;
|
|
@@ -117,7 +117,7 @@ function b(e) {
|
|
|
117
117
|
words: e.split(/(?=[A-Z])|[-_]/).map((t) => t.toLowerCase()).filter(Boolean)
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
|
-
function
|
|
120
|
+
function E(e, t = {}) {
|
|
121
121
|
return [
|
|
122
122
|
{
|
|
123
123
|
// Canonical id — the `_meta` prefix is a convention for meta-endpoints.
|
|
@@ -162,7 +162,7 @@ const x = {
|
|
|
162
162
|
capabilities: {
|
|
163
163
|
mount: {
|
|
164
164
|
operations(e, t) {
|
|
165
|
-
return
|
|
165
|
+
return E(
|
|
166
166
|
e,
|
|
167
167
|
t
|
|
168
168
|
);
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhd/apigen-plugin-openapi",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"main": "./index.js",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@adhd/apigen-core-client": "^0.1.
|
|
7
|
-
"@adhd/apigen-codegen-openapi": "^0.1.
|
|
6
|
+
"@adhd/apigen-core-client": "^0.1.2",
|
|
7
|
+
"@adhd/apigen-codegen-openapi": "^0.1.4"
|
|
8
8
|
},
|
|
9
9
|
"publishConfig": {
|
|
10
10
|
"access": "public"
|