@useairfoil/connector-kit 0.5.3 → 0.6.1
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/METRICS.md +20 -19
- package/README.md +127 -430
- package/dist/annotations-BE74YmAO.d.mts +39 -0
- package/dist/auth/index.d.mts +2 -0
- package/dist/{auth-DvU_CZ7C.js → auth/index.mjs} +6 -8
- package/dist/auth/index.mjs.map +1 -0
- package/dist/connector-app/index.d.mts +2 -0
- package/dist/connector-app/index.mjs +2 -0
- package/dist/connector-app-C-L4QqG6.mjs +123 -0
- package/dist/connector-app-C-L4QqG6.mjs.map +1 -0
- package/dist/{constants-BRhIX_rz.js → constants-D-GQzpHZ.mjs} +10 -6
- package/dist/constants-D-GQzpHZ.mjs.map +1 -0
- package/dist/demo/index.d.mts +2 -0
- package/dist/demo/index.mjs +2 -0
- package/dist/demo-B30Vlkk2.mjs +191 -0
- package/dist/demo-B30Vlkk2.mjs.map +1 -0
- package/dist/errors/index.d.mts +2 -0
- package/dist/errors/index.mjs +2 -0
- package/dist/{errors-DHWylqRZ.js → errors-pT0YVJU5.mjs} +2 -3
- package/dist/errors-pT0YVJU5.mjs.map +1 -0
- package/dist/iceberg/index.d.mts +3 -0
- package/dist/iceberg/index.mjs +3 -0
- package/dist/iceberg-D6L8I6vA.mjs +39 -0
- package/dist/iceberg-D6L8I6vA.mjs.map +1 -0
- package/dist/{index-Cq2IzY7T.d.ts → index-BUHSGm9Q.d.mts} +11 -13
- package/dist/{index-CTHVudvQ.d.ts → index-BX8G7d41.d.mts} +1 -2
- package/dist/{index-J42AZaXU.d.ts → index-BfXpgxk9.d.mts} +9 -10
- package/dist/{index-DKWIh6M1.d.ts → index-CRVAwJ5-.d.mts} +10 -8
- package/dist/index-CUPP2A_m.d.mts +27 -0
- package/dist/{index-Coh_SkBs.d.ts → index-CgvFomO6.d.mts} +6 -7
- package/dist/index-DJhDp4Xc.d.mts +58 -0
- package/dist/index-Dk7urvpF.d.mts +76 -0
- package/dist/{index-D54ySAKn.d.ts → index-DkVNO6ks.d.mts} +2 -5
- package/dist/{index-DtJX_tic.d.ts → index-NnCutIqM.d.mts} +77 -38
- package/dist/{index-CBT-JX-T.d.ts → index-eFuoMZZ3.d.mts} +18 -20
- package/dist/index-ygpml2kC.d.mts +73 -0
- package/dist/index.d.mts +16 -0
- package/dist/index.mjs +45 -0
- package/dist/index.mjs.map +1 -0
- package/dist/ingestion/index.d.mts +2 -0
- package/dist/ingestion/index.mjs +2 -0
- package/dist/ingestion-q0WwqUSt.mjs +275 -0
- package/dist/ingestion-q0WwqUSt.mjs.map +1 -0
- package/dist/ingestor/index.d.mts +3 -0
- package/dist/ingestor/index.mjs +4 -0
- package/dist/ingestor-D4L-HPkg.mjs +26 -0
- package/dist/ingestor-D4L-HPkg.mjs.map +1 -0
- package/dist/instrumented-Bhp7fyvh.mjs +29 -0
- package/dist/instrumented-Bhp7fyvh.mjs.map +1 -0
- package/dist/manifest/index.d.mts +2 -0
- package/dist/manifest/{index.js → index.mjs} +2 -4
- package/dist/{manifest-ByE6YPG0.js → manifest-BGKxmKLy.mjs} +14 -19
- package/dist/manifest-BGKxmKLy.mjs.map +1 -0
- package/dist/metrics-BaEX__MN.mjs +352 -0
- package/dist/metrics-BaEX__MN.mjs.map +1 -0
- package/dist/metrics-CVXFL7YT.d.mts +64 -0
- package/dist/metrics.d.mts +2 -0
- package/dist/metrics.mjs +2 -0
- package/dist/{chunk-Bp6m_JJh.js → rolldown-runtime-D7D4PA-g.mjs} +4 -4
- package/dist/runtime-config/index.d.mts +2 -0
- package/dist/runtime-config/index.mjs +3 -0
- package/dist/runtime-config-Oa55gAew.mjs +97 -0
- package/dist/runtime-config-Oa55gAew.mjs.map +1 -0
- package/dist/{server-CXTQ7odl.js → server-CMnxz3h0.mjs} +21 -60
- package/dist/server-CMnxz3h0.mjs.map +1 -0
- package/dist/service-DLkhuxYl.d.mts +27 -0
- package/dist/service-jWxxfEht.mjs +7 -0
- package/dist/service-jWxxfEht.mjs.map +1 -0
- package/dist/state-store/index.d.mts +2 -0
- package/dist/state-store/index.mjs +2 -0
- package/dist/{state-store-yl6LiTQ0.js → state-store-uFvHY7Ax.mjs} +15 -22
- package/dist/state-store-uFvHY7Ax.mjs.map +1 -0
- package/dist/status/{index.d.ts → index.d.mts} +1 -4
- package/dist/status/{index.js → index.mjs} +2 -6
- package/dist/{status-B-oZBc33.js → status-CKb9IhQ9.mjs} +14 -16
- package/dist/status-CKb9IhQ9.mjs.map +1 -0
- package/dist/table-V0tgUC5i.mjs +370 -0
- package/dist/table-V0tgUC5i.mjs.map +1 -0
- package/dist/types-xZtSmrGA.d.mts +220 -0
- package/dist/webhook/index.d.mts +3 -0
- package/dist/webhook/index.mjs +12 -0
- package/dist/webhook/index.mjs.map +1 -0
- package/dist/wings-I56hGtNv.mjs +325 -0
- package/dist/wings-I56hGtNv.mjs.map +1 -0
- package/package.json +64 -51
- package/src/catalog/table.ts +201 -0
- package/src/connector-app/create-table.ts +47 -0
- package/src/connector-app/demo.ts +42 -0
- package/src/connector-app/index.ts +2 -0
- package/src/connector-app/start.ts +1 -1
- package/src/core/builder.ts +15 -23
- package/src/core/index.ts +11 -1
- package/src/core/types.ts +114 -39
- package/src/demo/definition.ts +68 -0
- package/src/demo/generate.ts +167 -0
- package/src/demo/index.ts +3 -0
- package/src/demo/run.ts +142 -0
- package/src/iceberg/annotations.ts +62 -0
- package/src/iceberg/error.ts +9 -0
- package/src/iceberg/index.ts +3 -0
- package/src/iceberg/table.ts +511 -0
- package/src/index.ts +3 -1
- package/src/ingestion/engine.ts +240 -60
- package/src/ingestor/arrow.ts +248 -0
- package/src/ingestor/console.ts +13 -0
- package/src/ingestor/index.ts +9 -0
- package/src/ingestor/instrumented.ts +61 -0
- package/src/ingestor/service.ts +30 -0
- package/src/ingestor/wings.ts +143 -0
- package/src/manifest/config.ts +3 -3
- package/src/metrics.ts +125 -33
- package/src/runtime-config/config.ts +14 -0
- package/src/runtime-config/constants.ts +5 -2
- package/src/runtime-config/error.ts +4 -1
- package/src/runtime-config/index.ts +2 -0
- package/src/runtime-config/layer.ts +1 -1
- package/src/runtime-config/table-bindings.ts +92 -0
- package/src/state-store/layer-key-value-store.ts +8 -3
- package/src/state-store/layer-memory.ts +1 -1
- package/src/state-store/layer-sql.ts +4 -4
- package/src/state-store/schema.ts +2 -2
- package/src/state-store/state.ts +14 -2
- package/src/status/route.ts +2 -2
- package/src/telemetry.ts +25 -10
- package/src/webhook/server.ts +39 -40
- package/dist/auth/index.d.ts +0 -2
- package/dist/auth/index.js +0 -3
- package/dist/auth-DvU_CZ7C.js.map +0 -1
- package/dist/connector-app/index.d.ts +0 -14
- package/dist/connector-app/index.js +0 -11
- package/dist/connector-app-d7Z-SZLQ.js +0 -64
- package/dist/connector-app-d7Z-SZLQ.js.map +0 -1
- package/dist/constants-BRhIX_rz.js.map +0 -1
- package/dist/errors/index.d.ts +0 -2
- package/dist/errors/index.js +0 -3
- package/dist/errors-DHWylqRZ.js.map +0 -1
- package/dist/index-Byr0Lf0C.d.ts +0 -25
- package/dist/index-o1-Z3n7V.d.ts +0 -55
- package/dist/index.d.ts +0 -14
- package/dist/index.js +0 -58
- package/dist/index.js.map +0 -1
- package/dist/ingestion/index.d.ts +0 -6
- package/dist/ingestion/index.js +0 -10
- package/dist/ingestion-BQo4cbGw.js +0 -200
- package/dist/ingestion-BQo4cbGw.js.map +0 -1
- package/dist/manifest/index.d.ts +0 -2
- package/dist/manifest-ByE6YPG0.js.map +0 -1
- package/dist/metrics-CVAk1ALR.d.ts +0 -53
- package/dist/metrics-CWPy2Aqc.js +0 -96
- package/dist/metrics-CWPy2Aqc.js.map +0 -1
- package/dist/metrics.d.ts +0 -4
- package/dist/metrics.js +0 -3
- package/dist/publisher/index.d.ts +0 -5
- package/dist/publisher/index.js +0 -6
- package/dist/publisher-BYT3RLVO.js +0 -131
- package/dist/publisher-BYT3RLVO.js.map +0 -1
- package/dist/runtime-config/index.d.ts +0 -2
- package/dist/runtime-config/index.js +0 -4
- package/dist/runtime-config-BpB-d_bF.js +0 -51
- package/dist/runtime-config-BpB-d_bF.js.map +0 -1
- package/dist/server-CXTQ7odl.js.map +0 -1
- package/dist/service-DuFiQwid.js +0 -191
- package/dist/service-DuFiQwid.js.map +0 -1
- package/dist/service-IE5qaf9x.d.ts +0 -32
- package/dist/state-store/index.d.ts +0 -3
- package/dist/state-store/index.js +0 -5
- package/dist/state-store-yl6LiTQ0.js.map +0 -1
- package/dist/status-B-oZBc33.js.map +0 -1
- package/dist/types-B35J5YPu.d.ts +0 -174
- package/dist/webhook/index.d.ts +0 -6
- package/dist/webhook/index.js +0 -8
- package/dist/webhook-DI-Afxkg.js +0 -13
- package/dist/webhook-DI-Afxkg.js.map +0 -1
- package/src/publisher/console.ts +0 -18
- package/src/publisher/index.ts +0 -7
- package/src/publisher/instrumented.ts +0 -68
- package/src/publisher/service.ts +0 -37
- package/src/publisher/wings.ts +0 -204
package/README.md
CHANGED
|
@@ -1,485 +1,182 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Connector Kit
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
`@useairfoil/connector-kit` gives connector authors the pieces needed to define, run, test, and publish resource-first producer connectors:
|
|
6
|
-
|
|
7
|
-
- `Connector.define(...)` describes a connector.
|
|
8
|
-
- `Resource.entity(...)` describes a Wings table-backed resource.
|
|
9
|
-
- `Fetch.page(...)` and `Fetch.changes(...)` describe backfill and polling fetches.
|
|
10
|
-
- `Resource.webhook(...)` lets a resource normalize provider webhook payloads into mutations.
|
|
11
|
-
- `Webhook.route(...)` defines connector-level HTTP webhook routes.
|
|
12
|
-
- `Ingestion.run(...)` runs resource ingestion and checkpoints after accepted publishes.
|
|
13
|
-
- `Publisher.layerConsole` logs accepted batches locally.
|
|
14
|
-
- `Publisher.layerWings(...)` publishes resource mutations into Wings tables.
|
|
15
|
-
- `ConnectorApp.check(...)` validates selected resources before provisioning.
|
|
16
|
-
- `ConnectorApp.start(...)` starts a complete Node HTTP connector process.
|
|
17
|
-
- `Auth.makeTokenCache(...)` caches short-lived provider access tokens in memory.
|
|
18
|
-
|
|
19
|
-
The package is intentionally Layer-oriented: connector code defines resources and routes, while entrypoints provide runtime dependencies such as publishers, state stores, telemetry, and API clients.
|
|
3
|
+
Use this package to build an Airfoil producer connector.
|
|
20
4
|
|
|
21
5
|
## Install
|
|
22
6
|
|
|
23
7
|
```bash
|
|
24
|
-
pnpm add @useairfoil/connector-kit effect@^4
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
This repo currently uses the Effect v4 release candidate. In workspace packages, prefer the workspace catalog (`"effect": "catalog:"`) so all packages stay on the supported RC version.
|
|
28
|
-
|
|
29
|
-
## Package Shape
|
|
30
|
-
|
|
31
|
-
The package root exposes the common connector authoring API:
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
import {
|
|
35
|
-
Auth,
|
|
36
|
-
Connector,
|
|
37
|
-
ConnectorApp,
|
|
38
|
-
ConnectorError,
|
|
39
|
-
Cursor,
|
|
40
|
-
Fetch,
|
|
41
|
-
Ingestion,
|
|
42
|
-
Manifest,
|
|
43
|
-
Metrics,
|
|
44
|
-
Publisher,
|
|
45
|
-
Resource,
|
|
46
|
-
RuntimeConfig,
|
|
47
|
-
StateStore,
|
|
48
|
-
Status,
|
|
49
|
-
Telemetry,
|
|
50
|
-
Webhook,
|
|
51
|
-
} from "@useairfoil/connector-kit";
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Subpath exports are available for runtime domains:
|
|
55
|
-
|
|
56
|
-
- `@useairfoil/connector-kit/auth`
|
|
57
|
-
- `@useairfoil/connector-kit/ingestion`
|
|
58
|
-
- `@useairfoil/connector-kit/connector-app`
|
|
59
|
-
- `@useairfoil/connector-kit/manifest`
|
|
60
|
-
- `@useairfoil/connector-kit/metrics`
|
|
61
|
-
- `@useairfoil/connector-kit/publisher`
|
|
62
|
-
- `@useairfoil/connector-kit/runtime-config`
|
|
63
|
-
- `@useairfoil/connector-kit/state-store`
|
|
64
|
-
- `@useairfoil/connector-kit/status`
|
|
65
|
-
- `@useairfoil/connector-kit/webhook`
|
|
66
|
-
- `@useairfoil/connector-kit/errors`
|
|
67
|
-
|
|
68
|
-
Core definition helpers and `Telemetry` are intentionally root-only.
|
|
69
|
-
|
|
70
|
-
## Provider Credentials
|
|
71
|
-
|
|
72
|
-
`Auth.makeTokenCache(...)` wraps a provider token exchange with Effect's cache. Acquisition stays lazy, concurrent callers share one exchange, and failures are not cached. The default refresh point is halfway through the provider lifetime.
|
|
73
|
-
|
|
74
|
-
```ts
|
|
75
|
-
import { Auth } from "@useairfoil/connector-kit";
|
|
76
|
-
import { Duration, Effect, Redacted } from "effect";
|
|
77
|
-
|
|
78
|
-
const tokenCache =
|
|
79
|
-
yield *
|
|
80
|
-
Auth.makeTokenCache({
|
|
81
|
-
acquire: exchangeCredentials.pipe(
|
|
82
|
-
Effect.map(({ accessToken, expiresInSeconds }) => ({
|
|
83
|
-
value: Redacted.make(accessToken),
|
|
84
|
-
expiresIn: Duration.seconds(expiresInSeconds),
|
|
85
|
-
})),
|
|
86
|
-
),
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
const accessToken = yield * tokenCache.get;
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
The cache is process-local and does not use `StateStore`. It does not add retries or background refresh. The first request after the refresh point performs the next exchange.
|
|
93
|
-
|
|
94
|
-
## Manifests
|
|
95
|
-
|
|
96
|
-
Connector manifests are pure metadata for UI, Wings deployment, and validation. Put user-facing connector config in `src/manifest.ts` and keep platform-owned runtime config, such as ports, Wings host, table names, and OTEL flags, as normal Effect `Config` in entrypoints.
|
|
97
|
-
|
|
98
|
-
```ts
|
|
99
|
-
import * as Manifest from "@useairfoil/connector-kit/manifest";
|
|
100
|
-
|
|
101
|
-
export const ExampleConfigDef = Manifest.defineConfig({
|
|
102
|
-
apiBaseUrl: Manifest.string({
|
|
103
|
-
runtimeKey: "EXAMPLE_API_BASE_URL",
|
|
104
|
-
required: false,
|
|
105
|
-
default: "https://api.example.test",
|
|
106
|
-
}),
|
|
107
|
-
apiToken: Manifest.secret({ runtimeKey: "EXAMPLE_API_TOKEN" }),
|
|
108
|
-
maxRetries: Manifest.number({
|
|
109
|
-
runtimeKey: "EXAMPLE_MAX_RETRIES",
|
|
110
|
-
default: 5,
|
|
111
|
-
integer: true,
|
|
112
|
-
minimum: 0,
|
|
113
|
-
}),
|
|
114
|
-
webhookSecret: Manifest.optional(Manifest.secret({ runtimeKey: "EXAMPLE_WEBHOOK_SECRET" })),
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
export type ExampleConfig = Manifest.ConfigValuesOf<typeof ExampleConfigDef>;
|
|
118
|
-
|
|
119
|
-
export const manifest = Manifest.define({
|
|
120
|
-
name: "producer-example",
|
|
121
|
-
title: "Example",
|
|
122
|
-
config: ExampleConfigDef.spec,
|
|
123
|
-
resources: [{ name: "posts", capabilities: ["backfill", "webhook"] }],
|
|
124
|
-
});
|
|
8
|
+
pnpm add @useairfoil/connector-kit effect@^4
|
|
125
9
|
```
|
|
126
10
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
Use `Manifest.configSchema(manifest)` for form validation and `Schema.toStandardSchemaV1(...)` for standard-schema form resolvers. The schema accepts normal JSON values and browser form-shaped values: number fields may arrive as strings, boolean fields may arrive as booleans or `"true"`/`"false"`, optional fields may be omitted or submitted as `""`, and required strings reject `""`.
|
|
11
|
+
## Usage
|
|
130
12
|
|
|
131
|
-
|
|
13
|
+
- `Connector`, `Resource`, `Fetch`, and `Cursor` define the data flow.
|
|
14
|
+
- `Manifest` defines user config.
|
|
15
|
+
- `ConnectorApp` checks config and runs the HTTP server.
|
|
16
|
+
- `Ingestor` writes batches to the console or Wings.
|
|
17
|
+
- `StateStore` keeps ingestion progress.
|
|
18
|
+
- `Webhook` defines webhook routes.
|
|
132
19
|
|
|
133
|
-
|
|
20
|
+
Here is a small resource:
|
|
134
21
|
|
|
135
22
|
```ts
|
|
136
|
-
import
|
|
137
|
-
import {
|
|
138
|
-
import { Schema } from "effect";
|
|
139
|
-
|
|
140
|
-
const configSchema = Manifest.configSchema(polarManifest);
|
|
141
|
-
const formSchema = Schema.toStandardSchemaV1(configSchema);
|
|
142
|
-
|
|
143
|
-
const result = await formSchema["~standard"].validate({
|
|
144
|
-
accessToken: "polar_oat_xxx",
|
|
145
|
-
apiBaseUrl: "https://api.polar.sh/v1/",
|
|
146
|
-
organizationId: "",
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
if ("issues" in result) {
|
|
150
|
-
// Show validation issues in your form.
|
|
151
|
-
} else {
|
|
152
|
-
// Store by manifest field name: accessToken, apiBaseUrl, organizationId.
|
|
153
|
-
// Secrets should be persisted server-side and not echoed back to the browser.
|
|
154
|
-
const values = result.value;
|
|
155
|
-
}
|
|
156
|
-
```
|
|
23
|
+
import { Connector, Cursor, Fetch, Iceberg, Resource } from "@useairfoil/connector-kit";
|
|
24
|
+
import { Effect, Schema } from "effect";
|
|
157
25
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
### Pre-provision Checks
|
|
163
|
-
|
|
164
|
-
The dashboard backend imports each connector package and runs `ConnectorApp.check(...)` after manifest decoding but before persisting secrets or creating database and Kubernetes resources. Validate submitted resource names against `manifest.resources` at the request boundary before calling the typed check API. Replace the backend process provider with the submitted runtime document so unrelated backend environment variables cannot satisfy connector configuration:
|
|
165
|
-
|
|
166
|
-
```ts
|
|
167
|
-
import { ConnectorApp, Manifest } from "@useairfoil/connector-kit";
|
|
168
|
-
import { PolarConnector, manifest } from "@useairfoil/producer-polar";
|
|
169
|
-
import { ConfigProvider, Effect } from "effect";
|
|
170
|
-
import { FetchHttpClient } from "effect/unstable/http";
|
|
171
|
-
|
|
172
|
-
const values = yield * Manifest.decodeConfig(manifest, submittedConfig);
|
|
173
|
-
const runtimeDocument = Manifest.toRuntimeDocument(manifest, values);
|
|
174
|
-
const provider = ConfigProvider.fromUnknown(runtimeDocument, { preserveEmptyStrings: true });
|
|
175
|
-
|
|
176
|
-
const result =
|
|
177
|
-
yield *
|
|
178
|
-
ConnectorApp.check(
|
|
179
|
-
PolarConnector.PolarConnector,
|
|
180
|
-
PolarConnector.layerConfig(PolarConnector.PolarConfigDef.config),
|
|
181
|
-
{ resources: ["customers", "orders"] },
|
|
182
|
-
).pipe(Effect.provide(ConfigProvider.layer(provider)), Effect.provide(FetchHttpClient.layer));
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
Every resource declares a required, read-only `check` effect. Layer/configuration failures are returned for every selected resource; resource-specific failures are returned only under that resource name. Expected failures are values (`{ _tag: "error", message }`), while defects and interruption remain Effect failures. Provision only when all selected results are `{ _tag: "ok" }`. Checks must never publish, checkpoint, or mutate the upstream provider.
|
|
186
|
-
|
|
187
|
-
`RuntimeConfig.PlatformRuntimeKey` is the canonical shared-key contract used by hosted config, StateStore, telemetry, Wings, and PostgreSQL bootstrap code. `Manifest.defineConfig` rejects only those exact shared values; it does not impose connector-specific naming, uniqueness, or metadata policy. Connector packages are first-party code, so their table, port, field, and resource naming remains the connector author's responsibility.
|
|
188
|
-
|
|
189
|
-
## Resource Model
|
|
190
|
-
|
|
191
|
-
Resources emit mutations, not raw rows. Connector Kit currently supports `Resource.entity(...)` for Wings table-backed resources.
|
|
26
|
+
const PostSchema = Schema.Struct({
|
|
27
|
+
id: Schema.String.pipe(Iceberg.field(1)),
|
|
28
|
+
updatedAt: Schema.String.pipe(Iceberg.field(2)),
|
|
29
|
+
}).pipe(Iceberg.identifier("id"));
|
|
192
30
|
|
|
193
|
-
```ts
|
|
194
31
|
const Posts = Resource.entity({
|
|
195
32
|
name: "posts",
|
|
196
|
-
|
|
197
|
-
key: "id",
|
|
33
|
+
rowSchema: PostSchema,
|
|
198
34
|
version: "updatedAt",
|
|
199
|
-
|
|
35
|
+
|
|
36
|
+
check: Effect.void,
|
|
200
37
|
backfill: Fetch.page({
|
|
201
38
|
pageCursor: Cursor.number(),
|
|
202
39
|
cutoff: Cursor.isoDateTime(),
|
|
203
|
-
fetch: (
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
})),
|
|
210
|
-
),
|
|
211
|
-
}),
|
|
212
|
-
});
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
`key` and `version` autocomplete from the decoded schema row type. Invalid field names are rejected at compile time.
|
|
216
|
-
|
|
217
|
-
Mutations are created with:
|
|
218
|
-
|
|
219
|
-
```ts
|
|
220
|
-
Resource.upsert(row);
|
|
221
|
-
|
|
222
|
-
Resource.delete({
|
|
223
|
-
key: "post_123",
|
|
224
|
-
version: "2026-01-01T00:00:00.000Z",
|
|
225
|
-
});
|
|
226
|
-
```
|
|
227
|
-
|
|
228
|
-
## Fetches
|
|
229
|
-
|
|
230
|
-
`Fetch.page(...)` is for paginated backfill. It receives the stored page cursor and the resource cutoff.
|
|
231
|
-
|
|
232
|
-
`Fetch.changes(...)` is for polling incremental changes. It receives the stored changes cursor and can be configured with an interval.
|
|
233
|
-
|
|
234
|
-
Cursor helpers:
|
|
235
|
-
|
|
236
|
-
- `Cursor.string()`
|
|
237
|
-
- `Cursor.number()`
|
|
238
|
-
- `Cursor.isoDateTime()`
|
|
239
|
-
- `Cursor.nowIsoDateTime`
|
|
240
|
-
|
|
241
|
-
Date cursor values are accepted at API boundaries and normalized to ISO strings before checkpoint writes.
|
|
242
|
-
|
|
243
|
-
## Webhooks
|
|
244
|
-
|
|
245
|
-
Webhooks are connector-level routes with schema-validated payloads. A route dispatches to resource-owned webhook handlers by calling `to(resource, payload)`.
|
|
246
|
-
|
|
247
|
-
```ts
|
|
248
|
-
const Posts = Resource.entity({
|
|
249
|
-
name: "posts",
|
|
250
|
-
schema: PostSchema,
|
|
251
|
-
key: "id",
|
|
252
|
-
version: "updatedAt",
|
|
253
|
-
check: Effect.void,
|
|
254
|
-
webhook: Resource.webhook({
|
|
255
|
-
schema: PostEventSchema,
|
|
256
|
-
handler: ({ payload }) => Effect.succeed([Resource.upsert(payload.data)]),
|
|
40
|
+
fetch: () =>
|
|
41
|
+
Effect.succeed({
|
|
42
|
+
rows: [],
|
|
43
|
+
nextPageCursor: 1,
|
|
44
|
+
hasMore: false,
|
|
45
|
+
}),
|
|
257
46
|
}),
|
|
258
47
|
});
|
|
259
48
|
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
schema: ProviderWebhookSchema,
|
|
264
|
-
handler: ({ request, rawBody, payload, to }) =>
|
|
265
|
-
Effect.gen(function* () {
|
|
266
|
-
yield* verifySignature({ request, rawBody });
|
|
267
|
-
yield* to(Posts, payload);
|
|
268
|
-
return HttpServerResponse.jsonUnsafe({ ok: true });
|
|
269
|
-
}),
|
|
49
|
+
const connector = Connector.define({
|
|
50
|
+
name: "producer-example",
|
|
51
|
+
resources: [Posts],
|
|
270
52
|
});
|
|
271
53
|
```
|
|
272
54
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
- parses JSON once
|
|
277
|
-
- validates the route payload schema
|
|
278
|
-
- passes `request`, `rawBody`, typed `payload`, and `to(...)` to the handler
|
|
279
|
-
- returns `400` for invalid body reads, invalid JSON, or invalid route payloads
|
|
280
|
-
- returns `500` for unexpected handler/runtime/publisher failures
|
|
281
|
-
|
|
282
|
-
Signature verification is connector-owned and should use `rawBody`.
|
|
283
|
-
|
|
284
|
-
`ackMode: "after-publish"` publishes collected mutations before the response is completed. `ackMode: "after-enqueue"` enqueues collected mutations and lets the background webhook consumer publish them.
|
|
285
|
-
|
|
286
|
-
## Connector Definition
|
|
55
|
+
When one provider log covers several resources, such as an events API, give
|
|
56
|
+
the connector a single `changes` feed instead of `changes` on each resource. The
|
|
57
|
+
feed reads the log once per run and returns rows by resource name:
|
|
287
58
|
|
|
288
59
|
```ts
|
|
289
60
|
const connector = Connector.define({
|
|
290
61
|
name: "producer-example",
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
62
|
+
resources: [Posts, Comments],
|
|
63
|
+
changes: Fetch.feed({
|
|
64
|
+
resources: [Posts, Comments],
|
|
65
|
+
cursor: Cursor.string(),
|
|
66
|
+
interval: "5 minutes",
|
|
67
|
+
fetch: ({ cursor }) =>
|
|
68
|
+
readEvents(cursor).pipe(
|
|
69
|
+
Effect.map((page) => ({
|
|
70
|
+
rows: { posts: page.posts, comments: page.comments },
|
|
71
|
+
cursor: page.next,
|
|
72
|
+
// Runs again without waiting for the interval.
|
|
73
|
+
hasMore: page.hasMore,
|
|
74
|
+
})),
|
|
75
|
+
),
|
|
76
|
+
}),
|
|
294
77
|
});
|
|
295
78
|
```
|
|
296
79
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
`Ingestion.run(...)` contains the runtime engine. `ConnectorApp.start(...)` is the usual entrypoint for runnable producer CLIs.
|
|
80
|
+
A resource's own `changes` can return `hasMore` the same way, to split a long
|
|
81
|
+
read across runs.
|
|
300
82
|
|
|
301
|
-
|
|
83
|
+
Each resource in the feed stores the feed cursor, so status shows it per
|
|
84
|
+
resource. A resource in the feed cannot also define its own `changes`.
|
|
302
85
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
- publishes through `Publisher.Publisher`
|
|
306
|
-
- checkpoints only after an accepted publish ACK
|
|
307
|
-
- does not checkpoint rejected publishes
|
|
308
|
-
- allows empty accepted batches to advance state
|
|
309
|
-
- stores `lastSuccessAt` with each durable checkpoint
|
|
310
|
-
- persists resource state through `StateStore.StateStore`
|
|
311
|
-
- parks a source when an expected failure reaches the ingestion engine, after any dependency retries
|
|
312
|
-
- keeps other sources, webhooks, and HTTP routes running when one source is parked
|
|
86
|
+
`fetch` also gets `selected`, the resources that run. Skip the work for the
|
|
87
|
+
others.
|
|
313
88
|
|
|
314
|
-
|
|
89
|
+
`Iceberg.identifier` names the row key. Put it last on the schema. Wings
|
|
90
|
+
upserts rows with the same key, and replaces the whole row.
|
|
315
91
|
|
|
316
|
-
|
|
92
|
+
So changes and webhooks return full rows. When a webhook leaves fields out,
|
|
93
|
+
read the record again. For a delete, send only the key, version, and
|
|
94
|
+
`_deleted: true`; the other columns become null. Send `_deleted: false` on other
|
|
95
|
+
rows only when the provider can restore records, so a restore comes back.
|
|
96
|
+
Otherwise leave it out.
|
|
317
97
|
|
|
318
|
-
|
|
98
|
+
For local runs, use `StateStore.layerMemory` and `Ingestor.layerConsole`. For a
|
|
99
|
+
hosted run, use `RuntimeConfig.layerHosted()`, PostgreSQL state, and
|
|
100
|
+
`Ingestor.layerWingsConfig(connector)`.
|
|
319
101
|
|
|
320
|
-
|
|
102
|
+
## Configuration
|
|
321
103
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
maxConnections: Config.succeed(2),
|
|
333
|
-
});
|
|
104
|
+
| Variable | Required | Default |
|
|
105
|
+
| ------------------------------- | -------- | --------------------------- |
|
|
106
|
+
| `AIRFOIL_CONFIG_PATH` | yes | none |
|
|
107
|
+
| `WINGS_URI` | yes | none |
|
|
108
|
+
| `AIRFOIL_CATALOG` | yes | none |
|
|
109
|
+
| `AIRFOIL_TABLE_BINDINGS` | yes | none |
|
|
110
|
+
| `AIRFOIL_CONNECTOR_INSTANCE_ID` | yes | none |
|
|
111
|
+
| `POSTGRES_CONNECTION_STRING` | yes | none |
|
|
112
|
+
| `AIRFOIL_HTTP_PORT` | no | `8080` |
|
|
113
|
+
| `AIRFOIL_STATE_TABLE` | no | `_airfoil_connectors_state` |
|
|
334
114
|
|
|
335
|
-
|
|
115
|
+
```env
|
|
116
|
+
AIRFOIL_TABLE_BINDINGS={"posts":{"namespace":["default"],"name":"posts","location":"s3://warehouse/default/posts"}}
|
|
336
117
|
```
|
|
337
118
|
|
|
338
|
-
|
|
119
|
+
Only the resources in `AIRFOIL_TABLE_BINDINGS` run. List at least one, and use
|
|
120
|
+
only the connector's resource names.
|
|
339
121
|
|
|
340
|
-
|
|
122
|
+
Use only lowercase letters, numbers, and underscores in table names. S3 Tables
|
|
123
|
+
catalogs reject other characters, such as `-`.
|
|
341
124
|
|
|
342
|
-
|
|
125
|
+
The HTTP server has `/health`, `/status`, and `/metrics` endpoints.
|
|
343
126
|
|
|
344
|
-
|
|
127
|
+
## Demo data
|
|
345
128
|
|
|
346
|
-
`
|
|
129
|
+
A connector with a demo has a `demo` command. It writes made-up rows to
|
|
130
|
+
the bound tables, then exits. It needs no provider credentials and no
|
|
131
|
+
PostgreSQL state.
|
|
347
132
|
|
|
348
|
-
```
|
|
349
|
-
|
|
350
|
-
connector,
|
|
351
|
-
tables: {
|
|
352
|
-
posts: "namespaces/default/tables/posts",
|
|
353
|
-
},
|
|
354
|
-
});
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
The `tables` map is keyed by resource name. Values can be table names or objects with a table name and optional partition value.
|
|
358
|
-
|
|
359
|
-
```ts
|
|
360
|
-
Publisher.layerWings({
|
|
361
|
-
connector,
|
|
362
|
-
tables: {
|
|
363
|
-
posts: {
|
|
364
|
-
name: "namespaces/default/tables/posts",
|
|
365
|
-
partitionValue: "account_123",
|
|
366
|
-
},
|
|
367
|
-
},
|
|
368
|
-
});
|
|
133
|
+
```bash
|
|
134
|
+
producer-stripe demo --size 128MiB --seed 42
|
|
369
135
|
```
|
|
370
136
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
137
|
+
- `--size` is the uncompressed Arrow size to write, such as `512KiB`, `128MiB`,
|
|
138
|
+
or `1GiB`. The run can go over by one chunk of groups, about 1 MiB with the
|
|
139
|
+
current connectors.
|
|
140
|
+
- `--seed` picks the data. Dates depend on when the run starts, so two runs
|
|
141
|
+
with the same seed match only if they start at the same time and bind the
|
|
142
|
+
same tables.
|
|
143
|
+
- Setup tables, written once, such as a product catalog, don't grow. If only
|
|
144
|
+
those are bound, the run writes them once and stops below the size. Demos
|
|
145
|
+
with limited history, such as daily reports, also stop when they run out.
|
|
146
|
+
- It needs `AIRFOIL_CONFIG_PATH`, `WINGS_URI`, `AIRFOIL_CATALOG`, and
|
|
147
|
+
`AIRFOIL_TABLE_BINDINGS`. An empty `{}` config file is enough.
|
|
148
|
+
|
|
149
|
+
A demo lists its `setup` and `group` tables and builds their rows with
|
|
150
|
+
`Demo.define`. Build keys from the group index, so they are unique and other
|
|
151
|
+
rows can point to them. Required fields a row leaves out get placeholder values
|
|
152
|
+
from the row schema. Every row is checked against its row schema before it's
|
|
153
|
+
written.
|
|
154
|
+
|
|
155
|
+
## Iceberg field IDs
|
|
156
|
+
|
|
157
|
+
Give each field a unique ID. Keep its ID if you rename it, and never reuse an ID
|
|
158
|
+
after removing a field. Use `Iceberg.field` for struct fields. Put list element
|
|
159
|
+
and map key/value IDs on their schemas with `.annotate({ fieldId })`.
|
|
374
160
|
|
|
375
161
|
```ts
|
|
376
|
-
import {
|
|
377
|
-
import {
|
|
378
|
-
Connector,
|
|
379
|
-
ConnectorApp,
|
|
380
|
-
Cursor,
|
|
381
|
-
Fetch,
|
|
382
|
-
Publisher,
|
|
383
|
-
Resource,
|
|
384
|
-
StateStore,
|
|
385
|
-
Telemetry,
|
|
386
|
-
} from "@useairfoil/connector-kit";
|
|
387
|
-
import { Effect, Layer, Schema } from "effect";
|
|
388
|
-
import { FetchHttpClient } from "effect/unstable/http";
|
|
389
|
-
|
|
390
|
-
const PostSchema = Schema.Struct({
|
|
391
|
-
id: Schema.Number,
|
|
392
|
-
title: Schema.String,
|
|
393
|
-
updatedAt: Schema.String,
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
type Post = Schema.Schema.Type<typeof PostSchema>;
|
|
397
|
-
|
|
398
|
-
const Posts = Resource.entity({
|
|
399
|
-
name: "posts",
|
|
400
|
-
schema: PostSchema,
|
|
401
|
-
key: "id",
|
|
402
|
-
version: "updatedAt",
|
|
403
|
-
check: Effect.void,
|
|
404
|
-
backfill: Fetch.page({
|
|
405
|
-
pageCursor: Cursor.number(),
|
|
406
|
-
cutoff: Cursor.isoDateTime(),
|
|
407
|
-
fetch: () =>
|
|
408
|
-
Effect.succeed({
|
|
409
|
-
mutations: [
|
|
410
|
-
Resource.upsert({ id: 1, title: "Hello", updatedAt: "2026-01-01T00:00:00.000Z" }),
|
|
411
|
-
],
|
|
412
|
-
nextPageCursor: 1,
|
|
413
|
-
hasMore: false,
|
|
414
|
-
}),
|
|
415
|
-
}),
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
const connector = Connector.define({
|
|
419
|
-
name: "producer-example",
|
|
420
|
-
resources: [Posts],
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
const BootstrapLayer = FetchHttpClient.layer;
|
|
424
|
-
|
|
425
|
-
const TelemetryLayer = Telemetry.layerOtlp().pipe(Layer.provide(BootstrapLayer));
|
|
426
|
-
const RuntimeLayer = Layer.mergeAll(
|
|
427
|
-
BootstrapLayer,
|
|
428
|
-
StateStore.layerMemory,
|
|
429
|
-
Publisher.layerConsole,
|
|
430
|
-
TelemetryLayer,
|
|
431
|
-
);
|
|
162
|
+
import { Iceberg } from "@useairfoil/connector-kit";
|
|
163
|
+
import { Schema } from "effect";
|
|
432
164
|
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
)
|
|
165
|
+
const Row = Schema.Struct({
|
|
166
|
+
id: Schema.String.pipe(Iceberg.field(1, { description: "Row ID." })),
|
|
167
|
+
tags: Schema.Array(Schema.String.annotate({ fieldId: 101 })).pipe(
|
|
168
|
+
Iceberg.field(2, { description: "Tags for the row." }),
|
|
169
|
+
),
|
|
170
|
+
})
|
|
171
|
+
.annotate({ description: "Example rows." })
|
|
172
|
+
.pipe(Iceberg.identifier("id"));
|
|
437
173
|
```
|
|
438
174
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
`/status` includes `lastSuccessAt` for each checkpointed backfill or changes source. Use `time() - airfoil_connector_last_success_timestamp_seconds` to monitor source freshness without interpreting provider cursors.
|
|
444
|
-
|
|
445
|
-
Common entry points:
|
|
446
|
-
|
|
447
|
-
- `Telemetry.layer(config, options?)`
|
|
448
|
-
- `Telemetry.layerConfig(config, options?)`
|
|
449
|
-
- `Telemetry.layerOtlpTracing(options?)` for OTLP traces
|
|
450
|
-
- `Telemetry.layerOtlpMetrics(options?)` for OTLP metrics
|
|
451
|
-
- `Telemetry.layerOtlp(options?)` for both traces and metrics
|
|
452
|
-
- `Telemetry.layerMetricsConsoleDump(interval?)`
|
|
453
|
-
|
|
454
|
-
Default redacted headers:
|
|
455
|
-
|
|
456
|
-
- `authorization`
|
|
457
|
-
- `cookie`
|
|
458
|
-
- `set-cookie`
|
|
459
|
-
- `x-api-key`
|
|
460
|
-
- `/api[-_]?key/i`
|
|
461
|
-
- `/secret/i`
|
|
462
|
-
- `/signature/i`
|
|
463
|
-
- `/token/i`
|
|
464
|
-
|
|
465
|
-
Telemetry environment variables:
|
|
466
|
-
|
|
467
|
-
| Variable | Read by | Description |
|
|
468
|
-
| ----------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
469
|
-
| `OTEL_ENABLED` | Connector Kit | Enables OTLP export when `true`. Defaults to `false`. |
|
|
470
|
-
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Connector Kit | OTLP base URL, for example `http://localhost:4318`. Connector Kit appends `/v1/traces` and `/v1/metrics` as needed. Required when `OTEL_ENABLED=true`. |
|
|
471
|
-
| `OTEL_EXPORTER_OTLP_HEADERS` | Connector Kit | Optional comma-separated headers, for example `Authorization=Bearer <token>,X-Axiom-Dataset=<dataset>`. |
|
|
472
|
-
| `OTEL_SERVICE_NAME` | Effect | Service name resource attribute, for example `producer-shopify`. |
|
|
473
|
-
| `OTEL_SERVICE_VERSION` | Effect | Optional service version resource attribute. |
|
|
474
|
-
| `OTEL_RESOURCE_ATTRIBUTES` | Effect | Optional comma-separated resource attributes, for example `deployment.environment=production,team=data`. |
|
|
475
|
-
|
|
476
|
-
## Testing
|
|
477
|
-
|
|
478
|
-
For tests, the most common setup is:
|
|
479
|
-
|
|
480
|
-
- `StateStore.layerMemory` for state
|
|
481
|
-
- a small in-memory `Publisher.Publisher` test layer
|
|
482
|
-
- `ConnectorApp.check(...)` with an in-memory `ConfigProvider` for pre-provision validation
|
|
483
|
-
- `Ingestion.run(...)` or `ConnectorApp.start(...)` inside `Effect.scoped`
|
|
175
|
+
Hosted connectors create missing tables from these bindings. To create one
|
|
176
|
+
manually, use the connector's `create-table <namespace.table> --catalog <id>`
|
|
177
|
+
command. Use `--resource <name>` if the table name differs from the resource.
|
|
484
178
|
|
|
485
|
-
|
|
179
|
+
Both paths use `commitTable` and check the schema after creation. The catalog's
|
|
180
|
+
`createTable` can change field IDs. Set `location` in the binding, or pass
|
|
181
|
+
`--location <uri>` to the command, if the catalog cannot choose a location or
|
|
182
|
+
you want to choose it yourself.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
//#region src/iceberg/annotations.d.ts
|
|
3
|
+
declare module "effect/Schema" {
|
|
4
|
+
namespace Annotations {
|
|
5
|
+
interface Annotations {
|
|
6
|
+
readonly fieldId?: number | undefined;
|
|
7
|
+
readonly icebergIdentifierFields?: ReadonlyArray<string> | undefined;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Sets a field's Iceberg ID, and any other annotations. */
|
|
12
|
+
declare const field: <A = unknown>(fieldId: number, annotations?: Schema.Annotations.Key<A>) => <S extends Schema.Schema<A>>(schema: S) => S["Rebuild"];
|
|
13
|
+
/** Adds an offset to a base ID for reusable schemas. */
|
|
14
|
+
declare const ids: (baseId: number) => (offset: number) => number;
|
|
15
|
+
type KnownField<S extends Schema.Top> = keyof S["Type"] & string;
|
|
16
|
+
/** A row field that cannot be null or undefined. */
|
|
17
|
+
type RequiredField<S extends Schema.Top> = [KnownField<S>] extends [never] ? string : { readonly [Field in KnownField<S>]-?: undefined extends S["Type"][Field] ? never : null extends S["Type"][Field] ? never : Field; }[KnownField<S>];
|
|
18
|
+
/** A row schema whose identifier fields are `Key`. Only the type has it. */
|
|
19
|
+
interface Identified<Key extends string> {
|
|
20
|
+
readonly "~iceberg.identifier": Key;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Marks the fields that identify a row. Wings upserts rows by them.
|
|
24
|
+
*
|
|
25
|
+
* Put it last. A later `.annotate()` keeps the runtime annotation, but the type
|
|
26
|
+
* loses the key, so `Resource.entity` stops compiling.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* ```ts
|
|
30
|
+
* const Post = Schema.Struct({
|
|
31
|
+
* id: Schema.String.pipe(Iceberg.field(1)),
|
|
32
|
+
* updatedAt: Schema.String.pipe(Iceberg.field(2)),
|
|
33
|
+
* }).pipe(Iceberg.identifier("id"));
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
declare const identifier: <S extends Schema.Top, const Key extends RequiredField<S>>(...keys: readonly [Key, ...ReadonlyArray<Key>]) => (schema: S) => S["Rebuild"] & Identified<Key>;
|
|
37
|
+
//#endregion
|
|
38
|
+
export { ids as a, identifier as i, RequiredField as n, field as r, Identified as t };
|
|
39
|
+
//# sourceMappingURL=annotations-BE74YmAO.d.mts.map
|