@stackables/bridge 1.1.1 → 1.2.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
@@ -50,14 +50,14 @@ Access const values in bridges or tools via `with const as c`, then reference as
50
50
  Defines the "Where" and the "How." Takes a function (or parent tool) and configures i, giving it a new name.
51
51
 
52
52
  ```hcl
53
- extend <source> as <name>
53
+ extend <source> as <name> {
54
54
  [with context] # Injects GraphQL context (auth, secrets, etc.)
55
55
  [on error = <json_fallback>] # Fallback value if tool fails
56
56
  [on error <- <source>] # Pull fallback from context/tool
57
57
 
58
58
  <param> = <value> # Constant/Default value
59
59
  <param> <- <source> # Dynamic wire
60
-
60
+ }
61
61
  ```
62
62
 
63
63
  When `<source>` is a function name (e.g. `httpCall`), a new tool is created.
@@ -68,7 +68,7 @@ When `<source>` is an existing tool name, the new tool inherits its configuratio
68
68
  The resolver logic connecting GraphQL schema fields to your tools.
69
69
 
70
70
  ```hcl
71
- bridge <Type.field>
71
+ bridge <Type.field> {
72
72
  with <tool> [as <alias>]
73
73
  with input [as <i>]
74
74
 
@@ -87,7 +87,7 @@ bridge <Type.field>
87
87
  # Array Mapping
88
88
  <field>[] <- <source>[]
89
89
  .<sub_field> <- .<sub_src> # Relative scoping
90
-
90
+ }
91
91
  ```
92
92
 
93
93
  ---
@@ -103,11 +103,12 @@ Each layer handles a different failure mode. They compose freely.
103
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.
104
104
 
105
105
  ```hcl
106
- extend httpCall as geo
106
+ extend httpCall as geo {
107
107
  baseUrl = "https://nominatim.openstreetmap.org"
108
108
  method = GET
109
109
  path = /search
110
110
  on error = { "lat": 0, "lon": 0 } # tool-level default
111
+ }
111
112
  ```
112
113
 
113
114
  #### Layer 2 — Wire `||` (null / absent values)
@@ -159,11 +160,12 @@ Multiple `||` sources desugar to **parallel wires** — all sources are evaluate
159
160
  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
161
 
161
162
  ```hcl
162
- bridge Mutation.updateUser
163
+ bridge Mutation.updateUser {
163
164
  with audit.logger as log
164
165
 
165
166
  # 'log' runs even if the client doesn't query the 'status' field
166
167
  status <-! log:i.changeData
168
+ }
167
169
  ```
168
170
 
169
171
  ### The Pipe Operator (`:`)
@@ -178,18 +180,20 @@ result <- transform:normalize:i.rawData
178
180
  Full example with a tool that has 2 input parameters:
179
181
 
180
182
  ```hcl
181
- extend currencyConverter as convert
183
+ extend currencyConverter as convert {
182
184
  currency = EUR # default currency
185
+ }
183
186
 
184
- bridge Query.price
187
+ bridge Query.price {
185
188
  with convert as c
186
189
  with input as i
187
190
 
188
- c.currency <- i.currency # overrides the default per request
191
+ c.currency <- i.currency # overrides the default per request
189
192
 
190
- # Safe to use repeatedly — each is an independent tool call
191
- itemPrice <- c:i.itemPrice
192
- totalPrice <- c:i.totalPrice
193
+ # Safe to use repeatedly — each is an independent tool call
194
+ itemPrice <- c:i.itemPrice
195
+ totalPrice <- c:i.totalPrice
196
+ }
193
197
  ```
194
198
 
195
199
  ---
@@ -266,27 +270,30 @@ The Bridge ships with built-in tools under the `std` namespace, always available
266
270
  **No `extend` block needed** for pipe-like tools — reference them with the `std.` prefix in the `with` header:
267
271
 
268
272
  ```hcl
269
- bridge Query.format
273
+ bridge Query.format {
270
274
  with std.upperCase as up
271
275
  with std.lowerCase as lo
272
276
  with input as i
273
277
 
274
- upper <- up:i.text
275
- lower <- lo:i.text
278
+ upper <- up:i.text
279
+ lower <- lo:i.text
280
+ }
276
281
  ```
