@zenera/faker 1.1.21 → 1.1.23
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/README.md +12 -11
- package/dist/cache.d.ts +14 -1
- package/dist/cache.js +57 -1
- package/dist/command.js +11 -1
- package/dist/generate.d.ts +10 -0
- package/dist/generate.js +50 -3
- package/dist/logger.d.ts +62 -0
- package/dist/logger.js +273 -0
- package/dist/paging.d.ts +31 -3
- package/dist/paging.js +315 -6
- package/dist/probe.js +11 -0
- package/dist/prompt.d.ts +14 -0
- package/dist/prompt.js +38 -5
- package/dist/server.d.ts +8 -0
- package/dist/server.js +323 -46
- package/dist/spec.d.ts +5 -0
- package/dist/spec.js +7 -1
- package/package.json +3 -3
package/dist/paging.d.ts
CHANGED
|
@@ -3,8 +3,10 @@ import type { ParamSpec } from './spec.ts';
|
|
|
3
3
|
export type PagingStyle = 'cursor' | 'offset';
|
|
4
4
|
export interface Paging {
|
|
5
5
|
style: PagingStyle;
|
|
6
|
-
/** the
|
|
6
|
+
/** the parameter that turns the page */
|
|
7
7
|
param: string;
|
|
8
|
+
/** where the parameter lives: 'body' when in request body, absent when in query */
|
|
9
|
+
paramIn?: 'query' | 'body';
|
|
8
10
|
/** the page-size parameter, when the operation takes one */
|
|
9
11
|
size?: string;
|
|
10
12
|
/** the response property carrying the token for the page after this one */
|
|
@@ -25,7 +27,7 @@ export interface Paging {
|
|
|
25
27
|
* a one-shot list — so a control that turns the page *and* a property that says
|
|
26
28
|
* where the next one is are both required.
|
|
27
29
|
*/
|
|
28
|
-
export declare function pagingOf(params: readonly ParamSpec[], schema: Schema | undefined): Paging | undefined;
|
|
30
|
+
export declare function pagingOf(params: readonly ParamSpec[], schema: Schema | undefined, bodySchema?: Schema | undefined): Paging | undefined;
|
|
29
31
|
/**
|
|
30
32
|
* The paging an actual request reveals, for a document that declared none.
|
|
31
33
|
*
|
|
@@ -35,7 +37,7 @@ export declare function pagingOf(params: readonly ParamSpec[], schema: Schema |
|
|
|
35
37
|
* evidence even when the document is silent, and silence is exactly the case
|
|
36
38
|
* nothing else here can catch.
|
|
37
39
|
*/
|
|
38
|
-
export declare function pagingSeen(params: readonly ParamSpec[], schema: Schema | undefined, query: Iterable<[string, string]
|
|
40
|
+
export declare function pagingSeen(params: readonly ParamSpec[], schema: Schema | undefined, query: Iterable<[string, string]>, body?: unknown): Paging | undefined;
|
|
39
41
|
/** The token a body offers for the next page, or nothing when it offers none. */
|
|
40
42
|
export declare function tokenOf(value: unknown, paging: Paging): string | undefined;
|
|
41
43
|
/**
|
|
@@ -48,4 +50,30 @@ export declare function tokenOf(value: unknown, paging: Paging): string | undefi
|
|
|
48
50
|
* obvious. Where the schema leaves no room, this changes nothing and says so.
|
|
49
51
|
*/
|
|
50
52
|
export declare function cutLoop(value: unknown, paging: Paging, sent: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Terminates pagination unconditionally on a response object: nullifies or deletes
|
|
55
|
+
* the next token if allowed, or empties items if the token is required non-nullable.
|
|
56
|
+
*/
|
|
57
|
+
export declare function cutPaging(value: unknown, paging: Paging): boolean;
|
|
58
|
+
/** Sets the items array to empty and sets more to false. */
|
|
59
|
+
export declare function emptyItems(value: unknown, paging: Paging): boolean;
|
|
60
|
+
/** Retrieves the sent pagination token from query parameters or request body. */
|
|
61
|
+
export declare function getSentToken(query: URLSearchParams, body: unknown, paging: Paging): string | undefined;
|
|
62
|
+
/** Decodes the 1-based page index from base64 JSON token `{"p": n}` if present. */
|
|
63
|
+
export declare function parsePageFromToken(token: string, style?: PagingStyle): number | undefined;
|
|
64
|
+
export interface PaginationCutResult {
|
|
65
|
+
cut: boolean;
|
|
66
|
+
reason: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Tracks active pagination streams to bound iterations and prevent infinite loops.
|
|
70
|
+
*
|
|
71
|
+
* Randomly bounds pagination to 1-10 pages by default, intercepts cycle loops and
|
|
72
|
+
* echoed tokens, and empties responses when clients loop past the end.
|
|
73
|
+
*/
|
|
74
|
+
export declare class PaginationLimiter {
|
|
75
|
+
#private;
|
|
76
|
+
constructor(maxPages?: number | (() => number), maxEntries?: number);
|
|
77
|
+
process(operationKey: string, streamKey: string, sent: string | undefined, value: unknown, paging: Paging): PaginationCutResult | undefined;
|
|
78
|
+
}
|
|
51
79
|
//# sourceMappingURL=paging.d.ts.map
|
package/dist/paging.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomInt } from 'node:crypto';
|
|
1
2
|
import { properties } from "./schema.js";
|
|
2
3
|
const squash = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
3
4
|
const CURSOR_PARAMS = new Set([
|
|
@@ -73,13 +74,22 @@ const ITEMS_PROPS = new Set([
|
|
|
73
74
|
* a one-shot list — so a control that turns the page *and* a property that says
|
|
74
75
|
* where the next one is are both required.
|
|
75
76
|
*/
|
|
76
|
-
export function pagingOf(params, schema) {
|
|
77
|
+
export function pagingOf(params, schema, bodySchema) {
|
|
77
78
|
if (!schema) {
|
|
78
79
|
return undefined;
|
|
79
80
|
}
|
|
80
81
|
const query = params.filter((p) => p.in === 'query');
|
|
81
|
-
const
|
|
82
|
-
const
|
|
82
|
+
const queryCursor = query.find((p) => CURSOR_PARAMS.has(squash(p.name)));
|
|
83
|
+
const queryOffset = query.find((p) => OFFSET_PARAMS.has(squash(p.name)) && numeric(p.schema));
|
|
84
|
+
let bodyCursor;
|
|
85
|
+
let bodyOffset;
|
|
86
|
+
if (!queryCursor && !queryOffset && bodySchema) {
|
|
87
|
+
const bodyProps = properties(bodySchema);
|
|
88
|
+
bodyCursor = bodyProps.find((p) => CURSOR_PARAMS.has(squash(p.name)));
|
|
89
|
+
bodyOffset = bodyProps.find((p) => OFFSET_PARAMS.has(squash(p.name)) && numeric(p.schema));
|
|
90
|
+
}
|
|
91
|
+
const cursor = queryCursor ?? bodyCursor;
|
|
92
|
+
const offset = queryOffset ?? bodyOffset;
|
|
83
93
|
const param = cursor ?? offset;
|
|
84
94
|
if (!param) {
|
|
85
95
|
return undefined;
|
|
@@ -90,10 +100,16 @@ export function pagingOf(params, schema) {
|
|
|
90
100
|
if (!next && !more) {
|
|
91
101
|
return undefined;
|
|
92
102
|
}
|
|
103
|
+
const isBody = Boolean(!queryCursor && !queryOffset && (bodyCursor || bodyOffset));
|
|
104
|
+
const size = query.find((p) => SIZE_PARAMS.has(squash(p.name)))?.name ??
|
|
105
|
+
(bodySchema
|
|
106
|
+
? properties(bodySchema).find((p) => SIZE_PARAMS.has(squash(p.name)))?.name
|
|
107
|
+
: undefined);
|
|
93
108
|
return {
|
|
94
109
|
style: cursor ? 'cursor' : 'offset',
|
|
95
110
|
param: param.name,
|
|
96
|
-
|
|
111
|
+
...(isBody ? { paramIn: 'body' } : {}),
|
|
112
|
+
size,
|
|
97
113
|
next: next?.name,
|
|
98
114
|
nextNullable: next ? nullable(next.schema) : undefined,
|
|
99
115
|
nextRequired: next?.required,
|
|
@@ -110,7 +126,7 @@ export function pagingOf(params, schema) {
|
|
|
110
126
|
* evidence even when the document is silent, and silence is exactly the case
|
|
111
127
|
* nothing else here can catch.
|
|
112
128
|
*/
|
|
113
|
-
export function pagingSeen(params, schema, query) {
|
|
129
|
+
export function pagingSeen(params, schema, query, body) {
|
|
114
130
|
const declared = new Set(params.map((p) => p.name));
|
|
115
131
|
const extra = [];
|
|
116
132
|
for (const [name, value] of query) {
|
|
@@ -119,7 +135,31 @@ export function pagingSeen(params, schema, query) {
|
|
|
119
135
|
extra.push({ name, in: 'query', required: false, schema: guess(value) });
|
|
120
136
|
}
|
|
121
137
|
}
|
|
122
|
-
|
|
138
|
+
if (extra.length > 0) {
|
|
139
|
+
const detected = pagingOf([...params, ...extra], schema);
|
|
140
|
+
if (detected) {
|
|
141
|
+
return detected;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (isObject(body)) {
|
|
145
|
+
for (const [name, value] of Object.entries(body)) {
|
|
146
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
147
|
+
if (CURSOR_PARAMS.has(squash(name)) || OFFSET_PARAMS.has(squash(name))) {
|
|
148
|
+
const syntheticBody = {
|
|
149
|
+
type: 'object',
|
|
150
|
+
properties: {
|
|
151
|
+
[name]: guess(typeof value === 'string' ? value : JSON.stringify(value)),
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
const detected = pagingOf(params, schema, syntheticBody);
|
|
155
|
+
if (detected) {
|
|
156
|
+
return detected;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
123
163
|
}
|
|
124
164
|
/** The token a body offers for the next page, or nothing when it offers none. */
|
|
125
165
|
export function tokenOf(value, paging) {
|
|
@@ -165,6 +205,274 @@ export function cutLoop(value, paging, sent) {
|
|
|
165
205
|
}
|
|
166
206
|
return true;
|
|
167
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Terminates pagination unconditionally on a response object: nullifies or deletes
|
|
210
|
+
* the next token if allowed, or empties items if the token is required non-nullable.
|
|
211
|
+
*/
|
|
212
|
+
export function cutPaging(value, paging) {
|
|
213
|
+
let cut = false;
|
|
214
|
+
if (paging.next) {
|
|
215
|
+
const holder = holderOf(value, paging.next);
|
|
216
|
+
if (holder) {
|
|
217
|
+
if (paging.nextNullable) {
|
|
218
|
+
holder[paging.next] = null;
|
|
219
|
+
cut = true;
|
|
220
|
+
}
|
|
221
|
+
else if (!paging.nextRequired) {
|
|
222
|
+
delete holder[paging.next];
|
|
223
|
+
cut = true;
|
|
224
|
+
}
|
|
225
|
+
else if (paging.items) {
|
|
226
|
+
const itemsHolder = holderOf(value, paging.items);
|
|
227
|
+
if (itemsHolder && Array.isArray(itemsHolder[paging.items])) {
|
|
228
|
+
itemsHolder[paging.items] = [];
|
|
229
|
+
cut = true;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (paging.more) {
|
|
235
|
+
const moreHolder = holderOf(value, paging.more);
|
|
236
|
+
if (moreHolder) {
|
|
237
|
+
moreHolder[paging.more] = false;
|
|
238
|
+
cut = true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return cut;
|
|
242
|
+
}
|
|
243
|
+
/** Sets the items array to empty and sets more to false. */
|
|
244
|
+
export function emptyItems(value, paging) {
|
|
245
|
+
let cut = false;
|
|
246
|
+
if (paging.items) {
|
|
247
|
+
const itemsHolder = holderOf(value, paging.items);
|
|
248
|
+
if (itemsHolder && Array.isArray(itemsHolder[paging.items])) {
|
|
249
|
+
itemsHolder[paging.items] = [];
|
|
250
|
+
cut = true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (paging.next) {
|
|
254
|
+
const holder = holderOf(value, paging.next);
|
|
255
|
+
if (holder) {
|
|
256
|
+
if (paging.nextNullable) {
|
|
257
|
+
holder[paging.next] = null;
|
|
258
|
+
cut = true;
|
|
259
|
+
}
|
|
260
|
+
else if (!paging.nextRequired) {
|
|
261
|
+
delete holder[paging.next];
|
|
262
|
+
cut = true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (paging.more) {
|
|
267
|
+
const moreHolder = holderOf(value, paging.more);
|
|
268
|
+
if (moreHolder) {
|
|
269
|
+
moreHolder[paging.more] = false;
|
|
270
|
+
cut = true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return cut;
|
|
274
|
+
}
|
|
275
|
+
/** Retrieves the sent pagination token from query parameters or request body. */
|
|
276
|
+
export function getSentToken(query, body, paging) {
|
|
277
|
+
if (paging.paramIn === 'body' && isObject(body)) {
|
|
278
|
+
const val = body[paging.param];
|
|
279
|
+
if (val !== undefined && val !== null && val !== '') {
|
|
280
|
+
return typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
const qVal = query.get(paging.param);
|
|
284
|
+
if (qVal !== null && qVal !== '') {
|
|
285
|
+
return qVal;
|
|
286
|
+
}
|
|
287
|
+
if (isObject(body)) {
|
|
288
|
+
const val = body[paging.param];
|
|
289
|
+
if (val !== undefined && val !== null && val !== '') {
|
|
290
|
+
return typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
291
|
+
}
|
|
292
|
+
if (paging.next) {
|
|
293
|
+
const nextVal = body[paging.next];
|
|
294
|
+
if (nextVal !== undefined && nextVal !== null && nextVal !== '') {
|
|
295
|
+
return typeof nextVal === 'object' ? JSON.stringify(nextVal) : String(nextVal);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
/** Decodes the 1-based page index from base64 JSON token `{"p": n}` if present. */
|
|
302
|
+
export function parsePageFromToken(token, style = 'cursor') {
|
|
303
|
+
if (style !== 'cursor') {
|
|
304
|
+
return undefined;
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const decoded = Buffer.from(token, 'base64').toString('utf8');
|
|
308
|
+
const parsed = JSON.parse(decoded);
|
|
309
|
+
if (typeof parsed?.p === 'number')
|
|
310
|
+
return parsed.p;
|
|
311
|
+
if (typeof parsed?.page === 'number')
|
|
312
|
+
return parsed.page;
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
// Not base64 JSON
|
|
316
|
+
}
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Tracks active pagination streams to bound iterations and prevent infinite loops.
|
|
321
|
+
*
|
|
322
|
+
* Randomly bounds pagination to 1-10 pages by default, intercepts cycle loops and
|
|
323
|
+
* echoed tokens, and empties responses when clients loop past the end.
|
|
324
|
+
*/
|
|
325
|
+
export class PaginationLimiter {
|
|
326
|
+
#tokens = new Map();
|
|
327
|
+
#maxPagesFactory;
|
|
328
|
+
#maxEntries;
|
|
329
|
+
constructor(maxPages = () => randomInt(1, 11), maxEntries = 5000) {
|
|
330
|
+
this.#maxPagesFactory = typeof maxPages === 'function' ? maxPages : () => maxPages;
|
|
331
|
+
this.#maxEntries = maxEntries;
|
|
332
|
+
}
|
|
333
|
+
process(operationKey, streamKey, sent, value, paging) {
|
|
334
|
+
this.#prune();
|
|
335
|
+
const nextToken = tokenOf(value, paging);
|
|
336
|
+
// Case 1: First page (no cursor sent)
|
|
337
|
+
if (sent === undefined) {
|
|
338
|
+
const maxPages = Math.max(1, this.#maxPagesFactory());
|
|
339
|
+
if (maxPages === 1) {
|
|
340
|
+
if (nextToken !== undefined) {
|
|
341
|
+
cutPaging(value, paging);
|
|
342
|
+
return { cut: true, reason: 'cut pagination at max pages (1)' };
|
|
343
|
+
}
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
346
|
+
if (nextToken !== undefined) {
|
|
347
|
+
this.#tokens.set(nextToken, {
|
|
348
|
+
operationKey,
|
|
349
|
+
page: 2,
|
|
350
|
+
maxPages,
|
|
351
|
+
seenTokens: new Set([nextToken]),
|
|
352
|
+
streamKey,
|
|
353
|
+
createdAt: Date.now(),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|
|
358
|
+
// Case 2: Cursor was sent
|
|
359
|
+
const state = this.#tokens.get(sent);
|
|
360
|
+
// Subcase 2A: Re-requesting an already-terminated token
|
|
361
|
+
if (state?.terminated) {
|
|
362
|
+
emptyItems(value, paging);
|
|
363
|
+
return {
|
|
364
|
+
cut: true,
|
|
365
|
+
reason: `cut pagination at max pages (${state.maxPages})`,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
// Subcase 2B: Echo loop (server returned the exact token that was sent)
|
|
369
|
+
if (nextToken !== undefined && nextToken === sent) {
|
|
370
|
+
const didCut = cutLoop(value, paging, sent);
|
|
371
|
+
if (state) {
|
|
372
|
+
state.terminated = true;
|
|
373
|
+
}
|
|
374
|
+
if (didCut) {
|
|
375
|
+
return { cut: true, reason: 'cut a looping page token' };
|
|
376
|
+
}
|
|
377
|
+
// Where the schema leaves no room to null or delete, leave alone
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
// Subcase 2C: Cycle loop (server returned a token seen earlier in this stream)
|
|
381
|
+
if (state && nextToken !== undefined && state.seenTokens.has(nextToken)) {
|
|
382
|
+
cutPaging(value, paging);
|
|
383
|
+
state.terminated = true;
|
|
384
|
+
return { cut: true, reason: 'cut a cycling page token' };
|
|
385
|
+
}
|
|
386
|
+
// Subcase 2D: Tracked stream reached maxPages
|
|
387
|
+
if (state) {
|
|
388
|
+
if (state.page >= state.maxPages) {
|
|
389
|
+
cutPaging(value, paging);
|
|
390
|
+
state.terminated = true;
|
|
391
|
+
if (nextToken !== undefined) {
|
|
392
|
+
this.#tokens.set(nextToken, {
|
|
393
|
+
operationKey,
|
|
394
|
+
page: state.page + 1,
|
|
395
|
+
maxPages: state.maxPages,
|
|
396
|
+
seenTokens: state.seenTokens,
|
|
397
|
+
streamKey,
|
|
398
|
+
terminated: true,
|
|
399
|
+
createdAt: Date.now(),
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
cut: true,
|
|
404
|
+
reason: `cut pagination at max pages (${state.maxPages})`,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
// Not yet at maxPages: advance to next page
|
|
408
|
+
if (nextToken !== undefined) {
|
|
409
|
+
state.seenTokens.add(nextToken);
|
|
410
|
+
this.#tokens.set(nextToken, {
|
|
411
|
+
operationKey,
|
|
412
|
+
page: state.page + 1,
|
|
413
|
+
maxPages: state.maxPages,
|
|
414
|
+
seenTokens: state.seenTokens,
|
|
415
|
+
streamKey,
|
|
416
|
+
createdAt: Date.now(),
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
return undefined;
|
|
420
|
+
}
|
|
421
|
+
// Subcase 2E: Untracked token (first time seen)
|
|
422
|
+
const decodedPage = parsePageFromToken(sent, paging.style);
|
|
423
|
+
if (decodedPage !== undefined) {
|
|
424
|
+
const maxPages = Math.max(decodedPage, this.#maxPagesFactory());
|
|
425
|
+
if (decodedPage >= maxPages) {
|
|
426
|
+
cutPaging(value, paging);
|
|
427
|
+
return {
|
|
428
|
+
cut: true,
|
|
429
|
+
reason: `cut pagination at max pages (${maxPages})`,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
if (nextToken !== undefined) {
|
|
433
|
+
this.#tokens.set(nextToken, {
|
|
434
|
+
operationKey,
|
|
435
|
+
page: decodedPage + 1,
|
|
436
|
+
maxPages,
|
|
437
|
+
seenTokens: new Set([sent, nextToken]),
|
|
438
|
+
streamKey,
|
|
439
|
+
createdAt: Date.now(),
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
// Untracked arbitrary string token (e.g. 'page-1' in server.test.ts)
|
|
445
|
+
const maxPages = Math.max(2, this.#maxPagesFactory());
|
|
446
|
+
if (nextToken !== undefined) {
|
|
447
|
+
this.#tokens.set(nextToken, {
|
|
448
|
+
operationKey,
|
|
449
|
+
page: 2,
|
|
450
|
+
maxPages,
|
|
451
|
+
seenTokens: new Set([sent, nextToken]),
|
|
452
|
+
streamKey,
|
|
453
|
+
createdAt: Date.now(),
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
return undefined;
|
|
457
|
+
}
|
|
458
|
+
#prune() {
|
|
459
|
+
const now = Date.now();
|
|
460
|
+
for (const [token, entry] of this.#tokens) {
|
|
461
|
+
if (now - entry.createdAt > 10 * 60 * 1000) {
|
|
462
|
+
this.#tokens.delete(token);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
while (this.#tokens.size > this.#maxEntries) {
|
|
466
|
+
const first = this.#tokens.keys().next().value;
|
|
467
|
+
if (first !== undefined) {
|
|
468
|
+
this.#tokens.delete(first);
|
|
469
|
+
}
|
|
470
|
+
else {
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
168
476
|
/** The nearest object carrying `name`; a real body nests its envelope. */
|
|
169
477
|
function holderOf(value, name) {
|
|
170
478
|
const seen = new Set();
|
|
@@ -202,6 +510,7 @@ const numeric = (schema) => types(schema).some((t) => t === 'integer' || t === '
|
|
|
202
510
|
const boolish = (schema) => types(schema).includes('boolean');
|
|
203
511
|
const listish = (schema) => types(schema).includes('array') || schema.items !== undefined;
|
|
204
512
|
const nullable = (schema) => types(schema).includes('null');
|
|
513
|
+
const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
205
514
|
/** An undeclared parameter has only its value to be typed by. */
|
|
206
515
|
const guess = (value) => ({
|
|
207
516
|
type: /^-?\d+$/.test(value) ? 'integer' : 'string',
|
package/dist/probe.js
CHANGED
|
@@ -178,15 +178,26 @@ function text(schema, variant) {
|
|
|
178
178
|
// mistake being looked for, and a still seed makes it a fixed point — visible
|
|
179
179
|
// on the second page here instead of on somebody's client.
|
|
180
180
|
// ---------------------------------------------------------------------------
|
|
181
|
+
const isObject = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
181
182
|
/** The first page: an ordinary probe with the paging control taken back off. */
|
|
182
183
|
export function walkStart(operation, paging) {
|
|
183
184
|
const input = probesFor(operation)[0];
|
|
185
|
+
if (paging.paramIn === 'body') {
|
|
186
|
+
const body = isObject(input.body) ? { ...input.body } : {};
|
|
187
|
+
delete body[paging.param];
|
|
188
|
+
return { ...input, body };
|
|
189
|
+
}
|
|
184
190
|
const query = { ...input.query };
|
|
185
191
|
delete query[paging.param];
|
|
186
192
|
return { ...input, query };
|
|
187
193
|
}
|
|
188
194
|
/** The same request again, asking for whatever the last answer pointed at. */
|
|
189
195
|
export function nextPage(previous, paging, token) {
|
|
196
|
+
if (paging.paramIn === 'body') {
|
|
197
|
+
const body = isObject(previous.body) ? { ...previous.body } : {};
|
|
198
|
+
body[paging.param] = token;
|
|
199
|
+
return { ...previous, body };
|
|
200
|
+
}
|
|
190
201
|
return { ...previous, query: { ...previous.query, [paging.param]: token } };
|
|
191
202
|
}
|
|
192
203
|
// ---------------------------------------------------------------------------
|
package/dist/prompt.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { GeneratorInput } from './envelope.ts';
|
|
1
2
|
import { type Operation } from './spec.ts';
|
|
2
3
|
export declare const SYSTEM: string;
|
|
3
4
|
export declare function brief(operation: Operation): string;
|
|
@@ -8,4 +9,17 @@ export declare function brief(operation: Operation): string;
|
|
|
8
9
|
*/
|
|
9
10
|
export declare function instruction(operation: Operation): string;
|
|
10
11
|
export declare function retry(diagnostics: readonly string[]): string;
|
|
12
|
+
export interface ExampleRequest {
|
|
13
|
+
input: GeneratorInput;
|
|
14
|
+
output?: unknown;
|
|
15
|
+
}
|
|
16
|
+
export interface RegenerationPromptContext {
|
|
17
|
+
operation: Operation;
|
|
18
|
+
currentSource: string;
|
|
19
|
+
failingInput: GeneratorInput;
|
|
20
|
+
fault: string;
|
|
21
|
+
stderr?: string;
|
|
22
|
+
examples?: readonly ExampleRequest[];
|
|
23
|
+
}
|
|
24
|
+
export declare function regenerationInstruction(ctx: RegenerationPromptContext): string;
|
|
11
25
|
//# sourceMappingURL=prompt.d.ts.map
|
package/dist/prompt.js
CHANGED
|
@@ -39,8 +39,9 @@ export const SYSTEM = [
|
|
|
39
39
|
' this generically, by looking the names up at run time. Do the same for a',
|
|
40
40
|
' query parameter where it plainly describes the content rather than',
|
|
41
41
|
' controlling the call. A paging control — a cursor, page, offset or',
|
|
42
|
-
' page-size parameter
|
|
43
|
-
' body; see PAGINATION below where the
|
|
42
|
+
' page-size parameter (whether in query or request body) — is never content',
|
|
43
|
+
' and must not be copied into the body; see PAGINATION below where the',
|
|
44
|
+
' operation has one.',
|
|
44
45
|
'3. Every required property must be present. Optional ones may be omitted',
|
|
45
46
|
' sometimes; that is what makes a mock useful.',
|
|
46
47
|
'4. Values must suit their names, not just their types. Use `faker` for anything',
|
|
@@ -91,11 +92,12 @@ function pagination(paging) {
|
|
|
91
92
|
const advance = paging.style === 'cursor'
|
|
92
93
|
? 'the base64 of a small JSON object holding the next page index, such as {"p": 2}'
|
|
93
94
|
: "the offset of the next page — this page's offset plus its size";
|
|
95
|
+
const where = paging.paramIn === 'body' ? 'In the request `body`, ' : '';
|
|
94
96
|
const lines = [
|
|
95
97
|
'PAGINATION',
|
|
96
|
-
` This operation is paged. \`${paging.param}\` asks for a page;`,
|
|
98
|
+
` This operation is paged. ${where}\`${paging.param}\` asks for a page;`,
|
|
97
99
|
' absent or empty means the first one.',
|
|
98
|
-
' - Fabricate
|
|
100
|
+
' - Fabricate a random number of pages between 1 and 10 in total and no more.',
|
|
99
101
|
];
|
|
100
102
|
if (paging.next) {
|
|
101
103
|
lines.push(` - \`${paging.next}\` carries the token for the page after this one.`, ` Build it out of \`${paging.param}\`:`, ` ${advance}.`, ' - Never build it out of `seed`. Unpinned, the seed changes on every', ' request; pinned, it is a function of the query. A token made from it', ' either wanders or never changes.', ' - It must strictly advance. Answering with the token you were given is', ' the one failure that matters: a client following it loops forever.');
|
|
@@ -127,7 +129,7 @@ function last(paging) {
|
|
|
127
129
|
.join(' and ');
|
|
128
130
|
return [
|
|
129
131
|
` - The schema requires \`${paging.next}\` on every page, so the last page`,
|
|
130
|
-
` ends the list the other way: ${otherwise || 'an empty page'}.`,
|
|
132
|
+
` ends the list the other way: ${otherwise || 'an empty page'} (e.g. \`${paging.items || 'results'}\` as an empty list \`[]\`).`,
|
|
131
133
|
];
|
|
132
134
|
}
|
|
133
135
|
/**
|
|
@@ -149,5 +151,36 @@ export function retry(diagnostics) {
|
|
|
149
151
|
...diagnostics,
|
|
150
152
|
].join('\n');
|
|
151
153
|
}
|
|
154
|
+
export function regenerationInstruction(ctx) {
|
|
155
|
+
const { operation, currentSource, failingInput, fault, stderr, examples } = ctx;
|
|
156
|
+
const lines = [
|
|
157
|
+
brief(operation),
|
|
158
|
+
'',
|
|
159
|
+
'Embed the response schema in the file as a literal and validate against it.',
|
|
160
|
+
'',
|
|
161
|
+
'CURRENT GENERATOR CODE:',
|
|
162
|
+
'```python',
|
|
163
|
+
currentSource.trim(),
|
|
164
|
+
'```',
|
|
165
|
+
'',
|
|
166
|
+
'THE GENERATOR FAILED ON THIS REQUEST:',
|
|
167
|
+
'Input:',
|
|
168
|
+
json(failingInput),
|
|
169
|
+
'',
|
|
170
|
+
`Fault: ${fault}`,
|
|
171
|
+
];
|
|
172
|
+
if (stderr && stderr.trim()) {
|
|
173
|
+
lines.push('', 'Stderr / Traceback:', '```', stderr.trim(), '```');
|
|
174
|
+
}
|
|
175
|
+
if (examples && examples.length > 0) {
|
|
176
|
+
lines.push('', 'PREVIOUS SUCCESSFUL REQUESTS (use as examples):');
|
|
177
|
+
for (let i = 0; i < examples.length; i++) {
|
|
178
|
+
const ex = examples[i];
|
|
179
|
+
lines.push(`Example ${i + 1}:`, 'Input:', json(ex.input), 'Output produced:', json(ex.output), '');
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
lines.push('TASK:', 'Fix the generator code so that it handles the failing request without error, produces valid output that matches the schema, and continues to produce valid output for the previous example requests.', 'ANSWER WITH THE COMPLETE REVISED PYTHON FILE AND NOTHING ELSE. No explanation, no markdown fence.');
|
|
183
|
+
return lines.join('\n');
|
|
184
|
+
}
|
|
152
185
|
const json = (value) => JSON.stringify(value, null, 1);
|
|
153
186
|
//# sourceMappingURL=prompt.js.map
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type Server } from 'node:http';
|
|
2
2
|
import type { Box } from './box.ts';
|
|
3
3
|
import { type Cache } from './cache.ts';
|
|
4
|
+
import { RegenerationLimiter, RequestHistory } from './logger.ts';
|
|
5
|
+
import { PaginationLimiter } from './paging.ts';
|
|
4
6
|
import type { Router } from './router.ts';
|
|
5
7
|
import { type Checks } from './validate.ts';
|
|
6
8
|
export interface ServerOptions {
|
|
@@ -12,6 +14,12 @@ export interface ServerOptions {
|
|
|
12
14
|
seed?: number;
|
|
13
15
|
maxBody?: number;
|
|
14
16
|
onRequest?: (line: string) => void;
|
|
17
|
+
requestsDir?: string;
|
|
18
|
+
regenLimit?: number;
|
|
19
|
+
history?: RequestHistory;
|
|
20
|
+
limiter?: RegenerationLimiter;
|
|
21
|
+
maxPages?: number | (() => number);
|
|
22
|
+
paginationLimiter?: PaginationLimiter;
|
|
15
23
|
}
|
|
16
24
|
export interface Listening {
|
|
17
25
|
server: Server;
|