@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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@useairfoil/connector-kit",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"repository": "useairfoil/
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"repository": "useairfoil/connector-kit",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
7
7
|
"src",
|
|
@@ -9,96 +9,110 @@
|
|
|
9
9
|
"README.md"
|
|
10
10
|
],
|
|
11
11
|
"type": "module",
|
|
12
|
-
"main": "./dist/index.
|
|
13
|
-
"types": "./dist/index.d.
|
|
12
|
+
"main": "./dist/index.mjs",
|
|
13
|
+
"types": "./dist/index.d.mts",
|
|
14
14
|
"exports": {
|
|
15
15
|
".": {
|
|
16
|
-
"types": "./dist/index.d.
|
|
17
|
-
"import": "./dist/index.
|
|
18
|
-
"default": "./dist/index.
|
|
16
|
+
"types": "./dist/index.d.mts",
|
|
17
|
+
"import": "./dist/index.mjs",
|
|
18
|
+
"default": "./dist/index.mjs"
|
|
19
19
|
},
|
|
20
20
|
"./auth": {
|
|
21
|
-
"types": "./dist/auth/index.d.
|
|
22
|
-
"import": "./dist/auth/index.
|
|
23
|
-
"default": "./dist/auth/index.
|
|
21
|
+
"types": "./dist/auth/index.d.mts",
|
|
22
|
+
"import": "./dist/auth/index.mjs",
|
|
23
|
+
"default": "./dist/auth/index.mjs"
|
|
24
24
|
},
|
|
25
25
|
"./connector-app": {
|
|
26
|
-
"types": "./dist/connector-app/index.d.
|
|
27
|
-
"import": "./dist/connector-app/index.
|
|
28
|
-
"default": "./dist/connector-app/index.
|
|
26
|
+
"types": "./dist/connector-app/index.d.mts",
|
|
27
|
+
"import": "./dist/connector-app/index.mjs",
|
|
28
|
+
"default": "./dist/connector-app/index.mjs"
|
|
29
|
+
},
|
|
30
|
+
"./demo": {
|
|
31
|
+
"types": "./dist/demo/index.d.mts",
|
|
32
|
+
"import": "./dist/demo/index.mjs",
|
|
33
|
+
"default": "./dist/demo/index.mjs"
|
|
34
|
+
},
|
|
35
|
+
"./iceberg": {
|
|
36
|
+
"types": "./dist/iceberg/index.d.mts",
|
|
37
|
+
"import": "./dist/iceberg/index.mjs",
|
|
38
|
+
"default": "./dist/iceberg/index.mjs"
|
|
29
39
|
},
|
|
30
40
|
"./ingestion": {
|
|
31
|
-
"types": "./dist/ingestion/index.d.
|
|
32
|
-
"import": "./dist/ingestion/index.
|
|
33
|
-
"default": "./dist/ingestion/index.
|
|
41
|
+
"types": "./dist/ingestion/index.d.mts",
|
|
42
|
+
"import": "./dist/ingestion/index.mjs",
|
|
43
|
+
"default": "./dist/ingestion/index.mjs"
|
|
44
|
+
},
|
|
45
|
+
"./ingestor": {
|
|
46
|
+
"types": "./dist/ingestor/index.d.mts",
|
|
47
|
+
"import": "./dist/ingestor/index.mjs",
|
|
48
|
+
"default": "./dist/ingestor/index.mjs"
|
|
34
49
|
},
|
|
35
50
|
"./manifest": {
|
|
36
|
-
"types": "./dist/manifest/index.d.
|
|
37
|
-
"import": "./dist/manifest/index.
|
|
38
|
-
"default": "./dist/manifest/index.
|
|
51
|
+
"types": "./dist/manifest/index.d.mts",
|
|
52
|
+
"import": "./dist/manifest/index.mjs",
|
|
53
|
+
"default": "./dist/manifest/index.mjs"
|
|
39
54
|
},
|
|
40
55
|
"./metrics": {
|
|
41
|
-
"types": "./dist/metrics.d.
|
|
42
|
-
"import": "./dist/metrics.
|
|
43
|
-
"default": "./dist/metrics.
|
|
44
|
-
},
|
|
45
|
-
"./publisher": {
|
|
46
|
-
"types": "./dist/publisher/index.d.ts",
|
|
47
|
-
"import": "./dist/publisher/index.js",
|
|
48
|
-
"default": "./dist/publisher/index.js"
|
|
56
|
+
"types": "./dist/metrics.d.mts",
|
|
57
|
+
"import": "./dist/metrics.mjs",
|
|
58
|
+
"default": "./dist/metrics.mjs"
|
|
49
59
|
},
|
|
50
60
|
"./runtime-config": {
|
|
51
|
-
"types": "./dist/runtime-config/index.d.
|
|
52
|
-
"import": "./dist/runtime-config/index.
|
|
53
|
-
"default": "./dist/runtime-config/index.
|
|
61
|
+
"types": "./dist/runtime-config/index.d.mts",
|
|
62
|
+
"import": "./dist/runtime-config/index.mjs",
|
|
63
|
+
"default": "./dist/runtime-config/index.mjs"
|
|
54
64
|
},
|
|
55
65
|
"./state-store": {
|
|
56
|
-
"types": "./dist/state-store/index.d.
|
|
57
|
-
"import": "./dist/state-store/index.
|
|
58
|
-
"default": "./dist/state-store/index.
|
|
66
|
+
"types": "./dist/state-store/index.d.mts",
|
|
67
|
+
"import": "./dist/state-store/index.mjs",
|
|
68
|
+
"default": "./dist/state-store/index.mjs"
|
|
59
69
|
},
|
|
60
70
|
"./status": {
|
|
61
|
-
"types": "./dist/status/index.d.
|
|
62
|
-
"import": "./dist/status/index.
|
|
63
|
-
"default": "./dist/status/index.
|
|
71
|
+
"types": "./dist/status/index.d.mts",
|
|
72
|
+
"import": "./dist/status/index.mjs",
|
|
73
|
+
"default": "./dist/status/index.mjs"
|
|
64
74
|
},
|
|
65
75
|
"./webhook": {
|
|
66
|
-
"types": "./dist/webhook/index.d.
|
|
67
|
-
"import": "./dist/webhook/index.
|
|
68
|
-
"default": "./dist/webhook/index.
|
|
76
|
+
"types": "./dist/webhook/index.d.mts",
|
|
77
|
+
"import": "./dist/webhook/index.mjs",
|
|
78
|
+
"default": "./dist/webhook/index.mjs"
|
|
69
79
|
},
|
|
70
80
|
"./errors": {
|
|
71
|
-
"types": "./dist/errors/index.d.
|
|
72
|
-
"import": "./dist/errors/index.
|
|
73
|
-
"default": "./dist/errors/index.
|
|
81
|
+
"types": "./dist/errors/index.d.mts",
|
|
82
|
+
"import": "./dist/errors/index.mjs",
|
|
83
|
+
"default": "./dist/errors/index.mjs"
|
|
74
84
|
}
|
|
75
85
|
},
|
|
76
|
-
"scripts": {
|
|
77
|
-
"build": "tsdown",
|
|
78
|
-
"test": "vitest",
|
|
79
|
-
"test:ci": "vitest run",
|
|
80
|
-
"typecheck": "tsc --noEmit"
|
|
81
|
-
},
|
|
82
86
|
"dependencies": {
|
|
83
|
-
"@effect/platform-node": "^4.
|
|
84
|
-
"@
|
|
85
|
-
"
|
|
87
|
+
"@effect/platform-node": "^4.1.0",
|
|
88
|
+
"@faker-js/faker": "10.6.0",
|
|
89
|
+
"apache-arrow": "^21.2.0",
|
|
90
|
+
"human-format": "^1.4.0",
|
|
91
|
+
"iceberg-js": "^1.0.0",
|
|
92
|
+
"@useairfoil/api": "0.5.0"
|
|
86
93
|
},
|
|
87
94
|
"devDependencies": {
|
|
88
|
-
"@effect/sql-pg": "^4.
|
|
89
|
-
"@effect/vitest": "^4.
|
|
95
|
+
"@effect/sql-pg": "^4.1.0",
|
|
96
|
+
"@effect/vitest": "^4.1.0",
|
|
90
97
|
"@types/node": "^24.2.0",
|
|
91
|
-
"effect": "^4.
|
|
98
|
+
"effect": "^4.1.0",
|
|
92
99
|
"testcontainers": "^11.14.0",
|
|
93
|
-
"tsdown": "^0.
|
|
94
|
-
"vitest": "^
|
|
100
|
+
"tsdown": "^0.23.0",
|
|
101
|
+
"vitest": "^5.0.3",
|
|
102
|
+
"@useairfoil/testing": "0.2.0"
|
|
95
103
|
},
|
|
96
104
|
"peerDependencies": {
|
|
97
|
-
"effect": "^4.
|
|
105
|
+
"effect": "^4.1.0"
|
|
98
106
|
},
|
|
99
107
|
"peerDependenciesMeta": {
|
|
100
108
|
"effect": {
|
|
101
109
|
"optional": false
|
|
102
110
|
}
|
|
111
|
+
},
|
|
112
|
+
"scripts": {
|
|
113
|
+
"build": "tsdown",
|
|
114
|
+
"test": "vitest",
|
|
115
|
+
"test:ci": "vitest run",
|
|
116
|
+
"typecheck": "tsc --noEmit"
|
|
103
117
|
}
|
|
104
|
-
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Effect, Schedule, type Duration } from "effect";
|
|
2
|
+
|
|
3
|
+
export type TableRetryOptions = {
|
|
4
|
+
readonly interval: Duration.Input;
|
|
5
|
+
/** Retries after the initial attempt. */
|
|
6
|
+
readonly times: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** Retries transient catalog responses at the request boundary. */
|
|
10
|
+
export const retryCatalogRequest = <A, E extends { readonly status?: number }, R>(
|
|
11
|
+
request: Effect.Effect<A, E, R>,
|
|
12
|
+
options?: TableRetryOptions,
|
|
13
|
+
) =>
|
|
14
|
+
options === undefined
|
|
15
|
+
? request
|
|
16
|
+
: request.pipe(
|
|
17
|
+
Effect.retry({
|
|
18
|
+
schedule: Schedule.spaced(options.interval),
|
|
19
|
+
times: options.times,
|
|
20
|
+
while: (error) =>
|
|
21
|
+
error.status === 408 ||
|
|
22
|
+
error.status === 429 ||
|
|
23
|
+
(error.status !== undefined && error.status >= 500 && error.status <= 599),
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IcebergType,
|
|
3
|
+
StructField,
|
|
4
|
+
TableIdentifier,
|
|
5
|
+
TableMetadata,
|
|
6
|
+
TableSchema,
|
|
7
|
+
} from "iceberg-js";
|
|
8
|
+
|
|
9
|
+
import { CatalogManager } from "@useairfoil/api";
|
|
10
|
+
import { Effect, type Schema } from "effect";
|
|
11
|
+
import { HttpClient } from "effect/http";
|
|
12
|
+
import { getCurrentSchema } from "iceberg-js";
|
|
13
|
+
|
|
14
|
+
import { ConnectorError } from "../errors";
|
|
15
|
+
import { IcebergSchemaError } from "../iceberg/error";
|
|
16
|
+
import { compileTable, makeCreateRequest } from "../iceberg/table";
|
|
17
|
+
import { retryCatalogRequest, type TableRetryOptions } from "./retry";
|
|
18
|
+
|
|
19
|
+
type Catalog = Effect.Success<
|
|
20
|
+
ReturnType<CatalogManager.CatalogManagerService["getIcebergCatalog"]>
|
|
21
|
+
>;
|
|
22
|
+
|
|
23
|
+
type TableOptions = {
|
|
24
|
+
readonly retry?: TableRetryOptions;
|
|
25
|
+
readonly catalog: Pick<Catalog, "loadTable" | "createTable" | "createNamespaceIfNotExists">;
|
|
26
|
+
readonly identifier: TableIdentifier;
|
|
27
|
+
readonly location?: string | undefined;
|
|
28
|
+
readonly properties?: Readonly<Record<string, string>> | undefined;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type EngineTableOptions = {
|
|
32
|
+
readonly engineUri: string;
|
|
33
|
+
readonly catalogId: string;
|
|
34
|
+
readonly identifier: TableIdentifier;
|
|
35
|
+
readonly location?: string | undefined;
|
|
36
|
+
readonly properties?: Readonly<Record<string, string>> | undefined;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const tableError = (cause: unknown): ConnectorError => {
|
|
40
|
+
if (cause instanceof ConnectorError) return cause;
|
|
41
|
+
|
|
42
|
+
const message = cause instanceof Error ? cause.message : "Failed to prepare Iceberg table";
|
|
43
|
+
return new ConnectorError({
|
|
44
|
+
message: cause instanceof IcebergSchemaError ? `${cause.path}: ${message}` : message,
|
|
45
|
+
cause,
|
|
46
|
+
});
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const compareTypes = (
|
|
50
|
+
expected: IcebergType,
|
|
51
|
+
actual: IcebergType,
|
|
52
|
+
path: string,
|
|
53
|
+
): string | undefined => {
|
|
54
|
+
if (typeof expected === "string" || typeof actual === "string") {
|
|
55
|
+
return expected === actual ? undefined : path;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (expected.type !== actual.type) return path;
|
|
59
|
+
|
|
60
|
+
switch (expected.type) {
|
|
61
|
+
case "struct":
|
|
62
|
+
if (actual.type !== "struct") return path;
|
|
63
|
+
return compareFields(expected.fields, actual.fields, path);
|
|
64
|
+
case "list":
|
|
65
|
+
if (actual.type !== "list" || expected["element-required"] !== actual["element-required"]) {
|
|
66
|
+
return path;
|
|
67
|
+
}
|
|
68
|
+
return compareTypes(expected.element, actual.element, `${path}.element`);
|
|
69
|
+
case "map":
|
|
70
|
+
if (actual.type !== "map" || expected["value-required"] !== actual["value-required"]) {
|
|
71
|
+
return path;
|
|
72
|
+
}
|
|
73
|
+
return (
|
|
74
|
+
compareTypes(expected.key, actual.key, `${path}.key`) ??
|
|
75
|
+
compareTypes(expected.value, actual.value, `${path}.value`)
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const compareFields = (
|
|
81
|
+
expected: ReadonlyArray<StructField>,
|
|
82
|
+
actual: ReadonlyArray<StructField>,
|
|
83
|
+
path: string,
|
|
84
|
+
): string | undefined => {
|
|
85
|
+
if (expected.length !== actual.length) return path;
|
|
86
|
+
|
|
87
|
+
for (const field of expected) {
|
|
88
|
+
const fieldPath = `${path}.${field.name}`;
|
|
89
|
+
const other = actual.find((candidate) => candidate.name === field.name);
|
|
90
|
+
|
|
91
|
+
if (!other || field.required !== other.required) return fieldPath;
|
|
92
|
+
|
|
93
|
+
const mismatch = compareTypes(field.type, other.type, fieldPath);
|
|
94
|
+
if (mismatch) return mismatch;
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const verifyTable = (
|
|
99
|
+
metadata: TableMetadata,
|
|
100
|
+
expected: TableSchema,
|
|
101
|
+
): Effect.Effect<TableSchema, ConnectorError> =>
|
|
102
|
+
Effect.try({
|
|
103
|
+
try: () => {
|
|
104
|
+
if (!Array.isArray(metadata.schemas)) throw new Error("Invalid table metadata");
|
|
105
|
+
if (metadata["format-version"] !== 3) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
"Storage-engine tables must use Iceberg format v3. Migrate the table before ingesting.",
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const partitionSpec = metadata["partition-specs"].find(
|
|
111
|
+
(spec) => spec["spec-id"] === metadata["default-spec-id"],
|
|
112
|
+
);
|
|
113
|
+
if (!partitionSpec) throw new Error("Table has no default partition spec");
|
|
114
|
+
if (partitionSpec.fields.length > 0) {
|
|
115
|
+
throw new Error("Storage-engine tables must be unpartitioned.");
|
|
116
|
+
}
|
|
117
|
+
if (metadata.properties?.["encryption.key-id"] !== undefined) {
|
|
118
|
+
throw new Error("Storage-engine tables must be unencrypted.");
|
|
119
|
+
}
|
|
120
|
+
const actual = getCurrentSchema(metadata);
|
|
121
|
+
if (!actual) throw new Error("Table has no current schema");
|
|
122
|
+
|
|
123
|
+
const path = compareFields(expected.fields, actual.fields, "$");
|
|
124
|
+
if (path) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`Table schema does not match at ${path}. Migrate the table or revert the schema change.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const identifierNames = (schema: TableSchema) =>
|
|
131
|
+
new Set(
|
|
132
|
+
schema.fields
|
|
133
|
+
.filter((field) => schema["identifier-field-ids"]?.includes(field.id))
|
|
134
|
+
.map((field) => field.name),
|
|
135
|
+
);
|
|
136
|
+
const expectedNames = identifierNames(expected);
|
|
137
|
+
const actualNames = identifierNames(actual);
|
|
138
|
+
if (
|
|
139
|
+
expectedNames.size !== actualNames.size ||
|
|
140
|
+
[...expectedNames].some((name) => !actualNames.has(name))
|
|
141
|
+
) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
"Table identifier fields do not match. Recreate the table or revert the schema change.",
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return actual;
|
|
148
|
+
},
|
|
149
|
+
catch: (cause) =>
|
|
150
|
+
new ConnectorError({
|
|
151
|
+
message: cause instanceof Error ? cause.message : "Invalid table metadata",
|
|
152
|
+
cause,
|
|
153
|
+
}),
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
/** Creates a table through the REST catalog, then checks the loaded schema. */
|
|
157
|
+
export const createTable = (
|
|
158
|
+
schema: Schema.Top,
|
|
159
|
+
options: TableOptions,
|
|
160
|
+
): Effect.Effect<TableSchema, ConnectorError> =>
|
|
161
|
+
Effect.gen(function* () {
|
|
162
|
+
const table = yield* compileTable(schema);
|
|
163
|
+
yield* options.catalog.createTable(
|
|
164
|
+
{ namespace: options.identifier.namespace },
|
|
165
|
+
makeCreateRequest(table, {
|
|
166
|
+
name: options.identifier.name,
|
|
167
|
+
location: options.location,
|
|
168
|
+
properties: options.properties,
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const metadata = yield* options.catalog.loadTable(options.identifier);
|
|
173
|
+
return yield* verifyTable(metadata, table.schema);
|
|
174
|
+
}).pipe(Effect.mapError(tableError));
|
|
175
|
+
|
|
176
|
+
/** Creates a missing table through an engine catalog and verifies its schema. */
|
|
177
|
+
export const ensureTableFromEngine = (
|
|
178
|
+
schema: Schema.Top,
|
|
179
|
+
options: EngineTableOptions,
|
|
180
|
+
): Effect.Effect<TableSchema, ConnectorError, HttpClient.HttpClient> =>
|
|
181
|
+
Effect.scoped(
|
|
182
|
+
Effect.gen(function* () {
|
|
183
|
+
const manager = yield* CatalogManager.make({ engineUri: options.engineUri });
|
|
184
|
+
const catalog = yield* manager.getIcebergCatalog(options.catalogId);
|
|
185
|
+
return yield* ensureTable(schema, {
|
|
186
|
+
catalog,
|
|
187
|
+
identifier: options.identifier,
|
|
188
|
+
location: options.location,
|
|
189
|
+
properties: options.properties,
|
|
190
|
+
});
|
|
191
|
+
}),
|
|
192
|
+
).pipe(Effect.mapError(tableError));
|
|
193
|
+
|
|
194
|
+
/** Loads a table, creating it when it is missing. */
|
|
195
|
+
export const ensureTable = (
|
|
196
|
+
schema: Schema.Top,
|
|
197
|
+
options: TableOptions,
|
|
198
|
+
): Effect.Effect<TableSchema, ConnectorError> =>
|
|
199
|
+
Effect.gen(function* () {
|
|
200
|
+
const table = yield* compileTable(schema);
|
|
201
|
+
const request = <A>(
|
|
202
|
+
effect: Effect.Effect<A, Effect.Error<ReturnType<Catalog["loadConfig"]>>>,
|
|
203
|
+
) => retryCatalogRequest(effect, options.retry);
|
|
204
|
+
const metadata = yield* request(options.catalog.loadTable(options.identifier)).pipe(
|
|
205
|
+
Effect.catchIf(
|
|
206
|
+
(error) => error.status === 404,
|
|
207
|
+
() =>
|
|
208
|
+
request(
|
|
209
|
+
options.catalog.createNamespaceIfNotExists({ namespace: options.identifier.namespace }),
|
|
210
|
+
)
|
|
211
|
+
.pipe(
|
|
212
|
+
Effect.andThen(
|
|
213
|
+
request(
|
|
214
|
+
options.catalog.createTable(
|
|
215
|
+
{ namespace: options.identifier.namespace },
|
|
216
|
+
makeCreateRequest(table, {
|
|
217
|
+
name: options.identifier.name,
|
|
218
|
+
location: options.location,
|
|
219
|
+
properties: options.properties,
|
|
220
|
+
}),
|
|
221
|
+
),
|
|
222
|
+
),
|
|
223
|
+
),
|
|
224
|
+
)
|
|
225
|
+
.pipe(
|
|
226
|
+
// Another instance may have created the table. Reload and check it.
|
|
227
|
+
Effect.catchIf(
|
|
228
|
+
(error) => error.status === 409 || error.status === 412,
|
|
229
|
+
() => Effect.void,
|
|
230
|
+
),
|
|
231
|
+
Effect.flatMap(() => request(options.catalog.loadTable(options.identifier))),
|
|
232
|
+
),
|
|
233
|
+
),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
return yield* verifyTable(metadata, table.schema);
|
|
237
|
+
}).pipe(Effect.mapError(tableError));
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Effect, Option, type Schema } from "effect";
|
|
2
|
+
import { Argument, CliError, Command, Flag } from "effect/cli";
|
|
3
|
+
|
|
4
|
+
import { ensureTableFromEngine } from "../catalog/table";
|
|
5
|
+
import { engineUri } from "../runtime-config/config";
|
|
6
|
+
|
|
7
|
+
/** Creates the table command; existing tables must match the connector's row schemas. */
|
|
8
|
+
export const makeCreateTableCommand = (schemas: Readonly<Record<string, Schema.Top>>) =>
|
|
9
|
+
Command.make(
|
|
10
|
+
"create-table",
|
|
11
|
+
{
|
|
12
|
+
table: Argument.String("table"),
|
|
13
|
+
catalog: Flag.String("catalog"),
|
|
14
|
+
location: Flag.String("location").pipe(Flag.optional),
|
|
15
|
+
resource: Flag.String("resource").pipe(Flag.optional),
|
|
16
|
+
},
|
|
17
|
+
({ table, catalog, location, resource }) =>
|
|
18
|
+
Effect.gen(function* () {
|
|
19
|
+
const namespace = table.split(".");
|
|
20
|
+
const name = namespace.pop();
|
|
21
|
+
|
|
22
|
+
if (!name || namespace.length === 0 || namespace.some((part) => part.length === 0)) {
|
|
23
|
+
return yield* new CliError.UserError({
|
|
24
|
+
cause: "Invalid table name",
|
|
25
|
+
userMessage: "Use a table name like my.namespace.table",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const resourceName = Option.getOrElse(resource, () => name);
|
|
30
|
+
const schema = Object.hasOwn(schemas, resourceName) ? schemas[resourceName] : undefined;
|
|
31
|
+
if (!schema) {
|
|
32
|
+
return yield* new CliError.UserError({
|
|
33
|
+
cause: "Unknown resource",
|
|
34
|
+
userMessage: `Unknown resource: ${resourceName}`,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const uri = yield* engineUri;
|
|
39
|
+
yield* ensureTableFromEngine(schema, {
|
|
40
|
+
engineUri: uri.href,
|
|
41
|
+
catalogId: catalog,
|
|
42
|
+
identifier: { namespace, name },
|
|
43
|
+
location: Option.getOrUndefined(location),
|
|
44
|
+
});
|
|
45
|
+
yield* Effect.log(`Prepared ${table}`);
|
|
46
|
+
}),
|
|
47
|
+
).pipe(Command.withDescription("Create or verify an Iceberg table for a connector resource"));
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { DateTime, Effect, Option, Schema } from "effect";
|
|
2
|
+
import { Command, Flag } from "effect/cli";
|
|
3
|
+
import humanFormat from "human-format";
|
|
4
|
+
|
|
5
|
+
import { run, type DemoDefinition } from "../demo";
|
|
6
|
+
import { layerArrowFlightConfig } from "../ingestor/arrow-flight";
|
|
7
|
+
|
|
8
|
+
/** Reads byte sizes such as `512kB`, `128MB` or `1.5GB`. */
|
|
9
|
+
export const parseSize = (input: string): Option.Option<number> =>
|
|
10
|
+
Option.liftThrowable(() => humanFormat.parse.bytes(input))().pipe(
|
|
11
|
+
Option.filter((bytes) => Number.isSafeInteger(bytes) && bytes > 0),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Creates the command that writes demo data over Arrow Flight, then exits.
|
|
16
|
+
* It reads hosted config and table bindings, so provide `RuntimeConfig.layerHosted()`.
|
|
17
|
+
*/
|
|
18
|
+
export const makeDemoCommand = (demo: DemoDefinition) =>
|
|
19
|
+
Command.make(
|
|
20
|
+
"demo",
|
|
21
|
+
{
|
|
22
|
+
size: Flag.String("size").pipe(
|
|
23
|
+
Flag.filterMap(parseSize, () => "a size like 512kB, 128MB or 1.5GB"),
|
|
24
|
+
),
|
|
25
|
+
seed: Flag.Int("seed").pipe(Flag.optional),
|
|
26
|
+
batchSize: Flag.Int("batch-size").pipe(
|
|
27
|
+
Flag.withSchema(Schema.Int.check(Schema.isGreaterThan(0))),
|
|
28
|
+
Flag.withDescription("Maximum rows per ingestion batch (default: 500)"),
|
|
29
|
+
Flag.optional,
|
|
30
|
+
),
|
|
31
|
+
},
|
|
32
|
+
({ size, seed, batchSize }) =>
|
|
33
|
+
Effect.gen(function* () {
|
|
34
|
+
const now = yield* DateTime.nowAsDate;
|
|
35
|
+
|
|
36
|
+
yield* run(demo, {
|
|
37
|
+
bytes: size,
|
|
38
|
+
seed: Option.getOrUndefined(seed),
|
|
39
|
+
batchSize: Option.getOrUndefined(batchSize),
|
|
40
|
+
now,
|
|
41
|
+
}).pipe(
|
|
42
|
+
Effect.provide(
|
|
43
|
+
layerArrowFlightConfig({ resources: [...demo.setup.tables, ...demo.group.tables] }),
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
}),
|
|
47
|
+
).pipe(Command.withDescription("Write demo data for the bound tables, then exit"));
|
|
@@ -1,2 +1,5 @@
|
|
|
1
1
|
export { check, type CheckOptions, type CheckResult, type ResourceCheckResult } from "./check";
|
|
2
|
+
export { makeCreateTableCommand } from "./create-table";
|
|
3
|
+
export { makeDemoCommand, parseSize } from "./demo";
|
|
2
4
|
export { start, type App, type StartOptions } from "./start";
|
|
5
|
+
export { makeSetupCommand } from "./setup";
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { CatalogManager } from "@useairfoil/api";
|
|
2
|
+
import { Duration, Effect, Option, Schema } from "effect";
|
|
3
|
+
import { Command, Flag } from "effect/cli";
|
|
4
|
+
|
|
5
|
+
import { retryCatalogRequest } from "../catalog/retry";
|
|
6
|
+
import { ensureTable } from "../catalog/table";
|
|
7
|
+
import * as RuntimeConfig from "../runtime-config";
|
|
8
|
+
|
|
9
|
+
/** Creates or verifies all bound tables without loading provider credentials. */
|
|
10
|
+
export const makeSetupCommand = (schemas: Readonly<Record<string, Schema.Top>>) =>
|
|
11
|
+
Command.make(
|
|
12
|
+
"setup",
|
|
13
|
+
{
|
|
14
|
+
checkpointIntervalSec: Flag.Int("checkpoint-interval-sec").pipe(
|
|
15
|
+
Flag.withSchema(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 900 }))),
|
|
16
|
+
Flag.withDescription(
|
|
17
|
+
"Checkpoint interval for new tables in seconds (0 disables the interval delay)",
|
|
18
|
+
),
|
|
19
|
+
Flag.optional,
|
|
20
|
+
),
|
|
21
|
+
retryInterval: Flag.String("retry-interval").pipe(
|
|
22
|
+
Flag.withDefault("5 seconds"),
|
|
23
|
+
Flag.withSchema(Schema.DurationFromString),
|
|
24
|
+
Flag.filter(
|
|
25
|
+
(duration) => Duration.isPositive(duration) && Duration.isFinite(duration),
|
|
26
|
+
() => "a positive duration, such as 5 seconds",
|
|
27
|
+
),
|
|
28
|
+
Flag.withDescription("Delay between transient catalog retries"),
|
|
29
|
+
),
|
|
30
|
+
retries: Flag.Int("retries").pipe(
|
|
31
|
+
Flag.withDefault(10),
|
|
32
|
+
Flag.filter(
|
|
33
|
+
(value) => value >= 0,
|
|
34
|
+
() => "a non-negative number of retries",
|
|
35
|
+
),
|
|
36
|
+
Flag.withDescription("Maximum retries per catalog request after the initial attempt"),
|
|
37
|
+
),
|
|
38
|
+
},
|
|
39
|
+
({ retryInterval, retries, checkpointIntervalSec }) =>
|
|
40
|
+
Effect.gen(function* () {
|
|
41
|
+
const source = {
|
|
42
|
+
resources: Object.entries(schemas).map(([name, rowSchema]) => ({ name, rowSchema })),
|
|
43
|
+
};
|
|
44
|
+
const tables = yield* RuntimeConfig.loadTableBindings(source);
|
|
45
|
+
const uri = yield* RuntimeConfig.engineUri;
|
|
46
|
+
const catalogId = yield* RuntimeConfig.catalog;
|
|
47
|
+
const manager = yield* CatalogManager.make({ engineUri: uri.href });
|
|
48
|
+
const retry = { interval: retryInterval, times: retries };
|
|
49
|
+
const properties = Option.match(checkpointIntervalSec, {
|
|
50
|
+
onNone: () => undefined,
|
|
51
|
+
onSome: (seconds) => ({ "airfoil.checkpoint.interval-sec": String(seconds) }),
|
|
52
|
+
});
|
|
53
|
+
const catalog = yield* retryCatalogRequest(manager.getIcebergCatalog(catalogId), retry);
|
|
54
|
+
|
|
55
|
+
// Sequential setup avoids bursting the catalog's table-creation quota.
|
|
56
|
+
for (const resource of source.resources) {
|
|
57
|
+
const binding = tables[resource.name];
|
|
58
|
+
if (!binding) continue;
|
|
59
|
+
yield* ensureTable(resource.rowSchema, {
|
|
60
|
+
catalog,
|
|
61
|
+
identifier: binding,
|
|
62
|
+
location: binding.location,
|
|
63
|
+
retry,
|
|
64
|
+
properties,
|
|
65
|
+
});
|
|
66
|
+
yield* Effect.logInfo(`Prepared ${[...binding.namespace, binding.name].join(".")}`);
|
|
67
|
+
}
|
|
68
|
+
}),
|
|
69
|
+
).pipe(Command.withDescription("Create or verify the Iceberg tables in AIRFOIL_TABLE_BINDINGS"));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NodeHttpServer } from "@effect/platform-node";
|
|
2
2
|
import { DateTime, Effect } from "effect";
|
|
3
|
-
import { HttpRouter } from "effect/
|
|
3
|
+
import { HttpRouter } from "effect/http";
|
|
4
4
|
import { createServer } from "node:http";
|
|
5
5
|
|
|
6
6
|
import type { ConnectorDefinition } from "../core";
|