@unrdf/project-engine 5.0.1 → 26.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +16 -15
- package/src/golden-structure.mjs +2 -2
- package/src/materialize-apply.mjs +2 -2
- package/README.md +0 -53
- package/src/api-contract-validator.mjs +0 -711
- package/src/auto-test-generator.mjs +0 -444
- package/src/autonomic-mapek.mjs +0 -511
- package/src/capabilities-manifest.mjs +0 -125
- package/src/code-complexity-js.mjs +0 -368
- package/src/dependency-graph.mjs +0 -276
- package/src/doc-drift-checker.mjs +0 -172
- package/src/doc-generator.mjs +0 -229
- package/src/domain-infer.mjs +0 -966
- package/src/drift-snapshot.mjs +0 -775
- package/src/file-roles.mjs +0 -94
- package/src/fs-scan.mjs +0 -305
- package/src/gap-finder.mjs +0 -376
- package/src/hotspot-analyzer.mjs +0 -412
- package/src/index.mjs +0 -151
- package/src/initialize.mjs +0 -957
- package/src/lens/project-structure.mjs +0 -74
- package/src/mapek-orchestration.mjs +0 -665
- package/src/materialize-plan.mjs +0 -422
- package/src/materialize.mjs +0 -137
- package/src/policy-derivation.mjs +0 -869
- package/src/project-config.mjs +0 -142
- package/src/project-diff.mjs +0 -28
- package/src/project-engine/build-utils.mjs +0 -237
- package/src/project-engine/code-analyzer.mjs +0 -248
- package/src/project-engine/doc-generator.mjs +0 -407
- package/src/project-engine/infrastructure.mjs +0 -213
- package/src/project-engine/metrics.mjs +0 -146
- package/src/project-model.mjs +0 -111
- package/src/project-report.mjs +0 -348
- package/src/refactoring-guide.mjs +0 -242
- package/src/stack-detect.mjs +0 -102
- package/src/stack-linter.mjs +0 -213
- package/src/template-infer.mjs +0 -674
- package/src/type-auditor.mjs +0 -609
|
@@ -1,711 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file API Contract Validator - validates API files against domain schemas
|
|
3
|
-
* @module project-engine/api-contract-validator
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { z } from 'zod';
|
|
7
|
-
import { UnrdfDataFactory as DataFactory } from '@unrdf/core/rdf/n3-justified-only';
|
|
8
|
-
|
|
9
|
-
const { namedNode } = DataFactory;
|
|
10
|
-
|
|
11
|
-
/* ========================================================================= */
|
|
12
|
-
/* Namespace prefixes */
|
|
13
|
-
/* ========================================================================= */
|
|
14
|
-
|
|
15
|
-
const NS = {
|
|
16
|
-
rdf: 'http://www.w3.org/1999/02/22-rdf-syntax-ns#',
|
|
17
|
-
rdfs: 'http://www.w3.org/2000/01/rdf-schema#',
|
|
18
|
-
xsd: 'http://www.w3.org/2001/XMLSchema#',
|
|
19
|
-
dom: 'http://example.org/unrdf/domain#',
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
/* ========================================================================= */
|
|
23
|
-
/* Zod Schemas */
|
|
24
|
-
/* ========================================================================= */
|
|
25
|
-
|
|
26
|
-
const FieldSchemaSchema = z.object({
|
|
27
|
-
name: z.string(),
|
|
28
|
-
type: z.string(),
|
|
29
|
-
optional: z.boolean().default(false),
|
|
30
|
-
array: z.boolean().default(false),
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
const EntitySchemaSchema = z.object({
|
|
34
|
-
entityName: z.string(),
|
|
35
|
-
fields: z.array(FieldSchemaSchema),
|
|
36
|
-
zodSchema: z.string().optional(),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const ViolationSchema = z.object({
|
|
40
|
-
type: z.enum([
|
|
41
|
-
'missing_field',
|
|
42
|
-
'extra_field',
|
|
43
|
-
'type_mismatch',
|
|
44
|
-
'optionality_mismatch',
|
|
45
|
-
'missing_validation',
|
|
46
|
-
'missing_response_field',
|
|
47
|
-
]),
|
|
48
|
-
field: z.string(),
|
|
49
|
-
expected: z.string().optional(),
|
|
50
|
-
actual: z.string().optional(),
|
|
51
|
-
severity: z.enum(['low', 'medium', 'high', 'critical']),
|
|
52
|
-
message: z.string(),
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const ValidationResultSchema = z.object({
|
|
56
|
-
violations: z.array(ViolationSchema),
|
|
57
|
-
coverage: z.number().min(0).max(100),
|
|
58
|
-
breaking: z.boolean(),
|
|
59
|
-
summary: z.string(),
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const BreakingChangeSchema = z.object({
|
|
63
|
-
type: z.enum(['field_removed', 'field_required', 'type_changed', 'entity_removed']),
|
|
64
|
-
entity: z.string(),
|
|
65
|
-
field: z.string().optional(),
|
|
66
|
-
oldValue: z.string().optional(),
|
|
67
|
-
newValue: z.string().optional(),
|
|
68
|
-
message: z.string(),
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const ContractBreaksSchema = z.object({
|
|
72
|
-
breakingChanges: z.array(BreakingChangeSchema),
|
|
73
|
-
affectedAPIs: z.array(z.string()),
|
|
74
|
-
safe: z.boolean(),
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* @typedef {z.infer<typeof FieldSchemaSchema>} FieldSchema
|
|
79
|
-
* @typedef {z.infer<typeof EntitySchemaSchema>} EntitySchema
|
|
80
|
-
* @typedef {z.infer<typeof ViolationSchema>} Violation
|
|
81
|
-
* @typedef {z.infer<typeof ValidationResultSchema>} ValidationResult
|
|
82
|
-
* @typedef {z.infer<typeof BreakingChangeSchema>} BreakingChange
|
|
83
|
-
* @typedef {z.infer<typeof ContractBreaksSchema>} ContractBreaks
|
|
84
|
-
*/
|
|
85
|
-
|
|
86
|
-
/* ========================================================================= */
|
|
87
|
-
/* Type mapping from XSD to Zod */
|
|
88
|
-
/* ========================================================================= */
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Map XSD type to Zod type name
|
|
92
|
-
* @param {string} xsdType
|
|
93
|
-
* @returns {string}
|
|
94
|
-
*/
|
|
95
|
-
function xsdToZodType(xsdType) {
|
|
96
|
-
const typeMap = {
|
|
97
|
-
[`${NS.xsd}string`]: 'z.string()',
|
|
98
|
-
[`${NS.xsd}integer`]: 'z.number()',
|
|
99
|
-
[`${NS.xsd}decimal`]: 'z.number()',
|
|
100
|
-
[`${NS.xsd}float`]: 'z.number()',
|
|
101
|
-
[`${NS.xsd}double`]: 'z.number()',
|
|
102
|
-
[`${NS.xsd}boolean`]: 'z.boolean()',
|
|
103
|
-
[`${NS.xsd}date`]: 'z.string().date()',
|
|
104
|
-
[`${NS.xsd}dateTime`]: 'z.string().datetime()',
|
|
105
|
-
[`${NS.xsd}anyURI`]: 'z.string().url()',
|
|
106
|
-
string: 'z.string()',
|
|
107
|
-
number: 'z.number()',
|
|
108
|
-
boolean: 'z.boolean()',
|
|
109
|
-
date: 'z.string().date()',
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
return typeMap[xsdType] || 'z.string()';
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Map type string to simple type name for comparison
|
|
117
|
-
* @param {string} xsdType
|
|
118
|
-
* @returns {string}
|
|
119
|
-
*/
|
|
120
|
-
function normalizeTypeName(xsdType) {
|
|
121
|
-
if (
|
|
122
|
-
xsdType.includes('integer') ||
|
|
123
|
-
xsdType.includes('decimal') ||
|
|
124
|
-
xsdType.includes('float') ||
|
|
125
|
-
xsdType.includes('double')
|
|
126
|
-
) {
|
|
127
|
-
return 'number';
|
|
128
|
-
}
|
|
129
|
-
if (xsdType.includes('boolean')) {
|
|
130
|
-
return 'boolean';
|
|
131
|
-
}
|
|
132
|
-
if (xsdType.includes('date')) {
|
|
133
|
-
return 'date';
|
|
134
|
-
}
|
|
135
|
-
return 'string';
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/* ========================================================================= */
|
|
139
|
-
/* Domain store extraction */
|
|
140
|
-
/* ========================================================================= */
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Extract entity fields from domain model store
|
|
144
|
-
* @param {import('n3').Store} domainStore
|
|
145
|
-
* @param {string} entityName
|
|
146
|
-
* @param {string} [baseIri]
|
|
147
|
-
* @returns {FieldSchema[]}
|
|
148
|
-
*/
|
|
149
|
-
function extractEntityFields(
|
|
150
|
-
domainStore,
|
|
151
|
-
entityName,
|
|
152
|
-
baseIri = 'http://example.org/unrdf/domain#'
|
|
153
|
-
) {
|
|
154
|
-
/** @type {FieldSchema[]} */
|
|
155
|
-
const fields = [];
|
|
156
|
-
|
|
157
|
-
const entityIri = `${baseIri}${entityName}`;
|
|
158
|
-
|
|
159
|
-
// Get all fields for this entity
|
|
160
|
-
const fieldQuads = domainStore.getQuads(
|
|
161
|
-
namedNode(entityIri),
|
|
162
|
-
namedNode(`${NS.dom}hasField`),
|
|
163
|
-
null
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
for (const fieldQuad of fieldQuads) {
|
|
167
|
-
const fieldIri = fieldQuad.object.value;
|
|
168
|
-
|
|
169
|
-
// Extract field name
|
|
170
|
-
const nameQuads = domainStore.getQuads(
|
|
171
|
-
namedNode(fieldIri),
|
|
172
|
-
namedNode(`${NS.dom}fieldName`),
|
|
173
|
-
null
|
|
174
|
-
);
|
|
175
|
-
const fieldName = nameQuads[0]?.object.value || fieldIri.split('.').pop() || '';
|
|
176
|
-
|
|
177
|
-
// Extract field type
|
|
178
|
-
const typeQuads = domainStore.getQuads(
|
|
179
|
-
namedNode(fieldIri),
|
|
180
|
-
namedNode(`${NS.dom}fieldType`),
|
|
181
|
-
null
|
|
182
|
-
);
|
|
183
|
-
const fieldType = normalizeTypeName(typeQuads[0]?.object.value || 'string');
|
|
184
|
-
|
|
185
|
-
// Extract optional flag
|
|
186
|
-
const optionalQuads = domainStore.getQuads(
|
|
187
|
-
namedNode(fieldIri),
|
|
188
|
-
namedNode(`${NS.dom}isOptional`),
|
|
189
|
-
null
|
|
190
|
-
);
|
|
191
|
-
const isOptional = optionalQuads[0]?.object.value === 'true';
|
|
192
|
-
|
|
193
|
-
// Extract array flag
|
|
194
|
-
const arrayQuads = domainStore.getQuads(
|
|
195
|
-
namedNode(fieldIri),
|
|
196
|
-
namedNode(`${NS.dom}isArray`),
|
|
197
|
-
null
|
|
198
|
-
);
|
|
199
|
-
const isArray = arrayQuads[0]?.object.value === 'true';
|
|
200
|
-
|
|
201
|
-
fields.push({
|
|
202
|
-
name: fieldName,
|
|
203
|
-
type: fieldType,
|
|
204
|
-
optional: isOptional,
|
|
205
|
-
array: isArray,
|
|
206
|
-
});
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return fields;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* Get all entity names from domain store
|
|
214
|
-
* @param {import('n3').Store} domainStore
|
|
215
|
-
* @param {string} [baseIri]
|
|
216
|
-
* @returns {string[]}
|
|
217
|
-
*/
|
|
218
|
-
function getEntityNames(domainStore, baseIri = 'http://example.org/unrdf/domain#') {
|
|
219
|
-
const entityQuads = domainStore.getQuads(
|
|
220
|
-
null,
|
|
221
|
-
namedNode(`${NS.rdf}type`),
|
|
222
|
-
namedNode(`${NS.dom}Entity`)
|
|
223
|
-
);
|
|
224
|
-
|
|
225
|
-
return entityQuads.map(quad => {
|
|
226
|
-
const iri = quad.subject.value;
|
|
227
|
-
return iri.replace(baseIri, '');
|
|
228
|
-
});
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/* ========================================================================= */
|
|
232
|
-
/* API file parsing */
|
|
233
|
-
/* ========================================================================= */
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Extract request/response fields from Next.js API route
|
|
237
|
-
* @param {string} content
|
|
238
|
-
* @returns {{request: string[], response: string[], validations: string[]}}
|
|
239
|
-
*/
|
|
240
|
-
function parseNextJsRoute(content) {
|
|
241
|
-
const request = [];
|
|
242
|
-
const response = [];
|
|
243
|
-
const validations = [];
|
|
244
|
-
|
|
245
|
-
// Match destructuring from request body: const { field1, field2 } = await req.json()
|
|
246
|
-
const bodyPattern =
|
|
247
|
-
/(?:const|let)\s*\{\s*([^}]+)\s*\}\s*=\s*(?:await\s+)?(?:req\.json|request\.json|body)/g;
|
|
248
|
-
let match;
|
|
249
|
-
while ((match = bodyPattern.exec(content)) !== null) {
|
|
250
|
-
const fields = match[1].split(',').map(f => f.trim().split(':')[0].trim());
|
|
251
|
-
request.push(...fields.filter(f => f && !f.startsWith('...')));
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Match direct body property access: body.fieldName, req.body.fieldName
|
|
255
|
-
const directBodyPattern = /(?:body|req\.body)\.(\w+)/g;
|
|
256
|
-
while ((match = directBodyPattern.exec(content)) !== null) {
|
|
257
|
-
if (!request.includes(match[1])) {
|
|
258
|
-
request.push(match[1]);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Match NextResponse.json({ ... }) or res.json({ ... })
|
|
263
|
-
const responsePattern =
|
|
264
|
-
/(?:NextResponse\.json|Response\.json|res\.(?:json|send))\s*\(\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g;
|
|
265
|
-
while ((match = responsePattern.exec(content)) !== null) {
|
|
266
|
-
const responseBlock = match[1];
|
|
267
|
-
// Extract field names from response object
|
|
268
|
-
const fieldPattern = /(\w+)\s*:/g;
|
|
269
|
-
let fieldMatch;
|
|
270
|
-
while ((fieldMatch = fieldPattern.exec(responseBlock)) !== null) {
|
|
271
|
-
if (!response.includes(fieldMatch[1])) {
|
|
272
|
-
response.push(fieldMatch[1]);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Match Zod schema validation: schema.parse(), schema.safeParse()
|
|
278
|
-
const zodPattern = /(\w+)Schema\.(?:parse|safeParse|parseAsync)/g;
|
|
279
|
-
while ((match = zodPattern.exec(content)) !== null) {
|
|
280
|
-
validations.push(match[1]);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Match z.object validation inline
|
|
284
|
-
const zodInlinePattern = /z\.object\(\s*\{([^}]+)\}\s*\)\.(?:parse|safeParse)/g;
|
|
285
|
-
while ((match = zodInlinePattern.exec(content)) !== null) {
|
|
286
|
-
validations.push('inline');
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
return { request, response, validations };
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/**
|
|
293
|
-
* Extract request/response fields from Express route handler
|
|
294
|
-
* @param {string} content
|
|
295
|
-
* @returns {{request: string[], response: string[], validations: string[]}}
|
|
296
|
-
*/
|
|
297
|
-
function parseExpressRoute(content) {
|
|
298
|
-
const request = [];
|
|
299
|
-
const response = [];
|
|
300
|
-
const validations = [];
|
|
301
|
-
|
|
302
|
-
// Match destructuring from req.body: const { field1, field2 } = req.body
|
|
303
|
-
const bodyPattern = /(?:const|let)\s*\{\s*([^}]+)\s*\}\s*=\s*req\.body/g;
|
|
304
|
-
let match;
|
|
305
|
-
while ((match = bodyPattern.exec(content)) !== null) {
|
|
306
|
-
const fields = match[1].split(',').map(f => f.trim().split(':')[0].trim());
|
|
307
|
-
request.push(...fields.filter(f => f && !f.startsWith('...')));
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
// Match direct req.body property access
|
|
311
|
-
const directBodyPattern = /req\.body\.(\w+)/g;
|
|
312
|
-
while ((match = directBodyPattern.exec(content)) !== null) {
|
|
313
|
-
if (!request.includes(match[1])) {
|
|
314
|
-
request.push(match[1]);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Match res.json({ ... }) or res.send({ ... })
|
|
319
|
-
const responsePattern = /res\.(?:json|send)\s*\(\s*\{([^}]+(?:\{[^}]*\}[^}]*)*)\}/g;
|
|
320
|
-
while ((match = responsePattern.exec(content)) !== null) {
|
|
321
|
-
const responseBlock = match[1];
|
|
322
|
-
const fieldPattern = /(\w+)\s*:/g;
|
|
323
|
-
let fieldMatch;
|
|
324
|
-
while ((fieldMatch = fieldPattern.exec(responseBlock)) !== null) {
|
|
325
|
-
if (!response.includes(fieldMatch[1])) {
|
|
326
|
-
response.push(fieldMatch[1]);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
// Match Zod/Joi validation
|
|
332
|
-
const zodPattern = /(\w+)Schema\.(?:parse|safeParse|validate)/g;
|
|
333
|
-
while ((match = zodPattern.exec(content)) !== null) {
|
|
334
|
-
validations.push(match[1]);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
// Match express-validator
|
|
338
|
-
const validatorPattern = /body\(['"](\w+)['"]\)/g;
|
|
339
|
-
while ((match = validatorPattern.exec(content)) !== null) {
|
|
340
|
-
validations.push(match[1]);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
return { request, response, validations };
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Parse API file content and extract request/response fields
|
|
348
|
-
* @param {string} content
|
|
349
|
-
* @param {string} [framework]
|
|
350
|
-
* @returns {{request: string[], response: string[], validations: string[]}}
|
|
351
|
-
*/
|
|
352
|
-
function parseAPIFile(content, framework) {
|
|
353
|
-
// Auto-detect framework if not specified
|
|
354
|
-
if (!framework) {
|
|
355
|
-
if (
|
|
356
|
-
content.includes('NextResponse') ||
|
|
357
|
-
content.includes('NextRequest') ||
|
|
358
|
-
content.includes('export async function')
|
|
359
|
-
) {
|
|
360
|
-
framework = 'nextjs';
|
|
361
|
-
} else if (
|
|
362
|
-
content.includes('express') ||
|
|
363
|
-
content.includes('req, res') ||
|
|
364
|
-
content.includes('Router')
|
|
365
|
-
) {
|
|
366
|
-
framework = 'express';
|
|
367
|
-
} else {
|
|
368
|
-
framework = 'nextjs'; // Default
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (framework === 'nextjs') {
|
|
373
|
-
return parseNextJsRoute(content);
|
|
374
|
-
} else {
|
|
375
|
-
return parseExpressRoute(content);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/* ========================================================================= */
|
|
380
|
-
/* Public API: generateAPISchema */
|
|
381
|
-
/* ========================================================================= */
|
|
382
|
-
|
|
383
|
-
/**
|
|
384
|
-
* Generate API schema from domain store for an entity
|
|
385
|
-
*
|
|
386
|
-
* @param {import('n3').Store} domainStore - Domain model RDF store
|
|
387
|
-
* @param {string} entity - Entity name to generate schema for
|
|
388
|
-
* @param {Object} [options]
|
|
389
|
-
* @param {string} [options.baseIri] - Base IRI for domain resources
|
|
390
|
-
* @returns {EntitySchema}
|
|
391
|
-
*/
|
|
392
|
-
export function generateAPISchema(domainStore, entity, options = {}) {
|
|
393
|
-
const { baseIri = 'http://example.org/unrdf/domain#' } = options;
|
|
394
|
-
|
|
395
|
-
const fields = extractEntityFields(domainStore, entity, baseIri);
|
|
396
|
-
|
|
397
|
-
// Generate Zod schema string
|
|
398
|
-
const fieldDefs = fields.map(f => {
|
|
399
|
-
let zodType = xsdToZodType(f.type);
|
|
400
|
-
if (f.array) {
|
|
401
|
-
zodType = `z.array(${zodType})`;
|
|
402
|
-
}
|
|
403
|
-
if (f.optional) {
|
|
404
|
-
zodType = `${zodType}.optional()`;
|
|
405
|
-
}
|
|
406
|
-
return ` ${f.name}: ${zodType}`;
|
|
407
|
-
});
|
|
408
|
-
|
|
409
|
-
const zodSchema = `const ${entity}Schema = z.object({\n${fieldDefs.join(',\n')}\n})`;
|
|
410
|
-
|
|
411
|
-
return EntitySchemaSchema.parse({
|
|
412
|
-
entityName: entity,
|
|
413
|
-
fields,
|
|
414
|
-
zodSchema,
|
|
415
|
-
});
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
/**
|
|
419
|
-
* Generate API schemas for all entities in domain store
|
|
420
|
-
*
|
|
421
|
-
* @param {import('n3').Store} domainStore
|
|
422
|
-
* @param {Object} [options]
|
|
423
|
-
* @param {string} [options.baseIri]
|
|
424
|
-
* @returns {EntitySchema[]}
|
|
425
|
-
*/
|
|
426
|
-
export function generateAllAPISchemas(domainStore, options = {}) {
|
|
427
|
-
const { baseIri = 'http://example.org/unrdf/domain#' } = options;
|
|
428
|
-
|
|
429
|
-
const entityNames = getEntityNames(domainStore, baseIri);
|
|
430
|
-
|
|
431
|
-
return entityNames.map(entity => generateAPISchema(domainStore, entity, options));
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/* ========================================================================= */
|
|
435
|
-
/* Public API: validateAPIFiles */
|
|
436
|
-
/* ========================================================================= */
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* Validate API files against expected schema
|
|
440
|
-
*
|
|
441
|
-
* @param {Array<{path: string, content: string}>} apiFiles - API file contents
|
|
442
|
-
* @param {EntitySchema} expectedSchema - Expected schema from domain model
|
|
443
|
-
* @param {Object} [options]
|
|
444
|
-
* @param {string} [options.framework] - 'nextjs' or 'express'
|
|
445
|
-
* @returns {ValidationResult}
|
|
446
|
-
*/
|
|
447
|
-
export function validateAPIFiles(apiFiles, expectedSchema, options = {}) {
|
|
448
|
-
const { framework } = options;
|
|
449
|
-
|
|
450
|
-
/** @type {Violation[]} */
|
|
451
|
-
const violations = [];
|
|
452
|
-
const expectedFields = new Set(expectedSchema.fields.map(f => f.name));
|
|
453
|
-
const foundRequestFields = new Set();
|
|
454
|
-
const foundResponseFields = new Set();
|
|
455
|
-
let hasValidation = false;
|
|
456
|
-
|
|
457
|
-
for (const file of apiFiles) {
|
|
458
|
-
const parsed = parseAPIFile(file.content, framework);
|
|
459
|
-
|
|
460
|
-
// Track found fields
|
|
461
|
-
for (const field of parsed.request) {
|
|
462
|
-
foundRequestFields.add(field);
|
|
463
|
-
}
|
|
464
|
-
for (const field of parsed.response) {
|
|
465
|
-
foundResponseFields.add(field);
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// Check for validation
|
|
469
|
-
if (parsed.validations.length > 0) {
|
|
470
|
-
hasValidation = true;
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
// Check for extra fields not in schema
|
|
474
|
-
for (const field of parsed.request) {
|
|
475
|
-
if (!expectedFields.has(field)) {
|
|
476
|
-
violations.push({
|
|
477
|
-
type: 'extra_field',
|
|
478
|
-
field,
|
|
479
|
-
expected: 'not defined',
|
|
480
|
-
actual: 'present in request',
|
|
481
|
-
severity: 'medium',
|
|
482
|
-
message: `Field "${field}" in API request is not defined in domain schema`,
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Check for missing required fields
|
|
489
|
-
for (const field of expectedSchema.fields) {
|
|
490
|
-
if (!field.optional && !foundRequestFields.has(field.name)) {
|
|
491
|
-
violations.push({
|
|
492
|
-
type: 'missing_field',
|
|
493
|
-
field: field.name,
|
|
494
|
-
expected: 'required',
|
|
495
|
-
actual: 'not found in API',
|
|
496
|
-
severity: 'high',
|
|
497
|
-
message: `Required field "${field.name}" not found in API request handling`,
|
|
498
|
-
});
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
// Check for missing response fields
|
|
503
|
-
for (const field of expectedSchema.fields) {
|
|
504
|
-
if (!foundResponseFields.has(field.name) && !field.optional) {
|
|
505
|
-
violations.push({
|
|
506
|
-
type: 'missing_response_field',
|
|
507
|
-
field: field.name,
|
|
508
|
-
expected: 'in response',
|
|
509
|
-
actual: 'not found',
|
|
510
|
-
severity: 'medium',
|
|
511
|
-
message: `Field "${field.name}" not found in API response`,
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
// Check for missing validation
|
|
517
|
-
if (!hasValidation && apiFiles.length > 0) {
|
|
518
|
-
violations.push({
|
|
519
|
-
type: 'missing_validation',
|
|
520
|
-
field: 'request body',
|
|
521
|
-
expected: 'schema validation',
|
|
522
|
-
actual: 'no validation found',
|
|
523
|
-
severity: 'high',
|
|
524
|
-
message: 'No schema validation found in API handlers',
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
// Calculate coverage
|
|
529
|
-
const totalExpectedFields = expectedSchema.fields.length;
|
|
530
|
-
const coveredFields = expectedSchema.fields.filter(
|
|
531
|
-
f => foundRequestFields.has(f.name) || foundResponseFields.has(f.name)
|
|
532
|
-
).length;
|
|
533
|
-
const coverage =
|
|
534
|
-
totalExpectedFields > 0 ? Math.round((coveredFields / totalExpectedFields) * 100) : 100;
|
|
535
|
-
|
|
536
|
-
// Determine if breaking
|
|
537
|
-
const breaking = violations.some(
|
|
538
|
-
v => v.severity === 'critical' || (v.severity === 'high' && v.type === 'missing_field')
|
|
539
|
-
);
|
|
540
|
-
|
|
541
|
-
// Generate summary
|
|
542
|
-
const criticalCount = violations.filter(v => v.severity === 'critical').length;
|
|
543
|
-
const highCount = violations.filter(v => v.severity === 'high').length;
|
|
544
|
-
let summary = `${violations.length} violations found`;
|
|
545
|
-
if (criticalCount > 0) {
|
|
546
|
-
summary += ` (${criticalCount} critical)`;
|
|
547
|
-
} else if (highCount > 0) {
|
|
548
|
-
summary += ` (${highCount} high severity)`;
|
|
549
|
-
}
|
|
550
|
-
if (violations.length === 0) {
|
|
551
|
-
summary = 'API contract is valid';
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
return ValidationResultSchema.parse({
|
|
555
|
-
violations,
|
|
556
|
-
coverage,
|
|
557
|
-
breaking,
|
|
558
|
-
summary,
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
/* ========================================================================= */
|
|
563
|
-
/* Public API: detectContractBreaks */
|
|
564
|
-
/* ========================================================================= */
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* Detect breaking changes between old and new schemas
|
|
568
|
-
*
|
|
569
|
-
* @param {EntitySchema} oldSchema - Previous version schema
|
|
570
|
-
* @param {EntitySchema} newSchema - New version schema
|
|
571
|
-
* @param {Array<{path: string, content: string}>} [implementations] - API implementation files
|
|
572
|
-
* @returns {ContractBreaks}
|
|
573
|
-
*/
|
|
574
|
-
export function detectContractBreaks(oldSchema, newSchema, implementations = []) {
|
|
575
|
-
/** @type {BreakingChange[]} */
|
|
576
|
-
const breakingChanges = [];
|
|
577
|
-
/** @type {Set<string>} */
|
|
578
|
-
const affectedAPIs = new Set();
|
|
579
|
-
|
|
580
|
-
const oldFields = new Map(oldSchema.fields.map(f => [f.name, f]));
|
|
581
|
-
const newFields = new Map(newSchema.fields.map(f => [f.name, f]));
|
|
582
|
-
|
|
583
|
-
// Check for removed fields (breaking)
|
|
584
|
-
for (const [fieldName, oldField] of oldFields) {
|
|
585
|
-
if (!newFields.has(fieldName)) {
|
|
586
|
-
breakingChanges.push({
|
|
587
|
-
type: 'field_removed',
|
|
588
|
-
entity: oldSchema.entityName,
|
|
589
|
-
field: fieldName,
|
|
590
|
-
oldValue: oldField.type,
|
|
591
|
-
newValue: undefined,
|
|
592
|
-
message: `Field "${fieldName}" was removed from ${oldSchema.entityName}`,
|
|
593
|
-
});
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// Check for fields that became required (breaking)
|
|
598
|
-
for (const [fieldName, newField] of newFields) {
|
|
599
|
-
const oldField = oldFields.get(fieldName);
|
|
600
|
-
if (oldField && oldField.optional && !newField.optional) {
|
|
601
|
-
breakingChanges.push({
|
|
602
|
-
type: 'field_required',
|
|
603
|
-
entity: newSchema.entityName,
|
|
604
|
-
field: fieldName,
|
|
605
|
-
oldValue: 'optional',
|
|
606
|
-
newValue: 'required',
|
|
607
|
-
message: `Field "${fieldName}" changed from optional to required in ${newSchema.entityName}`,
|
|
608
|
-
});
|
|
609
|
-
}
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
// Check for type changes (breaking)
|
|
613
|
-
for (const [fieldName, newField] of newFields) {
|
|
614
|
-
const oldField = oldFields.get(fieldName);
|
|
615
|
-
if (oldField && oldField.type !== newField.type) {
|
|
616
|
-
breakingChanges.push({
|
|
617
|
-
type: 'type_changed',
|
|
618
|
-
entity: newSchema.entityName,
|
|
619
|
-
field: fieldName,
|
|
620
|
-
oldValue: oldField.type,
|
|
621
|
-
newValue: newField.type,
|
|
622
|
-
message: `Field "${fieldName}" type changed from ${oldField.type} to ${newField.type} in ${newSchema.entityName}`,
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
// Find affected API files
|
|
628
|
-
for (const impl of implementations) {
|
|
629
|
-
const parsed = parseAPIFile(impl.content);
|
|
630
|
-
const allFields = [...parsed.request, ...parsed.response];
|
|
631
|
-
|
|
632
|
-
for (const change of breakingChanges) {
|
|
633
|
-
if (change.field && allFields.includes(change.field)) {
|
|
634
|
-
affectedAPIs.add(impl.path);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
return ContractBreaksSchema.parse({
|
|
640
|
-
breakingChanges,
|
|
641
|
-
affectedAPIs: Array.from(affectedAPIs),
|
|
642
|
-
safe: breakingChanges.length === 0,
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
|
-
|
|
646
|
-
/**
|
|
647
|
-
* Compare two domain stores and detect API contract breaks
|
|
648
|
-
*
|
|
649
|
-
* @param {import('n3').Store} oldStore - Previous domain model
|
|
650
|
-
* @param {import('n3').Store} newStore - New domain model
|
|
651
|
-
* @param {Array<{path: string, content: string}>} [implementations] - API files
|
|
652
|
-
* @param {Object} [options]
|
|
653
|
-
* @param {string} [options.baseIri]
|
|
654
|
-
* @returns {Map<string, ContractBreaks>}
|
|
655
|
-
*/
|
|
656
|
-
export function detectAllContractBreaks(oldStore, newStore, implementations = [], options = {}) {
|
|
657
|
-
const { baseIri = 'http://example.org/unrdf/domain#' } = options;
|
|
658
|
-
|
|
659
|
-
const oldEntities = getEntityNames(oldStore, baseIri);
|
|
660
|
-
const newEntities = getEntityNames(newStore, baseIri);
|
|
661
|
-
|
|
662
|
-
const allEntities = new Set([...oldEntities, ...newEntities]);
|
|
663
|
-
/** @type {Map<string, ContractBreaks>} */
|
|
664
|
-
const results = new Map();
|
|
665
|
-
|
|
666
|
-
for (const entity of allEntities) {
|
|
667
|
-
const oldSchema = oldEntities.includes(entity)
|
|
668
|
-
? generateAPISchema(oldStore, entity, options)
|
|
669
|
-
: { entityName: entity, fields: [], zodSchema: '' };
|
|
670
|
-
|
|
671
|
-
const newSchema = newEntities.includes(entity)
|
|
672
|
-
? generateAPISchema(newStore, entity, options)
|
|
673
|
-
: { entityName: entity, fields: [], zodSchema: '' };
|
|
674
|
-
|
|
675
|
-
// Check for entity removal
|
|
676
|
-
if (oldEntities.includes(entity) && !newEntities.includes(entity)) {
|
|
677
|
-
results.set(entity, {
|
|
678
|
-
breakingChanges: [
|
|
679
|
-
{
|
|
680
|
-
type: 'entity_removed',
|
|
681
|
-
entity,
|
|
682
|
-
message: `Entity "${entity}" was removed`,
|
|
683
|
-
},
|
|
684
|
-
],
|
|
685
|
-
affectedAPIs: implementations.map(i => i.path),
|
|
686
|
-
safe: false,
|
|
687
|
-
});
|
|
688
|
-
continue;
|
|
689
|
-
}
|
|
690
|
-
|
|
691
|
-
const breaks = detectContractBreaks(oldSchema, newSchema, implementations);
|
|
692
|
-
if (breaks.breakingChanges.length > 0) {
|
|
693
|
-
results.set(entity, breaks);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
return results;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/* ========================================================================= */
|
|
701
|
-
/* Exports for module */
|
|
702
|
-
/* ========================================================================= */
|
|
703
|
-
|
|
704
|
-
export {
|
|
705
|
-
FieldSchemaSchema,
|
|
706
|
-
EntitySchemaSchema,
|
|
707
|
-
ViolationSchema,
|
|
708
|
-
ValidationResultSchema,
|
|
709
|
-
BreakingChangeSchema,
|
|
710
|
-
ContractBreaksSchema,
|
|
711
|
-
};
|