opinionated-machine 6.16.1 → 6.18.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/README.md +319 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/AbstractController.d.ts +21 -0
- package/dist/lib/AbstractController.js +20 -0
- package/dist/lib/AbstractController.js.map +1 -1
- package/dist/lib/DIContext.d.ts +25 -0
- package/dist/lib/DIContext.js +43 -4
- package/dist/lib/DIContext.js.map +1 -1
- package/dist/lib/api-contracts/AbstractApiController.d.ts +29 -8
- package/dist/lib/api-contracts/AbstractApiController.js +25 -6
- package/dist/lib/api-contracts/AbstractApiController.js.map +1 -1
- package/dist/lib/gateway/fastifyGatewayPlugin.d.ts +38 -0
- package/dist/lib/gateway/fastifyGatewayPlugin.js +36 -0
- package/dist/lib/gateway/fastifyGatewayPlugin.js.map +1 -0
- package/dist/lib/gateway/gatewayMetadata.d.ts +176 -0
- package/dist/lib/gateway/gatewayMetadata.js +168 -0
- package/dist/lib/gateway/gatewayMetadata.js.map +1 -0
- package/dist/lib/gateway/gatewaySymbol.d.ts +9 -0
- package/dist/lib/gateway/gatewaySymbol.js +10 -0
- package/dist/lib/gateway/gatewaySymbol.js.map +1 -0
- package/dist/lib/gateway/gatewayTypes.d.ts +59 -0
- package/dist/lib/gateway/gatewayTypes.js +2 -0
- package/dist/lib/gateway/gatewayTypes.js.map +1 -0
- package/dist/lib/gateway/index.d.ts +8 -0
- package/dist/lib/gateway/index.js +8 -0
- package/dist/lib/gateway/index.js.map +1 -0
- package/dist/lib/gateway/manifest/buildManifest.d.ts +38 -0
- package/dist/lib/gateway/manifest/buildManifest.js +90 -0
- package/dist/lib/gateway/manifest/buildManifest.js.map +1 -0
- package/dist/lib/gateway/manifest/manifestSchema.d.ts +321 -0
- package/dist/lib/gateway/manifest/manifestSchema.js +29 -0
- package/dist/lib/gateway/manifest/manifestSchema.js.map +1 -0
- package/dist/lib/gateway/manifest/pathNormalize.d.ts +22 -0
- package/dist/lib/gateway/manifest/pathNormalize.js +43 -0
- package/dist/lib/gateway/manifest/pathNormalize.js.map +1 -0
- package/dist/lib/gateway/withGatewayMetadata.d.ts +43 -0
- package/dist/lib/gateway/withGatewayMetadata.js +55 -0
- package/dist/lib/gateway/withGatewayMetadata.js.map +1 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -84,6 +84,15 @@ Very opinionated DI framework for fastify, built on top of awilix
|
|
|
84
84
|
- [Registering Dual-Mode Controllers](#registering-dual-mode-controllers)
|
|
85
85
|
- [Accept Header Routing](#accept-header-routing)
|
|
86
86
|
- [Testing Dual-Mode Controllers](#testing-dual-mode-controllers)
|
|
87
|
+
- [Gateway Configuration](#gateway-configuration)
|
|
88
|
+
- [Quick Start](#quick-start)
|
|
89
|
+
- [Annotating Routes](#annotating-routes)
|
|
90
|
+
- [Avoiding Repetition With Defaults](#avoiding-repetition-with-defaults)
|
|
91
|
+
- [Type-Safe Matching](#type-safe-matching)
|
|
92
|
+
- [Field Reference](#field-reference)
|
|
93
|
+
- [Generating Gateway Configs](#generating-gateway-configs)
|
|
94
|
+
- [Inspecting the Manifest at Runtime](#inspecting-the-manifest-at-runtime)
|
|
95
|
+
- [What's Not Covered](#whats-not-covered)
|
|
87
96
|
|
|
88
97
|
## Basic usage
|
|
89
98
|
|
|
@@ -2799,3 +2808,313 @@ describe('DashboardDualModeController', () => {
|
|
|
2799
2808
|
})
|
|
2800
2809
|
})
|
|
2801
2810
|
|
|
2811
|
+
## Gateway Configuration
|
|
2812
|
+
|
|
2813
|
+
Most services keep two copies of every route's policy: one in code, another in
|
|
2814
|
+
a hand-edited Envoy / KrakenD / Kong config. They drift, and outages happen at
|
|
2815
|
+
the seam. This feature lets you declare routing policy — timeouts, retries,
|
|
2816
|
+
rate limits, CORS, JWT auth, caching, header transforms, traffic matching —
|
|
2817
|
+
**next to the controller route it applies to**, then generate the gateway
|
|
2818
|
+
config from a single source of truth.
|
|
2819
|
+
|
|
2820
|
+
Generators ship as separate npm packages so your service binary doesn't pull
|
|
2821
|
+
them in:
|
|
2822
|
+
|
|
2823
|
+
| Gateway | Package | Output |
|
|
2824
|
+
| ------- | ------- | ------ |
|
|
2825
|
+
| Envoy | [`@opinionated-machine/gateway-envoy`](./packages/gateway-envoy) | static v3 YAML/JSON |
|
|
2826
|
+
| KrakenD | [`@opinionated-machine/gateway-krakend`](./packages/gateway-krakend) | declarative v3 JSON |
|
|
2827
|
+
| Kong | [`@opinionated-machine/gateway-kong`](./packages/gateway-kong) | DB-less declarative YAML/JSON |
|
|
2828
|
+
|
|
2829
|
+
### Quick Start
|
|
2830
|
+
|
|
2831
|
+
A complete round-trip in two steps. First, annotate routes in your existing
|
|
2832
|
+
controller:
|
|
2833
|
+
|
|
2834
|
+
```ts
|
|
2835
|
+
import { buildRestContract } from '@lokalise/api-contracts'
|
|
2836
|
+
import { buildFastifyRoute } from '@lokalise/fastify-api-contracts'
|
|
2837
|
+
import {
|
|
2838
|
+
AbstractController,
|
|
2839
|
+
type BuildRoutesReturnType,
|
|
2840
|
+
type GatewayMetadataValue,
|
|
2841
|
+
withGatewayMetadata,
|
|
2842
|
+
} from 'opinionated-machine'
|
|
2843
|
+
import { z } from 'zod/v4'
|
|
2844
|
+
|
|
2845
|
+
const getUser = buildRestContract({
|
|
2846
|
+
method: 'get',
|
|
2847
|
+
successResponseBodySchema: z.object({ id: z.string() }),
|
|
2848
|
+
requestPathParamsSchema: z.object({ userId: z.string() }),
|
|
2849
|
+
pathResolver: (p) => `/users/${p.userId}`,
|
|
2850
|
+
})
|
|
2851
|
+
const createUser = buildRestContract({
|
|
2852
|
+
method: 'post',
|
|
2853
|
+
requestBodySchema: z.object({ name: z.string() }),
|
|
2854
|
+
successResponseBodySchema: z.object({ id: z.string() }),
|
|
2855
|
+
pathResolver: () => '/users',
|
|
2856
|
+
})
|
|
2857
|
+
|
|
2858
|
+
export class UsersController extends AbstractController<typeof UsersController.contracts> {
|
|
2859
|
+
static readonly contracts = { getUser, createUser } as const
|
|
2860
|
+
|
|
2861
|
+
// Applies to every route in this controller; routes can override.
|
|
2862
|
+
override readonly gatewayDefaults: GatewayMetadataValue = {
|
|
2863
|
+
upstream: 'users-service',
|
|
2864
|
+
timeouts: { request: '5s' },
|
|
2865
|
+
auth: { required: true },
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
private getUser = buildFastifyRoute(UsersController.contracts.getUser, async (req, reply) => { /* … */ })
|
|
2869
|
+
private createUser = buildFastifyRoute(UsersController.contracts.createUser, async (req, reply) => { /* … */ })
|
|
2870
|
+
|
|
2871
|
+
buildRoutes(): BuildRoutesReturnType<typeof UsersController.contracts> {
|
|
2872
|
+
return {
|
|
2873
|
+
getUser: withGatewayMetadata(UsersController.contracts.getUser, this.getUser, {
|
|
2874
|
+
cache: { ttl: '60s' },
|
|
2875
|
+
}),
|
|
2876
|
+
createUser: withGatewayMetadata(UsersController.contracts.createUser, this.createUser, {
|
|
2877
|
+
rateLimit: { requests: 10, per: '1m', key: 'ip' },
|
|
2878
|
+
}),
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
```
|
|
2883
|
+
|
|
2884
|
+
Then write a small script that turns the running service definition into a
|
|
2885
|
+
gateway config — wire it into your build / CI pipeline:
|
|
2886
|
+
|
|
2887
|
+
```ts
|
|
2888
|
+
// bin/render-envoy.ts
|
|
2889
|
+
import { writeFileSync } from 'node:fs'
|
|
2890
|
+
import { renderEnvoyConfig } from '@opinionated-machine/gateway-envoy'
|
|
2891
|
+
import { buildContext } from '../src/diContext.ts' // your DIContext factory
|
|
2892
|
+
|
|
2893
|
+
const ctx = await buildContext()
|
|
2894
|
+
const manifest = ctx.buildGatewayManifest({
|
|
2895
|
+
service: 'users-api',
|
|
2896
|
+
defaults: { cors: { origins: ['https://app.example.com'], credentials: true } },
|
|
2897
|
+
})
|
|
2898
|
+
|
|
2899
|
+
const { yaml, warnings } = renderEnvoyConfig(manifest, {
|
|
2900
|
+
listenPort: 8080,
|
|
2901
|
+
clusters: { 'users-service': { hosts: ['users:8081'] } },
|
|
2902
|
+
})
|
|
2903
|
+
|
|
2904
|
+
writeFileSync('envoy.yaml', yaml)
|
|
2905
|
+
if (warnings.length) console.warn('[envoy]', warnings)
|
|
2906
|
+
```
|
|
2907
|
+
|
|
2908
|
+
```sh
|
|
2909
|
+
$ tsx bin/render-envoy.ts && envoy --mode validate -c envoy.yaml
|
|
2910
|
+
configuration 'envoy.yaml' OK
|
|
2911
|
+
```
|
|
2912
|
+
|
|
2913
|
+
The rest of this section unpacks each piece in detail.
|
|
2914
|
+
|
|
2915
|
+
### Annotating Routes
|
|
2916
|
+
|
|
2917
|
+
`withGatewayMetadata(contract, route, metadata)` takes:
|
|
2918
|
+
|
|
2919
|
+
- the **contract** — used purely for type inference on `match.headers`,
|
|
2920
|
+
`match.query` and `rateLimit.key`,
|
|
2921
|
+
- the **route** built by `buildFastifyRoute(...)` (or `buildApiRoute(...)`),
|
|
2922
|
+
- the **metadata** — see [Field Reference](#field-reference).
|
|
2923
|
+
|
|
2924
|
+
Apply it inside `buildRoutes()` (or in the `routes` array for
|
|
2925
|
+
`AbstractApiController`) so every route's gateway policy is in one scannable
|
|
2926
|
+
block. Annotated and un-annotated routes mix freely, and your existing
|
|
2927
|
+
`buildFastifyRoute(...)` calls don't change at all:
|
|
2928
|
+
|
|
2929
|
+
```ts
|
|
2930
|
+
buildRoutes() {
|
|
2931
|
+
return {
|
|
2932
|
+
getUser: withGatewayMetadata(c.getUser, this.getUser, { cache: { ttl: '60s' } }),
|
|
2933
|
+
createUser: withGatewayMetadata(c.createUser, this.createUser, { rateLimit: { requests: 10, per: '1m', key: 'ip' } }),
|
|
2934
|
+
deleteUser: this.deleteUser, // no per-route policy; inherits defaults
|
|
2935
|
+
}
|
|
2936
|
+
}
|
|
2937
|
+
```
|
|
2938
|
+
|
|
2939
|
+
For api-contract controllers, drop it directly into `routes`:
|
|
2940
|
+
|
|
2941
|
+
```ts
|
|
2942
|
+
class UsersApiController extends AbstractApiController {
|
|
2943
|
+
readonly routes = [
|
|
2944
|
+
withGatewayMetadata(getUser, buildApiRoute(getUser, async (req) => /* … */), { cache: { ttl: '60s' } }),
|
|
2945
|
+
buildApiRoute(deleteUser, async (req) => /* … */),
|
|
2946
|
+
]
|
|
2947
|
+
}
|
|
2948
|
+
```
|
|
2949
|
+
|
|
2950
|
+
Annotations are invisible to Fastify — adding them never changes runtime
|
|
2951
|
+
behaviour, so you can introduce them gradually on an existing service.
|
|
2952
|
+
|
|
2953
|
+
### Avoiding Repetition With Defaults
|
|
2954
|
+
|
|
2955
|
+
Most fields you'd write per route — upstream, base timeouts, auth posture,
|
|
2956
|
+
shared tags — are the same across every route in a controller, or every route
|
|
2957
|
+
in a service. Declare them once:
|
|
2958
|
+
|
|
2959
|
+
| Layer | Where | When to use |
|
|
2960
|
+
| ----- | ----- | ----------- |
|
|
2961
|
+
| Service-wide | `buildGatewayManifest({ defaults: … })` | Cross-cutting policy: CORS, idle timeouts, observability tags |
|
|
2962
|
+
| Controller | `override readonly gatewayDefaults = { … }` | Per-controller upstream, auth posture, base timeouts |
|
|
2963
|
+
| Per-route | `withGatewayMetadata(...)` | Anything specific to one endpoint |
|
|
2964
|
+
|
|
2965
|
+
Layers deep-merge in that order: service → controller → route. **Arrays in
|
|
2966
|
+
later layers replace** (not append), which keeps `weights`, `tags`, and
|
|
2967
|
+
`match.headers` predictable.
|
|
2968
|
+
|
|
2969
|
+
```ts
|
|
2970
|
+
context.buildGatewayManifest({
|
|
2971
|
+
service: 'users-api',
|
|
2972
|
+
defaults: {
|
|
2973
|
+
timeouts: { idle: '60s', connect: '1s' },
|
|
2974
|
+
cors: { origins: ['https://app.example.com'], credentials: true },
|
|
2975
|
+
tags: ['users-api'],
|
|
2976
|
+
},
|
|
2977
|
+
})
|
|
2978
|
+
```
|
|
2979
|
+
|
|
2980
|
+
### Type-Safe Matching
|
|
2981
|
+
|
|
2982
|
+
`match.headers` and `match.query` keys are inferred from the contract's
|
|
2983
|
+
`requestHeaderSchema` / `requestQuerySchema`. Typos and stale references
|
|
2984
|
+
become compile errors before you ever ship a config:
|
|
2985
|
+
|
|
2986
|
+
```ts
|
|
2987
|
+
const getUser = buildRestContract({
|
|
2988
|
+
method: 'get',
|
|
2989
|
+
successResponseBodySchema: ResponseBody,
|
|
2990
|
+
requestHeaderSchema: z.object({ 'x-trace-id': z.string() }),
|
|
2991
|
+
requestPathParamsSchema: z.object({ userId: z.string() }),
|
|
2992
|
+
pathResolver: (p) => `/users/${p.userId}`,
|
|
2993
|
+
})
|
|
2994
|
+
|
|
2995
|
+
withGatewayMetadata(getUser, this.getUser, {
|
|
2996
|
+
match: {
|
|
2997
|
+
headers: {
|
|
2998
|
+
'x-trace-id': { regex: '^[a-f0-9]+$' }, // ✅ type-checked against the contract
|
|
2999
|
+
'x-typo': 'foo', // ❌ compile error
|
|
3000
|
+
},
|
|
3001
|
+
customHeaders: {
|
|
3002
|
+
'x-cf-tenant': 'enterprise', // ✅ explicit escape hatch for headers not in the contract
|
|
3003
|
+
},
|
|
3004
|
+
},
|
|
3005
|
+
})
|
|
3006
|
+
```
|
|
3007
|
+
|
|
3008
|
+
`rateLimit.key` narrows the same way — `{ header: 'x-trace-id' }` only works
|
|
3009
|
+
if `'x-trace-id'` is in `requestHeaderSchema`; otherwise use
|
|
3010
|
+
`{ customHeader: '…' }`.
|
|
3011
|
+
|
|
3012
|
+
### Field Reference
|
|
3013
|
+
|
|
3014
|
+
Every field is optional. The shapes below cover the common cases — see
|
|
3015
|
+
[`gatewayMetadata.ts`](./lib/gateway/gatewayMetadata.ts) for the complete Zod
|
|
3016
|
+
schema, which is also what produces precise validation errors at generation
|
|
3017
|
+
time.
|
|
3018
|
+
|
|
3019
|
+
| Field | Example | Notes |
|
|
3020
|
+
| ----- | ------- | ----- |
|
|
3021
|
+
| `upstream` | `'users-service'` | Logical cluster name; resolved to a host by the generator |
|
|
3022
|
+
| `timeouts` | `{ request: '5s', idle: '60s', connect: '1s' }` | Duration units: `ms` / `s` / `m` / `h` |
|
|
3023
|
+
| `retry` | `{ attempts: 2, on: ['5xx', 'connect-failure'], perTryTimeout: '2s' }` | |
|
|
3024
|
+
| `rateLimit` | `{ requests: 100, per: '1m', key: 'ip' }` | `key`: `'ip'`, `{ header }`, `{ customHeader }`, `{ query }`, `{ customQuery }` |
|
|
3025
|
+
| `cache` | `{ ttl: '60s', methods: ['GET'], vary: ['Accept-Language'] }` | |
|
|
3026
|
+
| `cors` | `{ origins: ['https://app.example.com'], credentials: true }` | |
|
|
3027
|
+
| `auth` | `{ required: true, jwt: { issuer: '…', audiences: ['…'], jwksUri: '…' } }` | |
|
|
3028
|
+
| `circuitBreaker` | `{ maxRequests: 100, maxRetries: 3 }` | |
|
|
3029
|
+
| `match` | `{ headers, customHeaders, query, customQuery, host }` | Rule values: bare string (exact), `{ exact }`, `{ prefix }`, `{ regex }` |
|
|
3030
|
+
| `rewrite` | `{ stripPrefix: '/v2' }` or `{ replacePrefix: { from: '/v1', to: '/v2' } }` | |
|
|
3031
|
+
| `traffic` | `{ weights: [{ upstream: 'a', weight: 80 }, { upstream: 'b', weight: 20 }] }` | Also `shadow: { upstream, percent }` |
|
|
3032
|
+
| `headers` | `{ request: { add: { 'x-internal': 'true' }, remove: ['cookie'] }, response: … }` | Free-form keys; typically infra headers not in the contract |
|
|
3033
|
+
| `tags`, `visibility` | `tags: ['users']`, `visibility: 'internal'` | Documentation / partitioning |
|
|
3034
|
+
| `extensions` | `{ envoy: { … }, krakend: { … }, kong: { … } }` | Vendor escape hatch; merged onto the generated route last |
|
|
3035
|
+
|
|
3036
|
+
### Generating Gateway Configs
|
|
3037
|
+
|
|
3038
|
+
Each generator is a pure function — manifest in, config out — so you typically
|
|
3039
|
+
call them from a small build-time script. Pick one or all:
|
|
3040
|
+
|
|
3041
|
+
```ts
|
|
3042
|
+
import { writeFileSync } from 'node:fs'
|
|
3043
|
+
import { renderEnvoyConfig } from '@opinionated-machine/gateway-envoy'
|
|
3044
|
+
import { renderKrakendConfig } from '@opinionated-machine/gateway-krakend'
|
|
3045
|
+
import { renderKongConfig } from '@opinionated-machine/gateway-kong'
|
|
3046
|
+
|
|
3047
|
+
const manifest = context.buildGatewayManifest({ service: 'users-api' })
|
|
3048
|
+
|
|
3049
|
+
writeFileSync('envoy.yaml',
|
|
3050
|
+
renderEnvoyConfig(manifest, {
|
|
3051
|
+
listenPort: 8080,
|
|
3052
|
+
clusters: { 'users-service': { hosts: ['users:8081'] } },
|
|
3053
|
+
}).yaml)
|
|
3054
|
+
|
|
3055
|
+
writeFileSync('krakend.json',
|
|
3056
|
+
JSON.stringify(renderKrakendConfig(manifest, {
|
|
3057
|
+
port: 8080,
|
|
3058
|
+
upstreams: { 'users-service': 'http://users:8081' },
|
|
3059
|
+
}).json, null, 2))
|
|
3060
|
+
|
|
3061
|
+
writeFileSync('kong.yaml',
|
|
3062
|
+
renderKongConfig(manifest, {
|
|
3063
|
+
upstreams: { 'users-service': { url: 'http://users:8081' } },
|
|
3064
|
+
}).yaml)
|
|
3065
|
+
```
|
|
3066
|
+
|
|
3067
|
+
Each result includes `warnings: string[]` listing metadata fields the gateway
|
|
3068
|
+
can't natively express — log them so policy isn't silently dropped (e.g. Envoy
|
|
3069
|
+
doesn't ship an HTTP cache filter, so `cache.ttl` will appear in
|
|
3070
|
+
`warnings` under the Envoy generator). When you need a knob the universal
|
|
3071
|
+
model doesn't cover, hand-write it under `extensions.<vendor>` on the route —
|
|
3072
|
+
generators merge that block onto the rendered route last.
|
|
3073
|
+
|
|
3074
|
+
For each gateway's full mapping table and quirks:
|
|
3075
|
+
|
|
3076
|
+
- [`@opinionated-machine/gateway-envoy`](./packages/gateway-envoy/README.md)
|
|
3077
|
+
- [`@opinionated-machine/gateway-krakend`](./packages/gateway-krakend/README.md)
|
|
3078
|
+
- [`@opinionated-machine/gateway-kong`](./packages/gateway-kong/README.md)
|
|
3079
|
+
|
|
3080
|
+
### Inspecting the Manifest at Runtime
|
|
3081
|
+
|
|
3082
|
+
When you want the manifest from outside Node — a deployment CLI written in
|
|
3083
|
+
another language, an ops dashboard, a debug-time `curl` — register
|
|
3084
|
+
`fastifyGatewayPlugin`. The running service then exposes its manifest both in
|
|
3085
|
+
code and over HTTP:
|
|
3086
|
+
|
|
3087
|
+
```ts
|
|
3088
|
+
import { fastifyGatewayPlugin } from 'opinionated-machine'
|
|
3089
|
+
|
|
3090
|
+
await app.register(fastifyGatewayPlugin, {
|
|
3091
|
+
context, // your DIContext
|
|
3092
|
+
defaults: { service: 'users-api' }, // service name + any service-wide defaults
|
|
3093
|
+
// exposeRoute: '/__gateway/manifest', // opt-in HTTP route; omit to keep the manifest in-process only
|
|
3094
|
+
})
|
|
3095
|
+
|
|
3096
|
+
// In code, e.g. in another plugin or a graceful-shutdown drain hook:
|
|
3097
|
+
const manifest = app.buildGatewayManifest()
|
|
3098
|
+
|
|
3099
|
+
// Optionally fetch over HTTP from a CLI / sibling process — only when you
|
|
3100
|
+
// set `exposeRoute` above. The plugin never registers an HTTP route by
|
|
3101
|
+
// default to avoid leaking internal routing topology to unauthenticated
|
|
3102
|
+
// callers; pair it with auth middleware appropriate for your service.
|
|
3103
|
+
// curl http://localhost:8080/__gateway/manifest | jq '.routes'
|
|
3104
|
+
```
|
|
3105
|
+
|
|
3106
|
+
The manifest is rebuilt on every call, so it always reflects the current set
|
|
3107
|
+
of registered controllers.
|
|
3108
|
+
|
|
3109
|
+
### What's Not Covered
|
|
3110
|
+
|
|
3111
|
+
- **SSE and dual-mode controllers.** Only routes from `AbstractController` and
|
|
3112
|
+
`AbstractApiController` appear in the manifest today. Streaming routes still
|
|
3113
|
+
proxy through every gateway, but they aren't listed.
|
|
3114
|
+
- **Fields a particular gateway can't natively express.** They show up in
|
|
3115
|
+
`result.warnings` rather than disappearing. Reach for `extensions.<vendor>`
|
|
3116
|
+
to hand-write the missing piece on a per-route basis.
|
|
3117
|
+
- **Runtime drift detection.** The manifest is built from your code; the
|
|
3118
|
+
gateway runs separately. The generators don't compare deployed gateway
|
|
3119
|
+
state against the manifest.
|
|
3120
|
+
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export type { NestedPartial } from './lib/configUtils.js';
|
|
|
6
6
|
export { type DependencyInjectionOptions, DIContext, type RegisterDependenciesParams, } from './lib/DIContext.js';
|
|
7
7
|
export { ENABLE_ALL, isAnyMessageQueueConsumerEnabled, isEnqueuedJobWorkersEnabled, isJobQueueEnabled, isMessageQueueConsumerEnabled, isPeriodicJobEnabled, resolveJobQueuesEnabled, } from './lib/diConfigUtils.js';
|
|
8
8
|
export * from './lib/dualmode/index.js';
|
|
9
|
+
export * from './lib/gateway/index.js';
|
|
9
10
|
export * from './lib/resolverFunctions.js';
|
|
10
11
|
export * from './lib/routes/index.js';
|
|
11
12
|
export * from './lib/sse/index.js';
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,8 @@ export { DIContext, } from './lib/DIContext.js';
|
|
|
6
6
|
export { ENABLE_ALL, isAnyMessageQueueConsumerEnabled, isEnqueuedJobWorkersEnabled, isJobQueueEnabled, isMessageQueueConsumerEnabled, isPeriodicJobEnabled, resolveJobQueuesEnabled, } from './lib/diConfigUtils.js';
|
|
7
7
|
// Dual-mode (SSE + JSON)
|
|
8
8
|
export * from './lib/dualmode/index.js';
|
|
9
|
+
// Gateway metadata & manifest
|
|
10
|
+
export * from './lib/gateway/index.js';
|
|
9
11
|
export * from './lib/resolverFunctions.js';
|
|
10
12
|
// Routes (unified route builder)
|
|
11
13
|
export * from './lib/routes/index.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAA8B,MAAM,6BAA6B,CAAA;AAC5F,OAAO,EACL,cAAc,GAKf,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,0BAA0B,GAE3B,MAAM,qCAAqC,CAAA;AAC5C,cAAc,8BAA8B,CAAA;AAE5C,OAAO,EAEL,SAAS,GAEV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,UAAU,EACV,gCAAgC,EAChC,2BAA2B,EAC3B,iBAAiB,EACjB,6BAA6B,EAC7B,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,wBAAwB,CAAA;AAC/B,yBAAyB;AACzB,cAAc,yBAAyB,CAAA;AACvC,cAAc,4BAA4B,CAAA;AAC1C,iCAAiC;AACjC,cAAc,uBAAuB,CAAA;AACrC,MAAM;AACN,cAAc,oBAAoB,CAAA;AAClC,wBAAwB;AACxB,cAAc,wBAAwB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAA8B,MAAM,6BAA6B,CAAA;AAC5F,OAAO,EACL,cAAc,GAKf,MAAM,yBAAyB,CAAA;AAChC,OAAO,EACL,0BAA0B,GAE3B,MAAM,qCAAqC,CAAA;AAC5C,cAAc,8BAA8B,CAAA;AAE5C,OAAO,EAEL,SAAS,GAEV,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,UAAU,EACV,gCAAgC,EAChC,2BAA2B,EAC3B,iBAAiB,EACjB,6BAA6B,EAC7B,oBAAoB,EACpB,uBAAuB,GACxB,MAAM,wBAAwB,CAAA;AAC/B,yBAAyB;AACzB,cAAc,yBAAyB,CAAA;AACvC,8BAA8B;AAC9B,cAAc,wBAAwB,CAAA;AACtC,cAAc,4BAA4B,CAAA;AAC1C,iCAAiC;AACjC,cAAc,uBAAuB,CAAA;AACrC,MAAM;AACN,cAAc,oBAAoB,CAAA;AAClC,wBAAwB;AACxB,cAAc,wBAAwB,CAAA"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CommonRouteDefinition, DeleteRouteDefinition, GetRouteDefinition, PayloadRouteDefinition } from '@lokalise/api-contracts';
|
|
2
2
|
import type { buildFastifyRoute } from '@lokalise/fastify-api-contracts';
|
|
3
3
|
import type { z } from 'zod/v4';
|
|
4
|
+
import type { GatewayMetadataValue } from './gateway/gatewayMetadata.ts';
|
|
4
5
|
type AnyCommonRouteDefinition = CommonRouteDefinition<any, any, any, any, any, any, any, any>;
|
|
5
6
|
type OptionalZodSchema = z.Schema | undefined;
|
|
6
7
|
type FastifyPayloadRouteReturnType<RequestBody extends OptionalZodSchema, ResponseBody extends OptionalZodSchema, Path extends OptionalZodSchema, Query extends OptionalZodSchema, Headers extends OptionalZodSchema, ResponseHeaders extends OptionalZodSchema, IsNonJSONResponseExpected extends boolean, IsEmptyResponseExpected extends boolean, ResponseSchemasByStatusCode extends Record<number, any> | undefined> = ReturnType<typeof buildFastifyRoute<RequestBody, ResponseBody, Path, Query, Headers, ResponseHeaders, IsNonJSONResponseExpected, IsEmptyResponseExpected, ResponseSchemasByStatusCode>>;
|
|
@@ -10,5 +11,25 @@ export type BuildRoutesReturnType<APIContracts extends Record<string, AnyCommonR
|
|
|
10
11
|
};
|
|
11
12
|
export declare abstract class AbstractController<APIContracts extends Record<string, AnyCommonRouteDefinition>> {
|
|
12
13
|
abstract buildRoutes(): BuildRoutesReturnType<APIContracts>;
|
|
14
|
+
/**
|
|
15
|
+
* Optional controller-level defaults for gateway metadata.
|
|
16
|
+
*
|
|
17
|
+
* Merged underneath per-route metadata when `DIContext.buildGatewayManifest()`
|
|
18
|
+
* assembles a manifest. Use this for fields that apply to every route in the
|
|
19
|
+
* controller (e.g. `upstream`, `auth`, baseline `timeouts`).
|
|
20
|
+
*
|
|
21
|
+
* Service-wide defaults (passed to `buildGatewayManifest({ defaults })`) sit
|
|
22
|
+
* underneath these.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* public readonly gatewayDefaults: GatewayMetadataValue = {
|
|
27
|
+
* upstream: 'users-service',
|
|
28
|
+
* timeouts: { request: '5s' },
|
|
29
|
+
* auth: { required: true },
|
|
30
|
+
* }
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
readonly gatewayDefaults?: GatewayMetadataValue;
|
|
13
34
|
}
|
|
14
35
|
export {};
|
|
@@ -1,3 +1,23 @@
|
|
|
1
1
|
export class AbstractController {
|
|
2
|
+
/**
|
|
3
|
+
* Optional controller-level defaults for gateway metadata.
|
|
4
|
+
*
|
|
5
|
+
* Merged underneath per-route metadata when `DIContext.buildGatewayManifest()`
|
|
6
|
+
* assembles a manifest. Use this for fields that apply to every route in the
|
|
7
|
+
* controller (e.g. `upstream`, `auth`, baseline `timeouts`).
|
|
8
|
+
*
|
|
9
|
+
* Service-wide defaults (passed to `buildGatewayManifest({ defaults })`) sit
|
|
10
|
+
* underneath these.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* public readonly gatewayDefaults: GatewayMetadataValue = {
|
|
15
|
+
* upstream: 'users-service',
|
|
16
|
+
* timeouts: { request: '5s' },
|
|
17
|
+
* auth: { required: true },
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
gatewayDefaults;
|
|
2
22
|
}
|
|
3
23
|
//# sourceMappingURL=AbstractController.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AbstractController.js","sourceRoot":"","sources":["../../lib/AbstractController.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"AbstractController.js","sourceRoot":"","sources":["../../lib/AbstractController.ts"],"names":[],"mappings":"AAuHA,MAAM,OAAgB,kBAAkB;IAKtC;;;;;;;;;;;;;;;;;;OAkBG;IACa,eAAe,CAAuB;CACvD"}
|
package/dist/lib/DIContext.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { FastifyInstance } from 'fastify';
|
|
|
4
4
|
import type { AbstractModule } from './AbstractModule.js';
|
|
5
5
|
import { type NestedPartial } from './configUtils.js';
|
|
6
6
|
import type { ENABLE_ALL } from './diConfigUtils.js';
|
|
7
|
+
import { type BuildGatewayManifestOptions, type GatewayManifest } from './gateway/index.js';
|
|
7
8
|
import { type RegisterDualModeRoutesOptions, type RegisterSSERoutesOptions } from './routes/index.js';
|
|
8
9
|
export type RegisterDependenciesParams<Dependencies, Config, ExternalDependencies> = {
|
|
9
10
|
modules: readonly AbstractModule<unknown, ExternalDependencies>[];
|
|
@@ -38,6 +39,30 @@ export declare class DIContext<Dependencies extends object, Config extends objec
|
|
|
38
39
|
private registerModule;
|
|
39
40
|
registerDependencies(params: RegisterDependenciesParams<Dependencies, Config, ExternalDependencies>, externalDependencies: ExternalDependencies, resolveControllers?: boolean): void;
|
|
40
41
|
registerRoutes(app: FastifyInstance<any, any, any, any>): void;
|
|
42
|
+
/**
|
|
43
|
+
* Build a vendor-neutral gateway manifest from all registered REST and
|
|
44
|
+
* api-contract controllers. Routes carrying gateway metadata (attached via
|
|
45
|
+
* `withGatewayMetadata()`) get that metadata merged with controller-level
|
|
46
|
+
* `gatewayDefaults` and the `defaults` passed here. Routes without any
|
|
47
|
+
* metadata still appear in the manifest with empty metadata.
|
|
48
|
+
*
|
|
49
|
+
* The returned object is JSON-serializable; pass it to a generator package
|
|
50
|
+
* like `@opinionated-machine/gateway-envoy` or
|
|
51
|
+
* `@opinionated-machine/gateway-krakend` to produce a config.
|
|
52
|
+
*
|
|
53
|
+
* SSE and dual-mode controllers are not included in v1.
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```ts
|
|
57
|
+
* const manifest = context.buildGatewayManifest({
|
|
58
|
+
* service: 'users-api',
|
|
59
|
+
* defaults: { cors: { origins: ['https://app.example.com'] } },
|
|
60
|
+
* })
|
|
61
|
+
* const envoy = renderEnvoyConfig(manifest, { listenPort: 8080, clusters: { 'users-service': { hosts: ['users:8081'] } } })
|
|
62
|
+
* writeFileSync('envoy.yaml', envoy.yaml)
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
buildGatewayManifest(options: BuildGatewayManifestOptions): GatewayManifest;
|
|
41
66
|
/**
|
|
42
67
|
* Check if any SSE controllers are registered.
|
|
43
68
|
* Use this to conditionally call registerSSERoutes().
|
package/dist/lib/DIContext.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AwilixManager } from 'awilix-manager';
|
|
2
2
|
import { merge } from 'ts-deepmerge';
|
|
3
3
|
import { mergeConfigAndDependencyOverrides } from './configUtils.js';
|
|
4
|
+
import { buildGatewayManifestFrom, } from './gateway/index.js';
|
|
4
5
|
import { buildFastifyRoute, } from './routes/index.js';
|
|
5
6
|
export class DIContext {
|
|
6
7
|
options;
|
|
@@ -53,7 +54,7 @@ export class DIContext {
|
|
|
53
54
|
targetDiConfig[name] = resolver;
|
|
54
55
|
}
|
|
55
56
|
else {
|
|
56
|
-
this.controllerResolvers.push(resolver);
|
|
57
|
+
this.controllerResolvers.push({ name, resolver: resolver });
|
|
57
58
|
}
|
|
58
59
|
}
|
|
59
60
|
}
|
|
@@ -99,9 +100,9 @@ export class DIContext {
|
|
|
99
100
|
}
|
|
100
101
|
// biome-ignore lint/suspicious/noExplicitAny: we don't care about what instance we get here
|
|
101
102
|
registerRoutes(app) {
|
|
102
|
-
for (const
|
|
103
|
+
for (const { resolver } of this.controllerResolvers) {
|
|
103
104
|
// biome-ignore lint/suspicious/noExplicitAny: any controller works here
|
|
104
|
-
const controller =
|
|
105
|
+
const controller = resolver.resolve(this.diContainer);
|
|
105
106
|
const routes = controller.buildRoutes();
|
|
106
107
|
for (const route of Object.values(routes)) {
|
|
107
108
|
// Cast needed: GET/DELETE routes have body:undefined, POST/PATCH have body:unknown
|
|
@@ -110,12 +111,50 @@ export class DIContext {
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
for (const controllerName of this.apiControllerNames) {
|
|
114
|
+
// biome-ignore lint/suspicious/noExplicitAny: any api controllers works here
|
|
113
115
|
const controller = this.diContainer.resolve(controllerName);
|
|
114
|
-
for (const route of controller.routes) {
|
|
116
|
+
for (const route of Object.values(controller.routes)) {
|
|
115
117
|
app.route(route);
|
|
116
118
|
}
|
|
117
119
|
}
|
|
118
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Build a vendor-neutral gateway manifest from all registered REST and
|
|
123
|
+
* api-contract controllers. Routes carrying gateway metadata (attached via
|
|
124
|
+
* `withGatewayMetadata()`) get that metadata merged with controller-level
|
|
125
|
+
* `gatewayDefaults` and the `defaults` passed here. Routes without any
|
|
126
|
+
* metadata still appear in the manifest with empty metadata.
|
|
127
|
+
*
|
|
128
|
+
* The returned object is JSON-serializable; pass it to a generator package
|
|
129
|
+
* like `@opinionated-machine/gateway-envoy` or
|
|
130
|
+
* `@opinionated-machine/gateway-krakend` to produce a config.
|
|
131
|
+
*
|
|
132
|
+
* SSE and dual-mode controllers are not included in v1.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* const manifest = context.buildGatewayManifest({
|
|
137
|
+
* service: 'users-api',
|
|
138
|
+
* defaults: { cors: { origins: ['https://app.example.com'] } },
|
|
139
|
+
* })
|
|
140
|
+
* const envoy = renderEnvoyConfig(manifest, { listenPort: 8080, clusters: { 'users-service': { hosts: ['users:8081'] } } })
|
|
141
|
+
* writeFileSync('envoy.yaml', envoy.yaml)
|
|
142
|
+
* ```
|
|
143
|
+
*/
|
|
144
|
+
buildGatewayManifest(options) {
|
|
145
|
+
const collected = [];
|
|
146
|
+
for (const { name, resolver } of this.controllerResolvers) {
|
|
147
|
+
// biome-ignore lint/suspicious/noExplicitAny: any controller works here
|
|
148
|
+
const controller = resolver.resolve(this.diContainer);
|
|
149
|
+
collected.push({ name, kind: 'rest', controller });
|
|
150
|
+
}
|
|
151
|
+
for (const name of this.apiControllerNames) {
|
|
152
|
+
// biome-ignore lint/suspicious/noExplicitAny: any api controller works here
|
|
153
|
+
const controller = this.diContainer.resolve(name);
|
|
154
|
+
collected.push({ name, kind: 'api', controller });
|
|
155
|
+
}
|
|
156
|
+
return buildGatewayManifestFrom(collected, options);
|
|
157
|
+
}
|
|
119
158
|
/**
|
|
120
159
|
* Check if any SSE controllers are registered.
|
|
121
160
|
* Use this to conditionally call registerSSERoutes().
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DIContext.js","sourceRoot":"","sources":["../../lib/DIContext.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAE9C,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AAIpC,OAAO,EAAE,iCAAiC,EAAsB,MAAM,kBAAkB,CAAA;AAGxF,OAAO,EACL,iBAAiB,GAGlB,MAAM,mBAAmB,CAAA;AAwB1B,MAAM,OAAO,SAAS;IAKH,OAAO,CAA4B;IACpC,aAAa,CAAe;IAC5B,WAAW,CAA+B;IAC1D,8EAA8E;IAC7D,mBAAmB,
|
|
1
|
+
{"version":3,"file":"DIContext.js","sourceRoot":"","sources":["../../lib/DIContext.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAE9C,OAAO,EAAE,KAAK,EAAE,MAAM,cAAc,CAAA;AAIpC,OAAO,EAAE,iCAAiC,EAAsB,MAAM,kBAAkB,CAAA;AAGxF,OAAO,EAEL,wBAAwB,GAGzB,MAAM,oBAAoB,CAAA;AAC3B,OAAO,EACL,iBAAiB,GAGlB,MAAM,mBAAmB,CAAA;AAwB1B,MAAM,OAAO,SAAS;IAKH,OAAO,CAA4B;IACpC,aAAa,CAAe;IAC5B,WAAW,CAA+B;IAC1D,8EAA8E;IAC7D,mBAAmB,CAAkD;IACtF,mFAAmF;IAClE,kBAAkB,CAAU;IAC7C,yFAAyF;IACxE,uBAAuB,CAAU;IAClD,2FAA2F;IAC1E,kBAAkB,CAAU;IAC5B,SAAS,CAAQ;IAElC,YACE,WAA0C,EAC1C,OAAmC,EACnC,SAAiB,EACjB,aAA6B;QAE7B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAA;QACtB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAA;QAC9B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAA;QAC1B,IAAI,CAAC,aAAa;YAChB,aAAa;gBACb,IAAI,aAAa,CAAC;oBAChB,YAAY,EAAE,IAAI;oBAClB,SAAS,EAAE,IAAI;oBACf,WAAW;oBACX,WAAW,EAAE,IAAI;oBACjB,qBAAqB,EAAE,IAAI;iBAC5B,CAAC,CAAA;QACJ,IAAI,CAAC,mBAAmB,GAAG,EAAE,CAAA;QAC7B,IAAI,CAAC,kBAAkB,GAAG,EAAE,CAAA;QAC5B,IAAI,CAAC,uBAAuB,GAAG,EAAE,CAAA;QACjC,IAAI,CAAC,kBAAkB,GAAG,EAAE,CAAA;IAC9B,CAAC;IAEO,mBAAmB;IACzB,4FAA4F;IAC5F,WAAgC,EAChC,cAAqD;QAErD,KAAK,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;YAC3D,IAAI,QAAQ,CAAC,oBAAoB,EAAE,CAAC;gBAClC,IAAI,CAAC,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBACvC,2DAA2D;gBAC3D,cAAc,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAA;YACjC,CAAC;iBAAM,IAAI,QAAQ,CAAC,eAAe,EAAE,CAAC;gBACpC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBAClC,2DAA2D;gBAC3D,cAAc,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAA;YACjC,CAAC;iBAAM,IAAI,QAAQ,CAAC,eAAe,EAAE,CAAC;gBACpC,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;gBAClC,2DAA2D;gBAC3D,cAAc,CAAC,IAAI,CAAC,GAAG,QAAQ,CAAA;YACjC,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAA6B,EAAE,CAAC,CAAA;YAClF,CAAC;QACH,CAAC;IACH,CAAC;IAEO,cAAc,CACpB,MAAqD,EACrD,cAAqD,EACrD,oBAA0C,EAC1C,kBAA2B,EAC3B,eAAwB;QAExB,MAAM,gBAAgB,GAAG,MAAM,CAAC,mBAAmB,CAAC,IAAI,CAAC,OAAO,EAAE,oBAAoB,CAAC,CAAA;QAEvF,KAAK,MAAM,GAAG,IAAI,gBAAgB,EAAE,CAAC;YACnC,2DAA2D;YAC3D,IAAI,eAAe,IAAI,gBAAgB,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC;gBACpD,2DAA2D;gBAC3D,cAAc,CAAC,GAAG,CAAC,GAAG,gBAAgB,CAAC,GAAG,CAAC,CAAA;YAC7C,CAAC;QACH,CAAC;QAED,IAAI,eAAe,IAAI,kBAAkB,EAAE,CAAC;YAC1C,MAAM,WAAW,GAAG,MAAM,CAAC,kBAAkB,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;YAE3D,IAAI,CAAC,mBAAmB,CAAC,WAAW,EAAE,cAAc,CAAC,CAAA;QACvD,CAAC;IACH,CAAC;IAED,oBAAoB,CAClB,MAA8E,EAC9E,oBAA0C,EAC1C,kBAAkB,GAAG,IAAI;QAEzB,MAAM,eAAe,GAAG,iCAAiC,CACvD,IAAI,CAAC,SAAS,EACd,MAAM,CAAC,kBAAkB,IAAI,QAAQ,EACrC,MAAM,CAAC,eAAe,EACtB,MAAM,CAAC,mBAAmB,IAAI,EAAE,CACjC,CAAA;QACD,MAAM,cAAc,GAA0C,EAAE,CAAA;QAEhE,KAAK,MAAM,aAAa,IAAI,MAAM,CAAC,OAAO,EAAE,CAAC;YAC3C,IAAI,CAAC,cAAc,CACjB,aAAa,EACb,cAAc,EACd,oBAAoB,EACpB,kBAAkB,EAClB,IAAI,CACL,CAAA;QACH,CAAC;QAED,IAAI,MAAM,CAAC,gBAAgB,EAAE,CAAC;YAC5B,KAAK,MAAM,eAAe,IAAI,MAAM,CAAC,gBAAgB,EAAE,CAAC;gBACtD,IAAI,CAAC,cAAc,CACjB,eAAe,EACf,cAAc,EACd,oBAAoB,EACpB,kBAAkB,EAClB,KAAK,CACN,CAAA;YACH,CAAC;QACH,CAAC;QAED,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,cAAmD,CAAC,CAAA;QAE9E,8BAA8B;QAC9B,0CAA0C;QAC1C,KAAK,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,eAAe,CAAC,EAAE,CAAC;YAChF,MAAM,eAAe,GAAG,EAAE,GAAI,gBAAsC,EAAE,CAAA;YAEtE,2CAA2C;YAC3C,MAAM,gBAAgB,GAAG,IAAI,CAAC,WAAW,CAAC,eAAe,CAAC,aAAa,CAAC,CAAA;YACxE,mBAAmB;YACnB,IAAI,eAAe,CAAC,QAAQ,KAAK,gBAAgB,CAAC,QAAQ,EAAE,CAAC;gBAC3D,mBAAmB;gBACnB,eAAe,CAAC,QAAQ,GAAG,gBAAgB,CAAC,QAAQ,CAAA;YACtD,CAAC;YAED,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,aAAa,EAAE,eAAe,CAAC,CAAA;QAC3D,CAAC;IACH,CAAC;IAED,4FAA4F;IAC5F,cAAc,CAAC,GAAwC;QACrD,KAAK,MAAM,EAAE,QAAQ,EAAE,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;YACpD,wEAAwE;YACxE,MAAM,UAAU,GAA4B,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YAC9E,MAAM,MAAM,GAAG,UAAU,CAAC,WAAW,EAAE,CAAA;YACvC,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC1C,mFAAmF;gBACnF,2EAA2E;gBAC3E,GAAG,CAAC,KAAK,CAAC,KAAkB,CAAC,CAAA;YAC/B,CAAC;QACH,CAAC;QAED,KAAK,MAAM,cAAc,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;YACrD,6EAA6E;YAC7E,MAAM,UAAU,GAA+B,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;YAEvF,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;gBACrD,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAClB,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACH,oBAAoB,CAAC,OAAoC;QACvD,MAAM,SAAS,GAA0B,EAAE,CAAA;QAE3C,KAAK,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;YAC1D,wEAAwE;YACxE,MAAM,UAAU,GAA4B,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YAC9E,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAA;QACpD,CAAC;QAED,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC3C,4EAA4E;YAC5E,MAAM,UAAU,GAA+B,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAA;YAC7E,SAAS,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAA;QACnD,CAAC;QAED,OAAO,wBAAwB,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;IACrD,CAAC;IAED;;;OAGG;IACH,iBAAiB;QACf,OAAO,IAAI,CAAC,kBAAkB,CAAC,MAAM,GAAG,CAAC,CAAA;IAC3C,CAAC;IAED;;;OAGG;IACH,sBAAsB;QACpB,OAAO,IAAI,CAAC,uBAAuB,CAAC,MAAM,GAAG,CAAC,CAAA;IAChD,CAAC;IAED;;;;;;;;;;;;;;;;;OAiBG;IACH,iBAAiB;IACf,iFAAiF;IACjF,GAAwC,EACxC,OAAkC;QAElC,IAAI,CAAC,IAAI,CAAC,iBAAiB,EAAE,EAAE,CAAC;YAC9B,OAAM;QACR,CAAC;QAED,KAAK,MAAM,cAAc,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;YACrD,uDAAuD;YACvD,MAAM,aAAa,GACjB,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;YAC1C,MAAM,SAAS,GAAG,aAAa,CAAC,cAAc,EAAE,CAAA;YAEhD,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,CAAC;gBACnD,MAAM,KAAK,GAAG,iBAAiB,CAAC,aAAa,EAAE,WAAW,CAAC,CAAA;gBAC3D,IAAI,CAAC,oBAAoB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;gBACzC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAClB,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,sBAAsB;IACpB,iFAAiF;IACjF,GAAwC,EACxC,OAAuC;QAEvC,IAAI,CAAC,IAAI,CAAC,sBAAsB,EAAE,EAAE,CAAC;YACnC,OAAM;QACR,CAAC;QAED,KAAK,MAAM,cAAc,IAAI,IAAI,CAAC,uBAAuB,EAAE,CAAC;YAC1D,uDAAuD;YACvD,MAAM,kBAAkB,GAEpB,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,cAAc,CAAC,CAAA;YAC5C,MAAM,cAAc,GAAG,kBAAkB,CAAC,mBAAmB,EAAE,CAAA;YAE/D,KAAK,MAAM,WAAW,IAAI,MAAM,CAAC,MAAM,CAAC,cAAc,CAAC,EAAE,CAAC;gBACxD,MAAM,KAAK,GAAG,iBAAiB,CAAC,kBAAkB,EAAE,WAAW,CAAC,CAAA;gBAChE,IAAI,CAAC,yBAAyB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAA;gBAC9C,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAA;YAClB,CAAC;QACH,CAAC;IACH,CAAC;IAEO,yBAAyB,CAC/B,KAAmB,EACnB,OAAuC;QAEvC,IAAI,OAAO,EAAE,UAAU,EAAE,CAAC;YACxB,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,CAAA;QAClD,CAAC;QACD,IAAI,OAAO,EAAE,SAAS,EAAE,CAAC;YACvB,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAA;QAC/C,CAAC;QACD,0EAA0E;QAC1E,IAAI,OAAO,EAAE,iBAAiB,KAAK,SAAS,IAAI,OAAO,EAAE,UAAU,KAAK,SAAS,EAAE,CAAC;YAClF,2EAA2E;YAC3E,MAAM,eAAe,GAAG,KAAwC,CAAA;YAChE,eAAe,CAAC,MAAM,GAAG,KAAK,CAAC,eAAe,CAAC,MAAM,IAAI,EAAE,EAAE;gBAC3D,GAAG,EAAE;oBACH,GAAG,CAAC,OAAO,CAAC,iBAAiB,KAAK,SAAS,IAAI;wBAC7C,iBAAiB,EAAE,OAAO,CAAC,iBAAiB;qBAC7C,CAAC;oBACF,GAAG,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC;iBAC5E;aACF,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAEO,oBAAoB,CAAC,KAAmB,EAAE,OAAkC;QAClF,IAAI,OAAO,EAAE,UAAU,EAAE,CAAC;YACxB,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,UAAU,CAAC,CAAA;QAClD,CAAC;QACD,IAAI,OAAO,EAAE,SAAS,EAAE,CAAC;YACvB,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,OAAO,CAAC,SAAS,CAAC,CAAA;QAC/C,CAAC;QACD,6DAA6D;QAC7D,IAAI,OAAO,EAAE,iBAAiB,KAAK,SAAS,IAAI,OAAO,EAAE,UAAU,KAAK,SAAS,EAAE,CAAC;YAClF,2EAA2E;YAC3E,MAAM,eAAe,GAAG,KAAwC,CAAA;YAChE,eAAe,CAAC,MAAM,GAAG,KAAK,CAAC,eAAe,CAAC,MAAM,IAAI,EAAE,EAAE;gBAC3D,GAAG,EAAE;oBACH,GAAG,CAAC,OAAO,CAAC,iBAAiB,KAAK,SAAS,IAAI;wBAC7C,iBAAiB,EAAE,OAAO,CAAC,iBAAiB;qBAC7C,CAAC;oBACF,GAAG,CAAC,OAAO,CAAC,UAAU,KAAK,SAAS,IAAI,EAAE,UAAU,EAAE,OAAO,CAAC,UAAU,EAAE,CAAC;iBAC5E;aACF,CAAC,CAAA;QACJ,CAAC;IACH,CAAC;IAEO,gBAAgB,CACtB,KAAmB,EACnB,gBAA4C;QAE5C,MAAM,kBAAkB,GAAG,KAAK,CAAC,UAAU,CAAA;QAC3C,IAAI,CAAC,kBAAkB,EAAE,CAAC;YACxB,KAAK,CAAC,UAAU,GAAG,gBAAgB,CAAA;YACnC,OAAM;QACR,CAAC;QACD,2EAA2E;QAC3E,MAAM,QAAQ,GAAU,KAAK,CAAC,OAAO,CAAC,kBAAkB,CAAC;YACvD,CAAC,CAAC,kBAAkB;YACpB,CAAC,CAAC,CAAC,kBAAkB,CAAC,CAAA;QACxB,2EAA2E;QAC3E,MAAM,cAAc,GAAU,KAAK,CAAC,OAAO,CAAC,gBAAgB,CAAC;YAC3D,CAAC,CAAC,gBAAgB;YAClB,CAAC,CAAC,CAAC,gBAAgB,CAAC,CAAA;QACtB,KAAK,CAAC,UAAU,GAAG,CAAC,GAAG,cAAc,EAAE,GAAG,QAAQ,CAAC,CAAA;IACrD,CAAC;IAEO,cAAc,CACpB,KAAmB,EACnB,SAA6D;QAE7D,2EAA2E;QAC3E,MAAM,eAAe,GAAG,KAAwC,CAAA;QAChE,eAAe,CAAC,MAAM,GAAG;YACvB,GAAG,CAAC,eAAe,CAAC,MAAM,IAAI,EAAE,CAAC;YACjC,SAAS;SACV,CAAA;IACH,CAAC;IAED,KAAK,CAAC,OAAO;QACX,MAAM,IAAI,CAAC,aAAa,CAAC,cAAc,EAAE,CAAA;QACzC,MAAM,IAAI,CAAC,WAAW,CAAC,OAAO,EAAE,CAAA;IAClC,CAAC;IAED,KAAK,CAAC,IAAI;QACR,MAAM,IAAI,CAAC,aAAa,CAAC,WAAW,EAAE,CAAA;IACxC,CAAC;CACF"}
|
|
@@ -1,19 +1,40 @@
|
|
|
1
|
+
import type { ApiContract } from '@lokalise/api-contracts';
|
|
1
2
|
import type { RouteOptions } from 'fastify';
|
|
3
|
+
import type { GatewayMetadataValue } from '../gateway/gatewayMetadata.ts';
|
|
2
4
|
/**
|
|
3
5
|
* Abstract base class for controllers that use the `ApiContract` API.
|
|
4
6
|
*
|
|
5
|
-
* Concrete controllers declare a `
|
|
7
|
+
* Concrete controllers declare a static `contracts` field and a `routes` object
|
|
8
|
+
* built with `buildApiRoute()`. The generic ensures every contract has a matching route.
|
|
6
9
|
*
|
|
7
10
|
* @example
|
|
8
11
|
* ```typescript
|
|
9
|
-
* class UserController extends AbstractApiController {
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* class UserController extends AbstractApiController<typeof UserController.contracts> {
|
|
13
|
+
* static contracts = {
|
|
14
|
+
* getUser: getUserContract,
|
|
15
|
+
* streamUpdates: streamUpdatesContract,
|
|
16
|
+
* } as const
|
|
17
|
+
*
|
|
18
|
+
* readonly routes = {
|
|
19
|
+
* getUser: buildApiRoute(UserController.contracts.getUser, async (req) => ({
|
|
20
|
+
* status: 200,
|
|
21
|
+
* body: { id: req.params.id },
|
|
22
|
+
* })),
|
|
23
|
+
* streamUpdates: buildApiRoute(UserController.contracts.streamUpdates, async (_req, sse) => {
|
|
24
|
+
* sse.start('keepAlive')
|
|
25
|
+
* }),
|
|
26
|
+
* }
|
|
14
27
|
* }
|
|
15
28
|
* ```
|
|
16
29
|
*/
|
|
17
|
-
export declare abstract class AbstractApiController {
|
|
18
|
-
abstract readonly routes: RouteOptions
|
|
30
|
+
export declare abstract class AbstractApiController<APIContracts extends Record<string, ApiContract>> {
|
|
31
|
+
abstract readonly routes: Record<keyof APIContracts, RouteOptions>;
|
|
32
|
+
/**
|
|
33
|
+
* Optional controller-level defaults for gateway metadata.
|
|
34
|
+
*
|
|
35
|
+
* Merged underneath per-route metadata (attached via `withGatewayMetadata`)
|
|
36
|
+
* when `DIContext.buildGatewayManifest()` assembles a manifest. See
|
|
37
|
+
* `AbstractController.gatewayDefaults` for full semantics.
|
|
38
|
+
*/
|
|
39
|
+
readonly gatewayDefaults?: GatewayMetadataValue;
|
|
19
40
|
}
|