@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 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 {};