@stackables/bridge 1.2.0 → 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,68 +72,159 @@ 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
84
+
85
+ .<param> = <value> # Constant/Default value (dot = "this tool's param")
86
+ .<param> <- <source> # Dynamic wire
60
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
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
+ ```
168
+
169
+ Bridge can be fully implemented in the defined pipeline.
86
170
 
87
- # Array Mapping
88
- <field>[] <- <source>[]
89
- .<sub_field> <- .<sub_src> # Relative scoping
171
+ ```
172
+ define namedOperation {
173
+ ....
90
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
111
229
  }
112
230
  ```
@@ -116,14 +234,16 @@ extend httpCall as geo {
116
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`.
117
235
 
118
236
  ```hcl
237
+ with output as o
238
+
119
239
  # JSON literal fallback
120
- lat <- geo.lat || 0.0
240
+ o.lat <- geo.lat || 0.0
121
241
 
122
242
  # Alternative source fallback
123
- label <- api.label || backup.label || "unknown"
243
+ o.label <- api.label || backup.label || "unknown"
124
244
 
125
245
  # Pipe chain as alternative
126
- textPart <- i.textBody || convert:i.htmlBody || "empty"
246
+ o.textPart <- i.textBody || convert:i.htmlBody || "empty"
127
247
  ```
128
248
 
129
249
  #### Layer 3 — Wire `??` (errors and exceptions)
@@ -131,11 +251,13 @@ textPart <- i.textBody || convert:i.htmlBody || "empty"
131
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).
132
252
 
133
253
  ```hcl
254
+ with output as o
255
+
134
256
  # JSON literal error fallback
135
- lat <- geo.lat ?? 0.0
257
+ o.lat <- geo.lat ?? 0.0
136
258
 
137
259
  # Error fallback pulls from another source
138
- label <- api.label ?? errorHandler:i.fallbackMsg
260
+ o.label <- api.label ?? errorHandler:i.fallbackMsg
139
261
  ```
140
262
 
141
263
  #### Full COALESCE — composing all three layers
@@ -143,8 +265,10 @@ label <- api.label ?? errorHandler:i.fallbackMsg
143
265
  `||` and `??` compose into a Postgres-style `COALESCE` with an error guard at the end:
144
266
 
145
267
  ```hcl
146
- # label <- A || B || C || "literal" ?? errorSource
147
- 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
148
272
 
149
273
  # Evaluation order:
150
274
  # api.label non-null → use it immediately
@@ -162,9 +286,11 @@ By default, the engine is **lazy**. Use `<-!` to force execution regardless of d
162
286
  ```hcl
163
287
  bridge Mutation.updateUser {
164
288
  with audit.logger as log
289
+ with input as in
290
+ with output as out
165
291
 
166
292
  # 'log' runs even if the client doesn't query the 'status' field
167
- status <-! log:i.changeData
293
+ out.status <-! log:in.changeData
168
294
  }
169
295
  ```
170
296
 
@@ -173,26 +299,51 @@ bridge Mutation.updateUser {
173
299
  Routes data right-to-left through one or more tool handles: `dest <- handle:source`.
174
300
 
175
301
  ```hcl
302
+ with output as o
303
+
176
304
  # i.rawData → normalize → transform → result
177
- result <- transform:normalize:i.rawData
305
+ o.result <- transform:normalize:i.rawData
178
306
  ```
179
307
 
180
308
  Full example with a tool that has 2 input parameters:
181
309
 
182
310
  ```hcl
183
- extend currencyConverter as convert {
184
- currency = EUR # default currency
311
+ tool convert from currencyConverter {
312
+ .currency = EUR # default currency
185
313
  }
186
314
 
315
+ # example with pipe syntax
187
316
  bridge Query.price {
188
317
  with convert as c
189
318
  with input as i
319
+ with output as o
190
320
 
191
321
  c.currency <- i.currency # overrides the default per request
192
322
 
193
323
  # Safe to use repeatedly — each is an independent tool call
194
- itemPrice <- c:i.itemPrice
195
- totalPrice <- c:i.totalPrice
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
340
+
341
+ c1.in <- i.itemPrice
342
+ c2.in <- i.totalPrice
343
+
344
+ # Safe to use repeatedly — each is an independent tool call
345
+ o.itemPrice <- c1
346
+ o.totalPrice <- c2
196
347
  }
197
348
  ```
198
349
 
@@ -209,9 +360,10 @@ bridge Query.price {
209
360
  | **`\|\|`** | Null-coalesce | Next alternative if current source is `null`/`undefined`. Fires on absent values, not errors. |
210
361
  | **`??`** | Error-fallback | Alternative used when the resolution chain **throws**. Fires on errors, not null values. |
211
362
  | **`on error`** | Tool Fallback | Returns a default if the tool's `fn(input)` throws. |
212
- | **`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. |
213
365
  | **`const`** | Named Value | Declares reusable JSON constants. |
214
- | **`[] <- []`** | 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. |
215
367
 
216
368
  ---
217
369
 
@@ -267,32 +419,34 @@ The Bridge ships with built-in tools under the `std` namespace, always available
267
419
 
268
420
  ### Using Built-in Tools
269
421
 
270
- **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:
271
423
 
272
424
  ```hcl
273
425
  bridge Query.format {
274
426
  with std.upperCase as up
275
427
  with std.lowerCase as lo
276
428
  with input as i
429
+ with output as o
277
430
 
278
- upper <- up:i.text
279
- lower <- lo:i.text
431
+ o.upper <- up:i.text
432
+ o.lower <- lo:i.text
280
433
  }
281
434
  ```
282
435
 
283
- Use an `extend` block when you need to configure defaults:
436
+ Use a `tool` block when you need to configure defaults:
284
437
 
285
438
  ```hcl
286
- extend std.pickFirst as pf {
287
- strict = true
439
+ tool pf from std.pickFirst {
440
+ .strict = true
288
441
  }
289
442
 
290
443
  bridge Query.onlyResult {
291
444
  with pf
292
445
  with someApi as api
293
446
  with input as i
447
+ with output as o
294
448
 
295
- value <- pf:api.items
449
+ o.value <- pf:api.items
296
450
  }
297
451
  ```
298
452
 
@@ -328,11 +482,11 @@ const schema = bridgeTransform(createSchema({ typeDefs }), instructions, {
328
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.
329
483
 
330
484
  ```hcl
331
- extend httpCall as geo {
332
- cache = 300 # cache for 5 minutes
333
- baseUrl = "https://nominatim.openstreetmap.org"
334
- method = GET
335
- 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
336
490
  }
337
491
  ```
338
492
 
@@ -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.