@useairfoil/connector-kit 0.6.0 → 0.7.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.txt +201 -0
- package/METRICS.md +38 -42
- package/README.md +111 -444
- package/dist/annotations-vzSzXtr-.d.mts +34 -0
- package/dist/arrow-flight-DDQ0ktx9.mjs +251 -0
- package/dist/arrow-flight-DDQ0ktx9.mjs.map +1 -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-C6_dZnhE.mjs +256 -0
- package/dist/connector-app-C6_dZnhE.mjs.map +1 -0
- package/dist/{constants-D113Ut1x.js → constants-v3UX-hf6.mjs} +5 -5
- package/dist/constants-v3UX-hf6.mjs.map +1 -0
- package/dist/demo/index.d.mts +2 -0
- package/dist/demo/index.mjs +2 -0
- package/dist/demo-DqTJ7TWI.mjs +204 -0
- package/dist/demo-DqTJ7TWI.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-Cj2PwlNm.mjs +30 -0
- package/dist/iceberg-Cj2PwlNm.mjs.map +1 -0
- package/dist/index-BTAbexBp.d.mts +55 -0
- package/dist/{index-DXa26Ry5.d.ts → index-BX8G7d41.d.mts} +1 -2
- package/dist/{index-VpHaZXZw.d.ts → index-Byr8NTdG.d.mts} +11 -13
- package/dist/index-CV6V6fyx.d.mts +79 -0
- package/dist/{index-C5Ma7V7t.d.ts → index-CZKx2Yce.d.mts} +72 -37
- package/dist/{index-DYpEMJf-.d.ts → index-CgvFomO6.d.mts} +6 -7
- package/dist/{index-bfjH7Yb0.d.ts → index-DeTwIUKl.d.mts} +9 -10
- package/dist/{index-DKoRwyFn.d.ts → index-DkVNO6ks.d.mts} +2 -5
- package/dist/{index-BKw7jA2a.d.ts → index-dHS59EZl.d.mts} +10 -8
- package/dist/{index-CR9HJDdr.d.ts → index-eFuoMZZ3.d.mts} +18 -20
- package/dist/{index-ZSuiZX5H.d.ts → index-fHIJJoRe.d.mts} +25 -30
- package/dist/index-uFa2wwYo.d.mts +27 -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-DoCUwm5N.mjs +275 -0
- package/dist/ingestion-DoCUwm5N.mjs.map +1 -0
- package/dist/ingestor/index.d.mts +3 -0
- package/dist/ingestor/index.mjs +4 -0
- package/dist/ingestor-DjCEzBzZ.mjs +26 -0
- package/dist/ingestor-DjCEzBzZ.mjs.map +1 -0
- package/dist/instrumented-Bq1WX2L5.mjs +29 -0
- package/dist/instrumented-Bq1WX2L5.mjs.map +1 -0
- package/dist/manifest/index.d.mts +2 -0
- package/dist/manifest/{index.js → index.mjs} +2 -4
- package/dist/{manifest-P7kBnch9.js → manifest-CABeIhAG.mjs} +14 -19
- package/dist/manifest-CABeIhAG.mjs.map +1 -0
- package/dist/{telemetry-j_nxwYfh.js → metrics-C901IIYZ.mjs} +172 -26
- package/dist/metrics-C901IIYZ.mjs.map +1 -0
- package/dist/{metrics-C4V1pm6v.d.ts → metrics-DoJ0WeKu.d.mts} +18 -13
- 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-Bn8Fivt1.mjs +100 -0
- package/dist/runtime-config-Bn8Fivt1.mjs.map +1 -0
- package/dist/server-D1TTN0_D.mjs +144 -0
- package/dist/server-D1TTN0_D.mjs.map +1 -0
- package/dist/service-BRoy_EId.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-BdYulQOY.js → state-store-DhSrKVEH.mjs} +15 -22
- package/dist/state-store-DhSrKVEH.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-BTTd7AUR.js → status-4h6Oxh-O.mjs} +14 -16
- package/dist/status-4h6Oxh-O.mjs.map +1 -0
- package/dist/table-CxM09xf7.mjs +307 -0
- package/dist/table-CxM09xf7.mjs.map +1 -0
- package/dist/types-NtSv6gwl.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/package.json +72 -58
- package/src/catalog/retry.ts +25 -0
- package/src/catalog/table.ts +237 -0
- package/src/connector-app/create-table.ts +47 -0
- package/src/connector-app/demo.ts +47 -0
- package/src/connector-app/index.ts +3 -0
- package/src/connector-app/setup.ts +69 -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 +172 -0
- package/src/iceberg/annotations.ts +49 -0
- package/src/iceberg/error.ts +9 -0
- package/src/iceberg/index.ts +3 -0
- package/src/iceberg/table.ts +461 -0
- package/src/index.ts +3 -1
- package/src/ingestion/engine.ts +240 -60
- package/src/ingestor/arrow-flight.ts +141 -0
- package/src/ingestor/arrow.ts +248 -0
- package/src/ingestor/console.ts +13 -0
- package/src/ingestor/index.ts +13 -0
- package/src/ingestor/instrumented.ts +61 -0
- package/src/ingestor/service.ts +30 -0
- package/src/manifest/config.ts +3 -3
- package/src/metrics.ts +66 -20
- package/src/runtime-config/config.ts +11 -8
- package/src/runtime-config/constants.ts +3 -2
- package/src/runtime-config/index.ts +2 -7
- package/src/runtime-config/layer.ts +1 -1
- package/src/runtime-config/table-bindings.ts +50 -136
- 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 +20 -28
- 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 -12
- package/dist/connector-app-BljHd0Pe.js +0 -64
- package/dist/connector-app-BljHd0Pe.js.map +0 -1
- package/dist/constants-D113Ut1x.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-BA8ASHKr.d.ts +0 -31
- package/dist/index.d.ts +0 -14
- package/dist/index.js +0 -60
- package/dist/index.js.map +0 -1
- package/dist/ingestion/index.d.ts +0 -6
- package/dist/ingestion/index.js +0 -11
- package/dist/ingestion-QBGKrFr9.js +0 -200
- package/dist/ingestion-QBGKrFr9.js.map +0 -1
- package/dist/manifest/index.d.ts +0 -2
- package/dist/manifest-P7kBnch9.js.map +0 -1
- package/dist/metrics-inkCAxKq.js +0 -136
- package/dist/metrics-inkCAxKq.js.map +0 -1
- package/dist/metrics.d.ts +0 -4
- package/dist/metrics.js +0 -5
- package/dist/publisher/index.d.ts +0 -6
- package/dist/publisher/index.js +0 -8
- package/dist/publisher-BdWJOfer.js +0 -138
- package/dist/publisher-BdWJOfer.js.map +0 -1
- package/dist/runtime-config/index.d.ts +0 -4
- package/dist/runtime-config/index.js +0 -5
- package/dist/runtime-config-IbyCVpei.js +0 -60
- package/dist/runtime-config-IbyCVpei.js.map +0 -1
- package/dist/server-D-eLX3yN.js +0 -183
- package/dist/server-D-eLX3yN.js.map +0 -1
- package/dist/service-CUjGhe-7.d.ts +0 -32
- package/dist/service-DiP-XETo.js +0 -8
- package/dist/service-DiP-XETo.js.map +0 -1
- package/dist/state-store/index.d.ts +0 -3
- package/dist/state-store/index.js +0 -5
- package/dist/state-store-BdYulQOY.js.map +0 -1
- package/dist/status-BTTd7AUR.js.map +0 -1
- package/dist/table-bindings-Dp1DhTeS.js +0 -106
- package/dist/table-bindings-Dp1DhTeS.js.map +0 -1
- package/dist/telemetry-j_nxwYfh.js.map +0 -1
- package/dist/types-jVD5Ys43.d.ts +0 -174
- package/dist/webhook/index.d.ts +0 -7
- package/dist/webhook/index.js +0 -9
- package/dist/webhook-MhFcLW-o.js +0 -13
- package/dist/webhook-MhFcLW-o.js.map +0 -1
- package/src/publisher/console.ts +0 -18
- package/src/publisher/index.ts +0 -7
- package/src/publisher/instrumented.ts +0 -75
- package/src/publisher/service.ts +0 -37
- package/src/publisher/wings.ts +0 -204
package/README.md
CHANGED
|
@@ -1,497 +1,164 @@
|
|
|
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
|
-
});
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
`ExampleConfigDef.config` is the runtime Effect config. `ExampleConfigDef.spec` is serializable manifest metadata. `required` defaults to `true`. Number fields may set `integer` and `minimum`; Effect Schema applies the same constraints to runtime and form values. Use `required: false` with a default when the form may omit a field but the runtime must always receive a concrete value. Use `Manifest.optional(...)` when the runtime result should be an `Option`. Secret fields cannot declare defaults, so optional secrets use `Manifest.optional(Manifest.secret(...))`.
|
|
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 `""`.
|
|
130
|
-
|
|
131
|
-
Use `Manifest.decodeConfig(manifest, input)` as the canonical server-side decoder. It rejects undeclared fields, treats `""` on a declared optional field as not supplied, and then applies manifest defaults. Therefore clearing or omitting a defaulted optional control means “use the manifest default”; it does not disable that setting. Model disabling as an explicit select/boolean/nullable contract instead of overloading an empty string. An optional field without a default remains absent.
|
|
132
|
-
|
|
133
|
-
Frontend/backend validation example:
|
|
134
|
-
|
|
135
|
-
```ts
|
|
136
|
-
import * as Manifest from "@useairfoil/connector-kit/manifest";
|
|
137
|
-
import { manifest as polarManifest } from "@useairfoil/producer-polar/manifest";
|
|
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
|
-
```
|
|
157
|
-
|
|
158
|
-
Render fields from `manifest.config`: use `field.name` as the form/storage key, `field.description` for help text, `field.secret` for password inputs/redaction, `field.values` for selects, `field.default` for the documented fallback, `field.required` for required markers, and number `integer` and `minimum` values for input constraints. The UI may show a default as an initial value or clearly labelled placeholder, but clearing it still resolves to that default during canonical decoding. Decode submitted values with `Manifest.decodeConfig(...)`, then map the canonical logical object to its flat runtime document with `Manifest.toRuntimeDocument(...)`. Each `field.runtimeKey` is an Effect Config key, not an instruction to create one Pod environment variable.
|
|
159
|
-
|
|
160
|
-
Hosted connectors receive the runtime document as a read-only JSON file. `AIRFOIL_CONFIG_PATH` selects the file, and `RuntimeConfig.layerHosted()` adds the file beneath Effect's existing environment provider. The file is required and must contain valid JSON; each connector's Effect Config validates the fields it reads. Local sandboxes use Effect's default environment provider. Platform-owned config such as Wings host, namespace, table names, PostgreSQL settings, ports, and OTEL settings stays outside the manifest and outside the connector JSON.
|
|
161
|
-
|
|
162
|
-
Hosted HTTP runtimes use `AIRFOIL_HTTP_PORT` (default `8080`). Wings table routing is one atomic `AIRFOIL_TABLE_BINDINGS` JSON object keyed by connector resource name. `Publisher.layerWingsConfig(connector)` rejects missing or unknown resources and validates every binding before ingestion starts:
|
|
163
|
-
|
|
164
|
-
```env
|
|
165
|
-
AIRFOIL_TABLE_BINDINGS={"posts":"namespaces/default/tables/posts"}
|
|
8
|
+
pnpm add @useairfoil/connector-kit effect@^4
|
|
166
9
|
```
|
|
167
10
|
|
|
168
|
-
|
|
11
|
+
## Usage
|
|
169
12
|
|
|
170
|
-
|
|
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
|
+
- `Webhook` defines webhook routes.
|
|
17
|
+
- `Ingestor` writes batches to the console or Iceberg.
|
|
18
|
+
- `StateStore` keeps ingestion progress.
|
|
171
19
|
|
|
172
|
-
|
|
20
|
+
Here is a small resource:
|
|
173
21
|
|
|
174
22
|
```ts
|
|
175
|
-
import {
|
|
176
|
-
import {
|
|
177
|
-
import { ConfigProvider, Effect } from "effect";
|
|
178
|
-
import { FetchHttpClient } from "effect/unstable/http";
|
|
179
|
-
|
|
180
|
-
const values = yield * Manifest.decodeConfig(manifest, submittedConfig);
|
|
181
|
-
const runtimeDocument = Manifest.toRuntimeDocument(manifest, values);
|
|
182
|
-
const provider = ConfigProvider.fromUnknown(runtimeDocument, { preserveEmptyStrings: true });
|
|
183
|
-
|
|
184
|
-
const result =
|
|
185
|
-
yield *
|
|
186
|
-
ConnectorApp.check(
|
|
187
|
-
PolarConnector.PolarConnector,
|
|
188
|
-
PolarConnector.layerConfig(PolarConnector.PolarConfigDef.config),
|
|
189
|
-
{ resources: ["customers", "orders"] },
|
|
190
|
-
).pipe(Effect.provide(ConfigProvider.layer(provider)), Effect.provide(FetchHttpClient.layer));
|
|
191
|
-
```
|
|
23
|
+
import { Connector, Cursor, Fetch, Iceberg, Resource } from "@useairfoil/connector-kit";
|
|
24
|
+
import { Effect, Schema } from "effect";
|
|
192
25
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
## Resource Model
|
|
198
|
-
|
|
199
|
-
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"));
|
|
200
30
|
|
|
201
|
-
```ts
|
|
202
31
|
const Posts = Resource.entity({
|
|
203
32
|
name: "posts",
|
|
204
|
-
|
|
205
|
-
key: "id",
|
|
33
|
+
rowSchema: PostSchema,
|
|
206
34
|
version: "updatedAt",
|
|
207
|
-
|
|
35
|
+
|
|
36
|
+
check: Effect.void,
|
|
208
37
|
backfill: Fetch.page({
|
|
209
38
|
pageCursor: Cursor.number(),
|
|
210
39
|
cutoff: Cursor.isoDateTime(),
|
|
211
|
-
fetch: (
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
})),
|
|
218
|
-
),
|
|
219
|
-
}),
|
|
220
|
-
});
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
`key` and `version` autocomplete from the decoded schema row type. Invalid field names are rejected at compile time.
|
|
224
|
-
|
|
225
|
-
Mutations are created with:
|
|
226
|
-
|
|
227
|
-
```ts
|
|
228
|
-
Resource.upsert(row);
|
|
229
|
-
|
|
230
|
-
Resource.delete({
|
|
231
|
-
key: "post_123",
|
|
232
|
-
version: "2026-01-01T00:00:00.000Z",
|
|
233
|
-
});
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
## Fetches
|
|
237
|
-
|
|
238
|
-
`Fetch.page(...)` is for paginated backfill. It receives the stored page cursor and the resource cutoff.
|
|
239
|
-
|
|
240
|
-
`Fetch.changes(...)` is for polling incremental changes. It receives the stored changes cursor and can be configured with an interval.
|
|
241
|
-
|
|
242
|
-
Cursor helpers:
|
|
243
|
-
|
|
244
|
-
- `Cursor.string()`
|
|
245
|
-
- `Cursor.number()`
|
|
246
|
-
- `Cursor.isoDateTime()`
|
|
247
|
-
- `Cursor.nowIsoDateTime`
|
|
248
|
-
|
|
249
|
-
Date cursor values are accepted at API boundaries and normalized to ISO strings before checkpoint writes.
|
|
250
|
-
|
|
251
|
-
## Webhooks
|
|
252
|
-
|
|
253
|
-
Webhooks are connector-level routes with schema-validated payloads. A route dispatches to resource-owned webhook handlers by calling `to(resource, payload)`.
|
|
254
|
-
|
|
255
|
-
```ts
|
|
256
|
-
const Posts = Resource.entity({
|
|
257
|
-
name: "posts",
|
|
258
|
-
schema: PostSchema,
|
|
259
|
-
key: "id",
|
|
260
|
-
version: "updatedAt",
|
|
261
|
-
check: Effect.void,
|
|
262
|
-
webhook: Resource.webhook({
|
|
263
|
-
schema: PostEventSchema,
|
|
264
|
-
handler: ({ payload }) => Effect.succeed([Resource.upsert(payload.data)]),
|
|
40
|
+
fetch: () =>
|
|
41
|
+
Effect.succeed({
|
|
42
|
+
rows: [],
|
|
43
|
+
nextPageCursor: 1,
|
|
44
|
+
hasMore: false,
|
|
45
|
+
}),
|
|
265
46
|
}),
|
|
266
47
|
});
|
|
267
48
|
|
|
268
|
-
const route = Webhook.route({
|
|
269
|
-
path: "/webhooks/example",
|
|
270
|
-
ackMode: "after-publish",
|
|
271
|
-
schema: ProviderWebhookSchema,
|
|
272
|
-
handler: ({ request, rawBody, payload, to }) =>
|
|
273
|
-
Effect.gen(function* () {
|
|
274
|
-
yield* verifySignature({ request, rawBody });
|
|
275
|
-
yield* to(Posts, payload);
|
|
276
|
-
return HttpServerResponse.jsonUnsafe({ ok: true });
|
|
277
|
-
}),
|
|
278
|
-
});
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
Route behavior:
|
|
282
|
-
|
|
283
|
-
- reads the raw body once
|
|
284
|
-
- parses JSON once
|
|
285
|
-
- validates the route payload schema
|
|
286
|
-
- passes `request`, `rawBody`, typed `payload`, and `to(...)` to the handler
|
|
287
|
-
- returns `400` for invalid body reads, invalid JSON, or invalid route payloads
|
|
288
|
-
- returns `500` for unexpected handler/runtime/publisher failures
|
|
289
|
-
|
|
290
|
-
Signature verification is connector-owned and should use `rawBody`.
|
|
291
|
-
|
|
292
|
-
`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.
|
|
293
|
-
|
|
294
|
-
## Connector Definition
|
|
295
|
-
|
|
296
|
-
```ts
|
|
297
49
|
const connector = Connector.define({
|
|
298
50
|
name: "producer-example",
|
|
299
|
-
title: "Producer Example",
|
|
300
51
|
resources: [Posts],
|
|
301
|
-
webhooks: [route],
|
|
302
52
|
});
|
|
303
53
|
```
|
|
304
54
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
Current runtime behavior:
|
|
310
|
-
|
|
311
|
-
- runs resource backfill, changes, and webhooks concurrently
|
|
312
|
-
- initializes the same initial cutoff for backfill and changes
|
|
313
|
-
- publishes through `Publisher.Publisher`
|
|
314
|
-
- checkpoints only after an accepted publish ACK
|
|
315
|
-
- does not checkpoint rejected publishes
|
|
316
|
-
- allows empty accepted batches to advance state
|
|
317
|
-
- stores `lastSuccessAt` with each durable checkpoint
|
|
318
|
-
- persists resource state through `StateStore.StateStore`
|
|
319
|
-
- parks a source when an expected failure reaches the ingestion engine, after any dependency retries
|
|
320
|
-
- keeps other sources, webhooks, and HTTP routes running when one source is parked
|
|
321
|
-
|
|
322
|
-
Parked sources resume after process restart or configuration redeployment. Defects and interruption are not isolated and still stop the runtime.
|
|
323
|
-
|
|
324
|
-
`Ingestion.run` depends only on the typed Airfoil `StateStore`; it does not expose the underlying Effect `KeyValueStore`. Provide `StateStore.layerMemory` for development/tests. Production entrypoints provide `StateStore.layerSql()`, which privately composes Effect's `KeyValueStore.layerSql` and scopes every key with the non-empty `AIRFOIL_CONNECTOR_INSTANCE_ID`. The platform ID is opaque and is not required to be a UUID.
|
|
325
|
-
|
|
326
|
-
State keys use plain colon-delimited segments and are intentionally inspectable; they are not escaped or encoded. The trusted backend must issue connector instance IDs without `:`, and connector authors must keep resource names free of `:`. Connector Kit currently relies on that naming contract rather than adding runtime validation.
|
|
327
|
-
|
|
328
|
-
The shared SQL table defaults to `_airfoil_connectors_state`. Set `AIRFOIL_STATE_TABLE` only to override that default. `POSTGRES_CONNECTION_STRING` is consumed by the PostgreSQL client layer, not by `StateStore` itself:
|
|
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:
|
|
329
58
|
|
|
330
59
|
```ts
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
60
|
+
const connector = Connector.define({
|
|
61
|
+
name: "producer-example",
|
|
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
|
+
}),
|
|
341
77
|
});
|
|
342
|
-
|
|
343
|
-
const StateStoreLive = StateStore.layerSql().pipe(Layer.provide(PostgresLive));
|
|
344
78
|
```
|
|
345
79
|
|
|
346
|
-
|
|
80
|
+
A resource's own `changes` can return `hasMore` the same way, to split a long
|
|
81
|
+
read across runs.
|
|
347
82
|
|
|
348
|
-
|
|
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`.
|
|
349
85
|
|
|
350
|
-
|
|
86
|
+
The Airfoil Storage Engine supports upsert operations on Iceberg rows. Use the
|
|
87
|
+
`Iceberg.identifier` helper to annotate identifier rows (primary keys). This
|
|
88
|
+
annotation must always be used last.
|
|
351
89
|
|
|
352
|
-
|
|
90
|
+
Upsert operations replace the full row conten, for this reason the change feed
|
|
91
|
+
must include all fields. If the webhook events don't include all fields, the
|
|
92
|
+
implementation must fetch the resource before pushing it into the engine.
|
|
353
93
|
|
|
354
|
-
|
|
94
|
+
The Airfoil Storage Engine implements soft deletions. To delete a row, push only
|
|
95
|
+
the key, version column (if any), `_deleted = true` and any other required column.
|
|
96
|
+
If the target service supports restoring records, the connector must send
|
|
97
|
+
`_deleted = false` with every row.
|
|
355
98
|
|
|
356
|
-
|
|
357
|
-
const publisherLayer = Publisher.layerWings({
|
|
358
|
-
connector,
|
|
359
|
-
tables: {
|
|
360
|
-
posts: "namespaces/default/tables/posts",
|
|
361
|
-
},
|
|
362
|
-
});
|
|
363
|
-
```
|
|
99
|
+
### Running
|
|
364
100
|
|
|
365
|
-
|
|
101
|
+
When testing locally, you can run the connector in memory:
|
|
366
102
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
Publisher.layerWings({
|
|
371
|
-
connector,
|
|
372
|
-
tables: {
|
|
373
|
-
posts: {
|
|
374
|
-
name: "namespaces/default/tables/posts",
|
|
375
|
-
partitionValue: Wings.PartitionValue.string("account_123"),
|
|
376
|
-
},
|
|
377
|
-
},
|
|
378
|
-
});
|
|
379
|
-
```
|
|
380
|
-
|
|
381
|
-
Hosted entrypoints should use `Publisher.layerWingsConfig(connector)` instead. It loads the exact resource map from `AIRFOIL_TABLE_BINDINGS`, delegates partition decoding and table compatibility to Wings, and applies Connector Kit's manifest-resource validation before ingestion starts.
|
|
382
|
-
|
|
383
|
-
The Wings publisher resolves table metadata during layer construction, validates resource key/version/partition compatibility, sends upserts with full rows, and sends deletes with key/version-only rows.
|
|
384
|
-
|
|
385
|
-
## Minimal Example
|
|
103
|
+
- `StateStore.layerMemory`: stores the connector's state in memory.
|
|
104
|
+
- `Ingestor.layerConsole`: outputs new batches to console.
|
|
386
105
|
|
|
387
|
-
|
|
388
|
-
import { NodeRuntime } from "@effect/platform-node";
|
|
389
|
-
import {
|
|
390
|
-
Connector,
|
|
391
|
-
ConnectorApp,
|
|
392
|
-
Cursor,
|
|
393
|
-
Fetch,
|
|
394
|
-
Publisher,
|
|
395
|
-
Resource,
|
|
396
|
-
StateStore,
|
|
397
|
-
Telemetry,
|
|
398
|
-
} from "@useairfoil/connector-kit";
|
|
399
|
-
import { Effect, Layer, Schema } from "effect";
|
|
400
|
-
import { FetchHttpClient } from "effect/unstable/http";
|
|
106
|
+
For a durable run:
|
|
401
107
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
});
|
|
108
|
+
- `RuntimeConfig.layerHosted()`: loads the configuration from a file.
|
|
109
|
+
- `StateStore.layerSql()`: stores the connector's state in PostgreSQL.
|
|
110
|
+
- `Ingestor.layerArrowFlightConfig(connector)`: publish data to Apache Iceberg
|
|
111
|
+
using the Airfoil Storage Engine and Arrow Flight.
|
|
407
112
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
const Posts = Resource.entity({
|
|
411
|
-
name: "posts",
|
|
412
|
-
schema: PostSchema,
|
|
413
|
-
key: "id",
|
|
414
|
-
version: "updatedAt",
|
|
415
|
-
check: Effect.void,
|
|
416
|
-
backfill: Fetch.page({
|
|
417
|
-
pageCursor: Cursor.number(),
|
|
418
|
-
cutoff: Cursor.isoDateTime(),
|
|
419
|
-
fetch: () =>
|
|
420
|
-
Effect.succeed({
|
|
421
|
-
mutations: [
|
|
422
|
-
Resource.upsert({ id: 1, title: "Hello", updatedAt: "2026-01-01T00:00:00.000Z" }),
|
|
423
|
-
],
|
|
424
|
-
nextPageCursor: 1,
|
|
425
|
-
hasMore: false,
|
|
426
|
-
}),
|
|
427
|
-
}),
|
|
428
|
-
});
|
|
113
|
+
The built-in HTTP server has `/health`, `/status`, and `/metrics` endpoints.
|
|
429
114
|
|
|
430
|
-
|
|
431
|
-
name: "producer-example",
|
|
432
|
-
resources: [Posts],
|
|
433
|
-
});
|
|
115
|
+
## Configuration
|
|
434
116
|
|
|
435
|
-
|
|
117
|
+
| Variable | Required | Default |
|
|
118
|
+
| ------------------------------- | -------- | --------------------------- |
|
|
119
|
+
| `AIRFOIL_CONFIG_PATH` | yes | none |
|
|
120
|
+
| `ENGINE_URI` | yes | none |
|
|
121
|
+
| `AIRFOIL_CATALOG` | yes | none |
|
|
122
|
+
| `AIRFOIL_TABLE_BINDINGS` | yes | none |
|
|
123
|
+
| `AIRFOIL_CONNECTOR_INSTANCE_ID` | yes | none |
|
|
124
|
+
| `POSTGRES_CONNECTION_STRING` | yes | none |
|
|
125
|
+
| `AIRFOIL_HTTP_PORT` | no | `8080` |
|
|
126
|
+
| `AIRFOIL_STATE_TABLE` | no | `_airfoil_connectors_state` |
|
|
436
127
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
BootstrapLayer,
|
|
440
|
-
StateStore.layerMemory,
|
|
441
|
-
Publisher.layerConsole,
|
|
442
|
-
TelemetryLayer,
|
|
443
|
-
);
|
|
128
|
+
The `AIRFOIL_TABLE_BINDINGS` environment variables controls which resources are
|
|
129
|
+
synced and the target table. At least one resource must be defined.
|
|
444
130
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
NodeRuntime.runMain,
|
|
448
|
-
);
|
|
131
|
+
```env
|
|
132
|
+
AIRFOIL_TABLE_BINDINGS={"posts":{"namespace":["default"],"name":"posts"}}
|
|
449
133
|
```
|
|
450
134
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
`Telemetry` contains connector-kit span names, span attributes, error annotation helpers, OTLP tracing layers, and OTLP metrics layers. HTTP connector runtimes expose shallow process health at `GET /health`, Prometheus metrics at `GET /metrics`, and durable sync state at `GET /status` by default. Provider, Wings, and PostgreSQL failures do not make `/health` fail.
|
|
135
|
+
Use only lowercase letters, numbers, and underscores in table names. S3 Tables
|
|
136
|
+
catalogs reject other characters, such as `-`.
|
|
454
137
|
|
|
455
|
-
|
|
138
|
+
## Demo data
|
|
456
139
|
|
|
457
|
-
|
|
140
|
+
A connector with a demo has a `demo` command. It writes made-up rows to
|
|
141
|
+
the bound tables, then exits. It needs no provider credentials and no
|
|
142
|
+
PostgreSQL state.
|
|
458
143
|
|
|
459
|
-
|
|
460
|
-
-
|
|
461
|
-
|
|
462
|
-
- `Telemetry.layerOtlpMetrics(options?)` for OTLP metrics
|
|
463
|
-
- `Telemetry.layerOtlp(options?)` for both traces and metrics
|
|
464
|
-
- `Telemetry.layerMetricsConsoleDump(interval?)`
|
|
465
|
-
|
|
466
|
-
Default redacted headers:
|
|
467
|
-
|
|
468
|
-
- `authorization`
|
|
469
|
-
- `cookie`
|
|
470
|
-
- `set-cookie`
|
|
471
|
-
- `x-api-key`
|
|
472
|
-
- `/api[-_]?key/i`
|
|
473
|
-
- `/secret/i`
|
|
474
|
-
- `/signature/i`
|
|
475
|
-
- `/token/i`
|
|
476
|
-
|
|
477
|
-
Telemetry environment variables:
|
|
478
|
-
|
|
479
|
-
| Variable | Read by | Description |
|
|
480
|
-
| ----------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
481
|
-
| `OTEL_ENABLED` | Connector Kit | Enables OTLP export when `true`. Defaults to `false`. |
|
|
482
|
-
| `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`. |
|
|
483
|
-
| `OTEL_EXPORTER_OTLP_HEADERS` | Connector Kit | Optional comma-separated headers, for example `Authorization=Bearer <token>,X-Axiom-Dataset=<dataset>`. |
|
|
484
|
-
| `OTEL_SERVICE_NAME` | Effect | Service name resource attribute, for example `producer-shopify`. |
|
|
485
|
-
| `OTEL_SERVICE_VERSION` | Effect | Optional service version resource attribute. |
|
|
486
|
-
| `OTEL_RESOURCE_ATTRIBUTES` | Effect | Operator-owned identity, for example `service.instance.id=instance-1,airfoil.connector.revision=4,airfoil.team.id=team-acme`. |
|
|
487
|
-
|
|
488
|
-
## Testing
|
|
489
|
-
|
|
490
|
-
For tests, the most common setup is:
|
|
491
|
-
|
|
492
|
-
- `StateStore.layerMemory` for state
|
|
493
|
-
- a small in-memory `Publisher.Publisher` test layer
|
|
494
|
-
- `ConnectorApp.check(...)` with an in-memory `ConfigProvider` for pre-provision validation
|
|
495
|
-
- `Ingestion.run(...)` or `ConnectorApp.start(...)` inside `Effect.scoped`
|
|
144
|
+
```bash
|
|
145
|
+
producer-stripe demo --size 128MiB --seed 42
|
|
146
|
+
```
|
|
496
147
|
|
|
497
|
-
|
|
148
|
+
- `--size` is the uncompressed Arrow size to write, such as `512KiB`, `128MiB`,
|
|
149
|
+
or `1GiB`. The run can go over by one chunk of groups, about 1 MiB with the
|
|
150
|
+
current connectors.
|
|
151
|
+
- `--seed` picks the data. Dates depend on when the run starts, so two runs
|
|
152
|
+
with the same seed match only if they start at the same time and bind the
|
|
153
|
+
same tables.
|
|
154
|
+
- Setup tables, written once, such as a product catalog, don't grow. If only
|
|
155
|
+
those are bound, the run writes them once and stops below the size. Demos
|
|
156
|
+
with limited history, such as daily reports, also stop when they run out.
|
|
157
|
+
- It needs `AIRFOIL_CONFIG_PATH`, `ENGINE_URI`, `AIRFOIL_CATALOG`, and
|
|
158
|
+
`AIRFOIL_TABLE_BINDINGS`. An empty `{}` config file is enough.
|
|
159
|
+
|
|
160
|
+
A demo lists its `setup` and `group` tables and builds their rows with
|
|
161
|
+
`Demo.define`. Build keys from the group index, so they are unique and other
|
|
162
|
+
rows can point to them. Required fields a row leaves out get placeholder values
|
|
163
|
+
from the row schema. Every row is checked against its row schema before it's
|
|
164
|
+
written.
|
|
@@ -0,0 +1,34 @@
|
|
|
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 icebergIdentifierFields?: ReadonlyArray<string> | undefined;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
type KnownField<S extends Schema.Top> = keyof S["Type"] & string;
|
|
11
|
+
/** A row field that cannot be null or undefined. */
|
|
12
|
+
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>];
|
|
13
|
+
/** A row schema whose identifier fields are `Key`. Only the type has it. */
|
|
14
|
+
interface Identified<Key extends string> {
|
|
15
|
+
readonly "~iceberg.identifier": Key;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Marks the fields that identify a row. The engine upserts rows by them.
|
|
19
|
+
*
|
|
20
|
+
* Put it last. A later `.annotate()` keeps the runtime annotation, but the type
|
|
21
|
+
* loses the key, so `Resource.entity` stops compiling.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const Post = Schema.Struct({
|
|
26
|
+
* id: Schema.String,
|
|
27
|
+
* updatedAt: Schema.String,
|
|
28
|
+
* }).pipe(Iceberg.identifier("id"));
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
declare const identifier: <S extends Schema.Top, const Key extends RequiredField<S>>(...keys: readonly [Key, ...ReadonlyArray<Key>]) => (schema: S) => S["Rebuild"] & Identified<Key>;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { RequiredField as n, identifier as r, Identified as t };
|
|
34
|
+
//# sourceMappingURL=annotations-vzSzXtr-.d.mts.map
|