@stackables/bridge 1.1.1 → 1.3.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/README.md CHANGED
@@ -7,10 +7,11 @@ Wire data between APIs, tools, and fields using `.bridge` files—no resolvers,
7
7
  npm install @stackables/bridge
8
8
  ```
9
9
 
10
- > **Developer Preview**
10
+ > **Developer Preview**
11
11
  > The Bridge v1.x is a public preview and is not recommended for production use.
12
- > - Stability: Breaking changes to the .bridge language and TypeScript API will occur frequently.
13
- > - Versioning: We follow strict SemVer starting from v2.0.0.
12
+ >
13
+ > * Stability: Breaking changes to the .bridge language and TypeScript API will occur frequently.
14
+ > * Versioning: We follow strict SemVer starting from v2.0.0.
14
15
  >
15
16
  > Feedback: We are actively looking for use cases. Please share yours in our GitHub Discussions.
16
17
 
@@ -29,10 +30,36 @@ The Bridge is a **Smart Mapping Outgoing Proxy**, not a replacement for your app
29
30
  * **Use it to:** Morph external API shapes, enforce single exit points for security, and swap providers (e.g., SendGrid to Postmark) without changing app code.
30
31
  * **Don't use it for:** Complex business logic or database transactions. Keep the "intelligence" in your Tools; keep the "connectivity" in your Bridge.
31
32
 
33
+ ### Wiring, not Programming
34
+
35
+ The Bridge is not a programming language. It is a Data Topology Language.
36
+
37
+ Unlike Python or JavaScript, where you write a list of instructions for a computer to execute in order, a .bridge file describes a static circuit. There is no "execution pointer" that moves from the top of the file to the bottom.
38
+
39
+ No Sequential Logic: Shuffling the lines inside a define or bridge block changes nothing. The engine doesn't "run" your file; it uses your file to understand how your GraphQL fields are physically wired to your tools.
40
+
41
+ Pull, Don't Push: In a normal language, you "push" data into variables. In The Bridge, the GraphQL query "pulls" data through the wires. If a client doesn't ask for a field, the wire is "dead"—no code runs, and no API is called.
42
+
43
+ Declarative Connections: When you write o.name <- api.name, you aren't commanding a copy operation; you are soldering a permanent link between two points in your graph.
44
+
45
+ **Don't think in scripts. Think in schematics.**
46
+
47
+ ### Portability & Performance
48
+
49
+ While the reference engine is implemented in TypeScript, the Bridge language itself is a simple, high-level specification for data flow. Because it describes intent rather than execution, it is architecturally "runtime-blind." It can be interpreted by any high-performance engines written in Rust, Go, or C++ without changing a single line of your .bridge files.
50
+
32
51
  ---
33
52
 
34
53
  ## The Language
35
54
 
55
+ Every `.bridge` file must begin with a version declaration.
56
+
57
+ ```hcl
58
+ version 1.4
59
+ ```
60
+
61
+ This is the first non-blank, non-comment line. Everything else follows after it.
62
+
36
63
  ### 1. Const Blocks (`const`)
37
64
 
38
65
  Named JSON values reusable across tools and bridges. Avoids repetition for fallback payloads, defaults, and config fragments.
@@ -45,69 +72,161 @@ const maxRetries = 3
45
72
 
46
73
  Access const values in bridges or tools via `with const as c`, then reference as `c.<name>.<path>`.
47
74
 
48
- ### 2. Extend Blocks (`extend`)
75
+ ### 2. Tool Blocks (`tool ... from`)
49
76
 
50
- Defines the "Where" and the "How." Takes a function (or parent tool) and configures i, giving it a new name.
77
+ Defines the "Where" and the "How." Takes a function (or parent tool) and configures it, giving it a new name.
51
78
 
52
79
  ```hcl