277
282
 
278
283
  Use an `extend` block when you need to configure defaults:
279
284
 
280
285
  ```hcl
281
- extend std.pickFirst as pf
286
+ extend std.pickFirst as pf {
282
287
  strict = true
288
+ }
283
289
 
284
- bridge Query.onlyResult
290
+ bridge Query.onlyResult {
285
291
  with pf
286
292
  with someApi as api
287
293
  with input as i
288
294
 
289
- value <- pf:api.items
295
+ value <- pf:api.items
296
+ }
290
297
  ```
291
298
 
292
299
  ### Adding Custom Tools
@@ -321,11 +328,12 @@ const schema = bridgeTransform(createSchema({ typeDefs }), instructions, {
321
328
  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
329
 
323
330
  ```hcl
324
- extend httpCall as geo
331
+ extend httpCall as geo {
325
332
  cache = 300 # cache for 5 minutes
326
333
  baseUrl = "https://nominatim.openstreetmap.org"
327
334
  method = GET
328
335
  path = /search
336
+ }
329
337
  ```
330
338
 
331
339
  The default is an in-memory store. For Redis or other backends, pass a custom `CacheStore` to `createHttpCall`:
@@ -85,7 +85,28 @@ function isJsonLiteral(s) {
85
85
  s === "true" || s === "false" || s === "null";
86
86
  }
87
87
  function parseBridgeBlock(block, lineOffset) {
88
- const lines = block.split("\n").map((l) => l.trimEnd());
88
+ // Validate mandatory braces: `bridge Foo.bar {` ... `}`
89
+ const rawLines = block.split("\n");
90
+ const keywordIdx = rawLines.findIndex((l) => /^bridge\s/i.test(l.trim()));
91
+ if (keywordIdx !== -1) {
92
+ const kw = rawLines[keywordIdx].trim();
93
+ if (!kw.endsWith("{")) {
94
+ throw new Error(`Line ${lineOffset + keywordIdx + 1}: bridge block must use braces: bridge Type.field {`);
95
+ }
96
+ const hasClose = rawLines.some((l) => l.trimEnd() === "}");
97
+ if (!hasClose) {
98
+ throw new Error(`Line ${lineOffset + keywordIdx + 1}: bridge block missing closing }`);
99
+ }
100
+ }
101
+ // Strip braces for internal parsing
102
+ const lines = rawLines.map((l) => {
103
+ const trimmed = l.trimEnd();
104
+ if (trimmed === "}")
105
+ return "";
106
+ if (/^bridge\s/i.test(trimmed) && trimmed.endsWith("{"))
107
+ return trimmed.replace(/\s*\{\s*$/, "");
108
+ return trimmed;
109
+ });
89
110
  const instructions = [];
90
111
  /** 1-based global line number for error messages */
91
112
  const ln = (i) => lineOffset + i + 1;
@@ -650,7 +671,35 @@ function parseConstLines(block, lineOffset) {
650
671
  * is treated as a function name.
651
672
  */
652
673
  function parseToolBlock(block, lineOffset, previousInstructions) {
653
- const lines = block.split("\n").map((l) => l.trimEnd());
674
+ // Validate mandatory braces for blocks that have a body (deps / wires)
675
+ const rawLines = block.split("\n");
676
+ const keywordIdx = rawLines.findIndex((l) => /^(tool|extend)\s/i.test(l.trim()));
677
+ if (keywordIdx !== -1) {
678
+ // Check if there are non-blank, non-comment body lines after the keyword
679
+ const bodyLines = rawLines.slice(keywordIdx + 1).filter((l) => {
680
+ const t = l.trim();
681
+ return t !== "" && !t.startsWith("#") && t !== "}";
682
+ });
683
+ const kw = rawLines[keywordIdx].trim();
684
+ if (bodyLines.length > 0) {
685
+ if (!kw.endsWith("{")) {
686
+ throw new Error(`Line ${lineOffset + keywordIdx + 1}: extend/tool block with body must use braces: extend Foo as bar {`);
687
+ }
688
+ const hasClose = rawLines.some((l) => l.trimEnd() === "}");
689
+ if (!hasClose) {
690
+ throw new Error(`Line ${lineOffset + keywordIdx + 1}: extend/tool block missing closing }`);
691
+ }
692
+ }
693
+ }
694
+ // Strip braces for internal parsing
695
+ const lines = rawLines.map((l) => {
696
+ const trimmed = l.trimEnd();
697
+ if (trimmed === "}")
698
+ return "";
699
+ if (/^(tool|extend)\s/i.test(trimmed) && trimmed.endsWith("{"))
700
+ return trimmed.replace(/\s*\{\s*$/, "");
701
+ return trimmed;
702
+ });
654
703
  /** 1-based global line number for error messages */
655
704
  const ln = (i) => lineOffset + i + 1;
656
705
  let toolName = "";
@@ -826,16 +875,17 @@ export function serializeBridge(instructions) {
826
875
  for (const bridge of bridges) {
827
876
  blocks.push(serializeBridgeBlock(bridge));
828
877
  }
829
- return blocks.join("\n\n---\n\n") + "\n";
878
+ return blocks.join("\n\n") + "\n";
830
879
  }
831
880
  function serializeToolBlock(tool) {
832
881
  const lines = [];
882
+ const hasBody = tool.deps.length > 0 || tool.wires.length > 0;
833
883
  // Declaration line — use `extend` format
834
884
  if (tool.extends) {
835
- lines.push(`extend ${tool.extends} as ${tool.name}`);
885
+ lines.push(hasBody ? `extend ${tool.extends} as ${tool.name} {` : `extend ${tool.extends} as ${tool.name}`);
836
886
  }
837
887
  else {
838
- lines.push(`extend ${tool.fn} as ${tool.name}`);
888
+ lines.push(hasBody ? `extend ${tool.fn} as ${tool.name} {` : `extend ${tool.fn} as ${tool.name}`);
839
889
  }
840
890
  // Dependencies
841
891
  for (const dep of tool.deps) {
@@ -882,6 +932,8 @@ function serializeToolBlock(tool) {
882
932
  lines.push(` ${wire.target} <- ${wire.source}`);
883
933
  }
884
934
  }
935
+ if (hasBody)
936
+ lines.push(`}`);
885
937
  return lines.join("\n");
886
938
  }
887
939
  /**
@@ -934,7 +986,7 @@ function serializePipeOrRef(ref, pipeHandleTrunkKeys, toInMap, handleMap, bridge
934
986
  function serializeBridgeBlock(bridge) {
935
987
  const lines = [];
936
988
  // ── Header ──────────────────────────────────────────────────────────
937
- lines.push(`bridge ${bridge.type}.${bridge.field}`);
989
+ lines.push(`bridge ${bridge.type}.${bridge.field} {`);
938
990
  for (const h of bridge.handles) {
939
991
  switch (h.kind) {
940
992
  case "tool": {
@@ -966,6 +1018,8 @@ function serializeBridgeBlock(bridge) {
966
1018
  }
967
1019
  }
968
1020
  lines.push("");
1021
+ // Mark where the wire body starts — everything after this gets 2-space indent
1022
+ const wireBodyStart = lines.length;
969
1023
  // ── Build handle map for reverse resolution ─────────────────────────
970
1024
  const { handleMap, inputHandle } = buildHandleMap(bridge);
971
1025
  // ── Pipe fork registry ──────────────────────────────────────────────
@@ -1096,10 +1150,15 @@ function serializeBridgeBlock(bridge) {
1096
1150
  lines.push(`${destStr} ${arrow} ${handleChain.join(":")}:${sourceStr}${nfb}${errf}`);
1097
1151
  }
1098
1152
  }
1153
+ // Indent wire body lines and close the block
1154
+ for (let i = wireBodyStart; i < lines.length; i++) {
1155
+ if (lines[i] !== "")
1156
+ lines[i] = ` ${lines[i]}`;
1157
+ }
1158
+ lines.push(`}`);
1099
1159
  return lines.join("\n");
1100
1160
  }
1101
1161
  /**
1102
- * Build a reverse lookup: trunk key → handle name.
1103
1162
  * Recomputes instance numbers from handle bindings in declaration order.
1104
1163
  */
1105
1164
  function buildHandleMap(bridge) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackables/bridge",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Declarative dataflow for GraphQL",
5
5
  "main": "./build/index.js",
6
6
  "type": "module",