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.
Files changed (99) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +351 -0
  3. package/dist/cli/templates.d.ts +16 -0
  4. package/dist/cli/templates.d.ts.map +1 -0
  5. package/dist/cli/templates.js +377 -0
  6. package/dist/cli/templates.js.map +1 -0
  7. package/dist/cli.d.ts +9 -0
  8. package/dist/cli.d.ts.map +1 -0
  9. package/dist/cli.js +978 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/generator/dependencyGenerator.d.ts +43 -0
  12. package/dist/generator/dependencyGenerator.d.ts.map +1 -0
  13. package/dist/generator/dependencyGenerator.js +159 -0
  14. package/dist/generator/dependencyGenerator.js.map +1 -0
  15. package/dist/generator/domainGenerator.d.ts +16 -0
  16. package/dist/generator/domainGenerator.d.ts.map +1 -0
  17. package/dist/generator/domainGenerator.js +212 -0
  18. package/dist/generator/domainGenerator.js.map +1 -0
  19. package/dist/generator/index.d.ts +37 -0
  20. package/dist/generator/index.d.ts.map +1 -0
  21. package/dist/generator/index.js +747 -0
  22. package/dist/generator/index.js.map +1 -0
  23. package/dist/generator/linter.d.ts +24 -0
  24. package/dist/generator/linter.d.ts.map +1 -0
  25. package/dist/generator/linter.js +202 -0
  26. package/dist/generator/linter.js.map +1 -0
  27. package/dist/generator/overlayProcessor.d.ts +90 -0
  28. package/dist/generator/overlayProcessor.d.ts.map +1 -0
  29. package/dist/generator/overlayProcessor.js +532 -0
  30. package/dist/generator/overlayProcessor.js.map +1 -0
  31. package/dist/generator/schemaGenerator.d.ts +10 -0
  32. package/dist/generator/schemaGenerator.d.ts.map +1 -0
  33. package/dist/generator/schemaGenerator.js +299 -0
  34. package/dist/generator/schemaGenerator.js.map +1 -0
  35. package/dist/generator/templateProcessor.d.ts +178 -0
  36. package/dist/generator/templateProcessor.d.ts.map +1 -0
  37. package/dist/generator/templateProcessor.js +607 -0
  38. package/dist/generator/templateProcessor.js.map +1 -0
  39. package/dist/generator/typeGenerator.d.ts +9 -0
  40. package/dist/generator/typeGenerator.d.ts.map +1 -0
  41. package/dist/generator/typeGenerator.js +395 -0
  42. package/dist/generator/typeGenerator.js.map +1 -0
  43. package/dist/guardrails/allowlist.d.ts +45 -0
  44. package/dist/guardrails/allowlist.d.ts.map +1 -0
  45. package/dist/guardrails/allowlist.js +261 -0
  46. package/dist/guardrails/allowlist.js.map +1 -0
  47. package/dist/guardrails/config.d.ts +40 -0
  48. package/dist/guardrails/config.d.ts.map +1 -0
  49. package/dist/guardrails/config.js +174 -0
  50. package/dist/guardrails/config.js.map +1 -0
  51. package/dist/guardrails/docs.d.ts +24 -0
  52. package/dist/guardrails/docs.d.ts.map +1 -0
  53. package/dist/guardrails/docs.js +138 -0
  54. package/dist/guardrails/docs.js.map +1 -0
  55. package/dist/guardrails/drift.d.ts +23 -0
  56. package/dist/guardrails/drift.d.ts.map +1 -0
  57. package/dist/guardrails/drift.js +127 -0
  58. package/dist/guardrails/drift.js.map +1 -0
  59. package/dist/guardrails/index.d.ts +19 -0
  60. package/dist/guardrails/index.d.ts.map +1 -0
  61. package/dist/guardrails/index.js +25 -0
  62. package/dist/guardrails/index.js.map +1 -0
  63. package/dist/guardrails/lint.d.ts +20 -0
  64. package/dist/guardrails/lint.d.ts.map +1 -0
  65. package/dist/guardrails/lint.js +274 -0
  66. package/dist/guardrails/lint.js.map +1 -0
  67. package/dist/guardrails/manifest.d.ts +43 -0
  68. package/dist/guardrails/manifest.d.ts.map +1 -0
  69. package/dist/guardrails/manifest.js +231 -0
  70. package/dist/guardrails/manifest.js.map +1 -0
  71. package/dist/guardrails/runner.d.ts +31 -0
  72. package/dist/guardrails/runner.d.ts.map +1 -0
  73. package/dist/guardrails/runner.js +268 -0
  74. package/dist/guardrails/runner.js.map +1 -0
  75. package/dist/guardrails/security.d.ts +31 -0
  76. package/dist/guardrails/security.d.ts.map +1 -0
  77. package/dist/guardrails/security.js +181 -0
  78. package/dist/guardrails/security.js.map +1 -0
  79. package/dist/guardrails/typecheck.d.ts +15 -0
  80. package/dist/guardrails/typecheck.d.ts.map +1 -0
  81. package/dist/guardrails/typecheck.js +104 -0
  82. package/dist/guardrails/typecheck.js.map +1 -0
  83. package/dist/guardrails/types.d.ts +196 -0
  84. package/dist/guardrails/types.d.ts.map +1 -0
  85. package/dist/guardrails/types.js +8 -0
  86. package/dist/guardrails/types.js.map +1 -0
  87. package/dist/index.d.ts +7 -0
  88. package/dist/index.d.ts.map +1 -0
  89. package/dist/index.js +7 -0
  90. package/dist/index.js.map +1 -0
  91. package/dist/types.d.ts +489 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +297 -0
  94. package/dist/types.js.map +1 -0
  95. package/docs/architecture.svg +226 -0
  96. package/docs/development-guardrails.md +541 -0
  97. package/docs/guardrails-concept.svg +252 -0
  98. package/docs/overlays-deep-dive.md +298 -0
  99. package/package.json +66 -0