53
- extend <source> as <name>
80
+ tool <name> from <source> {
54
81
  [with context] # Injects GraphQL context (auth, secrets, etc.)
55
82
  [on error = <json_fallback>] # Fallback value if tool fails
56
83
  [on error <- <source>] # Pull fallback from context/tool
57
-
58
- <param> = <value> # Constant/Default value
59
- <param> <- <source> # Dynamic wire
60
84
 
85
+ .<param> = <value> # Constant/Default value (dot = "this tool's param")
86
+ .<param> <- <source> # Dynamic wire
87
+ }
61
88
  ```
62
89
 
90
+ Param lines use a `.` prefix — the dot means "this tool's own field". `with` and `on error` lines do not use a dot; they are control flow, not param assignments.
91
+
63
92
  When `<source>` is a function name (e.g. `httpCall`), a new tool is created.
64
93
  When `<source>` is an existing tool name, the new tool inherits its configuration.
65
94
 
66
- ### 3. Bridge Blocks (`bridge`)
95
+ ### 3. Define Blocks (`define`)
96
+
97
+ Reusable named subgraphs — compose tools and wires into a pipeline, then invoke it from any bridge.
98
+
99
+ ```hcl
100
+ define <name> {
101
+ with <tool> as <handle> # Tools used inside the pipeline
102
+ with input as <handle> # Inputs provided by the caller
103
+ with output as <handle> # Outputs returned to the caller
104
+
105
+ <handle>.<param> <- <source> # Wiring (same syntax as bridge)
106
+ <handle>.<param> = <value> # Constants
107
+ }
108
+ ```
109
+
110
+ Use a define in a bridge with `with <define> as <handle>`:
111
+
112
+ ```hcl
113
+ define geocode {
114
+ with std.httpCall as geo
115
+ with input as i
116
+ with output as o
117
+
118
+ geo.baseUrl = "https://nominatim.openstreetmap.org"
119
+ geo.method = GET
120
+ geo.path = /search
121
+ geo.q <- i.city
122
+ o.lat <- geo[0].lat
123
+ o.lon <- geo[0].lon
124
+ }
125
+
126
+ bridge Query.location {
127
+ with geocode as g
128
+ with input as i
129
+ with output as o
130
+
131
+ g.city <- i.city
132
+ o.lat <- g.lat
133
+ o.lon <- g.lon
134
+ }
135
+ ```
136
+
137
+ Each invocation is fully isolated — calling the same define twice creates independent tool instances with no namespace collisions.
138
+
139
+ ### 4. Bridge Blocks (`bridge`)
67
140
 
68
141
  The resolver logic connecting GraphQL schema fields to your tools.
69
142
 
70
143
  ```hcl
71
- bridge <Type.field>
144
+ bridge <Type.field> {
72
145
  with <tool> [as <alias>]
73
- with input [as <i>]
146
+ with input as i
147
+ with output as o
74
148
 
75
149
  # Field Mapping
76
- <field> = <json> # Constant value
77
- <field> <- <source> # Standard Pull (lazy)
78
- <field> <-! <source> # Forced Push (eager/side-effect)
150
+ o.<field> = <json> # Constant output value
151
+ o.<field> <- <source> # Standard Pull (lazy)
152
+ o.<field> <-! <source> # Forced Push (eager/side-effect)
79
153
 
80
154
  # Pipe chain (tool transformation)
81
- <field> <- handle:source # Route source through tool handle
155
+ o.<field> <- handle:source # Route source through tool handle
82
156
 
83
157
  # Fallbacks
84
- <field> <- <source> || <alt> || <alt> # Null-coalesce: use alt if source is null
85
- <field> <- <source> ?? <fallback> # Error-fallback: use fallback if chain throws
158
+ o.<field> <- <source> || <alt> || <alt> # Null-coalesce: use alt if source is null
159
+ o.<field> <- <source> ?? <fallback> # Error-fallback: use fallback if chain throws
160
+
161
+ # Array Mapping (brace block per element)
162
+ o.<field> <- <source>[] as <iter> {
163
+ .<sub_field> <- <iter>.<sub_src> # Element field via iterator
164
+ .<sub_field> = "constant" # Element constant
165
+ }
166
+ }
167
+ ```
86
168
 
87
- # Array Mapping
88
- <field>[] <- <source>[]
89
- .<sub_field> <- .<sub_src> # Relative scoping
169
+ Bridge can be fully implemented in the defined pipeline.
90
170
 
171
+ ```
172
+ define namedOperation {
173
+ ....
174
+ }
175
+
176
+ bridge <Type.field> with namedOperation
91
177
  ```
92
178
 
93
179
  ---
94
180
 
95
181
  ## Key Features
96
182
 
183
+ ### Reserved Words
184
+
185
+ **Keywords** — cannot be used as tool names, handle aliases, or const names:
186
+
187
+ > `bridge` `with` `as` `from` `const` `tool` `version` `define`
188
+
189
+ **Source identifiers** — reserved for their specific role inside `bridge` and `tool` blocks:
190
+
191
+ > `input` `output` `context`
192
+
193
+ A parse error is thrown immediately if any of these appear where a user-defined name is expected.
194
+
195
+ ### Scope Rules
196
+
197
+ Bridge uses explicit scoping. Any entity referenced inside a `bridge` or `tool` block must first be introduced into scope using a `with` clause.
198
+
199
+ This includes:
200
+
201
+ * `tools`
202
+ * `input`
203
+ * `output`
204
+ * `context`
205
+ * `tool aliases`
206
+
207
+ The `input` and `output` handles represents GraphQL field arguments and output type. They exists **only inside `bridge` blocks**.
208
+
209
+ Because `tool` blocks are evaluated before any GraphQL execution occurs, they cannot reference `input` or `output`.
210
+
211
+ > **Rule of thumb:**
212
+ > `tool ... from` defines tools, `bridge` executes the graph.
213
+ > Since `input` and `output` belong to GraphQL execution, they only exist inside bridges.
214
+
97
215
  ### Resiliency
98
216
 
99
217
  Each layer handles a different failure mode. They compose freely.
100
218
 
101
219
  #### Layer 1 — Tool `on error` (execution errors)
102
220
 
103
- Declared inside the `extend` block. Catches any exception thrown by the tool's `fn(input)`. All tools that `extend` this tool inherit the fallback.
221
+ Declared inside the `tool` block. Catches any exception thrown by the tool's `fn(input)`. All tools that inherit from this tool inherit the fallback.
104
222
 
105
223
  ```hcl
