@sensigo/realm 0.1.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 +150 -0
- package/dist/adapters/adapter-utils.d.ts +14 -0
- package/dist/adapters/adapter-utils.d.ts.map +1 -0
- package/dist/adapters/adapter-utils.js +18 -0
- package/dist/adapters/adapter-utils.js.map +1 -0
- package/dist/adapters/airtable-adapter.d.ts +51 -0
- package/dist/adapters/airtable-adapter.d.ts.map +1 -0
- package/dist/adapters/airtable-adapter.js +425 -0
- package/dist/adapters/airtable-adapter.js.map +1 -0
- package/dist/adapters/file-adapter.d.ts +14 -0
- package/dist/adapters/file-adapter.d.ts.map +1 -0
- package/dist/adapters/file-adapter.js +89 -0
- package/dist/adapters/file-adapter.js.map +1 -0
- package/dist/adapters/github-adapter.d.ts +40 -0
- package/dist/adapters/github-adapter.d.ts.map +1 -0
- package/dist/adapters/github-adapter.js +286 -0
- package/dist/adapters/github-adapter.js.map +1 -0
- package/dist/adapters/gorgias-adapter.d.ts +40 -0
- package/dist/adapters/gorgias-adapter.d.ts.map +1 -0
- package/dist/adapters/gorgias-adapter.js +300 -0
- package/dist/adapters/gorgias-adapter.js.map +1 -0
- package/dist/adapters/http-adapter.d.ts +28 -0
- package/dist/adapters/http-adapter.d.ts.map +1 -0
- package/dist/adapters/http-adapter.js +97 -0
- package/dist/adapters/http-adapter.js.map +1 -0
- package/dist/adapters/mock-adapter.d.ts +16 -0
- package/dist/adapters/mock-adapter.d.ts.map +1 -0
- package/dist/adapters/mock-adapter.js +61 -0
- package/dist/adapters/mock-adapter.js.map +1 -0
- package/dist/adapters/notion-adapter.d.ts +54 -0
- package/dist/adapters/notion-adapter.d.ts.map +1 -0
- package/dist/adapters/notion-adapter.js +751 -0
- package/dist/adapters/notion-adapter.js.map +1 -0
- package/dist/adapters/parcelpanel-adapter.d.ts +60 -0
- package/dist/adapters/parcelpanel-adapter.d.ts.map +1 -0
- package/dist/adapters/parcelpanel-adapter.js +251 -0
- package/dist/adapters/parcelpanel-adapter.js.map +1 -0
- package/dist/adapters/rate-limiter.d.ts +26 -0
- package/dist/adapters/rate-limiter.d.ts.map +1 -0
- package/dist/adapters/rate-limiter.js +3 -0
- package/dist/adapters/rate-limiter.js.map +1 -0
- package/dist/adapters/shopify-adapter.d.ts +92 -0
- package/dist/adapters/shopify-adapter.d.ts.map +1 -0
- package/dist/adapters/shopify-adapter.js +415 -0
- package/dist/adapters/shopify-adapter.js.map +1 -0
- package/dist/adapters/slack-adapter.d.ts +18 -0
- package/dist/adapters/slack-adapter.d.ts.map +1 -0
- package/dist/adapters/slack-adapter.js +81 -0
- package/dist/adapters/slack-adapter.js.map +1 -0
- package/dist/adapters/token-bucket.d.ts +27 -0
- package/dist/adapters/token-bucket.d.ts.map +1 -0
- package/dist/adapters/token-bucket.js +109 -0
- package/dist/adapters/token-bucket.js.map +1 -0
- package/dist/config/secrets.d.ts +15 -0
- package/dist/config/secrets.d.ts.map +1 -0
- package/dist/config/secrets.js +33 -0
- package/dist/config/secrets.js.map +1 -0
- package/dist/engine/eligibility.d.ts +50 -0
- package/dist/engine/eligibility.d.ts.map +1 -0
- package/dist/engine/eligibility.js +267 -0
- package/dist/engine/eligibility.js.map +1 -0
- package/dist/engine/error-resolution.d.ts +20 -0
- package/dist/engine/error-resolution.d.ts.map +1 -0
- package/dist/engine/error-resolution.js +32 -0
- package/dist/engine/error-resolution.js.map +1 -0
- package/dist/engine/execution-loop.d.ts +101 -0
- package/dist/engine/execution-loop.d.ts.map +1 -0
- package/dist/engine/execution-loop.js +1156 -0
- package/dist/engine/execution-loop.js.map +1 -0
- package/dist/engine/lifecycle.d.ts +14 -0
- package/dist/engine/lifecycle.d.ts.map +1 -0
- package/dist/engine/lifecycle.js +17 -0
- package/dist/engine/lifecycle.js.map +1 -0
- package/dist/engine/precondition.d.ts +30 -0
- package/dist/engine/precondition.d.ts.map +1 -0
- package/dist/engine/precondition.js +125 -0
- package/dist/engine/precondition.js.map +1 -0
- package/dist/engine/prompt-template.d.ts +25 -0
- package/dist/engine/prompt-template.d.ts.map +1 -0
- package/dist/engine/prompt-template.js +66 -0
- package/dist/engine/prompt-template.js.map +1 -0
- package/dist/engine/render-template.d.ts +52 -0
- package/dist/engine/render-template.d.ts.map +1 -0
- package/dist/engine/render-template.js +548 -0
- package/dist/engine/render-template.js.map +1 -0
- package/dist/engine/state-guard.d.ts +15 -0
- package/dist/engine/state-guard.d.ts.map +1 -0
- package/dist/engine/state-guard.js +40 -0
- package/dist/engine/state-guard.js.map +1 -0
- package/dist/engine/trace-normalizer.d.ts +36 -0
- package/dist/engine/trace-normalizer.d.ts.map +1 -0
- package/dist/engine/trace-normalizer.js +146 -0
- package/dist/engine/trace-normalizer.js.map +1 -0
- package/dist/engine/trace-policy.d.ts +53 -0
- package/dist/engine/trace-policy.d.ts.map +1 -0
- package/dist/engine/trace-policy.js +35 -0
- package/dist/engine/trace-policy.js.map +1 -0
- package/dist/engine/workflow-context-loader.d.ts +9 -0
- package/dist/engine/workflow-context-loader.d.ts.map +1 -0
- package/dist/engine/workflow-context-loader.js +41 -0
- package/dist/engine/workflow-context-loader.js.map +1 -0
- package/dist/evidence/snapshot.d.ts +38 -0
- package/dist/evidence/snapshot.d.ts.map +1 -0
- package/dist/evidence/snapshot.js +53 -0
- package/dist/evidence/snapshot.js.map +1 -0
- package/dist/extensions/default-registry.d.ts +19 -0
- package/dist/extensions/default-registry.d.ts.map +1 -0
- package/dist/extensions/default-registry.js +31 -0
- package/dist/extensions/default-registry.js.map +1 -0
- package/dist/extensions/processor.d.ts +13 -0
- package/dist/extensions/processor.d.ts.map +1 -0
- package/dist/extensions/processor.js +3 -0
- package/dist/extensions/processor.js.map +1 -0
- package/dist/extensions/registry.d.ts +25 -0
- package/dist/extensions/registry.d.ts.map +1 -0
- package/dist/extensions/registry.js +43 -0
- package/dist/extensions/registry.js.map +1 -0
- package/dist/extensions/service-adapter.d.ts +35 -0
- package/dist/extensions/service-adapter.d.ts.map +1 -0
- package/dist/extensions/service-adapter.js +3 -0
- package/dist/extensions/service-adapter.js.map +1 -0
- package/dist/extensions/step-handler.d.ts +28 -0
- package/dist/extensions/step-handler.d.ts.map +1 -0
- package/dist/extensions/step-handler.js +3 -0
- package/dist/extensions/step-handler.js.map +1 -0
- package/dist/handlers/primitives/compare-strings.d.ts +13 -0
- package/dist/handlers/primitives/compare-strings.d.ts.map +1 -0
- package/dist/handlers/primitives/compare-strings.js +28 -0
- package/dist/handlers/primitives/compare-strings.js.map +1 -0
- package/dist/handlers/primitives/count-results.d.ts +21 -0
- package/dist/handlers/primitives/count-results.d.ts.map +1 -0
- package/dist/handlers/primitives/count-results.js +23 -0
- package/dist/handlers/primitives/count-results.js.map +1 -0
- package/dist/handlers/primitives/partition-by-substring.d.ts +18 -0
- package/dist/handlers/primitives/partition-by-substring.d.ts.map +1 -0
- package/dist/handlers/primitives/partition-by-substring.js +28 -0
- package/dist/handlers/primitives/partition-by-substring.js.map +1 -0
- package/dist/handlers/primitives/resolve-resource.d.ts +13 -0
- package/dist/handlers/primitives/resolve-resource.d.ts.map +1 -0
- package/dist/handlers/primitives/resolve-resource.js +21 -0
- package/dist/handlers/primitives/resolve-resource.js.map +1 -0
- package/dist/handlers/primitives/walk-field.d.ts +11 -0
- package/dist/handlers/primitives/walk-field.d.ts.map +1 -0
- package/dist/handlers/primitives/walk-field.js +31 -0
- package/dist/handlers/primitives/walk-field.js.map +1 -0
- package/dist/handlers/validate-field-match.d.ts +11 -0
- package/dist/handlers/validate-field-match.d.ts.map +1 -0
- package/dist/handlers/validate-field-match.js +39 -0
- package/dist/handlers/validate-field-match.js.map +1 -0
- package/dist/handlers/validate-verbatim-quotes.d.ts +11 -0
- package/dist/handlers/validate-verbatim-quotes.d.ts.map +1 -0
- package/dist/handlers/validate-verbatim-quotes.js +37 -0
- package/dist/handlers/validate-verbatim-quotes.js.map +1 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/index.js.map +1 -0
- package/dist/pipeline/processing-pipeline.d.ts +24 -0
- package/dist/pipeline/processing-pipeline.d.ts.map +1 -0
- package/dist/pipeline/processing-pipeline.js +53 -0
- package/dist/pipeline/processing-pipeline.js.map +1 -0
- package/dist/processors/compute-hash.d.ts +4 -0
- package/dist/processors/compute-hash.d.ts.map +1 -0
- package/dist/processors/compute-hash.js +14 -0
- package/dist/processors/compute-hash.js.map +1 -0
- package/dist/processors/normalize-text.d.ts +8 -0
- package/dist/processors/normalize-text.d.ts.map +1 -0
- package/dist/processors/normalize-text.js +47 -0
- package/dist/processors/normalize-text.js.map +1 -0
- package/dist/store/json-file-store.d.ts +24 -0
- package/dist/store/json-file-store.d.ts.map +1 -0
- package/dist/store/json-file-store.js +210 -0
- package/dist/store/json-file-store.js.map +1 -0
- package/dist/store/store-interface.d.ts +33 -0
- package/dist/store/store-interface.d.ts.map +1 -0
- package/dist/store/store-interface.js +2 -0
- package/dist/store/store-interface.js.map +1 -0
- package/dist/store/trace-buffer-store.d.ts +55 -0
- package/dist/store/trace-buffer-store.d.ts.map +1 -0
- package/dist/store/trace-buffer-store.js +113 -0
- package/dist/store/trace-buffer-store.js.map +1 -0
- package/dist/types/mcp-types.d.ts +17 -0
- package/dist/types/mcp-types.d.ts.map +1 -0
- package/dist/types/mcp-types.js +5 -0
- package/dist/types/mcp-types.js.map +1 -0
- package/dist/types/response-envelope.d.ts +96 -0
- package/dist/types/response-envelope.d.ts.map +1 -0
- package/dist/types/response-envelope.js +2 -0
- package/dist/types/response-envelope.js.map +1 -0
- package/dist/types/run-record.d.ts +169 -0
- package/dist/types/run-record.d.ts.map +1 -0
- package/dist/types/run-record.js +2 -0
- package/dist/types/run-record.js.map +1 -0
- package/dist/types/workflow-definition.d.ts +292 -0
- package/dist/types/workflow-definition.d.ts.map +1 -0
- package/dist/types/workflow-definition.js +2 -0
- package/dist/types/workflow-definition.js.map +1 -0
- package/dist/types/workflow-error.d.ts +26 -0
- package/dist/types/workflow-error.d.ts.map +1 -0
- package/dist/types/workflow-error.js +28 -0
- package/dist/types/workflow-error.js.map +1 -0
- package/dist/utils/schema-skeleton.d.ts +11 -0
- package/dist/utils/schema-skeleton.d.ts.map +1 -0
- package/dist/utils/schema-skeleton.js +40 -0
- package/dist/utils/schema-skeleton.js.map +1 -0
- package/dist/validation/input-schema.d.ts +26 -0
- package/dist/validation/input-schema.d.ts.map +1 -0
- package/dist/validation/input-schema.js +67 -0
- package/dist/validation/input-schema.js.map +1 -0
- package/dist/workflow/registrar.d.ts +20 -0
- package/dist/workflow/registrar.d.ts.map +1 -0
- package/dist/workflow/registrar.js +61 -0
- package/dist/workflow/registrar.js.map +1 -0
- package/dist/workflow/template-resolver.d.ts +25 -0
- package/dist/workflow/template-resolver.d.ts.map +1 -0
- package/dist/workflow/template-resolver.js +112 -0
- package/dist/workflow/template-resolver.js.map +1 -0
- package/dist/workflow/yaml-loader.d.ts +15 -0
- package/dist/workflow/yaml-loader.d.ts.map +1 -0
- package/dist/workflow/yaml-loader.js +408 -0
- package/dist/workflow/yaml-loader.js.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# @sensigo/realm
|
|
2
|
+
|
|
3
|
+
`@sensigo/realm` — the Realm workflow execution engine. Use this package to load, register, and execute YAML-defined workflows programmatically, or to build custom service adapters and step handlers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npm install @sensigo/realm
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage — Execute a Workflow
|
|
12
|
+
|
|
13
|
+
Load a workflow definition and drive it step by step. Each `executeStep` call advances the run and returns a `ResponseEnvelope` with the current state and what comes next.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { loadWorkflowFromFile, JsonFileStore, executeStep } from '@sensigo/realm';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
|
|
20
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
|
|
22
|
+
const definition = loadWorkflowFromFile(path.join(__dirname, 'my-workflow/workflow.yaml'));
|
|
23
|
+
|
|
24
|
+
const store = new JsonFileStore(); // defaults to ~/.realm/runs/
|
|
25
|
+
const run = await store.create({
|
|
26
|
+
workflowId: definition.id,
|
|
27
|
+
workflowVersion: definition.version,
|
|
28
|
+
params: { input: 'hello' },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const response = await executeStep(store, definition, {
|
|
32
|
+
runId: run.id,
|
|
33
|
+
command: 'my_step',
|
|
34
|
+
input: { answer: 'done' },
|
|
35
|
+
dispatcher: async (_stepName, stepInput) => stepInput, // replace with real agent/handler
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
console.log(response.status);
|
|
39
|
+
// response.next_actions[0] carries the next step to execute — repeat until next_actions is empty
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Usage — Custom Service Adapter
|
|
43
|
+
|
|
44
|
+
Implement `ServiceAdapter` to connect a workflow step to any external API, then register it with `ExtensionRegistry` before executing.
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import {
|
|
48
|
+
ExtensionRegistry,
|
|
49
|
+
executeStep,
|
|
50
|
+
type ServiceAdapter,
|
|
51
|
+
type ServiceResponse,
|
|
52
|
+
} from '@sensigo/realm';
|
|
53
|
+
|
|
54
|
+
const myAdapter: ServiceAdapter = {
|
|
55
|
+
id: 'my-api',
|
|
56
|
+
async fetch(operation, params, _config): Promise<ServiceResponse> {
|
|
57
|
+
const data = await callMyApi(operation, params);
|
|
58
|
+
return { status: 200, data };
|
|
59
|
+
},
|
|
60
|
+
async create(operation, params, _config): Promise<ServiceResponse> {
|
|
61
|
+
return this.fetch(operation, params, _config);
|
|
62
|
+
},
|
|
63
|
+
async update(operation, params, _config): Promise<ServiceResponse> {
|
|
64
|
+
return this.fetch(operation, params, _config);
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const registry = new ExtensionRegistry();
|
|
69
|
+
registry.register('adapter', 'my-adapter', myAdapter);
|
|
70
|
+
|
|
71
|
+
// Pass registry to executeStep or executeChain:
|
|
72
|
+
// await executeStep(store, definition, { ..., registry });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## API Reference
|
|
76
|
+
|
|
77
|
+
### Engine
|
|
78
|
+
|
|
79
|
+
| Symbol | Notes |
|
|
80
|
+
| --------------------- | --------------------------------------------------------------- |
|
|
81
|
+
| `executeStep` | Advance a run by one step. Returns `ResponseEnvelope`. |
|
|
82
|
+
| `executeChain` | Auto-chain through auto steps until an agent step is reached. |
|
|
83
|
+
| `submitHumanResponse` | Resolve an open human gate. |
|
|
84
|
+
| `buildNextActions` | Build `NextAction[]` for all currently eligible agent steps. |
|
|
85
|
+
| `findEligibleSteps` | Return names of steps ready to execute given current run state. |
|
|
86
|
+
| `propagateSkips` | Propagate skip flags through dependent steps. |
|
|
87
|
+
|
|
88
|
+
### Store
|
|
89
|
+
|
|
90
|
+
| Symbol | Notes |
|
|
91
|
+
| ------------------- | ------------------------------------------------------ |
|
|
92
|
+
| `JsonFileStore` | File-backed `RunStore`. Defaults to `~/.realm/runs/`. |
|
|
93
|
+
| `JsonWorkflowStore` | File-backed store for registered workflow definitions. |
|
|
94
|
+
|
|
95
|
+
### Workflow
|
|
96
|
+
|
|
97
|
+
| Symbol | Notes |
|
|
98
|
+
| ------------------------ | ------------------------------------------------------ |
|
|
99
|
+
| `loadWorkflowFromFile` | Synchronous. Parses `workflow.yaml` at the given path. |
|
|
100
|
+
| `loadWorkflowFromString` | Parse a workflow from a raw YAML string. |
|
|
101
|
+
|
|
102
|
+
### Adapters
|
|
103
|
+
|
|
104
|
+
| Symbol |
|
|
105
|
+
| -------------------- |
|
|
106
|
+
| `FileSystemAdapter` |
|
|
107
|
+
| `GitHubAdapter` |
|
|
108
|
+
| `SlackAdapter` |
|
|
109
|
+
| `GenericHttpAdapter` |
|
|
110
|
+
| `MockAdapter` |
|
|
111
|
+
|
|
112
|
+
### Registry
|
|
113
|
+
|
|
114
|
+
| Symbol |
|
|
115
|
+
| ----------------------- |
|
|
116
|
+
| `ExtensionRegistry` |
|
|
117
|
+
| `createDefaultRegistry` |
|
|
118
|
+
|
|
119
|
+
### Types
|
|
120
|
+
|
|
121
|
+
| Symbol |
|
|
122
|
+
| -------------------- |
|
|
123
|
+
| `WorkflowDefinition` |
|
|
124
|
+
| `RunRecord` |
|
|
125
|
+
| `ResponseEnvelope` |
|
|
126
|
+
| `ServiceAdapter` |
|
|
127
|
+
| `ServiceResponse` |
|
|
128
|
+
| `RunStore` |
|
|
129
|
+
| `WorkflowError` |
|
|
130
|
+
|
|
131
|
+
### Processors
|
|
132
|
+
|
|
133
|
+
| Symbol |
|
|
134
|
+
| --------------- |
|
|
135
|
+
| `normalizeText` |
|
|
136
|
+
| `computeHash` |
|
|
137
|
+
|
|
138
|
+
### Handler primitives
|
|
139
|
+
|
|
140
|
+
| Symbol |
|
|
141
|
+
| ---------------------- |
|
|
142
|
+
| `resolveResource` |
|
|
143
|
+
| `walkField` |
|
|
144
|
+
| `partitionBySubstring` |
|
|
145
|
+
| `countResults` |
|
|
146
|
+
| `compareStrings` |
|
|
147
|
+
|
|
148
|
+
## Full documentation
|
|
149
|
+
|
|
150
|
+
Full documentation: https://github.com/sensigo-hq/realm
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses the Retry-After HTTP header value into a delay in seconds.
|
|
3
|
+
* Handles both integer-seconds form (e.g. "30") and HTTP-date form
|
|
4
|
+
* (e.g. "Sat, 31 May 2026 12:00:00 GMT").
|
|
5
|
+
*
|
|
6
|
+
* Known limitation: clock skew between client and server is not corrected.
|
|
7
|
+
* Math.max(0, ...) prevents negative values when the date is in the past.
|
|
8
|
+
*
|
|
9
|
+
* @param raw The raw header value from response.headers.get('Retry-After').
|
|
10
|
+
* @param fallback Conservative default to use when the header is absent or unparseable.
|
|
11
|
+
*/
|
|
12
|
+
export declare function parseRetryAfterHeader(raw: string | null, fallback: number): number;
|
|
13
|
+
export declare function parseRetryAfterHeader(raw: string | null, fallback?: undefined): number | undefined;
|
|
14
|
+
//# sourceMappingURL=adapter-utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter-utils.d.ts","sourceRoot":"","sources":["../../src/adapters/adapter-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAC;AACpF,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,QAAQ,CAAC,EAAE,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function parseRetryAfterHeader(raw, fallback) {
|
|
2
|
+
if (raw !== null) {
|
|
3
|
+
// Integer-seconds form (e.g. "30"). parseInt handles potential decimal inputs
|
|
4
|
+
// from non-conformant servers (e.g. "30.5" → 30).
|
|
5
|
+
const n = parseInt(raw, 10);
|
|
6
|
+
if (Number.isFinite(n))
|
|
7
|
+
return Math.max(0, n);
|
|
8
|
+
// HTTP-date form (e.g. "Sat, 31 May 2026 12:00:00 GMT").
|
|
9
|
+
// Known limitation: clock skew between client and server is not corrected.
|
|
10
|
+
// Math.max(0, ...) prevents negative values when the date is in the past.
|
|
11
|
+
const date = new Date(raw);
|
|
12
|
+
if (Number.isFinite(date.getTime())) {
|
|
13
|
+
return Math.max(0, Math.floor((date.getTime() - Date.now()) / 1000));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=adapter-utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter-utils.js","sourceRoot":"","sources":["../../src/adapters/adapter-utils.ts"],"names":[],"mappings":"AAaA,MAAM,UAAU,qBAAqB,CAAC,GAAkB,EAAE,QAAiB;IACzE,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;QACjB,8EAA8E;QAC9E,kDAAkD;QAClD,MAAM,CAAC,GAAG,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;QAC5B,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAC9C,yDAAyD;QACzD,2EAA2E;QAC3E,0EAA0E;QAC1E,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC;QAC3B,IAAI,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;YACpC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC;QACvE,CAAC;IACH,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ServiceAdapter, ServiceResponse } from '../extensions/service-adapter.js';
|
|
2
|
+
/**
|
|
3
|
+
* Configuration for AirtableAdapter.
|
|
4
|
+
*
|
|
5
|
+
* One adapter instance is scoped to a single Airtable base. For multi-base workflows,
|
|
6
|
+
* create one adapter instance per base.
|
|
7
|
+
*/
|
|
8
|
+
export interface AirtableAdapterConfig {
|
|
9
|
+
/**
|
|
10
|
+
* Personal Access Token (PAT). Required.
|
|
11
|
+
* Rate limit: 5 req/sec per base. On HTTP 429, wait 30 seconds before retrying.
|
|
12
|
+
* No Retry-After header is provided by Airtable.
|
|
13
|
+
*/
|
|
14
|
+
api_key: string;
|
|
15
|
+
/**
|
|
16
|
+
* Airtable base ID. Format: /^app[a-zA-Z0-9]{14}$/.
|
|
17
|
+
* One adapter instance per base. Multi-base = two instances.
|
|
18
|
+
*/
|
|
19
|
+
base_id: string;
|
|
20
|
+
/**
|
|
21
|
+
* Override the base URL for tests (replaces https://api.airtable.com).
|
|
22
|
+
* Trailing slash is stripped.
|
|
23
|
+
*/
|
|
24
|
+
base_url?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* AirtableAdapter wraps the Airtable REST API.
|
|
28
|
+
*
|
|
29
|
+
* Supported operations:
|
|
30
|
+
* fetch('get_record', { table, record_id }) — GET /v0/{base}/{table}/{id}
|
|
31
|
+
* fetch('list_records', { table, ...query }) — GET /v0/{base}/{table}
|
|
32
|
+
* create('create_record', { table, fields, ... }) — POST /v0/{base}/{table}
|
|
33
|
+
* update('upsert_record', { table, fields, ... }) — POST /v0/{base}/{table} (upsert)
|
|
34
|
+
*/
|
|
35
|
+
export declare class AirtableAdapter implements ServiceAdapter {
|
|
36
|
+
readonly id: string;
|
|
37
|
+
readonly defaultRetryAfterSeconds = 30;
|
|
38
|
+
private readonly baseUrl;
|
|
39
|
+
private readonly apiKey;
|
|
40
|
+
private readonly baseId;
|
|
41
|
+
constructor(id: string, config: AirtableAdapterConfig);
|
|
42
|
+
private checkAborted;
|
|
43
|
+
private buildHeaders;
|
|
44
|
+
private buildUrl;
|
|
45
|
+
private executeRequest;
|
|
46
|
+
private throwHttpError;
|
|
47
|
+
fetch(operation: string, params: Record<string, unknown>, _config: Record<string, unknown>, signal?: AbortSignal): Promise<ServiceResponse>;
|
|
48
|
+
create(operation: string, params: Record<string, unknown>, _config: Record<string, unknown>, signal?: AbortSignal): Promise<ServiceResponse>;
|
|
49
|
+
update(operation: string, params: Record<string, unknown>, _config: Record<string, unknown>, signal?: AbortSignal): Promise<ServiceResponse>;
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=airtable-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"airtable-adapter.d.ts","sourceRoot":"","sources":["../../src/adapters/airtable-adapter.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AAKxF;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AASD;;;;;;;;GAQG;AACH,qBAAa,eAAgB,YAAW,cAAc;IACpD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,wBAAwB,MAAM;IACvC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;gBAEpB,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,qBAAqB;IAerD,OAAO,CAAC,YAAY;IAWpB,OAAO,CAAC,YAAY;IAOpB,OAAO,CAAC,QAAQ;YAQF,cAAc;YAyCd,cAAc;IAqFtB,KAAK,CACT,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,eAAe,CAAC;IA4HrB,MAAM,CACV,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,eAAe,CAAC;IAwErB,MAAM,CACV,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,eAAe,CAAC;CAqG5B"}
|
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
// AirtableAdapter — wraps the Airtable REST API for record-level operations.
|
|
2
|
+
import { WorkflowError } from '../types/workflow-error.js';
|
|
3
|
+
import { parseRetryAfterHeader } from './adapter-utils.js';
|
|
4
|
+
const AIRTABLE_DEFAULT_BASE_URL = 'https://api.airtable.com';
|
|
5
|
+
/**
|
|
6
|
+
* AirtableAdapter wraps the Airtable REST API.
|
|
7
|
+
*
|
|
8
|
+
* Supported operations:
|
|
9
|
+
* fetch('get_record', { table, record_id }) — GET /v0/{base}/{table}/{id}
|
|
10
|
+
* fetch('list_records', { table, ...query }) — GET /v0/{base}/{table}
|
|
11
|
+
* create('create_record', { table, fields, ... }) — POST /v0/{base}/{table}
|
|
12
|
+
* update('upsert_record', { table, fields, ... }) — POST /v0/{base}/{table} (upsert)
|
|
13
|
+
*/
|
|
14
|
+
export class AirtableAdapter {
|
|
15
|
+
id;
|
|
16
|
+
defaultRetryAfterSeconds = 30;
|
|
17
|
+
baseUrl;
|
|
18
|
+
apiKey;
|
|
19
|
+
baseId;
|
|
20
|
+
constructor(id, config) {
|
|
21
|
+
if (!config.api_key) {
|
|
22
|
+
throw new Error('AirtableAdapter: api_key must not be empty');
|
|
23
|
+
}
|
|
24
|
+
if (!/^app[a-zA-Z0-9]{14}$/.test(config.base_id)) {
|
|
25
|
+
throw new Error('AirtableAdapter: base_id must be a valid Airtable base ID (format: appXXXXXXXXXXXXXX)');
|
|
26
|
+
}
|
|
27
|
+
this.id = id;
|
|
28
|
+
this.apiKey = config.api_key;
|
|
29
|
+
this.baseId = config.base_id;
|
|
30
|
+
this.baseUrl = config.base_url?.replace(/\/$/, '') ?? AIRTABLE_DEFAULT_BASE_URL;
|
|
31
|
+
}
|
|
32
|
+
checkAborted(signal) {
|
|
33
|
+
if (signal?.aborted === true) {
|
|
34
|
+
throw new WorkflowError('Adapter request aborted', {
|
|
35
|
+
code: 'STEP_ABORTED',
|
|
36
|
+
category: 'ENGINE',
|
|
37
|
+
agentAction: 'report_to_user',
|
|
38
|
+
retryable: false,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
buildHeaders() {
|
|
43
|
+
return {
|
|
44
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
45
|
+
Accept: 'application/json',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
buildUrl(table, recordId) {
|
|
49
|
+
let path = `/v0/${this.baseId}/${encodeURIComponent(table)}`;
|
|
50
|
+
if (recordId !== undefined) {
|
|
51
|
+
path += `/${encodeURIComponent(recordId)}`;
|
|
52
|
+
}
|
|
53
|
+
return new URL(path, this.baseUrl);
|
|
54
|
+
}
|
|
55
|
+
async executeRequest(url, method, body, signal) {
|
|
56
|
+
this.checkAborted(signal);
|
|
57
|
+
const hasBody = method !== 'GET' && body !== undefined;
|
|
58
|
+
const headers = {
|
|
59
|
+
...this.buildHeaders(),
|
|
60
|
+
...(hasBody ? { 'Content-Type': 'application/json' } : {}),
|
|
61
|
+
};
|
|
62
|
+
let response;
|
|
63
|
+
try {
|
|
64
|
+
response = await fetch(url.href, {
|
|
65
|
+
method,
|
|
66
|
+
headers,
|
|
67
|
+
...(hasBody ? { body: JSON.stringify(body) } : {}),
|
|
68
|
+
signal: signal ?? null,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
73
|
+
throw new WorkflowError('Adapter request aborted', {
|
|
74
|
+
code: 'STEP_ABORTED',
|
|
75
|
+
category: 'ENGINE',
|
|
76
|
+
agentAction: 'report_to_user',
|
|
77
|
+
retryable: false,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
81
|
+
throw new WorkflowError(message, {
|
|
82
|
+
code: 'NETWORK_UNREACHABLE',
|
|
83
|
+
category: 'NETWORK',
|
|
84
|
+
agentAction: 'wait_for_human',
|
|
85
|
+
retryable: true,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return response;
|
|
89
|
+
}
|
|
90
|
+
async throwHttpError(response, operation) {
|
|
91
|
+
// Read body as text first — avoids "body already consumed" if JSON.parse fails.
|
|
92
|
+
const rawText = await response.text();
|
|
93
|
+
let body;
|
|
94
|
+
try {
|
|
95
|
+
body = JSON.parse(rawText);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
body = rawText;
|
|
99
|
+
}
|
|
100
|
+
const status = response.status;
|
|
101
|
+
const baseDetails = { status, operation };
|
|
102
|
+
if (status === 401) {
|
|
103
|
+
throw new WorkflowError('Airtable authentication failed — check API key', {
|
|
104
|
+
code: 'SERVICE_AUTH_FAILED',
|
|
105
|
+
category: 'SERVICE',
|
|
106
|
+
agentAction: 'stop',
|
|
107
|
+
retryable: false,
|
|
108
|
+
details: baseDetails,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (status === 403) {
|
|
112
|
+
throw new WorkflowError('Airtable: forbidden (HTTP 403)', {
|
|
113
|
+
code: 'SERVICE_HTTP_4XX',
|
|
114
|
+
category: 'SERVICE',
|
|
115
|
+
agentAction: 'stop',
|
|
116
|
+
retryable: false,
|
|
117
|
+
details: baseDetails,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (status === 404) {
|
|
121
|
+
throw new WorkflowError('Airtable: record not found', {
|
|
122
|
+
code: 'SERVICE_NOT_FOUND',
|
|
123
|
+
category: 'SERVICE',
|
|
124
|
+
agentAction: 'provide_input',
|
|
125
|
+
retryable: false,
|
|
126
|
+
details: baseDetails,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (status === 422) {
|
|
130
|
+
throw new WorkflowError('Airtable: unprocessable entity (HTTP 422)', {
|
|
131
|
+
code: 'SERVICE_HTTP_4XX',
|
|
132
|
+
category: 'SERVICE',
|
|
133
|
+
agentAction: 'stop',
|
|
134
|
+
retryable: false,
|
|
135
|
+
details: { ...baseDetails, body },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
if (status === 429) {
|
|
139
|
+
const retryAfterFromHeader = parseRetryAfterHeader(response.headers.get('Retry-After'));
|
|
140
|
+
throw new WorkflowError('Rate limited by Airtable API', {
|
|
141
|
+
code: 'SERVICE_RATE_LIMITED',
|
|
142
|
+
category: 'SERVICE',
|
|
143
|
+
agentAction: 'wait_and_proceed',
|
|
144
|
+
retryable: true,
|
|
145
|
+
...(retryAfterFromHeader !== undefined ? { retry_after: retryAfterFromHeader } : {}),
|
|
146
|
+
details: baseDetails,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
if (status >= 500) {
|
|
150
|
+
throw new WorkflowError(`Airtable server error (HTTP ${status})`, {
|
|
151
|
+
code: 'SERVICE_HTTP_5XX',
|
|
152
|
+
category: 'SERVICE',
|
|
153
|
+
agentAction: 'report_to_user',
|
|
154
|
+
retryable: true,
|
|
155
|
+
details: baseDetails,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// 400 and other 4xx
|
|
159
|
+
throw new WorkflowError(`HTTP ${status}: ${response.statusText}`, {
|
|
160
|
+
code: 'SERVICE_HTTP_4XX',
|
|
161
|
+
category: 'SERVICE',
|
|
162
|
+
agentAction: 'stop',
|
|
163
|
+
retryable: false,
|
|
164
|
+
details: baseDetails,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
async fetch(operation, params, _config, signal) {
|
|
168
|
+
if (operation === 'get_record') {
|
|
169
|
+
const table = params['table'];
|
|
170
|
+
const recordId = params['record_id'];
|
|
171
|
+
if (typeof table !== 'string' || table === '') {
|
|
172
|
+
throw new WorkflowError('AirtableAdapter: table param must be a non-empty string', {
|
|
173
|
+
code: 'ADAPTER_VALIDATION_FAILED',
|
|
174
|
+
category: 'ENGINE',
|
|
175
|
+
agentAction: 'provide_input',
|
|
176
|
+
retryable: false,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (typeof recordId !== 'string' || recordId === '') {
|
|
180
|
+
throw new WorkflowError('AirtableAdapter: record_id param must be a non-empty string', {
|
|
181
|
+
code: 'ADAPTER_VALIDATION_FAILED',
|
|
182
|
+
category: 'ENGINE',
|
|
183
|
+
agentAction: 'provide_input',
|
|
184
|
+
retryable: false,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const url = this.buildUrl(table, recordId);
|
|
188
|
+
const response = await this.executeRequest(url, 'GET', undefined, signal);
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
await this.throwHttpError(response, 'get_record');
|
|
191
|
+
}
|
|
192
|
+
let parsed;
|
|
193
|
+
try {
|
|
194
|
+
parsed = await response.json();
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
throw new WorkflowError('AirtableAdapter: failed to parse response body', {
|
|
198
|
+
code: 'SERVICE_RESPONSE_INVALID',
|
|
199
|
+
category: 'SERVICE',
|
|
200
|
+
agentAction: 'report_to_user',
|
|
201
|
+
retryable: false,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const data = parsed;
|
|
205
|
+
if (typeof data.id !== 'string' || data.id === '') {
|
|
206
|
+
throw new WorkflowError('AirtableAdapter: get_record response missing id field', {
|
|
207
|
+
code: 'SERVICE_RESPONSE_INVALID',
|
|
208
|
+
category: 'SERVICE',
|
|
209
|
+
agentAction: 'report_to_user',
|
|
210
|
+
retryable: false,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return { status: response.status, data: parsed };
|
|
214
|
+
}
|
|
215
|
+
if (operation === 'list_records') {
|
|
216
|
+
const table = params['table'];
|
|
217
|
+
if (typeof table !== 'string' || table === '') {
|
|
218
|
+
throw new WorkflowError('AirtableAdapter: table param must be a non-empty string', {
|
|
219
|
+
code: 'ADAPTER_VALIDATION_FAILED',
|
|
220
|
+
category: 'ENGINE',
|
|
221
|
+
agentAction: 'provide_input',
|
|
222
|
+
retryable: false,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const url = this.buildUrl(table);
|
|
226
|
+
if (typeof params['filter_by_formula'] === 'string') {
|
|
227
|
+
url.searchParams.set('filterByFormula', params['filter_by_formula']);
|
|
228
|
+
}
|
|
229
|
+
if (typeof params['view'] === 'string') {
|
|
230
|
+
url.searchParams.set('view', params['view']);
|
|
231
|
+
}
|
|
232
|
+
if (typeof params['max_records'] === 'number') {
|
|
233
|
+
url.searchParams.set('maxRecords', String(params['max_records']));
|
|
234
|
+
}
|
|
235
|
+
if (Array.isArray(params['fields'])) {
|
|
236
|
+
for (const f of params['fields']) {
|
|
237
|
+
url.searchParams.append('fields[]', String(f));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
if (typeof params['offset'] === 'string') {
|
|
241
|
+
url.searchParams.set('offset', params['offset']);
|
|
242
|
+
}
|
|
243
|
+
const response = await this.executeRequest(url, 'GET', undefined, signal);
|
|
244
|
+
if (!response.ok) {
|
|
245
|
+
await this.throwHttpError(response, 'list_records');
|
|
246
|
+
}
|
|
247
|
+
let parsed;
|
|
248
|
+
try {
|
|
249
|
+
parsed = await response.json();
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
throw new WorkflowError('AirtableAdapter: failed to parse response body', {
|
|
253
|
+
code: 'SERVICE_RESPONSE_INVALID',
|
|
254
|
+
category: 'SERVICE',
|
|
255
|
+
agentAction: 'report_to_user',
|
|
256
|
+
retryable: false,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
const data = parsed;
|
|
260
|
+
if (!Array.isArray(data.records)) {
|
|
261
|
+
throw new WorkflowError('AirtableAdapter: list_records response missing records array', {
|
|
262
|
+
code: 'SERVICE_RESPONSE_INVALID',
|
|
263
|
+
category: 'SERVICE',
|
|
264
|
+
agentAction: 'report_to_user',
|
|
265
|
+
retryable: false,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return { status: response.status, data: parsed };
|
|
269
|
+
}
|
|
270
|
+
throw new WorkflowError(`AirtableAdapter: unsupported fetch operation "${operation}"`, {
|
|
271
|
+
code: 'ADAPTER_OP_UNSUPPORTED',
|
|
272
|
+
category: 'ENGINE',
|
|
273
|
+
agentAction: 'report_to_user',
|
|
274
|
+
retryable: false,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async create(operation, params, _config, signal) {
|
|
278
|
+
if (operation === 'create_record') {
|
|
279
|
+
const table = params['table'];
|
|
280
|
+
const fields = params['fields'];
|
|
281
|
+
if (typeof table !== 'string' || table === '') {
|
|
282
|
+
throw new WorkflowError('AirtableAdapter: table param must be a non-empty string', {
|
|
283
|
+
code: 'ADAPTER_VALIDATION_FAILED',
|
|
284
|
+
category: 'ENGINE',
|
|
285
|
+
agentAction: 'provide_input',
|
|
286
|
+
retryable: false,
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
if (fields === undefined ||
|
|
290
|
+
fields === null ||
|
|
291
|
+
typeof fields !== 'object' ||
|
|
292
|
+
Array.isArray(fields)) {
|
|
293
|
+
throw new WorkflowError('AirtableAdapter: fields param must be a plain object', {
|
|
294
|
+
code: 'ADAPTER_VALIDATION_FAILED',
|
|
295
|
+
category: 'ENGINE',
|
|
296
|
+
agentAction: 'provide_input',
|
|
297
|
+
retryable: false,
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
const body = { fields };
|
|
301
|
+
if (params['typecast'] === true) {
|
|
302
|
+
body['typecast'] = true;
|
|
303
|
+
}
|
|
304
|
+
const url = this.buildUrl(table);
|
|
305
|
+
const response = await this.executeRequest(url, 'POST', body, signal);
|
|
306
|
+
if (!response.ok) {
|
|
307
|
+
await this.throwHttpError(response, 'create_record');
|
|
308
|
+
}
|
|
309
|
+
let parsed;
|
|
310
|
+
try {
|
|
311
|
+
parsed = await response.json();
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
throw new WorkflowError('AirtableAdapter: failed to parse response body', {
|
|
315
|
+
code: 'SERVICE_RESPONSE_INVALID',
|
|
316
|
+
category: 'SERVICE',
|
|
317
|
+
agentAction: 'report_to_user',
|
|
318
|
+
retryable: false,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
const data = parsed;
|
|
322
|
+
if (typeof data.id !== 'string' || data.id === '') {
|
|
323
|
+
throw new WorkflowError('AirtableAdapter: create_record response missing id field', {
|
|
324
|
+
code: 'SERVICE_RESPONSE_INVALID',
|
|
325
|
+
category: 'SERVICE',
|
|
326
|
+
agentAction: 'report_to_user',
|
|
327
|
+
retryable: false,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return { status: response.status, data: parsed };
|
|
331
|
+
}
|
|
332
|
+
throw new WorkflowError(`AirtableAdapter: unsupported create operation "${operation}"`, {
|
|
333
|
+
code: 'ADAPTER_OP_UNSUPPORTED',
|
|
334
|
+
category: 'ENGINE',
|
|
335
|
+
agentAction: 'report_to_user',
|
|
336
|
+
retryable: false,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
async update(operation, params, _config, signal) {
|
|
340
|
+
if (operation === 'upsert_record') {
|
|
341
|
+
const table = params['table'];
|
|
342
|
+
const fields = params['fields'];
|
|
343
|
+
const fieldsToMergeOn = params['fields_to_merge_on'];
|
|
344
|
+
if (typeof table !== 'string' || table === '') {
|
|
345
|
+
throw new WorkflowError('AirtableAdapter: table param must be a non-empty string', {
|
|
346
|
+
code: 'ADAPTER_VALIDATION_FAILED',
|
|
347
|
+
category: 'ENGINE',
|
|
348
|
+
agentAction: 'provide_input',
|
|
349
|
+
retryable: false,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
if (fields === undefined ||
|
|
353
|
+
fields === null ||
|
|
354
|
+
typeof fields !== 'object' ||
|
|
355
|
+
Array.isArray(fields)) {
|
|
356
|
+
throw new WorkflowError('AirtableAdapter: fields param must be a plain object', {
|
|
357
|
+
code: 'ADAPTER_VALIDATION_FAILED',
|
|
358
|
+
category: 'ENGINE',
|
|
359
|
+
agentAction: 'provide_input',
|
|
360
|
+
retryable: false,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
// Validate fields_to_merge_on: must be a non-empty array of non-empty strings
|
|
364
|
+
if (!Array.isArray(fieldsToMergeOn) || fieldsToMergeOn.length === 0) {
|
|
365
|
+
throw new WorkflowError('AirtableAdapter: fields_to_merge_on must be a non-empty array of non-empty strings', {
|
|
366
|
+
code: 'ADAPTER_VALIDATION_FAILED',
|
|
367
|
+
category: 'ENGINE',
|
|
368
|
+
agentAction: 'provide_input',
|
|
369
|
+
retryable: false,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
for (const element of fieldsToMergeOn) {
|
|
373
|
+
if (typeof element !== 'string' || element === '') {
|
|
374
|
+
throw new WorkflowError('AirtableAdapter: fields_to_merge_on must be a non-empty array of non-empty strings', {
|
|
375
|
+
code: 'ADAPTER_VALIDATION_FAILED',
|
|
376
|
+
category: 'ENGINE',
|
|
377
|
+
agentAction: 'provide_input',
|
|
378
|
+
retryable: false,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const body = {
|
|
383
|
+
records: [{ fields }],
|
|
384
|
+
performUpsert: { fieldsToMergeOn },
|
|
385
|
+
};
|
|
386
|
+
if (params['typecast'] === true) {
|
|
387
|
+
body['typecast'] = true;
|
|
388
|
+
}
|
|
389
|
+
const url = this.buildUrl(table);
|
|
390
|
+
const response = await this.executeRequest(url, 'POST', body, signal);
|
|
391
|
+
if (!response.ok) {
|
|
392
|
+
await this.throwHttpError(response, 'upsert_record');
|
|
393
|
+
}
|
|
394
|
+
let parsed;
|
|
395
|
+
try {
|
|
396
|
+
parsed = await response.json();
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
throw new WorkflowError('AirtableAdapter: failed to parse response body', {
|
|
400
|
+
code: 'SERVICE_RESPONSE_INVALID',
|
|
401
|
+
category: 'SERVICE',
|
|
402
|
+
agentAction: 'report_to_user',
|
|
403
|
+
retryable: false,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
const data = parsed;
|
|
407
|
+
if (!Array.isArray(data.records)) {
|
|
408
|
+
throw new WorkflowError('AirtableAdapter: upsert_record response missing records array', {
|
|
409
|
+
code: 'SERVICE_RESPONSE_INVALID',
|
|
410
|
+
category: 'SERVICE',
|
|
411
|
+
agentAction: 'report_to_user',
|
|
412
|
+
retryable: false,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
return { status: response.status, data: parsed };
|
|
416
|
+
}
|
|
417
|
+
throw new WorkflowError(`AirtableAdapter: unsupported update operation "${operation}"`, {
|
|
418
|
+
code: 'ADAPTER_OP_UNSUPPORTED',
|
|
419
|
+
category: 'ENGINE',
|
|
420
|
+
agentAction: 'report_to_user',
|
|
421
|
+
retryable: false,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
//# sourceMappingURL=airtable-adapter.js.map
|