@tulipes/spec 0.1.0
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/README.md +115 -0
- package/dist/extract.d.ts +41 -0
- package/dist/extract.js +59 -0
- package/dist/extract.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/json-schema.d.ts +17 -0
- package/dist/json-schema.js +95 -0
- package/dist/json-schema.js.map +1 -0
- package/dist/openapi.d.ts +18 -0
- package/dist/openapi.js +171 -0
- package/dist/openapi.js.map +1 -0
- package/dist/postman.d.ts +23 -0
- package/dist/postman.js +134 -0
- package/dist/postman.js.map +1 -0
- package/package.json +48 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# @tulipes/spec
|
|
2
|
+
|
|
3
|
+
Generates an **OpenAPI 3.1 document** and a **Postman collection** from a
|
|
4
|
+
[Tulipes](https://www.npmjs.com/package/@tulipes/core) app's declared
|
|
5
|
+
routes.
|
|
6
|
+
|
|
7
|
+
The source is the RAI registry of a *booted* app — not parsed source — so a
|
|
8
|
+
generated document describes exactly the router that is mounted. It cannot
|
|
9
|
+
invent an endpoint, and it cannot miss one.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
yarn add -D @tulipes/spec
|
|
13
|
+
yarn tulipes spec --openapi docs/openapi.json --postman docs/collection.json
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
wrote docs/openapi.json
|
|
18
|
+
wrote docs/collection.json
|
|
19
|
+
|
|
20
|
+
21 endpoint(s) across 4 folder(s): auth, hello, sessions, users
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Where the content comes from
|
|
24
|
+
|
|
25
|
+
Everything except the request shapes is already declared:
|
|
26
|
+
|
|
27
|
+
| In the document | Comes from |
|
|
28
|
+
|---|---|
|
|
29
|
+
| path, method | the mounted express route |
|
|
30
|
+
| `operationId`, summary, description | the route's `rai({ id, name, description })` |
|
|
31
|
+
| tag / folder | `rai({ folder })`, defaulting to the declaring module |
|
|
32
|
+
| security, `x-tulipes-roles` | the ACL — which roles hold that permission |
|
|
33
|
+
| response envelope | the framework's one response shape |
|
|
34
|
+
| request/response schemas | `rai({ params, query, body, returns })` |
|
|
35
|
+
|
|
36
|
+
Schemas are zod, declared on the route and **validated at runtime by the
|
|
37
|
+
framework** — which is the point. A document generated from the same
|
|
38
|
+
declaration that enforces the request cannot describe a shape the endpoint
|
|
39
|
+
would reject.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { z } from "zod/v4";
|
|
43
|
+
|
|
44
|
+
router.post(
|
|
45
|
+
`${base}/users`,
|
|
46
|
+
rai({
|
|
47
|
+
id: "users:create",
|
|
48
|
+
name: "Create a user",
|
|
49
|
+
body: z.object({ email: z.email("emailInvalid") }),
|
|
50
|
+
returns: z.object({ id: z.string(), email: z.string() }),
|
|
51
|
+
}),
|
|
52
|
+
users.create(),
|
|
53
|
+
);
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Use `zod/v4`.** `z.toJSONSchema` is what this package calls and it only
|
|
57
|
+
understands v4 schemas. A classic `from "zod"` schema still validates, but
|
|
58
|
+
produces no shape in the document — `tulipes spec` reports each one it
|
|
59
|
+
could not describe rather than failing silently.
|
|
60
|
+
|
|
61
|
+
## Why a Postman collection and not just the OpenAPI
|
|
62
|
+
|
|
63
|
+
Postman imports OpenAPI perfectly well. A native collection carries what
|
|
64
|
+
that import cannot:
|
|
65
|
+
|
|
66
|
+
- **one folder per `folder`**, mirroring how the app is organised
|
|
67
|
+
- `{{baseUrl}}`, `{{accessToken}}` and `{{refreshToken}}` variables, with
|
|
68
|
+
bearer auth inherited by every request
|
|
69
|
+
- `noauth` on public routes, so a public endpoint is genuinely exercised
|
|
70
|
+
as the public would reach it
|
|
71
|
+
- a script on the login request that **captures the token pair** — sign in
|
|
72
|
+
once and the rest of the collection is authenticated
|
|
73
|
+
- example bodies built from the schemas, so a request is runnable as
|
|
74
|
+
imported rather than an empty `{}`
|
|
75
|
+
|
|
76
|
+
## Programmatic use
|
|
77
|
+
|
|
78
|
+
The CLI is a thin wrapper; the pieces are exported:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { extract, toOpenApi, toPostman } from "@tulipes/spec";
|
|
82
|
+
import { boot } from "@tulipes/core/boot";
|
|
83
|
+
|
|
84
|
+
const handle = await boot({ rootDir, mode: "backend", listen: false });
|
|
85
|
+
const source = extract(handle.ctx);
|
|
86
|
+
|
|
87
|
+
const { document, warnings } = await toOpenApi(source, {
|
|
88
|
+
servers: ["https://api.example.com"],
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`listen: false` is what lets this run while the app is already serving.
|
|
93
|
+
|
|
94
|
+
## Options
|
|
95
|
+
|
|
96
|
+
| Flag | Meaning |
|
|
97
|
+
|---|---|
|
|
98
|
+
| `--openapi <file>` | write an OpenAPI 3.1 document |
|
|
99
|
+
| `--postman <file>` | write a Postman v2.1 collection |
|
|
100
|
+
| `--url <base>` | base URL both documents advertise; defaults to the app's own `PORT` / `PUBLIC_DOMAIN` |
|
|
101
|
+
|
|
102
|
+
Name at least one output. Commit what it writes: a generated document is
|
|
103
|
+
an artifact a reviewer can diff, and an unexpected change in it usually
|
|
104
|
+
means an unexpected change to the API.
|
|
105
|
+
|
|
106
|
+
## Requirements
|
|
107
|
+
|
|
108
|
+
`@tulipes/core` ≥ 0.6 · zod ≥ 3.25 (for the `zod/v4` subpath) — both peer
|
|
109
|
+
dependencies, so this package never pins a second copy of either.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
<div align="center">
|
|
114
|
+
<sub>Part of <a href="https://www.npmjs.com/package/@tulipes/core">Tulipes</a></sub>
|
|
115
|
+
</div>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Ctx } from "@tulipes/core/boot";
|
|
2
|
+
import type { ValidatableSchema } from "@tulipes/core/http";
|
|
3
|
+
/**
|
|
4
|
+
* Everything a generated document needs about one endpoint, gathered from
|
|
5
|
+
* the running app rather than from parsing source — so a spec cannot
|
|
6
|
+
* describe a route that is not actually mounted, nor miss one that is.
|
|
7
|
+
*/
|
|
8
|
+
export interface SpecRoute {
|
|
9
|
+
id: string;
|
|
10
|
+
method: string;
|
|
11
|
+
/** As express declared it: `/users/:email`. */
|
|
12
|
+
path: string;
|
|
13
|
+
module: string;
|
|
14
|
+
/** Postman folder / OpenAPI tag. `info.folder`, else the module. */
|
|
15
|
+
folder: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
params?: ValidatableSchema;
|
|
19
|
+
query?: ValidatableSchema;
|
|
20
|
+
body?: ValidatableSchema;
|
|
21
|
+
returns?: ValidatableSchema;
|
|
22
|
+
/** Roles that hold this permission. Empty means nobody can reach it. */
|
|
23
|
+
roles: string[];
|
|
24
|
+
/** Whether an unauthenticated caller can reach it. */
|
|
25
|
+
public: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface SpecSource {
|
|
28
|
+
app: {
|
|
29
|
+
name: string;
|
|
30
|
+
version?: string;
|
|
31
|
+
};
|
|
32
|
+
routes: SpecRoute[];
|
|
33
|
+
folders: string[];
|
|
34
|
+
}
|
|
35
|
+
export declare function extract(ctx: Ctx): SpecSource;
|
|
36
|
+
/** `/users/:email` → `/users/{email}`, which is how OpenAPI writes params. */
|
|
37
|
+
export declare function toOpenApiPath(path: string): string;
|
|
38
|
+
/** `/users/:email` → `/users/{{email}}`, Postman's variable syntax. */
|
|
39
|
+
export declare function toPostmanPath(path: string): string;
|
|
40
|
+
/** The `:param` names a path declares, in order. */
|
|
41
|
+
export declare function pathParams(path: string): string[];
|
package/dist/extract.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** The role an unauthenticated caller has — see the framework's guest model. */
|
|
2
|
+
const GUEST = "guest";
|
|
3
|
+
export function extract(ctx) {
|
|
4
|
+
const grants = ctx.acl?.describe() ?? [];
|
|
5
|
+
// A grant reaches a permission directly, through its namespace wildcard,
|
|
6
|
+
// or through "*" — the same three ways the ACL itself resolves one, so
|
|
7
|
+
// the document can never disagree with what the app enforces.
|
|
8
|
+
const rolesFor = (id) => {
|
|
9
|
+
const namespace = `${id.split(":")[0]}:*`;
|
|
10
|
+
const roles = grants
|
|
11
|
+
.filter(({ resource }) => resource === id || resource === "*" || resource === namespace)
|
|
12
|
+
.map(({ role }) => role);
|
|
13
|
+
return [...new Set(roles)].sort();
|
|
14
|
+
};
|
|
15
|
+
const routes = [...ctx.routes.all()]
|
|
16
|
+
.map((record) => {
|
|
17
|
+
const { info } = record;
|
|
18
|
+
const roles = rolesFor(info.id);
|
|
19
|
+
return {
|
|
20
|
+
id: info.id,
|
|
21
|
+
method: record.method.toUpperCase(),
|
|
22
|
+
path: record.path,
|
|
23
|
+
module: record.module,
|
|
24
|
+
folder: info.folder ?? record.module,
|
|
25
|
+
...(info.name && { name: info.name }),
|
|
26
|
+
...(info.description && { description: info.description }),
|
|
27
|
+
...(info.params && { params: info.params }),
|
|
28
|
+
...(info.query && { query: info.query }),
|
|
29
|
+
...(info.body && { body: info.body }),
|
|
30
|
+
...(info.returns && { returns: info.returns }),
|
|
31
|
+
roles,
|
|
32
|
+
public: roles.includes(GUEST),
|
|
33
|
+
};
|
|
34
|
+
})
|
|
35
|
+
.sort((a, b) => a.folder.localeCompare(b.folder) || a.path.localeCompare(b.path));
|
|
36
|
+
return {
|
|
37
|
+
app: {
|
|
38
|
+
name: String(ctx.config.app?.name ?? "api"),
|
|
39
|
+
...(typeof ctx.config.app?.version === "string" && {
|
|
40
|
+
version: String(ctx.config.app.version),
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
routes,
|
|
44
|
+
folders: [...new Set(routes.map((route) => route.folder))].sort(),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** `/users/:email` → `/users/{email}`, which is how OpenAPI writes params. */
|
|
48
|
+
export function toOpenApiPath(path) {
|
|
49
|
+
return path.replace(/:(\w+)/g, "{$1}");
|
|
50
|
+
}
|
|
51
|
+
/** `/users/:email` → `/users/{{email}}`, Postman's variable syntax. */
|
|
52
|
+
export function toPostmanPath(path) {
|
|
53
|
+
return path.replace(/:(\w+)/g, "{{$1}}");
|
|
54
|
+
}
|
|
55
|
+
/** The `:param` names a path declares, in order. */
|
|
56
|
+
export function pathParams(path) {
|
|
57
|
+
return [...path.matchAll(/:(\w+)/g)].map((match) => match[1]);
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=extract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extract.js","sourceRoot":"","sources":["../src/extract.ts"],"names":[],"mappings":"AAkCA,gFAAgF;AAChF,MAAM,KAAK,GAAG,OAAO,CAAC;AAEtB,MAAM,UAAU,OAAO,CAAC,GAAQ;IAC9B,MAAM,MAAM,GAAG,GAAG,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAEzC,yEAAyE;IACzE,uEAAuE;IACvE,8DAA8D;IAC9D,MAAM,QAAQ,GAAG,CAAC,EAAU,EAAY,EAAE;QACxC,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC1C,MAAM,KAAK,GAAG,MAAM;aACjB,MAAM,CAAC,CAAC,EAAE,QAAQ,EAAE,EAAE,EAAE,CAAC,QAAQ,KAAK,EAAE,IAAI,QAAQ,KAAK,GAAG,IAAI,QAAQ,KAAK,SAAS,CAAC;aACvF,GAAG,CAAC,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;QAC3B,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;IACpC,CAAC,CAAC;IAEF,MAAM,MAAM,GAAgB,CAAC,GAAG,GAAG,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;SAC9C,GAAG,CAAC,CAAC,MAAmB,EAAE,EAAE;QAC3B,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC;QACxB,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAEhC,OAAO;YACL,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,WAAW,EAAE;YACnC,IAAI,EAAE,MAAM,CAAC,IAAI;YACjB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM;YACpC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;YACrC,GAAG,CAAC,IAAI,CAAC,WAAW,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC;YAC1D,GAAG,CAAC,IAAI,CAAC,MAAM,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC;YAC3C,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC;YACxC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;YACrC,GAAG,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;YAC9C,KAAK;YACL,MAAM,EAAE,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC;SAC9B,CAAC;IACJ,CAAC,CAAC;SACD,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAEpF,OAAO;QACL,GAAG,EAAE;YACH,IAAI,EAAE,MAAM,CAAE,GAAG,CAAC,MAAM,CAAC,GAAsC,EAAE,IAAI,IAAI,KAAK,CAAC;YAC/E,GAAG,CAAC,OAAQ,GAAG,CAAC,MAAM,CAAC,GAAyC,EAAE,OAAO,KAAK,QAAQ,IAAI;gBACxF,OAAO,EAAE,MAAM,CAAE,GAAG,CAAC,MAAM,CAAC,GAA6B,CAAC,OAAO,CAAC;aACnE,CAAC;SACH;QACD,MAAM;QACN,OAAO,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE;KAClE,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;AACzC,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,OAAO,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;AAC3C,CAAC;AAED,oDAAoD;AACpD,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC;AACjE,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates an OpenAPI document and a Postman collection from a Tulipes
|
|
3
|
+
* app's declared routes.
|
|
4
|
+
*
|
|
5
|
+
* The source is the RAI registry of a *booted* app, not parsed source, so
|
|
6
|
+
* a generated document describes exactly what is mounted — it cannot
|
|
7
|
+
* invent an endpoint or miss one. `tulipes spec` is the CLI over this.
|
|
8
|
+
*/
|
|
9
|
+
export { extract, pathParams, toOpenApiPath, toPostmanPath } from "./extract.js";
|
|
10
|
+
export type { SpecRoute, SpecSource } from "./extract.js";
|
|
11
|
+
export { toOpenApi } from "./openapi.js";
|
|
12
|
+
export type { GeneratedDocument, OpenApiOptions } from "./openapi.js";
|
|
13
|
+
export { toPostman } from "./postman.js";
|
|
14
|
+
export type { PostmanOptions } from "./postman.js";
|
|
15
|
+
export { toJsonSchema, exampleFor } from "./json-schema.js";
|
|
16
|
+
export type { JsonSchema } from "./json-schema.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates an OpenAPI document and a Postman collection from a Tulipes
|
|
3
|
+
* app's declared routes.
|
|
4
|
+
*
|
|
5
|
+
* The source is the RAI registry of a *booted* app, not parsed source, so
|
|
6
|
+
* a generated document describes exactly what is mounted — it cannot
|
|
7
|
+
* invent an endpoint or miss one. `tulipes spec` is the CLI over this.
|
|
8
|
+
*/
|
|
9
|
+
export { extract, pathParams, toOpenApiPath, toPostmanPath } from "./extract.js";
|
|
10
|
+
export { toOpenApi } from "./openapi.js";
|
|
11
|
+
export { toPostman } from "./postman.js";
|
|
12
|
+
export { toJsonSchema, exampleFor } from "./json-schema.js";
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAEjF,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEzC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ValidatableSchema } from "@tulipes/core/http";
|
|
2
|
+
export type JsonSchema = Record<string, unknown>;
|
|
3
|
+
export interface ConvertResult {
|
|
4
|
+
schema?: JsonSchema;
|
|
5
|
+
/** Why a schema was declared but produced nothing, for the report. */
|
|
6
|
+
warning?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function toJsonSchema(schema: ValidatableSchema | undefined, label: string): Promise<ConvertResult>;
|
|
9
|
+
/**
|
|
10
|
+
* A best-effort example object, so a Postman request arrives with a body
|
|
11
|
+
* worth sending rather than an empty `{}`.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately shallow and unclever: it reads defaults, examples, enums
|
|
14
|
+
* and types, and gives up on anything else. A wrong guess in a template
|
|
15
|
+
* body costs nothing; a wrong guess in the schema itself would.
|
|
16
|
+
*/
|
|
17
|
+
export declare function exampleFor(schema: JsonSchema | undefined): unknown;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
let converter;
|
|
2
|
+
async function loadConverter() {
|
|
3
|
+
if (converter !== undefined)
|
|
4
|
+
return converter;
|
|
5
|
+
try {
|
|
6
|
+
const zod = (await import("zod/v4"));
|
|
7
|
+
converter = zod.toJSONSchema ?? null;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
converter = null;
|
|
11
|
+
}
|
|
12
|
+
return converter;
|
|
13
|
+
}
|
|
14
|
+
export async function toJsonSchema(schema, label) {
|
|
15
|
+
if (!schema)
|
|
16
|
+
return {};
|
|
17
|
+
const convert = await loadConverter();
|
|
18
|
+
if (!convert) {
|
|
19
|
+
return { warning: `${label}: zod 4 not available, shape omitted` };
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const json = convert(schema, {
|
|
23
|
+
// A spec describes what a client sends and receives, so a schema
|
|
24
|
+
// that cannot be represented should degrade to "any" rather than
|
|
25
|
+
// abort the whole document.
|
|
26
|
+
unrepresentable: "any",
|
|
27
|
+
io: "input",
|
|
28
|
+
});
|
|
29
|
+
// The $schema declaration belongs to a standalone document, not to an
|
|
30
|
+
// inlined sub-schema inside OpenAPI or Postman.
|
|
31
|
+
delete json.$schema;
|
|
32
|
+
return { schema: json };
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
return { warning: `${label}: ${error.message}` };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A best-effort example object, so a Postman request arrives with a body
|
|
40
|
+
* worth sending rather than an empty `{}`.
|
|
41
|
+
*
|
|
42
|
+
* Deliberately shallow and unclever: it reads defaults, examples, enums
|
|
43
|
+
* and types, and gives up on anything else. A wrong guess in a template
|
|
44
|
+
* body costs nothing; a wrong guess in the schema itself would.
|
|
45
|
+
*/
|
|
46
|
+
export function exampleFor(schema) {
|
|
47
|
+
if (!schema)
|
|
48
|
+
return undefined;
|
|
49
|
+
if (schema.default !== undefined)
|
|
50
|
+
return schema.default;
|
|
51
|
+
if (Array.isArray(schema.examples) && schema.examples.length > 0)
|
|
52
|
+
return schema.examples[0];
|
|
53
|
+
if (Array.isArray(schema.enum) && schema.enum.length > 0)
|
|
54
|
+
return schema.enum[0];
|
|
55
|
+
switch (schema.type) {
|
|
56
|
+
case "object": {
|
|
57
|
+
const properties = (schema.properties ?? {});
|
|
58
|
+
const out = {};
|
|
59
|
+
for (const [key, value] of Object.entries(properties))
|
|
60
|
+
out[key] = exampleFor(value);
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
case "array":
|
|
64
|
+
return [exampleFor(schema.items)].filter((value) => value !== undefined);
|
|
65
|
+
case "integer":
|
|
66
|
+
case "number":
|
|
67
|
+
return typeof schema.minimum === "number" ? schema.minimum : 0;
|
|
68
|
+
case "boolean":
|
|
69
|
+
return false;
|
|
70
|
+
case "null":
|
|
71
|
+
return null;
|
|
72
|
+
case "string":
|
|
73
|
+
return placeholderFor(schema);
|
|
74
|
+
default:
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function placeholderFor(schema) {
|
|
79
|
+
switch (schema.format) {
|
|
80
|
+
case "email":
|
|
81
|
+
return "user@example.com";
|
|
82
|
+
case "uri":
|
|
83
|
+
case "url":
|
|
84
|
+
return "https://example.com";
|
|
85
|
+
case "uuid":
|
|
86
|
+
return "00000000-0000-0000-0000-000000000000";
|
|
87
|
+
case "date-time":
|
|
88
|
+
return new Date(0).toISOString();
|
|
89
|
+
case "date":
|
|
90
|
+
return "1970-01-01";
|
|
91
|
+
default:
|
|
92
|
+
return "string";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=json-schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-schema.js","sourceRoot":"","sources":["../src/json-schema.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SpecSource } from "./extract.js";
|
|
2
|
+
/**
|
|
3
|
+
* OpenAPI 3.1 — chosen over 3.0 because its schema dialect *is* JSON
|
|
4
|
+
* Schema 2020-12, which is exactly what zod emits. On 3.0 every schema
|
|
5
|
+
* would need lossy rewriting (nullable, exclusiveMinimum, examples), and
|
|
6
|
+
* a document that quietly misdescribes a type is worse than none.
|
|
7
|
+
*/
|
|
8
|
+
export interface OpenApiOptions {
|
|
9
|
+
/** Server URLs the document advertises. */
|
|
10
|
+
servers?: string[];
|
|
11
|
+
title?: string;
|
|
12
|
+
version?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface GeneratedDocument {
|
|
15
|
+
document: Record<string, unknown>;
|
|
16
|
+
warnings: string[];
|
|
17
|
+
}
|
|
18
|
+
export declare function toOpenApi(source: SpecSource, options?: OpenApiOptions): Promise<GeneratedDocument>;
|
package/dist/openapi.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { pathParams, toOpenApiPath } from "./extract.js";
|
|
2
|
+
import { toJsonSchema } from "./json-schema.js";
|
|
3
|
+
export async function toOpenApi(source, options = {}) {
|
|
4
|
+
const warnings = [];
|
|
5
|
+
const paths = {};
|
|
6
|
+
for (const route of source.routes) {
|
|
7
|
+
const path = toOpenApiPath(route.path);
|
|
8
|
+
paths[path] ??= {};
|
|
9
|
+
paths[path][route.method.toLowerCase()] = await operation(route, warnings);
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
document: {
|
|
13
|
+
openapi: "3.1.0",
|
|
14
|
+
info: {
|
|
15
|
+
title: options.title ?? source.app.name,
|
|
16
|
+
version: options.version ?? source.app.version ?? "0.0.0",
|
|
17
|
+
},
|
|
18
|
+
...(options.servers?.length && {
|
|
19
|
+
servers: options.servers.map((url) => ({ url })),
|
|
20
|
+
}),
|
|
21
|
+
tags: source.folders.map((name) => ({ name })),
|
|
22
|
+
paths,
|
|
23
|
+
components: {
|
|
24
|
+
securitySchemes: {
|
|
25
|
+
bearerAuth: { type: "http", scheme: "bearer", bearerFormat: "JWT" },
|
|
26
|
+
},
|
|
27
|
+
schemas: { Envelope: ENVELOPE_SCHEMA, ApiError: API_ERROR_SCHEMA },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
warnings,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
async function operation(route, warnings) {
|
|
34
|
+
const collect = (warning) => {
|
|
35
|
+
if (warning)
|
|
36
|
+
warnings.push(warning);
|
|
37
|
+
};
|
|
38
|
+
const [params, query, body, returns] = await Promise.all([
|
|
39
|
+
toJsonSchema(route.params, `${route.id} params`),
|
|
40
|
+
toJsonSchema(route.query, `${route.id} query`),
|
|
41
|
+
toJsonSchema(route.body, `${route.id} body`),
|
|
42
|
+
toJsonSchema(route.returns, `${route.id} returns`),
|
|
43
|
+
]);
|
|
44
|
+
[params, query, body, returns].forEach((result) => collect(result.warning));
|
|
45
|
+
const parameters = [
|
|
46
|
+
...pathParameters(route, params.schema),
|
|
47
|
+
...queryParameters(query.schema),
|
|
48
|
+
];
|
|
49
|
+
return {
|
|
50
|
+
operationId: route.id,
|
|
51
|
+
tags: [route.folder],
|
|
52
|
+
...(route.name && { summary: route.name }),
|
|
53
|
+
...(route.description && { description: route.description }),
|
|
54
|
+
// The permission is the RAI id, and the roles holding it are policy a
|
|
55
|
+
// reader of the document genuinely needs.
|
|
56
|
+
"x-tulipes-permission": route.id,
|
|
57
|
+
"x-tulipes-roles": route.roles,
|
|
58
|
+
...(parameters.length > 0 && { parameters }),
|
|
59
|
+
...(body.schema && {
|
|
60
|
+
requestBody: {
|
|
61
|
+
required: true,
|
|
62
|
+
content: { "application/json": { schema: body.schema } },
|
|
63
|
+
},
|
|
64
|
+
}),
|
|
65
|
+
// A public route needs no credential; every other one does. An empty
|
|
66
|
+
// array on `security` is OpenAPI's way of saying "optional".
|
|
67
|
+
security: route.public ? [] : [{ bearerAuth: [] }],
|
|
68
|
+
responses: responses(route, returns.schema),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Path parameters are always required — a missing one does not match the
|
|
73
|
+
* route at all — and their shapes come from the `params` schema when the
|
|
74
|
+
* route declares one.
|
|
75
|
+
*/
|
|
76
|
+
function pathParameters(route, params) {
|
|
77
|
+
const properties = (params?.properties ?? {});
|
|
78
|
+
return pathParams(route.path).map((name) => ({
|
|
79
|
+
name,
|
|
80
|
+
in: "path",
|
|
81
|
+
required: true,
|
|
82
|
+
schema: properties[name] ?? { type: "string" },
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
function queryParameters(query) {
|
|
86
|
+
if (!query?.properties)
|
|
87
|
+
return [];
|
|
88
|
+
const required = new Set(query.required ?? []);
|
|
89
|
+
return Object.entries(query.properties).map(([name, schema]) => ({
|
|
90
|
+
name,
|
|
91
|
+
in: "query",
|
|
92
|
+
required: required.has(name),
|
|
93
|
+
schema,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Every response is the envelope, so the shell is described once and only
|
|
98
|
+
* `data` varies. A reader learns the contract from any single operation.
|
|
99
|
+
*/
|
|
100
|
+
function responses(route, returns) {
|
|
101
|
+
const success = {
|
|
102
|
+
description: "Success",
|
|
103
|
+
content: {
|
|
104
|
+
"application/json": {
|
|
105
|
+
schema: {
|
|
106
|
+
allOf: [
|
|
107
|
+
{ $ref: "#/components/schemas/Envelope" },
|
|
108
|
+
{
|
|
109
|
+
type: "object",
|
|
110
|
+
properties: {
|
|
111
|
+
success: { const: true },
|
|
112
|
+
data: returns ?? {},
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
const failure = (description) => ({
|
|
121
|
+
description,
|
|
122
|
+
content: {
|
|
123
|
+
"application/json": {
|
|
124
|
+
schema: {
|
|
125
|
+
allOf: [
|
|
126
|
+
{ $ref: "#/components/schemas/Envelope" },
|
|
127
|
+
{ type: "object", properties: { success: { const: false }, data: { const: null } } },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
return {
|
|
134
|
+
"200": success,
|
|
135
|
+
...(hasValidation(route) && { "422": failure("Validation failed") }),
|
|
136
|
+
...(!route.public && {
|
|
137
|
+
"401": failure("Authentication required"),
|
|
138
|
+
"403": failure("Authenticated, but not permitted"),
|
|
139
|
+
}),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
const hasValidation = (route) => Boolean(route.params ?? route.query ?? route.body);
|
|
143
|
+
const API_ERROR_SCHEMA = {
|
|
144
|
+
type: "object",
|
|
145
|
+
required: ["field", "message", "code"],
|
|
146
|
+
properties: {
|
|
147
|
+
field: { type: ["string", "null"], description: "The offending input, when there is one" },
|
|
148
|
+
message: { type: "string", description: "Translated into the caller's locale" },
|
|
149
|
+
code: { type: "string", description: "Stable and never translated — branch on this" },
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
const ENVELOPE_SCHEMA = {
|
|
153
|
+
type: "object",
|
|
154
|
+
required: ["success", "data", "errors", "meta"],
|
|
155
|
+
properties: {
|
|
156
|
+
success: { type: "boolean" },
|
|
157
|
+
data: { description: "The payload on success; null on failure" },
|
|
158
|
+
errors: { type: "array", items: { $ref: "#/components/schemas/ApiError" } },
|
|
159
|
+
meta: {
|
|
160
|
+
type: "object",
|
|
161
|
+
properties: {
|
|
162
|
+
action: { type: "string", description: "What the client should do next" },
|
|
163
|
+
page: { type: "integer" },
|
|
164
|
+
per_page: { type: "integer" },
|
|
165
|
+
total_pages: { type: "integer" },
|
|
166
|
+
total_items: { type: "integer" },
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
//# sourceMappingURL=openapi.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"openapi.js","sourceRoot":"","sources":["../src/openapi.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SpecSource } from "./extract.js";
|
|
2
|
+
import type { GeneratedDocument } from "./openapi.js";
|
|
3
|
+
/**
|
|
4
|
+
* Postman Collection v2.1.
|
|
5
|
+
*
|
|
6
|
+
* This exists rather than telling people to import the OpenAPI document
|
|
7
|
+
* because a collection can carry what a spec cannot: folders per module,
|
|
8
|
+
* a `{{baseUrl}}` and `{{accessToken}}` wired through every request, and a
|
|
9
|
+
* login request that captures its own token so the rest of the collection
|
|
10
|
+
* is authenticated after one click.
|
|
11
|
+
*/
|
|
12
|
+
export interface PostmanOptions {
|
|
13
|
+
/** Default value for the collection's `baseUrl` variable. */
|
|
14
|
+
baseUrl?: string;
|
|
15
|
+
name?: string;
|
|
16
|
+
/**
|
|
17
|
+
* RAI id of the request that returns a token pair. Its response is
|
|
18
|
+
* captured into `{{accessToken}}`, so signing in once authenticates the
|
|
19
|
+
* whole collection. Defaults to "auth:login" when the app declares it.
|
|
20
|
+
*/
|
|
21
|
+
loginRouteId?: string;
|
|
22
|
+
}
|
|
23
|
+
export declare function toPostman(source: SpecSource, options?: PostmanOptions): Promise<GeneratedDocument>;
|
package/dist/postman.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { pathParams, toPostmanPath } from "./extract.js";
|
|
2
|
+
import { exampleFor, toJsonSchema } from "./json-schema.js";
|
|
3
|
+
export async function toPostman(source, options = {}) {
|
|
4
|
+
const warnings = [];
|
|
5
|
+
const loginId = resolveLoginId(source, options.loginRouteId);
|
|
6
|
+
// One folder per `folder` value — the module name unless a route said
|
|
7
|
+
// otherwise — so the collection mirrors how the app is organised.
|
|
8
|
+
const folders = await Promise.all(source.folders.map(async (folder) => ({
|
|
9
|
+
name: folder,
|
|
10
|
+
item: await Promise.all(source.routes
|
|
11
|
+
.filter((route) => route.folder === folder)
|
|
12
|
+
.map((route) => request(route, route.id === loginId, warnings))),
|
|
13
|
+
})));
|
|
14
|
+
return {
|
|
15
|
+
document: {
|
|
16
|
+
info: {
|
|
17
|
+
name: options.name ?? source.app.name,
|
|
18
|
+
description: "Generated from the app's declared routes. Sign in with the auth " +
|
|
19
|
+
"request and every other request is authenticated automatically.",
|
|
20
|
+
schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json",
|
|
21
|
+
},
|
|
22
|
+
item: folders,
|
|
23
|
+
// Collection-level bearer auth, inherited by every request that does
|
|
24
|
+
// not override it — which is why public requests set auth: noauth.
|
|
25
|
+
auth: {
|
|
26
|
+
type: "bearer",
|
|
27
|
+
bearer: [{ key: "token", value: "{{accessToken}}", type: "string" }],
|
|
28
|
+
},
|
|
29
|
+
variable: [
|
|
30
|
+
{ key: "baseUrl", value: options.baseUrl ?? "http://localhost:3000", type: "string" },
|
|
31
|
+
{ key: "accessToken", value: "", type: "string" },
|
|
32
|
+
{ key: "refreshToken", value: "", type: "string" },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
warnings,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Only use a login capture if the app actually declares that route. */
|
|
39
|
+
function resolveLoginId(source, override) {
|
|
40
|
+
const candidate = override ?? "auth:login";
|
|
41
|
+
return source.routes.some((route) => route.id === candidate) ? candidate : undefined;
|
|
42
|
+
}
|
|
43
|
+
async function request(route, isLogin, warnings) {
|
|
44
|
+
const [query, body, params] = await Promise.all([
|
|
45
|
+
toJsonSchema(route.query, `${route.id} query`),
|
|
46
|
+
toJsonSchema(route.body, `${route.id} body`),
|
|
47
|
+
toJsonSchema(route.params, `${route.id} params`),
|
|
48
|
+
]);
|
|
49
|
+
[query, body, params].forEach((r) => r.warning && warnings.push(r.warning));
|
|
50
|
+
const path = toPostmanPath(route.path).replace(/^\//, "").split("/");
|
|
51
|
+
return {
|
|
52
|
+
name: route.name ?? route.id,
|
|
53
|
+
request: {
|
|
54
|
+
method: route.method,
|
|
55
|
+
...(route.description && { description: route.description }),
|
|
56
|
+
header: [
|
|
57
|
+
...(body.schema ? [{ key: "Content-Type", value: "application/json" }] : []),
|
|
58
|
+
// Present but disabled: a reviewer can flip it on to check a
|
|
59
|
+
// locale without reading the i18n documentation first.
|
|
60
|
+
{ key: "Accept-Language", value: "en", disabled: true },
|
|
61
|
+
],
|
|
62
|
+
url: {
|
|
63
|
+
raw: `{{baseUrl}}/${path.join("/")}`,
|
|
64
|
+
host: ["{{baseUrl}}"],
|
|
65
|
+
path,
|
|
66
|
+
...(queryItems(query.schema).length > 0 && { query: queryItems(query.schema) }),
|
|
67
|
+
...(pathParams(route.path).length > 0 && {
|
|
68
|
+
variable: pathVariables(route, params.schema),
|
|
69
|
+
}),
|
|
70
|
+
},
|
|
71
|
+
...(body.schema && {
|
|
72
|
+
body: {
|
|
73
|
+
mode: "raw",
|
|
74
|
+
raw: JSON.stringify(exampleFor(body.schema), null, 2),
|
|
75
|
+
options: { raw: { language: "json" } },
|
|
76
|
+
},
|
|
77
|
+
}),
|
|
78
|
+
// A public route must not send the collection's bearer token: it
|
|
79
|
+
// would mask the very thing a reviewer is checking.
|
|
80
|
+
...(route.public && { auth: { type: "noauth" } }),
|
|
81
|
+
},
|
|
82
|
+
event: isLogin ? [captureTokens()] : [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Query parameters are disabled by default, so a request runs as-is. */
|
|
86
|
+
function queryItems(query) {
|
|
87
|
+
if (!query?.properties)
|
|
88
|
+
return [];
|
|
89
|
+
const required = new Set(query.required ?? []);
|
|
90
|
+
return Object.entries(query.properties).map(([key, schema]) => ({
|
|
91
|
+
key,
|
|
92
|
+
value: String(exampleFor(schema) ?? ""),
|
|
93
|
+
disabled: !required.has(key),
|
|
94
|
+
...(typeof schema.description === "string" && { description: schema.description }),
|
|
95
|
+
}));
|
|
96
|
+
}
|
|
97
|
+
function pathVariables(route, params) {
|
|
98
|
+
const properties = (params?.properties ?? {});
|
|
99
|
+
return pathParams(route.path).map((key) => ({
|
|
100
|
+
key,
|
|
101
|
+
value: String(exampleFor(properties[key]) ?? ""),
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Captures the token pair out of the envelope. Written against
|
|
106
|
+
* `data.access_token`, which is the framework's shape — an app that
|
|
107
|
+
* renames them adjusts this one script rather than every request.
|
|
108
|
+
*/
|
|
109
|
+
function captureTokens() {
|
|
110
|
+
return {
|
|
111
|
+
listen: "test",
|
|
112
|
+
script: {
|
|
113
|
+
type: "text/javascript",
|
|
114
|
+
exec: [
|
|
115
|
+
"const body = pm.response.json();",
|
|
116
|
+
"if (body.success && body.data) {",
|
|
117
|
+
" if (body.data.access_token) {",
|
|
118
|
+
" pm.collectionVariables.set('accessToken', body.data.access_token);",
|
|
119
|
+
" }",
|
|
120
|
+
" if (body.data.refresh_token) {",
|
|
121
|
+
" pm.collectionVariables.set('refreshToken', body.data.refresh_token);",
|
|
122
|
+
" }",
|
|
123
|
+
"}",
|
|
124
|
+
"pm.test('enveloped response', () => {",
|
|
125
|
+
" pm.expect(body).to.have.property('success');",
|
|
126
|
+
" pm.expect(body).to.have.property('data');",
|
|
127
|
+
" pm.expect(body).to.have.property('errors');",
|
|
128
|
+
" pm.expect(body).to.have.property('meta');",
|
|
129
|
+
"});",
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=postman.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postman.js","sourceRoot":"","sources":["../src/postman.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tulipes/spec",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Generate OpenAPI documents and Postman collections from a Tulipes app's declared routes",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
15
|
+
"build": "tsc -p tsconfig.json",
|
|
16
|
+
"prepack": "yarn build"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@tulipes/core": "^0.6.0",
|
|
26
|
+
"zod": "^3.25 || ^4"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@tulipes/core": "0.6.0",
|
|
30
|
+
"@types/node": "^24.2.0",
|
|
31
|
+
"typescript": "^7.0.2",
|
|
32
|
+
"zod": "^3.25"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/kemora13conf/bp-backend-express.git",
|
|
37
|
+
"directory": "packages/spec"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"tulipes",
|
|
41
|
+
"openapi",
|
|
42
|
+
"postman",
|
|
43
|
+
"api-documentation",
|
|
44
|
+
"express",
|
|
45
|
+
"zod"
|
|
46
|
+
],
|
|
47
|
+
"license": "MIT"
|
|
48
|
+
}
|