@stackables/bridge 1.1.0 → 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 +35 -21
- package/build/bridge-format.js +66 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,9 +5,15 @@ Wire data between APIs, tools, and fields using `.bridge` files—no resolvers,
|
|
|
5
5
|
|
|
6
6
|
```bash
|
|
7
7
|
npm install @stackables/bridge
|
|
8
|
-
|
|
9
8
|
```
|
|
10
9
|
|
|
10
|
+
> **Developer Preview**
|
|
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.
|
|
14
|
+
>
|
|
15
|
+
> Feedback: We are actively looking for use cases. Please share yours in our GitHub Discussions.
|
|
16
|
+
|
|
11
17
|
---
|
|
12
18
|
|
|
13
19
|
## The Idea
|
|
@@ -41,17 +47,17 @@ Access const values in bridges or tools via `with const as c`, then reference as
|
|
|
41
47
|
|
|
42
48
|
### 2. Extend Blocks (`extend`)
|
|
43
49
|
|
|
44
|
-
Defines the "Where" and the "How." Takes a function (or parent tool) and configures i, giving it a new
|
|
50
|
+
Defines the "Where" and the "How." Takes a function (or parent tool) and configures i, giving it a new name.
|
|
45
51
|
|
|
46
52
|
```hcl
|
|
47
|
-
extend <source> as <name>
|
|
53
|
+
extend <source> as <name> {
|
|
48
54
|
[with context] # Injects GraphQL context (auth, secrets, etc.)
|
|
49
55
|
[on error = <json_fallback>] # Fallback value if tool fails
|
|
50
56
|
[on error <- <source>] # Pull fallback from context/tool
|
|
51
57
|
|
|
52
58
|
<param> = <value> # Constant/Default value
|
|
53
59
|
<param> <- <source> # Dynamic wire
|
|
54
|
-
|
|
60
|
+
}
|
|
55
61
|
```
|
|
56
62
|
|
|
57
63
|
When `<source>` is a function name (e.g. `httpCall`), a new tool is created.
|
|
@@ -62,7 +68,7 @@ When `<source>` is an existing tool name, the new tool inherits its configuratio
|
|
|
62
68
|
The resolver logic connecting GraphQL schema fields to your tools.
|
|
63
69
|
|
|
64
70
|
```hcl
|
|
65
|
-
bridge <Type.field>
|
|
71
|
+
bridge <Type.field> {
|
|
66
72
|
with <tool> [as <alias>]
|
|
67
73
|
with input [as <i>]
|
|
68
74
|
|
|
@@ -81,7 +87,7 @@ bridge <Type.field>
|
|
|
81
87
|
# Array Mapping
|
|
82
88
|
<field>[] <- <source>[]
|
|
83
89
|
.<sub_field> <- .<sub_src> # Relative scoping
|
|
84
|
-
|
|
90
|
+
}
|
|
85
91
|
```
|
|
86
92
|
|
|
87
93
|
---
|
|
@@ -97,11 +103,12 @@ Each layer handles a different failure mode. They compose freely.
|
|
|
97
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.
|
|
98
104
|
|
|
99
105
|
```hcl
|
|
100
|
-
extend httpCall as geo
|
|
106
|
+
extend httpCall as geo {
|
|
101
107
|
baseUrl = "https://nominatim.openstreetmap.org"
|
|
102
108
|
method = GET
|
|
103
109
|
path = /search
|
|
104
110
|
on error = { "lat": 0, "lon": 0 } # tool-level default
|
|
111
|
+
}
|
|
105
112
|
```
|
|
106
113
|
|
|
107
114
|
#### Layer 2 — Wire `||` (null / absent values)
|
|
@@ -153,11 +160,12 @@ Multiple `||` sources desugar to **parallel wires** — all sources are evaluate
|
|
|
153
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.
|
|
154
161
|
|
|
155
162
|
```hcl
|
|
156
|
-
bridge Mutation.updateUser
|
|
163
|
+
bridge Mutation.updateUser {
|
|
157
164
|
with audit.logger as log
|
|
158
165
|
|
|
159
166
|
# 'log' runs even if the client doesn't query the 'status' field
|
|
160
167
|
status <-! log:i.changeData
|
|
168
|
+
}
|
|
161
169
|
```
|
|
162
170
|
|
|
163
171
|
### The Pipe Operator (`:`)
|
|
@@ -172,18 +180,20 @@ result <- transform:normalize:i.rawData
|
|
|
172
180
|
Full example with a tool that has 2 input parameters:
|
|
173
181
|
|
|
174
182
|
```hcl
|
|
175
|
-
extend currencyConverter as convert
|
|
183
|
+
extend currencyConverter as convert {
|
|
176
184
|
currency = EUR # default currency
|
|
185
|
+
}
|
|
177
186
|
|
|
178
|
-
bridge Query.price
|
|
187
|
+
bridge Query.price {
|
|
179
188
|
with convert as c
|
|
180
189
|
with input as i
|
|
181
190
|
|
|
182
|
-
c.currency <- i.currency # overrides the default per request
|
|
191
|
+
c.currency <- i.currency # overrides the default per request
|
|
183
192
|
|
|
184
|
-
# Safe to use repeatedly — each is an independent tool call
|
|
185
|
-
itemPrice <- c:i.itemPrice
|
|
186
|
-
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
|
+
}
|
|
187
197
|
```
|
|
188
198
|
|
|
189
199
|
---
|
|
@@ -260,27 +270,30 @@ The Bridge ships with built-in tools under the `std` namespace, always available
|
|
|
260
270
|
**No `extend` block needed** for pipe-like tools — reference them with the `std.` prefix in the `with` header:
|
|
261
271
|
|
|
262
272
|
```hcl
|
|
263
|
-
bridge Query.format
|
|
273
|
+
bridge Query.format {
|
|
264
274
|
with std.upperCase as up
|
|
265
275
|
with std.lowerCase as lo
|
|
266
276
|
with input as i
|
|
267
277
|
|
|
268
|
-
upper <- up:i.text
|
|
269
|
-
lower <- lo:i.text
|
|
278
|
+
upper <- up:i.text
|
|
279
|
+
lower <- lo:i.text
|
|
280
|
+
}
|
|
270
281
|
```
|
|
271
282
|
|
|
272
283
|
Use an `extend` block when you need to configure defaults:
|
|
273
284
|
|
|
274
285
|
```hcl
|
|
275
|
-
extend std.pickFirst as pf
|
|
286
|
+
extend std.pickFirst as pf {
|
|
276
287
|
strict = true
|
|
288
|
+
}
|
|
277
289
|
|
|
278
|
-
bridge Query.onlyResult
|
|
290
|
+
bridge Query.onlyResult {
|
|
279
291
|
with pf
|
|
280
292
|
with someApi as api
|
|
281
293
|
with input as i
|
|
282
294
|
|
|
283
|
-
value <- pf:api.items
|
|
295
|
+
value <- pf:api.items
|
|
296
|
+
}
|
|
284
297
|
```
|
|
285
298
|
|
|
286
299
|
### Adding Custom Tools
|
|
@@ -315,11 +328,12 @@ const schema = bridgeTransform(createSchema({ typeDefs }), instructions, {
|
|
|
315
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.
|
|
316
329
|
|
|
317
330
|
```hcl
|
|
318
|
-
extend httpCall as geo
|
|
331
|
+
extend httpCall as geo {
|
|
319
332
|
cache = 300 # cache for 5 minutes
|
|
320
333
|
baseUrl = "https://nominatim.openstreetmap.org"
|
|
321
334
|
method = GET
|
|
322
335
|
path = /search
|
|
336
|
+
}
|
|
323
337
|
```
|
|
324
338
|
|
|
325
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) {
|