@usejourney/postman-adapter 0.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/LICENSE +21 -0
- package/dist/index.d.ts +154 -0
- package/dist/index.js +411 -0
- package/package.json +35 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 femoral <me@femoral.dev>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { StepDef } from '@usejourney/core';
|
|
2
|
+
|
|
3
|
+
interface PostmanUrl {
|
|
4
|
+
raw: string;
|
|
5
|
+
host: string[];
|
|
6
|
+
path: string[];
|
|
7
|
+
query: Array<{
|
|
8
|
+
key: string;
|
|
9
|
+
value: string;
|
|
10
|
+
}>;
|
|
11
|
+
}
|
|
12
|
+
interface PostmanHeader {
|
|
13
|
+
key: string;
|
|
14
|
+
value: string;
|
|
15
|
+
}
|
|
16
|
+
interface PostmanBody {
|
|
17
|
+
mode: "raw";
|
|
18
|
+
raw: string;
|
|
19
|
+
options: {
|
|
20
|
+
raw: {
|
|
21
|
+
language: "json";
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
interface PostmanRequest {
|
|
26
|
+
method: string;
|
|
27
|
+
header: PostmanHeader[];
|
|
28
|
+
url: PostmanUrl;
|
|
29
|
+
body?: PostmanBody;
|
|
30
|
+
}
|
|
31
|
+
interface PostmanItem {
|
|
32
|
+
name: string;
|
|
33
|
+
request: PostmanRequest;
|
|
34
|
+
/** Request-scoped pre-request / test scripts (used by state threading). */
|
|
35
|
+
event?: PostmanEvent[];
|
|
36
|
+
}
|
|
37
|
+
/** Folder-scoped variable — sub-journey inputs are serialized as these. */
|
|
38
|
+
interface PostmanVariable {
|
|
39
|
+
key: string;
|
|
40
|
+
value: string;
|
|
41
|
+
}
|
|
42
|
+
/** A Postman script — `exec` is the source split into lines. */
|
|
43
|
+
interface PostmanScript {
|
|
44
|
+
type: "text/javascript";
|
|
45
|
+
exec: string[];
|
|
46
|
+
}
|
|
47
|
+
/** A pre-request or test script bound to an item (request or folder). */
|
|
48
|
+
interface PostmanEvent {
|
|
49
|
+
listen: "prerequest" | "test";
|
|
50
|
+
script: PostmanScript;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A Postman folder. `item` holds requests and/or nested folders, so a
|
|
54
|
+
* sub-journey invocation nests as a child folder among the parent's requests.
|
|
55
|
+
* `event` carries folder-scoped pre-request / test scripts (used to translate
|
|
56
|
+
* the sub-journey output cache — see `buildItems`).
|
|
57
|
+
*/
|
|
58
|
+
interface PostmanFolder {
|
|
59
|
+
name: string;
|
|
60
|
+
item: Array<PostmanItem | PostmanFolder>;
|
|
61
|
+
description?: string;
|
|
62
|
+
variable?: PostmanVariable[];
|
|
63
|
+
event?: PostmanEvent[];
|
|
64
|
+
}
|
|
65
|
+
interface PostmanInfo {
|
|
66
|
+
name: string;
|
|
67
|
+
schema: string;
|
|
68
|
+
}
|
|
69
|
+
interface PostmanCollection {
|
|
70
|
+
info: PostmanInfo;
|
|
71
|
+
item: PostmanFolder[];
|
|
72
|
+
}
|
|
73
|
+
interface PostmanEnvValue {
|
|
74
|
+
key: string;
|
|
75
|
+
value: string;
|
|
76
|
+
enabled: boolean;
|
|
77
|
+
}
|
|
78
|
+
interface PostmanEnvironment {
|
|
79
|
+
name: string;
|
|
80
|
+
values: PostmanEnvValue[];
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The shape `buildFolder` walks. The CLI resolves a journey's `PipelineNode[]`
|
|
84
|
+
* into this tree — recursing into each sub-journey via `collectSubPipeline` —
|
|
85
|
+
* so the adapter stays free of a runtime dependency on `@usejourney/core`.
|
|
86
|
+
*/
|
|
87
|
+
type ExportNode = {
|
|
88
|
+
kind: "step";
|
|
89
|
+
def: StepDef;
|
|
90
|
+
} | {
|
|
91
|
+
kind: "sub";
|
|
92
|
+
/** Timeline display label — the call's `name` override or the child journey's name. */
|
|
93
|
+
name: string;
|
|
94
|
+
/** Resolved call inputs, written as folder-scoped Postman variables. */
|
|
95
|
+
inputs?: Record<string, unknown>;
|
|
96
|
+
/**
|
|
97
|
+
* Under `--thread-state`, a sub-journey call with **dynamic** `inputs`
|
|
98
|
+
* (`() => ({ … })`) carries the closure source plus the child body's
|
|
99
|
+
* parameter name. The folder pre-request re-runs the closure against the
|
|
100
|
+
* carrier and seeds the result under `inputParam` so the child's own
|
|
101
|
+
* closures resolve `input.*`. Absent for static / `env()` inputs.
|
|
102
|
+
*/
|
|
103
|
+
inputsSrc?: string;
|
|
104
|
+
inputParam?: string;
|
|
105
|
+
/** The child journey's own pipeline, already resolved. */
|
|
106
|
+
nodes: ExportNode[];
|
|
107
|
+
/**
|
|
108
|
+
* Composite cache key `childName:resolvedKey`, resolved at export time.
|
|
109
|
+
* Present only when the call opts into the cache (`cacheKey` set,
|
|
110
|
+
* `cache !== "off"`); its presence emits the folder cache scripts.
|
|
111
|
+
*/
|
|
112
|
+
cacheKey?: string;
|
|
113
|
+
/** Per-call TTL in ms; absent → cache for the whole collection run. */
|
|
114
|
+
cacheTtlMs?: number;
|
|
115
|
+
/**
|
|
116
|
+
* The call's `after(out)` / `assert(out)` hooks. Under `--thread-state`
|
|
117
|
+
* they run on the sub-folder's terminal request, consuming the child's
|
|
118
|
+
* `output(...)`.
|
|
119
|
+
*/
|
|
120
|
+
after?: (out: unknown) => void | Promise<void>;
|
|
121
|
+
assert?: (out: unknown) => void | Promise<void>;
|
|
122
|
+
};
|
|
123
|
+
declare const ENV_PROXY: Record<string, string>;
|
|
124
|
+
/** Options for {@link buildFolder}. */
|
|
125
|
+
interface BuildFolderOpts {
|
|
126
|
+
/**
|
|
127
|
+
* Experimental: thread journey closure-state through a collection variable so
|
|
128
|
+
* sub-journey outputs and step-to-step state reach later requests. Emits
|
|
129
|
+
* per-request pre-request/test scripts and a folder-level carrier reset.
|
|
130
|
+
*/
|
|
131
|
+
threadState?: boolean;
|
|
132
|
+
/**
|
|
133
|
+
* With `threadState`: emit non-enforcing assertions (legacy swallow-all) so a
|
|
134
|
+
* failing `expect()` stays a console line instead of a red `pm.test`. Off by
|
|
135
|
+
* default — threaded assertions enforce. No effect without `threadState`.
|
|
136
|
+
*/
|
|
137
|
+
lenient?: boolean;
|
|
138
|
+
}
|
|
139
|
+
/** Build the top-level Postman folder for a journey from its resolved tree. */
|
|
140
|
+
declare function buildFolder(name: string, nodes: ReadonlyArray<ExportNode>, opts?: BuildFolderOpts): Promise<PostmanFolder>;
|
|
141
|
+
/** Options for {@link buildCollection}. */
|
|
142
|
+
interface BuildCollectionOpts {
|
|
143
|
+
/**
|
|
144
|
+
* Prepend a skipped reset request that clears the threaded carrier + cache
|
|
145
|
+
* slots at the start of a run, so a Postman Runner re-run starts cold (Postman
|
|
146
|
+
* persists collection variables across runs; Newman does not). Set when
|
|
147
|
+
* exporting with `--thread-state`.
|
|
148
|
+
*/
|
|
149
|
+
reset?: boolean;
|
|
150
|
+
}
|
|
151
|
+
declare function buildCollection(name: string, folders: PostmanFolder[], opts?: BuildCollectionOpts): PostmanCollection;
|
|
152
|
+
declare function buildEnvironment(name: string, values: Record<string, string>): PostmanEnvironment;
|
|
153
|
+
|
|
154
|
+
export { type BuildCollectionOpts, type BuildFolderOpts, ENV_PROXY, type ExportNode, type PostmanBody, type PostmanCollection, type PostmanEnvValue, type PostmanEnvironment, type PostmanEvent, type PostmanFolder, type PostmanHeader, type PostmanInfo, type PostmanItem, type PostmanRequest, type PostmanScript, type PostmanUrl, type PostmanVariable, buildCollection, buildEnvironment, buildFolder };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
// src/stateThread.ts
|
|
2
|
+
var STATE_VAR = "__journey_state";
|
|
3
|
+
function toCallable(src) {
|
|
4
|
+
const s = src.trim();
|
|
5
|
+
if (/^(async\s+)?function\b/.test(s) || /^(async\s*)?\(/.test(s) || /^(async\s+)?[A-Za-z_$][\w$]*\s*=>/.test(s)) {
|
|
6
|
+
return `(${s})`;
|
|
7
|
+
}
|
|
8
|
+
return `(${s.replace(
|
|
9
|
+
/^(async\s+)?[A-Za-z_$][\w$]*\s*\(/,
|
|
10
|
+
(m) => /^async/.test(m) ? "async function(" : "function("
|
|
11
|
+
)})`;
|
|
12
|
+
}
|
|
13
|
+
var JS_RESERVED = /* @__PURE__ */ new Set([
|
|
14
|
+
"if",
|
|
15
|
+
"else",
|
|
16
|
+
"for",
|
|
17
|
+
"while",
|
|
18
|
+
"do",
|
|
19
|
+
"return",
|
|
20
|
+
"var",
|
|
21
|
+
"let",
|
|
22
|
+
"const",
|
|
23
|
+
"function",
|
|
24
|
+
"new",
|
|
25
|
+
"typeof",
|
|
26
|
+
"instanceof",
|
|
27
|
+
"in",
|
|
28
|
+
"of",
|
|
29
|
+
"this",
|
|
30
|
+
"true",
|
|
31
|
+
"false",
|
|
32
|
+
"null",
|
|
33
|
+
"undefined",
|
|
34
|
+
"void",
|
|
35
|
+
"delete",
|
|
36
|
+
"try",
|
|
37
|
+
"catch",
|
|
38
|
+
"throw",
|
|
39
|
+
"switch",
|
|
40
|
+
"case",
|
|
41
|
+
"break",
|
|
42
|
+
"continue",
|
|
43
|
+
"async",
|
|
44
|
+
"await"
|
|
45
|
+
]);
|
|
46
|
+
function writeTargets(src, locals) {
|
|
47
|
+
const skip = /* @__PURE__ */ new Set([...locals, ...JS_RESERVED]);
|
|
48
|
+
const out = /* @__PURE__ */ new Set();
|
|
49
|
+
const re = /(^|[^.=!<>+\-*/%&|^\w$])([A-Za-z_$][\w$]*)\s*=(?![=>])/g;
|
|
50
|
+
let m;
|
|
51
|
+
while ((m = re.exec(src)) !== null) {
|
|
52
|
+
const name = m[2];
|
|
53
|
+
if (!skip.has(name)) out.add(name);
|
|
54
|
+
}
|
|
55
|
+
return [...out];
|
|
56
|
+
}
|
|
57
|
+
function paramNames(src) {
|
|
58
|
+
const s = src.trim();
|
|
59
|
+
const arrowBare = /^(async\s+)?([A-Za-z_$][\w$]*)\s*=>/.exec(s);
|
|
60
|
+
if (arrowBare) return [arrowBare[2]];
|
|
61
|
+
const paren = /^(?:async\s+)?(?:[A-Za-z_$][\w$]*)?\s*\(([^)]*)\)/.exec(s);
|
|
62
|
+
if (!paren) return [];
|
|
63
|
+
return paren[1].split(",").map((p) => p.trim().split(/[=:\s]/)[0]).filter(Boolean);
|
|
64
|
+
}
|
|
65
|
+
var PRELUDE = [
|
|
66
|
+
`var __s = JSON.parse(pm.collectionVariables.get(${JSON.stringify(STATE_VAR)}) || "{}");`,
|
|
67
|
+
`function __save(){ pm.collectionVariables.set(${JSON.stringify(STATE_VAR)}, JSON.stringify(__s)); }`,
|
|
68
|
+
`function env(k){ var v = pm.variables.get(k); return v === undefined ? pm.collectionVariables.get(k) : v; }`
|
|
69
|
+
];
|
|
70
|
+
function testShims(strict) {
|
|
71
|
+
if (!strict) {
|
|
72
|
+
return [
|
|
73
|
+
`function expect(v){ return {`,
|
|
74
|
+
` toBe:function(e){ pm.expect(v).to.eql(e); },`,
|
|
75
|
+
` toEqual:function(e){ pm.expect(v).to.eql(e); },`,
|
|
76
|
+
` toBeDefined:function(){ pm.expect(v).to.not.be.undefined; },`,
|
|
77
|
+
` toContain:function(e){ pm.expect(v).to.include(e); },`,
|
|
78
|
+
` toMatch:function(e){ pm.expect(String(v)).to.match(typeof e === "string" ? new RegExp(e) : e); },`,
|
|
79
|
+
`}; }`,
|
|
80
|
+
`function output(v){ __s.__out = v; }`
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
return [
|
|
84
|
+
`var __ai = 0;`,
|
|
85
|
+
`function __label(){ return (typeof __base !== "undefined" ? __base : "assertion") + " \xB7 assert " + (++__ai); }`,
|
|
86
|
+
`function expect(v){ return {`,
|
|
87
|
+
` toBe:function(e){ pm.test(__label(), function(){ pm.expect(v).to.eql(e); }); },`,
|
|
88
|
+
` toEqual:function(e){ pm.test(__label(), function(){ pm.expect(v).to.eql(e); }); },`,
|
|
89
|
+
` toBeDefined:function(){ pm.test(__label(), function(){ pm.expect(v).to.not.be.undefined; }); },`,
|
|
90
|
+
` toContain:function(e){ pm.test(__label(), function(){ pm.expect(v).to.include(e); }); },`,
|
|
91
|
+
` toMatch:function(e){ pm.test(__label(), function(){ pm.expect(String(v)).to.match(typeof e === "string" ? new RegExp(e) : e); }); },`,
|
|
92
|
+
`}; }`,
|
|
93
|
+
`function output(v){ __s.__out = v; }`
|
|
94
|
+
];
|
|
95
|
+
}
|
|
96
|
+
function runLazy(src) {
|
|
97
|
+
return `(new Function("__s","env","with(__s){ try { return ${toCallable(src).replace(/"/g, '\\"')}(); } catch (e) { return undefined; } }"))(__s, env)`;
|
|
98
|
+
}
|
|
99
|
+
function isFn(v) {
|
|
100
|
+
return typeof v === "function";
|
|
101
|
+
}
|
|
102
|
+
function subInputSeed(paramName, inputsSrc) {
|
|
103
|
+
return [
|
|
104
|
+
...PRELUDE,
|
|
105
|
+
`var __in = ${runLazy(inputsSrc)};`,
|
|
106
|
+
`if (__in && typeof __in === "object") __s[${JSON.stringify(paramName)}] = __in;`,
|
|
107
|
+
`__save();`
|
|
108
|
+
];
|
|
109
|
+
}
|
|
110
|
+
function stepPrerequest(step) {
|
|
111
|
+
const lines = [];
|
|
112
|
+
const { headers, params, query, body } = step.options;
|
|
113
|
+
if (isFn(headers)) {
|
|
114
|
+
lines.push(
|
|
115
|
+
`var __h = ${runLazy(headers.toString())};`,
|
|
116
|
+
`if (__h) Object.keys(__h).forEach(function(k){ pm.request.headers.upsert({ key: k, value: String(__h[k]) }); });`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (isFn(params)) {
|
|
120
|
+
lines.push(
|
|
121
|
+
`var __p = ${runLazy(params.toString())};`,
|
|
122
|
+
`if (__p) Object.keys(__p).forEach(function(k){ pm.variables.set(k, String(__p[k])); });`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (isFn(query)) {
|
|
126
|
+
lines.push(
|
|
127
|
+
`var __q = ${runLazy(query.toString())};`,
|
|
128
|
+
`if (__q) Object.keys(__q).forEach(function(k){ if (__q[k] !== undefined) pm.variables.set("__q_" + k, String(__q[k])); });`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
if (isFn(body)) {
|
|
132
|
+
lines.push(
|
|
133
|
+
`var __b = ${runLazy(body.toString())};`,
|
|
134
|
+
`pm.variables.set("__journey_body", __b === undefined ? "" : (typeof __b === "string" ? __b : JSON.stringify(__b)));`
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
if (lines.length === 0) return null;
|
|
138
|
+
return [...PRELUDE, ...lines];
|
|
139
|
+
}
|
|
140
|
+
function runHook(src, resExpr) {
|
|
141
|
+
const targets = writeTargets(src, paramNames(src));
|
|
142
|
+
const seed = targets.map(
|
|
143
|
+
(t) => `if (!(${JSON.stringify(t)} in __s)) __s[${JSON.stringify(t)}] = undefined;`
|
|
144
|
+
);
|
|
145
|
+
const callable = toCallable(src).replace(/"/g, '\\"');
|
|
146
|
+
return [
|
|
147
|
+
...seed,
|
|
148
|
+
`try { (new Function("__s","__arg","env","expect","output","with(__s){ ${callable}(__arg); }"))(__s, ${resExpr}, env, expect, output); } catch (e) { console.log("journey hook:", e && e.message ? e.message : e); }`
|
|
149
|
+
];
|
|
150
|
+
}
|
|
151
|
+
function stepTest(step, parentHooks = [], cacheStore, strict = true) {
|
|
152
|
+
const assert = step.options.assert;
|
|
153
|
+
const after = step.options.after;
|
|
154
|
+
if (!isFn(assert) && !isFn(after) && parentHooks.length === 0 && !cacheStore) return null;
|
|
155
|
+
const lines = [
|
|
156
|
+
...PRELUDE,
|
|
157
|
+
`var __base = ${JSON.stringify(step.name)};`,
|
|
158
|
+
...testShims(strict),
|
|
159
|
+
`var res = { status: pm.response.code, headers: pm.response.headers && pm.response.headers.toObject ? pm.response.headers.toObject() : {}, body: (function(){ try { return pm.response.json(); } catch (e) { return pm.response.text(); } })() };`
|
|
160
|
+
];
|
|
161
|
+
if (isFn(assert)) lines.push(...runHook(assert.toString(), "res"));
|
|
162
|
+
if (isFn(after)) lines.push(...runHook(after.toString(), "res"));
|
|
163
|
+
for (const hook of parentHooks) lines.push(...runHook(hook.toString(), "__s.__out"));
|
|
164
|
+
lines.push(`__save();`);
|
|
165
|
+
if (cacheStore) lines.push(...cacheStoreLines(cacheStore));
|
|
166
|
+
return lines;
|
|
167
|
+
}
|
|
168
|
+
function hookLines(srcs, outExpr) {
|
|
169
|
+
const lines = [];
|
|
170
|
+
for (const src of srcs) lines.push(...runHook(src, outExpr));
|
|
171
|
+
return lines;
|
|
172
|
+
}
|
|
173
|
+
function cacheHitPrerequest(store, hooks, strict = true) {
|
|
174
|
+
const hookSrcs = hooks.map((h) => h.toString());
|
|
175
|
+
return [
|
|
176
|
+
...PRELUDE,
|
|
177
|
+
`var __base = "sub-journey";`,
|
|
178
|
+
...testShims(strict),
|
|
179
|
+
`var __exp = pm.collectionVariables.get(${JSON.stringify(store.jcVar)});`,
|
|
180
|
+
`if (__exp && Date.now() < Number(__exp)) {`,
|
|
181
|
+
` var __v = pm.collectionVariables.get(${JSON.stringify(store.jcvVar)});`,
|
|
182
|
+
` if (__v) { try { __s.__out = JSON.parse(__v); } catch (e) {} }`,
|
|
183
|
+
...hookLines(hookSrcs, "__s.__out").map((l) => " " + l),
|
|
184
|
+
` __save();`,
|
|
185
|
+
` pm.execution.skipRequest();`,
|
|
186
|
+
`}`
|
|
187
|
+
];
|
|
188
|
+
}
|
|
189
|
+
function cacheSkipPrerequest(store) {
|
|
190
|
+
return [
|
|
191
|
+
`var __exp = pm.collectionVariables.get(${JSON.stringify(store.jcVar)});`,
|
|
192
|
+
`if (__exp && Date.now() < Number(__exp)) { pm.execution.skipRequest(); }`
|
|
193
|
+
];
|
|
194
|
+
}
|
|
195
|
+
function cacheExpirySet(store) {
|
|
196
|
+
return [
|
|
197
|
+
`var __exp = pm.collectionVariables.get(${JSON.stringify(store.jcVar)});`,
|
|
198
|
+
`if (!(__exp && Date.now() < Number(__exp))) { pm.collectionVariables.set(${JSON.stringify(store.jcVar)}, String(${store.expExpr})); }`
|
|
199
|
+
];
|
|
200
|
+
}
|
|
201
|
+
function cacheStoreLines(store) {
|
|
202
|
+
return [
|
|
203
|
+
`var __exp = pm.collectionVariables.get(${JSON.stringify(store.jcVar)});`,
|
|
204
|
+
`if (!(__exp && Date.now() < Number(__exp))) {`,
|
|
205
|
+
` pm.collectionVariables.set(${JSON.stringify(store.jcVar)}, String(${store.expExpr}));`,
|
|
206
|
+
` if (__s.__out !== undefined) pm.collectionVariables.set(${JSON.stringify(store.jcvVar)}, JSON.stringify(__s.__out));`,
|
|
207
|
+
`}`
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
function journeyResetEvent(journeyName) {
|
|
211
|
+
const v = JSON.stringify(STATE_VAR);
|
|
212
|
+
const n = JSON.stringify(journeyName);
|
|
213
|
+
return [
|
|
214
|
+
`var __s = JSON.parse(pm.collectionVariables.get(${v}) || "{}");`,
|
|
215
|
+
`if (__s.__journey !== ${n}) { pm.collectionVariables.set(${v}, JSON.stringify({ __journey: ${n} })); }`
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
function collectionResetScript() {
|
|
219
|
+
return [
|
|
220
|
+
`var __all = pm.collectionVariables.toObject ? pm.collectionVariables.toObject() : {};`,
|
|
221
|
+
`Object.keys(__all).forEach(function(k){`,
|
|
222
|
+
` if (k === ${JSON.stringify(STATE_VAR)} || k.indexOf("__jc_") === 0 || k.indexOf("__jcv_") === 0) {`,
|
|
223
|
+
` pm.collectionVariables.unset(k);`,
|
|
224
|
+
` }`,
|
|
225
|
+
`});`,
|
|
226
|
+
`if (pm.execution && pm.execution.skipRequest) pm.execution.skipRequest();`
|
|
227
|
+
];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/index.ts
|
|
231
|
+
var SUB_JOURNEY_NOTE = "Sub-journey invocation. Postman's sidebar lists folders above sibling requests, so a sub-journey invoked mid-pipeline looks reordered there \u2014 execution order (Collection Runner / Newman) still follows the journey pipeline.";
|
|
232
|
+
var CACHE_NOTE = " Cached: a collection variable holds an expiry timestamp; while it is valid this folder's requests are skipped (pm.execution.skipRequest), so a shared sub-journey runs once per collection run. The window opens on the child's terminal request, so a multi-request child runs fully on the cold pass and skips as a whole on a hit. Without --thread-state the child's output() is not carried into Postman variables; with it, a hit still delivers the output.";
|
|
233
|
+
var ENV_PROXY = new Proxy({}, {
|
|
234
|
+
get(_target, key) {
|
|
235
|
+
return `{{${key}}}`;
|
|
236
|
+
},
|
|
237
|
+
has() {
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
async function tryResolve(v) {
|
|
242
|
+
if (v === void 0) return void 0;
|
|
243
|
+
if (typeof v !== "function") return v;
|
|
244
|
+
try {
|
|
245
|
+
return await v();
|
|
246
|
+
} catch {
|
|
247
|
+
return void 0;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
function isUnresolved(v) {
|
|
251
|
+
return v == null || typeof v === "number" && Number.isNaN(v);
|
|
252
|
+
}
|
|
253
|
+
function buildPostmanUrl(path, baseUrl, params, query) {
|
|
254
|
+
const substituted = path.replace(/\{([^}]+)\}/g, (_, key) => {
|
|
255
|
+
const val = params?.[key];
|
|
256
|
+
return isUnresolved(val) ? `{{${key}}}` : encodeURIComponent(String(val));
|
|
257
|
+
});
|
|
258
|
+
const raw = baseUrl + substituted;
|
|
259
|
+
const segments = substituted.replace(/^\//, "").split("/").filter(Boolean);
|
|
260
|
+
const queryItems = Object.entries(query ?? {}).filter(([, v]) => !isUnresolved(v)).map(([key, value]) => ({ key, value: String(value) }));
|
|
261
|
+
return {
|
|
262
|
+
raw: queryItems.length ? `${raw}?${queryItems.map((q) => `${q.key}=${q.value}`).join("&")}` : raw,
|
|
263
|
+
host: [baseUrl],
|
|
264
|
+
path: segments,
|
|
265
|
+
query: queryItems
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
async function buildRequestItem(s, threadState = false) {
|
|
269
|
+
const params = threadState && typeof s.options.params === "function" ? void 0 : await tryResolve(s.options.params);
|
|
270
|
+
let query = await tryResolve(s.options.query);
|
|
271
|
+
if (threadState && typeof s.options.query === "function" && query && typeof query === "object") {
|
|
272
|
+
query = Object.fromEntries(Object.keys(query).map((k) => [k, `{{__q_${k}}}`]));
|
|
273
|
+
}
|
|
274
|
+
const headers = await tryResolve(s.options.headers);
|
|
275
|
+
const body = threadState && typeof s.options.body === "function" ? "{{__journey_body}}" : await tryResolve(s.options.body);
|
|
276
|
+
const baseUrl = s.options.endpoint.baseUrl ?? "{{BASE_URL}}";
|
|
277
|
+
const url = buildPostmanUrl(s.options.endpoint.path, baseUrl, params, query);
|
|
278
|
+
const headerItems = Object.entries(headers ?? {}).map(([key, value]) => ({
|
|
279
|
+
key,
|
|
280
|
+
value: String(value)
|
|
281
|
+
}));
|
|
282
|
+
let postmanBody;
|
|
283
|
+
if (body !== void 0) {
|
|
284
|
+
postmanBody = {
|
|
285
|
+
mode: "raw",
|
|
286
|
+
raw: typeof body === "string" ? body : JSON.stringify(body, null, 2),
|
|
287
|
+
options: { raw: { language: "json" } }
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
name: s.name,
|
|
292
|
+
request: {
|
|
293
|
+
method: s.options.endpoint.method.toUpperCase(),
|
|
294
|
+
header: headerItems,
|
|
295
|
+
url,
|
|
296
|
+
...postmanBody ? { body: postmanBody } : {}
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function inputsToVariables(inputs) {
|
|
301
|
+
if (!inputs || typeof inputs !== "object") return [];
|
|
302
|
+
return Object.entries(inputs).map(([key, value]) => ({
|
|
303
|
+
key,
|
|
304
|
+
value: typeof value === "string" ? value : JSON.stringify(value)
|
|
305
|
+
}));
|
|
306
|
+
}
|
|
307
|
+
var SCRIPT = (exec) => ({ type: "text/javascript", exec });
|
|
308
|
+
async function buildItems(nodes, opts, parentHooks = [], cacheStore) {
|
|
309
|
+
const items = [];
|
|
310
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
311
|
+
const node = nodes[i];
|
|
312
|
+
const isLast = i === nodes.length - 1;
|
|
313
|
+
const hooksHere = isLast ? parentHooks : [];
|
|
314
|
+
const storeHere = isLast ? cacheStore : void 0;
|
|
315
|
+
if (node.kind === "step") {
|
|
316
|
+
const item = await buildRequestItem(node.def, opts.threadState);
|
|
317
|
+
if (opts.threadState) {
|
|
318
|
+
const events = [];
|
|
319
|
+
const pre = stepPrerequest(node.def);
|
|
320
|
+
const test = stepTest(node.def, hooksHere, storeHere, opts.strict);
|
|
321
|
+
if (pre) events.push({ listen: "prerequest", script: SCRIPT(pre) });
|
|
322
|
+
if (test) events.push({ listen: "test", script: SCRIPT(test) });
|
|
323
|
+
if (events.length > 0) item.event = events;
|
|
324
|
+
} else if (storeHere) {
|
|
325
|
+
item.event = [{ listen: "test", script: SCRIPT(cacheExpirySet(storeHere)) }];
|
|
326
|
+
}
|
|
327
|
+
items.push(item);
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const ownHooks = opts.threadState ? [node.assert, node.after].filter(Boolean) : [];
|
|
331
|
+
const childHooks = [...ownHooks, ...hooksHere];
|
|
332
|
+
let childStore = storeHere;
|
|
333
|
+
const folderPre = [];
|
|
334
|
+
if (opts.threadState && node.inputParam && node.inputsSrc) {
|
|
335
|
+
folderPre.push(...subInputSeed(node.inputParam, node.inputsSrc));
|
|
336
|
+
}
|
|
337
|
+
if (node.cacheKey) {
|
|
338
|
+
const safe = node.cacheKey.replace(/[^A-Za-z0-9]+/g, "_");
|
|
339
|
+
const store = {
|
|
340
|
+
jcVar: `__jc_${safe}`,
|
|
341
|
+
jcvVar: `__jcv_${safe}`,
|
|
342
|
+
expExpr: node.cacheTtlMs !== void 0 ? `Date.now() + ${node.cacheTtlMs}` : "9999999999999"
|
|
343
|
+
};
|
|
344
|
+
childStore = store;
|
|
345
|
+
folderPre.push(
|
|
346
|
+
...opts.threadState ? cacheHitPrerequest(store, ownHooks, opts.strict) : cacheSkipPrerequest(store)
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
const folder = {
|
|
350
|
+
name: node.name,
|
|
351
|
+
item: await buildItems(node.nodes, opts, childHooks, childStore),
|
|
352
|
+
description: node.cacheKey ? SUB_JOURNEY_NOTE + CACHE_NOTE : SUB_JOURNEY_NOTE
|
|
353
|
+
};
|
|
354
|
+
const variables = inputsToVariables(node.inputs);
|
|
355
|
+
if (variables.length > 0) folder.variable = variables;
|
|
356
|
+
if (folderPre.length > 0) folder.event = [{ listen: "prerequest", script: SCRIPT(folderPre) }];
|
|
357
|
+
items.push(folder);
|
|
358
|
+
}
|
|
359
|
+
return items;
|
|
360
|
+
}
|
|
361
|
+
async function buildFolder(name, nodes, opts = {}) {
|
|
362
|
+
const threadState = opts.threadState ?? false;
|
|
363
|
+
const strict = threadState && !opts.lenient;
|
|
364
|
+
const folder = { name, item: await buildItems(nodes, { threadState, strict }) };
|
|
365
|
+
if (threadState) {
|
|
366
|
+
folder.event = [{ listen: "prerequest", script: SCRIPT(journeyResetEvent(name)) }];
|
|
367
|
+
}
|
|
368
|
+
return folder;
|
|
369
|
+
}
|
|
370
|
+
function resetFolder() {
|
|
371
|
+
return {
|
|
372
|
+
name: "Journey: reset state (auto)",
|
|
373
|
+
item: [
|
|
374
|
+
{
|
|
375
|
+
name: "reset",
|
|
376
|
+
event: [{ listen: "prerequest", script: SCRIPT(collectionResetScript()) }],
|
|
377
|
+
request: {
|
|
378
|
+
method: "GET",
|
|
379
|
+
header: [],
|
|
380
|
+
url: {
|
|
381
|
+
raw: "https://journey.invalid/reset",
|
|
382
|
+
host: ["journey", "invalid"],
|
|
383
|
+
path: ["reset"],
|
|
384
|
+
query: []
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
]
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function buildCollection(name, folders, opts = {}) {
|
|
392
|
+
return {
|
|
393
|
+
info: {
|
|
394
|
+
name,
|
|
395
|
+
schema: "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
|
396
|
+
},
|
|
397
|
+
item: opts.reset ? [resetFolder(), ...folders] : [...folders]
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function buildEnvironment(name, values) {
|
|
401
|
+
return {
|
|
402
|
+
name,
|
|
403
|
+
values: Object.entries(values).map(([key, value]) => ({ key, value, enabled: true }))
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
export {
|
|
407
|
+
ENV_PROXY,
|
|
408
|
+
buildCollection,
|
|
409
|
+
buildEnvironment,
|
|
410
|
+
buildFolder
|
|
411
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usejourney/postman-adapter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/femoral/journey.git",
|
|
8
|
+
"directory": "packages/postman-adapter"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/femoral/journey#readme",
|
|
11
|
+
"bugs": "https://github.com/femoral/journey/issues",
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist"
|
|
26
|
+
],
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@usejourney/core": "0.1.0"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsup src/index.ts --format esm --dts --clean",
|
|
32
|
+
"typecheck": "tsc --noEmit",
|
|
33
|
+
"test": "vitest run --passWithNoTests"
|
|
34
|
+
}
|
|
35
|
+
}
|