@objectstack/connector-rest 16.1.0 → 17.0.0-rc.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,341 @@
1
1
  # @objectstack/connector-rest
2
2
 
3
+ ## 17.0.0-rc.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 2e836de: chore(packaging): CHANGELOG.md ships in every npm tarball (#4261)
8
+
9
+ The AGENTS.md post-task checklist requires breaking changesets to carry their
10
+ FROM → TO migration because "this text ships to consumers as `CHANGELOG.md`
11
+ inside the npm package and is what an upgrading agent greps after the tombstone
12
+ error." That delivery path was severed for 68 of the 69 publishable packages:
13
+ npm packs `package.json` / `README*` / `LICENSE*` unconditionally but — unlike
14
+ older npm versions — not `CHANGELOG.md`, and the canonical
15
+ `"files": ["dist", "README.md"]` whitelist never named it. Measured on npm
16
+ 10.9.7: `npm pack --dry-run` on `@objectstack/types` shipped 3 files while its
17
+ 70KB `CHANGELOG.md` stayed behind. Only `@objectstack/spec` listed it
18
+ explicitly.
19
+
20
+ The tombstone-error scenario is precisely the one where the repo is out of
21
+ reach — the upgrading agent has `node_modules` and nothing else — so the
22
+ migration text has to ride in the tarball. Every publishable package now
23
+ declares `CHANGELOG.md` in `files`, and the canonical whitelist is
24
+ `["dist", "README.md", "CHANGELOG.md"]`.
25
+
26
+ The other half is the gate: `check:published-files` gains a fifth invariant,
27
+ COMPLETE — a whitelist that fails to cover `CHANGELOG.md` fails the
28
+ always-required lint job, so the next package cannot silently sever the path
29
+ again. `@objectstack/spec`'s per-package EXTRA_ENTRIES exemption dissolves
30
+ into the canonical set.
31
+
32
+ Consumer-visible change: one more file per install (the package's changelog,
33
+ e.g. 70.8KB for `@objectstack/types`), and `grep -r "removed key"
34
+ node_modules/@objectstack/*/CHANGELOG.md` now finds the migration it was
35
+ promised.
36
+
37
+ - cc2de0e: chore(packaging): 20 packages stop publishing their sources, tests and build tooling (#4248)
38
+
39
+ These 20 packages declared no `files` field, so npm fell back to packing the
40
+ whole package directory. `npm pack --dry-run` on `@objectstack/plugin-webhooks`
41
+ listed **21 files** — 15 under `src/`, three of them unit tests
42
+ (`auto-enqueuer.test.ts`, `bootstrap-declared-webhooks.test.ts`, …), plus the
43
+ build-time `scripts/i18n-extract.config.ts`. `dist/` lands on top of that at
44
+ publish time rather than instead of it, so consumers were installing the
45
+ TypeScript sources and the test suite alongside the artifact they asked for.
46
+
47
+ Each now declares `"files": ["dist", "README.md"]`, matching the 29 packages
48
+ that already did. Nothing a consumer imports moves: every `main` / `types` /
49
+ `exports` target in all 20 already resolved inside `dist/`, which the new
50
+ `check:published-files` guard verifies rather than assumes. The visible change
51
+ is a smaller install and a smaller dependency-scanning surface — `npm pack` on
52
+ `@objectstack/plugin-webhooks` now yields 2 files plus `dist/`.
53
+
54
+ The other half of the fix is the gate. Half the packages declaring `files` and
55
+ half not was the #3786 shape — a hand-copied convention with nothing enforcing
56
+ it, where whoever forgets the line gets no signal at all. `check:published-files`
57
+ (new, wired into the always-required `lint` job) holds every non-private
58
+ workspace package to four invariants: `files` is **declared**; it is
59
+ **sufficient** (covers every entry point, so tightening a whitelist cannot ship
60
+ a package that fails to resolve); it is **minimal** (admits no test, test-harness
61
+ config or build script); and anything beyond `dist` + `README.md` is
62
+ **registered** with a reason, reconciled in both directions so a stale exemption
63
+ is an error rather than dead text. `@objectstack/spec` is the one package with
64
+ registered extras — its `.zod.ts` sources, JSON Schemas, liveness ledgers and
65
+ `CHANGELOG.md` are product, not build input.
66
+
67
+ This also closes an assumption #4206 was resting on. Excluding `<pkg>/scripts/**`
68
+ from the docs-drift implementation test is sound only while no package publishes
69
+ `scripts/` as runtime code; that held, but it held because someone read all three
70
+ offenders by hand. It is now checked on every PR.
71
+
72
+ - Updated dependencies [6a67d7a]
73
+ - Updated dependencies [0ecc656]
74
+ - Updated dependencies [06772eb]
75
+ - Updated dependencies [270650f]
76
+ - Updated dependencies [3aef718]
77
+ - Updated dependencies [1ea6bce]
78
+ - Updated dependencies [c1dcacd]
79
+ - Updated dependencies [ad303ed]
80
+ - Updated dependencies [32ccb23]
81
+ - Updated dependencies [f5a4ef0]
82
+ - Updated dependencies [2d3e255]
83
+ - Updated dependencies [7d7521f]
84
+ - Updated dependencies [5dc4d02]
85
+ - Updated dependencies [05154a1]
86
+ - Updated dependencies [9b6fe7c]
87
+ - Updated dependencies [8c711fb]
88
+ - Updated dependencies [09e4547]
89
+ - Updated dependencies [91f4c78]
90
+ - Updated dependencies [820eff9]
91
+ - Updated dependencies [8d895ff]
92
+ - Updated dependencies [f6472d7]
93
+ - Updated dependencies [78caf51]
94
+ - Updated dependencies [62a789b]
95
+ - Updated dependencies [789ad63]
96
+ - Updated dependencies [2af1988]
97
+ - Updated dependencies [0af50a3]
98
+ - Updated dependencies [2e836de]
99
+ - Updated dependencies [12a19a8]
100
+ - Updated dependencies [41dcda3]
101
+ - Updated dependencies [c8124e5]
102
+ - Updated dependencies [a1a4140]
103
+ - Updated dependencies [217e2e6]
104
+ - Updated dependencies [86a71d1]
105
+ - Updated dependencies [d5c75e2]
106
+ - Updated dependencies [03d26f7]
107
+ - Updated dependencies [4384921]
108
+ - Updated dependencies [3c628ce]
109
+ - Updated dependencies [7cb922e]
110
+ - Updated dependencies [1d22114]
111
+ - Updated dependencies [b5f9397]
112
+ - Updated dependencies [ed77493]
113
+ - Updated dependencies [58a03d2]
114
+ - Updated dependencies [dc530b4]
115
+ - Updated dependencies [e59786e]
116
+ - Updated dependencies [bcf1112]
117
+ - Updated dependencies [9774b78]
118
+ - Updated dependencies [b07d829]
119
+ - Updated dependencies [a648e96]
120
+ - Updated dependencies [a47ac06]
121
+ - Updated dependencies [e4c61a7]
122
+ - Updated dependencies [cc60165]
123
+ - Updated dependencies [081aa6f]
124
+ - Updated dependencies [91f4c78]
125
+ - Updated dependencies [e8d0c21]
126
+ - Updated dependencies [45dc446]
127
+ - Updated dependencies [c1d44f7]
128
+ - Updated dependencies [ab9fb5c]
129
+ - Updated dependencies [f985b3f]
130
+ - Updated dependencies [9a4932a]
131
+ - Updated dependencies [f9fc874]
132
+ - Updated dependencies [011b386]
133
+ - Updated dependencies [7777e8f]
134
+ - Updated dependencies [507b92a]
135
+ - Updated dependencies [7309c81]
136
+ - Updated dependencies [20bc1ec]
137
+ - Updated dependencies [90c2b15]
138
+ - Updated dependencies [42eeb7d]
139
+ - Updated dependencies [01e124d]
140
+ - Updated dependencies [7ce02eb]
141
+ - Updated dependencies [a13827e]
142
+ - Updated dependencies [7733604]
143
+ - Updated dependencies [40e420f]
144
+ - Updated dependencies [d13004a]
145
+ - Updated dependencies [be7360c]
146
+ - Updated dependencies [5b47ab5]
147
+ - Updated dependencies [b09d8d9]
148
+ - Updated dependencies [b09d8d9]
149
+ - Updated dependencies [8675db6]
150
+ - Updated dependencies [b09d8d9]
151
+ - Updated dependencies [3eb1b2b]
152
+ - Updated dependencies [59b85c0]
153
+ - Updated dependencies [6e357ed]
154
+ - Updated dependencies [d6938bf]
155
+ - Updated dependencies [31e0be9]
156
+ - Updated dependencies [4bfd455]
157
+ - Updated dependencies [ffd2ce2]
158
+ - Updated dependencies [62f8017]
159
+ - Updated dependencies [a831df1]
160
+ - Updated dependencies [f752ee3]
161
+ - Updated dependencies [a1b61e0]
162
+ - Updated dependencies [cd6b9f2]
163
+ - Updated dependencies [2cb6d3c]
164
+ - Updated dependencies [af2a095]
165
+ - Updated dependencies [ec796d5]
166
+ - Updated dependencies [e87fea1]
167
+ - Updated dependencies [c65e529]
168
+ - Updated dependencies [3ca34c1]
169
+ - Updated dependencies [239c3a3]
170
+ - Updated dependencies [94a0bbc]
171
+ - Updated dependencies [d6bfb3d]
172
+ - Updated dependencies [a2266a6]
173
+ - Updated dependencies [d25a0ec]
174
+ - Updated dependencies [667b83e]
175
+ - Updated dependencies [627b188]
176
+ - Updated dependencies [8d4eae7]
177
+ - Updated dependencies [857a6cf]
178
+ - Updated dependencies [65a3a84]
179
+ - Updated dependencies [ccd9397]
180
+ - Updated dependencies [bca935b]
181
+ - Updated dependencies [d92c72d]
182
+ - Updated dependencies [c54c822]
183
+ - Updated dependencies [8dcc0f5]
184
+ - Updated dependencies [75b9e51]
185
+ - Updated dependencies [0a2f233]
186
+ - Updated dependencies [8621cdd]
187
+ - Updated dependencies [6f23667]
188
+ - Updated dependencies [5d21a48]
189
+ - Updated dependencies [19365b7]
190
+ - Updated dependencies [b7ed26d]
191
+ - Updated dependencies [b3a3d83]
192
+ - Updated dependencies [7a55913]
193
+ - Updated dependencies [35accbf]
194
+ - Updated dependencies [6038de7]
195
+ - Updated dependencies [eb95d97]
196
+ - Updated dependencies [e4c2dc8]
197
+ - Updated dependencies [1bd2795]
198
+ - Updated dependencies [8186a70]
199
+ - Updated dependencies [a329cca]
200
+ - Updated dependencies [6eec18c]
201
+ - Updated dependencies [4d7bebf]
202
+ - Updated dependencies [821ac7a]
203
+ - Updated dependencies [8f81731]
204
+ - Updated dependencies [8b50cb3]
205
+ - Updated dependencies [8c2db68]
206
+ - Updated dependencies [22b5e54]
207
+ - Updated dependencies [0166bd5]
208
+ - Updated dependencies [9b702dc]
209
+ - Updated dependencies [ab16331]
210
+ - @objectstack/spec@17.0.0-rc.1
211
+ - @objectstack/core@17.0.0-rc.1
212
+
213
+ ## 17.0.0-rc.0
214
+
215
+ ### Patch Changes
216
+
217
+ - Updated dependencies [50616d9]
218
+ - Updated dependencies [08b5a3d]
219
+ - Updated dependencies [d99aeb3]
220
+ - Updated dependencies [4727eb8]
221
+ - Updated dependencies [f63cd09]
222
+ - Updated dependencies [fa3d0cf]
223
+ - Updated dependencies [af5a224]
224
+ - Updated dependencies [71f76e1]
225
+ - Updated dependencies [37b1346]
226
+ - Updated dependencies [99736a0]
227
+ - Updated dependencies [fe67e34]
228
+ - Updated dependencies [fdb4f50]
229
+ - Updated dependencies [1bd5652]
230
+ - Updated dependencies [14252d3]
231
+ - Updated dependencies [7fb436c]
232
+ - Updated dependencies [879ea13]
233
+ - Updated dependencies [201b31f]
234
+ - Updated dependencies [e2616e0]
235
+ - Updated dependencies [6fdc5c6]
236
+ - Updated dependencies [8b9d71e]
237
+ - Updated dependencies [33f5e23]
238
+ - Updated dependencies [259af21]
239
+ - Updated dependencies [587fc91]
240
+ - Updated dependencies [1986594]
241
+ - Updated dependencies [ad4af62]
242
+ - Updated dependencies [d44dbfa]
243
+ - Updated dependencies [474fe39]
244
+ - Updated dependencies [0bc685a]
245
+ - Updated dependencies [b949059]
246
+ - Updated dependencies [be1c52c]
247
+ - Updated dependencies [c5ff96d]
248
+ - Updated dependencies [84e7be9]
249
+ - Updated dependencies [a6c3f38]
250
+ - Updated dependencies [debc23a]
251
+ - Updated dependencies [0f8ad09]
252
+ - Updated dependencies [8f9689f]
253
+ - Updated dependencies [57a3bb3]
254
+ - Updated dependencies [5f9a987]
255
+ - Updated dependencies [db02d47]
256
+ - Updated dependencies [0bfdf46]
257
+ - Updated dependencies [376a061]
258
+ - Updated dependencies [7c7e246]
259
+ - Updated dependencies [f35cdc5]
260
+ - Updated dependencies [9ea2bc5]
261
+ - Updated dependencies [c2d9098]
262
+ - Updated dependencies [a227ed7]
263
+ - Updated dependencies [9613396]
264
+ - Updated dependencies [e47b342]
265
+ - Updated dependencies [4ed7ed4]
266
+ - Updated dependencies [2fa4ca1]
267
+ - Updated dependencies [f5a2320]
268
+ - Updated dependencies [deb538f]
269
+ - Updated dependencies [5b89711]
270
+ - Updated dependencies [0c8a22f]
271
+ - Updated dependencies [763931e]
272
+ - Updated dependencies [de9af8a]
273
+ - Updated dependencies [c4df271]
274
+ - Updated dependencies [a41ba5c]
275
+ - Updated dependencies [189854c]
276
+ - Updated dependencies [0e3a226]
277
+ - Updated dependencies [1d4756e]
278
+ - Updated dependencies [720c5ad]
279
+ - Updated dependencies [a8d1e24]
280
+ - Updated dependencies [41642b0]
281
+ - Updated dependencies [4cca74c]
282
+ - Updated dependencies [88ef03e]
283
+ - Updated dependencies [9e2caf3]
284
+ - Updated dependencies [81ce41a]
285
+ - Updated dependencies [85e1e4e]
286
+ - Updated dependencies [dac6a08]
287
+ - Updated dependencies [394b7a1]
288
+ - Updated dependencies [677b591]
289
+ - Updated dependencies [d77d1b7]
290
+ - Updated dependencies [5b79a34]
291
+ - Updated dependencies [c757854]
292
+ - Updated dependencies [0045682]
293
+ - Updated dependencies [2a5f04a]
294
+ - Updated dependencies [4f740b0]
295
+ - Updated dependencies [67452d1]
296
+ - Updated dependencies [0fc6219]
297
+ - Updated dependencies [605e190]
298
+ - Updated dependencies [c6c59f1]
299
+ - Updated dependencies [b0e78a8]
300
+ - Updated dependencies [f31cc8d]
301
+ - Updated dependencies [f343dc4]
302
+ - Updated dependencies [8269e32]
303
+ - Updated dependencies [74f7339]
304
+ - Updated dependencies [a6c35a2]
305
+ - Updated dependencies [c2f1002]
306
+ - Updated dependencies [f163028]
307
+ - Updated dependencies [f07808c]
308
+ - Updated dependencies [7ffc3d3]
309
+ - Updated dependencies [88346ba]
310
+ - Updated dependencies [4631592]
311
+ - Updated dependencies [32ff033]
312
+ - Updated dependencies [5ac93d4]
313
+ - Updated dependencies [93f267f]
314
+ - Updated dependencies [0024abf]
315
+ - Updated dependencies [acbf364]
316
+ - Updated dependencies [7687f7b]
317
+ - Updated dependencies [1659072]
318
+ - Updated dependencies [abceb0d]
319
+ - Updated dependencies [0c302a7]
320
+ - Updated dependencies [6633337]
321
+ - Updated dependencies [f00d8d4]
322
+ - Updated dependencies [503be86]
323
+ - Updated dependencies [cde1975]
324
+ - Updated dependencies [0bc685a]
325
+ - Updated dependencies [11949fc]
326
+ - Updated dependencies [b098b0e]
327
+ - Updated dependencies [4d00b13]
328
+ - Updated dependencies [57bab76]
329
+ - Updated dependencies [b90086a]
330
+ - Updated dependencies [b95577a]
331
+ - Updated dependencies [83c161f]
332
+ - Updated dependencies [d8c4957]
333
+ - Updated dependencies [f24cb83]
334
+ - Updated dependencies [5dbbb92]
335
+ - Updated dependencies [69f1dfd]
336
+ - @objectstack/spec@17.0.0-rc.0
337
+ - @objectstack/core@17.0.0-rc.0
338
+
3
339
  ## 16.1.0
4
340
 
5
341
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/connector-rest",
3
- "version": "16.1.0",
3
+ "version": "17.0.0-rc.1",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Generic REST connector for ObjectStack — the reference concrete connector that registers a `request` action on the automation engine's connector registry (ADR-0018 §Addendum).",
6
6
  "main": "dist/index.js",
@@ -13,14 +13,14 @@
13
13
  }
