@schmock/openapi 1.1.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/dist/crud-detector.d.ts +35 -0
- package/dist/crud-detector.d.ts.map +1 -0
- package/dist/crud-detector.js +153 -0
- package/dist/generators.d.ts +14 -0
- package/dist/generators.d.ts.map +1 -0
- package/dist/generators.js +158 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +221 -0
- package/dist/normalizer.d.ts +14 -0
- package/dist/normalizer.d.ts.map +1 -0
- package/dist/normalizer.js +194 -0
- package/dist/parser.d.ts +32 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +282 -0
- package/dist/plugin.d.ts +32 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +129 -0
- package/dist/seed.d.ts +15 -0
- package/dist/seed.d.ts.map +1 -0
- package/dist/seed.js +41 -0
- package/package.json +45 -0
- package/src/__fixtures__/faker-stress-test.openapi.yaml +1030 -0
- package/src/__fixtures__/openapi31.json +34 -0
- package/src/__fixtures__/petstore-openapi3.json +168 -0
- package/src/__fixtures__/petstore-swagger2.json +141 -0
- package/src/__fixtures__/scalar-galaxy.yaml +1314 -0
- package/src/__fixtures__/stripe-fixtures3.json +6542 -0
- package/src/__fixtures__/stripe-spec3.yaml +161621 -0
- package/src/__fixtures__/train-travel.yaml +1264 -0
- package/src/crud-detector.test.ts +150 -0
- package/src/crud-detector.ts +194 -0
- package/src/generators.test.ts +214 -0
- package/src/generators.ts +212 -0
- package/src/index.ts +4 -0
- package/src/normalizer.test.ts +253 -0
- package/src/normalizer.ts +233 -0
- package/src/parser.test.ts +181 -0
- package/src/parser.ts +389 -0
- package/src/plugin.test.ts +205 -0
- package/src/plugin.ts +185 -0
- package/src/seed.ts +62 -0
- package/src/steps/openapi-crud.steps.ts +132 -0
- package/src/steps/openapi-parsing.steps.ts +111 -0
- package/src/steps/openapi-seed.steps.ts +94 -0
- package/src/stress.test.ts +2814 -0
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/// <reference path="../../../types/schmock.d.ts" />
|
|
2
|
+
import { detectCrudResources } from "./crud-detector.js";
|
|
3
|
+
import { createCreateGenerator, createDeleteGenerator, createListGenerator, createReadGenerator, createStaticGenerator, createUpdateGenerator, } from "./generators.js";
|
|
4
|
+
import { parseSpec } from "./parser.js";
|
|
5
|
+
import { loadSeed } from "./seed.js";
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Create an OpenAPI plugin that auto-registers CRUD routes from a spec.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const mock = schmock();
|
|
15
|
+
* mock.pipe(await openapi({
|
|
16
|
+
* spec: "./petstore.yaml",
|
|
17
|
+
* seed: { pets: { count: 10 } },
|
|
18
|
+
* }));
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export async function openapi(options) {
|
|
22
|
+
const spec = await parseSpec(options.spec);
|
|
23
|
+
const { resources, nonCrudPaths } = detectCrudResources(spec.paths);
|
|
24
|
+
const seedData = options.seed
|
|
25
|
+
? loadSeed(options.seed, resources)
|
|
26
|
+
: new Map();
|
|
27
|
+
return {
|
|
28
|
+
name: "@schmock/openapi",
|
|
29
|
+
version: "1.0.0",
|
|
30
|
+
install(instance) {
|
|
31
|
+
// Seed initial state — we need state to be initialized before registering routes
|
|
32
|
+
// The state is populated via generator context.state on first request,
|
|
33
|
+
// but we need to pre-populate it. We'll use a global config state approach.
|
|
34
|
+
// Since generators use ctx.state (the global config state), we set up seed
|
|
35
|
+
// data through a setup route that runs on first access, or we just seed
|
|
36
|
+
// in the generators themselves.
|
|
37
|
+
// Register CRUD routes
|
|
38
|
+
for (const resource of resources) {
|
|
39
|
+
registerCrudRoutes(instance, resource, seedData.get(resource.name));
|
|
40
|
+
}
|
|
41
|
+
// Register non-CRUD routes with static generators
|
|
42
|
+
for (const parsedPath of nonCrudPaths) {
|
|
43
|
+
const routeKey = `${parsedPath.method} ${parsedPath.path}`;
|
|
44
|
+
instance(routeKey, createStaticGenerator(parsedPath));
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
process(context, response) {
|
|
48
|
+
// Pass through — generators handle everything
|
|
49
|
+
return { context, response };
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
function registerCrudRoutes(instance, resource, seedItems) {
|
|
54
|
+
// Create a seeded generator wrapper that initializes state on first call
|
|
55
|
+
const ensureSeeded = createSeeder(resource, seedItems);
|
|
56
|
+
for (const op of resource.operations) {
|
|
57
|
+
switch (op) {
|
|
58
|
+
case "list": {
|
|
59
|
+
const gen = createListGenerator(resource);
|
|
60
|
+
const routeKey = `GET ${resource.basePath}`;
|
|
61
|
+
instance(routeKey, wrapWithSeeder(ensureSeeded, gen));
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
case "create": {
|
|
65
|
+
const gen = createCreateGenerator(resource);
|
|
66
|
+
const routeKey = `POST ${resource.basePath}`;
|
|
67
|
+
instance(routeKey, wrapWithSeeder(ensureSeeded, gen));
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
case "read": {
|
|
71
|
+
const gen = createReadGenerator(resource);
|
|
72
|
+
const routeKey = `GET ${resource.itemPath}`;
|
|
73
|
+
instance(routeKey, wrapWithSeeder(ensureSeeded, gen));
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
case "update": {
|
|
77
|
+
const gen = createUpdateGenerator(resource);
|
|
78
|
+
const putKey = `PUT ${resource.itemPath}`;
|
|
79
|
+
const patchKey = `PATCH ${resource.itemPath}`;
|
|
80
|
+
instance(putKey, wrapWithSeeder(ensureSeeded, gen));
|
|
81
|
+
instance(patchKey, wrapWithSeeder(ensureSeeded, gen));
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case "delete": {
|
|
85
|
+
const gen = createDeleteGenerator(resource);
|
|
86
|
+
const routeKey = `DELETE ${resource.itemPath}`;
|
|
87
|
+
instance(routeKey, wrapWithSeeder(ensureSeeded, gen));
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Create a seeder function that initializes collection state once.
|
|
95
|
+
*/
|
|
96
|
+
function createSeeder(resource, seedItems) {
|
|
97
|
+
const stateKey = `openapi:collections:${resource.name}`;
|
|
98
|
+
const counterKey = `openapi:counter:${resource.name}`;
|
|
99
|
+
const seededKey = `openapi:seeded:${resource.name}`;
|
|
100
|
+
return (state) => {
|
|
101
|
+
if (state[seededKey])
|
|
102
|
+
return;
|
|
103
|
+
state[seededKey] = true;
|
|
104
|
+
if (seedItems && seedItems.length > 0) {
|
|
105
|
+
state[stateKey] = [...seedItems];
|
|
106
|
+
// Set counter to highest existing ID
|
|
107
|
+
let maxId = 0;
|
|
108
|
+
for (const item of seedItems) {
|
|
109
|
+
if (isRecord(item) && resource.idParam in item) {
|
|
110
|
+
const id = item[resource.idParam];
|
|
111
|
+
if (typeof id === "number" && id > maxId) {
|
|
112
|
+
maxId = id;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
state[counterKey] = maxId;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
state[stateKey] = [];
|
|
120
|
+
state[counterKey] = 0;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function wrapWithSeeder(seeder, generator) {
|
|
125
|
+
return (ctx) => {
|
|
126
|
+
seeder(ctx.state);
|
|
127
|
+
return generator(ctx);
|
|
128
|
+
};
|
|
129
|
+
}
|
package/dist/seed.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { CrudResource } from "./crud-detector.js";
|
|
2
|
+
export type SeedSource = unknown[] | string | {
|
|
3
|
+
count: number;
|
|
4
|
+
};
|
|
5
|
+
export type SeedConfig = Record<string, SeedSource>;
|
|
6
|
+
/**
|
|
7
|
+
* Load seed data for CRUD resources.
|
|
8
|
+
*
|
|
9
|
+
* Sources:
|
|
10
|
+
* - unknown[]: inline array of objects
|
|
11
|
+
* - string: file path to a JSON array
|
|
12
|
+
* - { count: number }: auto-generate N items from resource schema
|
|
13
|
+
*/
|
|
14
|
+
export declare function loadSeed(config: SeedConfig, resources: CrudResource[]): Map<string, unknown[]>;
|
|
15
|
+
//# sourceMappingURL=seed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"seed.d.ts","sourceRoot":"","sources":["../src/seed.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD,MAAM,MAAM,UAAU,GAAG,OAAO,EAAE,GAAG,MAAM,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAEhE,MAAM,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AAEpD;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CACtB,MAAM,EAAE,UAAU,EAClB,SAAS,EAAE,YAAY,EAAE,GACxB,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAwCxB"}
|
package/dist/seed.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/// <reference path="../../../types/schmock.d.ts" />
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { generateSeedItems } from "./generators.js";
|
|
4
|
+
/**
|
|
5
|
+
* Load seed data for CRUD resources.
|
|
6
|
+
*
|
|
7
|
+
* Sources:
|
|
8
|
+
* - unknown[]: inline array of objects
|
|
9
|
+
* - string: file path to a JSON array
|
|
10
|
+
* - { count: number }: auto-generate N items from resource schema
|
|
11
|
+
*/
|
|
12
|
+
export function loadSeed(config, resources) {
|
|
13
|
+
const result = new Map();
|
|
14
|
+
for (const [resourceName, source] of Object.entries(config)) {
|
|
15
|
+
const resource = resources.find((r) => r.name === resourceName);
|
|
16
|
+
if (Array.isArray(source)) {
|
|
17
|
+
// Inline array
|
|
18
|
+
result.set(resourceName, [...source]);
|
|
19
|
+
}
|
|
20
|
+
else if (typeof source === "string") {
|
|
21
|
+
// File path
|
|
22
|
+
const content = readFileSync(source, "utf-8");
|
|
23
|
+
const parsed = JSON.parse(content);
|
|
24
|
+
if (!Array.isArray(parsed)) {
|
|
25
|
+
throw new Error(`Seed file "${source}" for resource "${resourceName}" must contain a JSON array`);
|
|
26
|
+
}
|
|
27
|
+
result.set(resourceName, parsed);
|
|
28
|
+
}
|
|
29
|
+
else if (typeof source === "object" &&
|
|
30
|
+
source !== null &&
|
|
31
|
+
"count" in source) {
|
|
32
|
+
// Auto-generate from schema
|
|
33
|
+
if (!resource?.schema) {
|
|
34
|
+
throw new Error(`Cannot auto-generate seed for "${resourceName}": no schema found in spec`);
|
|
35
|
+
}
|
|
36
|
+
const items = generateSeedItems(resource.schema, source.count, resource.idParam);
|
|
37
|
+
result.set(resourceName, items);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return result;
|
|
41
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@schmock/openapi",
|
|
3
|
+
"description": "OpenAPI/Swagger spec-driven auto-registration plugin for Schmock",
|
|
4
|
+
"version": "1.1.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"dist",
|
|
10
|
+
"src"
|
|
11
|
+
],
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"default": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "bun build:lib && bun build:types",
|
|
22
|
+
"build:lib": "bun build --minify --outdir=dist src/index.ts",
|
|
23
|
+
"build:types": "tsc -p tsconfig.json",
|
|
24
|
+
"test": "vitest",
|
|
25
|
+
"test:watch": "vitest --watch",
|
|
26
|
+
"test:bdd": "vitest run --config vitest.config.bdd.ts",
|
|
27
|
+
"lint": "biome check src/*.ts",
|
|
28
|
+
"lint:fix": "biome check --write --unsafe src/*.ts"
|
|
29
|
+
},
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@apidevtools/swagger-parser": "^10.1.1",
|
|
33
|
+
"@schmock/schema": "workspace:*",
|
|
34
|
+
"openapi-types": "^12.1.3"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@schmock/core": "^1.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@amiceli/vitest-cucumber": "^6.2.0",
|
|
41
|
+
"@types/json-schema": "^7.0.15",
|
|
42
|
+
"@types/node": "^25.1.0",
|
|
43
|
+
"vitest": "^4.0.15"
|
|
44
|
+
}
|
|
45
|
+
}
|