xpref 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "xpref",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -1,2 +1,2 @@
1
1
  import type { RequestForwarder, Response, Request, NextFunction } from '../types';
2
- export default function requestFowarder(props: RequestForwarder): (req: Request, res: Response, next: NextFunction) => import("node:http").ClientRequest;
2
+ export default function requestFowarder(props: RequestForwarder): (req: Request, res: Response, next: NextFunction) => import("http").ClientRequest;
@@ -1,4 +1,7 @@
1
1
  import type { ProxyResult, Request, Response } from '../types';
2
2
  import type { IncomingMessage } from 'http';
3
+ type ProxyBody = string | Buffer | Record<string, unknown>;
4
+ export declare const parseProxyBody: (body: Buffer, headers: IncomingMessage['headers']) => ProxyBody;
3
5
  export declare const applyProxyResultToRequest: (req: Request, res: Response, result: ProxyResult) => void;
4
6
  export declare const collectProxyResult: (proxyResponse: IncomingMessage) => Promise<ProxyResult>;
7
+ export {};
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.collectProxyResult = exports.applyProxyResultToRequest = void 0;
3
+ exports.collectProxyResult = exports.applyProxyResultToRequest = exports.parseProxyBody = void 0;
4
4
  const normalizeProxyHeaders = (headers) => (Object.entries(headers).reduce((acc, [key, value]) => {
5
5
  if (value === undefined)
6
6
  return acc;
@@ -22,9 +22,10 @@ const parseProxyBody = (body, headers) => {
22
22
  return body.toString('utf8');
23
23
  }
24
24
  };
25
+ exports.parseProxyBody = parseProxyBody;
25
26
  const applyProxyResultToRequest = (req, res, result) => {
26
27
  const upstreamHeaders = normalizeProxyHeaders(result.headers);
27
- req.body = parseProxyBody(result.body, result.headers);
28
+ req.body = (0, exports.parseProxyBody)(result.body, result.headers);
28
29
  req.headers = { ...req.headers, ...upstreamHeaders };
29
30
  res.statusCode = result.statusCode;
30
31
  Object.entries(upstreamHeaders).forEach(([key, value]) => {
@@ -5,4 +5,4 @@
5
5
  * with it's prototol (http or https).
6
6
  */
7
7
  import type { Request, Response, RequestForwarder } from '../types';
8
- export default function requestFowarder(props: RequestForwarder): (req: Request, res: Response) => import("node:http").ClientRequest;
8
+ export default function requestFowarder(props: RequestForwarder): (req: Request, res: Response) => import("http").ClientRequest;
package/seo/index.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { default as seoMiddleware } from './seo.middleware';
2
+ export type { SeoProps } from './type';
package/seo/index.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.seoMiddleware = void 0;
7
+ var seo_middleware_1 = require("./seo.middleware");
8
+ Object.defineProperty(exports, "seoMiddleware", { enumerable: true, get: function () { return __importDefault(seo_middleware_1).default; } });
@@ -0,0 +1,9 @@
1
+ import type { Request, Response, NextFunction } from '../types';
2
+ import type { SeoProps } from './type';
3
+ type SeoProxyProps = {
4
+ siteName: string;
5
+ templatePath?: string;
6
+ };
7
+ type CallbackData = (data: any, req?: Request) => SeoProps | Promise<SeoProps>;
8
+ export default function prepareTemplate(props: SeoProxyProps, onData: CallbackData): (req: Request, res: Response, next: NextFunction) => void | Promise<void | Response<any, Record<string, any>>> | Response<any, Record<string, any>>;
9
+ export {};
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.default = prepareTemplate;
7
+ const path_1 = __importDefault(require("path"));
8
+ const readResPayload = (req) => {
9
+ const payload = req.body;
10
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
11
+ throw new Error('Invalid Application Request');
12
+ }
13
+ return payload;
14
+ };
15
+ const isValidCrawler = (req) => {
16
+ const strBool = String(req.headers['is-certified-crawler'] || '');
17
+ return strBool.trim() === 'true';
18
+ };
19
+ const getTemplatePath = (props) => {
20
+ const templatePath = props.templatePath || '';
21
+ if (templatePath !== '')
22
+ return templatePath;
23
+ return path_1.default.join(__dirname, 'seo.template.ejs');
24
+ };
25
+ const getSeoProps = (req, dataHandler) => {
26
+ const data = readResPayload(req);
27
+ if (!(data || false))
28
+ throw new Error('Invalid Response.');
29
+ const result = dataHandler(data, req);
30
+ if (result instanceof Promise)
31
+ return result.then((value) => value);
32
+ return Promise.resolve(result);
33
+ };
34
+ function prepareTemplate(props, onData) {
35
+ return (req, res, next) => {
36
+ var _a;
37
+ if (req.method === 'OPTIONS')
38
+ return next();
39
+ if (req.method !== 'GET')
40
+ return next();
41
+ if (!isValidCrawler(req))
42
+ return next();
43
+ const statusCode = Number(res.statusCode || 200);
44
+ if (statusCode >= 400) {
45
+ const message = ((_a = req.body) === null || _a === void 0 ? void 0 : _a.message)
46
+ || 'Something goes wrong.';
47
+ return res.status(statusCode).json({ message });
48
+ }
49
+ return getSeoProps(req, onData)
50
+ .then((payload) => {
51
+ const templatePath = getTemplatePath(props);
52
+ res.removeHeader('content-type');
53
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
54
+ return res.status(200).render(templatePath, {
55
+ title: payload.title,
56
+ description: payload.description,
57
+ canonicalUrl: payload.canonicalUrl,
58
+ mediaType: payload.mediaType,
59
+ imageUrl: payload.imageUrl,
60
+ siteName: props.siteName,
61
+ redirectUrlJson: JSON.stringify(payload.canonicalUrl),
62
+ });
63
+ })
64
+ .catch((error) => {
65
+ const { message } = error;
66
+ return res.status(400).json({ message });
67
+ });
68
+ };
69
+ }
package/seo/type.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ type OpenGrapType = 'website' | 'article' | 'product' | 'profile' | 'music.song' | 'music.album' | 'music.playlist' | 'music.radio_station' | 'video.movie' | 'video.episode' | 'video.tv_show' | 'video.other';
2
+ export type MovieSubTag = 'video.actor:role' | 'video:director' | 'video:writer' | 'video:duration' | 'video:release_date' | 'video:tag';
3
+ export type MusicSubTag = 'music:duration' | 'music:album' | 'music:album:disc' | 'music:album:track' | 'music:musician';
4
+ export type SeoProps = {
5
+ siteName: string;
6
+ title: string;
7
+ description: string | null;
8
+ imageUrl: string;
9
+ canonicalUrl: string;
10
+ mediaType?: OpenGrapType;
11
+ };
12
+ export {};
package/seo/type.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/README.md DELETED
@@ -1,442 +0,0 @@
1
- # xpref
2
-
3
- Express.js application bootstrap for APIs — nested routing, request validation, OpenAPI docs, idempotency, proxying, and i18n.
4
-
5
- ## Features
6
-
7
- ### Core server
8
- - Express 5 app bootstrap with a single `xpref()` call
9
- - Automatic port fallback when the configured port is in use
10
- - Manual start control via `manuallyStart`
11
- - `onInit` hook (runs before built-in middleware)
12
- - `interceptor` hook (runs after built-in middleware, before routes)
13
- - Startup banner with app name, environment, and port
14
- - TypeScript-first types and exports
15
-
16
- ### Security & request pipeline
17
- - Helmet security headers
18
- - CORS enabled
19
- - Trust proxy enabled
20
- - JSON body parsing (8MB limit)
21
- - URL-encoded body parsing
22
- - Unique request ID (`Request-Id` / `request-id`) on every request and response
23
- - `getRequestId()` helper to read the current request ID
24
-
25
- ### Routing
26
- - Declarative nested route trees (parent path + children)
27
- - Per-route middleware arrays
28
- - HTTP methods: `GET`, `POST`, `PUT`, `DELETE`, `PATCH`
29
- - Handler as a function, array of functions, or `{ action, params, description }`
30
- - Static file serving via `staticRoutes`
31
-
32
- ### Request validation (AJV)
33
- - Shared schemas via the top-level `schemas` option
34
- - Per-method validation for `query`, `path` (`params`), and `body`
35
- - Type coercion, `$data`, and `ajv-errors` support
36
- - Human-readable field error messages (400 responses)
37
-
38
- ### Request logging
39
- - Morgan console access logs (app name, env, request ID, method, status, URL, timing, user-agent)
40
- - Optional external logger integration (e.g. `@core/log-client`)
41
- - Structured log payload: request ID, IP, country, language, device ID, origin, referer, and base64 body for non-GET
42
-
43
- ### OpenAPI / Swagger (`xpref/api-docs`)
44
- - OpenAPI 3.0 document generation from routes and schemas
45
- - Swagger UI middleware (`swagger-ui-express`)
46
- - Tags and external docs metadata
47
- - Bearer (JWT) and API key security schemes
48
-
49
- ### Idempotency (`xpref/idempotency`)
50
- - POST-only idempotency via `idempotency-key` header (UUID v4)
51
- - Optional enforcement per route
52
- - Configurable TTL (default 5 minutes)
53
- - In-progress (`202`), cached success replay, and expired (`410`) responses
54
- - Optional `validateResponse` callback for custom success rules
55
- - JSON and `x-www-form-urlencoded` bodies
56
-
57
- ### Request forwarder / proxy (`xpref/request-forwarder`)
58
- - `forwarder` — proxy HTTP/HTTPS upstream with body forwarding
59
- - `proxy` — stream pipe to upstream
60
- - Custom host, `proxyPrefix`, `withPrefix`, extra headers
61
- - `onUrlConstructed` URL rewrite hook
62
- - `passToNext` — collect upstream result and continue the middleware chain
63
-
64
- ### Internationalization (`xpref/i18n`)
65
- - Locale JSON files loaded at init
66
- - `translate(key, replace?, lang?)` with `{placeholder}` substitution
67
- - Fallback language support (`fallbackLang`, `fallbackLangOnly`)
68
-
69
- ## Usage
70
-
71
- ### Basic server setup
72
-
73
- ```typescript
74
- import xpref from 'xpref';
75
-
76
- xpref({
77
- appName: 'my-app',
78
- appEnv: 'development',
79
- port: 3000,
80
- routes: {
81
- '/api/users': [
82
- 'users',
83
- [],
84
- {
85
- get: (req, res) => {
86
- res.json({ users: [] });
87
- },
88
- },
89
- {},
90
- ],
91
- },
92
- }).then(({ port, app }) => {
93
- console.log(`Server running on port ${port}`);
94
- });
95
- ```
96
-
97
- ### Nested routes and middleware
98
-
99
- ```typescript
100
- xpref({
101
- appName: 'my-app',
102
- appEnv: 'development',
103
- port: 3000,
104
- interceptor: (app) => {
105
- app.use((req, res, next) => {
106
- console.log('Custom middleware');
107
- next();
108
- });
109
- },
110
- routes: {
111
- '/api/users': [
112
- 'users',
113
- [],
114
- {
115
- get: (req, res) => res.json({ users: [] }),
116
- post: (req, res) => res.json({ message: 'User created' }),
117
- },
118
- {
119
- '/:id': [
120
- 'user-by-id',
121
- [authMiddleware],
122
- {
123
- get: (req, res) => res.json({ id: req.params.id }),
124
- put: (req, res) => res.json({ message: 'Updated' }),
125
- delete: (req, res) => res.json({ message: 'Deleted' }),
126
- },
127
- ],
128
- },
129
- ],
130
- },
131
- });
132
- ```
133
-
134
- ### Request validation
135
-
136
- ```typescript
137
- xpref({
138
- appName: 'my-app',
139
- appEnv: 'development',
140
- port: 3000,
141
- schemas: {
142
- email: { type: 'string', format: 'email' },
143
- name: { type: 'string', minLength: 1 },
144
- },
145
- routes: {
146
- '/api/users': [
147
- 'users',
148
- [],
149
- {
150
- post: {
151
- description: 'Create a user',
152
- params: {
153
- body: {
154
- type: 'object',
155
- properties: ['email', 'name'],
156
- required: ['email', 'name'],
157
- },
158
- },
159
- action: (req, res) => {
160
- res.json({ message: 'User created', ...req.body });
161
- },
162
- },
163
- },
164
- ],
165
- },
166
- });
167
- ```
168
-
169
- ### With request logging
170
-
171
- ```typescript
172
- import xpref from 'xpref';
173
- import { createLogger } from '@core/log-client';
174
-
175
- const logger = createLogger({
176
- appName: 'my-app',
177
- appEnv: 'development',
178
- });
179
-
180
- xpref({
181
- appName: 'my-app',
182
- appEnv: 'development',
183
- port: 3000,
184
- logger,
185
- routes: {
186
- '/api/logs': [
187
- 'logs',
188
- [],
189
- {
190
- get: (req, res) => res.json({ logs: [] }),
191
- },
192
- ],
193
- },
194
- });
195
- ```
196
-
197
- ### Static files
198
-
199
- ```typescript
200
- xpref({
201
- appName: 'my-app',
202
- appEnv: 'development',
203
- port: 3000,
204
- staticRoutes: {
205
- '/public': './public',
206
- '/uploads': './uploads',
207
- },
208
- routes: {
209
- '/api/files': [
210
- 'files',
211
- [],
212
- {
213
- get: (req, res) => res.json({ files: [] }),
214
- },
215
- ],
216
- },
217
- });
218
- ```
219
-
220
- ### Manual start
221
-
222
- ```typescript
223
- xpref({
224
- appName: 'my-app',
225
- appEnv: 'development',
226
- port: 3000,
227
- manuallyStart: ({ app, port }) =>
228
- new Promise((resolve) => {
229
- const server = app.listen(port, () => {
230
- resolve({ port, app, server });
231
- });
232
- }),
233
- routes: {
234
- '/api/health': [
235
- 'health',
236
- [],
237
- {
238
- get: (req, res) => res.json({ status: 'ok' }),
239
- },
240
- ],
241
- },
242
- });
243
- ```
244
-
245
- ### Idempotency
246
-
247
- ```typescript
248
- import type { Route } from 'xpref';
249
- import idempotency from 'xpref/idempotency';
250
-
251
- const routes = {
252
- '/wallets': [
253
- 'wallets',
254
- [],
255
- {},
256
- {
257
- '/transfer': [
258
- 'transfer',
259
- [idempotency({ enforced: true, ttl: 300 })],
260
- {
261
- post: walletTransferAction,
262
- },
263
- ],
264
- },
265
- ],
266
- } as Route;
267
- ```
268
-
269
- Clients send `idempotency-key: <uuid-v4>` on POST. Retry on `5xx`, `422`, `429`, and similar failures with exponential backoff.
270
-
271
- ### Request forwarder / proxy
272
-
273
- ```typescript
274
- import { forwarder, proxy } from 'xpref/request-forwarder';
275
-
276
- // As middleware: forward and respond from upstream
277
- app.use('/upstream', forwarder({
278
- host: 'https://api.example.com',
279
- proxyPrefix: '/v1',
280
- headers: { 'x-api-key': 'secret' },
281
- }));
282
-
283
- // Collect upstream result and continue the chain
284
- app.use('/gateway', forwarder({
285
- host: 'https://api.example.com',
286
- passToNext: true,
287
- }), (req, res) => {
288
- // Upstream status/headers/body available via applyProxyResultToRequest
289
- res.json({ ok: true });
290
- });
291
-
292
- // Stream pipe proxy
293
- app.use('/proxy', proxy({
294
- host: 'https://api.example.com',
295
- withPrefix: true,
296
- onUrlConstructed: (url) => url.replace(/\/+$/, ''),
297
- }));
298
- ```
299
-
300
- ### i18n
301
-
302
- ```typescript
303
- import { i18n } from 'xpref';
304
-
305
- const t = i18n({
306
- locale: {
307
- en: './locales/en.json',
308
- km: './locales/km.json',
309
- },
310
- fallbackLang: 'en',
311
- });
312
-
313
- t('welcome.message', { name: 'Ada' }, 'en');
314
- ```
315
-
316
- ### OpenAPI / Swagger UI
317
-
318
- ```typescript
319
- import setupApiDocs from 'xpref/api-docs';
320
-
321
- const [serve, setup] = setupApiDocs(
322
- {
323
- info: {
324
- title: 'My API',
325
- version: '1.0.0',
326
- description: 'API documentation',
327
- },
328
- bearerAuth: true,
329
- apiKeys: ['x-api-key'],
330
- tags: {
331
- users: { description: 'User endpoints' },
332
- },
333
- },
334
- { routes, schemas },
335
- );
336
-
337
- app.use('/docs', serve, setup);
338
- ```
339
-
340
- ## API reference
341
-
342
- ### `xpref(props: Xpref): Promise<{ port: number; app: Application }>`
343
-
344
- | Option | Type | Description |
345
- | --- | --- | --- |
346
- | `appName` | `string` | Application name |
347
- | `appEnv` | `string` | Environment (e.g. `development`, `production`) |
348
- | `port` | `number` | Listen port (default `3000`; auto-increments if in use) |
349
- | `routes` | `Route` | Nested route configuration |
350
- | `schemas` | `Record<string, any>` | Shared AJV schemas for method validation |
351
- | `staticRoutes` | `Record<string, string>` | URL path → filesystem path |
352
- | `logger` | `any` | Optional logger factory used by request logging |
353
- | `onInit` | `(app) => void` | Hook before built-in middleware |
354
- | `interceptor` | `(app) => void` | Hook after built-in middleware |
355
- | `manuallyStart` | `({ app, port }) => Promise` | Skip auto-listen; start the server yourself |
356
-
357
- ### Route configuration
358
-
359
- ```typescript
360
- type MethodOptions =
361
- | CallableFunction
362
- | CallableFunction[]
363
- | {
364
- action: CallableFunction | CallableFunction[];
365
- params?: { query?: any; path?: any; body?: any };
366
- description?: string | [string, string];
367
- };
368
-
369
- type PathDetail = [
370
- string, // route name
371
- any[], // middleware
372
- MethodHandler, // get/post/put/delete/patch
373
- Record<string, PathDetail>?, // children
374
- ];
375
-
376
- type Route = Record<string, PathDetail>;
377
- ```
378
-
379
- ### Exports
380
-
381
- | Export | From | Description |
382
- | --- | --- | --- |
383
- | default `xpref` | `xpref` | Create and start the app |
384
- | `getRequestId` | `xpref` | Read request ID from a request |
385
- | `i18n` | `xpref` | Initialize translations |
386
- | Express types / `urlencoded` | `xpref` | Re-exported for convenience |
387
- | `idempotency` | `xpref/idempotency` | Idempotency middleware |
388
- | `setupApiDocs` | `xpref/api-docs` | OpenAPI + Swagger UI |
389
- | `forwarder`, `proxy` | `xpref/request-forwarder` | Upstream proxy helpers |
390
-
391
- ## Example project structure
392
-
393
- ```
394
- src/
395
- ├── routes/
396
- │ ├── users.ts
397
- │ └── auth.ts
398
- ├── middleware/
399
- │ └── auth.ts
400
- ├── locales/
401
- │ └── en.json
402
- ├── static/
403
- │ └── public/
404
- └── index.ts
405
- ```
406
-
407
- ```typescript
408
- // src/index.ts
409
- import xpref from 'xpref';
410
- import { createLogger } from '@core/log-client';
411
- import userRoutes from './routes/users';
412
- import authRoutes from './routes/auth';
413
- import authMiddleware from './middleware/auth';
414
-
415
- const logger = createLogger({
416
- appName: 'my-api',
417
- appEnv: process.env.NODE_ENV || 'development',
418
- });
419
-
420
- xpref({
421
- appName: 'my-api',
422
- appEnv: process.env.NODE_ENV || 'development',
423
- port: 3000,
424
- staticRoutes: {
425
- '/public': './static/public',
426
- },
427
- interceptor: (app) => {
428
- app.use('/api/protected', authMiddleware);
429
- },
430
- logger,
431
- routes: {
432
- ...userRoutes,
433
- ...authRoutes,
434
- },
435
- }).then(({ port }) => {
436
- console.log(`Server running on port ${port}`);
437
- });
438
- ```
439
-
440
- ## License
441
-
442
- ISC