expediate 0.0.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.npmignore +5 -4
- package/LICENSE +21 -0
- package/README.md +661 -49
- package/dist/apis.d.ts +166 -0
- package/dist/apis.d.ts.map +1 -0
- package/dist/apis.js +250 -0
- package/dist/apis.js.map +1 -0
- package/dist/git.d.ts +74 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +244 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +55 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt-auth.d.ts +280 -0
- package/dist/jwt-auth.d.ts.map +1 -0
- package/dist/jwt-auth.js +575 -0
- package/dist/jwt-auth.js.map +1 -0
- package/dist/misc.d.ts +203 -0
- package/dist/misc.d.ts.map +1 -0
- package/dist/misc.js +549 -0
- package/dist/misc.js.map +1 -0
- package/dist/router.d.ts +224 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +502 -0
- package/dist/router.js.map +1 -0
- package/dist/static.d.ts +164 -0
- package/dist/static.d.ts.map +1 -0
- package/dist/static.js +703 -0
- package/dist/static.js.map +1 -0
- package/package.json +31 -6
- package/.gitignore +0 -14
- package/index.js +0 -266
- package/sample.js +0 -9
- package/static.js +0 -388
package/dist/apis.d.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { RouterRequest, Router } from './router.js';
|
|
2
|
+
/**
|
|
3
|
+
* An API error thrown (or rejected) by a service method.
|
|
4
|
+
*
|
|
5
|
+
* When a service method throws or rejects with an object of this shape, the
|
|
6
|
+
* framework translates it into an HTTP error response automatically:
|
|
7
|
+
* - `httpStatus` → HTTP status code (defaults to `500`).
|
|
8
|
+
* - `data` → JSON-serialised response body (takes precedence over `message`).
|
|
9
|
+
* - `message` → Plain-text response body.
|
|
10
|
+
*/
|
|
11
|
+
export interface ApiError {
|
|
12
|
+
/** HTTP status code to send (e.g. `404`, `503`). Defaults to `500`. */
|
|
13
|
+
httpStatus?: number;
|
|
14
|
+
/** Structured error payload; serialised to JSON when present. */
|
|
15
|
+
data?: unknown;
|
|
16
|
+
/** Human-readable error message used when `data` is absent. */
|
|
17
|
+
message?: string;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A service method handler.
|
|
21
|
+
*
|
|
22
|
+
* Called with `this` bound to the current service instance.
|
|
23
|
+
*
|
|
24
|
+
* @param params - Route parameters extracted from the URL (e.g. `{ uid: '42' }`),
|
|
25
|
+
* merged with URL query-string parameters.
|
|
26
|
+
* @param body - Parsed request body (populated by a body-parsing middleware
|
|
27
|
+
* such as `json()`).
|
|
28
|
+
* @returns The value to send as the JSON response body, a `Promise` of the
|
|
29
|
+
* same, or `undefined` / `null` / any falsy value to send **201 No
|
|
30
|
+
* Content** (useful for mutations that produce no response body).
|
|
31
|
+
*/
|
|
32
|
+
export type ServiceMethod<TInstance = ServiceInstance> = (this: TInstance, params: Record<string, string>, body?: unknown) => unknown | Promise<unknown>;
|
|
33
|
+
/**
|
|
34
|
+
* The runtime state object that backs a service instance.
|
|
35
|
+
*
|
|
36
|
+
* Produced by `service.data()` and extended with the methods from
|
|
37
|
+
* `service.methods` before `service.setup()` is called. A hidden `$key`
|
|
38
|
+
* property carries the scope key when `data()` is not supplied.
|
|
39
|
+
*/
|
|
40
|
+
export type ServiceInstance = Record<string, unknown> & {
|
|
41
|
+
/** The scope key used to identify this instance (set by the framework). */
|
|
42
|
+
$key?: string | null;
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* A named map of method functions to mix into every service instance.
|
|
46
|
+
*
|
|
47
|
+
* Methods declared here are copied onto the instance object, bound to `this`,
|
|
48
|
+
* so they can call each other and read/write instance state naturally.
|
|
49
|
+
*/
|
|
50
|
+
export type ServiceMethods<TInstance extends ServiceInstance = ServiceInstance> = {
|
|
51
|
+
[name: string]: (this: TInstance, ...args: unknown[]) => unknown;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* A route map: keys are Express-style path patterns, values are handler
|
|
55
|
+
* functions that are invoked with `this` bound to the service instance.
|
|
56
|
+
*/
|
|
57
|
+
export type RouteMap<TInstance extends ServiceInstance = ServiceInstance> = {
|
|
58
|
+
[path: string]: ServiceMethod<TInstance>;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* A service definition object — the single argument to {@link apiBuilder}.
|
|
62
|
+
*
|
|
63
|
+
* A service declares its state, helper methods, and HTTP route handlers in
|
|
64
|
+
* one cohesive object. The framework instantiates the service (once globally
|
|
65
|
+
* for a singleton, or per-scope key), mixes in the helper methods, runs
|
|
66
|
+
* `setup()`, then calls the appropriate route handler for each HTTP request.
|
|
67
|
+
*
|
|
68
|
+
* **Scoping:**
|
|
69
|
+
* - When `scope` is **absent** (or not a function), the service is a
|
|
70
|
+
* **singleton**: one shared instance handles all requests.
|
|
71
|
+
* - When `scope` returns a **truthy string**, the same instance is reused
|
|
72
|
+
* for all requests that share that key (e.g. one instance per session).
|
|
73
|
+
* - When `scope` returns **`null`**, a **fresh instance** is created for
|
|
74
|
+
* every request and discarded afterwards (recommended for stateless services).
|
|
75
|
+
*
|
|
76
|
+
* @template TInstance - The shape of the service's state object.
|
|
77
|
+
*/
|
|
78
|
+
export interface ServiceDefinition<TInstance extends ServiceInstance = ServiceInstance> {
|
|
79
|
+
/**
|
|
80
|
+
* Determine the scope key for the current request.
|
|
81
|
+
*
|
|
82
|
+
* - Return a **string** → instances are cached by that key (e.g. session ID).
|
|
83
|
+
* - Return **`null`** → create a new, disposable instance per request.
|
|
84
|
+
* - Omit entirely → the service is a **singleton** (one global instance).
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* scope: (req) => (req as any).session?.ssid ?? null,
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
scope?: (req: RouterRequest) => string | null;
|
|
92
|
+
/**
|
|
93
|
+
* Factory that returns the initial state object for a new instance.
|
|
94
|
+
*
|
|
95
|
+
* When omitted, the instance is initialised as `{ $key: key }`.
|
|
96
|
+
*
|
|
97
|
+
* @param key - The scope key passed by the framework (`'singleton'` for
|
|
98
|
+
* global instances, `null` for ephemeral ones, or the string
|
|
99
|
+
* returned by `scope()`).
|
|
100
|
+
*/
|
|
101
|
+
data?: (key: string | null) => TInstance;
|
|
102
|
+
/**
|
|
103
|
+
* Lifecycle hook called once after an instance is created and its methods
|
|
104
|
+
* are mixed in.
|
|
105
|
+
*
|
|
106
|
+
* May be synchronous or asynchronous. When asynchronous, the returned
|
|
107
|
+
* `Promise` is not awaited by the framework — use a `throwIfNotReady()`
|
|
108
|
+
* pattern in your methods to guard against premature access.
|
|
109
|
+
*/
|
|
110
|
+
setup?: (this: TInstance) => void | Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Helper methods mixed into every service instance.
|
|
113
|
+
*
|
|
114
|
+
* All methods are bound to the instance (`this` = instance), so they can
|
|
115
|
+
* read and write state, call other methods, and throw {@link ApiError}
|
|
116
|
+
* objects to trigger HTTP error responses.
|
|
117
|
+
*/
|
|
118
|
+
methods?: ServiceMethods<TInstance>;
|
|
119
|
+
/** Route handlers for `GET` requests. */
|
|
120
|
+
GET?: RouteMap<TInstance>;
|
|
121
|
+
/** Route handlers for `POST` requests. */
|
|
122
|
+
POST?: RouteMap<TInstance>;
|
|
123
|
+
/** Route handlers for `PUT` requests. */
|
|
124
|
+
PUT?: RouteMap<TInstance>;
|
|
125
|
+
/** Route handlers for `DELETE` requests. */
|
|
126
|
+
DELETE?: RouteMap<TInstance>;
|
|
127
|
+
/** Route handlers for `PATCH` requests. */
|
|
128
|
+
PATCH?: RouteMap<TInstance>;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Build an Express-compatible router from a service definition object.
|
|
132
|
+
*
|
|
133
|
+
* The returned router is suitable for mounting via `app.use()`:
|
|
134
|
+
*
|
|
135
|
+
* ```ts
|
|
136
|
+
* import myService from './my-service.js';
|
|
137
|
+
*
|
|
138
|
+
* app.use('/api', apiBuilder(myService));
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* **Route handlers** declared in `service.GET`, `service.POST`, etc. are
|
|
142
|
+
* called with `this` bound to the service instance. They receive two
|
|
143
|
+
* arguments:
|
|
144
|
+
* 1. `params` — merged route + query-string parameters from `req.params`.
|
|
145
|
+
* 2. `body` — the parsed request body from `req.body` (requires a
|
|
146
|
+
* body-parsing middleware such as `json()` to run first).
|
|
147
|
+
*
|
|
148
|
+
* **Return values:**
|
|
149
|
+
* - A **truthy value** (or a `Promise` resolving to one) → serialised as JSON
|
|
150
|
+
* with status `200 OK`.
|
|
151
|
+
* - A **falsy value** (`undefined`, `null`, `false`, `0`, `''`) or a
|
|
152
|
+
* `Promise` resolving to one → `201 No Content` (useful for mutations).
|
|
153
|
+
*
|
|
154
|
+
* **Error handling:**
|
|
155
|
+
* - Throwing or rejecting with `{ httpStatus, message }` sends the
|
|
156
|
+
* corresponding HTTP error.
|
|
157
|
+
* - Throwing or rejecting with `{ httpStatus, data }` sends the `data` object
|
|
158
|
+
* as a JSON body.
|
|
159
|
+
* - Any other thrown value produces `500 Internal Server Error`.
|
|
160
|
+
*
|
|
161
|
+
* @param service - The service definition (see {@link ServiceDefinition}).
|
|
162
|
+
* @returns A router instance pre-configured with all declared routes.
|
|
163
|
+
*/
|
|
164
|
+
export declare function apiBuilder<TInstance extends ServiceInstance = ServiceInstance>(service: ServiceDefinition<TInstance>): Router;
|
|
165
|
+
export default apiBuilder;
|
|
166
|
+
//# sourceMappingURL=apis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apis.d.ts","sourceRoot":"","sources":["../src/apis.ts"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,aAAa,EAAkB,MAAM,EAAE,MAAM,aAAa,CAAC;AAMzE;;;;;;;;GAQG;AACH,MAAM,WAAW,QAAQ;IACvB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iEAAiE;IACjE,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,+DAA+D;IAC/D,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,aAAa,CAAC,SAAS,GAAG,eAAe,IAAI,CACvD,IAAI,EAAI,SAAS,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC9B,IAAI,CAAC,EAAG,OAAO,KACZ,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAEhC;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG;IACtD,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,cAAc,CAAC,SAAS,SAAS,eAAe,GAAG,eAAe,IAAI;IAChF,CAAC,IAAI,EAAE,MAAM,GAAG,CAAC,IAAI,EAAE,SAAS,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,CAAC;CAClE,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,QAAQ,CAAC,SAAS,SAAS,eAAe,GAAG,eAAe,IAAI;IAC1E,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC;CAC1C,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,iBAAiB,CAAC,SAAS,SAAS,eAAe,GAAG,eAAe;IACpF;;;;;;;;;;;OAWG;IACH,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,MAAM,GAAG,IAAI,CAAC;IAE9C;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,SAAS,CAAC;IAEzC;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAElD;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,cAAc,CAAC,SAAS,CAAC,CAAC;IAEpC,yCAAyC;IACzC,GAAG,CAAC,EAAK,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC7B,0CAA0C;IAC1C,IAAI,CAAC,EAAI,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC7B,yCAAyC;IACzC,GAAG,CAAC,EAAK,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC7B,4CAA4C;IAC5C,MAAM,CAAC,EAAE,QAAQ,CAAC,SAAS,CAAC,CAAC;IAC7B,2CAA2C;IAC3C,KAAK,CAAC,EAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;CAC9B;AAyID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,UAAU,CAAC,SAAS,SAAS,eAAe,GAAG,eAAe,EAC5E,OAAO,EAAE,iBAAiB,CAAC,SAAS,CAAC,GACpC,MAAM,CAmFR;AAED,eAAe,UAAU,CAAC"}
|
package/dist/apis.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/* Copyright 2021 Fabien Bavent
|
|
2
|
+
*
|
|
3
|
+
* Permission is hereby granted, free of charge, to any person obtaining a
|
|
4
|
+
* copy of this software and associated documentation files (the "Software"),
|
|
5
|
+
* to deal in the Software without restriction, including without limitation
|
|
6
|
+
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
7
|
+
* and/or sell copies of the Software, and to permit persons to whom the
|
|
8
|
+
* Software is furnished to do so, subject to the following conditions:
|
|
9
|
+
*
|
|
10
|
+
* The above copyright notice and this permission notice shall be included
|
|
11
|
+
* in all copies or substantial portions of the Software.
|
|
12
|
+
*
|
|
13
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
14
|
+
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
16
|
+
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
18
|
+
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
19
|
+
* DEALINGS IN THE SOFTWARE.
|
|
20
|
+
*/
|
|
21
|
+
'use strict';
|
|
22
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
23
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
24
|
+
};
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.apiBuilder = apiBuilder;
|
|
27
|
+
const router_js_1 = __importDefault(require("./router.js"));
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Internal helpers
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
/**
|
|
32
|
+
* Instantiate a new service module for the given scope `key`.
|
|
33
|
+
*
|
|
34
|
+
* The lifecycle is:
|
|
35
|
+
* 1. **`data(key)`** — create the initial state object (or use `{ $key: key }`
|
|
36
|
+
* when `data` is not defined).
|
|
37
|
+
* 2. **Mix in methods** — each entry in `service.methods` is copied onto the
|
|
38
|
+
* instance as a regular function bound to `this`.
|
|
39
|
+
* 3. **`setup()`** — called once on the new instance; may be async.
|
|
40
|
+
*
|
|
41
|
+
* @param service - The service definition.
|
|
42
|
+
* @param key - The scope key (`'singleton'`, `null`, or a session ID).
|
|
43
|
+
* @returns A fully initialised service instance.
|
|
44
|
+
*/
|
|
45
|
+
function buildModule(service, key) {
|
|
46
|
+
const instance = service.data
|
|
47
|
+
? service.data(key)
|
|
48
|
+
: { $key: key };
|
|
49
|
+
// Mix service methods into the instance, bound to `this = instance`.
|
|
50
|
+
// BUG FIX: the original used arrow functions `() => method.apply(module, arguments)`.
|
|
51
|
+
// Arrow functions do NOT have their own `arguments` object — they inherit it
|
|
52
|
+
// from the enclosing `buildModule` scope (which holds `(service, key)`).
|
|
53
|
+
// Any arguments forwarded to the method were therefore silently dropped.
|
|
54
|
+
// Corrected to a regular function expression that captures its own `arguments`
|
|
55
|
+
// via a rest parameter spread.
|
|
56
|
+
if (service.methods) {
|
|
57
|
+
for (const methodName of Object.keys(service.methods)) {
|
|
58
|
+
const method = service.methods[methodName];
|
|
59
|
+
instance[methodName] = function (...args) {
|
|
60
|
+
return method.apply(instance, args);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (service.setup)
|
|
65
|
+
service.setup.apply(instance, []);
|
|
66
|
+
return instance;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the correct service instance for an incoming request.
|
|
70
|
+
*
|
|
71
|
+
* Instance lifecycle by scope:
|
|
72
|
+
* - **Singleton** (`scope` absent): always returns `modules['singleton']`.
|
|
73
|
+
* - **Keyed** (`scope` returns a string): look up `modules[key]`; create and
|
|
74
|
+
* cache a new instance on first access.
|
|
75
|
+
* - **Ephemeral** (`scope` returns `null`): create a fresh instance every time;
|
|
76
|
+
* never cached.
|
|
77
|
+
*
|
|
78
|
+
* @param service - The service definition.
|
|
79
|
+
* @param modules - The instance cache (mutated when a new keyed instance is created).
|
|
80
|
+
* @param req - The incoming request.
|
|
81
|
+
* @returns The service instance to use for this request.
|
|
82
|
+
*/
|
|
83
|
+
function resolveInstance(service, modules, req) {
|
|
84
|
+
if (typeof service.scope !== 'function') {
|
|
85
|
+
// Singleton — always the same global instance.
|
|
86
|
+
return modules['singleton'];
|
|
87
|
+
}
|
|
88
|
+
// BUG FIX: the original called `service.key(req)` which does not exist on
|
|
89
|
+
// the service definition. The correct method is `service.scope(req)`.
|
|
90
|
+
const key = service.scope(req);
|
|
91
|
+
if (key === null) {
|
|
92
|
+
// Ephemeral — create a fresh, uncached instance for every request.
|
|
93
|
+
return buildModule(service, null);
|
|
94
|
+
}
|
|
95
|
+
// Keyed — retrieve from cache or create and store.
|
|
96
|
+
if (!modules[key])
|
|
97
|
+
modules[key] = buildModule(service, key);
|
|
98
|
+
return modules[key];
|
|
99
|
+
}
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Response helpers
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
/**
|
|
104
|
+
* Send a JSON response with the appropriate `Content-Type` header.
|
|
105
|
+
*
|
|
106
|
+
* @param res - The outgoing response.
|
|
107
|
+
* @param data - Any JSON-serialisable value.
|
|
108
|
+
*/
|
|
109
|
+
function sendJson(res, data) {
|
|
110
|
+
// BUG FIX: the original called `res.send(JSON.stringify(val))` without
|
|
111
|
+
// setting a `Content-Type` header. Clients had no way to detect that the
|
|
112
|
+
// response body was JSON. Corrected by setting the header explicitly.
|
|
113
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
114
|
+
res.send(JSON.stringify(data));
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Translate a caught error (thrown or rejected by a service method) into an
|
|
118
|
+
* HTTP error response.
|
|
119
|
+
*
|
|
120
|
+
* Expected shape: `{ httpStatus?, data?, message? }` (see {@link ApiError}).
|
|
121
|
+
* Any other thrown value is treated as an opaque 500 Internal Server Error.
|
|
122
|
+
*
|
|
123
|
+
* @param res - The outgoing response.
|
|
124
|
+
* @param err - The caught value.
|
|
125
|
+
*/
|
|
126
|
+
function sendError(res, err) {
|
|
127
|
+
const e = err;
|
|
128
|
+
const status = e?.httpStatus ?? 500;
|
|
129
|
+
if (e?.data !== undefined) {
|
|
130
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
131
|
+
res.status(status).send(JSON.stringify(e.data));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
res.status(status).send(e?.message ?? 'Internal error');
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Public API
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
/**
|
|
141
|
+
* Build an Express-compatible router from a service definition object.
|
|
142
|
+
*
|
|
143
|
+
* The returned router is suitable for mounting via `app.use()`:
|
|
144
|
+
*
|
|
145
|
+
* ```ts
|
|
146
|
+
* import myService from './my-service.js';
|
|
147
|
+
*
|
|
148
|
+
* app.use('/api', apiBuilder(myService));
|
|
149
|
+
* ```
|
|
150
|
+
*
|
|
151
|
+
* **Route handlers** declared in `service.GET`, `service.POST`, etc. are
|
|
152
|
+
* called with `this` bound to the service instance. They receive two
|
|
153
|
+
* arguments:
|
|
154
|
+
* 1. `params` — merged route + query-string parameters from `req.params`.
|
|
155
|
+
* 2. `body` — the parsed request body from `req.body` (requires a
|
|
156
|
+
* body-parsing middleware such as `json()` to run first).
|
|
157
|
+
*
|
|
158
|
+
* **Return values:**
|
|
159
|
+
* - A **truthy value** (or a `Promise` resolving to one) → serialised as JSON
|
|
160
|
+
* with status `200 OK`.
|
|
161
|
+
* - A **falsy value** (`undefined`, `null`, `false`, `0`, `''`) or a
|
|
162
|
+
* `Promise` resolving to one → `201 No Content` (useful for mutations).
|
|
163
|
+
*
|
|
164
|
+
* **Error handling:**
|
|
165
|
+
* - Throwing or rejecting with `{ httpStatus, message }` sends the
|
|
166
|
+
* corresponding HTTP error.
|
|
167
|
+
* - Throwing or rejecting with `{ httpStatus, data }` sends the `data` object
|
|
168
|
+
* as a JSON body.
|
|
169
|
+
* - Any other thrown value produces `500 Internal Server Error`.
|
|
170
|
+
*
|
|
171
|
+
* @param service - The service definition (see {@link ServiceDefinition}).
|
|
172
|
+
* @returns A router instance pre-configured with all declared routes.
|
|
173
|
+
*/
|
|
174
|
+
function apiBuilder(service) {
|
|
175
|
+
const api = (0, router_js_1.default)();
|
|
176
|
+
const modules = {};
|
|
177
|
+
// Pre-build the singleton instance eagerly so `setup()` runs at startup.
|
|
178
|
+
if (typeof service.scope !== 'function')
|
|
179
|
+
// BUG FIX: the original called `buildModule(service)` without a key,
|
|
180
|
+
// passing `undefined` to `data()` and leaving `$key` as `undefined`.
|
|
181
|
+
// Corrected to pass `'singleton'` as the canonical key.
|
|
182
|
+
modules['singleton'] = buildModule(service, 'singleton');
|
|
183
|
+
/**
|
|
184
|
+
* Register all route handlers from a route map for a given HTTP method.
|
|
185
|
+
*
|
|
186
|
+
* Each handler:
|
|
187
|
+
* 1. Resolves the correct service instance (singleton / keyed / ephemeral).
|
|
188
|
+
* 2. Invokes the service method with `(params, body)`.
|
|
189
|
+
* 3. Sends the return value as JSON (or 201 if falsy).
|
|
190
|
+
* 4. Catches thrown / rejected {@link ApiError} objects and translates them
|
|
191
|
+
* into the appropriate HTTP error response.
|
|
192
|
+
*
|
|
193
|
+
* @param routeMap - Map of path patterns to service methods (`undefined` = skip).
|
|
194
|
+
* @param register - Registers a handler on the router for the current HTTP method.
|
|
195
|
+
*/
|
|
196
|
+
function buildRoutes(routeMap, register) {
|
|
197
|
+
if (!routeMap)
|
|
198
|
+
return;
|
|
199
|
+
// Sort routes by decreasing specificity so that more precise patterns
|
|
200
|
+
// (more segments, fewer parameters) are registered first in the router.
|
|
201
|
+
// Without this, a plain path like '/items' would match '/items/1' as a
|
|
202
|
+
// prefix and steal requests intended for '/items/:id'.
|
|
203
|
+
// Specificity = (segment count * 100) - (parameter count * 10).
|
|
204
|
+
const sortedPaths = Object.keys(routeMap).sort((a, b) => {
|
|
205
|
+
const score = (p) => {
|
|
206
|
+
const segs = p.split('/').filter(s => s.length > 0);
|
|
207
|
+
return segs.length * 100 - segs.filter(s => s.startsWith(':')).length * 10;
|
|
208
|
+
};
|
|
209
|
+
return score(b) - score(a) || b.localeCompare(a);
|
|
210
|
+
});
|
|
211
|
+
for (const path of sortedPaths) {
|
|
212
|
+
const method = routeMap[path];
|
|
213
|
+
register(path, (req, res) => {
|
|
214
|
+
const params = req.params;
|
|
215
|
+
const body = req.body;
|
|
216
|
+
try {
|
|
217
|
+
const instance = resolveInstance(service, modules, req);
|
|
218
|
+
const ret = method.apply(instance, [params, body]);
|
|
219
|
+
if (ret instanceof Promise) {
|
|
220
|
+
ret
|
|
221
|
+
.then((val) => {
|
|
222
|
+
if (val !== undefined && val !== null && val !== false && val !== 0 && val !== '')
|
|
223
|
+
sendJson(res, val);
|
|
224
|
+
else
|
|
225
|
+
res.status(201).end();
|
|
226
|
+
})
|
|
227
|
+
.catch((err) => sendError(res, err));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
if (ret !== undefined && ret !== null && ret !== false && ret !== 0 && ret !== '')
|
|
231
|
+
sendJson(res, ret);
|
|
232
|
+
else
|
|
233
|
+
res.status(201).end();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
sendError(res, err);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
buildRoutes(service.GET, (path, h) => api.get(path, h));
|
|
243
|
+
buildRoutes(service.POST, (path, h) => api.post(path, h));
|
|
244
|
+
buildRoutes(service.PUT, (path, h) => api.put(path, h));
|
|
245
|
+
buildRoutes(service.DELETE, (path, h) => api.delete(path, h));
|
|
246
|
+
buildRoutes(service.PATCH, (path, h) => api.patch(path, h));
|
|
247
|
+
return api;
|
|
248
|
+
}
|
|
249
|
+
exports.default = apiBuilder;
|
|
250
|
+
//# sourceMappingURL=apis.js.map
|
package/dist/apis.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apis.js","sourceRoot":"","sources":["../src/apis.ts"],"names":[],"mappings":"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"}
|
package/dist/git.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { RouterRequest, RouterResponse } from './router.js';
|
|
2
|
+
/**
|
|
3
|
+
* Configuration options for {@link gitHandler}.
|
|
4
|
+
*/
|
|
5
|
+
export interface GitHandlerOptions {
|
|
6
|
+
/**
|
|
7
|
+
* Resolve the absolute filesystem path of the Git repository for an
|
|
8
|
+
* incoming request.
|
|
9
|
+
*
|
|
10
|
+
* Return a non-empty string to serve that repository, or a falsy value
|
|
11
|
+
* (`''`, `null`, `undefined`) to respond with **404 Repository not found**.
|
|
12
|
+
*
|
|
13
|
+
* This is the only required option; all others have defaults.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* repository: (req) => path.join('/srv/git', req.params.repo + '.git')
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
repository: (req: RouterRequest) => string | null | undefined | false;
|
|
21
|
+
/**
|
|
22
|
+
* Directory that contains the `git-upload-pack` executable, including a
|
|
23
|
+
* trailing path separator (e.g. `'/usr/lib/git-core/'`).
|
|
24
|
+
*
|
|
25
|
+
* Leave empty (default) to locate the binary via the system `PATH`.
|
|
26
|
+
*/
|
|
27
|
+
gitPath?: string;
|
|
28
|
+
/**
|
|
29
|
+
* When `true`, the `--strict` flag is passed to `git-upload-pack`.
|
|
30
|
+
* This causes the process to exit with an error when the resolved path is
|
|
31
|
+
* not a bare Git repository.
|
|
32
|
+
*
|
|
33
|
+
* Defaults to `false` (non-strict / `--no-strict`).
|
|
34
|
+
*/
|
|
35
|
+
strict?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Kill the `git-upload-pack` process if it does not complete within this
|
|
38
|
+
* many **seconds**. When omitted or `0`, no timeout is applied.
|
|
39
|
+
*/
|
|
40
|
+
timeout?: number | string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Middleware factory that exposes a Git repository over the **Git Smart HTTP
|
|
44
|
+
* protocol** (read-only fetch / clone, via `git-upload-pack`).
|
|
45
|
+
*
|
|
46
|
+
* Mount this handler at the root of a repository-scoped path so that the two
|
|
47
|
+
* sub-paths it handles (`/info/refs` and `/git-upload-pack`) are reachable:
|
|
48
|
+
*
|
|
49
|
+
* ```ts
|
|
50
|
+
* app.use('/repos/:repo', gitHandler({
|
|
51
|
+
* repository: (req) => path.join('/srv/git', req.params.repo + '.git'),
|
|
52
|
+
* }));
|
|
53
|
+
* ```
|
|
54
|
+
*
|
|
55
|
+
* **Supported endpoints:**
|
|
56
|
+
*
|
|
57
|
+
* | Method | Path | Purpose |
|
|
58
|
+
* |--------|-------------------|----------------------------------------------|
|
|
59
|
+
* | GET | `/info/refs` | Smart HTTP capability advertisement |
|
|
60
|
+
* | POST | `/git-upload-pack`| Pack-file negotiation and transfer |
|
|
61
|
+
*
|
|
62
|
+
* Only `git-upload-pack` (fetch / clone) is implemented.
|
|
63
|
+
* `git-receive-pack` (push) is intentionally excluded.
|
|
64
|
+
*
|
|
65
|
+
* **Compression:** gzip-compressed POST bodies are transparently decompressed
|
|
66
|
+
* before being piped to `git-upload-pack`.
|
|
67
|
+
*
|
|
68
|
+
* @param opt - Handler configuration (see {@link GitHandlerOptions}).
|
|
69
|
+
* @returns An Express-compatible middleware function `(req, res) => void`.
|
|
70
|
+
* @throws {TypeError} When `opt.repository` is not a function.
|
|
71
|
+
*/
|
|
72
|
+
export declare function gitHandler(opt: GitHandlerOptions): (req: RouterRequest, res: RouterResponse) => void;
|
|
73
|
+
export default gitHandler;
|
|
74
|
+
//# sourceMappingURL=git.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"AAyBA,OAAO,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAMjE;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;;;;;;;;;;OAaG;IACH,UAAU,EAAE,CAAC,GAAG,EAAE,aAAa,KAAK,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,KAAK,CAAC;IAEtE;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,OAAO,CAAC;IAEjB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CAC3B;AA6CD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,iBAAiB,GAAG,CAAC,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,cAAc,KAAK,IAAI,CAwIpG;AAyCD,eAAe,UAAU,CAAC"}
|