@@ -0,0 +1,377 @@
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 const STARTER_FASTIFY_ROUTES_TEMPLATE = `/**
8
+ * Auto-generated Fastify routes
9
+ * Generated from: {{spec.info.title}} v{{spec.info.version}}
10
+ * DO NOT EDIT MANUALLY
11
+ */
12
+
13
+ import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
14
+ import { allSchemas } from '{{contractPackage}}/schemas/index.js';
15
+ import * as types from '{{contractPackage}}/schemas/types.js';
16
+ {{#if extensionInfo.length}}
17
+ import { runOverlays, toHttpRequest, sendError } from './overlayAdapter.generated.js';
18
+ {{/if}}
19
+
20
+ export async function registerRoutes(fastify: FastifyInstance): Promise<void> {
21
+ for (const schema of allSchemas) {
22
+ fastify.addSchema(schema);
23
+ }
24
+
25
+ const { {{#each domains}}{{key}}{{#unless @last}}, {{/unless}}{{/each}} } = {{domainsPath}};
26
+ {{#if extensionInfo.length}}
27
+ const handlers = fastify.overlayHandlers;
28
+ {{/if}}
29
+
30
+ {{#each routes}}
31
+ // {{uppercase method}} {{path}}{{#if isPublished}} [PUBLISHED]{{/if}}
32
+ fastify.{{method}}('{{fastifyPath}}', {
33
+ schema: {
34
+ {{#if paramsType}}
35
+ params: { $ref: '{{typeNameBase}}Params#' },
36
+ {{/if}}
37
+ {{#if requestBody}}
38
+ body: { $ref: '{{requestBody.schemaName}}#' },
39
+ {{/if}}
40
+ {{#if responses.length}}
41
+ response: { {{#each responses}}{{#if schemaName}}{{statusCode}}: { $ref: '{{schemaName}}#' }{{#unless @last}}, {{/unless}}{{/if}}{{/each}} },
42
+ {{/if}}
43
+ },
44
+ }, async (req: FastifyRequest, reply: FastifyReply) => {
45
+ {{#if extensions.length}}
46
+ const result = await runOverlays('{{operationId}}', handlers, toHttpRequest(req));
47
+ if (!result.success) return sendError(reply, result.error);
48
+ {{/if}}
49
+ // Build input object (always required, even if empty)
50
+ const input: types.{{inputType}} = {
51
+ {{#if pathParams.length}}
52
+ ...(req.params as types.{{typeNameBase}}Params),
53
+ {{/if}}
54
+ {{#if queryParams.length}}
55
+ ...(req.query as types.{{typeNameBase}}Params),
56
+ {{/if}}
57
+ {{#if requestBody}}
58
+ data: req.body as types.{{requestBody.schemaName}},
59
+ {{/if}}
60
+ };
61
+ return {{domainKey}}.{{domainMethod}}(input);
62
+ });
63
+
64
+ {{/each}}
65
+ }
66
+ `;
67
+ export const STARTER_FETCH_CLIENT_TEMPLATE = `/**
68
+ * Auto-generated HTTP Client from OpenAPI specification
69
+ * Generated from: {{title}} v{{version}}
70
+ *
71
+ * DO NOT EDIT MANUALLY
72
+ *
73
+ * Client API matches Domain API signature (single input object).
74
+ * Internally maps input to HTTP request (path params, query params, body).
75
+ */
76
+
77
+ import type {
78
+ {{#each domainTypes}}
79
+ {{this}},
80
+ {{/each}}
81
+ } from '{{contractPackage}}/domains';
82
+ import type {
83
+ {{#each schemaTypes}}
84
+ {{this}},
85
+ {{/each}}
86
+ } from '{{contractPackage}}/schemas';
87
+ import { ApiError } from '{{contractPackage}}/errors';
88
+
89
+ // BASE_URL derived from OpenAPI servers[0].url: {{baseUrl}}
90
+ // Can be overridden via environment variable (Vite: VITE_API_BASE_URL)
91
+ const BASE_URL = (typeof import.meta !== 'undefined' && import.meta.env?.VITE_API_BASE_URL) || '{{baseUrl}}';
92
+
93
+ /**
94
+ * Handle HTTP response with typed error handling
95
+ */
96
+ async function handleResponse<T>(res: Response): Promise<T> {
97
+ if (!res.ok) {
98
+ const problem = await res.json() as ProblemDetails;
99
+ throw new ApiError(
100
+ res.status,
101
+ problem,
102
+ res.headers.get('x-request-id') ?? undefined
103
+ );
104
+ }
105
+ // Handle 204 No Content and empty responses
106
+ if (res.status === 204 || res.headers.get('content-length') === '0') {
107
+ return undefined as T;
108
+ }
109
+ return res.json();
110
+ }
111
+
112
+ {{#each domains}}
113
+ // ==========================================================================
114
+ // {{name}} API Client
115
+ // ==========================================================================
116
+ export const {{key}}Api: {{name}}Api = {
117
+ {{#each ../routes}}
118
+ {{#if (eq domain ../name)}}
119
+ /**
120
+ * {{httpMethod}} {{path}}
121
+ {{#if summary}}* {{summary}}{{/if}}
122
+ {{#if isPublished}}* @published{{/if}}
123
+ */
124
+ async {{domainMethod}}(input: {{inputType}}): Promise<{{responseType}}> {
125
+ {{#if queryParams.length}}
126
+ const searchParams = new URLSearchParams();
127
+ {{#each queryParams}}
128
+ if (input.{{name}} !== undefined) searchParams.set('{{name}}', String(input.{{name}}));
129
+ {{/each}}
130
+ {{/if}}
131
+ {{#if pathParams.length}}
132
+ const url = \`\${BASE_URL}{{clientUrlPatternInput}}\`{{#if queryParams.length}} + (searchParams.toString() ? '?' + searchParams : ''){{/if}};
133
+ {{else}}
134
+ const url = \`\${BASE_URL}{{path}}\`{{#if queryParams.length}} + (searchParams.toString() ? '?' + searchParams : ''){{/if}};
135
+ {{/if}}
136
+ {{#if (eq httpMethod "GET")}}
137
+ const res = await fetch(url);
138
+ {{else if requestType}}
139
+ const res = await fetch(url, {
140
+ method: '{{httpMethod}}',
141
+ headers: { 'Content-Type': 'application/json' },
142
+ body: JSON.stringify(input.data),
143
+ });
144
+ {{else}}
145
+ const res = await fetch(url, { method: '{{httpMethod}}' });
146
+ {{/if}}
147
+ {{#if (eq responseType "void")}}
148
+ await handleResponse<void>(res);
149
+ {{else}}
150
+ return handleResponse<{{responseType}}>(res);
151
+ {{/if}}
152
+ },
153
+
154
+ {{/if}}
155
+ {{/each}}
156
+ };
157
+
158
+ {{/each}}
159
+ `;
160
+ export const STARTER_DOMAIN_STUBS_TEMPLATE = `{{!-- Domain implementation stubs template --}}
161
+ {{!-- Generates skeleton implementations for domain methods --}}
162
+ // Auto-generated domain stubs - Edit to implement business logic
163
+ import type { {{#each domains}}{{this}}DomainApi{{#unless @last}}, {{/unless}}{{/each}} } from '{{config.contractPackage}}/domains';
164
+ import type * as types from '{{config.contractPackage}}/schemas/types';
165
+
166
+ {{#each domains}}
167
+ /**
168
+ * {{this}} Domain Implementation
169
+ *
170
+ * Implement the methods below with your business logic.
171
+ * Generated methods receive HTTP-agnostic input objects.
172
+ */
173
+ export class {{this}}Domain implements {{this}}DomainApi {
174
+ {{#each ../operations}}
175
+ {{#if (eq domain ../this)}}
176
+ /**
177
+ * {{summary}}
178
+ * {{method}} {{path}}
179
+ */
180
+ async {{domainMethod}}({{#if inputType}}input: types.{{inputType}}{{/if}}): Promise<types.{{responseType}}> {
181
+ // TODO: Implement {{domainMethod}}
182
+ throw new Error('Not implemented: {{domainMethod}}');
183
+ }
184
+
185
+ {{/if}}
186
+ {{/each}}
187
+ }
188
+
189
+ {{/each}}
190
+ `;
191
+ export const STARTER_OVERLAY_ADAPTER_TEMPLATE = `/**
192
+ * Auto-generated Overlay Adapter
193
+ * Generated from: {{spec.info.title}} v{{spec.info.version}}
194
+ * DO NOT EDIT MANUALLY
195
+ */
196
+
197
+ import type { FastifyReply } from 'fastify';
198
+ import type { OverlayResult, OverlayRegistry } from '{{contractPackage}}/overlays/index.js';
199
+ import type { ProblemDetails } from '{{contractPackage}}/schemas/types.js';
200
+
201
+ // ==========================================================================
202
+ // Types
203
+ // ==========================================================================
204
+
205
+ /** HTTP request abstraction */
206
+ export interface HttpRequest {
207
+ headers: Record<string, string | string[] | undefined>;
208
+ params: Record<string, string>;
209
+ query: Record<string, unknown>;
210
+ body: unknown;
211
+ }
212
+
213
+ /** Parameter extractor function */
214
+ type ParamExtractor = (req: HttpRequest) => Record<string, unknown>;
215
+
216
+ // ==========================================================================
217
+ // Parameter Extractors (one per overlay, shared across endpoints)
218
+ // ==========================================================================
219
+
220
+ const getHeader = (req: HttpRequest, name: string): string | undefined =>
221
+ req.headers[name.toLowerCase()] as string | undefined;
222
+
223
+ const getQuery = (req: HttpRequest, name: string): unknown => req.query[name];
224
+
225
+ const getParam = (req: HttpRequest, name: string): string | undefined => req.params[name];
226
+
227
+ /** Overlay parameter extractors - each overlay defined once */
228
+ const extractors: Record<string, ParamExtractor> = {
229
+ {{#each uniqueOverlays}}
230
+ '{{name}}': (req) => ({
231
+ {{#each params}}
232
+ '{{name}}': {{#if (eq location "headers")}}getHeader(req, '{{name}}'){{else if (eq location "query")}}getQuery(req, '{{name}}'){{else}}getParam(req, '{{name}}'){{/if}},
233
+ {{/each}}
234
+ }),
235
+ {{/each}}
236
+ };
237
+
238
+ // ==========================================================================
239
+ // Endpoint → Overlay Mapping
240
+ // ==========================================================================
241
+
242
+ /** Which overlays apply to each endpoint */
243
+ export const endpointOverlays: Record<string, string[]> = {
244
+ {{#each routes}}
245
+ {{#if extensions.length}}
246
+ '{{operationId}}': [{{#each extensions}}'{{value}}'{{#unless @last}}, {{/unless}}{{/each}}],
247
+ {{/if}}
248
+ {{/each}}
249
+ };
250
+
251
+ // ==========================================================================
252
+ // Overlay Execution
253
+ // ==========================================================================
254
+
255
+ /**
256
+ * Execute overlays for an endpoint
257
+ */
258
+ export async function runOverlays(
259
+ operationId: string,
260
+ handlers: OverlayRegistry,
261
+ req: HttpRequest
262
+ ): Promise<{ success: true; context: Record<string, unknown> } | { success: false; error: ProblemDetails }> {
263
+ const overlayNames = endpointOverlays[operationId] || [];
264
+ const context: Record<string, unknown> = {};
265
+
266
+ for (const name of overlayNames) {
267
+ const extract = extractors[name];
268
+ const handler = handlers[name as keyof OverlayRegistry];
269
+
270
+ if (!extract || !handler) continue;
271
+
272
+ const input = extract(req);
273
+ const result = await (handler as (input: Record<string, unknown>) => Promise<OverlayResult<unknown>>)(input);
274
+
275
+ if (!result.success) {
276
+ return {
277
+ success: false,
278
+ error: {
279
+ type: \`/errors/\${result.error?.code?.toLowerCase() ?? 'error'}\`,
280
+ title: result.error?.message ?? 'Error',
281
+ status: result.error?.status ?? 500,
282
+ },
283
+ };
284
+ }
285
+
286
+ if (result.context) {
287
+ Object.assign(context, result.context);
288
+ }
289
+ }
290
+
291
+ return { success: true, context };
292
+ }
293
+
294
+ /**
295
+ * Build HttpRequest from Fastify request
296
+ */
297
+ export function toHttpRequest(req: { headers: unknown; params: unknown; query: unknown; body: unknown }): HttpRequest {
298
+ return {
299
+ headers: req.headers as Record<string, string | string[] | undefined>,
300
+ params: req.params as Record<string, string>,
301
+ query: req.query as Record<string, unknown>,
302
+ body: req.body,
303
+ };
304
+ }
305
+
306
+ /**
307
+ * Send error response
308
+ */
309
+ export function sendError(reply: FastifyReply, error: ProblemDetails): void {
310
+ reply.status(error.status).send(error);
311
+ }
312
+ `;
313
+ export const STARTER_OVERLAY_STUBS_TEMPLATE = `{{!-- Extension implementation stubs template --}}
314
+ {{!-- Generates skeleton implementations for overlay handlers --}}
315
+ // Auto-generated extension stubs - Edit to implement overlay logic
316
+ import type {
317
+ OverlayResult,
318
+ {{#each extensionTypes}}
319
+ {{this}}OverlayInput,
320
+ {{this}}Overlay,
321
+ {{/each}}
322
+ OverlayRegistry,
323
+ } from '{{config.contractPackage}}/overlays';
324
+
325
+ {{#each extensions}}
326
+ /**
327
+ * {{name}} overlay handler
328
+ *
329
+ * Called when an endpoint has x-middleware: [{{name}}]
330
+ * Returns OverlayResult with success/error status
331
+ */
332
+ export async function {{camelCase name}}(
333
+ input: {{pascalCase name}}OverlayInput
334
+ ): Promise<OverlayResult> {
335
+ // TODO: Implement {{name}} logic
336
+ {{#if injectedParameters}}
337
+ // Input parameters:
338
+ {{#each injectedParameters}}
339
+ // - {{name}}: {{schema.type}}{{#if required}} (required){{/if}}
340
+ {{/each}}
341
+ {{/if}}
342
+
343
+ // Example implementation:
344
+ // if (!validateSomething(input)) {
345
+ // return {
346
+ // success: false,
347
+ // error: { status: 4xx, message: 'Error message', code: 'ERROR_CODE' },
348
+ // };
349
+ // }
350
+
351
+ return { success: true, context: { /* optional context for domain */ } };
352
+ }
353
+
354
+ {{/each}}
355
+ /**
356
+ * Registry of all overlay handlers
357
+ * Register this with your server framework
358
+ */
359
+ export const overlayHandlers: OverlayRegistry = {
360
+ {{#each extensions}}
361
+ {{camelCase name}}: {{camelCase name}},
362
+ {{/each}}
363
+ };
364
+ `;
365
+ /**
366
+ * Get all starter templates
367
+ */
368
+ export function getStarterTemplates() {
369
+ return {
370
+ 'fastify-routes.hbs': STARTER_FASTIFY_ROUTES_TEMPLATE,
371
+ 'fetch-client.hbs': STARTER_FETCH_CLIENT_TEMPLATE,
372
+ 'domain-stubs.hbs': STARTER_DOMAIN_STUBS_TEMPLATE,
373
+ 'overlay-adapter.hbs': STARTER_OVERLAY_ADAPTER_TEMPLATE,
374
+ 'overlay-stubs.hbs': STARTER_OVERLAY_STUBS_TEMPLATE,
375
+ };
376
+ }
377
+ //# sourceMappingURL=templates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["../../src/cli/templates.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,+BAA+B,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2D9C,CAAC;AAEF,MAAM,CAAC,MAAM,6BAA6B,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4F5C,CAAC;AAEF,MAAM,CAAC,MAAM,6BAA6B,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8B5C,CAAC;AAEF,MAAM,CAAC,MAAM,gCAAgC,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyH/C,CAAC;AAEF,MAAM,CAAC,MAAM,8BAA8B,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmD7C,CAAC;AAEF;;GAEG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO;QACL,oBAAoB,EAAE,+BAA+B;QACrD,kBAAkB,EAAE,6BAA6B;QACjD,kBAAkB,EAAE,6BAA6B;QACjD,qBAAqB,EAAE,gCAAgC;QACvD,mBAAmB,EAAE,8BAA8B;KACpD,CAAC;AACJ,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * micro-contracts CLI
4
+ *
5
+ * A contract-first OpenAPI toolchain that keeps TypeScript UI
6
+ * and microservices aligned via code generation.
7
+ */
8
+ export {};
9
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;GAKG"}