@capixjs/transport-rest 0.1.0-alpha.1
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 +180 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/multipart-parser.d.ts +17 -0
- package/dist/multipart-parser.d.ts.map +1 -0
- package/dist/multipart-parser.js +69 -0
- package/dist/multipart-parser.js.map +1 -0
- package/dist/multipart.d.ts +36 -0
- package/dist/multipart.d.ts.map +1 -0
- package/dist/multipart.js +40 -0
- package/dist/multipart.js.map +1 -0
- package/dist/router.d.ts +40 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +255 -0
- package/dist/router.js.map +1 -0
- package/dist/serializer.d.ts +19 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/serializer.js +40 -0
- package/dist/serializer.js.map +1 -0
- package/dist/transport.d.ts +60 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +303 -0
- package/dist/transport.js.map +1 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Capix 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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# @capixjs/transport-rest
|
|
2
|
+
|
|
3
|
+
HTTP/1.1 REST transport for Capix. Mounts your capabilities as REST endpoints with automatic route inference.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @capixjs/core @capixjs/transport-rest zod
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { createServer } from '@capixjs/core';
|
|
15
|
+
import { restTransport } from '@capixjs/transport-rest';
|
|
16
|
+
|
|
17
|
+
createServer({
|
|
18
|
+
capabilities: {
|
|
19
|
+
users: { getUser, listUsers, createUser, updateUser, deleteUser },
|
|
20
|
+
},
|
|
21
|
+
transports: [restTransport({ port: 3000 })],
|
|
22
|
+
}).start();
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Route inference
|
|
26
|
+
|
|
27
|
+
Routes are inferred from capability names — no decorators or annotations needed:
|
|
28
|
+
|
|
29
|
+
| Capability | Group | Method | Path |
|
|
30
|
+
|---|---|---|---|
|
|
31
|
+
| `getUser` (with `id` field) | `users` | GET | `/users/:id` |
|
|
32
|
+
| `listUsers` | `users` | GET | `/users` |
|
|
33
|
+
| `getUsers` | `users` | GET | `/users` |
|
|
34
|
+
| `getMe` | `users` | GET | `/users/me` |
|
|
35
|
+
| `getStats` | `users` | GET | `/users/stats` |
|
|
36
|
+
| `findByEmail` | `users` | GET | `/users` |
|
|
37
|
+
| `searchUsers` | `users` | GET | `/users` |
|
|
38
|
+
| `createUser` | `users` | POST | `/users` |
|
|
39
|
+
| `addUser` | `users` | POST | `/users` |
|
|
40
|
+
| `updateUser` | `users` | PATCH | `/users/:id` |
|
|
41
|
+
| `replaceUser` | `users` | PUT | `/users/:id` |
|
|
42
|
+
| `deleteUser` | `users` | DELETE | `/users/:id` |
|
|
43
|
+
| `removeUser` | `users` | DELETE | `/users/:id` |
|
|
44
|
+
| `unfollow` | `users` | DELETE | `/users/:id/follow` |
|
|
45
|
+
| `bulkStatus` | `users` | POST | `/users/bulk-status` |
|
|
46
|
+
| `register` | `auth` | POST | `/auth/register` |
|
|
47
|
+
|
|
48
|
+
Rules:
|
|
49
|
+
- The path prefix comes from the capability's group key (`users`, `auth`, etc.)
|
|
50
|
+
- `get*` with an `id` field in the input schema → `GET /group/:id`
|
|
51
|
+
- `list*`, `find*`, `fetch*`, `read*`, `search*`, `filter*`, `all*` → `GET /group`
|
|
52
|
+
- `get*` without `id` field → collection (`GET /group`) only when the remainder matches the group name (e.g. `getUsers` in `users`); otherwise named endpoint (`GET /group/remainder`)
|
|
53
|
+
- `create*`, `add*`, `new*` → `POST /group`
|
|
54
|
+
- `un*` → `DELETE /group/:id/verb` (inverse sub-resource)
|
|
55
|
+
- `update*`, `edit*`, `patch*`, `modify*` → `PATCH /group/:id`
|
|
56
|
+
- `replace*`, `set*`, `put*` → `PUT /group/:id`
|
|
57
|
+
- `delete*`, `remove*`, `destroy*`, `cancel*` → `DELETE /group/:id`
|
|
58
|
+
- Anything else with `query` intent → `GET /group/key`
|
|
59
|
+
- Anything else with `mutation` intent → `POST /group/key`
|
|
60
|
+
|
|
61
|
+
Capability keys are converted to kebab-case by default: `bulkStatus` → `bulk-status`. Override with `urlCase: 'camel' | 'snake'`.
|
|
62
|
+
|
|
63
|
+
For nested resource routes (`/projects/:projectId/tasks`), use `overrides`:
|
|
64
|
+
|
|
65
|
+
```ts
|
|
66
|
+
restTransport({
|
|
67
|
+
port: 3000,
|
|
68
|
+
overrides: {
|
|
69
|
+
'tasks.listTasks': { method: 'GET', path: '/projects/:projectId/tasks' },
|
|
70
|
+
'tasks.createTask': { method: 'POST', path: '/projects/:projectId/tasks' },
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## HTTP override
|
|
76
|
+
|
|
77
|
+
Override the inferred route when needed:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const importUsers = capability(
|
|
81
|
+
z.object({ file: z.string() }),
|
|
82
|
+
handler,
|
|
83
|
+
{ http: { method: 'POST', path: '/admin/import' } },
|
|
84
|
+
);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## CORS
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
restTransport({
|
|
91
|
+
port: 3000,
|
|
92
|
+
cors: {
|
|
93
|
+
origin: (origin) => origin.endsWith('.example.com'),
|
|
94
|
+
methods: ['GET', 'POST', 'PATCH', 'DELETE'],
|
|
95
|
+
headers: ['Content-Type', 'Authorization'],
|
|
96
|
+
credentials: true,
|
|
97
|
+
},
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`origin` can be a string, string array, or `(origin: string) => boolean` function.
|
|
102
|
+
|
|
103
|
+
## Request hooks
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
restTransport({
|
|
107
|
+
port: 3000,
|
|
108
|
+
onRequest: (req, res) => {
|
|
109
|
+
res.setHeader('X-Request-Id', crypto.randomUUID());
|
|
110
|
+
},
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## File uploads
|
|
115
|
+
|
|
116
|
+
Use `UploadedFile` from the transport for typed file handling:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import type { UploadedFile } from '@capixjs/transport-rest';
|
|
120
|
+
import { z } from 'zod';
|
|
121
|
+
|
|
122
|
+
const uploadAvatar = capability(
|
|
123
|
+
z.object({
|
|
124
|
+
file: z.custom<UploadedFile>(),
|
|
125
|
+
userId: z.string(),
|
|
126
|
+
}),
|
|
127
|
+
async ({ file, userId }) => {
|
|
128
|
+
await storage.save(`avatars/${userId}`, file.buffer);
|
|
129
|
+
return { url: `/avatars/${userId}` };
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Send multipart/form-data. Non-file fields are merged alongside file fields in the parsed input.
|
|
135
|
+
|
|
136
|
+
## Options
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
restTransport({
|
|
140
|
+
port: 3000, // required
|
|
141
|
+
host: '0.0.0.0', // default: '0.0.0.0'
|
|
142
|
+
maxBodySize: 1_048_576, // bytes, default: 1 MiB
|
|
143
|
+
urlCase: 'kebab', // 'kebab' (default) | 'camel' | 'snake'
|
|
144
|
+
cors: { ... },
|
|
145
|
+
onRequest: (req, res) => void,
|
|
146
|
+
multipart: { maxFileSize, maxFiles, allowedMimeTypes },
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Per-transport capabilities
|
|
151
|
+
|
|
152
|
+
Pass `capabilities` directly to the transport to expose only a subset on REST, independent of other transports:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
const publicAPI = { items: { list: listItems, get: getItem } };
|
|
156
|
+
const memberAPI = { items: { create: createItem, update: updateItem } };
|
|
157
|
+
|
|
158
|
+
createServer({
|
|
159
|
+
context: buildContext,
|
|
160
|
+
transports: [
|
|
161
|
+
restTransport({ port: 3000, capabilities: { ...publicAPI, ...memberAPI } }),
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Exports
|
|
167
|
+
|
|
168
|
+
| Export | Description |
|
|
169
|
+
|---|---|
|
|
170
|
+
| `restTransport(opts)` | Creates an HTTP/REST transport |
|
|
171
|
+
| `generateRoutes(registry, opts?)` | Returns route definitions for a registry |
|
|
172
|
+
| `compileRouter(routes)` | Compiles routes into a radix-tree router |
|
|
173
|
+
| `uploadedFile()` | Zod schema for file upload fields |
|
|
174
|
+
| `UploadedFile` | Type for uploaded file objects |
|
|
175
|
+
| `RestTransportOptions` | Options type for `restTransport` |
|
|
176
|
+
| `GenerateRoutesOptions` | Options type for `generateRoutes` |
|
|
177
|
+
|
|
178
|
+
## License
|
|
179
|
+
|
|
180
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* index.ts — public API for capix-transport-rest
|
|
3
|
+
*/
|
|
4
|
+
export { restTransport } from './transport.js';
|
|
5
|
+
export type { RestTransportOptions } from './transport.js';
|
|
6
|
+
export { compileRouter, generateRoutes } from './router.js';
|
|
7
|
+
export type { RouteDefinition, RouterMatch, Router, GenerateRoutesOptions, HttpOverride } from './router.js';
|
|
8
|
+
export { uploadedFile } from './multipart.js';
|
|
9
|
+
export type { UploadedFile, MultipartOptions } from './multipart.js';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,YAAY,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAC5D,YAAY,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC7G,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,YAAY,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAE/C,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAE5D,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* multipart-parser.ts — busboy-based multipart/form-data parser
|
|
3
|
+
* Depends on: busboy, multipart.ts
|
|
4
|
+
*/
|
|
5
|
+
import type { IncomingHttpHeaders } from 'node:http';
|
|
6
|
+
import type { UploadedFile, MultipartOptions } from './multipart.js';
|
|
7
|
+
export type ParsedMultipart = {
|
|
8
|
+
fields: Record<string, string>;
|
|
9
|
+
files: Record<string, UploadedFile>;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Parses a multipart/form-data body from an already-read Buffer.
|
|
13
|
+
* Fields are returned as strings; files as UploadedFile objects.
|
|
14
|
+
* Rejects with an error that has a `.status` property on limit violations.
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseMultipart(headers: IncomingHttpHeaders, body: Buffer, options?: MultipartOptions): Promise<ParsedMultipart>;
|
|
17
|
+
//# sourceMappingURL=multipart-parser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multipart-parser.d.ts","sourceRoot":"","sources":["../src/multipart-parser.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAErE,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CACrC,CAAC;AAQF;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,eAAe,CAAC,CAgE1B"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* multipart-parser.ts — busboy-based multipart/form-data parser
|
|
3
|
+
* Depends on: busboy, multipart.ts
|
|
4
|
+
*/
|
|
5
|
+
import Busboy from 'busboy';
|
|
6
|
+
function parseError(message, status) {
|
|
7
|
+
return Object.assign(new Error(message), { status });
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Parses a multipart/form-data body from an already-read Buffer.
|
|
11
|
+
* Fields are returned as strings; files as UploadedFile objects.
|
|
12
|
+
* Rejects with an error that has a `.status` property on limit violations.
|
|
13
|
+
*/
|
|
14
|
+
export function parseMultipart(headers, body, options = {}) {
|
|
15
|
+
const maxFileSize = options.maxFileSize ?? 5 * 1024 * 1024;
|
|
16
|
+
const maxFiles = options.maxFiles ?? 1;
|
|
17
|
+
return new Promise((resolve, reject) => {
|
|
18
|
+
const fields = {};
|
|
19
|
+
const files = {};
|
|
20
|
+
let settled = false;
|
|
21
|
+
function fail(err) {
|
|
22
|
+
if (settled)
|
|
23
|
+
return;
|
|
24
|
+
settled = true;
|
|
25
|
+
reject(err);
|
|
26
|
+
}
|
|
27
|
+
const busboy = Busboy({
|
|
28
|
+
headers,
|
|
29
|
+
limits: { fileSize: maxFileSize, files: maxFiles },
|
|
30
|
+
});
|
|
31
|
+
busboy.on('field', (name, value) => {
|
|
32
|
+
fields[name] = value;
|
|
33
|
+
});
|
|
34
|
+
// Fired when the files limit is reached — busboy stops emitting 'file' events beyond this.
|
|
35
|
+
busboy.on('filesLimit', () => {
|
|
36
|
+
fail(parseError(`Too many files. Maximum is ${maxFiles}.`, 400));
|
|
37
|
+
});
|
|
38
|
+
busboy.on('file', (fieldName, stream, info) => {
|
|
39
|
+
const chunks = [];
|
|
40
|
+
let truncated = false;
|
|
41
|
+
stream.on('data', (chunk) => { chunks.push(chunk); });
|
|
42
|
+
stream.on('limit', () => { truncated = true; });
|
|
43
|
+
stream.on('end', () => {
|
|
44
|
+
if (truncated) {
|
|
45
|
+
fail(parseError(`File '${info.filename || fieldName}' exceeds ${maxFileSize} bytes.`, 413));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const buffer = Buffer.concat(chunks);
|
|
49
|
+
files[fieldName] = {
|
|
50
|
+
filename: info.filename || fieldName,
|
|
51
|
+
contentType: info.mimeType || 'application/octet-stream',
|
|
52
|
+
sizeBytes: buffer.byteLength,
|
|
53
|
+
buffer,
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
stream.on('error', (err) => fail(Object.assign(err, { status: 400 })));
|
|
57
|
+
});
|
|
58
|
+
busboy.on('finish', () => {
|
|
59
|
+
if (!settled) {
|
|
60
|
+
settled = true;
|
|
61
|
+
resolve({ fields, files });
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
busboy.on('error', (err) => fail(Object.assign(err, { status: 400 })));
|
|
65
|
+
busboy.write(body);
|
|
66
|
+
busboy.end();
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
//# sourceMappingURL=multipart-parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multipart-parser.js","sourceRoot":"","sources":["../src/multipart-parser.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* multipart.ts — UploadedFile type and Zod schema factory
|
|
3
|
+
* Depends on: zod
|
|
4
|
+
*/
|
|
5
|
+
import type { ZodType } from 'zod';
|
|
6
|
+
export type UploadedFile = {
|
|
7
|
+
filename: string;
|
|
8
|
+
contentType: string;
|
|
9
|
+
sizeBytes: number;
|
|
10
|
+
buffer: Buffer;
|
|
11
|
+
};
|
|
12
|
+
export type MultipartOptions = {
|
|
13
|
+
/** Max bytes per file. Default 5MB. */
|
|
14
|
+
maxFileSize?: number;
|
|
15
|
+
/** Max number of files per request. Default 1. */
|
|
16
|
+
maxFiles?: number;
|
|
17
|
+
};
|
|
18
|
+
type UploadedFileOptions = {
|
|
19
|
+
/** Max bytes for this specific field. Default 5MB. */
|
|
20
|
+
maxSize?: number;
|
|
21
|
+
/** Allowed MIME types. If omitted, all types are accepted. */
|
|
22
|
+
accept?: string[];
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Zod schema for a file uploaded via multipart/form-data.
|
|
26
|
+
* Use inside z.object() alongside regular fields.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* z.object({
|
|
30
|
+
* file: uploadedFile({ maxSize: 2 * 1024 * 1024, accept: ['image/jpeg', 'image/png'] }),
|
|
31
|
+
* title: z.string(),
|
|
32
|
+
* })
|
|
33
|
+
*/
|
|
34
|
+
export declare function uploadedFile(options?: UploadedFileOptions): ZodType<UploadedFile>;
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=multipart.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multipart.d.ts","sourceRoot":"","sources":["../src/multipart.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAEnC,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,EAAK,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAI,MAAM,CAAC;IACpB,MAAM,EAAO,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,uCAAuC;IACvC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,KAAK,mBAAmB,GAAG;IACzB,sDAAsD;IACtD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,OAAO,GAAE,mBAAwB,GAAG,OAAO,CAAC,YAAY,CAAC,CA4BrF"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* multipart.ts — UploadedFile type and Zod schema factory
|
|
3
|
+
* Depends on: zod
|
|
4
|
+
*/
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
/**
|
|
7
|
+
* Zod schema for a file uploaded via multipart/form-data.
|
|
8
|
+
* Use inside z.object() alongside regular fields.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* z.object({
|
|
12
|
+
* file: uploadedFile({ maxSize: 2 * 1024 * 1024, accept: ['image/jpeg', 'image/png'] }),
|
|
13
|
+
* title: z.string(),
|
|
14
|
+
* })
|
|
15
|
+
*/
|
|
16
|
+
export function uploadedFile(options = {}) {
|
|
17
|
+
const maxSize = options.maxSize ?? 5 * 1024 * 1024;
|
|
18
|
+
return z
|
|
19
|
+
.custom((val) => typeof val === 'object' &&
|
|
20
|
+
val !== null &&
|
|
21
|
+
typeof val.filename === 'string' &&
|
|
22
|
+
typeof val.contentType === 'string' &&
|
|
23
|
+
typeof val.sizeBytes === 'number' &&
|
|
24
|
+
Buffer.isBuffer(val.buffer), { message: 'Expected an uploaded file' })
|
|
25
|
+
.superRefine((file, ctx) => {
|
|
26
|
+
if (file.sizeBytes > maxSize) {
|
|
27
|
+
ctx.addIssue({
|
|
28
|
+
code: z.ZodIssueCode.custom,
|
|
29
|
+
message: `File size ${file.sizeBytes} bytes exceeds maximum ${maxSize} bytes`,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
if (options.accept !== undefined && !options.accept.includes(file.contentType)) {
|
|
33
|
+
ctx.addIssue({
|
|
34
|
+
code: z.ZodIssueCode.custom,
|
|
35
|
+
message: `File type '${file.contentType}' not accepted. Allowed: ${options.accept.join(', ')}`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=multipart.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"multipart.js","sourceRoot":"","sources":["../src/multipart.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAwBxB;;;;;;;;;GASG;AACH,MAAM,UAAU,YAAY,CAAC,UAA+B,EAAE;IAC5D,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC;IAEnD,OAAO,CAAC;SACL,MAAM,CACL,CAAC,GAAY,EAAuB,EAAE,CACpC,OAAO,GAAG,KAAK,QAAQ;QACvB,GAAG,KAAK,IAAI;QACZ,OAAQ,GAAoB,CAAC,QAAQ,KAAK,QAAQ;QAClD,OAAQ,GAAoB,CAAC,WAAW,KAAK,QAAQ;QACrD,OAAQ,GAAoB,CAAC,SAAS,KAAK,QAAQ;QACnD,MAAM,CAAC,QAAQ,CAAE,GAAoB,CAAC,MAAM,CAAC,EAC/C,EAAE,OAAO,EAAE,2BAA2B,EAAE,CACzC;SACA,WAAW,CAAC,CAAC,IAAkB,EAAE,GAAoB,EAAE,EAAE;QACxD,IAAI,IAAI,CAAC,SAAS,GAAG,OAAO,EAAE,CAAC;YAC7B,GAAG,CAAC,QAAQ,CAAC;gBACX,IAAI,EAAE,CAAC,CAAC,YAAY,CAAC,MAAM;gBAC3B,OAAO,EAAE,aAAa,IAAI,CAAC,SAAS,0BAA0B,OAAO,QAAQ;aAC9E,CAAC,CAAC;QACL,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;YAC/E,GAAG,CAAC,QAAQ,CAAC;gBACX,IAAI,EAAE,CAAC,CAAC,YAAY,CAAC,MAAM;gBAC3B,OAAO,EAAE,cAAc,IAAI,CAAC,WAAW,4BAA4B,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE;aAC/F,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC"}
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* router.ts — radix-tree router for the REST transport
|
|
3
|
+
* No core dependency except types (CapabilityRegistry, AnyCapability).
|
|
4
|
+
*/
|
|
5
|
+
import type { CapabilityRegistry } from '@capixjs/core';
|
|
6
|
+
export type RouteDefinition = {
|
|
7
|
+
readonly method: string;
|
|
8
|
+
readonly path: string;
|
|
9
|
+
readonly capability: string;
|
|
10
|
+
};
|
|
11
|
+
export type RouterMatch = {
|
|
12
|
+
readonly found: true;
|
|
13
|
+
readonly capability: string;
|
|
14
|
+
readonly params: Record<string, string> | null;
|
|
15
|
+
} | {
|
|
16
|
+
readonly found: false;
|
|
17
|
+
readonly allowedMethods?: string[];
|
|
18
|
+
};
|
|
19
|
+
export type Router = {
|
|
20
|
+
match(method: string, path: string): RouterMatch;
|
|
21
|
+
readonly routes: ReadonlyArray<RouteDefinition>;
|
|
22
|
+
};
|
|
23
|
+
export type HttpOverride = {
|
|
24
|
+
readonly method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS';
|
|
25
|
+
readonly path: string;
|
|
26
|
+
};
|
|
27
|
+
export type GenerateRoutesOptions = {
|
|
28
|
+
/** Case style for URL path segments. Default: 'kebab' (bulkStatus → bulk-status). */
|
|
29
|
+
urlCase?: 'kebab' | 'camel' | 'snake';
|
|
30
|
+
/** Per-capability HTTP route overrides keyed by dot-path. Takes full precedence over URL inference. */
|
|
31
|
+
overrides?: Record<string, HttpOverride>;
|
|
32
|
+
};
|
|
33
|
+
/** Builds a Router from route definitions. Throws on duplicate routes. */
|
|
34
|
+
export declare function compileRouter(routes: RouteDefinition[]): Router;
|
|
35
|
+
/**
|
|
36
|
+
* Generates route definitions from a compiled capability registry.
|
|
37
|
+
* Transport-level overrides take full precedence over URL inference.
|
|
38
|
+
*/
|
|
39
|
+
export declare function generateRoutes(registry: CapabilityRegistry, options?: GenerateRoutesOptions): RouteDefinition[];
|
|
40
|
+
//# sourceMappingURL=router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.d.ts","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAiB,MAAM,eAAe,CAAC;AAGvE,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B,CAAC;AAEF,MAAM,MAAM,WAAW,GACnB;IAAE,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAA;CAAE,GACrG;IAAE,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,CAAC;AAElE,MAAM,MAAM,MAAM,GAAG;IACnB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IACjD,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,eAAe,CAAC,CAAC;CACjD,CAAC;AAEF,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,CAAC;IAClF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,qFAAqF;IACrF,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IACtC,uGAAuG;IACvG,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;CAC1C,CAAC;AA0IF,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,CAmB/D;AA0BD;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,kBAAkB,EAC5B,OAAO,GAAE,qBAA0B,GAClC,eAAe,EAAE,CAqBnB"}
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* router.ts — radix-tree router for the REST transport
|
|
3
|
+
* No core dependency except types (CapabilityRegistry, AnyCapability).
|
|
4
|
+
*/
|
|
5
|
+
import { inferIntent } from '@capixjs/core';
|
|
6
|
+
function newNode() {
|
|
7
|
+
return { handlers: new Map(), staticChildren: new Map() };
|
|
8
|
+
}
|
|
9
|
+
/** Splits a URL path into segments in a single pass (no intermediate array from filter). */
|
|
10
|
+
function splitPath(path) {
|
|
11
|
+
const segments = [];
|
|
12
|
+
let start = path.charCodeAt(0) === 47 ? 1 : 0; // skip leading /
|
|
13
|
+
for (let i = start; i <= path.length; i++) {
|
|
14
|
+
if (i === path.length || path.charCodeAt(i) === 47) { // 47 = '/'
|
|
15
|
+
if (i > start)
|
|
16
|
+
segments.push(path.slice(start, i));
|
|
17
|
+
start = i + 1;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return segments;
|
|
21
|
+
}
|
|
22
|
+
function insertRoute(root, method, path, capability) {
|
|
23
|
+
const segments = splitPath(path);
|
|
24
|
+
// method is already uppercase — compileRouter calls toUpperCase() before insertRoute
|
|
25
|
+
const upperMethod = method;
|
|
26
|
+
let node = root;
|
|
27
|
+
for (const seg of segments) {
|
|
28
|
+
if (seg.startsWith(':')) {
|
|
29
|
+
const paramName = seg.slice(1);
|
|
30
|
+
if (!node.paramChild) {
|
|
31
|
+
node.paramChild = { node: newNode(), methodNames: new Map([[upperMethod, paramName]]) };
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
// Allow different param names per method — only error on same method with conflicting names
|
|
35
|
+
const existing = node.paramChild.methodNames.get(upperMethod);
|
|
36
|
+
if (existing !== undefined && existing !== paramName) {
|
|
37
|
+
throw new Error(`[capix] Router conflict: param name mismatch for ${upperMethod} at same level ` +
|
|
38
|
+
`(':${existing}' vs ':${paramName}') for capability '${capability}'`);
|
|
39
|
+
}
|
|
40
|
+
node.paramChild.methodNames.set(upperMethod, paramName);
|
|
41
|
+
}
|
|
42
|
+
node = node.paramChild.node;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
if (!node.staticChildren.has(seg)) {
|
|
46
|
+
node.staticChildren.set(seg, newNode());
|
|
47
|
+
}
|
|
48
|
+
node = node.staticChildren.get(seg);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (node.handlers.has(upperMethod)) {
|
|
52
|
+
throw new Error(`[capix] Duplicate route: ${upperMethod} ${path} (capability: ${capability})`);
|
|
53
|
+
}
|
|
54
|
+
node.handlers.set(upperMethod, capability);
|
|
55
|
+
}
|
|
56
|
+
function matchRoute(root, method, segments, index, params) {
|
|
57
|
+
if (index === segments.length) {
|
|
58
|
+
if (node_hasAnyHandler(root)) {
|
|
59
|
+
const cap = root.handlers.get(method);
|
|
60
|
+
if (cap !== undefined) {
|
|
61
|
+
// null params means no path params were encountered — skip allocation.
|
|
62
|
+
return { found: true, capability: cap, params };
|
|
63
|
+
}
|
|
64
|
+
return { found: false, allowedMethods: [...root.handlers.keys()] };
|
|
65
|
+
}
|
|
66
|
+
return { found: false };
|
|
67
|
+
}
|
|
68
|
+
const seg = segments[index];
|
|
69
|
+
if (seg === undefined)
|
|
70
|
+
return { found: false };
|
|
71
|
+
// Static match preferred over param match
|
|
72
|
+
const staticChild = root.staticChildren.get(seg);
|
|
73
|
+
if (staticChild !== undefined) {
|
|
74
|
+
const result = matchRoute(staticChild, method, segments, index + 1, params);
|
|
75
|
+
if (result.found)
|
|
76
|
+
return result;
|
|
77
|
+
// If static matched path but not method, return that result
|
|
78
|
+
if (!result.found && result.allowedMethods !== undefined)
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
// Param match — look up the param name for the current method
|
|
82
|
+
if (root.paramChild !== undefined) {
|
|
83
|
+
// Conditional decode: skip when there are no percent-encoded chars
|
|
84
|
+
const decoded = seg.includes('%') ? decodeURIComponent(seg) : seg;
|
|
85
|
+
const paramName = root.paramChild.methodNames.get(method) ??
|
|
86
|
+
root.paramChild.methodNames.values().next().value ??
|
|
87
|
+
'id';
|
|
88
|
+
// Lazily allocate params object (only when first param encountered).
|
|
89
|
+
// Mutate in place and restore on backtrack.
|
|
90
|
+
const actualParams = params ?? {};
|
|
91
|
+
const prev = actualParams[paramName];
|
|
92
|
+
actualParams[paramName] = decoded;
|
|
93
|
+
const result = matchRoute(root.paramChild.node, method, segments, index + 1, actualParams);
|
|
94
|
+
if (!result.found) {
|
|
95
|
+
if (prev === undefined)
|
|
96
|
+
delete actualParams[paramName];
|
|
97
|
+
else
|
|
98
|
+
actualParams[paramName] = prev;
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
return { found: false };
|
|
103
|
+
}
|
|
104
|
+
function node_hasAnyHandler(node) {
|
|
105
|
+
return node.handlers.size > 0;
|
|
106
|
+
}
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// compileRouter
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
/** Builds a Router from route definitions. Throws on duplicate routes. */
|
|
111
|
+
export function compileRouter(routes) {
|
|
112
|
+
const root = newNode();
|
|
113
|
+
for (const route of routes) {
|
|
114
|
+
insertRoute(root, route.method.toUpperCase(), route.path, route.capability);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
routes,
|
|
118
|
+
// Callers must pass method in uppercase — transport guarantees this via req.method.
|
|
119
|
+
match(method, rawPath) {
|
|
120
|
+
// Strip query string without creating an intermediate array.
|
|
121
|
+
const qIdx = rawPath.indexOf('?');
|
|
122
|
+
const pathOnly = qIdx !== -1 ? rawPath.slice(0, qIdx) : rawPath;
|
|
123
|
+
const segments = splitPath(pathOnly);
|
|
124
|
+
// Start with null params; allocate only if a route param is matched.
|
|
125
|
+
return matchRoute(root, method, segments, 0, null);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// URL case conversion
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
function toKebabCase(s) {
|
|
133
|
+
return s.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
134
|
+
}
|
|
135
|
+
function toSnakeCase(s) {
|
|
136
|
+
return s.replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
|
|
137
|
+
}
|
|
138
|
+
function applyUrlCase(s, urlCase) {
|
|
139
|
+
switch (urlCase) {
|
|
140
|
+
case 'kebab': return toKebabCase(s);
|
|
141
|
+
case 'snake': return toSnakeCase(s);
|
|
142
|
+
case 'camel': return s;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
// generateRoutes
|
|
147
|
+
// ---------------------------------------------------------------------------
|
|
148
|
+
/**
|
|
149
|
+
* Generates route definitions from a compiled capability registry.
|
|
150
|
+
* Transport-level overrides take full precedence over URL inference.
|
|
151
|
+
*/
|
|
152
|
+
export function generateRoutes(registry, options = {}) {
|
|
153
|
+
const urlCase = options.urlCase ?? 'kebab';
|
|
154
|
+
const overrides = options.overrides ?? {};
|
|
155
|
+
const routes = [];
|
|
156
|
+
for (const key of Object.keys(overrides)) {
|
|
157
|
+
if (!registry.has(key)) {
|
|
158
|
+
console.warn(`[capix] REST transport: override for '${key}' does not match any registered capability.`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const [dotPath, cap] of registry) {
|
|
162
|
+
const override = overrides[dotPath];
|
|
163
|
+
if (override !== undefined) {
|
|
164
|
+
routes.push({ method: override.method, path: override.path, capability: dotPath });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
routes.push(...inferRoutes(dotPath, cap, urlCase));
|
|
168
|
+
}
|
|
169
|
+
return routes;
|
|
170
|
+
}
|
|
171
|
+
function inferRoutes(dotPath, cap, urlCase) {
|
|
172
|
+
const segments = dotPath.split('.');
|
|
173
|
+
const key = segments[segments.length - 1] ?? dotPath;
|
|
174
|
+
// Group path = all segments except the last one, with case conversion applied
|
|
175
|
+
const groupSegments = segments.slice(0, -1).map((s) => applyUrlCase(s, urlCase));
|
|
176
|
+
const groupPath = '/' + groupSegments.join('/');
|
|
177
|
+
// Use cap.intent when the user set it explicitly; otherwise infer from key name
|
|
178
|
+
const intent = cap._intentExplicit ? cap.intent : inferIntent(key);
|
|
179
|
+
const hasIdField = cap.inputSchema
|
|
180
|
+
? 'shape' in cap.inputSchema &&
|
|
181
|
+
typeof cap.inputSchema.shape === 'object' &&
|
|
182
|
+
cap.inputSchema.shape !== null &&
|
|
183
|
+
'id' in cap.inputSchema.shape
|
|
184
|
+
: false;
|
|
185
|
+
// Key after case conversion (used when key appears in the URL)
|
|
186
|
+
const urlKey = applyUrlCase(key, urlCase);
|
|
187
|
+
switch (intent) {
|
|
188
|
+
case 'query':
|
|
189
|
+
if (hasIdField) {
|
|
190
|
+
// get* with id → GET /group/:id
|
|
191
|
+
return [{ method: 'GET', path: groupPath + '/:id', capability: dotPath }];
|
|
192
|
+
}
|
|
193
|
+
// list*, find*, fetch*, read*, search*, filter*, all* → always collection → GET /group
|
|
194
|
+
if (/^(list|find|fetch|read|search|filter|all)/i.test(key)) {
|
|
195
|
+
return [{ method: 'GET', path: groupPath || '/', capability: dotPath }];
|
|
196
|
+
}
|
|
197
|
+
{
|
|
198
|
+
// get* without id field: collection only when the remainder matches the parent
|
|
199
|
+
// group name. getMe → GET /group/me, getStats → GET /group/stats,
|
|
200
|
+
// getUsers (in users group) → GET /group.
|
|
201
|
+
const getMatch = /^get([A-Z].*)?/.exec(key);
|
|
202
|
+
if (getMatch) {
|
|
203
|
+
const remainder = getMatch[1] ?? '';
|
|
204
|
+
const lastGroup = groupSegments[groupSegments.length - 1] ?? '';
|
|
205
|
+
const rLow = remainder.toLowerCase();
|
|
206
|
+
const gLow = lastGroup.toLowerCase();
|
|
207
|
+
const matchesGroup = !remainder ||
|
|
208
|
+
rLow === gLow ||
|
|
209
|
+
rLow === gLow.replace(/s$/, '') || // "User" matches "users"
|
|
210
|
+
gLow === rLow.replace(/s$/, ''); // "Users" matches "user"
|
|
211
|
+
if (matchesGroup) {
|
|
212
|
+
return [{ method: 'GET', path: groupPath || '/', capability: dotPath }];
|
|
213
|
+
}
|
|
214
|
+
// Named: getMe → /group/me, getStats → /group/stats
|
|
215
|
+
const namedKey = applyUrlCase(remainder || key, urlCase);
|
|
216
|
+
const namedPath = groupSegments.length > 0
|
|
217
|
+
? '/' + [...groupSegments, namedKey].join('/')
|
|
218
|
+
: remainder
|
|
219
|
+
? '/' + namedKey
|
|
220
|
+
: '/' + applyUrlCase(key, urlCase);
|
|
221
|
+
return [{ method: 'GET', path: namedPath, capability: dotPath }];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Named query (me, status, health, etc.) — no collection prefix → GET /group/key
|
|
225
|
+
{
|
|
226
|
+
const namedPath = groupSegments.length > 0 ? '/' + [...groupSegments, urlKey].join('/') : '/' + urlKey;
|
|
227
|
+
return [{ method: 'GET', path: namedPath, capability: dotPath }];
|
|
228
|
+
}
|
|
229
|
+
case 'mutation': {
|
|
230
|
+
// create* → POST /group (drop capability key from path)
|
|
231
|
+
// 'register' intentionally excluded — it's a named action (POST /auth/register), not a resource creation
|
|
232
|
+
const isCreate = /^(create|add|new)/i.test(key);
|
|
233
|
+
if (isCreate) {
|
|
234
|
+
return [{ method: 'POST', path: groupPath || '/', capability: dotPath }];
|
|
235
|
+
}
|
|
236
|
+
// un* → DELETE /group/:id/verb (inverse sub-resource action)
|
|
237
|
+
const unMatch = /^un([A-Za-z].*)/.exec(key);
|
|
238
|
+
if (unMatch) {
|
|
239
|
+
const verb = applyUrlCase(unMatch[1], urlCase);
|
|
240
|
+
const basePath = groupSegments.length > 0 ? groupPath : '';
|
|
241
|
+
return [{ method: 'DELETE', path: basePath + '/:id/' + verb, capability: dotPath }];
|
|
242
|
+
}
|
|
243
|
+
// Named action → POST /group/key
|
|
244
|
+
const actionPath = groupSegments.length > 0 ? '/' + [...groupSegments, urlKey].join('/') : '/' + urlKey;
|
|
245
|
+
return [{ method: 'POST', path: actionPath, capability: dotPath }];
|
|
246
|
+
}
|
|
247
|
+
case 'update':
|
|
248
|
+
return [{ method: 'PATCH', path: groupPath + '/:id', capability: dotPath }];
|
|
249
|
+
case 'replace':
|
|
250
|
+
return [{ method: 'PUT', path: groupPath + '/:id', capability: dotPath }];
|
|
251
|
+
case 'delete':
|
|
252
|
+
return [{ method: 'DELETE', path: groupPath + '/:id', capability: dotPath }];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AA8C5C,SAAS,OAAO;IACd,OAAO,EAAE,QAAQ,EAAE,IAAI,GAAG,EAAE,EAAE,cAAc,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC;AAC5D,CAAC;AAED,4FAA4F;AAC5F,SAAS,SAAS,CAAC,IAAY;IAC7B,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,KAAK,GAAG,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB;IAChE,KAAK,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAC1C,IAAI,CAAC,KAAK,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,WAAW;YAC/D,IAAI,CAAC,GAAG,KAAK;gBAAE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;YACnD,KAAK,GAAG,CAAC,GAAG,CAAC,CAAC;QAChB,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,WAAW,CAAC,IAAe,EAAE,MAAc,EAAE,IAAY,EAAE,UAAkB;IACpF,MAAM,QAAQ,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;IACjC,qFAAqF;IACrF,MAAM,WAAW,GAAG,MAAM,CAAC;IAC3B,IAAI,IAAI,GAAG,IAAI,CAAC;IAEhB,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;QAC3B,IAAI,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACxB,MAAM,SAAS,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC/B,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,CAAC;gBACrB,IAAI,CAAC,UAAU,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,WAAW,EAAE,IAAI,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;YAC1F,CAAC;iBAAM,CAAC;gBACN,4FAA4F;gBAC5F,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;gBAC9D,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;oBACrD,MAAM,IAAI,KAAK,CACb,oDAAoD,WAAW,iBAAiB;wBAC9E,MAAM,QAAQ,UAAU,SAAS,sBAAsB,UAAU,GAAG,CACvE,CAAC;gBACJ,CAAC;gBACD,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,GAAG,CAAC,WAAW,EAAE,SAAS,CAAC,CAAC;YAC1D,CAAC;YACD,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;QAC9B,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBAClC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;YAC1C,CAAC;YACD,IAAI,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAE,CAAC;QACvC,CAAC;IACH,CAAC;IAED,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACb,4BAA4B,WAAW,IAAI,IAAI,iBAAiB,UAAU,GAAG,CAC9E,CAAC;IACJ,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;AAC7C,CAAC;AAED,SAAS,UAAU,CACjB,IAAe,EACf,MAAc,EACd,QAAkB,EAClB,KAAa,EACb,MAAqC;IAErC,IAAI,KAAK,KAAK,QAAQ,CAAC,MAAM,EAAE,CAAC;QAC9B,IAAI,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC;YAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YACtC,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;gBACtB,uEAAuE;gBACvE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC;YAClD,CAAC;YACD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;QACrE,CAAC;QACD,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAC1B,CAAC;IAED,MAAM,GAAG,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC;IAC5B,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IAE/C,0CAA0C;IAC1C,MAAM,WAAW,GAAG,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACjD,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;QAC9B,MAAM,MAAM,GAAG,UAAU,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC;QAC5E,IAAI,MAAM,CAAC,KAAK;YAAE,OAAO,MAAM,CAAC;QAChC,4DAA4D;QAC5D,IAAI,CAAC,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,cAAc,KAAK,SAAS;YAAE,OAAO,MAAM,CAAC;IAC1E,CAAC;IAED,8DAA8D;IAC9D,IAAI,IAAI,CAAC,UAAU,KAAK,SAAS,EAAE,CAAC;QAClC,mEAAmE;QACnE,MAAM,OAAO,GAAG,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,kBAAkB,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;QAClE,MAAM,SAAS,GACb,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,GAAG,CAAC,MAAM,CAAC;YACvC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK;YACjD,IAAI,CAAC;QACP,qEAAqE;QACrE,4CAA4C;QAC5C,MAAM,YAAY,GAAG,MAAM,IAAI,EAAE,CAAC;QAClC,MAAM,IAAI,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC;QACrC,YAAY,CAAC,SAAS,CAAC,GAAG,OAAO,CAAC;QAClC,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,GAAG,CAAC,EAAE,YAAY,CAAC,CAAC;QAC3F,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YAClB,IAAI,IAAI,KAAK,SAAS;gBAAE,OAAO,YAAY,CAAC,SAAS,CAAC,CAAC;;gBAClD,YAAY,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC;QACtC,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;AAC1B,CAAC;AAED,SAAS,kBAAkB,CAAC,IAAe;IACzC,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,GAAG,CAAC,CAAC;AAChC,CAAC;AAED,8EAA8E;AAC9E,gBAAgB;AAChB,8EAA8E;AAE9E,0EAA0E;AAC1E,MAAM,UAAU,aAAa,CAAC,MAAyB;IACrD,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IAEvB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,WAAW,EAAE,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;IAC9E,CAAC;IAED,OAAO;QACL,MAAM;QACN,oFAAoF;QACpF,KAAK,CAAC,MAAc,EAAE,OAAe;YACnC,6DAA6D;YAC7D,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YAClC,MAAM,QAAQ,GAAG,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;YAChE,MAAM,QAAQ,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;YACrC,qEAAqE;YACrE,OAAO,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;QACrD,CAAC;KACF,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,sBAAsB;AACtB,8EAA8E;AAE9E,SAAS,WAAW,CAAC,CAAS;IAC5B,OAAO,CAAC,CAAC,OAAO,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;AAC7D,CAAC;AAED,SAAS,WAAW,CAAC,CAAS;IAC5B,OAAO,CAAC,CAAC,OAAO,CAAC,iBAAiB,EAAE,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;AAC7D,CAAC;AAED,SAAS,YAAY,CAAC,CAAS,EAAE,OAAoC;IACnE,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,OAAO,CAAC,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC;QACpC,KAAK,OAAO,CAAC,CAAC,OAAO,WAAW,CAAC,CAAC,CAAC,CAAC;QACpC,KAAK,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC;IACzB,CAAC;AACH,CAAC;AAED,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E;;;GAGG;AACH,MAAM,UAAU,cAAc,CAC5B,QAA4B,EAC5B,UAAiC,EAAE;IAEnC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC;IAC3C,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,EAAE,CAAC;IAC1C,MAAM,MAAM,GAAsB,EAAE,CAAC;IAErC,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC;QACzC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;YACvB,OAAO,CAAC,IAAI,CAAC,yCAAyC,GAAG,6CAA6C,CAAC,CAAC;QAC1G,CAAC;IACH,CAAC;IAED,KAAK,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,QAAQ,EAAE,CAAC;QACtC,MAAM,QAAQ,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;QACpC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;YACnF,SAAS;QACX,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;IACrD,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,SAAS,WAAW,CAClB,OAAe,EACf,GAAkB,EAClB,OAAoC;IAEpC,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACpC,MAAM,GAAG,GAAG,QAAQ,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,OAAO,CAAC;IAErD,8EAA8E;IAC9E,MAAM,aAAa,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC;IACjF,MAAM,SAAS,GAAG,GAAG,GAAG,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAEhD,gFAAgF;IAChF,MAAM,MAAM,GAAG,GAAG,CAAC,eAAe,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IACnE,MAAM,UAAU,GAAG,GAAG,CAAC,WAAW;QAChC,CAAC,CAAC,OAAO,IAAI,GAAG,CAAC,WAAW;YAC1B,OAAO,GAAG,CAAC,WAAW,CAAC,KAAK,KAAK,QAAQ;YACzC,GAAG,CAAC,WAAW,CAAC,KAAK,KAAK,IAAI;YAC9B,IAAI,IAAK,GAAG,CAAC,WAAW,CAAC,KAAgB;QAC3C,CAAC,CAAC,KAAK,CAAC;IAEV,+DAA+D;IAC/D,MAAM,MAAM,GAAG,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAE1C,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,OAAO;YACV,IAAI,UAAU,EAAE,CAAC;gBACf,gCAAgC;gBAChC,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;YAC5E,CAAC;YACD,uFAAuF;YACvF,IAAI,4CAA4C,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC3D,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,IAAI,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;YAC1E,CAAC;YACD,CAAC;gBACC,+EAA+E;gBAC/E,kEAAkE;gBAClE,0CAA0C;gBAC1C,MAAM,QAAQ,GAAG,gBAAgB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBAC5C,IAAI,QAAQ,EAAE,CAAC;oBACb,MAAM,SAAS,GAAG,QAAQ,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;oBACpC,MAAM,SAAS,GAAG,aAAa,CAAC,aAAa,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;oBAChE,MAAM,IAAI,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC;oBACrC,MAAM,IAAI,GAAG,SAAS,CAAC,WAAW,EAAE,CAAC;oBACrC,MAAM,YAAY,GAChB,CAAC,SAAS;wBACV,IAAI,KAAK,IAAI;wBACb,IAAI,KAAK,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,IAAK,yBAAyB;wBAC7D,IAAI,KAAK,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAK,yBAAyB;oBAEhE,IAAI,YAAY,EAAE,CAAC;wBACjB,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,IAAI,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;oBAC1E,CAAC;oBACD,oDAAoD;oBACpD,MAAM,QAAQ,GAAG,YAAY,CAAC,SAAS,IAAI,GAAG,EAAE,OAAO,CAAC,CAAC;oBACzD,MAAM,SAAS,GACb,aAAa,CAAC,MAAM,GAAG,CAAC;wBACtB,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,aAAa,EAAE,QAAQ,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;wBAC9C,CAAC,CAAC,SAAS;4BACT,CAAC,CAAC,GAAG,GAAG,QAAQ;4BAChB,CAAC,CAAC,GAAG,GAAG,YAAY,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;oBACzC,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;gBACnE,CAAC;YACH,CAAC;YACD,iFAAiF;YACjF,CAAC;gBACC,MAAM,SAAS,GAAG,aAAa,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,aAAa,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,MAAM,CAAC;gBACvG,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;YACnE,CAAC;QAEH,KAAK,UAAU,CAAC,CAAC,CAAC;YAChB,wDAAwD;YACxD,yGAAyG;YACzG,MAAM,QAAQ,GAAG,oBAAoB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAChD,IAAI,QAAQ,EAAE,CAAC;gBACb,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,IAAI,GAAG,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;YAC3E,CAAC;YACD,6DAA6D;YAC7D,MAAM,OAAO,GAAG,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAC5C,IAAI,OAAO,EAAE,CAAC;gBACZ,MAAM,IAAI,GAAG,YAAY,CAAC,OAAO,CAAC,CAAC,CAAE,EAAE,OAAO,CAAC,CAAC;gBAChD,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC3D,OAAO,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;YACtF,CAAC;YACD,iCAAiC;YACjC,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,aAAa,EAAE,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,MAAM,CAAC;YACxG,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;QACrE,CAAC;QAED,KAAK,QAAQ;YACX,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;QAE9E,KAAK,SAAS;YACZ,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;QAE5E,KAAK,QAAQ;YACX,OAAO,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,GAAG,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAC;IACjF,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* serializer.ts — compile fast-json-stringify serializers from Zod output schemas.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the generic `JSON.stringify` call in the response path with a schema-compiled
|
|
5
|
+
* serializer for capabilities that declare an outputSchema. Compiled at mount time; zero
|
|
6
|
+
* per-request setup cost.
|
|
7
|
+
*/
|
|
8
|
+
import type { CapabilityRegistry } from '@capixjs/core';
|
|
9
|
+
/** Pre-compiled response serializer: data → '{"data":<json>}' */
|
|
10
|
+
export type ResponseSerializer = (data: unknown) => string;
|
|
11
|
+
declare const DEFAULT: ResponseSerializer;
|
|
12
|
+
export { DEFAULT as defaultSerializer };
|
|
13
|
+
/**
|
|
14
|
+
* Builds per-capability serializers from compiled output schemas.
|
|
15
|
+
* Falls back to JSON.stringify for capabilities without an outputSchema or
|
|
16
|
+
* if zod-to-json-schema cannot convert the schema.
|
|
17
|
+
*/
|
|
18
|
+
export declare function buildSerializers(registry: CapabilityRegistry): Map<string, ResponseSerializer>;
|
|
19
|
+
//# sourceMappingURL=serializer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAExD,iEAAiE;AACjE,MAAM,MAAM,kBAAkB,GAAG,CAAC,IAAI,EAAE,OAAO,KAAK,MAAM,CAAC;AAE3D,QAAA,MAAM,OAAO,EAAE,kBAAsE,CAAC;AAEtF,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,CAAC;AAExC;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,kBAAkB,GAAG,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,CAwB9F"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* serializer.ts — compile fast-json-stringify serializers from Zod output schemas.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the generic `JSON.stringify` call in the response path with a schema-compiled
|
|
5
|
+
* serializer for capabilities that declare an outputSchema. Compiled at mount time; zero
|
|
6
|
+
* per-request setup cost.
|
|
7
|
+
*/
|
|
8
|
+
import fastJsonStringify from 'fast-json-stringify';
|
|
9
|
+
import { zodToJsonSchema } from 'zod-to-json-schema';
|
|
10
|
+
const DEFAULT = (data) => '{"data":' + JSON.stringify(data) + '}';
|
|
11
|
+
export { DEFAULT as defaultSerializer };
|
|
12
|
+
/**
|
|
13
|
+
* Builds per-capability serializers from compiled output schemas.
|
|
14
|
+
* Falls back to JSON.stringify for capabilities without an outputSchema or
|
|
15
|
+
* if zod-to-json-schema cannot convert the schema.
|
|
16
|
+
*/
|
|
17
|
+
export function buildSerializers(registry) {
|
|
18
|
+
const map = new Map();
|
|
19
|
+
for (const [dotPath, cap] of registry) {
|
|
20
|
+
if (cap.outputSchema === null)
|
|
21
|
+
continue;
|
|
22
|
+
try {
|
|
23
|
+
const jsonSchema = zodToJsonSchema(cap.outputSchema, {
|
|
24
|
+
target: 'jsonSchema7',
|
|
25
|
+
$refStrategy: 'none',
|
|
26
|
+
});
|
|
27
|
+
// Remove the $schema meta field — fjs doesn't need it
|
|
28
|
+
if (typeof jsonSchema === 'object' && jsonSchema !== null) {
|
|
29
|
+
delete jsonSchema['$schema'];
|
|
30
|
+
}
|
|
31
|
+
const serializeData = fastJsonStringify(jsonSchema);
|
|
32
|
+
map.set(dotPath, (data) => '{"data":' + serializeData(data) + '}');
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// Schema type not supported by fjs (e.g. z.union with discriminants); fall back to JSON.stringify.
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return map;
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=serializer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"serializer.js","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,iBAAiB,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AAMrD,MAAM,OAAO,GAAuB,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC;AAEtF,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,CAAC;AAExC;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAAC,QAA4B;IAC3D,MAAM,GAAG,GAAG,IAAI,GAAG,EAA8B,CAAC;IAElD,KAAK,MAAM,CAAC,OAAO,EAAE,GAAG,CAAC,IAAI,QAAQ,EAAE,CAAC;QACtC,IAAI,GAAG,CAAC,YAAY,KAAK,IAAI;YAAE,SAAS;QAExC,IAAI,CAAC;YACH,MAAM,UAAU,GAAG,eAAe,CAAC,GAAG,CAAC,YAAY,EAAE;gBACnD,MAAM,EAAE,aAAa;gBACrB,YAAY,EAAE,MAAM;aACrB,CAAC,CAAC;YACH,sDAAsD;YACtD,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;gBAC1D,OAAQ,UAAsC,CAAC,SAAS,CAAC,CAAC;YAC5D,CAAC;YAED,MAAM,aAAa,GAAG,iBAAiB,CAAC,UAAqD,CAAC,CAAC;YAC/F,GAAG,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,UAAU,GAAG,aAAa,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,CAAC;QACrE,CAAC;QAAC,MAAM,CAAC;YACP,mGAAmG;QACrG,CAAC;IACH,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transport.ts — HTTP/REST transport
|
|
3
|
+
*
|
|
4
|
+
* Uses node:http for HTTP/1.1. node:http2 requires TLS for HTTP/2 browser support
|
|
5
|
+
* (ALPN negotiation); plain h2c is not usable by curl or browsers. For production
|
|
6
|
+
* HTTP/2, wrap with http2.createSecureServer and pass your TLS certificates.
|
|
7
|
+
*
|
|
8
|
+
* Depends on: router.ts, capix core
|
|
9
|
+
*/
|
|
10
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
11
|
+
import type { GroupTree, TransportWithCapabilities } from '@capixjs/core';
|
|
12
|
+
import type { HttpOverride } from './router.js';
|
|
13
|
+
import type { MultipartOptions } from './multipart.js';
|
|
14
|
+
export type RestTransportHooks = {
|
|
15
|
+
/** Called on every response before headers are written. Use to inject additional headers. */
|
|
16
|
+
readonly onRequest?: (req: IncomingMessage, res: ServerResponse) => void;
|
|
17
|
+
};
|
|
18
|
+
export type RestTransportOptions = {
|
|
19
|
+
readonly port: number;
|
|
20
|
+
readonly host?: string;
|
|
21
|
+
readonly cors?: {
|
|
22
|
+
readonly origin?: string | ((origin: string) => boolean);
|
|
23
|
+
readonly methods?: string;
|
|
24
|
+
readonly headers?: string;
|
|
25
|
+
};
|
|
26
|
+
readonly maxBodySize?: number;
|
|
27
|
+
readonly hooks?: RestTransportHooks;
|
|
28
|
+
/** Enable multipart/form-data parsing. Pass true or an options object. */
|
|
29
|
+
readonly multipart?: boolean | MultipartOptions;
|
|
30
|
+
/** Case style for inferred URL segments. Default: 'kebab' (bulkStatus → bulk-status). */
|
|
31
|
+
readonly urlCase?: 'kebab' | 'camel' | 'snake';
|
|
32
|
+
/**
|
|
33
|
+
* Per-request timeout in milliseconds. Default: 30_000 (30 seconds).
|
|
34
|
+
*
|
|
35
|
+
* Set to `false` to disable timeouts entirely. **Only use this in benchmarks
|
|
36
|
+
* or tests — never in production.** A hung capability will hold its connection
|
|
37
|
+
* and resources indefinitely, causing connection exhaustion under load.
|
|
38
|
+
*/
|
|
39
|
+
readonly timeout?: number | false;
|
|
40
|
+
/**
|
|
41
|
+
* HTTP route overrides keyed by capability dot-path.
|
|
42
|
+
* Use for nested resource routes that URL inference cannot produce.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* overrides: {
|
|
46
|
+
* 'tasks.listProjectTasks': { method: 'GET', path: '/v1/projects/:projectId/tasks' },
|
|
47
|
+
* 'tasks.createProjectTask': { method: 'POST', path: '/v1/projects/:projectId/tasks' },
|
|
48
|
+
* }
|
|
49
|
+
*/
|
|
50
|
+
readonly overrides?: Record<string, HttpOverride>;
|
|
51
|
+
/**
|
|
52
|
+
* Capability registry for this transport only.
|
|
53
|
+
* When set, overrides the server-level `capabilities` default for REST routes.
|
|
54
|
+
* Use to prevent non-REST capabilities from getting HTTP endpoints.
|
|
55
|
+
*/
|
|
56
|
+
readonly capabilities?: GroupTree;
|
|
57
|
+
};
|
|
58
|
+
/** Creates a REST transport using node:http. */
|
|
59
|
+
export declare function restTransport(options: RestTransportOptions): TransportWithCapabilities;
|
|
60
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACjE,OAAO,KAAK,EAAyD,SAAS,EAAE,yBAAyB,EAAE,MAAM,eAAe,CAAC;AAEjI,OAAO,KAAK,EAAU,YAAY,EAAE,MAAM,aAAa,CAAC;AAExD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAOvD,MAAM,MAAM,kBAAkB,GAAG;IAC/B,6FAA6F;IAC7F,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,IAAI,CAAC;CAC1E,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE;QACd,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC;QACzD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAC1B,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAC;IACF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,kBAAkB,CAAC;IACpC,0EAA0E;IAC1E,QAAQ,CAAC,SAAS,CAAC,EAAE,OAAO,GAAG,gBAAgB,CAAC;IAChD,yFAAyF;IACzF,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,OAAO,CAAC;IAC/C;;;;;;OAMG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,KAAK,CAAC;IAClC;;;;;;;;;OASG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAClD;;;;OAIG;IACH,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,CAAC;CACnC,CAAC;AAEF,gDAAgD;AAChD,wBAAgB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,yBAAyB,CAsStF"}
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* transport.ts — HTTP/REST transport
|
|
3
|
+
*
|
|
4
|
+
* Uses node:http for HTTP/1.1. node:http2 requires TLS for HTTP/2 browser support
|
|
5
|
+
* (ALPN negotiation); plain h2c is not usable by curl or browsers. For production
|
|
6
|
+
* HTTP/2, wrap with http2.createSecureServer and pass your TLS certificates.
|
|
7
|
+
*
|
|
8
|
+
* Depends on: router.ts, capix core
|
|
9
|
+
*/
|
|
10
|
+
import * as http from 'node:http';
|
|
11
|
+
import { compileRouter, generateRoutes } from './router.js';
|
|
12
|
+
import { parseMultipart } from './multipart-parser.js';
|
|
13
|
+
import { buildSerializers, defaultSerializer } from './serializer.js';
|
|
14
|
+
const DEFAULT_MAX_BODY_SIZE = 1024 * 1024; // 1MB
|
|
15
|
+
const EMPTY_INPUT = {};
|
|
16
|
+
/** Creates a REST transport using node:http. */
|
|
17
|
+
export function restTransport(options) {
|
|
18
|
+
let server = null;
|
|
19
|
+
let router = null;
|
|
20
|
+
let invokeFn = null;
|
|
21
|
+
let serializers = null;
|
|
22
|
+
const corsOriginOpt = options.cors?.origin ?? '*';
|
|
23
|
+
const maxBodySize = options.maxBodySize ?? DEFAULT_MAX_BODY_SIZE;
|
|
24
|
+
const corsMethodsValue = options.cors?.methods ?? 'GET, POST, PATCH, PUT, DELETE, OPTIONS';
|
|
25
|
+
const corsHeadersValue = options.cors?.headers ?? 'Content-Type, Authorization';
|
|
26
|
+
const hasDynamicOrigin = typeof corsOriginOpt === 'function';
|
|
27
|
+
const requestTimeout = options.timeout === undefined ? 30_000 : options.timeout;
|
|
28
|
+
// When timeout: false — share one never-aborted signal per transport instance.
|
|
29
|
+
const _noTimeoutSignal = requestTimeout === false ? new AbortController().signal : null;
|
|
30
|
+
if (requestTimeout === false) {
|
|
31
|
+
console.warn('[capix] WARNING: timeout: false disables request timeouts. ' +
|
|
32
|
+
'Hung capabilities will hold resources indefinitely. ' +
|
|
33
|
+
'Do not use this in production.');
|
|
34
|
+
}
|
|
35
|
+
// Pre-built headers for the fast path (static CORS origin).
|
|
36
|
+
// When origin is a function, fall back to per-request setCorsHeaders.
|
|
37
|
+
let _jsonHeaders200 = null;
|
|
38
|
+
let _jsonHeadersErr = null;
|
|
39
|
+
let _preflightHeaders = null;
|
|
40
|
+
if (!hasDynamicOrigin) {
|
|
41
|
+
const corsBase = {
|
|
42
|
+
'Access-Control-Allow-Methods': corsMethodsValue,
|
|
43
|
+
'Access-Control-Allow-Headers': corsHeadersValue,
|
|
44
|
+
};
|
|
45
|
+
if (corsOriginOpt)
|
|
46
|
+
corsBase['Access-Control-Allow-Origin'] = corsOriginOpt;
|
|
47
|
+
_jsonHeaders200 = { 'Content-Type': 'application/json', ...corsBase };
|
|
48
|
+
_jsonHeadersErr = { 'Content-Type': 'application/json', ...corsBase };
|
|
49
|
+
_preflightHeaders = { ...corsBase };
|
|
50
|
+
}
|
|
51
|
+
function setCorsHeaders(req, res) {
|
|
52
|
+
const reqOrigin = req.headers['origin'] ?? '';
|
|
53
|
+
const allowOrigin = corsOriginOpt(reqOrigin) ? reqOrigin : '';
|
|
54
|
+
if (allowOrigin)
|
|
55
|
+
res.setHeader('Access-Control-Allow-Origin', allowOrigin);
|
|
56
|
+
res.setHeader('Access-Control-Allow-Methods', corsMethodsValue);
|
|
57
|
+
res.setHeader('Access-Control-Allow-Headers', corsHeadersValue);
|
|
58
|
+
}
|
|
59
|
+
async function readBody(req) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const chunks = [];
|
|
62
|
+
let size = 0;
|
|
63
|
+
req.on('data', (chunk) => {
|
|
64
|
+
size += chunk.byteLength;
|
|
65
|
+
if (size > maxBodySize) {
|
|
66
|
+
req.resume(); // drain remaining data without closing socket so we can still respond
|
|
67
|
+
reject(new Error('PAYLOAD_TOO_LARGE'));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
chunks.push(chunk);
|
|
71
|
+
});
|
|
72
|
+
req.on('end', () => resolve(Buffer.concat(chunks)));
|
|
73
|
+
req.on('error', reject);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
function coerceQueryValue(val) {
|
|
77
|
+
if (val === 'true')
|
|
78
|
+
return true;
|
|
79
|
+
if (val === 'false')
|
|
80
|
+
return false;
|
|
81
|
+
if (val !== '' && !isNaN(Number(val)))
|
|
82
|
+
return Number(val);
|
|
83
|
+
return val;
|
|
84
|
+
}
|
|
85
|
+
function parseQueryString(url) {
|
|
86
|
+
const idx = url.indexOf('?');
|
|
87
|
+
if (idx === -1)
|
|
88
|
+
return null;
|
|
89
|
+
const qs = url.slice(idx + 1);
|
|
90
|
+
const result = {};
|
|
91
|
+
for (const part of qs.split('&')) {
|
|
92
|
+
const eqIdx = part.indexOf('=');
|
|
93
|
+
if (eqIdx === -1)
|
|
94
|
+
continue;
|
|
95
|
+
const key = decodeURIComponent(part.slice(0, eqIdx));
|
|
96
|
+
const val = decodeURIComponent(part.slice(eqIdx + 1));
|
|
97
|
+
result[key] = coerceQueryValue(val);
|
|
98
|
+
}
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
function sendJson(res, status, body) {
|
|
102
|
+
const json = JSON.stringify(body);
|
|
103
|
+
res.writeHead(status, _jsonHeadersErr ?? { 'Content-Type': 'application/json' });
|
|
104
|
+
res.end(json);
|
|
105
|
+
}
|
|
106
|
+
function handler(req, res) {
|
|
107
|
+
// Fast path: pre-built static headers avoid per-request setHeader calls.
|
|
108
|
+
// Dynamic origin functions still use setCorsHeaders.
|
|
109
|
+
if (hasDynamicOrigin)
|
|
110
|
+
setCorsHeaders(req, res);
|
|
111
|
+
options.hooks?.onRequest?.(req, res);
|
|
112
|
+
// CORS preflight
|
|
113
|
+
if (req.method === 'OPTIONS') {
|
|
114
|
+
if (_preflightHeaders) {
|
|
115
|
+
res.writeHead(204, _preflightHeaders);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
res.writeHead(204);
|
|
119
|
+
}
|
|
120
|
+
res.end();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (!router || !invokeFn) {
|
|
124
|
+
sendJson(res, 503, { error: 'ServiceUnavailable', message: 'Server not ready' });
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const url = req.url ?? '/';
|
|
128
|
+
// Node.js provides uppercase methods per RFC 7230; explicit toUpperCase() documents the contract.
|
|
129
|
+
const method = (req.method ?? 'GET').toUpperCase();
|
|
130
|
+
const match = router.match(method, url);
|
|
131
|
+
if (!match.found) {
|
|
132
|
+
if (match.allowedMethods !== undefined) {
|
|
133
|
+
res.setHeader('Allow', match.allowedMethods.join(', '));
|
|
134
|
+
sendJson(res, 405, { error: 'MethodNotAllowed', message: 'Method not allowed' });
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
sendJson(res, 404, { error: 'NotFound', message: 'Not found' });
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const queryParams = parseQueryString(url);
|
|
142
|
+
const pathParams = match.params;
|
|
143
|
+
const noBody = method === 'GET' || method === 'HEAD' || method === 'DELETE';
|
|
144
|
+
// Async continuation to keep handler signature synchronous (avoids unhandled rejection)
|
|
145
|
+
const finish = async () => {
|
|
146
|
+
let bodyParams = {};
|
|
147
|
+
let rawBodyForContext;
|
|
148
|
+
if (!noBody) {
|
|
149
|
+
let rawBody;
|
|
150
|
+
try {
|
|
151
|
+
rawBody = await readBody(req);
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
if (err instanceof Error && err.message === 'PAYLOAD_TOO_LARGE') {
|
|
155
|
+
sendJson(res, 413, { error: 'PayloadTooLarge', message: 'Request body exceeds size limit' });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
sendJson(res, 400, { error: 'BadRequest', message: 'Failed to read request body' });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (rawBody.length > 0) {
|
|
162
|
+
rawBodyForContext = rawBody;
|
|
163
|
+
const contentType = req.headers['content-type'] ?? '';
|
|
164
|
+
if (contentType.includes('multipart/form-data') && options.multipart) {
|
|
165
|
+
const multipartOpts = typeof options.multipart === 'object' ? options.multipart : {};
|
|
166
|
+
try {
|
|
167
|
+
const parsed = await parseMultipart(req.headers, rawBody, multipartOpts);
|
|
168
|
+
for (const [k, v] of Object.entries(parsed.fields))
|
|
169
|
+
bodyParams[k] = coerceQueryValue(v);
|
|
170
|
+
for (const [k, f] of Object.entries(parsed.files))
|
|
171
|
+
bodyParams[k] = f;
|
|
172
|
+
}
|
|
173
|
+
catch (err) {
|
|
174
|
+
const status = err.status ?? 400;
|
|
175
|
+
const message = err instanceof Error ? err.message : 'Failed to parse multipart body';
|
|
176
|
+
sendJson(res, status, { error: status === 413 ? 'PayloadTooLarge' : 'BadRequest', message });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else if (contentType.includes('application/json')) {
|
|
181
|
+
try {
|
|
182
|
+
bodyParams = JSON.parse(rawBody.toString('utf8'));
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
sendJson(res, 400, { error: 'BadRequest', message: 'Invalid JSON body' });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Merge: query params < body params < path params (path wins).
|
|
192
|
+
// Router returns null for params when no path params exist (avoids allocation).
|
|
193
|
+
// For the common case (GET with no query and no path params), reuse EMPTY_INPUT.
|
|
194
|
+
let input;
|
|
195
|
+
if (pathParams === null && noBody && queryParams === null) {
|
|
196
|
+
input = EMPTY_INPUT;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
input = {};
|
|
200
|
+
if (queryParams !== null)
|
|
201
|
+
Object.assign(input, queryParams);
|
|
202
|
+
Object.assign(input, bodyParams);
|
|
203
|
+
if (pathParams !== null)
|
|
204
|
+
Object.assign(input, pathParams);
|
|
205
|
+
}
|
|
206
|
+
// Build flat headers map using for..in (avoids Object.entries array allocation).
|
|
207
|
+
const headers = {};
|
|
208
|
+
for (const k in req.headers) {
|
|
209
|
+
const v = req.headers[k];
|
|
210
|
+
if (v !== undefined)
|
|
211
|
+
headers[k] = Array.isArray(v) ? v.join(', ') : v;
|
|
212
|
+
}
|
|
213
|
+
const signal = _noTimeoutSignal ?? AbortSignal.timeout(requestTimeout);
|
|
214
|
+
const invocation = invokeFn({
|
|
215
|
+
capability: match.capability,
|
|
216
|
+
input,
|
|
217
|
+
headers,
|
|
218
|
+
signal,
|
|
219
|
+
...(rawBodyForContext !== undefined ? { rawBody: rawBodyForContext } : {}),
|
|
220
|
+
});
|
|
221
|
+
// Race invocation against the abort signal so hung capabilities don't hold resources.
|
|
222
|
+
// When timeout: false the signal never fires, so we await directly.
|
|
223
|
+
const response = requestTimeout === false
|
|
224
|
+
? await invocation
|
|
225
|
+
: await Promise.race([
|
|
226
|
+
invocation,
|
|
227
|
+
new Promise((resolve) => {
|
|
228
|
+
signal.addEventListener('abort', () => resolve({
|
|
229
|
+
ok: false,
|
|
230
|
+
error: { status: 504, error: 'GatewayTimeout', message: 'Request timed out' },
|
|
231
|
+
}), { once: true });
|
|
232
|
+
}),
|
|
233
|
+
]);
|
|
234
|
+
if (response.ok) {
|
|
235
|
+
if (response.data === null || response.data === undefined) {
|
|
236
|
+
res.writeHead(204, _preflightHeaders ?? undefined);
|
|
237
|
+
res.end();
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
const serialize = serializers?.get(match.capability) ?? defaultSerializer;
|
|
241
|
+
const json = serialize(response.data);
|
|
242
|
+
res.writeHead(200, _jsonHeaders200 ?? { 'Content-Type': 'application/json' });
|
|
243
|
+
res.end(json);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
const { status, error, message, meta } = response.error;
|
|
248
|
+
const body = { error, message };
|
|
249
|
+
if (meta !== undefined)
|
|
250
|
+
body['meta'] = meta;
|
|
251
|
+
// Add Retry-After header for rate limit responses per RFC 6585
|
|
252
|
+
if (status === 429 && typeof meta?.retryAfter === 'number') {
|
|
253
|
+
res.setHeader('Retry-After', String(meta.retryAfter));
|
|
254
|
+
}
|
|
255
|
+
sendJson(res, status, body);
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
finish().catch((err) => {
|
|
259
|
+
console.error('[capix:rest] Unhandled error in handler:', err);
|
|
260
|
+
if (!res.headersSent) {
|
|
261
|
+
sendJson(res, 500, { error: 'Internal', message: 'Internal server error' });
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
...(options.capabilities !== undefined ? { _capabilities: options.capabilities } : {}),
|
|
267
|
+
async mount(invoke, mountOptions) {
|
|
268
|
+
invokeFn = invoke;
|
|
269
|
+
const routeOpts = {
|
|
270
|
+
...(options.urlCase !== undefined ? { urlCase: options.urlCase } : {}),
|
|
271
|
+
...(options.overrides !== undefined ? { overrides: options.overrides } : {}),
|
|
272
|
+
};
|
|
273
|
+
const routes = generateRoutes(mountOptions.registry, routeOpts);
|
|
274
|
+
router = compileRouter(routes);
|
|
275
|
+
serializers = buildSerializers(mountOptions.registry);
|
|
276
|
+
console.log('\nCapix REST transport starting...');
|
|
277
|
+
for (const route of routes) {
|
|
278
|
+
console.log(` ✓ ${route.method.padEnd(7)} ${route.path}`);
|
|
279
|
+
}
|
|
280
|
+
return new Promise((resolve, reject) => {
|
|
281
|
+
server = http.createServer(handler);
|
|
282
|
+
server.on('error', reject);
|
|
283
|
+
server.listen(options.port, options.host ?? '0.0.0.0', () => {
|
|
284
|
+
console.log(`\n Listening on http://localhost:${options.port}\n`);
|
|
285
|
+
resolve();
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
async unmount() {
|
|
290
|
+
return new Promise((resolve, reject) => {
|
|
291
|
+
if (!server)
|
|
292
|
+
return resolve();
|
|
293
|
+
server.close((err) => {
|
|
294
|
+
if (err)
|
|
295
|
+
reject(err);
|
|
296
|
+
else
|
|
297
|
+
resolve();
|
|
298
|
+
});
|
|
299
|
+
});
|
|
300
|
+
},
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@capixjs/transport-rest",
|
|
3
|
+
"version": "0.1.0-alpha.1",
|
|
4
|
+
"description": "Capix HTTP/REST transport",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"!dist/**/*.test.*",
|
|
18
|
+
"LICENSE"
|
|
19
|
+
],
|
|
20
|
+
"keywords": [
|
|
21
|
+
"capix",
|
|
22
|
+
"rest",
|
|
23
|
+
"http",
|
|
24
|
+
"transport",
|
|
25
|
+
"router",
|
|
26
|
+
"multipart",
|
|
27
|
+
"file-upload",
|
|
28
|
+
"typescript"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"typecheck": "tsc --noEmit"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"busboy": "^1.6.0",
|
|
40
|
+
"fast-json-stringify": "^6.4.0",
|
|
41
|
+
"zod": "^3.23.0",
|
|
42
|
+
"zod-to-json-schema": "^3.25.2"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@capixjs/core": ">=0.1.0-0"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/busboy": "^1.5.0",
|
|
49
|
+
"@types/form-data": "^2.5.2",
|
|
50
|
+
"@types/node": "^20.0.0",
|
|
51
|
+
"form-data": "^4.0.5",
|
|
52
|
+
"typescript": "^5.5.0",
|
|
53
|
+
"vitest": "^1.6.0",
|
|
54
|
+
"@capixjs/core": "workspace:*"
|
|
55
|
+
}
|
|
56
|
+
}
|