@aloma.io/integration-sdk 3.8.68 → 3.8.70
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/FINDINGS-PLAT-47.md +253 -0
- package/OPENAPI_TO_CONNECTOR.md +4 -1
- package/build/internal/fetcher/fetcher.d.mts +8 -0
- package/build/internal/fetcher/fetcher.mjs +47 -3
- package/build/openapi-to-connector.mjs +5 -2
- package/build/transform/index.d.mts +2 -2
- package/build/transform/index.mjs +493 -78
- package/package.json +3 -3
- package/src/internal/fetcher/fetcher.mts +52 -3
- package/src/openapi-to-connector.mts +6 -2
- package/src/transform/index.mts +530 -96
- package/test/fetcher-query-params.test.mjs +294 -0
- package/test/fixtures/transform/basic/expected.json +9 -0
- package/test/fixtures/transform/basic/src/controller/abstract.mts +5 -0
- package/test/fixtures/transform/basic/src/controller/index.mts +30 -0
- package/test/fixtures/transform/basic/tsconfig.json +1 -0
- package/test/fixtures/transform/interface-param/src/controller/abstract.mts +5 -0
- package/test/fixtures/transform/interface-param/src/controller/index.mts +12 -0
- package/test/fixtures/transform/interface-param/tsconfig.json +1 -0
- package/test/fixtures/transform/overloads/expected.json +6 -0
- package/test/fixtures/transform/overloads/src/controller/abstract.mts +5 -0
- package/test/fixtures/transform/overloads/src/controller/index.mts +11 -0
- package/test/fixtures/transform/overloads/tsconfig.json +1 -0
- package/test/fixtures/transform/tdd-stub/expected.json +8 -0
- package/test/fixtures/transform/tdd-stub/src/controller/abstract.mts +6 -0
- package/test/fixtures/transform/tdd-stub/src/controller/index.mts +204 -0
- package/test/fixtures/transform/tdd-stub/tsconfig.json +1 -0
- package/test/fixtures/transform/two-exports/src/controller/abstract.mts +5 -0
- package/test/fixtures/transform/two-exports/src/controller/index.mts +9 -0
- package/test/fixtures/transform/two-exports/tsconfig.json +1 -0
- package/test/fixtures/transform/type-error/src/controller/abstract.mts +5 -0
- package/test/fixtures/transform/type-error/src/controller/index.mts +7 -0
- package/test/fixtures/transform/type-error/tsconfig.json +1 -0
- package/test/fixtures/transform/typed/expected.json +8 -0
- package/test/fixtures/transform/typed/src/controller/abstract.mts +5 -0
- package/test/fixtures/transform/typed/src/controller/index.mts +13 -0
- package/test/fixtures/transform/typed/tsconfig.json +1 -0
- package/test/fixtures/transform/visibility/expected.json +9 -0
- package/test/fixtures/transform/visibility/src/controller/abstract.mts +5 -0
- package/test/fixtures/transform/visibility/src/controller/index.mts +19 -0
- package/test/fixtures/transform/visibility/tsconfig.json +1 -0
- package/test/openapi-to-connector.test.mts +7 -0
- package/test/transform.test.mjs +68 -0
- package/tools/mutation-run.mjs +23 -5
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
Issue: PLAT-47
|
|
2
|
+
|
|
3
|
+
# PLAT-47: the SDK Fetcher sends `fetchOptions.params` as the query string
|
|
4
|
+
|
|
5
|
+
Run 20261026-104553, on `origin/main` 70a9898 (`@aloma.io/integration-sdk` 3.8.69). The package version is unchanged.
|
|
6
|
+
|
|
7
|
+
## Update, run 20261026-111752: the fix is committed (e8f0fbb)
|
|
8
|
+
|
|
9
|
+
After `integration-sdk/node_modules` was reinstalled from the lockfile (lockfile unchanged), `yarn build` exits 0 and
|
|
10
|
+
`yarn test` passes in full: 5 mocha files and 8 standalone scripts, 0 failed, with fetcher-query-params at 19/19. The
|
|
11
|
+
stashed work was the same, byte for byte, as `.aeh/runs/20261026-110540/unfinished.patch`. Test, code and docs were
|
|
12
|
+
committed together as e8f0fbb, "PLAT-47: the Fetcher sends fetchOptions.params as the query string". The gate passed
|
|
13
|
+
every check on tree 370dfe838730: secret-files, secrets, static, dependencies, tdd, model and probe. The sections
|
|
14
|
+
below are the history of the earlier refusals. Their "not committed" status no longer holds.
|
|
15
|
+
|
|
16
|
+
## Status (runs 20261026-104553 and 20261026-110540, superseded): the fix is written and verified in the box but NOT committed (gate blocker)
|
|
17
|
+
|
|
18
|
+
The box's commit gate refused the code commit:
|
|
19
|
+
|
|
20
|
+
tdd fail no test command is configured for this project in the harness (config/tdd.json),
|
|
21
|
+
so a code change cannot be accepted
|
|
22
|
+
|
|
23
|
+
The gate's test command (`AEH_TDD`) is set from the Mac only; the box cannot change it.
|
|
24
|
+
|
|
25
|
+
- **Committed on this branch:** this FINDINGS file only. The gate's test-first check needs no test command for a
|
|
26
|
+
docs-only commit.
|
|
27
|
+
- **Not committed:** `src/internal/fetcher/fetcher.mts`, `test/fetcher-query-params.test.mjs`,
|
|
28
|
+
`tools/mutation-run.mjs` and the `OPENAPI_TO_CONNECTOR.md` edit. They are left in the box's working tree, and saved
|
|
29
|
+
as one patch at `.aeh/runs/20261026-104553/PLAT-47-fix.patch`.
|
|
30
|
+
- **Why the test was not committed on its own:** the gate requires the new test in the same commit as the code
|
|
31
|
+
change, so that it can see red then green.
|
|
32
|
+
|
|
33
|
+
**To land it:**
|
|
34
|
+
|
|
35
|
+
1. Configure the gate's test command for this project. For example, `cd integration-sdk && yarn build && node
|
|
36
|
+
test/fetcher-query-params.test.mjs`, or the full `yarn test`. The tester also needs `integration-sdk/node_modules`
|
|
37
|
+
linked (`link` in the tdd settings), because the tester runs with no network.
|
|
38
|
+
2. Then apply the patch (`git apply .aeh/runs/20261026-104553/PLAT-47-fix.patch`), review it and commit it as
|
|
39
|
+
"PLAT-47: Fetcher sends fetchOptions.params as the query string".
|
|
40
|
+
|
|
41
|
+
Everything below describes that patch.
|
|
42
|
+
|
|
43
|
+
## Run 20261026-110540: second attempt, refused again by the gate (tdd, green run fails)
|
|
44
|
+
|
|
45
|
+
With the gate's test command now configured, this run tried to land the same patch (stash `plat47-fetcher-fix`, checked
|
|
46
|
+
byte-identical to `.aeh/runs/20261026-104553/PLAT-47-fix.patch`, which is also saved as
|
|
47
|
+
`.aeh/runs/20261026-110540/unfinished.patch`). It is still **not committed**. It stays in the box's working tree.
|
|
48
|
+
|
|
49
|
+
What happened, in order:
|
|
50
|
+
|
|
51
|
+
1. **Test alone, then code (the task's plan).** The test file was committed alone as `401b1cf` (review warn: no
|
|
52
|
+
probe for the red step). The code and docs commit was then refused:
|
|
53
|
+
|
|
54
|
+
tdd fail non-test code changed (integration-sdk/src/internal/fetcher/fetcher.mts,
|
|
55
|
+
integration-sdk/tools/mutation-run.mjs) with no new or changed test: write a test that fails on the
|
|
56
|
+
current code first
|
|
57
|
+
|
|
58
|
+
2. **Following the gate: test and code in one commit.** `401b1cf` was reverted by an ordinary commit, with no history
|
|
59
|
+
rewrite because it may already be published. Then test, code and docs were staged together and reviewed. That
|
|
60
|
+
commit was refused, and this is the gate's exact reason (review of tree `ea44c6244395`):
|
|
61
|
+
|
|
62
|
+
tdd fail the tests fail after the change (exit 2)
|
|
63
|
+
--- green run:
|
|
64
|
+
warning You don't appear to have an internet connection. Try the --offline flag to use the cache for registry queries.
|
|
65
|
+
error Command failed with exit code 2.
|
|
66
|
+
|
|
67
|
+
Every other check on that tree passed: secret-files, secrets, static, dependencies, the gate's own model review
|
|
68
|
+
("No findings"), and the probe. The probe ran in the gate's tester and gave 19/19 for
|
|
69
|
+
`test/fetcher-query-params.test.mjs` on the fixed tree.
|
|
70
|
+
|
|
71
|
+
**Likely cause (environment, not the change):** the box cannot confirm this, because it cannot see the gate's test
|
|
72
|
+
command or its tester. In the box, `yarn build` (tsc) exits 2 on missing packages only:
|
|
73
|
+
|
|
74
|
+
src/openapi-to-connector.mts: Cannot find module 'zod'
|
|
75
|
+
src/testing/fake-aloma.mts: Cannot find module 'ws'
|
|
76
|
+
src/transform/index.mts: Cannot find module 'comment-parser'
|
|
77
|
+
|
|
78
|
+
`node_modules` here is incomplete, and `yarn install --frozen-lockfile` fails (registry proxy 403; `yarn.lock`
|
|
79
|
+
unchanged). `yarn test` then fails 8 of 13 files, every one with `ERR_MODULE_NOT_FOUND` (ws, zod, brace-expansion).
|
|
80
|
+
**The same 8 fail on HEAD without the patch**, and the new fetcher test passes 19/19. If the gate's command is
|
|
81
|
+
`yarn build && yarn test` and its linked `node_modules` is the same incomplete set, it fails before and after any
|
|
82
|
+
change, so no code commit can pass the tdd check.
|
|
83
|
+
|
|
84
|
+
**To land it (Mac side):** give the gate's tester a complete `node_modules` for `integration-sdk` (zod 4.6.6, ws,
|
|
85
|
+
comment-parser, brace-expansion, at the locked versions), or set its test command to one that runs offline. Then commit
|
|
86
|
+
test + `fetcher.mts` + `mutation-run.mjs` + `OPENAPI_TO_CONNECTOR.md` in **one** commit: the gate needs the new test
|
|
87
|
+
in the same commit as the code change.
|
|
88
|
+
|
|
89
|
+
## The change
|
|
90
|
+
|
|
91
|
+
`src/internal/fetcher/fetcher.mts`:
|
|
92
|
+
|
|
93
|
+
- `toQuery(params)` turns `params` into a query string with `URLSearchParams`:
|
|
94
|
+
- `undefined` and `null` values (and array items) are skipped;
|
|
95
|
+
- numbers and booleans are stringified (`String(value)`), strings are sent as they are;
|
|
96
|
+
- arrays become repeated keys (`properties=a&properties=b`);
|
|
97
|
+
- a nested object (or an object inside an array) is refused with
|
|
98
|
+
`params.<key> is an object; only strings, numbers, booleans and arrays of them are sent`;
|
|
99
|
+
- a `params` that is not an object is refused (`params must be an object`);
|
|
100
|
+
- a `URLSearchParams` instance is sent as it is. Without this, `Object.entries` of it is empty and it would be
|
|
101
|
+
silently dropped, the same kind of bug as PLAT-47.
|
|
102
|
+
- `withQuery(url, query)` appends the query: `?` if the url has none, `&` if it already has one (nothing if it ends in
|
|
103
|
+
`?` or `&`), before any `#fragment`. An empty query returns the url unchanged, so no params, `{}` or all-undefined
|
|
104
|
+
params give exactly today's URL, byte for byte.
|
|
105
|
+
- In `fetch()`, the full URL is built after `customize` and after the `//` collapse, into a local `fullURL` only.
|
|
106
|
+
`url` and `options0` are untouched, and `options0` keeps `params`. The caller's `params` object is never mutated.
|
|
107
|
+
Retries go through `onError` → `fetch` → `customize` with the same `url` and `options0`, so a retry rebuilds the
|
|
108
|
+
same URL with the query exactly once.
|
|
109
|
+
- Only the object handed to native `fetch` drops `params` (`const {params, ...init} = options0`). Headers, body,
|
|
110
|
+
method, timeout, `customize`, `onResponse`, `unwrap` and the retry policy are unchanged.
|
|
111
|
+
- A refused `params` throws a private `ParamsError`, which the `catch` rethrows at once. Without that the error would
|
|
112
|
+
be retried (500 ms each) as if it were a network error.
|
|
113
|
+
- **Logging:** the failed-attempt log `console.log(theURL, e)` now logs `theURL.split('?')[0]`, the URL without
|
|
114
|
+
its query string. Query values may hold ids or emails. The params query is never in `theURL` (it lives only in
|
|
115
|
+
`fullURL`), and a query already written into `url` is now stripped from the log too.
|
|
116
|
+
- **Typing:** `params?: {[key: string]: any} | URLSearchParams` is added, with JSDoc, to the typed `fetch` options, so
|
|
117
|
+
hand-written connectors using `getClient()` see it. `build/internal/fetcher/fetcher.d.mts` declares it.
|
|
118
|
+
- **OAuth:** `OAuthFetcher` does not override `fetch()` and builds no URL of its own, so it inherits the behaviour.
|
|
119
|
+
One test proves it, with the Bearer header still set. The OAuth token calls (`make-oauth.mts`, `finish-oauth.mts`)
|
|
120
|
+
are separate requests and are not touched.
|
|
121
|
+
|
|
122
|
+
`OPENAPI_TO_CONNECTOR.md` (Parameter Mapping) now says the Fetcher sends `params` and how values are serialised.
|
|
123
|
+
|
|
124
|
+
`tools/mutation-run.mjs` has Fetcher mutants F01 to F12. They run `test/fetcher-query-params.test.mjs` instead of
|
|
125
|
+
mocha, and the runner checks that file's baseline too. `test/MUTATION.md` was **not** regenerated, because the runner
|
|
126
|
+
cannot run in this box (see below).
|
|
127
|
+
|
|
128
|
+
## Array form chosen, and why
|
|
129
|
+
|
|
130
|
+
**Repeated keys** (`properties=a&properties=b`).
|
|
131
|
+
|
|
132
|
+
- connector-hubspot-private-v2 and the HubSpot OpenAPI spec are not in this box (no network, and no HubSpot spec under
|
|
133
|
+
`examples/`), so the connector's generated code could not be read here.
|
|
134
|
+
- The generator (`openapi-to-connector.mts`) copies raw values into `fetchOptions.params` and ignores `style` and
|
|
135
|
+
`explode`. A generator that ignores them is implicitly assuming the OpenAPI default for query parameters, which is
|
|
136
|
+
`style: form, explode: true`, i.e. repeated keys.
|
|
137
|
+
- The task notes that HubSpot accepts repeated keys for `properties` on CRM v3 GET endpoints, and that form is the
|
|
138
|
+
stated fallback.
|
|
139
|
+
- **Doubt:** not checked against HubSpot's live documentation from the box. Mutant F03 (comma-joined) shows the tests
|
|
140
|
+
pin the choice, so switching later is a one-line change plus test updates.
|
|
141
|
+
|
|
142
|
+
**Nested objects are refused, not JSON-encoded.** The generator emits no serialisation for objects and has no
|
|
143
|
+
`deepObject` or JSON intent. Sending `[object Object]` silently is the worst outcome, and guessing a JSON or
|
|
144
|
+
`a[b]=c` encoding would silently be wrong for some vendors. A clear error names the key instead.
|
|
145
|
+
|
|
146
|
+
## Tests (`test/fetcher-query-params.test.mjs`, standalone script)
|
|
147
|
+
|
|
148
|
+
The test uses a stubbed `globalThis.fetch`, plus one real local HTTP server on 127.0.0.1. Nothing leaves the test
|
|
149
|
+
process. "Today" means the unchanged 70a9898 source, built and run with the same test file.
|
|
150
|
+
|
|
151
|
+
| # | Test | Rule | Today | After |
|
|
152
|
+
|---|------|------|-------|-------|
|
|
153
|
+
| 1 | numbers, strings and booleans are stringified into the query string | 1 | FAIL (query missing) | pass |
|
|
154
|
+
| 2 | query parameters already in the url are kept | 1 | FAIL (`bar=2` missing) | pass |
|
|
155
|
+
| 3 | undefined and null values are skipped (`0` and `''` kept) | 1 | FAIL (query missing) | pass |
|
|
156
|
+
| 4 | arrays become repeated keys | 1 | FAIL (query missing) | pass |
|
|
157
|
+
| 5 | undefined and null array items are skipped, other items stringified | 1 | FAIL (query missing) | pass |
|
|
158
|
+
| 6 | nested objects are refused with a clear error, nothing is sent, not retried | 1 | FAIL (no error) | pass |
|
|
159
|
+
| 7 | objects inside arrays are refused too | 1 | FAIL (no error) | pass |
|
|
160
|
+
| 8 | values are encoded by URLSearchParams and a `//` inside a value is not collapsed | 1 | FAIL (query missing) | pass |
|
|
161
|
+
| 9 | a URLSearchParams instance is sent as it is | 1 | FAIL (query missing) | pass |
|
|
162
|
+
| 10 | without baseUrl the query is appended to the url as given | 1 | FAIL (query missing) | pass |
|
|
163
|
+
| 11 | params is not passed on to global fetch; headers, body and method unchanged | 2 | FAIL (`params` reaches fetch) | pass |
|
|
164
|
+
| 12 | the caller's options and params objects are not mutated | 2 | pass (guard) | pass |
|
|
165
|
+
| 13 | customize runs as before and can change url and params | 2 | FAIL (query missing) | pass |
|
|
166
|
+
| 14 | a retried request sends the same url with the query string exactly once | 2 | FAIL (query missing) | pass |
|
|
167
|
+
| 15 | a failed attempt logs the url without its query string (the query is still sent) | 2 | FAIL (query not sent) | pass |
|
|
168
|
+
| 16 | no params, `{}`, all-undefined or `undefined` params give exactly the old url | 3 | pass (guard) | pass |
|
|
169
|
+
| 17 | the OAuth fetcher sends the query string and its Bearer token | 2 | FAIL (query missing) | pass |
|
|
170
|
+
| 18 | a local HTTP server receives the query string through the real global fetch | 1 | FAIL (request line has no query) | pass |
|
|
171
|
+
| 19 | params is part of the typed fetch options (`fetcher.d.mts`) | typing | FAIL (not declared) | pass |
|
|
172
|
+
|
|
173
|
+
Today: 17 fail, 2 pass. After: 19/19 pass.
|
|
174
|
+
|
|
175
|
+
Tests 12 and 16 pass on today's code by design. Rule 3 says today's URL must not change, and today's code never
|
|
176
|
+
mutates the caller, so these two guard against regressions. Mutants F04, F05 and F10 show they bite.
|
|
177
|
+
|
|
178
|
+
A typing check, done by hand outside the repo, compiled a hand-written call with `params: {limit, properties: [...],
|
|
179
|
+
archived}` and with a `URLSearchParams`, and a `// @ts-expect-error` confirmed that `params: 'a=1'` is a type error.
|
|
180
|
+
|
|
181
|
+
## Mutation results
|
|
182
|
+
|
|
183
|
+
Run by hand with a driver that reads the F rows from `tools/mutation-run.mjs`. For each mutant it applies the change
|
|
184
|
+
to `src/`, rebuilds with tsc, runs the test file and restores the source.
|
|
185
|
+
|
|
186
|
+
| # | Mutant | Result | Killed by (failing tests) |
|
|
187
|
+
|---|--------|--------|---------------------------|
|
|
188
|
+
| F01 | params handling removed (`fullURL = theURL`) | killed | 15 tests (every query test) |
|
|
189
|
+
| F02 | array handling dropped | killed | arrays, array items, retry, OAuth, local server |
|
|
190
|
+
| F03 | arrays comma-joined instead of repeated keys | killed | arrays, array items, objects in arrays, retry, OAuth, local server |
|
|
191
|
+
| F04 | undefined not skipped (`=== null`) | killed | undefined/null skipped, array items, old url |
|
|
192
|
+
| F05 | null not skipped (`=== undefined`) | killed | undefined/null skipped, array items, old url |
|
|
193
|
+
| F06 | nested objects sent instead of refused | killed | nested objects refused, objects in arrays |
|
|
194
|
+
| F07 | refused params retried | killed | nested objects refused (timing) |
|
|
195
|
+
| F08 | always `?` even when the url has a query | killed | existing query kept, log test |
|
|
196
|
+
| F09 | `params` passed on to native fetch | killed | params not passed on, retry, OAuth |
|
|
197
|
+
| F10 | `delete options0.params` (the map's original approach) | killed | retry keeps the query |
|
|
198
|
+
| F11 | log the full URL with its query | killed | log test |
|
|
199
|
+
| F12 | URLSearchParams instance dropped | killed | URLSearchParams test |
|
|
200
|
+
|
|
201
|
+
**12/12 killed.** That covers the three mutants the task asks for: params handling removed (F01), array handling
|
|
202
|
+
dropped (F02), undefined not skipped (F04).
|
|
203
|
+
|
|
204
|
+
## Build and full suite: what could and could not be run in this box
|
|
205
|
+
|
|
206
|
+
**Doubt, for Patrick to re-run on the Mac:** this box could not install the repo's lockfile.
|
|
207
|
+
|
|
208
|
+
- `yarn install --frozen-lockfile` fails: the egress proxy answers 403 for registry.yarnpkg.com.
|
|
209
|
+
- The offline yarn cache lacks exact locked versions of `zod@4.6.6`, `ws@8.21.2`, `qs@6.15.2`, `body-parser@1.20.9`,
|
|
210
|
+
`brace-expansion@2.1.7`, `serialize-javascript@7.0.5`, `chai@4.5.0`, `comment-parser@1.4.1` and a few
|
|
211
|
+
ts-node/mocha-type dependencies.
|
|
212
|
+
- Installing other versions would change the dependencies, which needs Patrick's agreement, so it was not done.
|
|
213
|
+
- What was done: the exact locked versions that *are* in the local cache (typescript 5.2.2, @types/node 18.19.39,
|
|
214
|
+
prettier, mocha's own package and others) were copied into the git-ignored `node_modules/`. Nothing was downloaded,
|
|
215
|
+
`yarn.lock` and `package.json` are unchanged (lockfile sha256 checked before and after).
|
|
216
|
+
|
|
217
|
+
Results with that partial install:
|
|
218
|
+
|
|
219
|
+
- **`yarn build` (tsc):** the same three errors before and after the change, all `TS2307 Cannot find module` for the
|
|
220
|
+
uninstalled `zod`, `ws` and `comment-parser`. No new error. tsc still emits `build/`.
|
|
221
|
+
- **`node tools/run-tests.mjs`:** 8 of 13 entries fail both before and after the change, every one with
|
|
222
|
+
`Cannot find module/package` (`brace-expansion` for mocha; `ws` or `zod` for the standalone scripts). The new
|
|
223
|
+
`test/fetcher-query-params.test.mjs` is the only added entry, and it passes.
|
|
224
|
+
- **Not run here:** the mocha suite (including `harness.test.mjs` "records the url the Fetcher really sends", which
|
|
225
|
+
pins the path-only URL and should be unaffected by rule 3), the generator and scenario tests, and
|
|
226
|
+
`yarn test:mutation` (M01 to M32 plus the new F rows).
|
|
227
|
+
|
|
228
|
+
Please run `yarn install --frozen-lockfile && yarn build && yarn test && yarn test:mutation` on the Mac, and commit
|
|
229
|
+
the regenerated `test/MUTATION.md`.
|
|
230
|
+
|
|
231
|
+
## Follow-ups (not changed in this run)
|
|
232
|
+
|
|
233
|
+
- **Generator, dotted query names:** a name such as `a.b` becomes a nested option object in the method signature,
|
|
234
|
+
but the body emits `fetchOptions.params.a.b = options.a.b`. That throws at runtime when `fetchOptions.params.a` is
|
|
235
|
+
undefined (`openapi-to-connector.mts`, query params loop). It should emit `fetchOptions.params['a.b'] = ...`.
|
|
236
|
+
- **Generator, serialisation:** it ignores `style` and `explode` and emits raw values for null, arrays and objects. The
|
|
237
|
+
Fetcher now applies one fixed rule (form, explode true; objects refused). Specs that use `explode: false` (comma
|
|
238
|
+
form), `deepObject` or `pipeDelimited` would need the generator to pre-serialise, or to pass a per-parameter style.
|
|
239
|
+
- **Generator, headers and cookies:** `in: header` and `in: cookie` parameters are dropped by the generator (from the
|
|
240
|
+
map), separately from this issue.
|
|
241
|
+
- **Retry re-runs `customize`:** a non-idempotent `customize` (for example one that appends to `url`) is still applied
|
|
242
|
+
twice on retry. That is unchanged and out of scope; the params query is safe because it never enters `url`.
|
|
243
|
+
- **VendorMock string matchers** compare the full URL including the query. Connector tests that now send a query
|
|
244
|
+
must use a regexp or function matcher, or the exact query string.
|
|
245
|
+
|
|
246
|
+
## Rollout risks to check before release
|
|
247
|
+
|
|
248
|
+
- A connector whose `customize` already works around PLAT-47 by appending `params` to `options.url` itself (the
|
|
249
|
+
workaround noted in the map) would now send every parameter twice. Patrick checked 29 active connectors and only
|
|
250
|
+
connector-hubspot-private-v2 uses `params`. A grep of its `customize` for `params` or `URLSearchParams` before
|
|
251
|
+
release settles it.
|
|
252
|
+
- A call that passes an object value in `params` used to have it silently dropped; it now fails with a clear error and
|
|
253
|
+
sends nothing. This is intended (rule 1), but it is a visible behaviour change.
|
package/OPENAPI_TO_CONNECTOR.md
CHANGED
|
@@ -253,7 +253,10 @@ The script generates method names using the following priority:
|
|
|
253
253
|
## Parameter Mapping
|
|
254
254
|
|
|
255
255
|
- **Path parameters**: Mapped to `args.paramName`
|
|
256
|
-
- **Query parameters**: Mapped to `args.paramName`
|
|
256
|
+
- **Query parameters**: Mapped to `args.paramName`, copied into `fetchOptions.params`; the SDK Fetcher sends them as
|
|
257
|
+
the query string. `undefined` and `null` values are skipped, numbers and booleans are stringified,
|
|
258
|
+
arrays become repeated keys (`properties=a&properties=b`, the OpenAPI default `style: form, explode: true`), and
|
|
259
|
+
nested objects are refused with an error. Values are encoded by `URLSearchParams`; a query already in the url is kept.
|
|
257
260
|
- **Request body**: Mapped to `args.body`
|
|
258
261
|
- **Headers**: Mapped to `args.headerName`
|
|
259
262
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/// <reference types="node" resolution-mode="require"/>
|
|
2
|
+
/// <reference types="node" resolution-mode="require"/>
|
|
2
3
|
/**
|
|
3
4
|
* http request fetcher
|
|
4
5
|
*/
|
|
@@ -41,5 +42,12 @@ export default class Fetcher {
|
|
|
41
42
|
* request body like "hello world" or {hello: "world"}
|
|
42
43
|
*/
|
|
43
44
|
body?: any;
|
|
45
|
+
/**
|
|
46
|
+
* query parameters like {limit: 10, properties: ["a", "b"]}, appended to the url;
|
|
47
|
+
* undefined and null are skipped, arrays become repeated keys, objects are refused
|
|
48
|
+
*/
|
|
49
|
+
params?: {
|
|
50
|
+
[key: string]: any;
|
|
51
|
+
} | URLSearchParams;
|
|
44
52
|
}, retries?: number, args?: any): Promise<any>;
|
|
45
53
|
}
|
|
@@ -1,4 +1,42 @@
|
|
|
1
1
|
import { unwrap } from '../util/index.mjs';
|
|
2
|
+
class ParamsError extends Error {
|
|
3
|
+
}
|
|
4
|
+
/**
|
|
5
|
+
* build a query string from request params: undefined and null are skipped, arrays become repeated keys,
|
|
6
|
+
* objects are refused; values are encoded by URLSearchParams
|
|
7
|
+
*/
|
|
8
|
+
const toQuery = (params) => {
|
|
9
|
+
if (params == null)
|
|
10
|
+
return '';
|
|
11
|
+
if (params instanceof URLSearchParams)
|
|
12
|
+
return params.toString();
|
|
13
|
+
if (typeof params !== 'object' || Array.isArray(params))
|
|
14
|
+
throw new ParamsError('params must be an object');
|
|
15
|
+
const query = new URLSearchParams();
|
|
16
|
+
const add = (key, value) => {
|
|
17
|
+
if (value == null)
|
|
18
|
+
return;
|
|
19
|
+
if (typeof value === 'object') {
|
|
20
|
+
throw new ParamsError(`params.${key} is an object; only strings, numbers, booleans and arrays of them are sent`);
|
|
21
|
+
}
|
|
22
|
+
query.append(key, String(value));
|
|
23
|
+
};
|
|
24
|
+
for (const [key, value] of Object.entries(params)) {
|
|
25
|
+
if (Array.isArray(value))
|
|
26
|
+
value.forEach((item) => add(key, item));
|
|
27
|
+
else
|
|
28
|
+
add(key, value);
|
|
29
|
+
}
|
|
30
|
+
return query.toString();
|
|
31
|
+
};
|
|
32
|
+
const withQuery = (url, query) => {
|
|
33
|
+
if (!query)
|
|
34
|
+
return url;
|
|
35
|
+
const hash = url.indexOf('#');
|
|
36
|
+
const base = hash < 0 ? url : url.slice(0, hash);
|
|
37
|
+
const sep = !base.includes('?') ? '?' : /[?&]$/.test(base) ? '' : '&';
|
|
38
|
+
return base + sep + query + (hash < 0 ? '' : url.slice(hash));
|
|
39
|
+
};
|
|
2
40
|
/**
|
|
3
41
|
* http request fetcher
|
|
4
42
|
*/
|
|
@@ -55,6 +93,8 @@ export default class Fetcher {
|
|
|
55
93
|
url = options0.url;
|
|
56
94
|
delete options0.url;
|
|
57
95
|
theURL = !baseUrl ? url : `${baseUrl?.endsWith('/') ? baseUrl : baseUrl + '/'}${url}`.replace(/\/\/+/gi, '/');
|
|
96
|
+
// after the slash collapse, so encoded values are not altered; options0 keeps params for retries
|
|
97
|
+
const fullURL = withQuery(theURL, toQuery(options0.params));
|
|
58
98
|
if (!options0?.headers || !options0?.headers?.Accept) {
|
|
59
99
|
options0.headers = {
|
|
60
100
|
...options0.headers,
|
|
@@ -74,8 +114,9 @@ export default class Fetcher {
|
|
|
74
114
|
options0.body = JSON.stringify(options0.body);
|
|
75
115
|
}
|
|
76
116
|
const timeout = Math.min(options0?.timeout || 30 * 60 * 1000, 30 * 60 * 1000);
|
|
77
|
-
const
|
|
78
|
-
|
|
117
|
+
const { params, ...init } = options0;
|
|
118
|
+
const ret = await fetch(fullURL, {
|
|
119
|
+
...init,
|
|
79
120
|
signal: AbortSignal.timeout(timeout),
|
|
80
121
|
});
|
|
81
122
|
const status = await ret.status;
|
|
@@ -94,6 +135,8 @@ export default class Fetcher {
|
|
|
94
135
|
return unwrap(ret, options0);
|
|
95
136
|
}
|
|
96
137
|
catch (e) {
|
|
138
|
+
if (e instanceof ParamsError)
|
|
139
|
+
throw e;
|
|
97
140
|
// too many requests
|
|
98
141
|
if (e.status === 429) {
|
|
99
142
|
return local.onError(e, url, options0, retries, args, true);
|
|
@@ -103,7 +146,8 @@ export default class Fetcher {
|
|
|
103
146
|
throw e;
|
|
104
147
|
}
|
|
105
148
|
--retries;
|
|
106
|
-
|
|
149
|
+
// without the query string: its values may hold ids or emails
|
|
150
|
+
console.log(theURL.split('?')[0], e);
|
|
107
151
|
if (retries <= 0)
|
|
108
152
|
throw e;
|
|
109
153
|
return local.onError(e, url, options0, retries, args);
|
|
@@ -97,10 +97,13 @@ export class OpenAPIToConnector {
|
|
|
97
97
|
const validationResult = OpenAPISchema.safeParse(parsed);
|
|
98
98
|
if (!validationResult.success) {
|
|
99
99
|
// Check if the errors are just about missing 'type' fields in schemas
|
|
100
|
-
|
|
100
|
+
// zod 4 reports .issues (zod 3's .errors is gone), and a missing field is an invalid_type issue whose message no
|
|
101
|
+
// longer reads 'Required'
|
|
102
|
+
const isMissing = (err) => err.message.includes('Required') || (err.code === 'invalid_type' && /received undefined/.test(err.message));
|
|
103
|
+
const criticalErrors = validationResult.error.issues.filter((err) => {
|
|
101
104
|
const path = err.path.join('.');
|
|
102
105
|
// Allow missing 'type' in schema definitions as many OpenAPI specs don't include it
|
|
103
|
-
return !path.includes('components.schemas') || !err
|
|
106
|
+
return !path.includes('components.schemas') || !isMissing(err);
|
|
104
107
|
});
|
|
105
108
|
if (criticalErrors.length > 0) {
|
|
106
109
|
const errors = criticalErrors.map((err) => `${err.path.join('.')}: ${err.message}`).join(', ');
|