@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.
Files changed (46) hide show
  1. package/dist/crud-detector.d.ts +35 -0
  2. package/dist/crud-detector.d.ts.map +1 -0
  3. package/dist/crud-detector.js +153 -0
  4. package/dist/generators.d.ts +14 -0
  5. package/dist/generators.d.ts.map +1 -0
  6. package/dist/generators.js +158 -0
  7. package/dist/index.d.ts +3 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +221 -0
  10. package/dist/normalizer.d.ts +14 -0
  11. package/dist/normalizer.d.ts.map +1 -0
  12. package/dist/normalizer.js +194 -0
  13. package/dist/parser.d.ts +32 -0
  14. package/dist/parser.d.ts.map +1 -0
  15. package/dist/parser.js +282 -0
  16. package/dist/plugin.d.ts +32 -0
  17. package/dist/plugin.d.ts.map +1 -0
  18. package/dist/plugin.js +129 -0
  19. package/dist/seed.d.ts +15 -0
  20. package/dist/seed.d.ts.map +1 -0
  21. package/dist/seed.js +41 -0
  22. package/package.json +45 -0
  23. package/src/__fixtures__/faker-stress-test.openapi.yaml +1030 -0
  24. package/src/__fixtures__/openapi31.json +34 -0
  25. package/src/__fixtures__/petstore-openapi3.json +168 -0
  26. package/src/__fixtures__/petstore-swagger2.json +141 -0
  27. package/src/__fixtures__/scalar-galaxy.yaml +1314 -0
  28. package/src/__fixtures__/stripe-fixtures3.json +6542 -0
  29. package/src/__fixtures__/stripe-spec3.yaml +161621 -0
  30. package/src/__fixtures__/train-travel.yaml +1264 -0
  31. package/src/crud-detector.test.ts +150 -0
  32. package/src/crud-detector.ts +194 -0
  33. package/src/generators.test.ts +214 -0
  34. package/src/generators.ts +212 -0
  35. package/src/index.ts +4 -0
  36. package/src/normalizer.test.ts +253 -0
  37. package/src/normalizer.ts +233 -0
  38. package/src/parser.test.ts +181 -0
  39. package/src/parser.ts +389 -0
  40. package/src/plugin.test.ts +205 -0
  41. package/src/plugin.ts +185 -0
  42. package/src/seed.ts +62 -0
  43. package/src/steps/openapi-crud.steps.ts +132 -0
  44. package/src/steps/openapi-parsing.steps.ts +111 -0
  45. package/src/steps/openapi-seed.steps.ts +94 -0
  46. 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
+ }