@wylan24/mockgen 0.0.0-stage → 0.1.0-alpha.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/LICENSE +21 -0
- package/README.md +188 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +278 -0
- package/dist/cli.js.map +1 -0
- package/dist/collections.d.ts +46 -0
- package/dist/collections.js +215 -0
- package/dist/collections.js.map +1 -0
- package/dist/generate.d.ts +19 -0
- package/dist/generate.js +414 -0
- package/dist/generate.js.map +1 -0
- package/dist/heuristics.d.ts +28 -0
- package/dist/heuristics.js +139 -0
- package/dist/heuristics.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/infer.d.ts +10 -0
- package/dist/infer.js +166 -0
- package/dist/infer.js.map +1 -0
- package/dist/load.d.ts +3 -0
- package/dist/load.js +15 -0
- package/dist/load.js.map +1 -0
- package/dist/model.d.ts +34 -0
- package/dist/model.js +4 -0
- package/dist/model.js.map +1 -0
- package/dist/openapi.d.ts +17 -0
- package/dist/openapi.js +209 -0
- package/dist/openapi.js.map +1 -0
- package/dist/samples.d.ts +14 -0
- package/dist/samples.js +115 -0
- package/dist/samples.js.map +1 -0
- package/dist/schema.d.ts +38 -0
- package/dist/schema.js +321 -0
- package/dist/schema.js.map +1 -0
- package/dist/server.d.ts +45 -0
- package/dist/server.js +175 -0
- package/dist/server.js.map +1 -0
- package/dist/validate.d.ts +21 -0
- package/dist/validate.js +164 -0
- package/dist/validate.js.map +1 -0
- package/package.json +68 -4
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { generate } from './generate.js';
|
|
2
|
+
import { fits, mergeAllOf, typeOf } from './schema.js';
|
|
3
|
+
import { normalizeName } from './heuristics.js';
|
|
4
|
+
// List endpoints are treated as windows onto a fixed, seeded collection:
|
|
5
|
+
// record i always has the same data, `?page=2` shows records 10–19, and when a
|
|
6
|
+
// detail route exists (`/users` + `/users/{id}`), each list item is overlaid with
|
|
7
|
+
// the detail response for its id so the two always agree.
|
|
8
|
+
/** Property names that hold the records in a wrapped list response. */
|
|
9
|
+
const LIST_KEYS = /^(data|items|results|records|content|rows|entries|list|values|nodes|hits|objects|elements)$/;
|
|
10
|
+
/** Sibling names that mark an object as a page wrapper rather than a record with a nested array. */
|
|
11
|
+
const PAGE_HINT = /^(total|totalcount|count|page|pages|next|nextpage|cursor|nextcursor|meta|pagination|pageinfo|links|hasmore|hasnext|limit|offset)$/;
|
|
12
|
+
/** Recognize a list response: a bare array of objects, or an object wrapping one. */
|
|
13
|
+
export function findListShape(schema) {
|
|
14
|
+
if (!schema)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (typeOf(schema) === 'array') {
|
|
17
|
+
return isObjectSchema(schema.items) ? { arraySchema: schema, itemSchema: schema.items } : undefined;
|
|
18
|
+
}
|
|
19
|
+
const props = (schema.allOf ? mergeAllOf(schema) : schema).properties;
|
|
20
|
+
if (!props)
|
|
21
|
+
return undefined;
|
|
22
|
+
const arrays = Object.entries(props).filter(([, s]) => typeOf(s) === 'array' && isObjectSchema(s.items));
|
|
23
|
+
const pick = arrays.find(([k]) => LIST_KEYS.test(normalizeName(k))) ??
|
|
24
|
+
(arrays.length === 1 && Object.keys(props).some((k) => PAGE_HINT.test(normalizeName(k))) ? arrays[0] : undefined);
|
|
25
|
+
return pick && { key: pick[0], arraySchema: pick[1], itemSchema: pick[1].items };
|
|
26
|
+
}
|
|
27
|
+
function isObjectSchema(s) {
|
|
28
|
+
return !!s && (typeOf(s) === 'object' || !!s.allOf);
|
|
29
|
+
}
|
|
30
|
+
/** The `GET /users/{id}` route for list route `GET /users`, if the spec has one. */
|
|
31
|
+
export function findDetailRoute(routes, list) {
|
|
32
|
+
const prefix = `${list.path.replace(/\/$/, '')}/{`;
|
|
33
|
+
return routes.find((r) => r.method === 'GET' && r.path.startsWith(prefix) && r.path.endsWith('}') && !r.path.slice(prefix.length).includes('/'));
|
|
34
|
+
}
|
|
35
|
+
const LIMIT_PARAMS = ['limit', 'per_page', 'perPage', 'page_size', 'pageSize', 'size', 'first'];
|
|
36
|
+
const OFFSET_PARAMS = ['offset', 'skip', 'start'];
|
|
37
|
+
const PAGE_PARAMS = ['page', 'page_number', 'pageNumber'];
|
|
38
|
+
const CURSOR_PARAMS = ['cursor', 'after', 'starting_after', 'page_token', 'pageToken'];
|
|
39
|
+
/** Work out which slice of the collection a request asks for. */
|
|
40
|
+
export function readPage(query, arraySchema, total, defaultLimit = 10) {
|
|
41
|
+
const min = Math.max(1, arraySchema.minItems ?? 1);
|
|
42
|
+
const max = Math.min(1000, arraySchema.maxItems ?? 1000);
|
|
43
|
+
const limit = clamp(firstInt(query, LIMIT_PARAMS) ?? defaultLimit, min, Math.max(min, max));
|
|
44
|
+
const cursor = CURSOR_PARAMS.map((p) => query.get(p)).find(Boolean);
|
|
45
|
+
const page = firstInt(query, PAGE_PARAMS);
|
|
46
|
+
const offset = Math.max(0, (cursor ? decodeCursor(cursor) : undefined) ?? firstInt(query, OFFSET_PARAMS) ?? ((page ?? 1) - 1) * limit);
|
|
47
|
+
return { offset, limit, page: page && page > 0 ? page : Math.floor(offset / limit) + 1, total };
|
|
48
|
+
}
|
|
49
|
+
export function buildPage(input) {
|
|
50
|
+
const { shape, query, total } = input;
|
|
51
|
+
const win = readPage(query, shape.arraySchema, total);
|
|
52
|
+
const item = shape.itemSchema.allOf ? mergeAllOf(shape.itemSchema) : shape.itemSchema;
|
|
53
|
+
const props = item.properties ?? {};
|
|
54
|
+
const idKey = input.idKey && input.idKey in props ? input.idKey : 'id' in props ? 'id' : undefined;
|
|
55
|
+
const items = [];
|
|
56
|
+
const required = new Set(item.required ?? []);
|
|
57
|
+
const projectable = !item.oneOf && !item.anyOf && !!item.properties;
|
|
58
|
+
const detailProps = (input.detailSchema?.allOf ? mergeAllOf(input.detailSchema) : input.detailSchema)?.properties ?? {};
|
|
59
|
+
for (let i = win.offset; i < Math.min(total, win.offset + win.limit); i++) {
|
|
60
|
+
const key = `${input.requestPath}#${i}`;
|
|
61
|
+
const id = idKey ? recordId(props[idKey], i, input.seed, key, input.locale) : undefined;
|
|
62
|
+
const detail = id !== undefined ? input.detail?.(id) : undefined;
|
|
63
|
+
if (detail && !projectable) {
|
|
64
|
+
// Items that are a oneOf/anyOf can't be projected field by field: generate, then overlay.
|
|
65
|
+
const record = generate(shape.itemSchema, { seed: input.seed, locale: input.locale, key });
|
|
66
|
+
for (const prop of Object.keys(record))
|
|
67
|
+
if (prop in detail && fits(detail[prop], props[prop]))
|
|
68
|
+
record[prop] = detail[prop];
|
|
69
|
+
items.push(record);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!detail) {
|
|
73
|
+
const record = generate(shape.itemSchema, { seed: input.seed, locale: input.locale, key });
|
|
74
|
+
if (idKey && id !== undefined && fits(id, props[idKey]))
|
|
75
|
+
record[idKey] = id;
|
|
76
|
+
items.push(record);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
// The detail record is the source of truth: project it onto the item's fields
|
|
80
|
+
// rather than generating the item too (twice the work, on the hottest path).
|
|
81
|
+
const record = {};
|
|
82
|
+
for (const [prop, propSchema] of Object.entries(props)) {
|
|
83
|
+
if (prop in detail && (detailProps[prop] === propSchema || fits(detail[prop], propSchema)))
|
|
84
|
+
record[prop] = detail[prop];
|
|
85
|
+
else if (required.has(prop))
|
|
86
|
+
record[prop] = generate(propSchema, { seed: input.seed, locale: input.locale, key: `${key}.${prop}` });
|
|
87
|
+
}
|
|
88
|
+
// Required keys without a property schema still have to be there.
|
|
89
|
+
for (const prop of required)
|
|
90
|
+
if (!(prop in record))
|
|
91
|
+
record[prop] = prop in detail ? detail[prop] : generate({}, { seed: input.seed, locale: input.locale, key: `${key}.${prop}` });
|
|
92
|
+
items.push(record);
|
|
93
|
+
}
|
|
94
|
+
const hasMore = win.offset + items.length < total;
|
|
95
|
+
const headers = { 'x-total-count': String(total) };
|
|
96
|
+
const links = navLinks(input, win, hasMore);
|
|
97
|
+
const link = [links.next && `<${links.next}>; rel="next"`, links.prev && `<${links.prev}>; rel="prev"`].filter(Boolean).join(', ');
|
|
98
|
+
if (link)
|
|
99
|
+
headers.link = link;
|
|
100
|
+
if (!shape.key)
|
|
101
|
+
return { body: items, headers };
|
|
102
|
+
const body = (input.body && typeof input.body === 'object' ? input.body : {});
|
|
103
|
+
body[shape.key] = items;
|
|
104
|
+
const schema = input.schema.allOf ? mergeAllOf(input.schema) : input.schema;
|
|
105
|
+
fillMeta(body, schema, { ...win, count: items.length, hasMore, links }, shape.key, 0);
|
|
106
|
+
return { body, headers };
|
|
107
|
+
}
|
|
108
|
+
/** Fill pagination fields (`total`, `page`, `next`, `meta.pagination.totalPages`, …) by name. */
|
|
109
|
+
function fillMeta(obj, schema, m, listKey, depth) {
|
|
110
|
+
const props = schema?.properties ?? {};
|
|
111
|
+
const totalPages = Math.max(1, Math.ceil(m.total / m.limit));
|
|
112
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
113
|
+
if (key === listKey)
|
|
114
|
+
continue;
|
|
115
|
+
const s = props[key];
|
|
116
|
+
const n = normalizeName(key);
|
|
117
|
+
const set = (v) => {
|
|
118
|
+
if (fits(v, s))
|
|
119
|
+
obj[key] = v;
|
|
120
|
+
};
|
|
121
|
+
// Last page: nullable `next` becomes null, optional `next` disappears.
|
|
122
|
+
const end = () => {
|
|
123
|
+
if (s && (fits(null, s) || s.nullable))
|
|
124
|
+
obj[key] = null;
|
|
125
|
+
else if (!(schema?.required ?? []).includes(key))
|
|
126
|
+
delete obj[key];
|
|
127
|
+
};
|
|
128
|
+
if (/^(total|totalcount|totalitems|totalresults|totalrecords|totalelements|totalentries|count|totalsize)$/.test(n))
|
|
129
|
+
set(n === 'count' && 'total' in obj ? m.count : m.total); // `count` next to `total` is the page size
|
|
130
|
+
else if (/^(page|currentpage|pagenumber|pageindex)$/.test(n))
|
|
131
|
+
set(m.page);
|
|
132
|
+
else if (/^(perpage|pagesize|limit|size|itemsperpage|pagelength)$/.test(n))
|
|
133
|
+
set(m.limit);
|
|
134
|
+
else if (/^(totalpages|pagecount|pages|lastpage)$/.test(n))
|
|
135
|
+
set(totalPages);
|
|
136
|
+
else if (/^(offset|skip|start)$/.test(n))
|
|
137
|
+
set(m.offset);
|
|
138
|
+
else if (/^(hasmore|hasnext|hasnextpage|more)$/.test(n))
|
|
139
|
+
set(m.hasMore);
|
|
140
|
+
else if (/^(hasprev|hasprevious|haspreviouspage)$/.test(n))
|
|
141
|
+
set(m.offset > 0);
|
|
142
|
+
else if (/^(next|nextpage|nexturl|nextlink|nextcursor|endcursor|cursor)$/.test(n)) {
|
|
143
|
+
if (!m.hasMore)
|
|
144
|
+
end();
|
|
145
|
+
else if (typeof value === 'number' || (s && /integer|number/.test(String(typeOf(s)))))
|
|
146
|
+
set(m.page + 1);
|
|
147
|
+
else
|
|
148
|
+
set(/cursor/.test(n) ? m.links.nextCursor : m.links.next);
|
|
149
|
+
}
|
|
150
|
+
else if (/^(prev|previous|prevpage|previouspage|prevurl|prevlink|startcursor)$/.test(n)) {
|
|
151
|
+
if (m.offset === 0)
|
|
152
|
+
end();
|
|
153
|
+
else if (typeof value === 'number' || (s && /integer|number/.test(String(typeOf(s)))))
|
|
154
|
+
set(m.page - 1);
|
|
155
|
+
else
|
|
156
|
+
set(/cursor/.test(n) ? m.links.prevCursor : m.links.prev);
|
|
157
|
+
}
|
|
158
|
+
else if (depth < 2 && value && typeof value === 'object' && !Array.isArray(value)) {
|
|
159
|
+
fillMeta(value, s && (s.allOf ? mergeAllOf(s) : s), m, undefined, depth + 1);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/** Record i's id: sequential for numeric ids, otherwise generated from the id schema alone. */
|
|
164
|
+
function recordId(idSchema, i, seed, key, locale) {
|
|
165
|
+
const t = typeOf(idSchema);
|
|
166
|
+
if (t === 'integer' || t === 'number') {
|
|
167
|
+
const id = Math.max(1, Math.ceil(idSchema.minimum ?? 1)) + i;
|
|
168
|
+
if (idSchema.maximum === undefined || id <= idSchema.maximum)
|
|
169
|
+
return id;
|
|
170
|
+
}
|
|
171
|
+
return generate(idSchema, { seed, locale, key: `${key}.id` });
|
|
172
|
+
}
|
|
173
|
+
function navLinks(input, win, hasMore) {
|
|
174
|
+
const link = (offset) => {
|
|
175
|
+
const q = new URLSearchParams(input.query);
|
|
176
|
+
for (const p of CURSOR_PARAMS)
|
|
177
|
+
q.delete(p);
|
|
178
|
+
const usesOffset = OFFSET_PARAMS.some((p) => q.has(p));
|
|
179
|
+
if (usesOffset)
|
|
180
|
+
for (const p of OFFSET_PARAMS)
|
|
181
|
+
q.has(p) && q.set(p, String(offset));
|
|
182
|
+
else
|
|
183
|
+
q.set(PAGE_PARAMS.find((p) => q.has(p)) ?? 'page', String(Math.floor(offset / win.limit) + 1));
|
|
184
|
+
if (!LIMIT_PARAMS.some((p) => q.has(p)))
|
|
185
|
+
q.set('limit', String(win.limit));
|
|
186
|
+
return `${input.origin ?? ''}${input.requestPath}?${q}`;
|
|
187
|
+
};
|
|
188
|
+
const prevOffset = Math.max(0, win.offset - win.limit);
|
|
189
|
+
return {
|
|
190
|
+
next: hasMore ? link(win.offset + win.limit) : undefined,
|
|
191
|
+
prev: win.offset > 0 ? link(prevOffset) : undefined,
|
|
192
|
+
nextCursor: hasMore ? encodeCursor(win.offset + win.limit) : undefined,
|
|
193
|
+
prevCursor: win.offset > 0 ? encodeCursor(prevOffset) : undefined,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
/** Opaque-looking cursors that are really offsets, so any cursor we hand out works. */
|
|
197
|
+
export function encodeCursor(offset) {
|
|
198
|
+
return Buffer.from(`o:${offset}`).toString('base64url');
|
|
199
|
+
}
|
|
200
|
+
function decodeCursor(cursor) {
|
|
201
|
+
const m = /^o:(\d+)$/.exec(Buffer.from(cursor, 'base64url').toString());
|
|
202
|
+
return m ? Number(m[1]) : undefined;
|
|
203
|
+
}
|
|
204
|
+
function firstInt(query, names) {
|
|
205
|
+
for (const name of names) {
|
|
206
|
+
const v = query.get(name);
|
|
207
|
+
if (v !== null && /^-?\d+$/.test(v))
|
|
208
|
+
return Number(v);
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
function clamp(n, min, max) {
|
|
213
|
+
return Math.min(max, Math.max(min, n));
|
|
214
|
+
}
|
|
215
|
+
//# sourceMappingURL=collections.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"collections.js","sourceRoot":"","sources":["../src/collections.ts"],"names":[],"mappings":"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","sourcesContent":["import { generate } from './generate.js';\nimport { fits, mergeAllOf, typeOf } from './schema.js';\nimport { normalizeName } from './heuristics.js';\nimport type { MockRoute, Schema } from './model.js';\n\n// List endpoints are treated as windows onto a fixed, seeded collection:\n// record i always has the same data, `?page=2` shows records 10–19, and when a\n// detail route exists (`/users` + `/users/{id}`), each list item is overlaid with\n// the detail response for its id so the two always agree.\n\n/** Property names that hold the records in a wrapped list response. */\nconst LIST_KEYS = /^(data|items|results|records|content|rows|entries|list|values|nodes|hits|objects|elements)$/;\n/** Sibling names that mark an object as a page wrapper rather than a record with a nested array. */\nconst PAGE_HINT = /^(total|totalcount|count|page|pages|next|nextpage|cursor|nextcursor|meta|pagination|pageinfo|links|hasmore|hasnext|limit|offset)$/;\n\nexport interface ListShape {\n /** Property holding the records; undefined when the response is a bare array. */\n key?: string;\n arraySchema: Schema;\n itemSchema: Schema;\n}\n\n/** Recognize a list response: a bare array of objects, or an object wrapping one. */\nexport function findListShape(schema: Schema | undefined): ListShape | undefined {\n if (!schema) return undefined;\n if (typeOf(schema) === 'array') {\n return isObjectSchema(schema.items) ? { arraySchema: schema, itemSchema: schema.items } : undefined;\n }\n const props: Record<string, Schema> | undefined = (schema.allOf ? mergeAllOf(schema) : schema).properties;\n if (!props) return undefined;\n const arrays = Object.entries(props).filter(([, s]) => typeOf(s) === 'array' && isObjectSchema(s.items));\n const pick =\n arrays.find(([k]) => LIST_KEYS.test(normalizeName(k))) ??\n (arrays.length === 1 && Object.keys(props).some((k) => PAGE_HINT.test(normalizeName(k))) ? arrays[0] : undefined);\n return pick && { key: pick[0], arraySchema: pick[1], itemSchema: pick[1].items };\n}\n\nfunction isObjectSchema(s: Schema | undefined): s is Schema {\n return !!s && (typeOf(s) === 'object' || !!s.allOf);\n}\n\n/** The `GET /users/{id}` route for list route `GET /users`, if the spec has one. */\nexport function findDetailRoute(routes: MockRoute[], list: MockRoute): MockRoute | undefined {\n const prefix = `${list.path.replace(/\\/$/, '')}/{`;\n return routes.find(\n (r) => r.method === 'GET' && r.path.startsWith(prefix) && r.path.endsWith('}') && !r.path.slice(prefix.length).includes('/'),\n );\n}\n\nexport interface PageWindow {\n offset: number;\n limit: number;\n /** 1-based. */\n page: number;\n total: number;\n}\n\nconst LIMIT_PARAMS = ['limit', 'per_page', 'perPage', 'page_size', 'pageSize', 'size', 'first'];\nconst OFFSET_PARAMS = ['offset', 'skip', 'start'];\nconst PAGE_PARAMS = ['page', 'page_number', 'pageNumber'];\nconst CURSOR_PARAMS = ['cursor', 'after', 'starting_after', 'page_token', 'pageToken'];\n\n/** Work out which slice of the collection a request asks for. */\nexport function readPage(query: URLSearchParams, arraySchema: Schema, total: number, defaultLimit = 10): PageWindow {\n const min = Math.max(1, arraySchema.minItems ?? 1);\n const max = Math.min(1000, arraySchema.maxItems ?? 1000);\n const limit = clamp(firstInt(query, LIMIT_PARAMS) ?? defaultLimit, min, Math.max(min, max));\n const cursor = CURSOR_PARAMS.map((p) => query.get(p)).find(Boolean);\n const page = firstInt(query, PAGE_PARAMS);\n const offset = Math.max(0, (cursor ? decodeCursor(cursor) : undefined) ?? firstInt(query, OFFSET_PARAMS) ?? ((page ?? 1) - 1) * limit);\n return { offset, limit, page: page && page > 0 ? page : Math.floor(offset / limit) + 1, total };\n}\n\nexport interface BuildPageInput {\n /** The list response's schema. */\n schema: Schema;\n /** Concrete request path, used to seed records and build next/prev links. */\n requestPath: string;\n query: URLSearchParams;\n origin?: string;\n seed?: number;\n locale?: string;\n shape: ListShape;\n /** Generated response body; for wrapped lists, its metadata is filled in place. */\n body: unknown;\n total: number;\n /** Name of the id property that links items to the detail route. */\n idKey?: string;\n /** Detail response for an id, used to make list items agree with `GET /x/{id}`. */\n detail?: (id: unknown) => Record<string, unknown> | undefined;\n /** The detail route's schema; fields it shares with the item need no re-check. */\n detailSchema?: Schema;\n}\n\nexport function buildPage(input: BuildPageInput): { body: unknown; headers: Record<string, string> } {\n const { shape, query, total } = input;\n const win = readPage(query, shape.arraySchema, total);\n const item = shape.itemSchema.allOf ? mergeAllOf(shape.itemSchema) : shape.itemSchema;\n const props: Record<string, Schema> = item.properties ?? {};\n const idKey = input.idKey && input.idKey in props ? input.idKey : 'id' in props ? 'id' : undefined;\n\n const items: unknown[] = [];\n const required = new Set<string>(item.required ?? []);\n const projectable = !item.oneOf && !item.anyOf && !!item.properties;\n const detailProps: Record<string, Schema> = (input.detailSchema?.allOf ? mergeAllOf(input.detailSchema) : input.detailSchema)?.properties ?? {};\n for (let i = win.offset; i < Math.min(total, win.offset + win.limit); i++) {\n const key = `${input.requestPath}#${i}`;\n const id = idKey ? recordId(props[idKey]!, i, input.seed, key, input.locale) : undefined;\n const detail = id !== undefined ? input.detail?.(id) : undefined;\n if (detail && !projectable) {\n // Items that are a oneOf/anyOf can't be projected field by field: generate, then overlay.\n const record = generate(shape.itemSchema, { seed: input.seed, locale: input.locale, key }) as Record<string, unknown>;\n for (const prop of Object.keys(record)) if (prop in detail && fits(detail[prop], props[prop])) record[prop] = detail[prop];\n items.push(record);\n continue;\n }\n if (!detail) {\n const record = generate(shape.itemSchema, { seed: input.seed, locale: input.locale, key }) as Record<string, unknown>;\n if (idKey && id !== undefined && fits(id, props[idKey])) record[idKey] = id;\n items.push(record);\n continue;\n }\n // The detail record is the source of truth: project it onto the item's fields\n // rather than generating the item too (twice the work, on the hottest path).\n const record: Record<string, unknown> = {};\n for (const [prop, propSchema] of Object.entries(props)) {\n if (prop in detail && (detailProps[prop] === propSchema || fits(detail[prop], propSchema))) record[prop] = detail[prop];\n else if (required.has(prop)) record[prop] = generate(propSchema, { seed: input.seed, locale: input.locale, key: `${key}.${prop}` });\n }\n // Required keys without a property schema still have to be there.\n for (const prop of required) if (!(prop in record)) record[prop] = prop in detail ? detail[prop] : generate({}, { seed: input.seed, locale: input.locale, key: `${key}.${prop}` });\n items.push(record);\n }\n\n const hasMore = win.offset + items.length < total;\n const headers: Record<string, string> = { 'x-total-count': String(total) };\n const links = navLinks(input, win, hasMore);\n const link = [links.next && `<${links.next}>; rel=\"next\"`, links.prev && `<${links.prev}>; rel=\"prev\"`].filter(Boolean).join(', ');\n if (link) headers.link = link;\n\n if (!shape.key) return { body: items, headers };\n const body = (input.body && typeof input.body === 'object' ? input.body : {}) as Record<string, unknown>;\n body[shape.key] = items;\n const schema = input.schema.allOf ? mergeAllOf(input.schema) : input.schema;\n fillMeta(body, schema, { ...win, count: items.length, hasMore, links }, shape.key, 0);\n return { body, headers };\n}\n\ninterface MetaInfo extends PageWindow {\n count: number;\n hasMore: boolean;\n links: { next?: string; prev?: string; nextCursor?: string; prevCursor?: string };\n}\n\n/** Fill pagination fields (`total`, `page`, `next`, `meta.pagination.totalPages`, …) by name. */\nfunction fillMeta(obj: Record<string, unknown>, schema: Schema | undefined, m: MetaInfo, listKey: string | undefined, depth: number) {\n const props: Record<string, Schema> = schema?.properties ?? {};\n const totalPages = Math.max(1, Math.ceil(m.total / m.limit));\n for (const [key, value] of Object.entries(obj)) {\n if (key === listKey) continue;\n const s = props[key];\n const n = normalizeName(key);\n const set = (v: unknown) => {\n if (fits(v, s)) obj[key] = v;\n };\n // Last page: nullable `next` becomes null, optional `next` disappears.\n const end = () => {\n if (s && (fits(null, s) || s.nullable)) obj[key] = null;\n else if (!(schema?.required ?? []).includes(key)) delete obj[key];\n };\n\n if (/^(total|totalcount|totalitems|totalresults|totalrecords|totalelements|totalentries|count|totalsize)$/.test(n)) set(n === 'count' && 'total' in obj ? m.count : m.total); // `count` next to `total` is the page size\n else if (/^(page|currentpage|pagenumber|pageindex)$/.test(n)) set(m.page);\n else if (/^(perpage|pagesize|limit|size|itemsperpage|pagelength)$/.test(n)) set(m.limit);\n else if (/^(totalpages|pagecount|pages|lastpage)$/.test(n)) set(totalPages);\n else if (/^(offset|skip|start)$/.test(n)) set(m.offset);\n else if (/^(hasmore|hasnext|hasnextpage|more)$/.test(n)) set(m.hasMore);\n else if (/^(hasprev|hasprevious|haspreviouspage)$/.test(n)) set(m.offset > 0);\n else if (/^(next|nextpage|nexturl|nextlink|nextcursor|endcursor|cursor)$/.test(n)) {\n if (!m.hasMore) end();\n else if (typeof value === 'number' || (s && /integer|number/.test(String(typeOf(s))))) set(m.page + 1);\n else set(/cursor/.test(n) ? m.links.nextCursor : m.links.next);\n } else if (/^(prev|previous|prevpage|previouspage|prevurl|prevlink|startcursor)$/.test(n)) {\n if (m.offset === 0) end();\n else if (typeof value === 'number' || (s && /integer|number/.test(String(typeOf(s))))) set(m.page - 1);\n else set(/cursor/.test(n) ? m.links.prevCursor : m.links.prev);\n } else if (depth < 2 && value && typeof value === 'object' && !Array.isArray(value)) {\n fillMeta(value as Record<string, unknown>, s && (s.allOf ? mergeAllOf(s) : s), m, undefined, depth + 1);\n }\n }\n}\n\n/** Record i's id: sequential for numeric ids, otherwise generated from the id schema alone. */\nfunction recordId(idSchema: Schema, i: number, seed: number | undefined, key: string, locale?: string): unknown {\n const t = typeOf(idSchema);\n if (t === 'integer' || t === 'number') {\n const id = Math.max(1, Math.ceil(idSchema.minimum ?? 1)) + i;\n if (idSchema.maximum === undefined || id <= idSchema.maximum) return id;\n }\n return generate(idSchema, { seed, locale, key: `${key}.id` });\n}\n\nfunction navLinks(input: BuildPageInput, win: PageWindow, hasMore: boolean): MetaInfo['links'] {\n const link = (offset: number) => {\n const q = new URLSearchParams(input.query);\n for (const p of CURSOR_PARAMS) q.delete(p);\n const usesOffset = OFFSET_PARAMS.some((p) => q.has(p));\n if (usesOffset) for (const p of OFFSET_PARAMS) q.has(p) && q.set(p, String(offset));\n else q.set(PAGE_PARAMS.find((p) => q.has(p)) ?? 'page', String(Math.floor(offset / win.limit) + 1));\n if (!LIMIT_PARAMS.some((p) => q.has(p))) q.set('limit', String(win.limit));\n return `${input.origin ?? ''}${input.requestPath}?${q}`;\n };\n const prevOffset = Math.max(0, win.offset - win.limit);\n return {\n next: hasMore ? link(win.offset + win.limit) : undefined,\n prev: win.offset > 0 ? link(prevOffset) : undefined,\n nextCursor: hasMore ? encodeCursor(win.offset + win.limit) : undefined,\n prevCursor: win.offset > 0 ? encodeCursor(prevOffset) : undefined,\n };\n}\n\n/** Opaque-looking cursors that are really offsets, so any cursor we hand out works. */\nexport function encodeCursor(offset: number): string {\n return Buffer.from(`o:${offset}`).toString('base64url');\n}\n\nfunction decodeCursor(cursor: string): number | undefined {\n const m = /^o:(\\d+)$/.exec(Buffer.from(cursor, 'base64url').toString());\n return m ? Number(m[1]) : undefined;\n}\n\nfunction firstInt(query: URLSearchParams, names: string[]): number | undefined {\n for (const name of names) {\n const v = query.get(name);\n if (v !== null && /^-?\\d+$/.test(v)) return Number(v);\n }\n return undefined;\n}\n\nfunction clamp(n: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, n));\n}\n"]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Schema } from './model.js';
|
|
2
|
+
export interface GenerateOptions {
|
|
3
|
+
/** Global seed; the same seed + key always yields the same value. */
|
|
4
|
+
seed?: number;
|
|
5
|
+
/** Distinguishes values under one seed, e.g. `GET /users/42 200`. */
|
|
6
|
+
key?: string;
|
|
7
|
+
/** Recursion limit for circular schemas. */
|
|
8
|
+
maxDepth?: number;
|
|
9
|
+
/** Faker locale for names, addresses and text, e.g. `de` or `pt_BR`. Default `en`. */
|
|
10
|
+
locale?: string;
|
|
11
|
+
}
|
|
12
|
+
/** Locale codes Faker ships, e.g. `de`, `ja`, `pt_BR`. */
|
|
13
|
+
export declare function availableLocales(): string[];
|
|
14
|
+
/** Throw a readable error for a locale Faker doesn't ship. */
|
|
15
|
+
export declare function assertLocale(locale: string): void;
|
|
16
|
+
/** Generate a realistic, deterministic value that satisfies `schema`. */
|
|
17
|
+
export declare function generate(schema: Schema | undefined, opts?: GenerateOptions): unknown;
|
|
18
|
+
/** 32-bit FNV-1a; stable across platforms and Node versions. */
|
|
19
|
+
export declare function hash(input: string): number;
|