106
- extend httpCall as geo
107
- baseUrl = "https://nominatim.openstreetmap.org"
108
- method = GET
109
- path = /search
224
+ tool geo from httpCall {
225
+ .baseUrl = "https://nominatim.openstreetmap.org"
226
+ .method = GET
227
+ .path = /search
110
228
  on error = { "lat": 0, "lon": 0 } # tool-level default
229
+ }
111
230
  ```
112
231
 
113
232
  #### Layer 2 — Wire `||` (null / absent values)
@@ -115,14 +234,16 @@ extend httpCall as geo
115
234
  Fires when a source resolves **successfully but returns `null` or `undefined`**. The fallback can be a JSON literal or another source expression (handle path or pipe chain). Multiple `||` alternatives chain left-to-right like `COALESCE`.
116
235
 
117
236
  ```hcl
237
+ with output as o
238
+
118
239
  # JSON literal fallback
119
- lat <- geo.lat || 0.0
240
+ o.lat <- geo.lat || 0.0
120
241
 
121
242
  # Alternative source fallback
122
- label <- api.label || backup.label || "unknown"
243
+ o.label <- api.label || backup.label || "unknown"
123
244
 
124
245
  # Pipe chain as alternative
125
- textPart <- i.textBody || convert:i.htmlBody || "empty"
246
+ o.textPart <- i.textBody || convert:i.htmlBody || "empty"
126
247
  ```
127
248
 
128
249
  #### Layer 3 — Wire `??` (errors and exceptions)
@@ -130,11 +251,13 @@ textPart <- i.textBody || convert:i.htmlBody || "empty"
130
251
  Fires when the **entire resolution chain throws** (network failure, tool down, dependency error). Does not fire on null values — that's `||`'s job. The fallback can be a JSON literal or a source/pipe expression (evaluated lazily, only when the error fires).
131
252
 
132
253
  ```hcl
254
+ with output as o
255
+
133
256
  # JSON literal error fallback
134
- lat <- geo.lat ?? 0.0
257
+ o.lat <- geo.lat ?? 0.0
135
258
 
136
259
  # Error fallback pulls from another source
137
- label <- api.label ?? errorHandler:i.fallbackMsg
260
+ o.label <- api.label ?? errorHandler:i.fallbackMsg
138
261
  ```
139
262
 
140
263
  #### Full COALESCE — composing all three layers
@@ -142,8 +265,10 @@ label <- api.label ?? errorHandler:i.fallbackMsg
142
265
  `||` and `??` compose into a Postgres-style `COALESCE` with an error guard at the end:
143
266
 
144
267
  ```hcl
145
- # label <- A || B || C || "literal" ?? errorSource
146
- label <- api.label || tool:api.backup.label || "unknown" ?? tool:const.errorString
268
+ with output as o
269
+
270
+ # o.label <- A || B || C || "literal" ?? errorSource
271
+ o.label <- api.label || tool:api.backup.label || "unknown" ?? tool:const.errorString
147
272
 
