@webstir-io/module-contract 0.1.13
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/LICENSE +21 -0
- package/README.md +220 -0
- package/dist/adapters/ts-rest.d.ts +38 -0
- package/dist/adapters/ts-rest.js +147 -0
- package/dist/index.d.ts +1517 -0
- package/dist/index.js +109 -0
- package/package.json +64 -0
- package/schema/module-error.schema.json +35 -0
- package/schema/module-manifest.schema.json +357 -0
- package/schema/route-definition.schema.json +165 -0
- package/schema/route-input.schema.json +45 -0
- package/schema/route-output.schema.json +47 -0
- package/schema/view-definition.schema.json +88 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Electric Coding LLC and contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# @webstir-io/module-contract
|
|
2
|
+
|
|
3
|
+
TypeScript interfaces, helper utilities, and JSON schema describing Webstir modules and providers. The contract covers build-time provider APIs and the runtime surface (contexts, manifests, routes, views) that providers expose to the orchestrator.
|
|
4
|
+
|
|
5
|
+
## Status
|
|
6
|
+
|
|
7
|
+
- Experimental contract for the Webstir ecosystem — shapes and helpers may change as providers and orchestrator behavior are refined.
|
|
8
|
+
- Expect breaking changes across early versions; pin carefully if you integrate it outside Webstir itself.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```ini
|
|
13
|
+
# .npmrc
|
|
14
|
+
@webstir-io:registry=https://npm.pkg.github.com
|
|
15
|
+
//npm.pkg.github.com/:_authToken=${GH_PACKAGES_TOKEN}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @webstir-io/module-contract
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Consumers only need `read:packages`; publishers also require `write:packages`.
|
|
23
|
+
|
|
24
|
+
## Provided Types
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import {
|
|
28
|
+
ModuleProvider,
|
|
29
|
+
ModuleBuildResult,
|
|
30
|
+
moduleManifestSchema,
|
|
31
|
+
routeDefinitionSchema,
|
|
32
|
+
viewDefinitionSchema,
|
|
33
|
+
defineRoute,
|
|
34
|
+
defineView,
|
|
35
|
+
createModule,
|
|
36
|
+
CONTRACT_VERSION,
|
|
37
|
+
type RequestContext,
|
|
38
|
+
type SSRContext,
|
|
39
|
+
} from '@webstir-io/module-contract';
|
|
40
|
+
|
|
41
|
+
// Optional adapters live under a secondary export for teams using ts-rest
|
|
42
|
+
import { fromTsRestRoute, fromTsRestRouter } from '@webstir-io/module-contract/ts-rest';
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- `ModuleProvider` remains the build-time contract (`metadata`, `resolveWorkspace`, `build`).
|
|
46
|
+
- `moduleManifestSchema` / `routeDefinitionSchema` / `viewDefinitionSchema` model the runtime manifest that orchestrators ingest; the package publishes matching JSON schema under `schema/`.
|
|
47
|
+
- `defineRoute`, `defineView`, and `createModule` give providers ergonomic helpers with strong TypeScript inference.
|
|
48
|
+
- `RequestContext` and `SSRContext` describe what the orchestrator supplies to route and view handlers.
|
|
49
|
+
- `fromTsRestRoute` converts an `@ts-rest/core` route contract into a Webstir `RouteSpec`, and `fromTsRestRouter` adapts an entire ts-rest router tree at once.
|
|
50
|
+
- Routes and views support optional SSG metadata: `renderMode?: 'ssg' | 'ssr' | 'spa'`, `staticPaths?: string[]`, and a reserved `ssg?: { revalidateSeconds?: number }` bag for future incremental/static revalidation hints.
|
|
51
|
+
|
|
52
|
+
> Install `@ts-rest/core` to use the adapters; it's published as an optional peer dependency of this package.
|
|
53
|
+
|
|
54
|
+
## Schema References
|
|
55
|
+
|
|
56
|
+
`SchemaReference` objects describe how a manifest entry maps back to the typed schema that produced it. Webstir modules emit these references in `route.input`, `route.output`, and `view` definitions so downstream tooling can point developers to the right file when something fails validation.
|
|
57
|
+
|
|
58
|
+
Format:
|
|
59
|
+
- `kind` — schema system. Supported values: `zod` (default), `json-schema`, `ts-rest`.
|
|
60
|
+
- `name` — PascalCase identifier that matches the exported symbol or schema `$id`.
|
|
61
|
+
- `source` — optional module specifier (usually a workspace-relative path like `src/backend/routes/accounts.ts` or a package entry such as `@demo/contracts/accounts.ts`).
|
|
62
|
+
|
|
63
|
+
Naming & source guidance:
|
|
64
|
+
- Keep `name` stable across builds. Use the TypeScript identifier for `zod`, the `$id` (or filename) for JSON Schema, and the router key for ts-rest adapters.
|
|
65
|
+
- Use `source` whenever the schema lives outside the generated manifest (for example, in `src/shared/contracts/`), and prefer workspace-relative paths so the CLI can resolve them after scaffolding.
|
|
66
|
+
- When emitting references from the CLI, serialize the tuple as `kind:name@source` (drop `kind:` for `zod` and omit `@source` when not needed). This is the same string accepted by `--*-schema` flags on `webstir add-route`.
|
|
67
|
+
- Keep `kind`/`name` unique per file to avoid ambiguity when generators pre-populate manifest entries.
|
|
68
|
+
|
|
69
|
+
## Usage Example
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { createModule, defineRoute, defineView, RequestContext, SSRContext } from '@webstir-io/module-contract';
|
|
73
|
+
import { z } from 'zod';
|
|
74
|
+
|
|
75
|
+
const paramsSchema = z.object({ id: z.string().uuid() });
|
|
76
|
+
const responseSchema = z.object({
|
|
77
|
+
id: z.string().uuid(),
|
|
78
|
+
email: z.string().email()
|
|
79
|
+
});
|
|
80
|
+
const viewDataSchema = z.object({ account: responseSchema });
|
|
81
|
+
|
|
82
|
+
const getAccount = defineRoute<RequestContext, typeof paramsSchema, undefined, undefined, typeof responseSchema>({
|
|
83
|
+
definition: {
|
|
84
|
+
name: 'getAccount',
|
|
85
|
+
method: 'GET',
|
|
86
|
+
path: '/accounts/:id',
|
|
87
|
+
input: {
|
|
88
|
+
params: { kind: 'zod', name: 'AccountRouteParams', source: 'src/backend/server/routes/accounts.ts' }
|
|
89
|
+
},
|
|
90
|
+
output: {
|
|
91
|
+
body: { kind: 'zod', name: 'AccountRouteResponse', source: 'src/backend/server/routes/accounts.ts' },
|
|
92
|
+
status: 200
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
schemas: {
|
|
96
|
+
params: paramsSchema,
|
|
97
|
+
response: responseSchema
|
|
98
|
+
},
|
|
99
|
+
handler: async (ctx) => ({
|
|
100
|
+
status: 200,
|
|
101
|
+
body: await ctx.db.accounts.findById(ctx.params.id)
|
|
102
|
+
})
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const accountView = defineView<SSRContext, typeof paramsSchema, typeof viewDataSchema>({
|
|
106
|
+
definition: {
|
|
107
|
+
name: 'AccountView',
|
|
108
|
+
path: '/accounts/:id',
|
|
109
|
+
params: { kind: 'zod', name: 'AccountViewParams', source: 'src/backend/views/account.ts' },
|
|
110
|
+
data: { kind: 'zod', name: 'AccountViewData', source: 'src/backend/views/account.ts' },
|
|
111
|
+
// Optional SSG hints for frontend providers
|
|
112
|
+
renderMode: 'ssg',
|
|
113
|
+
staticPaths: ['/accounts/demo']
|
|
114
|
+
},
|
|
115
|
+
params: paramsSchema,
|
|
116
|
+
data: viewDataSchema,
|
|
117
|
+
load: async (ctx) => ({ account: await ctx.env.api.fetchAccount(ctx.params.id) })
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
export const accountsModule = createModule({
|
|
121
|
+
manifest: {
|
|
122
|
+
contractVersion: CONTRACT_VERSION,
|
|
123
|
+
name: '@demo/accounts',
|
|
124
|
+
version: '0.0.1',
|
|
125
|
+
kind: 'backend',
|
|
126
|
+
capabilities: ['auth', 'db', 'views'],
|
|
127
|
+
// Optional: pass-through metadata for providers
|
|
128
|
+
assets: [],
|
|
129
|
+
middlewares: [],
|
|
130
|
+
routes: [getAccount.definition],
|
|
131
|
+
views: [accountView.definition]
|
|
132
|
+
},
|
|
133
|
+
routes: [getAccount],
|
|
134
|
+
views: [accountView]
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### ts-rest Router Example
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
import { initContract } from '@ts-rest/core';
|
|
142
|
+
import { fromTsRestRouter, type RequestContext } from '@webstir-io/module-contract/ts-rest';
|
|
143
|
+
import { z } from 'zod';
|
|
144
|
+
|
|
145
|
+
const c = initContract();
|
|
146
|
+
|
|
147
|
+
const accountsRouter = c.router({
|
|
148
|
+
list: c.query({
|
|
149
|
+
path: '/accounts',
|
|
150
|
+
method: 'GET',
|
|
151
|
+
responses: {
|
|
152
|
+
200: z.object({
|
|
153
|
+
data: z.array(z.object({ id: z.string(), email: z.string().email() }))
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
}),
|
|
157
|
+
detail: c.query({
|
|
158
|
+
path: '/accounts/:id',
|
|
159
|
+
method: 'GET',
|
|
160
|
+
pathParams: z.object({ id: z.string().uuid() }),
|
|
161
|
+
responses: {
|
|
162
|
+
200: z.object({ id: z.string().uuid(), email: z.string().email() }),
|
|
163
|
+
404: z.null()
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const routeSpecs = fromTsRestRouter<RequestContext>({
|
|
169
|
+
router: accountsRouter,
|
|
170
|
+
baseName: 'accounts',
|
|
171
|
+
createRoute: ({ keyPath, appRoute }) => ({
|
|
172
|
+
handler: async (ctx) => {
|
|
173
|
+
if (keyPath.at(-1) === 'detail') {
|
|
174
|
+
const account = await ctx.db.accounts.findById(ctx.params.id);
|
|
175
|
+
return account
|
|
176
|
+
? { status: 200, body: account }
|
|
177
|
+
: { status: 404, errors: [{ code: 'not_found', message: 'Account missing' }] };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const accounts = await ctx.db.accounts.list();
|
|
181
|
+
return { status: 200, body: { data: accounts } };
|
|
182
|
+
},
|
|
183
|
+
successStatus: appRoute.method === 'GET' ? 200 : undefined
|
|
184
|
+
})
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// routeSpecs is a RouteSpec[] ready to feed into createModule({ routes: routeSpecs })
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
When authoring a provider:
|
|
191
|
+
|
|
192
|
+
1. Populate `metadata` with id, version, and CLI compatibility info.
|
|
193
|
+
2. Use `createModule`/`defineRoute`/`defineView` to declare runtime capabilities with Zod-powered validation.
|
|
194
|
+
3. Return absolute filesystem paths in `ModuleArtifact.path` from the build step.
|
|
195
|
+
4. Emit `ModuleDiagnostic`s for recoverable issues and include the module manifest in `ModuleBuildResult.manifest.module`.
|
|
196
|
+
|
|
197
|
+
## Community & Support
|
|
198
|
+
|
|
199
|
+
- Code of Conduct: https://github.com/webstir-io/.github/blob/main/CODE_OF_CONDUCT.md
|
|
200
|
+
- Contributing guidelines: https://github.com/webstir-io/.github/blob/main/CONTRIBUTING.md
|
|
201
|
+
- Security policy and disclosure process: https://github.com/webstir-io/.github/blob/main/SECURITY.md
|
|
202
|
+
- Support expectations and contact channels: https://github.com/webstir-io/.github/blob/main/SUPPORT.md
|
|
203
|
+
|
|
204
|
+
## Maintainer Workflow
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
npm install
|
|
208
|
+
npm run clean # remove dist/schema artifacts
|
|
209
|
+
npm run build # compiles TypeScript, regenerates schema/*.schema.json
|
|
210
|
+
npm run test # type-checks the Accounts example module
|
|
211
|
+
# Release helper (bumps version, pushes tags to trigger release workflow)
|
|
212
|
+
npm run release -- patch
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
- The `schema/` folder contains `*-definition.schema.json` files derived from the exported Zod schemas. Commit them with contract changes.
|
|
216
|
+
- Ensure CI runs `npm ci`, `npm run clean`, `npm run build`, `npm run test`, and `npm run smoke` before publish.
|
|
217
|
+
|
|
218
|
+
## License
|
|
219
|
+
|
|
220
|
+
MIT © Webstir
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AppRoute, AppRouter } from '@ts-rest/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { ModuleError, RequestContext, RouteHandler, RouteSpec } from '../index.js';
|
|
4
|
+
interface BaseFromTsRestRouteOptions {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly appRoute: AppRoute;
|
|
7
|
+
readonly summary?: string;
|
|
8
|
+
readonly description?: string;
|
|
9
|
+
readonly tags?: readonly string[];
|
|
10
|
+
readonly schemaBaseName?: string;
|
|
11
|
+
readonly successStatus?: number;
|
|
12
|
+
readonly errors?: readonly ModuleError[];
|
|
13
|
+
readonly errorSchemas?: readonly z.ZodTypeAny[];
|
|
14
|
+
}
|
|
15
|
+
export interface FromTsRestRouteOptions<TContext extends RequestContext, TParams extends z.ZodTypeAny | undefined, TQuery extends z.ZodTypeAny | undefined, TBody extends z.ZodTypeAny | undefined, TResponse extends z.ZodTypeAny> extends BaseFromTsRestRouteOptions {
|
|
16
|
+
readonly handler: RouteHandler<TContext, TParams, TQuery, TBody, TResponse>;
|
|
17
|
+
readonly paramsSchema?: TParams;
|
|
18
|
+
readonly querySchema?: TQuery;
|
|
19
|
+
readonly bodySchema?: TBody;
|
|
20
|
+
readonly headersSchema?: z.ZodTypeAny;
|
|
21
|
+
readonly responseSchema?: TResponse;
|
|
22
|
+
}
|
|
23
|
+
type AnyRouteOptions<TContext extends RequestContext> = FromTsRestRouteOptions<TContext, z.ZodTypeAny | undefined, z.ZodTypeAny | undefined, z.ZodTypeAny | undefined, z.ZodTypeAny>;
|
|
24
|
+
export interface RouterRouteConfig<TContext extends RequestContext> extends Omit<AnyRouteOptions<TContext>, 'appRoute' | 'name'> {
|
|
25
|
+
readonly name?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface FromTsRestRouterOptions<TContext extends RequestContext> {
|
|
28
|
+
readonly router: AppRouter;
|
|
29
|
+
readonly baseName?: string;
|
|
30
|
+
readonly createRoute: (info: {
|
|
31
|
+
readonly key: string;
|
|
32
|
+
readonly keyPath: readonly string[];
|
|
33
|
+
readonly appRoute: AppRoute;
|
|
34
|
+
}) => RouterRouteConfig<TContext>;
|
|
35
|
+
}
|
|
36
|
+
export declare function fromTsRestRoute<TContext extends RequestContext, TParams extends z.ZodTypeAny | undefined = undefined, TQuery extends z.ZodTypeAny | undefined = undefined, TBody extends z.ZodTypeAny | undefined = undefined, TResponse extends z.ZodTypeAny = z.ZodTypeAny>(options: FromTsRestRouteOptions<TContext, TParams, TQuery, TBody, TResponse>): RouteSpec<TContext, TParams, TQuery, TBody, TResponse>;
|
|
37
|
+
export declare function fromTsRestRouter<TContext extends RequestContext>(options: FromTsRestRouterOptions<TContext>): RouteSpec<TContext, any, any, any, any>[];
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { ContractNoBody, isAppRoute, isAppRouteMutation, isAppRouteNoBody, isAppRouteOtherResponse, isZodType } from '@ts-rest/core';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
const DEFAULT_SUCCESS_CODES = [200, 201, 202, 204];
|
|
4
|
+
const noopResponseSchema = z.void();
|
|
5
|
+
const isNonEmpty = (value) => Array.isArray(value) && value.length > 0;
|
|
6
|
+
const sanitizeSchemaBase = (raw) => raw.replace(/[^A-Za-z0-9]+/g, '_');
|
|
7
|
+
const capitalize = (value) => value.length === 0 ? value : value[0].toUpperCase() + value.slice(1);
|
|
8
|
+
const buildSchemaNames = (baseName, schemas) => {
|
|
9
|
+
const names = {};
|
|
10
|
+
const safeBase = sanitizeSchemaBase(baseName);
|
|
11
|
+
if (schemas.params) {
|
|
12
|
+
names.params = { kind: 'zod', name: `${safeBase}${capitalize('params')}` };
|
|
13
|
+
}
|
|
14
|
+
if (schemas.query) {
|
|
15
|
+
names.query = { kind: 'zod', name: `${safeBase}${capitalize('query')}` };
|
|
16
|
+
}
|
|
17
|
+
if (schemas.body) {
|
|
18
|
+
names.body = { kind: 'zod', name: `${safeBase}${capitalize('body')}` };
|
|
19
|
+
}
|
|
20
|
+
if (schemas.headers) {
|
|
21
|
+
names.headers = { kind: 'zod', name: `${safeBase}${capitalize('headers')}` };
|
|
22
|
+
}
|
|
23
|
+
if (schemas.response) {
|
|
24
|
+
names.response = { kind: 'zod', name: `${safeBase}${capitalize('response')}` };
|
|
25
|
+
}
|
|
26
|
+
return names;
|
|
27
|
+
};
|
|
28
|
+
const selectResponse = (route, preferred) => {
|
|
29
|
+
const statusCodes = Object.keys(route.responses).map((status) => Number.parseInt(status, 10));
|
|
30
|
+
if (statusCodes.length === 0) {
|
|
31
|
+
return { response: undefined };
|
|
32
|
+
}
|
|
33
|
+
const ordered = statusCodes.sort((a, b) => a - b);
|
|
34
|
+
const preferredStatus = preferred ?? ordered.find((code) => DEFAULT_SUCCESS_CODES.includes(code));
|
|
35
|
+
const status = preferredStatus ?? ordered[0];
|
|
36
|
+
return { status, response: route.responses[status] };
|
|
37
|
+
};
|
|
38
|
+
const toZod = (value) => {
|
|
39
|
+
if (!value) {
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
if (isZodType(value)) {
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
if (typeof value === 'object' && value !== null && 'body' in value) {
|
|
46
|
+
const body = value.body;
|
|
47
|
+
if (isZodType(body)) {
|
|
48
|
+
return body;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
};
|
|
53
|
+
const extractResponseSchema = (response) => {
|
|
54
|
+
if (!response) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
if (response === ContractNoBody || isAppRouteNoBody(response)) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
if (isAppRouteOtherResponse(response)) {
|
|
61
|
+
return toZod(response.body);
|
|
62
|
+
}
|
|
63
|
+
return toZod(response);
|
|
64
|
+
};
|
|
65
|
+
export function fromTsRestRoute(options) {
|
|
66
|
+
const { appRoute, name, summary, description, tags, schemaBaseName, successStatus, paramsSchema, querySchema, bodySchema, headersSchema, responseSchema, handler, errors, errorSchemas } = options;
|
|
67
|
+
const resolvedParamsSchema = (paramsSchema ?? toZod(appRoute.pathParams));
|
|
68
|
+
const resolvedQuerySchema = (querySchema ?? toZod(appRoute.query));
|
|
69
|
+
const resolvedHeadersSchema = headersSchema ?? toZod(appRoute.headers);
|
|
70
|
+
const mutationBodySchema = isAppRouteMutation(appRoute) ? toZod(appRoute.body) : undefined;
|
|
71
|
+
const resolvedBodySchema = (bodySchema ?? mutationBodySchema);
|
|
72
|
+
const { status: derivedStatus, response: routeResponse } = selectResponse(appRoute, successStatus);
|
|
73
|
+
const baseResponseSchema = extractResponseSchema(routeResponse);
|
|
74
|
+
const resolvedResponseSchema = (responseSchema ?? baseResponseSchema ?? noopResponseSchema);
|
|
75
|
+
const schemaNames = buildSchemaNames(schemaBaseName ?? name, {
|
|
76
|
+
params: resolvedParamsSchema,
|
|
77
|
+
query: resolvedQuerySchema,
|
|
78
|
+
body: resolvedBodySchema,
|
|
79
|
+
headers: resolvedHeadersSchema,
|
|
80
|
+
response: resolvedResponseSchema
|
|
81
|
+
});
|
|
82
|
+
const definitionInput = {
|
|
83
|
+
params: schemaNames.params,
|
|
84
|
+
query: schemaNames.query,
|
|
85
|
+
body: schemaNames.body,
|
|
86
|
+
headers: schemaNames.headers
|
|
87
|
+
};
|
|
88
|
+
const hasInput = Object.values(definitionInput).some((value) => value !== undefined);
|
|
89
|
+
return {
|
|
90
|
+
definition: {
|
|
91
|
+
name,
|
|
92
|
+
method: appRoute.method,
|
|
93
|
+
path: appRoute.path,
|
|
94
|
+
summary: summary ?? appRoute.summary,
|
|
95
|
+
description: description ?? appRoute.description,
|
|
96
|
+
tags: isNonEmpty(tags) ? [...tags] : undefined,
|
|
97
|
+
input: hasInput
|
|
98
|
+
? {
|
|
99
|
+
params: schemaNames.params,
|
|
100
|
+
query: schemaNames.query,
|
|
101
|
+
body: schemaNames.body,
|
|
102
|
+
headers: schemaNames.headers
|
|
103
|
+
}
|
|
104
|
+
: undefined,
|
|
105
|
+
output: schemaNames.response
|
|
106
|
+
? {
|
|
107
|
+
body: schemaNames.response,
|
|
108
|
+
status: derivedStatus
|
|
109
|
+
}
|
|
110
|
+
: undefined,
|
|
111
|
+
errors: isNonEmpty(errors) ? [...errors] : undefined
|
|
112
|
+
},
|
|
113
|
+
schemas: {
|
|
114
|
+
params: resolvedParamsSchema,
|
|
115
|
+
query: resolvedQuerySchema,
|
|
116
|
+
body: resolvedBodySchema,
|
|
117
|
+
headers: resolvedHeadersSchema,
|
|
118
|
+
response: resolvedResponseSchema,
|
|
119
|
+
errors: errorSchemas
|
|
120
|
+
},
|
|
121
|
+
handler
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
export function fromTsRestRouter(options) {
|
|
125
|
+
const { router, createRoute, baseName } = options;
|
|
126
|
+
const specs = [];
|
|
127
|
+
const visit = (node, path) => {
|
|
128
|
+
for (const [key, value] of Object.entries(node)) {
|
|
129
|
+
if (isAppRoute(value)) {
|
|
130
|
+
const keyPath = [...path, key];
|
|
131
|
+
const routeConfig = createRoute({ key, keyPath, appRoute: value });
|
|
132
|
+
const nameSegments = [baseName, ...keyPath.filter(Boolean)].filter(Boolean);
|
|
133
|
+
const routeName = routeConfig.name ?? nameSegments.join('.');
|
|
134
|
+
specs.push(fromTsRestRoute({
|
|
135
|
+
appRoute: value,
|
|
136
|
+
name: routeName,
|
|
137
|
+
...routeConfig
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
visit(value, [...path, key]);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
visit(router, []);
|
|
146
|
+
return specs;
|
|
147
|
+
}
|