@stackables/bridge 1.0.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/LICENSE +21 -0
- package/README.md +291 -0
- package/build/ExecutionTree.d.ts +50 -0
- package/build/ExecutionTree.js +403 -0
- package/build/bridge-format.d.ts +24 -0
- package/build/bridge-format.js +1056 -0
- package/build/bridge-transform.d.ts +15 -0
- package/build/bridge-transform.js +57 -0
- package/build/index.d.ts +5 -0
- package/build/index.js +3 -0
- package/build/tools/find-object.d.ts +4 -0
- package/build/tools/find-object.js +11 -0
- package/build/tools/http-call.d.ts +34 -0
- package/build/tools/http-call.js +116 -0
- package/build/tools/index.d.ts +43 -0
- package/build/tools/index.js +43 -0
- package/build/tools/lower-case.d.ts +3 -0
- package/build/tools/lower-case.js +3 -0
- package/build/tools/pick-first.d.ts +11 -0
- package/build/tools/pick-first.js +20 -0
- package/build/tools/to-array.d.ts +8 -0
- package/build/tools/to-array.js +8 -0
- package/build/tools/upper-case.d.ts +3 -0
- package/build/tools/upper-case.js +3 -0
- package/build/types.d.ts +218 -0
- package/build/types.js +2 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Stackables
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
# The Bridge
|
|
2
|
+
|
|
3
|
+
**Declarative dataflow for GraphQL.**
|
|
4
|
+
Wire data between APIs, tools, and fields using `.bridge` files—no resolvers, no codegen, no plumbing.
|
|
5
|
+
|
|
6
|
+
```bash
|
|
7
|
+
npm install @stackables/bridge
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## The Idea
|
|
14
|
+
|
|
15
|
+
Most GraphQL backends are just plumbing: take input, call an API, rename fields, and return. **The Bridge** turns that manual labor into a declarative graph of intent.
|
|
16
|
+
|
|
17
|
+
The engine resolves **backwards from demand**: when a GraphQL query requests `results[0].lat`, the engine traces the wire back to the `position.lat` of a specific API response. Only the data required to satisfy the query is ever fetched or executed.
|
|
18
|
+
|
|
19
|
+
### What it is (and isn't)
|
|
20
|
+
|
|
21
|
+
The Bridge is a **Smart Mapping Outgoing Proxy**, not a replacement for your application logic.
|
|
22
|
+
|
|
23
|
+
* **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.
|
|
24
|
+
* **Don't use it for:** Complex business logic or database transactions. Keep the "intelligence" in your Tools; keep the "connectivity" in your Bridge.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## The Language
|
|
29
|
+
|
|
30
|
+
### 1. Const Blocks (`const`)
|
|
31
|
+
|
|
32
|
+
Named JSON values reusable across tools and bridges. Avoids repetition for fallback payloads, defaults, and config fragments.
|
|
33
|
+
|
|
34
|
+
```hcl
|
|
35
|
+
const fallbackGeo = { "lat": 0, "lon": 0 }
|
|
36
|
+
const defaultCurrency = "EUR"
|
|
37
|
+
const maxRetries = 3
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Access const values in bridges or tools via `with const as c`, then reference as `c.<name>.<path>`.
|
|
41
|
+
|
|
42
|
+
### 2. Extend Blocks (`extend`)
|
|
43
|
+
|
|
44
|
+
Defines the "Where" and the "How." Takes a function (or parent tool) and configures i, giving it a new namet.
|
|
45
|
+
|
|
46
|
+
```hcl
|
|
47
|
+
extend <source> as <name>
|
|
48
|
+
[with context] # Injects GraphQL context (auth, secrets, etc.)
|
|
49
|
+
[on error = <json_fallback>] # Fallback value if tool fails
|
|
50
|
+
[on error <- <source>] # Pull fallback from context/tool
|
|
51
|
+
|
|
52
|
+
<param> = <value> # Constant/Default value
|
|
53
|
+
<param> <- <source> # Dynamic wire
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
When `<source>` is a function name (e.g. `httpCall`), a new tool is created.
|
|
58
|
+
When `<source>` is an existing tool name, the new tool inherits its configuration.
|
|
59
|
+
|
|
60
|
+
### 3. Bridge Blocks (`bridge`)
|
|
61
|
+
|
|
62
|
+
The resolver logic connecting GraphQL schema fields to your tools.
|
|
63
|
+
|
|
64
|
+
```hcl
|
|
65
|
+
bridge <Type.field>
|
|
66
|
+
with <tool> [as <alias>]
|
|
67
|
+
with input [as <i>]
|
|
68
|
+
|
|
69
|
+
# Field Mapping
|
|
70
|
+
<field> <- <source> # Standard Pull (Lazy)
|
|
71
|
+
<field> <-! <source> # Forced Push (Eager/Side-effect)
|
|
72
|
+
|
|
73
|
+
# Array Mapping
|
|
74
|
+
<field>[] <- <source>[]
|
|
75
|
+
.<sub_field> <- .<sub_src> # Relative scoping
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Key Features
|
|
82
|
+
|
|
83
|
+
### Resiliency
|
|
84
|
+
|
|
85
|
+
Two layers of fault tolerance prevent a single API failure from crashing the response:
|
|
86
|
+
|
|
87
|
+
1. **Layer 1 — Tool `on error`**: Catches tool execution failures. Child tools inherit this via `extend`.
|
|
88
|
+
2. **Layer 2 — Wire `??` fallback**: Catches any failure in the resolution chain (missing data, network timeout) as a last resort.
|
|
89
|
+
|
|
90
|
+
```hcl
|
|
91
|
+
lat <- geo.lat ?? 0.0
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Forced Wires (`<-!`)
|
|
96
|
+
|
|
97
|
+
By default, the engine is **lazy**. Use `<-!` to force execution regardless of demand—perfect for side-effects like analytics, audit logging, or cache warming.
|
|
98
|
+
|
|
99
|
+
```hcl
|
|
100
|
+
bridge Mutation.updateUser
|
|
101
|
+
with audit.logger as log
|
|
102
|
+
|
|
103
|
+
# 'log' runs even if the client doesn't query the 'status' field
|
|
104
|
+
status <-! log|i.changeData
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
### The Pipe Operator (`|`)
|
|
109
|
+
|
|
110
|
+
Chains data through tools right-to-left: `dest <- tool | source`.
|
|
111
|
+
|
|
112
|
+
```hcl
|
|
113
|
+
# i.rawData -> normalize -> transform -> result
|
|
114
|
+
result <- transform|normalize|i.rawData
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Full example with a tool with 2 input parameters.
|
|
118
|
+
|
|
119
|
+
```hcl
|
|
120
|
+
extend currencyConverter as convert
|
|
121
|
+
currency = EUR # default currency
|
|
122
|
+
|
|
123
|
+
bridge Query.price
|
|
124
|
+
with convert as c
|
|
125
|
+
with input as i
|
|
126
|
+
|
|
127
|
+
c.currency <- i.currency # overrides the default per request
|
|
128
|
+
|
|
129
|
+
# Safe to use repeatedly
|
|
130
|
+
itemPrice <- c|i.itemPrice
|
|
131
|
+
totalPrice <- c|i.totalPrice
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Syntax Reference
|
|
137
|
+
|
|
138
|
+
| Operator | Type | Behavior | Notes |
|
|
139
|
+
| --- | --- | --- | --- |
|
|
140
|
+
| **`=`** | **Constant** | Sets a static value. | |
|
|
141
|
+
| **`<-`** | **Wire** | Pulls data from a source at runtime. | |
|
|
142
|
+
| **`<-!`** | **Force** | Eagerly schedules a tool (for side-effects). | |
|
|
143
|
+
| **`\|`** | **Pipe** | Chains data through tools right-to-left. | |
|
|
144
|
+
| **`??`** | **Fallback** | Wire-level default if the resolution chain fails. | |
|
|
145
|
+
| **`on error`** | **Tool Fallback** | Returns a default if the tool's `fn(input)` throws. | |
|
|
146
|
+
| **`extend`** | **Tool Definition** | Configures a function or extends a parent tool. | |
|
|
147
|
+
| **`const`** | **Named Value** | Declares reusable JSON constants. | |
|
|
148
|
+
| **`[] <- []`** | **Map** | Iterates over arrays to create nested wire contexts. | |
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Usage
|
|
153
|
+
|
|
154
|
+
### 1. Basic Setup
|
|
155
|
+
|
|
156
|
+
The Bridge wraps your existing GraphQL schema, handling the `resolve` functions automatically.
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
import { createSchema, createYoga } from "graphql-yoga";
|
|
160
|
+
import { bridgeTransform, parseBridge } from "@stackables/bridge";
|
|
161
|
+
|
|
162
|
+
const schema = bridgeTransform(
|
|
163
|
+
createSchema({ typeDefs }),
|
|
164
|
+
parseBridge(bridgeFileText)
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
const yoga = createYoga({
|
|
168
|
+
schema,
|
|
169
|
+
context: () => ({
|
|
170
|
+
api: { key: process.env.API_KEY },
|
|
171
|
+
}),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 2. Custom Tools
|
|
177
|
+
|
|
178
|
+
```typescript
|
|
179
|
+
const schema = bridgeTransform(createSchema({ typeDefs }), instructions, {
|
|
180
|
+
tools: {
|
|
181
|
+
toCents: ({ in: dollars }) => ({ cents: dollars * 100 }),
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## Built-in Tools
|
|
190
|
+
|
|
191
|
+
The Bridge ships with built-in tools under the `std` namespace, always available by default. All tools (including `httpCall`) live under `std` and can be referenced with or without the `std.` prefix.
|
|
192
|
+
|
|
193
|
+
| Tool | Input | Output | Description |
|
|
194
|
+
| --- | --- | --- | --- |
|
|
195
|
+
| `httpCall` | `{ baseUrl, method?, path?, headers?, cache?, ...fields }` | JSON response | REST API caller. GET fields → query params; POST/PUT/PATCH/DELETE → JSON body. `cache` = TTL in seconds (0 = off). |
|
|
196
|
+
| `upperCase` | `{ in: string }` | `string` | Converts `in` to UPPER CASE. |
|
|
197
|
+
| `lowerCase` | `{ in: string }` | `string` | Converts `in` to lower case. |
|
|
198
|
+
| `findObject` | `{ in: any[], ...criteria }` | `object \| undefined` | Finds the first object in `in` where all criteria match. |
|
|
199
|
+
| `pickFirst` | `{ in: any[], strict?: bool }` | `any` | Returns the first array element. With `strict = true`, throws if the array is empty or has more than one item. |
|
|
200
|
+
| `toArray` | `{ in: any }` | `any[]` | Wraps a single value in an array. Returns as-is if already an array. |
|
|
201
|
+
|
|
202
|
+
### Using Built-in Tools
|
|
203
|
+
|
|
204
|
+
**No `extend` block needed** for pipe-like tools — reference them with the `std.` prefix in the `with` header:
|
|
205
|
+
|
|
206
|
+
```hcl
|
|
207
|
+
bridge Query.format
|
|
208
|
+
with std.upperCase as up
|
|
209
|
+
with std.lowerCase as lo
|
|
210
|
+
with input as i
|
|
211
|
+
|
|
212
|
+
upper <- up|i.text
|
|
213
|
+
lower <- lo|i.text
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Use an `extend` block when you need to configure defaults:
|
|
217
|
+
|
|
218
|
+
```hcl
|
|
219
|
+
extend std.pickFirst as pf
|
|
220
|
+
strict = true
|
|
221
|
+
|
|
222
|
+
bridge Query.onlyResult
|
|
223
|
+
with pf
|
|
224
|
+
with someApi as api
|
|
225
|
+
with input as i
|
|
226
|
+
|
|
227
|
+
value <- pf|api.items
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Adding Custom Tools
|
|
231
|
+
|
|
232
|
+
The `std` namespace is always included automatically. Just add your own tools — no need to spread `builtinTools`:
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { bridgeTransform } from "@stackables/bridge";
|
|
236
|
+
|
|
237
|
+
const schema = bridgeTransform(createSchema({ typeDefs }), instructions, {
|
|
238
|
+
tools: {
|
|
239
|
+
myCustomTool: (input) => ({ result: input.value * 2 }),
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
// std.upperCase, std.lowerCase, etc. are still available
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
To override a `std` tool, replace the namespace (shallow merge):
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import { bridgeTransform, std } from "@stackables/bridge";
|
|
249
|
+
|
|
250
|
+
const schema = bridgeTransform(createSchema({ typeDefs }), instructions, {
|
|
251
|
+
tools: {
|
|
252
|
+
std: { ...std, upperCase: myCustomUpperCase },
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### Response Caching
|
|
258
|
+
|
|
259
|
+
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.
|
|
260
|
+
|
|
261
|
+
```hcl
|
|
262
|
+
extend httpCall as geo
|
|
263
|
+
cache = 300 # cache for 5 minutes
|
|
264
|
+
baseUrl = "https://nominatim.openstreetmap.org"
|
|
265
|
+
method = GET
|
|
266
|
+
path = /search
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
The default is an in-memory store. For Redis or other backends, pass a custom `CacheStore` to `createHttpCall`:
|
|
270
|
+
|
|
271
|
+
```typescript
|
|
272
|
+
import { createHttpCall, std } from "@stackables/bridge";
|
|
273
|
+
import type { CacheStore } from "@stackables/bridge";
|
|
274
|
+
|
|
275
|
+
const redisCache: CacheStore = {
|
|
276
|
+
async get(key) { return redis.get(key).then(v => v ? JSON.parse(v) : undefined); },
|
|
277
|
+
async set(key, value, ttl) { await redis.set(key, JSON.stringify(value), "EX", ttl); },
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
bridgeTransform(schema, instructions, {
|
|
281
|
+
tools: { std: { ...std, httpCall: createHttpCall(fetch, redisCache) } },
|
|
282
|
+
});
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Why The Bridge?
|
|
288
|
+
|
|
289
|
+
* **No Resolver Sprawl:** Stop writing identical `fetch` and `map` logic.
|
|
290
|
+
* **Provider Agnostic:** Swap implementations (e.g., SendGrid vs Postmark) at the request level.
|
|
291
|
+
* **Edge-Ready:** Small footprint; works in Node, Bun, and Cloudflare Workers.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Bridge, Instruction, NodeRef, ToolMap } from "./types.js";
|
|
2
|
+
/** Matches graphql's internal Path type (not part of the public exports map) */
|
|
3
|
+
interface Path {
|
|
4
|
+
readonly prev: Path | undefined;
|
|
5
|
+
readonly key: string | number;
|
|
6
|
+
readonly typename: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
type Trunk = {
|
|
9
|
+
module: string;
|
|
10
|
+
type: string;
|
|
11
|
+
field: string;
|
|
12
|
+
instance?: number;
|
|
13
|
+
};
|
|
14
|
+
export declare class ExecutionTree {
|
|
15
|
+
trunk: Trunk;
|
|
16
|
+
private instructions;
|
|
17
|
+
private toolFns?;
|
|
18
|
+
private context?;
|
|
19
|
+
private parent?;
|
|
20
|
+
state: Record<string, any>;
|
|
21
|
+
bridge: Bridge | undefined;
|
|
22
|
+
private toolDepCache;
|
|
23
|
+
private toolDefCache;
|
|
24
|
+
private pipeHandleMap;
|
|
25
|
+
constructor(trunk: Trunk, instructions: Instruction[], toolFns?: ToolMap | undefined, context?: Record<string, any> | undefined, parent?: ExecutionTree | undefined);
|
|
26
|
+
/** Derive tool name from a trunk */
|
|
27
|
+
private getToolName;
|
|
28
|
+
/** Deep-lookup a tool function by dotted name (e.g. "std.upperCase").
|
|
29
|
+
* Falls back to a flat key lookup for backward compat (e.g. "hereapi.geocode" as literal key). */
|
|
30
|
+
private lookupToolFn;
|
|
31
|
+
/** Resolve a ToolDef by name, merging the extends chain (cached) */
|
|
32
|
+
private resolveToolDefByName;
|
|
33
|
+
/** Resolve a tool definition's wires into a nested input object */
|
|
34
|
+
private resolveToolWires;
|
|
35
|
+
/** Resolve a source reference from a tool wire against its dependencies */
|
|
36
|
+
private resolveToolSource;
|
|
37
|
+
/** Call a tool dependency (cached per request) */
|
|
38
|
+
private resolveToolDep;
|
|
39
|
+
schedule(target: Trunk): any;
|
|
40
|
+
shadow(): ExecutionTree;
|
|
41
|
+
private pullSingle;
|
|
42
|
+
pull(refs: NodeRef[]): Promise<any>;
|
|
43
|
+
push(args: Record<string, any>): void;
|
|
44
|
+
/** Eagerly schedule tools targeted by forced (<-!) wires. */
|
|
45
|
+
executeForced(): void;
|
|
46
|
+
/** Resolve a set of matched wires — constants win, then pull from sources.\n * If a wire has a `fallback` value and all sources reject, return the fallback. */
|
|
47
|
+
private resolveWires;
|
|
48
|
+
response(ipath: Path, array: boolean): Promise<any>;
|
|
49
|
+
}
|
|
50
|
+
export {};
|