148
273
  # Evaluation order:
149
274
  # api.label non-null → use it immediately
@@ -159,11 +284,14 @@ Multiple `||` sources desugar to **parallel wires** — all sources are evaluate
159
284
  By default, the engine is **lazy**. Use `<-!` to force execution regardless of demand—perfect for side-effects like analytics, audit logging, or cache warming.
160
285
 
161
286
  ```hcl
162
- bridge Mutation.updateUser
287
+ bridge Mutation.updateUser {
163
288
  with audit.logger as log
289
+ with input as in
290
+ with output as out
164
291
 
165
292
  # 'log' runs even if the client doesn't query the 'status' field
166
- status <-! log:i.changeData
293
+ out.status <-! log:in.changeData
294
+ }
167
295
  ```
168
296
 
169
297
  ### The Pipe Operator (`:`)
@@ -171,25 +299,52 @@ bridge Mutation.updateUser
171
299
  Routes data right-to-left through one or more tool handles: `dest <- handle:source`.
172
300
 
173
301
  ```hcl
302
+ with output as o
303
+
174
304
  # i.rawData → normalize → transform → result
175
- result <- transform:normalize:i.rawData
305
+ o.result <- transform:normalize:i.rawData
176
306
  ```
177
307
 
178
308
  Full example with a tool that has 2 input parameters:
179
309
 
180
310
  ```hcl
181
- extend currencyConverter as convert
182
- currency = EUR # default currency
311
+ tool convert from currencyConverter {
312
+ .currency = EUR # default currency
313
+ }
183
314
 
184
- bridge Query.price
315
+ # example with pipe syntax
316
+ bridge Query.price {
185
317
  with convert as c
186
318
  with input as i
319
+ with output as o
320
+
321
+ c.currency <- i.currency # overrides the default per request
322
+
323
+ # Safe to use repeatedly — each is an independent tool call
324
+ o.itemPrice <- c:i.itemPrice
325
+ o.totalPrice <- c:i.totalPrice
326
+ }
327
+
328
+ # same without the pipe syntax
329
+ tool c1 from convert
330
+ tool c2 from convert
331
+
332
+ bridge Query.price {
333
+ with c1
334
+ with c2
335
+ with input as i
336
+ with output as o
337
+
338
+ c1.currency <- i.currency # overrides the default per request
339
+ c2.currency <- i.currency # overrides the default per request
187
340
 
188
- c.currency <- i.currency # overrides the default per request
341
+ c1.in <- i.itemPrice
342
+ c2.in <- i.totalPrice
189
343
 
190
- # Safe to use repeatedly — each is an independent tool call
191
- itemPrice <- c:i.itemPrice
192
- totalPrice <- c:i.totalPrice
344
+ # Safe to use repeatedly — each is an independent tool call
345
+ o.itemPrice <- c1
346
+ o.totalPrice <- c2
347
+ }
193
348
  ```
194
349
 
195
350
  ---
@@ -205,9 +360,10 @@ totalPrice <- c:i.totalPrice
205
360
  | **`\|\|`** | Null-coalesce | Next alternative if current source is `null`/`undefined`. Fires on absent values, not errors. |
206
361
  | **`??`** | Error-fallback | Alternative used when the resolution chain **throws**. Fires on errors, not null values. |
207
362
  | **`on error`** | Tool Fallback | Returns a default if the tool's `fn(input)` throws. |
208
- | **`extend`** | Tool Definition | Configures a function or extends a parent tool. |
363
+ | **`tool ... from`** | Tool Definition | Configures a function or inherits from a parent tool. |
364
+ | **`define`** | Reusable Subgraph | Declares a named pipeline template invocable from bridges. |
209
365
  | **`const`** | Named Value | Declares reusable JSON constants. |
210
- | **`[] <- []`** | Map | Iterates over arrays to create nested wire contexts. |
366
+ | **`<- src[] as i { }`** | Map | Iterates over source array; each element accessed via the named iterator `i`. `i.field` references the current element. `.field = "value"` sets an element constant. |
211
367
 
212
368
  ---
213
369
 
@@ -263,30 +419,35 @@ The Bridge ships with built-in tools under the `std` namespace, always available
263
419
 
264
420
  ### Using Built-in Tools
265
421
 
266
- **No `extend` block needed** for pipe-like tools — reference them with the `std.` prefix in the `with` header:
422
+ **No `tool` block needed** for pipe-like tools — reference them with the `std.` prefix in the `with` header:
267
423
 
