micro-contracts 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +351 -0
- package/dist/cli/templates.d.ts +16 -0
- package/dist/cli/templates.d.ts.map +1 -0
- package/dist/cli/templates.js +377 -0
- package/dist/cli/templates.js.map +1 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +978 -0
- package/dist/cli.js.map +1 -0
- package/dist/generator/dependencyGenerator.d.ts +43 -0
- package/dist/generator/dependencyGenerator.d.ts.map +1 -0
- package/dist/generator/dependencyGenerator.js +159 -0
- package/dist/generator/dependencyGenerator.js.map +1 -0
- package/dist/generator/domainGenerator.d.ts +16 -0
- package/dist/generator/domainGenerator.d.ts.map +1 -0
- package/dist/generator/domainGenerator.js +212 -0
- package/dist/generator/domainGenerator.js.map +1 -0
- package/dist/generator/index.d.ts +37 -0
- package/dist/generator/index.d.ts.map +1 -0
- package/dist/generator/index.js +747 -0
- package/dist/generator/index.js.map +1 -0
- package/dist/generator/linter.d.ts +24 -0
- package/dist/generator/linter.d.ts.map +1 -0
- package/dist/generator/linter.js +202 -0
- package/dist/generator/linter.js.map +1 -0
- package/dist/generator/overlayProcessor.d.ts +90 -0
- package/dist/generator/overlayProcessor.d.ts.map +1 -0
- package/dist/generator/overlayProcessor.js +532 -0
- package/dist/generator/overlayProcessor.js.map +1 -0
- package/dist/generator/schemaGenerator.d.ts +10 -0
- package/dist/generator/schemaGenerator.d.ts.map +1 -0
- package/dist/generator/schemaGenerator.js +299 -0
- package/dist/generator/schemaGenerator.js.map +1 -0
- package/dist/generator/templateProcessor.d.ts +178 -0
- package/dist/generator/templateProcessor.d.ts.map +1 -0
- package/dist/generator/templateProcessor.js +607 -0
- package/dist/generator/templateProcessor.js.map +1 -0
- package/dist/generator/typeGenerator.d.ts +9 -0
- package/dist/generator/typeGenerator.d.ts.map +1 -0
- package/dist/generator/typeGenerator.js +395 -0
- package/dist/generator/typeGenerator.js.map +1 -0
- package/dist/guardrails/allowlist.d.ts +45 -0
- package/dist/guardrails/allowlist.d.ts.map +1 -0
- package/dist/guardrails/allowlist.js +261 -0
- package/dist/guardrails/allowlist.js.map +1 -0
- package/dist/guardrails/config.d.ts +40 -0
- package/dist/guardrails/config.d.ts.map +1 -0
- package/dist/guardrails/config.js +174 -0
- package/dist/guardrails/config.js.map +1 -0
- package/dist/guardrails/docs.d.ts +24 -0
- package/dist/guardrails/docs.d.ts.map +1 -0
- package/dist/guardrails/docs.js +138 -0
- package/dist/guardrails/docs.js.map +1 -0
- package/dist/guardrails/drift.d.ts +23 -0
- package/dist/guardrails/drift.d.ts.map +1 -0
- package/dist/guardrails/drift.js +127 -0
- package/dist/guardrails/drift.js.map +1 -0
- package/dist/guardrails/index.d.ts +19 -0
- package/dist/guardrails/index.d.ts.map +1 -0
- package/dist/guardrails/index.js +25 -0
- package/dist/guardrails/index.js.map +1 -0
- package/dist/guardrails/lint.d.ts +20 -0
- package/dist/guardrails/lint.d.ts.map +1 -0
- package/dist/guardrails/lint.js +274 -0
- package/dist/guardrails/lint.js.map +1 -0
- package/dist/guardrails/manifest.d.ts +43 -0
- package/dist/guardrails/manifest.d.ts.map +1 -0
- package/dist/guardrails/manifest.js +231 -0
- package/dist/guardrails/manifest.js.map +1 -0
- package/dist/guardrails/runner.d.ts +31 -0
- package/dist/guardrails/runner.d.ts.map +1 -0
- package/dist/guardrails/runner.js +268 -0
- package/dist/guardrails/runner.js.map +1 -0
- package/dist/guardrails/security.d.ts +31 -0
- package/dist/guardrails/security.d.ts.map +1 -0
- package/dist/guardrails/security.js +181 -0
- package/dist/guardrails/security.js.map +1 -0
- package/dist/guardrails/typecheck.d.ts +15 -0
- package/dist/guardrails/typecheck.d.ts.map +1 -0
- package/dist/guardrails/typecheck.js +104 -0
- package/dist/guardrails/typecheck.js.map +1 -0
- package/dist/guardrails/types.d.ts +196 -0
- package/dist/guardrails/types.d.ts.map +1 -0
- package/dist/guardrails/types.js +8 -0
- package/dist/guardrails/types.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/types.d.ts +489 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +297 -0
- package/dist/types.js.map +1 -0
- package/docs/architecture.svg +226 -0
- package/docs/development-guardrails.md +541 -0
- package/docs/guardrails-concept.svg +252 -0
- package/docs/overlays-deep-dive.md +298 -0
- package/package.json +66 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
# micro-contracts
|
|
2
|
+
|
|
3
|
+
**Contract-first vertical slices for TypeScript Web/API systems.**
|
|
4
|
+
|
|
5
|
+
micro-contracts is a contract-first toolchain for TypeScript Web/API development. It tackles common failure modes—**frontend/backend contract drift**, **duplicated "common" rules**, and **accidental breaking changes in public APIs**—by treating **OpenAPI as the Single Source of Truth (SSoT)**.
|
|
6
|
+
|
|
7
|
+
Contracts alone aren't enough—they must be **enforceable**. micro-contracts includes **[Enforceable Guardrails](docs/development-guardrails.md)** that prevent both humans and AI from bypassing the contract-first workflow: blocking direct edits to generated files, detecting drift, and verifying security declarations match implementations.
|
|
8
|
+
|
|
9
|
+
## Design Philosophy
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
The core architecture is organized along two axes:
|
|
14
|
+
|
|
15
|
+
| Axis | Description | Example |
|
|
16
|
+
|------|-------------|---------|
|
|
17
|
+
| **Vertical (feature-aligned slices)** | A *module* is a feature-aligned contract boundary. The same contract spans UI (frontend) and API (backend). | `core`, `billing`, `users` |
|
|
18
|
+
| **Horizontal (cross-cutting concerns)** | Auth, tenancy, rate limiting, and shared error behavior are applied consistently via **OpenAPI Overlays**. | `x-middleware: [requireAuth, tenantIsolation]` |
|
|
19
|
+
|
|
20
|
+
### Key Differentiators
|
|
21
|
+
|
|
22
|
+
| # | Differentiator | What it means |
|
|
23
|
+
|---|----------------|---------------|
|
|
24
|
+
| 1 | **Vertical Modules + Horizontal Overlays** | Feature-aligned modules as contract boundaries; cross-cutting concerns (auth, rate-limit) injected via [OpenAPI Overlays](https://www.openapis.org/blog/2024/10/22/announcing-overlay-specification). |
|
|
25
|
+
| 2 | **OpenAPI as SSoT → Multi-artifact generation** | Single spec generates contract packages, server routes, and frontend clients. No manual sync required. |
|
|
26
|
+
| 3 | **Enforceable Guardrails** | Built-in checks prevent bypassing contract-first workflow—blocks direct edits to generated files, detects drift, verifies security declarations. See **[Guardrails](docs/development-guardrails.md)**. |
|
|
27
|
+
| 4 | **Public Surface Governance** | `contract-published` is extracted (not duplicated) from the master contract. `x-micro-contracts-non-exportable` fails generation if internal data leaks. |
|
|
28
|
+
| 5 | **Explicit Module Dependencies** | `x-micro-contracts-depend-on` declares cross-module dependencies. `deps/` re-exports only declared types; enables impact analysis. |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Who is this for?
|
|
33
|
+
|
|
34
|
+
| Scenario | Why micro-contracts helps |
|
|
35
|
+
|----------|---------------------------|
|
|
36
|
+
| **Modular monolith → microservices** | Same contracts work in monolith or split services; dependency tracking prevents hidden coupling |
|
|
37
|
+
| **Multiple teams sharing OpenAPI** | Explicit module dependencies make cross-team impact visible |
|
|
38
|
+
| **Published API with compatibility SLA** | `contract-published` extraction + `x-micro-contracts-non-exportable` fail-fast prevents accidental exposure |
|
|
39
|
+
| **Cross-cutting concerns at scale** | OpenAPI Overlays inject auth/rate-limit/tenancy without copy-paste |
|
|
40
|
+
|
|
41
|
+
**Not the best fit for:** Single-developer projects, auto-generated UI from schema, multi-language SDK generation (use OpenAPI Generator instead).
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Quick Start
|
|
46
|
+
|
|
47
|
+
> **Prerequisites**: Node.js 18+, TypeScript 5.0+, ESM (`"type": "module"`).
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
# 1. Install
|
|
51
|
+
npm install --save-dev micro-contracts
|
|
52
|
+
|
|
53
|
+
# 2. Initialize module structure
|
|
54
|
+
npx micro-contracts init core --openapi path/to/your/spec.yaml
|
|
55
|
+
|
|
56
|
+
# 3. Generate all code
|
|
57
|
+
npx micro-contracts generate
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
// 4. Use in your server
|
|
62
|
+
import { registerRoutes } from './core/routes.generated.js';
|
|
63
|
+
await registerRoutes(fastify);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
> **What `init` creates**: The `init` command creates starter templates for **Fastify** (server) and **fetch API** (client).
|
|
67
|
+
> These are scaffolds to get you started — modify them for your framework (Express, Hono, Axios, etc.) or add new output types.
|
|
68
|
+
>
|
|
69
|
+
> **📦 Full working example**: See [`examples/`](./examples/) for a complete project with multiple modules, overlays, and cross-module dependencies.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Core Concepts
|
|
74
|
+
|
|
75
|
+
### OpenAPI as Single Source of Truth (SSoT)
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
OpenAPI spec (spec/{module}/openapi/*.yaml)
|
|
79
|
+
↓ micro-contracts generate
|
|
80
|
+
Contract packages (packages/contract/{module}/)
|
|
81
|
+
├── schemas/types.ts # Request/Response types
|
|
82
|
+
├── domains/ # Domain interfaces
|
|
83
|
+
└── overlays/ # Overlay handler interfaces
|
|
84
|
+
↓
|
|
85
|
+
Server routes + Frontend clients (generated via templates)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Modules vs Services
|
|
89
|
+
|
|
90
|
+
| Concept | Definition | Example |
|
|
91
|
+
|---------|------------|---------|
|
|
92
|
+
| **Module** | Logical contract boundary (OpenAPI + Domain) | `core`, `billing`, `users` |
|
|
93
|
+
| **Service** | Deployment unit (can contain 1+ modules) | `api-server` |
|
|
94
|
+
|
|
95
|
+
A monolith may have multiple modules in one service. Start with multiple modules in one service and split later as needed.
|
|
96
|
+
|
|
97
|
+
### Contract Packages
|
|
98
|
+
|
|
99
|
+
| Package | Description | Compatibility Policy |
|
|
100
|
+
|---------|-------------|---------------------|
|
|
101
|
+
| `contract` | Master contract (all APIs) | Internal APIs can change freely |
|
|
102
|
+
| `contract-published` | Public APIs only (`x-micro-contracts-published: true`) | Must maintain backward compatibility |
|
|
103
|
+
|
|
104
|
+
**Key insight**: `contract-published` is **extracted from** `contract` (not generated separately). This ensures a single SSoT.
|
|
105
|
+
|
|
106
|
+
### Cross-cutting Concerns with Overlays
|
|
107
|
+
|
|
108
|
+
1. Mark operations with `x-middleware` (or custom extensions) in OpenAPI
|
|
109
|
+
2. Define overlay that adds params/responses when extension is present
|
|
110
|
+
3. Generator applies overlays and produces `openapi.generated.yaml`
|
|
111
|
+
4. Generate code from the result
|
|
112
|
+
|
|
113
|
+
> **📖 Deep Dive**: See **[OpenAPI Overlays (Deep Dive)](docs/overlays-deep-dive.md)** for complete examples and configuration.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Directory Structure
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
project/
|
|
121
|
+
├── spec/ # ✅ Human-edited (contract source of truth)
|
|
122
|
+
│ ├── spectral.yaml # Global lint rules
|
|
123
|
+
│ ├── default/templates/ # Handlebars templates (customizable)
|
|
124
|
+
│ ├── _shared/
|
|
125
|
+
│ │ ├── openapi/ # Shared schemas (ProblemDetails, etc.)
|
|
126
|
+
│ │ └── overlays/ # Cross-module overlays
|
|
127
|
+
│ └── {module}/
|
|
128
|
+
│ ├── openapi/{module}.yaml # OpenAPI spec
|
|
129
|
+
│ └── overlays/ # Module-specific overlays
|
|
130
|
+
│
|
|
131
|
+
├── packages/ # ❌ Auto-generated (DO NOT EDIT)
|
|
132
|
+
│ ├── contract/{module}/
|
|
133
|
+
│ │ ├── schemas/ # Types, validators
|
|
134
|
+
│ │ ├── domains/ # Domain interfaces
|
|
135
|
+
│ │ ├── overlays/ # Overlay handler interfaces
|
|
136
|
+
│ │ └── deps/ # Re-exports from dependencies
|
|
137
|
+
│ └── contract-published/{module}/ # Public API subset
|
|
138
|
+
│
|
|
139
|
+
├── server/src/{module}/
|
|
140
|
+
│ ├── routes.generated.ts # ❌ Auto-generated (template: fastify-routes.hbs)
|
|
141
|
+
│ ├── domains/ # ✅ Human-edited (domain implementations)
|
|
142
|
+
│ └── overlays/ # ✅ Human-edited (overlay implementations)
|
|
143
|
+
│
|
|
144
|
+
└── frontend/src/{module}/
|
|
145
|
+
└── api.generated.ts # ❌ Auto-generated (template: fetch-client.hbs)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
> **Note**: `*.generated.ts` files are generated from Handlebars templates in `spec/default/templates/`.
|
|
149
|
+
> You can customize or replace templates for different frameworks (Express, Hono, Axios, etc.).
|
|
150
|
+
>
|
|
151
|
+
> **Why commit generated files?** Generated artifacts are committed to enable code review of contract changes and CI drift detection. If spec changes but generated code doesn't match, CI fails.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## OpenAPI Extensions
|
|
156
|
+
|
|
157
|
+
### Required Extensions
|
|
158
|
+
|
|
159
|
+
| Extension | Type | Description |
|
|
160
|
+
|-----------|------|-------------|
|
|
161
|
+
| `x-micro-contracts-domain` | string | Domain class name (e.g., `User`, `Order`) |
|
|
162
|
+
| `x-micro-contracts-method` | string | Method name to call (should match `operationId`) |
|
|
163
|
+
|
|
164
|
+
### Optional Extensions
|
|
165
|
+
|
|
166
|
+
| Extension | Type | Description |
|
|
167
|
+
|-----------|------|-------------|
|
|
168
|
+
| `x-micro-contracts-published` | boolean | Include in `contract-published` (compatibility SLA) |
|
|
169
|
+
| `x-micro-contracts-non-exportable` | boolean | Mark as non-exportable (fails if used in published endpoints) |
|
|
170
|
+
| `x-micro-contracts-depend-on` | string[] | Explicit dependencies on other modules' published APIs |
|
|
171
|
+
|
|
172
|
+
### Example
|
|
173
|
+
|
|
174
|
+
```yaml
|
|
175
|
+
paths:
|
|
176
|
+
/api/users:
|
|
177
|
+
get:
|
|
178
|
+
operationId: getUsers
|
|
179
|
+
x-micro-contracts-domain: User
|
|
180
|
+
x-micro-contracts-method: getUsers
|
|
181
|
+
x-micro-contracts-published: true
|
|
182
|
+
x-middleware: [requireAuth] # Custom extension for overlays
|
|
183
|
+
responses:
|
|
184
|
+
'200':
|
|
185
|
+
description: Success
|
|
186
|
+
content:
|
|
187
|
+
application/json:
|
|
188
|
+
schema:
|
|
189
|
+
$ref: '#/components/schemas/UserListResponse'
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Module Dependencies
|
|
193
|
+
|
|
194
|
+
Declare dependencies with `x-micro-contracts-depend-on`:
|
|
195
|
+
|
|
196
|
+
```yaml
|
|
197
|
+
# spec/billing/openapi/billing.yaml
|
|
198
|
+
info:
|
|
199
|
+
x-micro-contracts-depend-on:
|
|
200
|
+
- core.User.getUsers
|
|
201
|
+
- core.User.getUserById
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Import via generated `deps/`:
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
// ✅ Recommended: Import from deps/
|
|
208
|
+
import type { User } from '@project/contract/billing/deps/core';
|
|
209
|
+
|
|
210
|
+
// ❌ Avoid: Direct contract-published import
|
|
211
|
+
import type { User } from '@project/contract-published/core/schemas';
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Configuration
|
|
217
|
+
|
|
218
|
+
Create `micro-contracts.config.yaml`:
|
|
219
|
+
|
|
220
|
+
```yaml
|
|
221
|
+
defaults:
|
|
222
|
+
contract:
|
|
223
|
+
output: packages/contract/{module}
|
|
224
|
+
contractPublic:
|
|
225
|
+
output: packages/contract-published/{module}
|
|
226
|
+
outputs:
|
|
227
|
+
server-routes:
|
|
228
|
+
output: server/src/{module}/routes.generated.ts
|
|
229
|
+
template: spec/default/templates/fastify-routes.hbs
|
|
230
|
+
frontend-api:
|
|
231
|
+
output: frontend/src/{module}/api.generated.ts
|
|
232
|
+
template: spec/default/templates/fetch-client.hbs
|
|
233
|
+
overlays:
|
|
234
|
+
shared:
|
|
235
|
+
- spec/_shared/overlays/middleware.overlay.yaml
|
|
236
|
+
|
|
237
|
+
modules:
|
|
238
|
+
core:
|
|
239
|
+
openapi: openapi/core.yaml
|
|
240
|
+
billing:
|
|
241
|
+
openapi: openapi/billing.yaml
|
|
242
|
+
dependsOn:
|
|
243
|
+
- core.User.getUsers
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## CLI Reference
|
|
249
|
+
|
|
250
|
+
```bash
|
|
251
|
+
micro-contracts <command> [options]
|
|
252
|
+
|
|
253
|
+
Commands:
|
|
254
|
+
init <module> Initialize a new module structure
|
|
255
|
+
generate Generate code from OpenAPI specifications
|
|
256
|
+
lint Lint OpenAPI specifications (Spectral)
|
|
257
|
+
check Run guardrail checks
|
|
258
|
+
deps Analyze module dependencies
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### generate
|
|
262
|
+
|
|
263
|
+
```bash
|
|
264
|
+
micro-contracts generate [options]
|
|
265
|
+
|
|
266
|
+
Options:
|
|
267
|
+
-m, --module <names> Module names, comma-separated (default: all)
|
|
268
|
+
--contracts-only Generate contract packages only
|
|
269
|
+
--server-only Generate server routes only
|
|
270
|
+
--frontend-only Generate frontend clients only
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### lint
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
micro-contracts lint [options]
|
|
277
|
+
|
|
278
|
+
Options:
|
|
279
|
+
-m, --module <names> Module names (default: all)
|
|
280
|
+
--strict Treat warnings as errors
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### deps
|
|
284
|
+
|
|
285
|
+
```bash
|
|
286
|
+
micro-contracts deps [options]
|
|
287
|
+
|
|
288
|
+
Options:
|
|
289
|
+
--graph Display dependency graph
|
|
290
|
+
--who-depends-on <api> Find modules that depend on specific API
|
|
291
|
+
--impact <api> Analyze impact of changing specific API
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Generated Code
|
|
297
|
+
|
|
298
|
+
### Domain Interface
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// packages/contract/core/domains/UserDomainApi.ts
|
|
302
|
+
export interface UserDomainApi {
|
|
303
|
+
getUsers(input: UserDomain_getUsersInput): Promise<UserListResponse>;
|
|
304
|
+
getUserById(input: UserDomain_getUserByIdInput): Promise<User>;
|
|
305
|
+
}
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Domain Implementation
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
// server/src/core/domains/UserDomain.ts
|
|
312
|
+
import type { UserDomainApi } from '@project/contract/core/domains/UserDomainApi.js';
|
|
313
|
+
|
|
314
|
+
export class UserDomain implements UserDomainApi {
|
|
315
|
+
async getUsers(input) {
|
|
316
|
+
// Input is HTTP-agnostic: { limit?: number, offset?: number }
|
|
317
|
+
return { users: [...], total: 100 };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Related Documentation
|
|
325
|
+
|
|
326
|
+
| Document | Description |
|
|
327
|
+
|----------|-------------|
|
|
328
|
+
| **[Examples](./examples/)** | Complete working project with multiple modules, overlays, and cross-module dependencies |
|
|
329
|
+
| **[OpenAPI Overlays (Deep Dive)](docs/overlays-deep-dive.md)** | Complete overlay examples, JSONPath patterns, template context |
|
|
330
|
+
| **[Enforceable Guardrails (AI-ready)](docs/development-guardrails.md)** | CI integration, security checks, allowlist configuration |
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## Comparison with Similar Tools
|
|
335
|
+
|
|
336
|
+
| Aspect | micro-contracts | OpenAPI Generator | ts-rest |
|
|
337
|
+
|--------|-----------------|-------------------|---------|
|
|
338
|
+
| **Primary focus** | Contract governance (server + frontend + CI) | Multi-language SDK generation | TypeScript-first contract |
|
|
339
|
+
| **SSoT** | OpenAPI | OpenAPI | TypeScript |
|
|
340
|
+
| **Multi-artifact generation** | ✅ contract + routes + clients | △ SDK-focused (different goal) | ✅ Strong client/server alignment |
|
|
341
|
+
| **Enforceable guardrails** | ✅ Built-in (drift, no direct edit, CI gates) | ❌ Requires separate design | ❌ Requires separate design |
|
|
342
|
+
| **Public API governance** | ✅ `contract-published` + fail-fast | ❌ Manual | ❌ N/A |
|
|
343
|
+
| **Module dependencies** | ✅ `x-micro-contracts-depend-on` + `deps/` | ❌ Manual | ❌ Manual |
|
|
344
|
+
| **Cross-cutting concerns** | ✅ OpenAPI Overlays | ❌ Manual | △ Code-level implementation |
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## License
|
|
350
|
+
|
|
351
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Starter Templates for micro-contracts init command
|
|
3
|
+
*
|
|
4
|
+
* These templates are created in spec/default/templates/ and can be customized.
|
|
5
|
+
* Keep in sync with examples/spec/default/templates/
|
|
6
|
+
*/
|
|
7
|
+
export declare const STARTER_FASTIFY_ROUTES_TEMPLATE = "/**\n * Auto-generated Fastify routes\n * Generated from: {{spec.info.title}} v{{spec.info.version}}\n * DO NOT EDIT MANUALLY\n */\n\nimport type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';\nimport { allSchemas } from '{{contractPackage}}/schemas/index.js';\nimport * as types from '{{contractPackage}}/schemas/types.js';\n{{#if extensionInfo.length}}\nimport { runOverlays, toHttpRequest, sendError } from './overlayAdapter.generated.js';\n{{/if}}\n\nexport async function registerRoutes(fastify: FastifyInstance): Promise<void> {\n for (const schema of allSchemas) {\n fastify.addSchema(schema);\n }\n\n const { {{#each domains}}{{key}}{{#unless @last}}, {{/unless}}{{/each}} } = {{domainsPath}};\n{{#if extensionInfo.length}}\n const handlers = fastify.overlayHandlers;\n{{/if}}\n\n{{#each routes}}\n // {{uppercase method}} {{path}}{{#if isPublished}} [PUBLISHED]{{/if}}\n fastify.{{method}}('{{fastifyPath}}', {\n schema: {\n{{#if paramsType}}\n params: { $ref: '{{typeNameBase}}Params#' },\n{{/if}}\n{{#if requestBody}}\n body: { $ref: '{{requestBody.schemaName}}#' },\n{{/if}}\n{{#if responses.length}}\n response: { {{#each responses}}{{#if schemaName}}{{statusCode}}: { $ref: '{{schemaName}}#' }{{#unless @last}}, {{/unless}}{{/if}}{{/each}} },\n{{/if}}\n },\n }, async (req: FastifyRequest, reply: FastifyReply) => {\n{{#if extensions.length}}\n const result = await runOverlays('{{operationId}}', handlers, toHttpRequest(req));\n if (!result.success) return sendError(reply, result.error);\n{{/if}}\n // Build input object (always required, even if empty)\n const input: types.{{inputType}} = {\n{{#if pathParams.length}}\n ...(req.params as types.{{typeNameBase}}Params),\n{{/if}}\n{{#if queryParams.length}}\n ...(req.query as types.{{typeNameBase}}Params),\n{{/if}}\n{{#if requestBody}}\n data: req.body as types.{{requestBody.schemaName}},\n{{/if}}\n };\n return {{domainKey}}.{{domainMethod}}(input);\n });\n\n{{/each}}\n}\n";
|
|
8
|
+
export declare const STARTER_FETCH_CLIENT_TEMPLATE = "/**\n * Auto-generated HTTP Client from OpenAPI specification\n * Generated from: {{title}} v{{version}}\n * \n * DO NOT EDIT MANUALLY\n * \n * Client API matches Domain API signature (single input object).\n * Internally maps input to HTTP request (path params, query params, body).\n */\n\nimport type {\n{{#each domainTypes}}\n {{this}},\n{{/each}}\n} from '{{contractPackage}}/domains';\nimport type {\n{{#each schemaTypes}}\n {{this}},\n{{/each}}\n} from '{{contractPackage}}/schemas';\nimport { ApiError } from '{{contractPackage}}/errors';\n\n// BASE_URL derived from OpenAPI servers[0].url: {{baseUrl}}\n// Can be overridden via environment variable (Vite: VITE_API_BASE_URL)\nconst BASE_URL = (typeof import.meta !== 'undefined' && import.meta.env?.VITE_API_BASE_URL) || '{{baseUrl}}';\n\n/**\n * Handle HTTP response with typed error handling\n */\nasync function handleResponse<T>(res: Response): Promise<T> {\n if (!res.ok) {\n const problem = await res.json() as ProblemDetails;\n throw new ApiError(\n res.status,\n problem,\n res.headers.get('x-request-id') ?? undefined\n );\n }\n // Handle 204 No Content and empty responses\n if (res.status === 204 || res.headers.get('content-length') === '0') {\n return undefined as T;\n }\n return res.json();\n}\n\n{{#each domains}}\n// ==========================================================================\n// {{name}} API Client\n// ==========================================================================\nexport const {{key}}Api: {{name}}Api = {\n{{#each ../routes}}\n{{#if (eq domain ../name)}}\n /**\n * {{httpMethod}} {{path}}\n {{#if summary}}* {{summary}}{{/if}}\n {{#if isPublished}}* @published{{/if}}\n */\n async {{domainMethod}}(input: {{inputType}}): Promise<{{responseType}}> {\n{{#if queryParams.length}}\n const searchParams = new URLSearchParams();\n{{#each queryParams}}\n if (input.{{name}} !== undefined) searchParams.set('{{name}}', String(input.{{name}}));\n{{/each}}\n{{/if}}\n{{#if pathParams.length}}\n const url = `${BASE_URL}{{clientUrlPatternInput}}`{{#if queryParams.length}} + (searchParams.toString() ? '?' + searchParams : ''){{/if}};\n{{else}}\n const url = `${BASE_URL}{{path}}`{{#if queryParams.length}} + (searchParams.toString() ? '?' + searchParams : ''){{/if}};\n{{/if}}\n{{#if (eq httpMethod \"GET\")}}\n const res = await fetch(url);\n{{else if requestType}}\n const res = await fetch(url, {\n method: '{{httpMethod}}',\n headers: { 'Content-Type': 'application/json' },\n body: JSON.stringify(input.data),\n });\n{{else}}\n const res = await fetch(url, { method: '{{httpMethod}}' });\n{{/if}}\n{{#if (eq responseType \"void\")}}\n await handleResponse<void>(res);\n{{else}}\n return handleResponse<{{responseType}}>(res);\n{{/if}}\n },\n\n{{/if}}\n{{/each}}\n};\n\n{{/each}}\n";
|
|
9
|
+
export declare const STARTER_DOMAIN_STUBS_TEMPLATE = "{{!-- Domain implementation stubs template --}}\n{{!-- Generates skeleton implementations for domain methods --}}\n// Auto-generated domain stubs - Edit to implement business logic\nimport type { {{#each domains}}{{this}}DomainApi{{#unless @last}}, {{/unless}}{{/each}} } from '{{config.contractPackage}}/domains';\nimport type * as types from '{{config.contractPackage}}/schemas/types';\n\n{{#each domains}}\n/**\n * {{this}} Domain Implementation\n * \n * Implement the methods below with your business logic.\n * Generated methods receive HTTP-agnostic input objects.\n */\nexport class {{this}}Domain implements {{this}}DomainApi {\n{{#each ../operations}}\n{{#if (eq domain ../this)}}\n /**\n * {{summary}}\n * {{method}} {{path}}\n */\n async {{domainMethod}}({{#if inputType}}input: types.{{inputType}}{{/if}}): Promise<types.{{responseType}}> {\n // TODO: Implement {{domainMethod}}\n throw new Error('Not implemented: {{domainMethod}}');\n }\n\n{{/if}}\n{{/each}}\n}\n\n{{/each}}\n";
|
|
10
|
+
export declare const STARTER_OVERLAY_ADAPTER_TEMPLATE = "/**\n * Auto-generated Overlay Adapter\n * Generated from: {{spec.info.title}} v{{spec.info.version}}\n * DO NOT EDIT MANUALLY\n */\n\nimport type { FastifyReply } from 'fastify';\nimport type { OverlayResult, OverlayRegistry } from '{{contractPackage}}/overlays/index.js';\nimport type { ProblemDetails } from '{{contractPackage}}/schemas/types.js';\n\n// ==========================================================================\n// Types\n// ==========================================================================\n\n/** HTTP request abstraction */\nexport interface HttpRequest {\n headers: Record<string, string | string[] | undefined>;\n params: Record<string, string>;\n query: Record<string, unknown>;\n body: unknown;\n}\n\n/** Parameter extractor function */\ntype ParamExtractor = (req: HttpRequest) => Record<string, unknown>;\n\n// ==========================================================================\n// Parameter Extractors (one per overlay, shared across endpoints)\n// ==========================================================================\n\nconst getHeader = (req: HttpRequest, name: string): string | undefined =>\n req.headers[name.toLowerCase()] as string | undefined;\n\nconst getQuery = (req: HttpRequest, name: string): unknown => req.query[name];\n\nconst getParam = (req: HttpRequest, name: string): string | undefined => req.params[name];\n\n/** Overlay parameter extractors - each overlay defined once */\nconst extractors: Record<string, ParamExtractor> = {\n{{#each uniqueOverlays}}\n '{{name}}': (req) => ({\n{{#each params}}\n '{{name}}': {{#if (eq location \"headers\")}}getHeader(req, '{{name}}'){{else if (eq location \"query\")}}getQuery(req, '{{name}}'){{else}}getParam(req, '{{name}}'){{/if}},\n{{/each}}\n }),\n{{/each}}\n};\n\n// ==========================================================================\n// Endpoint \u2192 Overlay Mapping\n// ==========================================================================\n\n/** Which overlays apply to each endpoint */\nexport const endpointOverlays: Record<string, string[]> = {\n{{#each routes}}\n{{#if extensions.length}}\n '{{operationId}}': [{{#each extensions}}'{{value}}'{{#unless @last}}, {{/unless}}{{/each}}],\n{{/if}}\n{{/each}}\n};\n\n// ==========================================================================\n// Overlay Execution\n// ==========================================================================\n\n/**\n * Execute overlays for an endpoint\n */\nexport async function runOverlays(\n operationId: string,\n handlers: OverlayRegistry,\n req: HttpRequest\n): Promise<{ success: true; context: Record<string, unknown> } | { success: false; error: ProblemDetails }> {\n const overlayNames = endpointOverlays[operationId] || [];\n const context: Record<string, unknown> = {};\n\n for (const name of overlayNames) {\n const extract = extractors[name];\n const handler = handlers[name as keyof OverlayRegistry];\n \n if (!extract || !handler) continue;\n\n const input = extract(req);\n const result = await (handler as (input: Record<string, unknown>) => Promise<OverlayResult<unknown>>)(input);\n\n if (!result.success) {\n return {\n success: false,\n error: {\n type: `/errors/${result.error?.code?.toLowerCase() ?? 'error'}`,\n title: result.error?.message ?? 'Error',\n status: result.error?.status ?? 500,\n },\n };\n }\n\n if (result.context) {\n Object.assign(context, result.context);\n }\n }\n\n return { success: true, context };\n}\n\n/**\n * Build HttpRequest from Fastify request\n */\nexport function toHttpRequest(req: { headers: unknown; params: unknown; query: unknown; body: unknown }): HttpRequest {\n return {\n headers: req.headers as Record<string, string | string[] | undefined>,\n params: req.params as Record<string, string>,\n query: req.query as Record<string, unknown>,\n body: req.body,\n };\n}\n\n/**\n * Send error response\n */\nexport function sendError(reply: FastifyReply, error: ProblemDetails): void {\n reply.status(error.status).send(error);\n}\n";
|
|
11
|
+
export declare const STARTER_OVERLAY_STUBS_TEMPLATE = "{{!-- Extension implementation stubs template --}}\n{{!-- Generates skeleton implementations for overlay handlers --}}\n// Auto-generated extension stubs - Edit to implement overlay logic\nimport type {\n OverlayResult,\n{{#each extensionTypes}}\n {{this}}OverlayInput,\n {{this}}Overlay,\n{{/each}}\n OverlayRegistry,\n} from '{{config.contractPackage}}/overlays';\n\n{{#each extensions}}\n/**\n * {{name}} overlay handler\n * \n * Called when an endpoint has x-middleware: [{{name}}]\n * Returns OverlayResult with success/error status\n */\nexport async function {{camelCase name}}(\n input: {{pascalCase name}}OverlayInput\n): Promise<OverlayResult> {\n // TODO: Implement {{name}} logic\n{{#if injectedParameters}}\n // Input parameters:\n{{#each injectedParameters}}\n // - {{name}}: {{schema.type}}{{#if required}} (required){{/if}}\n{{/each}}\n{{/if}}\n\n // Example implementation:\n // if (!validateSomething(input)) {\n // return {\n // success: false,\n // error: { status: 4xx, message: 'Error message', code: 'ERROR_CODE' },\n // };\n // }\n\n return { success: true, context: { /* optional context for domain */ } };\n}\n\n{{/each}}\n/**\n * Registry of all overlay handlers\n * Register this with your server framework\n */\nexport const overlayHandlers: OverlayRegistry = {\n{{#each extensions}}\n {{camelCase name}}: {{camelCase name}},\n{{/each}}\n};\n";
|
|
12
|
+
/**
|
|
13
|
+
* Get all starter templates
|
|
14
|
+
*/
|
|
15
|
+
export declare function getStarterTemplates(): Record<string, string>;
|
|
16
|
+
//# sourceMappingURL=templates.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../../src/cli/templates.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,eAAO,MAAM,+BAA+B,g/DA2D3C,CAAC;AAEF,eAAO,MAAM,6BAA6B,+zFA4FzC,CAAC;AAEF,eAAO,MAAM,6BAA6B,k/BA8BzC,CAAC;AAEF,eAAO,MAAM,gCAAgC,ijIAyH5C,CAAC;AAEF,eAAO,MAAM,8BAA8B,63CAmD1C,CAAC;AAEF;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAQ5D"}
|