@usejourney/core 0.1.5 → 0.2.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/index.d.ts +69 -1
- package/dist/index.js +140 -6
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -416,6 +416,11 @@ interface HttpContext {
|
|
|
416
416
|
* `timeoutMs` always overrides this.
|
|
417
417
|
*/
|
|
418
418
|
defaultTimeoutMs?: number | null;
|
|
419
|
+
/**
|
|
420
|
+
* Directory that relative `fileFrom()` paths resolve against. The CLI sets
|
|
421
|
+
* it to the project root; unset falls back to `process.cwd()`.
|
|
422
|
+
*/
|
|
423
|
+
baseDir?: string;
|
|
419
424
|
}
|
|
420
425
|
/** Fallback request timeout (ms) when neither a step nor `HttpContext.defaultTimeoutMs` set one. */
|
|
421
426
|
declare const DEFAULT_REQUEST_TIMEOUT_MS = 60000;
|
|
@@ -456,6 +461,14 @@ interface StepOptions<E extends Endpoint> {
|
|
|
456
461
|
headers?: Lazy<Record<string, string>>;
|
|
457
462
|
body?: Lazy<unknown>;
|
|
458
463
|
timeoutMs?: number;
|
|
464
|
+
/**
|
|
465
|
+
* k6 export only — ignored by `journey run`. Overrides the slug used for this
|
|
466
|
+
* step's custom metrics when exporting with `--metrics-prefix <p>`: the step
|
|
467
|
+
* records Trend `<p>_<metric>_duration` and Counter `<p>_<metric>_tps`
|
|
468
|
+
* instead of names derived from the step name. `false` opts the step out of
|
|
469
|
+
* per-step metrics (it still counts toward `<p>_flow_duration`).
|
|
470
|
+
*/
|
|
471
|
+
metric?: string | false;
|
|
459
472
|
assert?: (res: HttpResponse<ResponseOf<E>>) => void | Promise<void>;
|
|
460
473
|
after?: (res: HttpResponse<ResponseOf<E>>) => void | Promise<void>;
|
|
461
474
|
}
|
|
@@ -715,4 +728,59 @@ type FetchResponse = Awaited<ReturnType<typeof globalThis.fetch>>;
|
|
|
715
728
|
*/
|
|
716
729
|
declare function fetch$1(input: FetchInput, init?: FetchInit): Promise<FetchResponse>;
|
|
717
730
|
|
|
718
|
-
|
|
731
|
+
/**
|
|
732
|
+
* Reference to a file on disk, used as a request body (raw binary upload) or
|
|
733
|
+
* as a part of a `multipart()` body. Created by `fileFrom()`; the bytes are
|
|
734
|
+
* read lazily at request time by the Node runner, and at k6 init time (via
|
|
735
|
+
* `open(path, "b")`) by the k6 export.
|
|
736
|
+
*/
|
|
737
|
+
interface FileRef {
|
|
738
|
+
readonly __journeyFile: true;
|
|
739
|
+
/** Path as written in the journey — relative paths resolve against the project root. */
|
|
740
|
+
readonly path: string;
|
|
741
|
+
/** MIME type sent for the file. Defaults to `application/octet-stream`. */
|
|
742
|
+
readonly contentType?: string;
|
|
743
|
+
/** Filename reported in a multipart part. Defaults to the path's basename. */
|
|
744
|
+
readonly filename?: string;
|
|
745
|
+
}
|
|
746
|
+
/** A value accepted as one part of a `multipart()` body. */
|
|
747
|
+
type MultipartValue = string | number | boolean | FileRef;
|
|
748
|
+
/** A `multipart/form-data` request body. Created by `multipart()`. */
|
|
749
|
+
interface MultipartBody {
|
|
750
|
+
readonly __journeyMultipart: true;
|
|
751
|
+
readonly parts: Readonly<Record<string, MultipartValue>>;
|
|
752
|
+
}
|
|
753
|
+
/**
|
|
754
|
+
* Reference a file to upload. Relative paths resolve against the **project
|
|
755
|
+
* root** (the directory holding `journey.config.json`), not the journey file.
|
|
756
|
+
*
|
|
757
|
+
* - As a step `body`, the file is sent as the raw request body with
|
|
758
|
+
* `Content-Type: <contentType>` (default `application/octet-stream`).
|
|
759
|
+
* - As a `multipart()` part, it becomes a file part with `filename` (default:
|
|
760
|
+
* the path's basename) and `contentType`.
|
|
761
|
+
*
|
|
762
|
+
* For `journey export k6`, the path should be a string literal so the export
|
|
763
|
+
* can pre-load it with `open(path, "b")` in the k6 init context.
|
|
764
|
+
*
|
|
765
|
+
* @example
|
|
766
|
+
* step("upload", {
|
|
767
|
+
* endpoint: endpoints.virusScan,
|
|
768
|
+
* body: () => multipart({ file: fileFrom("fixtures/sample.png", "image/png") }),
|
|
769
|
+
* });
|
|
770
|
+
*/
|
|
771
|
+
declare function fileFrom(path: string, contentType?: string, filename?: string): FileRef;
|
|
772
|
+
/**
|
|
773
|
+
* Build a `multipart/form-data` body. String/number/boolean values become text
|
|
774
|
+
* fields; `fileFrom(...)` values become file parts. The runner (and k6) set
|
|
775
|
+
* the `Content-Type: multipart/form-data; boundary=…` header — any
|
|
776
|
+
* `Content-Type` supplied in the step's `headers` is dropped for multipart
|
|
777
|
+
* bodies, because a hand-written one can't carry the generated boundary.
|
|
778
|
+
*
|
|
779
|
+
* @example
|
|
780
|
+
* body: () => multipart({ description: "id card", file: fileFrom("fixtures/id.png", "image/png") })
|
|
781
|
+
*/
|
|
782
|
+
declare function multipart(parts: Record<string, MultipartValue>): MultipartBody;
|
|
783
|
+
declare function isFileRef(value: unknown): value is FileRef;
|
|
784
|
+
declare function isMultipartBody(value: unknown): value is MultipartBody;
|
|
785
|
+
|
|
786
|
+
export { AssertionError, type BuildRequestOptions, type CacheEntry, type CacheMode, type ConsoleLoggerOptions, DEFAULT_REQUEST_TIMEOUT_MS, DiskSubJourneyCache, type Endpoint, type EndpointDescriptor, type EndpointRef, type EntryJourneyOptions, type EnvValues, type Expectation, type FileRef, type GroupEndEvent, type GroupStartEvent, type HttpContext, type HttpMethod, type HttpResponse, type InvokeJourneyOptions, type JourneyConfig, JourneyConfigSchema, type JourneyDef, type JourneyHandle, type JourneyLogger, type JourneyOptions, type JourneyResult, type K6JourneyOptions, type LoadedConfig, type LogEvent, MemorySubJourneyCache, type MultipartBody, type MultipartValue, type PipelineNode, type PlannedNode, type RequestLog, type RequestSpec, type ResponseLog, type ResponseOf, type ReusableJourneyOptions, type RunEndEvent, type RunMeta, type RunPlannedEvent, type RunRecord, type RunStartEvent, type RunSummary, SECRET_HEADERS, type StepDef, type StepEndEvent, type StepOptions, type StepResult, type StepStartEvent, type SubJourneyCache, type SubJourneyCallDef, buildRequest, clearActiveEnvironment, clearRegistry, collectPipeline, collectSubPipeline, createConsoleLogger, createSubJourneyCache, describeError, env, execute, expect, fetch$1 as fetch, fileFrom, formatOpaqueBody, getCurrentCtx, getRegisteredJourneys, invokeJourney, isEndpointRef, isFileRef, isMultipartBody, isTextualContentType, journey, listEnvironments, listRuns, loadConfig, loadEnvironment, loggerFromEnv, maskHeaders, multipart, output, planJourney, pruneRuns, readRun, resolveBaseUrl, resolveConfigPaths, resolveUrl, runAllRegistered, runJourney, setActiveEnvironment, step, subJourneyCacheKey, tryEnv, writeRun };
|
package/dist/index.js
CHANGED
|
@@ -398,6 +398,119 @@ function loggerFromEnv(env2 = process.env) {
|
|
|
398
398
|
return void 0;
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
+
// src/body.ts
|
|
402
|
+
import { randomBytes } from "crypto";
|
|
403
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
404
|
+
import { basename, isAbsolute as isAbsolute2, resolve } from "path";
|
|
405
|
+
function fileFrom(path, contentType, filename) {
|
|
406
|
+
if (typeof path !== "string" || path.length === 0) {
|
|
407
|
+
throw new TypeError("fileFrom(path): path must be a non-empty string");
|
|
408
|
+
}
|
|
409
|
+
return {
|
|
410
|
+
__journeyFile: true,
|
|
411
|
+
path,
|
|
412
|
+
...contentType !== void 0 ? { contentType } : {},
|
|
413
|
+
...filename !== void 0 ? { filename } : {}
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
function multipart(parts) {
|
|
417
|
+
if (parts === null || typeof parts !== "object") {
|
|
418
|
+
throw new TypeError("multipart(parts): parts must be an object");
|
|
419
|
+
}
|
|
420
|
+
return { __journeyMultipart: true, parts: { ...parts } };
|
|
421
|
+
}
|
|
422
|
+
function isFileRef(value) {
|
|
423
|
+
return typeof value === "object" && value !== null && value.__journeyFile === true;
|
|
424
|
+
}
|
|
425
|
+
function isMultipartBody(value) {
|
|
426
|
+
return typeof value === "object" && value !== null && value.__journeyMultipart === true;
|
|
427
|
+
}
|
|
428
|
+
function isBinary(value) {
|
|
429
|
+
return value instanceof ArrayBuffer || ArrayBuffer.isView(value) || typeof Blob !== "undefined" && value instanceof Blob;
|
|
430
|
+
}
|
|
431
|
+
function defaultContentType(body) {
|
|
432
|
+
if (isMultipartBody(body)) return null;
|
|
433
|
+
if (isFileRef(body)) return body.contentType ?? "application/octet-stream";
|
|
434
|
+
if (isBinary(body)) {
|
|
435
|
+
if (typeof Blob !== "undefined" && body instanceof Blob && body.type) return body.type;
|
|
436
|
+
return "application/octet-stream";
|
|
437
|
+
}
|
|
438
|
+
return "application/json";
|
|
439
|
+
}
|
|
440
|
+
function filePath(ref, baseDir) {
|
|
441
|
+
return isAbsolute2(ref.path) ? ref.path : resolve(baseDir ?? process.cwd(), ref.path);
|
|
442
|
+
}
|
|
443
|
+
async function readFileRef(ref, baseDir) {
|
|
444
|
+
const abs = filePath(ref, baseDir);
|
|
445
|
+
try {
|
|
446
|
+
return await readFile4(abs);
|
|
447
|
+
} catch (err) {
|
|
448
|
+
throw new Error(
|
|
449
|
+
`fileFrom(${JSON.stringify(ref.path)}): cannot read ${abs}: ${err.message}`
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function escapeDisposition(v) {
|
|
454
|
+
return v.replace(/\r/g, "%0D").replace(/\n/g, "%0A").replace(/"/g, "%22");
|
|
455
|
+
}
|
|
456
|
+
async function encodeMultipart(body, baseDir) {
|
|
457
|
+
const boundary = `----JourneyBoundary${randomBytes(12).toString("hex")}`;
|
|
458
|
+
const chunks = [];
|
|
459
|
+
const text = (s) => chunks.push(Buffer.from(s, "utf8"));
|
|
460
|
+
for (const [name, value] of Object.entries(body.parts)) {
|
|
461
|
+
text(`--${boundary}\r
|
|
462
|
+
`);
|
|
463
|
+
if (isFileRef(value)) {
|
|
464
|
+
const bytes = await readFileRef(value, baseDir);
|
|
465
|
+
const filename = value.filename ?? basename(value.path);
|
|
466
|
+
text(
|
|
467
|
+
`Content-Disposition: form-data; name="${escapeDisposition(name)}"; filename="${escapeDisposition(filename)}"\r
|
|
468
|
+
Content-Type: ${value.contentType ?? "application/octet-stream"}\r
|
|
469
|
+
\r
|
|
470
|
+
`
|
|
471
|
+
);
|
|
472
|
+
chunks.push(bytes);
|
|
473
|
+
text("\r\n");
|
|
474
|
+
} else {
|
|
475
|
+
text(
|
|
476
|
+
`Content-Disposition: form-data; name="${escapeDisposition(name)}"\r
|
|
477
|
+
\r
|
|
478
|
+
${String(value)}\r
|
|
479
|
+
`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
text(`--${boundary}--\r
|
|
484
|
+
`);
|
|
485
|
+
return {
|
|
486
|
+
body: Buffer.concat(chunks),
|
|
487
|
+
contentType: `multipart/form-data; boundary=${boundary}`
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
async function encodeBody(body, baseDir) {
|
|
491
|
+
if (typeof body === "string") return { body };
|
|
492
|
+
if (isMultipartBody(body)) return encodeMultipart(body, baseDir);
|
|
493
|
+
if (isFileRef(body)) return { body: await readFileRef(body, baseDir) };
|
|
494
|
+
if (typeof Blob !== "undefined" && body instanceof Blob) {
|
|
495
|
+
return { body: new Uint8Array(await body.arrayBuffer()) };
|
|
496
|
+
}
|
|
497
|
+
if (isBinary(body)) return { body };
|
|
498
|
+
return { body: JSON.stringify(body) };
|
|
499
|
+
}
|
|
500
|
+
function describeBodyForLog(body) {
|
|
501
|
+
const describeFile = (f) => `<file ${f.path}${f.contentType ? ` (${f.contentType})` : ""}>`;
|
|
502
|
+
if (isMultipartBody(body)) {
|
|
503
|
+
const parts = {};
|
|
504
|
+
for (const [k, v] of Object.entries(body.parts)) parts[k] = isFileRef(v) ? describeFile(v) : v;
|
|
505
|
+
return { "<multipart>": parts };
|
|
506
|
+
}
|
|
507
|
+
if (isFileRef(body)) return describeFile(body);
|
|
508
|
+
if (body instanceof ArrayBuffer) return `<binary ${body.byteLength} bytes>`;
|
|
509
|
+
if (ArrayBuffer.isView(body)) return `<binary ${body.byteLength} bytes>`;
|
|
510
|
+
if (typeof Blob !== "undefined" && body instanceof Blob) return `<binary ${body.size} bytes>`;
|
|
511
|
+
return body;
|
|
512
|
+
}
|
|
513
|
+
|
|
401
514
|
// src/http.ts
|
|
402
515
|
var DEFAULT_REQUEST_TIMEOUT_MS = 6e4;
|
|
403
516
|
function resolveUrl(endpoint, ctx, params, query) {
|
|
@@ -439,8 +552,15 @@ function buildRequest(opts, ctx) {
|
|
|
439
552
|
...opts.headers ?? {}
|
|
440
553
|
};
|
|
441
554
|
const hasBody = opts.body !== void 0;
|
|
442
|
-
if (hasBody
|
|
443
|
-
|
|
555
|
+
if (hasBody) {
|
|
556
|
+
const contentType = defaultContentType(opts.body);
|
|
557
|
+
if (contentType === null) {
|
|
558
|
+
for (const k of Object.keys(headers)) {
|
|
559
|
+
if (k.toLowerCase() === "content-type") delete headers[k];
|
|
560
|
+
}
|
|
561
|
+
} else if (!("content-type" in lower(headers))) {
|
|
562
|
+
headers["Content-Type"] = contentType;
|
|
563
|
+
}
|
|
444
564
|
}
|
|
445
565
|
const spec = { method: opts.endpoint.method, url, headers };
|
|
446
566
|
if (hasBody) spec.body = opts.body;
|
|
@@ -458,13 +578,23 @@ async function execute(req, ctx) {
|
|
|
458
578
|
method: req.method,
|
|
459
579
|
url: req.url,
|
|
460
580
|
headers: req.headers,
|
|
461
|
-
...req.body !== void 0 ? { body: req.body } : {}
|
|
581
|
+
...req.body !== void 0 ? { body: describeBodyForLog(req.body) } : {}
|
|
462
582
|
};
|
|
463
583
|
ctx.logger?.onRequest?.(logReq);
|
|
464
584
|
const f = ctx.fetchImpl ?? fetch;
|
|
465
585
|
const init = { method: req.method, headers: req.headers };
|
|
466
586
|
if (req.body !== void 0) {
|
|
467
|
-
|
|
587
|
+
let encoded;
|
|
588
|
+
try {
|
|
589
|
+
encoded = await encodeBody(req.body, ctx.baseDir);
|
|
590
|
+
} catch (err) {
|
|
591
|
+
ctx.logger?.onError?.(logReq, err, 0);
|
|
592
|
+
throw err;
|
|
593
|
+
}
|
|
594
|
+
init.body = encoded.body;
|
|
595
|
+
if (encoded.contentType !== void 0) {
|
|
596
|
+
init.headers = { ...req.headers, "Content-Type": encoded.contentType };
|
|
597
|
+
}
|
|
468
598
|
}
|
|
469
599
|
if (ctx.dispatcher !== void 0) {
|
|
470
600
|
init.dispatcher = ctx.dispatcher;
|
|
@@ -501,7 +631,7 @@ async function execute(req, ctx) {
|
|
|
501
631
|
|
|
502
632
|
// src/cache.ts
|
|
503
633
|
import { createHash } from "crypto";
|
|
504
|
-
import { mkdir as mkdir2, readFile as
|
|
634
|
+
import { mkdir as mkdir2, readFile as readFile5, rm, writeFile as writeFile2 } from "fs/promises";
|
|
505
635
|
import { join as join4 } from "path";
|
|
506
636
|
function subJourneyCacheKey(journeyName, resolvedKey) {
|
|
507
637
|
return `${journeyName}:${resolvedKey}`;
|
|
@@ -542,7 +672,7 @@ var DiskSubJourneyCache = class {
|
|
|
542
672
|
const path = this.pathFor(key);
|
|
543
673
|
let raw;
|
|
544
674
|
try {
|
|
545
|
-
raw = await
|
|
675
|
+
raw = await readFile5(path, "utf8");
|
|
546
676
|
} catch {
|
|
547
677
|
return void 0;
|
|
548
678
|
}
|
|
@@ -1215,11 +1345,14 @@ export {
|
|
|
1215
1345
|
execute,
|
|
1216
1346
|
expect,
|
|
1217
1347
|
fetch2 as fetch,
|
|
1348
|
+
fileFrom,
|
|
1218
1349
|
formatOpaqueBody,
|
|
1219
1350
|
getCurrentCtx,
|
|
1220
1351
|
getRegisteredJourneys,
|
|
1221
1352
|
invokeJourney,
|
|
1222
1353
|
isEndpointRef,
|
|
1354
|
+
isFileRef,
|
|
1355
|
+
isMultipartBody,
|
|
1223
1356
|
isTextualContentType,
|
|
1224
1357
|
journey,
|
|
1225
1358
|
listEnvironments,
|
|
@@ -1228,6 +1361,7 @@ export {
|
|
|
1228
1361
|
loadEnvironment,
|
|
1229
1362
|
loggerFromEnv,
|
|
1230
1363
|
maskHeaders,
|
|
1364
|
+
multipart,
|
|
1231
1365
|
output,
|
|
1232
1366
|
planJourney,
|
|
1233
1367
|
pruneRuns,
|