14
14
  },
15
15
  "dependencies": {
16
- "@objectstack/core": "16.1.0",
17
- "@objectstack/spec": "16.1.0"
16
+ "@objectstack/core": "17.0.0-rc.1",
17
+ "@objectstack/spec": "17.0.0-rc.1"
18
18
  },
19
19
  "devDependencies": {
20
20
  "@types/node": "^26.1.1",
21
21
  "typescript": "^6.0.3",
22
22
  "vitest": "^4.1.10",
23
- "@objectstack/service-automation": "16.1.0"
23
+ "@objectstack/service-automation": "17.0.0-rc.1"
24
24
  },
25
25
  "keywords": [
26
26
  "objectstack",
@@ -29,8 +29,14 @@
29
29
  "integration",
30
30
  "http"
31
31
  ],
32
+ "files": [
33
+ "dist",
34
+ "README.md",
35
+ "CHANGELOG.md"
36
+ ],
32
37
  "scripts": {
33
38
  "build": "tsup --config ../../../tsup.config.ts",
34
- "test": "vitest run --passWithNoTests"
39
+ "test": "vitest run --passWithNoTests",
40
+ "typecheck": "tsc --noEmit"
35
41
  }
36
42
  }
@@ -1,22 +0,0 @@
1
-
2
- > @objectstack/connector-rest@16.1.0 build /home/runner/work/objectstack/objectstack/packages/connectors/connector-rest
3
- > tsup --config ../../../tsup.config.ts
4
-
5
- CLI Building entry: src/index.ts
6
- CLI Using tsconfig: tsconfig.json
7
- CLI tsup v8.5.1
8
- CLI Using tsup config: /home/runner/work/objectstack/objectstack/tsup.config.ts
9
- CLI Target: es2020
10
- CLI Cleaning output folder
11
- ESM Build start
12
- CJS Build start
13
- ESM dist/index.mjs 6.23 KB
14
- ESM dist/index.mjs.map 18.35 KB
15
- ESM ⚡️ Build success in 82ms
16
- CJS dist/index.js 7.42 KB
17
- CJS dist/index.js.map 19.43 KB
18
- CJS ⚡️ Build success in 85ms
19
- DTS Build start
20
- DTS ⚡️ Build success in 13854ms
21
- DTS dist/index.d.mts 5.43 KB
22
- DTS dist/index.d.ts 5.43 KB
@@ -1,154 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { describe, it, expect } from 'vitest';
4
- import { LiteKernel } from '@objectstack/core';
5
- import { AutomationServicePlugin, type AutomationEngine } from '@objectstack/service-automation';
6
- import { ConnectorRestPlugin } from './connector-rest-plugin.js';
7
-
8
- /** A fetch stub recording calls, returning a fixed JSON response. */
9
- function stubFetch() {
10
- const calls: Array<{ url: string; init: RequestInit }> = [];
11
- const impl = (async (url: string, init: RequestInit) => {
12
- calls.push({ url, init });
13
- return {
14
- status: 201,
15
- ok: true,
16
- headers: { get: (h: string) => (h.toLowerCase() === 'content-type' ? 'application/json' : null) },
17
- json: async () => ({ id: 'created-1' }),
18
- text: async () => '{"id":"created-1"}',
19
- };
20
- }) as unknown as typeof fetch;
21
- return { impl, calls };
22
- }
23
-
24
- describe('ConnectorRestPlugin — end to end with the automation engine', () => {
25
- it('registers the REST connector so a connector_action flow dispatches to it', async () => {
26
- const { impl, calls } = stubFetch();
27
-
28
- const kernel = new LiteKernel();
29
- kernel.use(new AutomationServicePlugin());
30
- kernel.use(
31
- new ConnectorRestPlugin({
32
- baseUrl: 'https://api.example.com',
33
- auth: { type: 'bearer', token: 'secret-token' },
34
- fetchImpl: impl,
35
- }),
36
- );
37
- await kernel.bootstrap();
38
-
39
- const engine = kernel.getService<AutomationEngine>('automation');
40
-
41
- // The baseline node and the plugin-contributed connector are both present.
42
- expect(engine.getRegisteredNodeTypes()).toContain('connector_action');
43
- expect(engine.getRegisteredConnectors()).toContain('rest');
44
-
45
- engine.registerFlow('create_via_rest', {
46
- name: 'create_via_rest',
47
- label: 'Create via REST',
48
- type: 'autolaunched',
49
- variables: [{ name: 'call.body', type: 'json', isOutput: true }],
50
- nodes: [
51
- { id: 'start', type: 'start', label: 'Start' },
52
- {
53
- id: 'call',
54
- type: 'connector_action',
55
- label: 'POST /items',
56
- connectorConfig: {
57
- connectorId: 'rest',
58
- actionId: 'request',
59
- input: { method: 'POST', path: '/items', body: { name: 'Widget' } },
60
- },
61
- },
62
- { id: 'end', type: 'end', label: 'End' },
63
- ],
64
- edges: [
65
- { id: 'e1', source: 'start', target: 'call' },
66
- { id: 'e2', source: 'call', target: 'end' },
67
- ],
68
- });
69
-
70
- const result = await engine.execute('create_via_rest');
71
-
72
- expect(result.success).toBe(true);
73
- // The REST connector handled the dispatch: one fetch with auth + body.
74
- expect(calls).toHaveLength(1);
75
- expect(calls[0].url).toBe('https://api.example.com/items');
76
- expect(calls[0].init.method).toBe('POST');
77
- expect((calls[0].init.headers as Record<string, string>)['Authorization']).toBe('Bearer secret-token');
78
- // The action output propagated back into the flow.
79
- expect(result.output).toEqual({ 'call.body': { id: 'created-1' } });
80
-
81
- await kernel.shutdown();
82
- });
83
-
84
- it('materializes a declarative `provider: rest` instance from stack metadata (ADR-0097)', async () => {
85
- const { impl, calls } = stubFetch();
86
-
87
- // A provider-bound `connectors:` entry as it sits in the ObjectQL registry.
88
- const declared = [
89
- {
90
- name: 'billing',
91
- label: 'Billing API',
92
- type: 'api',
93
- provider: 'rest',
94
- providerConfig: { baseUrl: 'https://billing.example.com' },
95
- auth: { type: 'bearer', credentialRef: 'BILLING_TOKEN' },
96
- },
97
- ];
98
-
99
- const kernel = new LiteKernel();
100
- // The plugin, with no baseUrl, contributes ONLY the `rest` provider factory.
101
- kernel.use(new AutomationServicePlugin({ credentialResolver: (r) => (r === 'BILLING_TOKEN' ? 'sk-live' : undefined) }));
102
- kernel.use(new ConnectorRestPlugin({ fetchImpl: impl }));
103
- // A tiny harness that serves the declared connector metadata to the automation service.
104
- kernel.use({
105
- name: 'test.metadata',
106
- type: 'standard',
107
- version: '1.0.0',
108
- dependencies: [],
109
- async init(ctx: any) {
110
- ctx.registerService('objectql', {
111
- registry: { listItems: (t: string) => (t === 'connector' ? declared : []) },
112
- });
113
- },
114
- async start() {},
115
- } as never);
116
-
117
- await kernel.bootstrap();
118
- const engine = kernel.getService<AutomationEngine>('automation');
119
-
120
- // Materialized under the declared name, tagged declarative, and listed.
121
- expect(engine.getRegisteredConnectors()).toContain('billing');
122
- expect(engine.getConnectorOrigin('billing')).toBe('declarative');
123
- expect(engine.getConnectorDescriptors().find((d) => d.name === 'billing')?.actions.map((a) => a.key)).toEqual(['request']);
124
-
125
- engine.registerFlow('call_billing', {
126
- name: 'call_billing',
127
- label: 'Call Billing',
128
- type: 'autolaunched',
129
- variables: [{ name: 'call.status', type: 'number', isOutput: true }],
130
- nodes: [
131
- { id: 'start', type: 'start', label: 'Start' },
132
- {
133
- id: 'call',
134
- type: 'connector_action',
135
- label: 'GET /invoices',
136
- connectorConfig: { connectorId: 'billing', actionId: 'request', input: { path: '/invoices' } },
137
- },
138
- { id: 'end', type: 'end', label: 'End' },
139
- ],
140
- edges: [
141
- { id: 'e1', source: 'start', target: 'call' },
142
- { id: 'e2', source: 'call', target: 'end' },
143
- ],
144
- });
145
-
146
- const result = await engine.execute('call_billing');
147
- expect(result.success).toBe(true);
148
- // Dispatched to the resolved base URL, with the credentialRef-resolved bearer token.
149
- expect(calls[0].url).toBe('https://billing.example.com/invoices');
150
- expect((calls[0].init.headers as Record<string, string>)['Authorization']).toBe('Bearer sk-live');
151
-
152
- await kernel.shutdown();
153
- });
154
- });
@@ -1,108 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import type { Plugin, PluginContext } from '@objectstack/core';
4
- import type { Connector, ConnectorProviderFactory } from '@objectstack/spec/integration';
5
- import { createRestConnector, type RestConnectorOptions } from './rest-connector.js';
6
- import { createRestProviderFactory, REST_PROVIDER_KEY } from './rest-provider.js';
7
-
8
- /**
9
- * Minimal surface of the automation engine this plugin depends on — the
10
- * connector registry (ADR-0018 §Addendum) plus the provider registry (ADR-0097).
11
- * Kept structural so the plugin needs no runtime dependency on
12
- * `@objectstack/service-automation`.
13
- */
14
- export interface ConnectorRegistrySurface {
15
- registerConnector(
16
- def: Connector,
17
- handlers: Record<
18
- string,
19
- (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>
20
- >,
21
- ): void;
22
- unregisterConnector(name: string): void;
23
- registerConnectorProvider(providerKey: string, factory: ConnectorProviderFactory): void;
24
- }
25
-
26
- /**
27
- * Options for {@link ConnectorRestPlugin}. All optional (ADR-0097): with no
28
- * `baseUrl` the plugin contributes only the `rest` provider factory — so a stack
29
- * can declare `provider: 'rest'` instances as pure metadata. Supply `baseUrl`
30
- * (etc.) to ALSO register a single hand-wired `rest` connector, the pre-ADR-0097
31
- * usage.
32
- */
33
- export interface ConnectorRestPluginOptions extends Partial<RestConnectorOptions> {}
34
-
35
- /**
36
- * ConnectorRestPlugin — contributes the generic REST connector (ADR-0018
37
- * §Addendum) in two forms:
38
- *
39
- * 1. **Provider factory** (`rest`, ADR-0097): registered at `init()` so the
40
- * automation service can materialize declarative `provider: 'rest'`
41
- * `connectors:` entries into live connectors at boot.
42
- * 2. **Hand-wired instance** (optional, back-compat): when constructed with a
43
- * `baseUrl`, it also registers one concrete `rest` connector at `start()`.
44
- *
45
- * If no automation engine is present the plugin logs and skips — the connector
46
- * has nowhere to register, which is not an error.
47
- */
48
- export class ConnectorRestPlugin implements Plugin {
49
- name = 'com.objectstack.connector.rest';
50
- version = '1.0.0';
51
- type = 'standard' as const;
52
- // Ensure the automation engine (and its connector/provider registries) exist first.
53
- dependencies = ['com.objectstack.service-automation'];
54
-
55
- private readonly options: ConnectorRestPluginOptions;
56
- private connectorName?: string;
57
- private automation?: ConnectorRegistrySurface;
58
-
59
- constructor(options: ConnectorRestPluginOptions = {}) {
60
- this.options = options;
61
- }
62
-
63
- async init(ctx: PluginContext): Promise<void> {
64
- // Contribute the `rest` provider factory (ADR-0097). Done in init() — not
65
- // start() — so it is registered before the automation service materializes
66
- // declarative instances during its own start().
67
- const automation = this.tryGetAutomation(ctx);
68
- if (automation && typeof automation.registerConnectorProvider === 'function') {
69
- automation.registerConnectorProvider(
70
- REST_PROVIDER_KEY,
71
- createRestProviderFactory({ fetchImpl: this.options.fetchImpl }),
72
- );
73
- ctx.logger.info("ConnectorRestPlugin: registered 'rest' connector provider");
74
- }
75
- }
76
-
77
- async start(ctx: PluginContext): Promise<void> {
78
- // Only register a concrete connector when a baseUrl was supplied — the
79
- // provider-only usage (no options) contributes just the factory in init().
80
- if (!this.options.baseUrl) return;
81
-
82
- const automation = this.tryGetAutomation(ctx);
83
- if (!automation || typeof automation.registerConnector !== 'function') {
84
- ctx.logger.info('ConnectorRestPlugin: no automation engine — REST connector not registered');
85
- return;
86
- }
87
-
88
- const { def, handlers } = createRestConnector(this.options as RestConnectorOptions);
89
- automation.registerConnector(def, handlers);
90
- this.automation = automation;
91
- this.connectorName = def.name;
92
- ctx.logger.info(`ConnectorRestPlugin: REST connector '${def.name}' registered`);
93
- }
94
-
95
- async stop(_ctx: PluginContext): Promise<void> {
96
- if (this.automation && this.connectorName) {
97
- try { this.automation.unregisterConnector(this.connectorName); } catch { /* ignore */ }
98
- }
99
- }
100
-
101
- private tryGetAutomation(ctx: PluginContext): ConnectorRegistrySurface | undefined {
102
- try {
103
- return ctx.getService<ConnectorRegistrySurface>('automation');
104
- } catch {
105
- return undefined;
106
- }
107
- }
108
- }
package/src/index.ts DELETED
@@ -1,31 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- /**
4
- * @objectstack/connector-rest
5
- *
6
- * Generic REST connector — the reference *concrete* connector (ADR-0018
7
- * §Addendum). The baseline automation engine ships the `connector_action`
8
- * dispatch node + an empty connector registry; this plugin populates the
9
- * registry with a `rest` connector exposing a `request` action.
10
- *
11
- * Static auth only (`none` / `api-key` / `basic` / `bearer`); OAuth2 refresh,
12
- * credential vaulting, and multi-tenant lifecycle are the enterprise tier.
13
- */
14
-
15
- export {
16
- createRestConnector,
17
- type RestConnectorOptions,
18
- type RestConnectorBundle,
19
- type RestRequestInput,
20
- type RestAuth,
21
- } from './rest-connector.js';
22
- export {
23
- ConnectorRestPlugin,
24
- type ConnectorRestPluginOptions,
25
- type ConnectorRegistrySurface,
26
- } from './connector-rest-plugin.js';
27
- export {
28
- createRestProviderFactory,
29
- REST_PROVIDER_KEY,
30
- type RestProviderDeps,
31
- } from './rest-provider.js';
@@ -1,160 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { describe, it, expect, vi } from 'vitest';
4
- import { createRestConnector } from './rest-connector.js';
5
-
6
- // ─── Helpers ─────────────────────────────────────────────────────────
7
-
8
- interface CapturedCall {
9
- url: string;
10
- init: RequestInit;
11
- }
12
-
13
- /** A fetch stub that records calls and returns a fixed JSON response. */
14
- function stubFetch(responseBody: unknown = { ok: true }, status = 200) {
15
- const calls: CapturedCall[] = [];
16
- const impl = (async (url: string, init: RequestInit) => {
17
- calls.push({ url, init });
18
- return {
19
- status,
20
- ok: status >= 200 && status < 300,
21
- headers: { get: (h: string) => (h.toLowerCase() === 'content-type' ? 'application/json' : null) },
22
- json: async () => responseBody,
23
- text: async () => JSON.stringify(responseBody),
24
- };
25
- }) as unknown as typeof fetch;
26
- return { impl, calls };
27
- }
28
-
29
- function headersOf(call: CapturedCall): Record<string, string> {
30
- return (call.init.headers ?? {}) as Record<string, string>;
31
- }
32
-
33
- // ─── request action ──────────────────────────────────────────────────
34
-
35
- describe('createRestConnector — request action', () => {
36
- it('builds the URL from base + path + query and returns the parsed body', async () => {
37
- const { impl, calls } = stubFetch({ id: 1, name: 'Ada' });
38
- const { def, handlers } = createRestConnector({ baseUrl: 'https://api.example.com/', fetchImpl: impl });
39
-
40
- expect(def.name).toBe('rest');
41
- expect(def.actions?.[0].key).toBe('request');
42
-
43
- const out = await handlers.request({ path: '/users', query: { page: 2, active: true } }, {});
44
-
45
- expect(calls).toHaveLength(1);
46
- expect(calls[0].url).toBe('https://api.example.com/users?page=2&active=true');
47
- expect(calls[0].init.method).toBe('GET');
48
- expect(out).toEqual({ status: 200, ok: true, body: { id: 1, name: 'Ada' } });
49
- });
50
-
51
- it('retries a transient 503 then returns the success (P1-1)', async () => {
52
- let n = 0;
53
- const calls: number[] = [];
54
- const impl = (async () => {
55
- calls.push(1);
56
- const status = n++ === 0 ? 503 : 200;
57
- return {
58
- status,
59
- ok: status >= 200 && status < 300,
60
- headers: { get: (h: string) => (h.toLowerCase() === 'content-type' ? 'application/json' : null) },
61
- json: async () => ({ ok: status === 200 }),
62
- text: async () => '',
63
- };
64
- }) as unknown as typeof fetch;
65
- const { handlers } = createRestConnector({ baseUrl: 'https://api.example.com', fetchImpl: impl });
66
-
67
- const out = await handlers.request({ path: '/x' }, {});
68
- expect(calls.length).toBe(2); // retried the 503 once
69
- expect(out.status).toBe(200);
70
- });
71
-
72
- it('JSON-encodes the body and sets Content-Type for non-GET', async () => {
73
- const { impl, calls } = stubFetch();
74
- const { handlers } = createRestConnector({ baseUrl: 'https://api.example.com', fetchImpl: impl });
75
-
76
- await handlers.request({ method: 'post', path: 'items', body: { name: 'x' } }, {});
77
-
78
- expect(calls[0].init.method).toBe('POST');
79
- expect(calls[0].init.body).toBe('{"name":"x"}');
80
- expect(headersOf(calls[0])['Content-Type']).toBe('application/json');
81
- });
82
-
83
- it('does not send a body on GET', async () => {
84
- const { impl, calls } = stubFetch();
85
- const { handlers } = createRestConnector({ baseUrl: 'https://api.example.com', fetchImpl: impl });
86
-
87
- await handlers.request({ method: 'GET', path: '/ping', body: { ignored: true } }, {});
88
- expect(calls[0].init.body).toBeUndefined();
89
- });
90
- });
91
-
92
- // ─── auth injection ──────────────────────────────────────────────────
93
-
94
- describe('createRestConnector — static auth', () => {
95
- it('injects a bearer token', async () => {
96
- const { impl, calls } = stubFetch();
97
- const { handlers } = createRestConnector({
98
- baseUrl: 'https://api.example.com',
99
- auth: { type: 'bearer', token: 'tok-123' },
100
- fetchImpl: impl,
101
- });
102
- await handlers.request({ path: '/me' }, {});
103
- expect(headersOf(calls[0])['Authorization']).toBe('Bearer tok-123');
104
- });
105
-
106
- it('injects a basic auth header', async () => {
107
- const { impl, calls } = stubFetch();
108
- const { handlers } = createRestConnector({
109
- baseUrl: 'https://api.example.com',
110
- auth: { type: 'basic', username: 'user', password: 'pass' },
111
- fetchImpl: impl,
112
- });
113
- await handlers.request({ path: '/me' }, {});
114
- const expected = `Basic ${Buffer.from('user:pass').toString('base64')}`;
115
- expect(headersOf(calls[0])['Authorization']).toBe(expected);
116
- });
117
-
118
- it('injects an api-key header by default', async () => {
119
- const { impl, calls } = stubFetch();
120
- const { handlers } = createRestConnector({
121
- baseUrl: 'https://api.example.com',
122
- auth: { type: 'api-key', key: 'k-1', headerName: 'X-Api-Key' },
123
- fetchImpl: impl,
124
- });
125
- await handlers.request({ path: '/me' }, {});
126
- expect(headersOf(calls[0])['X-Api-Key']).toBe('k-1');
127
- });
128
-
129
- it('injects an api-key as a query param when paramName is set', async () => {
130
- const { impl, calls } = stubFetch();
131
- const { handlers } = createRestConnector({
132
- baseUrl: 'https://api.example.com',
133
- auth: { type: 'api-key', key: 'k-1', headerName: 'X-API-Key', paramName: 'api_key' },
134
- fetchImpl: impl,
135
- });
136
- await handlers.request({ path: '/me' }, {});
137
- expect(calls[0].url).toBe('https://api.example.com/me?api_key=k-1');
138
- expect(headersOf(calls[0])['X-API-Key']).toBeUndefined();
139
- });
140
-
141
- it('adds no auth for type none', async () => {
142
- const { impl, calls } = stubFetch();
143
- const { handlers } = createRestConnector({ baseUrl: 'https://api.example.com', fetchImpl: impl });
144
- await handlers.request({ path: '/public' }, {});
145
- expect(headersOf(calls[0])['Authorization']).toBeUndefined();
146
- });
147
-
148
- it('merges defaultHeaders, with per-request headers winning', async () => {
149
- const { impl, calls } = stubFetch();
150
- const { handlers } = createRestConnector({
151
- baseUrl: 'https://api.example.com',
152
- defaultHeaders: { 'X-Trace': 'on', 'X-Env': 'prod' },
153
- fetchImpl: impl,
154
- });
155
- await handlers.request({ path: '/x', headers: { 'X-Env': 'dev' } }, {});
156
- const h = headersOf(calls[0]);
157
- expect(h['X-Trace']).toBe('on');
158
- expect(h['X-Env']).toBe('dev');
159
- });
160
- });
@@ -1,174 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import type { Connector } from '@objectstack/spec/integration';
4
- import { resilientFetch } from '@objectstack/spec/shared';
5
-
6
- /**
7
- * Generic REST connector — the reference *concrete* connector (ADR-0018
8
- * §Addendum). It produces a {@link Connector} definition plus the handler for
9
- * its one action, `request`, which the baseline `connector_action` node
10
- * dispatches to.
11
- *
12
- * Open-source scope: **static** auth only (`none` / `api-key` / `basic` /
13
- * `bearer`), with credentials supplied by the caller. OAuth2 token acquisition
14
- * and refresh, credential vaulting, and multi-tenant connection lifecycle are
15
- * the enterprise tier (see `../cloud/docs/design/connector-tiering.md`) and are
16
- * deliberately out of scope here.
17
- */
18
-
19
- /** Auth config understood by the REST connector (the static subset). */
20
- export type RestAuth = Extract<
21
- Connector['authentication'],
22
- { type: 'none' | 'api-key' | 'basic' | 'bearer' }
23
- >;
24
-
25
- export interface RestConnectorOptions {
26
- /** Connector machine name (snake_case). Defaults to `rest`. */
27
- name?: string;
28
- /** Human-readable label. Defaults to a title derived from `name`. */
29
- label?: string;
30
- /** Base URL prepended to each request's `path` (e.g. `https://api.example.com`). */
31
- baseUrl: string;
32
- /** Static authentication. Defaults to `{ type: 'none' }`. */
33
- auth?: RestAuth;
34
- /** Headers merged into every request (request-level headers win). */
35
- defaultHeaders?: Record<string, string>;
36
- /** Injected for tests; defaults to the global `fetch`. */
37
- fetchImpl?: typeof fetch;
38
- }
39
-
40
- /** Input accepted by the `request` action. */
41
- export interface RestRequestInput {
42
- method?: string;
43
- path?: string;
44
- headers?: Record<string, string>;
45
- query?: Record<string, string | number | boolean | null | undefined>;
46
- body?: unknown;
47
- }
48
-
49
- /** A connector definition paired with its action handlers, ready for registerConnector(). */
50
- export interface RestConnectorBundle {
51
- def: Connector;
52
- handlers: Record<
53
- string,
54
- (input: Record<string, unknown>, ctx: unknown) => Promise<Record<string, unknown>>
55
- >;
56
- }
57
-
58
- /** Build the request URL from base + path + query, encoding query params. */
59
- function buildUrl(baseUrl: string, path: string, query?: RestRequestInput['query']): string {
60
- const base = baseUrl.replace(/\/+$/, '');
61
- const suffix = path ? (path.startsWith('/') ? path : `/${path}`) : '';
62
- const url = new URL(base + suffix);
63
- if (query) {
64
- for (const [key, value] of Object.entries(query)) {
65
- if (value !== undefined && value !== null) url.searchParams.set(key, String(value));
66
- }
67
- }
68
- return url.toString();
69
- }
70
-
71
- /**
72
- * Apply static auth to the outgoing headers / query. Returns possibly-extended
73
- * query so an `api-key` configured with `paramName` can ride the query string.
74
- */
75
- function applyAuth(
76
- auth: RestAuth,
77
- headers: Record<string, string>,
78
- query: Record<string, string | number | boolean | null | undefined>,
79
- ): void {
80
- switch (auth.type) {
81
- case 'none':
82
- return;
83
- case 'bearer':
84
- headers['Authorization'] = `Bearer ${auth.token}`;
85
- return;
86
- case 'basic': {
87
- const encoded = Buffer.from(`${auth.username}:${auth.password}`).toString('base64');
88
- headers['Authorization'] = `Basic ${encoded}`;
89
- return;
90
- }
91
- case 'api-key':
92
- if (auth.paramName) query[auth.paramName] = auth.key;
93
- else headers[auth.headerName ?? 'X-API-Key'] = auth.key;
94
- return;
95
- }
96
- }
97
-
98
- export function createRestConnector(opts: RestConnectorOptions): RestConnectorBundle {
99
- const name = opts.name ?? 'rest';
100
- const auth: RestAuth = opts.auth ?? { type: 'none' };
101
-
102
- const def: Connector = {
103
- name,
104
- label: opts.label ?? 'REST Connector',
105
- type: 'api',
106
- description: 'Generic REST/HTTP connector with static authentication.',
107
- icon: 'globe',
108
- authentication: auth,
109
- // Defaulted by ConnectorSchema; set explicitly so the literal satisfies
110
- // the (post-parse) Connector output type.
111
- status: 'active',
112
- enabled: true,
113
- connectionTimeoutMs: 30000,
114
- requestTimeoutMs: 30000,
115
- actions: [
116
- {
117
- key: 'request',
118
- label: 'HTTP Request',
119
- description: 'Send an HTTP request to the connector\'s base URL with static auth applied.',
120
- inputSchema: {
121
- type: 'object',
122
- properties: {
123
- method: { type: 'string', description: 'HTTP method (default GET)' },
124
- path: { type: 'string', description: 'Path appended to the base URL' },
125
- headers: { type: 'object', description: 'Per-request headers' },
126
- query: { type: 'object', description: 'Query parameters' },
127
- body: { description: 'Request body (JSON-encoded for non-GET)' },
128
- },
129
- },
130
- outputSchema: {
131
- type: 'object',
132
- properties: {
133
- status: { type: 'number' },
134
- ok: { type: 'boolean' },
135
- body: {},
136
- },
137
- },
138
- },
139
- ],
140
- };
141
-
142
- async function request(input: Record<string, unknown>): Promise<Record<string, unknown>> {
143
- const req = input as RestRequestInput;
144
- const method = (req.method ?? 'GET').toUpperCase();
145
- const headers: Record<string, string> = { ...opts.defaultHeaders, ...req.headers };
146
- const query: Record<string, string | number | boolean | null | undefined> = { ...req.query };
147
-
148
- applyAuth(auth, headers, query);
149
-
150
- const url = buildUrl(opts.baseUrl, req.path ?? '', query);
151
-
152
- const hasBody = req.body !== undefined && method !== 'GET' && method !== 'HEAD';
153
- if (hasBody && headers['Content-Type'] === undefined && headers['content-type'] === undefined) {
154
- headers['Content-Type'] = 'application/json';
155
- }
156
-
157
- const response = await resilientFetch(url, {
158
- method,
159
- headers,
160
- body: hasBody ? JSON.stringify(req.body) : undefined,
161
- }, { fetchImpl: opts.fetchImpl });
162
-
163
- // Parse JSON when advertised; fall back to text so non-JSON endpoints
164
- // don't throw.
165
- const contentType = response.headers.get('content-type') ?? '';
166
- const parsed = contentType.includes('application/json')
167
- ? await response.json()
168
- : await response.text();
169
-
170
- return { status: response.status, ok: response.ok, body: parsed };
171
- }
172
-
173
- return { def, handlers: { request } };
174
- }
@@ -1,61 +0,0 @@
1
- // Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2
- //
3
- // ADR-0097 — the `rest` provider factory: materialize a declarative
4
- // `provider: 'rest'` connector instance from providerConfig + resolved auth.
5
-
6
- import { describe, it, expect } from 'vitest';
7
- import type { ConnectorProviderContext } from '@objectstack/spec/integration';
8
- import { createRestProviderFactory, REST_PROVIDER_KEY } from './rest-provider.js';
9
-
10
- function stubFetch() {
11
- const calls: Array<{ url: string; init: RequestInit }> = [];
12
- const impl = (async (url: string, init: RequestInit) => {
13
- calls.push({ url, init });
14
- return {
15
- status: 200,
16
- ok: true,
17
- headers: { get: (h: string) => (h.toLowerCase() === 'content-type' ? 'application/json' : null) },
18
- json: async () => ({ ok: true }),
19
- text: async () => '{}',
20
- };
21
- }) as unknown as typeof fetch;
22
- return { impl, calls };
23
- }
24
-
25
- function ctx(partial: Partial<ConnectorProviderContext> & Pick<ConnectorProviderContext, 'providerConfig'>): ConnectorProviderContext {
26
- return { name: 'svc', label: 'Svc', type: 'api', ...partial };
27
- }
28
-
29
- describe('rest provider factory (ADR-0097)', () => {
30
- it('advertises the rest provider key', () => {
31
- expect(REST_PROVIDER_KEY).toBe('rest');
32
- });
33
-
34
- it('builds a def + request handler from providerConfig.baseUrl and applies resolved auth', async () => {
35
- const { impl, calls } = stubFetch();
36
- const factory = createRestProviderFactory({ fetchImpl: impl });
37
- const { def, handlers } = await factory(
38
- ctx({ name: 'billing', label: 'Billing', providerConfig: { baseUrl: 'https://api.example.com' }, auth: { type: 'bearer', token: 'tok' } }),
39
- );
40
-
41
- expect(def.name).toBe('billing');
42
- expect(Object.keys(handlers)).toEqual(['request']);
43
-
44
- const out = await handlers.request({ path: '/ping' }, {});
45
- expect(out).toMatchObject({ status: 200, ok: true });
46
- expect(calls[0].url).toBe('https://api.example.com/ping');
47
- expect((calls[0].init.headers as Record<string, string>).Authorization).toBe('Bearer tok');
48
- });
49
-
50
- it('throws when providerConfig.baseUrl is missing', () => {
51
- const factory = createRestProviderFactory();
52
- expect(() => factory(ctx({ providerConfig: {} }))).toThrow(/baseUrl/);
53
- });
54
-
55
- it('throws when providerConfig.defaultHeaders is not a string map', () => {
56
- const factory = createRestProviderFactory();
57
- expect(() =>
58
- factory(ctx({ providerConfig: { baseUrl: 'https://x', defaultHeaders: { n: 1 } } })),
59
- ).toThrow(/defaultHeaders/);
60
- });
61
- });
@@ -1,64 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import type { ConnectorProviderFactory, ResolvedConnectorAuth } from '@objectstack/spec/integration';
4
- import { createRestConnector, type RestAuth } from './rest-connector.js';
5
-
6
- /**
7
- * The provider key this package contributes (ADR-0097). A declarative
8
- * `connectors:` entry with `provider: 'rest'` is materialized by this factory.
9
- */
10
- export const REST_PROVIDER_KEY = 'rest';
11
-
12
- /** Injectable dependencies for {@link createRestProviderFactory} (tests). */
13
- export interface RestProviderDeps {
14
- /** Injected fetch implementation; defaults to the global `fetch`. */
15
- fetchImpl?: typeof fetch;
16
- }
17
-
18
- /** Shape of `providerConfig` for a `provider: 'rest'` declarative instance. */
19
- interface RestProviderConfig {
20
- baseUrl?: unknown;
21
- defaultHeaders?: unknown;
22
- }
23
-
24
- function isStringRecord(v: unknown): v is Record<string, string> {
25
- if (!v || typeof v !== 'object' || Array.isArray(v)) return false;
26
- return Object.values(v as Record<string, unknown>).every((x) => typeof x === 'string');
27
- }
28
-
29
- /**
30
- * Build the `rest` {@link ConnectorProviderFactory} (ADR-0097). At boot the
31
- * automation service invokes it for each `provider: 'rest'` declarative instance,
32
- * turning `providerConfig.baseUrl` (+ the resolved `auth`) into the same
33
- * `{ def, handlers }` bundle {@link createRestConnector} produces for a
34
- * hand-wired REST connector — one `request` action over static-auth HTTP.
35
- *
36
- * Hard-fails on invalid config (missing `baseUrl`), so a misconfigured instance
37
- * fails boot loudly rather than materializing a dead connector.
38
- */
39
- export function createRestProviderFactory(deps: RestProviderDeps = {}): ConnectorProviderFactory {
40
- return (ctx) => {
41
- const cfg = (ctx.providerConfig ?? {}) as RestProviderConfig;
42
- if (typeof cfg.baseUrl !== 'string' || cfg.baseUrl.length === 0) {
43
- throw new Error(
44
- `connector-rest provider: connector '${ctx.name}' requires providerConfig.baseUrl (a non-empty string, e.g. https://api.example.com).`,
45
- );
46
- }
47
- if (cfg.defaultHeaders !== undefined && !isStringRecord(cfg.defaultHeaders)) {
48
- throw new Error(
49
- `connector-rest provider: connector '${ctx.name}' providerConfig.defaultHeaders must be a string→string map.`,
50
- );
51
- }
52
- // `ResolvedConnectorAuth` is exactly the static-auth subset RestAuth expects
53
- // (the credentialRef has already been resolved to a secret upstream).
54
- const auth = ctx.auth as ResolvedConnectorAuth | undefined as RestAuth | undefined;
55
- return createRestConnector({
56
- name: ctx.name,
57
- label: ctx.label,
58
- baseUrl: cfg.baseUrl,
59
- auth,
60
- defaultHeaders: isStringRecord(cfg.defaultHeaders) ? cfg.defaultHeaders : undefined,
61
- fetchImpl: deps.fetchImpl,
62
- });
63
- };
64
- }
package/tsconfig.json DELETED
@@ -1,10 +0,0 @@
1
- {
2
- "extends": "../../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "./dist",
5
- "rootDir": "./src",
6
- "types": ["node"]
7
- },
8
- "include": ["src/**/*"],
9
- "exclude": ["dist", "node_modules", "**/*.test.ts"]
10
- }