@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 +27 -19
- package/build/bridge-format.js +66 -7
- package/package.json +1 -1
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`:
|
package/build/bridge-format.js
CHANGED
|
@@ -85,7 +85,28 @@ function isJsonLiteral(s) {
|
|
|
85
85
|
s === "true" || s === "false" || s === "null";
|
|
86
86
|
}
|
|
87
87
|
function parseBridgeBlock(block, lineOffset) {
|
|
88
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|