268
424
  ```hcl
269
- bridge Query.format
425
+ bridge Query.format {
270
426
  with std.upperCase as up
271
427
  with std.lowerCase as lo
272
428
  with input as i
429
+ with output as o
273
430
 
274
- upper <- up:i.text
275
- lower <- lo:i.text
431
+ o.upper <- up:i.text
432
+ o.lower <- lo:i.text
433
+ }
276
434
  ```
277
435
 
278
- Use an `extend` block when you need to configure defaults:
436
+ Use a `tool` block when you need to configure defaults:
279
437
 
280
438
  ```hcl
281
- extend std.pickFirst as pf
282
- strict = true
439
+ tool pf from std.pickFirst {
440
+ .strict = true
441
+ }
283
442
 
284
- bridge Query.onlyResult
443
+ bridge Query.onlyResult {
285
444
  with pf
286
445
  with someApi as api
287
446
  with input as i
447
+ with output as o
288
448
 
289
- value <- pf:api.items
449
+ o.value <- pf:api.items
450
+ }
290
451
  ```
291
452
 
292
453
  ### Adding Custom Tools
@@ -321,11 +482,12 @@ const schema = bridgeTransform(createSchema({ typeDefs }), instructions, {
321
482
  Add `cache = <seconds>` to any `httpCall` tool to enable TTL-based response caching. Identical requests (same method + URL + params) return the cached result without hitting the network.
322
483
 
323
484
  ```hcl
324
- extend httpCall as geo
325
- cache = 300 # cache for 5 minutes
326
- baseUrl = "https://nominatim.openstreetmap.org"
327
- method = GET
328
- path = /search
485
+ tool geo from httpCall {
486
+ .cache = 300 # cache for 5 minutes
487
+ .baseUrl = "https://nominatim.openstreetmap.org"
488
+ .method = GET
489
+ .path = /search
490
+ }
329
491
  ```
330
492
 
331
493
  The default is an in-memory store. For Redis or other backends, pass a custom `CacheStore` to `createHttpCall`:
@@ -264,7 +264,12 @@ export class ExecutionTree {
264
264
  return [w.to.path, value];
265
265
  }));
266
266
  for (const [path, value] of resolved) {
267
- setNested(input, path, value);
267
+ if (path.length === 0 && value != null && typeof value === "object") {
268
+ Object.assign(input, value);
269
+ }
270
+ else {
271
+ setNested(input, path, value);
272
+ }
268
273
  }
269
274
  // Call ToolDef-backed tool function
270
275
  if (toolDef) {
@@ -289,6 +294,11 @@ export class ExecutionTree {
289
294
  if (directFn) {
290
295
  return directFn(input);
291
296
  }
297
+ // Define pass-through: synthetic trunks created by define inlining
298
+ // act as data containers — bridge wires set their values, no tool needed.
299
+ if (target.module.startsWith("__define_")) {
300
+ return input;
301
+ }
292
302
  throw new Error(`No tool found for "${toolName}"`);
293
303
  })();
294
304
  }
@@ -432,7 +442,7 @@ export class ExecutionTree {
432
442
  // Strip numeric indices (array positions) from path for wire matching
433
443
  const cleanPath = pathSegments.filter((p) => !/^\d+$/.test(p));
434
444
  // Find wires whose target matches this trunk + path
435
- const matches = this.bridge?.wires.filter((w) => !w.to.element &&
445
+ const matches = this.bridge?.wires.filter((w) => (w.to.element ? !!this.parent : true) &&
436
446
  sameTrunk(w.to, this.trunk) &&
437
447
  pathEquals(w.to.path, cleanPath)) ?? [];
438
448
  if (matches.length > 0) {
@@ -1,14 +1,4 @@
1
1
  import type { Instruction } from "./types.js";
2
- /**
3
- * Parse .bridge text format into structured instructions.
4
- *
5
- * The .bridge format is a human-readable representation of connection wires.
6
- * Multiple blocks are separated by `---`.
7
- * Tool blocks define API tools, bridge blocks define wire mappings.
8
- *
9
- * @param text - Bridge definition text
10
- * @returns Array of instructions (Bridge, ToolDef)
11
- */
12
2
  export declare function parseBridge(text: string): Instruction[];
13
3
  /**
14
4
  * Parse a dot-separated path with optional array indices.