@tetrascience-npm/request 0.2.0-beta.106.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -0
- package/client.d.ts +1 -0
- package/client.js +1 -0
- package/dist/cli/generate-client.d.ts +3 -0
- package/dist/cli/generate-client.d.ts.map +1 -0
- package/dist/cli/generate-client.js +208 -0
- package/dist/cli/generate-schemas.d.ts +7 -0
- package/dist/cli/generate-schemas.d.ts.map +1 -0
- package/dist/cli/generate-schemas.js +210 -0
- package/dist/cli/templates.d.ts +27 -0
- package/dist/cli/templates.d.ts.map +1 -0
- package/dist/cli/templates.js +165 -0
- package/dist/client/console-logger.d.ts +10 -0
- package/dist/client/console-logger.d.ts.map +1 -0
- package/dist/client/console-logger.js +42 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +19 -0
- package/dist/client/install-middleware.d.ts +31 -0
- package/dist/client/install-middleware.d.ts.map +1 -0
- package/dist/client/install-middleware.js +117 -0
- package/dist/client/types.d.ts +13 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/server/index.d.ts +5 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +24 -0
- package/dist/server/request-context.d.ts +27 -0
- package/dist/server/request-context.d.ts.map +1 -0
- package/dist/server/request-context.js +42 -0
- package/dist/server/request-middleware.d.ts +14 -0
- package/dist/server/request-middleware.d.ts.map +1 -0
- package/dist/server/request-middleware.js +82 -0
- package/dist/server/types.d.ts +13 -0
- package/dist/server/types.d.ts.map +1 -0
- package/dist/server/types.js +2 -0
- package/dist/shared/client-types.d.ts +63 -0
- package/dist/shared/client-types.d.ts.map +1 -0
- package/dist/shared/client-types.js +7 -0
- package/dist/shared/constants.d.ts +7 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/shared/constants.js +9 -0
- package/dist/shared/generate-request-id.d.ts +10 -0
- package/dist/shared/generate-request-id.d.ts.map +1 -0
- package/dist/shared/generate-request-id.js +21 -0
- package/dist/shared/index.d.ts +8 -0
- package/dist/shared/index.d.ts.map +1 -0
- package/dist/shared/index.js +29 -0
- package/dist/shared/middleware/auth.d.ts +39 -0
- package/dist/shared/middleware/auth.d.ts.map +1 -0
- package/dist/shared/middleware/auth.js +164 -0
- package/dist/shared/middleware/default.d.ts +29 -0
- package/dist/shared/middleware/default.d.ts.map +1 -0
- package/dist/shared/middleware/default.js +67 -0
- package/dist/shared/middleware/index.d.ts +6 -0
- package/dist/shared/middleware/index.d.ts.map +1 -0
- package/dist/shared/middleware/index.js +21 -0
- package/dist/shared/middleware/safe-response.d.ts +22 -0
- package/dist/shared/middleware/safe-response.d.ts.map +1 -0
- package/dist/shared/middleware/safe-response.js +41 -0
- package/dist/shared/middleware/tracing.d.ts +23 -0
- package/dist/shared/middleware/tracing.d.ts.map +1 -0
- package/dist/shared/middleware/tracing.js +67 -0
- package/dist/shared/middleware/utils.d.ts +4 -0
- package/dist/shared/middleware/utils.d.ts.map +1 -0
- package/dist/shared/middleware/utils.js +13 -0
- package/dist/shared/middleware/validation.d.ts +43 -0
- package/dist/shared/middleware/validation.d.ts.map +1 -0
- package/dist/shared/middleware/validation.js +42 -0
- package/dist/shared/sanitize-url.d.ts +3 -0
- package/dist/shared/sanitize-url.d.ts.map +1 -0
- package/dist/shared/sanitize-url.js +12 -0
- package/dist/shared/types.d.ts +10 -0
- package/dist/shared/types.d.ts.map +1 -0
- package/dist/shared/types.js +2 -0
- package/package.json +98 -0
- package/server.d.ts +1 -0
- package/server.js +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# @tetrascience-npm/request <!-- omit in toc -->
|
|
2
|
+
|
|
3
|
+
Request middleware for TetraScience services and data apps. End-to-end tracing, auth, validation, and typed OpenAPI client generation — zero-config where possible.
|
|
4
|
+
|
|
5
|
+
## Table of Contents <!-- omit in toc -->
|
|
6
|
+
|
|
7
|
+
- [Installation](#installation)
|
|
8
|
+
- [Quick start](#quick-start)
|
|
9
|
+
- [Auth](#auth)
|
|
10
|
+
- [Middleware pipeline](#middleware-pipeline)
|
|
11
|
+
- [Client SDK generation](#client-sdk-generation)
|
|
12
|
+
- [Entrypoints](#entrypoints)
|
|
13
|
+
- [API reference](#api-reference)
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
yarn add @tetrascience-npm/request
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
**Server** — reads tracing + auth from headers/cookies, stores in AsyncLocalStorage:
|
|
24
|
+
|
|
25
|
+
```typescript
|
|
26
|
+
import cookieParser from 'cookie-parser'
|
|
27
|
+
import { createRequestMiddleware } from '@tetrascience-npm/request/server'
|
|
28
|
+
|
|
29
|
+
app.use(cookieParser())
|
|
30
|
+
app.use(createRequestMiddleware())
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Browser** — patches `fetch()`, auto-manages request ID + session cookie:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
import { installRequestMiddleware } from '@tetrascience-npm/request/client'
|
|
37
|
+
|
|
38
|
+
installRequestMiddleware()
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Using a generated client** — auth and tracing auto-resolve:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { createDataAppsClient } from '@tetrascience/data-apps-client'
|
|
45
|
+
|
|
46
|
+
// Browser — auth from cookies, tracing from installRequestMiddleware
|
|
47
|
+
const client = createDataAppsClient({ auth: 'direct' })
|
|
48
|
+
|
|
49
|
+
// Server (service-to-service) — reads INTERNAL_API_KEY env var, rest from context
|
|
50
|
+
const client = createDataAppsClient({
|
|
51
|
+
baseUrl: process.env.TDP_API_URL,
|
|
52
|
+
auth: 'internal',
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Fully typed
|
|
56
|
+
const { data } = await client.GET('/v1/dataapps/kv/{appSlug}', {
|
|
57
|
+
params: { path: { appSlug: 'my-app' } },
|
|
58
|
+
})
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Auth
|
|
62
|
+
|
|
63
|
+
Both auth modes auto-resolve values from `RequestContext` (server) or cookies (browser) when not explicitly provided.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// Zero-config shorthands
|
|
67
|
+
auth: 'direct' // user/browser
|
|
68
|
+
auth: 'internal' // service-to-service (reads INTERNAL_API_KEY env var)
|
|
69
|
+
|
|
70
|
+
// Explicit values
|
|
71
|
+
auth: { authToken: jwt, orgSlug: 'my-org' }
|
|
72
|
+
auth: { internalApiKey: env.KEY, orgSlug: 'my-org', authToken: 'tok' }
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Headers injected by each mode:
|
|
76
|
+
|
|
77
|
+
| Mode | Headers |
|
|
78
|
+
|------|---------|
|
|
79
|
+
| Direct | `ts-auth-token`, `x-org-slug` |
|
|
80
|
+
| Internal | `ts-internal-api-key`, `x-org-slug`, `ts-auth-token` |
|
|
81
|
+
|
|
82
|
+
## Middleware pipeline
|
|
83
|
+
|
|
84
|
+
`applyDefaultMiddleware(client, options, schemas)` wires the standard pipeline:
|
|
85
|
+
|
|
86
|
+
| Order | Middleware | Headers / Purpose |
|
|
87
|
+
|-------|-----------|-------------------|
|
|
88
|
+
| 1 | Tracing | `ts-request-id`, `ts-session-id`, `ts-initiating-service-name`, logging |
|
|
89
|
+
| 2 | Auth | `ts-internal-api-key` / `ts-auth-token` / `x-org-slug` |
|
|
90
|
+
| 3 | Validation | Zod request body validation |
|
|
91
|
+
| 4 | Safe response | Wraps non-JSON success responses |
|
|
92
|
+
|
|
93
|
+
On Node servers, request ID, session ID, orgSlug, and authToken auto-propagate from `createRequestMiddleware` context. New middleware added here is picked up by existing clients on dependency update — no regeneration needed.
|
|
94
|
+
|
|
95
|
+
## Client SDK generation
|
|
96
|
+
|
|
97
|
+
Add to `package.json`:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
"serviceClient": {
|
|
101
|
+
"packageName": "@tetrascience/data-apps-client",
|
|
102
|
+
"spec": "openapi/dataapps.yaml",
|
|
103
|
+
"version": "2.0.0"
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Run `generate-service-client` — produces a typed client directory. Multi-spec support:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
"spec": { "internal": "openapi/internal.yaml", "public": "openapi/public.yaml" }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Entrypoints
|
|
114
|
+
|
|
115
|
+
| Entrypoint | Environment | Purpose |
|
|
116
|
+
|-----------|-------------|---------|
|
|
117
|
+
| Root | Any | Middleware, types, constants, `createClient`, `applyDefaultMiddleware` |
|
|
118
|
+
| `/server` | Node only | `createRequestMiddleware`, `runWithRequestContext`, `getRequestId` |
|
|
119
|
+
| `/client` | Browser only | `installRequestMiddleware`, `createConsoleLogger` |
|
|
120
|
+
|
|
121
|
+
Independent — `/server` and `/client` do not re-export the root.
|
|
122
|
+
|
|
123
|
+
## API reference
|
|
124
|
+
|
|
125
|
+
### Root
|
|
126
|
+
|
|
127
|
+
**Middleware:**
|
|
128
|
+
|
|
129
|
+
| Export | Description |
|
|
130
|
+
|--------|-------------|
|
|
131
|
+
| `createClient` | Re-exported from `openapi-fetch` |
|
|
132
|
+
| `applyDefaultMiddleware(client, options, schemas?)` | Standard pipeline with auto-resolve |
|
|
133
|
+
| `createTracingMiddleware(options?)` | `ts-request-id`, `ts-session-id`, `ts-initiating-service-name`, logging |
|
|
134
|
+
| `createInternalAuthMiddleware(auth)` | Service-to-service auth (`'internal'` or explicit) |
|
|
135
|
+
| `createDirectAuthMiddleware(auth)` | User auth (`'direct'` or explicit) |
|
|
136
|
+
| `createRequestValidationMiddleware(opts)` | Zod request body validation |
|
|
137
|
+
| `createSafeResponseMiddleware()` | Wraps non-JSON responses |
|
|
138
|
+
|
|
139
|
+
**Types:**
|
|
140
|
+
|
|
141
|
+
| Export | Description |
|
|
142
|
+
|--------|-------------|
|
|
143
|
+
| `ServiceClientOptions` | `{ baseUrl?, auth, headers?, skipValidation? }` extends `TracingOptions` |
|
|
144
|
+
| `InternalAuth` | `InternalAuthExplicit \| 'internal'` |
|
|
145
|
+
| `DirectAuth` | `AuthBase \| 'direct'` |
|
|
146
|
+
| `AuthBase` | `{ authToken?, orgSlug? }` — auto-resolves from context/cookies |
|
|
147
|
+
| `InternalAuthExplicit` | `AuthBase & { internalApiKey }` |
|
|
148
|
+
| `TracingOptions` | `{ requestId?, sessionId?, serviceName?, logger? }` |
|
|
149
|
+
| `HeaderValue` | `string \| (() => string \| undefined)` |
|
|
150
|
+
| `RequestContext` | `{ requestId, sessionId, orgSlug?, authToken? }` |
|
|
151
|
+
|
|
152
|
+
**Constants:** `REQUEST_ID_HEADER`, `SESSION_ID_HEADER`, `ORG_SLUG_HEADER`, `AUTH_TOKEN_HEADER`, `INTERNAL_API_KEY_HEADER`, `INITIATING_SERVICE_NAME_HEADER`
|
|
153
|
+
|
|
154
|
+
### `/server`
|
|
155
|
+
|
|
156
|
+
| Export | Description |
|
|
157
|
+
|--------|-------------|
|
|
158
|
+
| `createRequestMiddleware()` | Express middleware — reads headers/cookies, sets context + session cookie |
|
|
159
|
+
| `runWithRequestContext(ctx, fn)` | Scoped context via `AsyncLocalStorage.run()` |
|
|
160
|
+
| `runWithoutRequestContext(fn)` | No context (testing) |
|
|
161
|
+
| `getRequestContext()` | Read current context |
|
|
162
|
+
| `getRequestId()` | Request ID from context or new UUID |
|
|
163
|
+
|
|
164
|
+
### `/client`
|
|
165
|
+
|
|
166
|
+
| Export | Description |
|
|
167
|
+
|--------|-------------|
|
|
168
|
+
| `installRequestMiddleware(opts?)` | Patches global `fetch()` — request ID + session cookie |
|
|
169
|
+
| `createConsoleLogger(opts?)` | Console logger with level filtering and prefix |
|
package/client.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './dist/client/index';
|
package/client.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./dist/client/index.js');
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-client.d.ts","sourceRoot":"","sources":["../../src/cli/generate-client.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
/**
|
|
38
|
+
* Generates a complete, publishable typed OpenAPI client SDK for a TetraScience service.
|
|
39
|
+
*
|
|
40
|
+
* Reads configuration from the service's package.json `serviceClient` field:
|
|
41
|
+
*
|
|
42
|
+
* // Single spec
|
|
43
|
+
* "serviceClient": {
|
|
44
|
+
* "packageName": "@tetrascience/data-apps-client",
|
|
45
|
+
* "spec": "openapi/dataapps.yaml",
|
|
46
|
+
* "version": "2.0.0"
|
|
47
|
+
* }
|
|
48
|
+
*
|
|
49
|
+
* // Multiple specs (internal/external API surfaces)
|
|
50
|
+
* "serviceClient": {
|
|
51
|
+
* "packageName": "@tetrascience/data-acq-client",
|
|
52
|
+
* "spec": {
|
|
53
|
+
* "internal": "openapi/internal.yaml",
|
|
54
|
+
* "public": "openapi/public.yaml"
|
|
55
|
+
* },
|
|
56
|
+
* "version": "2.0.0"
|
|
57
|
+
* }
|
|
58
|
+
*
|
|
59
|
+
* Usage:
|
|
60
|
+
* generate-service-client
|
|
61
|
+
*/
|
|
62
|
+
const child_process_1 = require("child_process");
|
|
63
|
+
const fs = __importStar(require("fs"));
|
|
64
|
+
const path = __importStar(require("path"));
|
|
65
|
+
const generate_schemas_1 = require("./generate-schemas");
|
|
66
|
+
const templates_1 = require("./templates");
|
|
67
|
+
function readConfig() {
|
|
68
|
+
const args = process.argv.slice(2);
|
|
69
|
+
let configPath = path.join(process.cwd(), 'package.json');
|
|
70
|
+
for (let i = 0; i < args.length; i++) {
|
|
71
|
+
if (args[i] === '--config' && args[i + 1]) {
|
|
72
|
+
configPath = path.resolve(args[++i]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (!fs.existsSync(configPath)) {
|
|
76
|
+
console.error(`Error: ${configPath} not found`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
const pkg = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
80
|
+
const config = pkg.serviceClient;
|
|
81
|
+
if (!config) {
|
|
82
|
+
console.error('Error: no "serviceClient" field found in package.json');
|
|
83
|
+
console.error('');
|
|
84
|
+
console.error('Add configuration like:');
|
|
85
|
+
console.error(' "serviceClient": {');
|
|
86
|
+
console.error(' "packageName": "@tetrascience/my-service-client",');
|
|
87
|
+
console.error(' "spec": "openapi/spec.yaml",');
|
|
88
|
+
console.error(' "version": "1.0.0"');
|
|
89
|
+
console.error(' }');
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
if (!config.packageName) {
|
|
93
|
+
console.error('Error: serviceClient.packageName is required');
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
if (!config.spec) {
|
|
97
|
+
console.error('Error: serviceClient.spec is required');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
if (!config.version) {
|
|
101
|
+
console.error('Error: serviceClient.version is required');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
104
|
+
config.outDir = config.outDir || 'client';
|
|
105
|
+
return config;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Clean previously generated files from the output directory,
|
|
109
|
+
* but preserve node_modules/, .yarn/, and yarn.lock for install cache.
|
|
110
|
+
*/
|
|
111
|
+
function cleanOutDir(outDir) {
|
|
112
|
+
const preserve = new Set(['node_modules', '.yarn', 'yarn.lock']);
|
|
113
|
+
if (!fs.existsSync(outDir))
|
|
114
|
+
return;
|
|
115
|
+
for (const entry of fs.readdirSync(outDir)) {
|
|
116
|
+
if (preserve.has(entry))
|
|
117
|
+
continue;
|
|
118
|
+
fs.rmSync(path.join(outDir, entry), { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Detect the installed version of @tetrascience-npm/request
|
|
123
|
+
* and return it as a dependency specifier for the generated client.
|
|
124
|
+
*/
|
|
125
|
+
function getMiddlewareVersion() {
|
|
126
|
+
const ownPkgPath = path.resolve(__dirname, '..', '..', 'package.json');
|
|
127
|
+
try {
|
|
128
|
+
const pkg = JSON.parse(fs.readFileSync(ownPkgPath, 'utf-8'));
|
|
129
|
+
return pkg.version;
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return '^0.2.0';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Normalize the spec config into a list of { name, specPath } entries.
|
|
137
|
+
* Single string → one unnamed entry. Object → named variants.
|
|
138
|
+
*/
|
|
139
|
+
function resolveSpecs(spec) {
|
|
140
|
+
if (typeof spec === 'string') {
|
|
141
|
+
const resolved = path.resolve(spec);
|
|
142
|
+
if (!fs.existsSync(resolved)) {
|
|
143
|
+
console.error(`Error: spec file not found: ${resolved}`);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
return [{ name: null, specPath: resolved }];
|
|
147
|
+
}
|
|
148
|
+
const entries = Object.entries(spec);
|
|
149
|
+
if (entries.length === 0) {
|
|
150
|
+
console.error('Error: spec object is empty — provide at least one named spec');
|
|
151
|
+
process.exit(1);
|
|
152
|
+
}
|
|
153
|
+
return entries.map(([name, specFile]) => {
|
|
154
|
+
const resolved = path.resolve(specFile);
|
|
155
|
+
if (!fs.existsSync(resolved)) {
|
|
156
|
+
console.error(`Error: spec file not found for "${name}": ${resolved}`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
return { name, specPath: resolved };
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function main() {
|
|
163
|
+
const config = readConfig();
|
|
164
|
+
const outDir = path.resolve(config.outDir);
|
|
165
|
+
const serviceName = (0, templates_1.deriveServiceName)(config.packageName);
|
|
166
|
+
const specs = resolveSpecs(config.spec);
|
|
167
|
+
const isMultiSpec = specs[0].name !== null;
|
|
168
|
+
const middlewareVersion = getMiddlewareVersion();
|
|
169
|
+
console.log(`Generating ${config.packageName} v${config.version}`);
|
|
170
|
+
console.log(`Using @tetrascience-npm/request@${middlewareVersion}\n`);
|
|
171
|
+
// 1. Clean and prepare output directory
|
|
172
|
+
cleanOutDir(outDir);
|
|
173
|
+
fs.mkdirSync(path.join(outDir, 'generated'), { recursive: true });
|
|
174
|
+
const openapiTsBin = (0, generate_schemas_1.findBin)('openapi-typescript');
|
|
175
|
+
const generatedDir = path.join(outDir, 'generated');
|
|
176
|
+
for (const { name, specPath } of specs) {
|
|
177
|
+
const prefix = name ? `${name}.` : '';
|
|
178
|
+
console.log(` spec: ${path.relative(process.cwd(), specPath)}${name ? ` (${name})` : ''}`);
|
|
179
|
+
// 2. Generate OpenAPI TypeScript types
|
|
180
|
+
const schemaOut = path.join(generatedDir, `${prefix}schema.ts`);
|
|
181
|
+
(0, child_process_1.execSync)(`"${openapiTsBin}" "${specPath}" -o "${schemaOut}"`, { stdio: 'inherit' });
|
|
182
|
+
// 3. Generate Zod request-body schemas
|
|
183
|
+
(0, generate_schemas_1.generateRequestSchemas)(specPath, generatedDir, `${prefix}request-schemas.ts`);
|
|
184
|
+
}
|
|
185
|
+
// 4. Write template files
|
|
186
|
+
const specVariants = isMultiSpec ? specs.map((s) => s.name) : null;
|
|
187
|
+
const files = {
|
|
188
|
+
'package.json': (0, templates_1.generateClientPackageJson)({
|
|
189
|
+
packageName: config.packageName,
|
|
190
|
+
version: config.version,
|
|
191
|
+
description: config.description,
|
|
192
|
+
serviceName,
|
|
193
|
+
middlewareVersion,
|
|
194
|
+
}),
|
|
195
|
+
'tsconfig.build.json': (0, templates_1.generateTsConfig)(),
|
|
196
|
+
'index.ts': (0, templates_1.generateIndexTs)(serviceName, specVariants),
|
|
197
|
+
'.gitignore': (0, templates_1.generateClientGitIgnore)(),
|
|
198
|
+
'.yarnrc.yml': (0, templates_1.generateYarnRc)(),
|
|
199
|
+
};
|
|
200
|
+
for (const [name, content] of Object.entries(files)) {
|
|
201
|
+
fs.writeFileSync(path.join(outDir, name), content);
|
|
202
|
+
}
|
|
203
|
+
console.log(`\nGenerated ${config.packageName} in ${path.relative(process.cwd(), outDir)}/`);
|
|
204
|
+
console.log('For local testing: cd ' + path.relative(process.cwd(), outDir) + ' && yarn install && yarn build');
|
|
205
|
+
}
|
|
206
|
+
if (require.main === module) {
|
|
207
|
+
main();
|
|
208
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Walk up from cwd looking for a binary in node_modules/.bin.
|
|
4
|
+
*/
|
|
5
|
+
export declare function findBin(name: string): string;
|
|
6
|
+
export declare function generateRequestSchemas(specPath: string, outDir: string, outFileName?: string): void;
|
|
7
|
+
//# sourceMappingURL=generate-schemas.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-schemas.d.ts","sourceRoot":"","sources":["../../src/cli/generate-schemas.ts"],"names":[],"mappings":";AA8DA;;GAEG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAS5C;AAMD,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CA6GnG"}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.findBin = findBin;
|
|
38
|
+
exports.generateRequestSchemas = generateRequestSchemas;
|
|
39
|
+
/**
|
|
40
|
+
* Generates Zod request-body schemas from an OpenAPI spec.
|
|
41
|
+
*
|
|
42
|
+
* 1. Runs openapi-zod-client to get Zod schemas for all components
|
|
43
|
+
* 2. Parses the OpenAPI spec to find which path+method has a request body
|
|
44
|
+
* 3. Outputs a validation map: "METHOD /path" -> Zod schema
|
|
45
|
+
*
|
|
46
|
+
* Usage:
|
|
47
|
+
* generate-request-schemas --spec <path-to-openapi-yaml> --out <output-dir>
|
|
48
|
+
*/
|
|
49
|
+
const child_process_1 = require("child_process");
|
|
50
|
+
const fs = __importStar(require("fs"));
|
|
51
|
+
const path = __importStar(require("path"));
|
|
52
|
+
function parseArgs() {
|
|
53
|
+
const args = process.argv.slice(2);
|
|
54
|
+
let specPath;
|
|
55
|
+
let outDir;
|
|
56
|
+
for (let i = 0; i < args.length; i++) {
|
|
57
|
+
if (args[i] === '--spec' && args[i + 1]) {
|
|
58
|
+
specPath = path.resolve(args[++i]);
|
|
59
|
+
}
|
|
60
|
+
else if (args[i] === '--out' && args[i + 1]) {
|
|
61
|
+
outDir = path.resolve(args[++i]);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!specPath) {
|
|
65
|
+
console.error('Error: --spec <path-to-openapi-yaml> is required');
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
if (!outDir) {
|
|
69
|
+
console.error('Error: --out <output-directory> is required');
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
if (!fs.existsSync(specPath)) {
|
|
73
|
+
console.error(`Error: spec file not found: ${specPath}`);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
return { specPath, outDir };
|
|
77
|
+
}
|
|
78
|
+
function loadYaml(filePath) {
|
|
79
|
+
let yaml;
|
|
80
|
+
try {
|
|
81
|
+
yaml = require('js-yaml');
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
console.error('Error: js-yaml is required for client generation. Install it as a devDependency: yarn add -D js-yaml');
|
|
85
|
+
process.exit(1);
|
|
86
|
+
}
|
|
87
|
+
return yaml.load(fs.readFileSync(filePath, 'utf-8'));
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Walk up from cwd looking for a binary in node_modules/.bin.
|
|
91
|
+
*/
|
|
92
|
+
function findBin(name) {
|
|
93
|
+
let dir = process.cwd();
|
|
94
|
+
while (dir !== path.dirname(dir)) {
|
|
95
|
+
const candidate = path.join(dir, 'node_modules', '.bin', name);
|
|
96
|
+
if (fs.existsSync(candidate))
|
|
97
|
+
return candidate;
|
|
98
|
+
dir = path.dirname(dir);
|
|
99
|
+
}
|
|
100
|
+
console.error(`Error: ${name} not found. Install it as a devDependency: yarn add -D ${name}`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
function findZodClientBin() {
|
|
104
|
+
return findBin('openapi-zod-client');
|
|
105
|
+
}
|
|
106
|
+
function generateRequestSchemas(specPath, outDir, outFileName) {
|
|
107
|
+
const tmpFile = path.join(outDir, '.zod-raw.ts');
|
|
108
|
+
const outFile = path.join(outDir, outFileName || 'request-schemas.ts');
|
|
109
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
110
|
+
// 1. Generate full zodios output to a temp file
|
|
111
|
+
const zodClientBin = findZodClientBin();
|
|
112
|
+
(0, child_process_1.execSync)(`"${zodClientBin}" "${specPath}" -o "${tmpFile}" --export-schemas`, { stdio: 'inherit' });
|
|
113
|
+
let raw;
|
|
114
|
+
try {
|
|
115
|
+
raw = fs.readFileSync(tmpFile, 'utf-8');
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
throw new Error(`Failed to read generated file: ${tmpFile}`);
|
|
119
|
+
}
|
|
120
|
+
// Extract schema section (everything before `const endpoints = makeApi(`)
|
|
121
|
+
const endpointsIdx = raw.indexOf('const endpoints = makeApi(');
|
|
122
|
+
if (endpointsIdx === -1) {
|
|
123
|
+
fs.unlinkSync(tmpFile);
|
|
124
|
+
throw new Error('Could not find endpoints definition in generated file');
|
|
125
|
+
}
|
|
126
|
+
const schemaSection = raw
|
|
127
|
+
.substring(0, endpointsIdx)
|
|
128
|
+
.split('\n')
|
|
129
|
+
.filter((l) => !l.startsWith('import '))
|
|
130
|
+
.join('\n')
|
|
131
|
+
.trim();
|
|
132
|
+
// 2. Parse the OpenAPI spec to find request body -> schema name mappings
|
|
133
|
+
const spec = loadYaml(specPath);
|
|
134
|
+
const bodyMap = {};
|
|
135
|
+
for (const [pathStr, methods] of Object.entries(spec.paths || {})) {
|
|
136
|
+
for (const [method, operation] of Object.entries(methods)) {
|
|
137
|
+
if (method === 'parameters')
|
|
138
|
+
continue;
|
|
139
|
+
const op = operation;
|
|
140
|
+
const requestBody = op.requestBody;
|
|
141
|
+
if (!requestBody?.content?.['application/json']?.schema)
|
|
142
|
+
continue;
|
|
143
|
+
const schema = requestBody.content['application/json'].schema;
|
|
144
|
+
const key = `${method.toUpperCase()} ${pathStr}`;
|
|
145
|
+
if (schema.$ref) {
|
|
146
|
+
const schemaName = schema.$ref.split('/').pop();
|
|
147
|
+
bodyMap[key] = schemaName;
|
|
148
|
+
}
|
|
149
|
+
else if (schema.type === 'array' && schema.items?.$ref) {
|
|
150
|
+
const opId = op.operationId?.replace(/-/g, '_');
|
|
151
|
+
if (opId)
|
|
152
|
+
bodyMap[key] = `${opId}_Body`;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
const opId = op.operationId?.replace(/-/g, '_');
|
|
156
|
+
if (opId)
|
|
157
|
+
bodyMap[key] = `${opId}_Body`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
// 3. Verify all referenced schema names exist in the generated code
|
|
162
|
+
const exportedSchemaNames = [...raw.matchAll(/^const (\w+)\s*=\s*z[.\s]/gm)].map((m) => m[1]);
|
|
163
|
+
for (const [key, schemaName] of Object.entries(bodyMap)) {
|
|
164
|
+
if (raw.includes(`const ${schemaName} =`) || raw.includes(`const ${schemaName}=`)) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
// Try to find a schema with matching suffix
|
|
168
|
+
const parts = schemaName.split('_');
|
|
169
|
+
const resourceHint = parts.slice(1, -1).join('_');
|
|
170
|
+
const fallback = exportedSchemaNames.find((name) => name.endsWith('_Body') && name.includes(resourceHint));
|
|
171
|
+
if (fallback) {
|
|
172
|
+
console.log(` info: ${key}: "${schemaName}" -> "${fallback}" (shared schema)`);
|
|
173
|
+
bodyMap[key] = fallback;
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
console.warn(` warn: Schema "${schemaName}" for ${key} not found — skipping`);
|
|
177
|
+
delete bodyMap[key];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// 4. Build output file
|
|
181
|
+
const lines = [
|
|
182
|
+
'// AUTO-GENERATED — do not edit. Run `generate-request-schemas` to regenerate.',
|
|
183
|
+
"import { z } from 'zod'",
|
|
184
|
+
'',
|
|
185
|
+
schemaSection,
|
|
186
|
+
'',
|
|
187
|
+
'/**',
|
|
188
|
+
' * Map of "METHOD /path" -> Zod schema for request body validation.',
|
|
189
|
+
' * Used by the request validation middleware.',
|
|
190
|
+
' */',
|
|
191
|
+
'export const requestBodySchemas: Record<string, z.ZodTypeAny> = {',
|
|
192
|
+
];
|
|
193
|
+
for (const [key, schemaName] of Object.entries(bodyMap)) {
|
|
194
|
+
lines.push(` '${key}': ${schemaName},`);
|
|
195
|
+
}
|
|
196
|
+
lines.push('}');
|
|
197
|
+
lines.push('');
|
|
198
|
+
fs.writeFileSync(outFile, lines.join('\n'));
|
|
199
|
+
fs.unlinkSync(tmpFile);
|
|
200
|
+
console.log(`\nGenerated ${outFile}`);
|
|
201
|
+
console.log(` ${Object.keys(bodyMap).length} request body schemas mapped:`);
|
|
202
|
+
for (const [key, schemaName] of Object.entries(bodyMap)) {
|
|
203
|
+
console.log(` ${key} -> ${schemaName}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Run as CLI
|
|
207
|
+
if (require.main === module) {
|
|
208
|
+
const { specPath, outDir } = parseArgs();
|
|
209
|
+
generateRequestSchemas(specPath, outDir);
|
|
210
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared template functions used by init-client and generate-client CLIs.
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Derive a PascalCase service name from the npm package name.
|
|
6
|
+
* @example "@tetrascience/data-apps-client" -> "DataApps"
|
|
7
|
+
* @example "@tetrascience/pipeline-client" -> "Pipeline"
|
|
8
|
+
*/
|
|
9
|
+
export declare function deriveServiceName(packageName: string): string;
|
|
10
|
+
export declare function generateTsConfig(): string;
|
|
11
|
+
/**
|
|
12
|
+
* Generate index.ts for a client package.
|
|
13
|
+
*
|
|
14
|
+
* @param serviceName - PascalCase service name (e.g. "DataApps")
|
|
15
|
+
* @param specVariants - null for single-spec, or an array of variant names (e.g. ["internal", "public"])
|
|
16
|
+
*/
|
|
17
|
+
export declare function generateIndexTs(serviceName: string, specVariants?: string[] | null): string;
|
|
18
|
+
export declare function generateClientPackageJson(config: {
|
|
19
|
+
packageName: string;
|
|
20
|
+
version: string;
|
|
21
|
+
description?: string;
|
|
22
|
+
serviceName: string;
|
|
23
|
+
middlewareVersion: string;
|
|
24
|
+
}): string;
|
|
25
|
+
export declare function generateClientGitIgnore(): string;
|
|
26
|
+
export declare function generateYarnRc(): string;
|
|
27
|
+
//# sourceMappingURL=templates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/cli/templates.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAM7D;AAED,wBAAgB,gBAAgB,IAAI,MAAM,CAkBzC;AAaD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,CAAC,EAAE,MAAM,EAAE,GAAG,IAAI,GAAG,MAAM,CA4E3F;AAED,wBAAgB,yBAAyB,CAAC,MAAM,EAAE;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;CAC1B,GAAG,MAAM,CAsBT;AAED,wBAAgB,uBAAuB,IAAI,MAAM,CAKhD;AAED,wBAAgB,cAAc,IAAI,MAAM,CAOvC"}
|