@useairfoil/connector-kit 0.6.0 → 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 +16 -17
- package/README.md +127 -442
- 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-D113Ut1x.js → constants-D-GQzpHZ.mjs} +4 -5
- 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-VpHaZXZw.d.ts → index-BUHSGm9Q.d.mts} +11 -13
- package/dist/{index-DXa26Ry5.d.ts → index-BX8G7d41.d.mts} +1 -2
- package/dist/{index-bfjH7Yb0.d.ts → index-BfXpgxk9.d.mts} +9 -10
- package/dist/{index-BKw7jA2a.d.ts → index-CRVAwJ5-.d.mts} +10 -8
- package/dist/index-CUPP2A_m.d.mts +27 -0
- package/dist/{index-DYpEMJf-.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-DKoRwyFn.d.ts → index-DkVNO6ks.d.mts} +2 -5
- package/dist/{index-C5Ma7V7t.d.ts → index-NnCutIqM.d.mts} +62 -37
- package/dist/{index-CR9HJDdr.d.ts → index-eFuoMZZ3.d.mts} +18 -20
- package/dist/{index-ZSuiZX5H.d.ts → index-ygpml2kC.d.mts} +19 -31
- 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-P7kBnch9.js → manifest-BGKxmKLy.mjs} +14 -19
- package/dist/manifest-BGKxmKLy.mjs.map +1 -0
- package/dist/{telemetry-j_nxwYfh.js → metrics-BaEX__MN.mjs} +170 -19
- package/dist/metrics-BaEX__MN.mjs.map +1 -0
- package/dist/{metrics-C4V1pm6v.d.ts → metrics-CVXFL7YT.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-Oa55gAew.mjs +97 -0
- package/dist/runtime-config-Oa55gAew.mjs.map +1 -0
- package/dist/server-CMnxz3h0.mjs +144 -0
- 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-BdYulQOY.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-BTTd7AUR.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 +66 -20
- package/src/runtime-config/config.ts +6 -8
- package/src/runtime-config/constants.ts +2 -2
- package/src/runtime-config/index.ts +2 -7
- package/src/runtime-config/layer.ts +1 -1
- package/src/runtime-config/table-bindings.ts +48 -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 +10 -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 -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
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IcebergType,
|
|
3
|
+
StructField,
|
|
4
|
+
TableIdentifier,
|
|
5
|
+
TableMetadata,
|
|
6
|
+
TableSchema,
|
|
7
|
+
} from "iceberg-js";
|
|
8
|
+
|
|
9
|
+
import * as Wings from "@useairfoil/wings";
|
|
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, makeCommitRequest } from "../iceberg/table";
|
|
17
|
+
|
|
18
|
+
type Catalog = Effect.Success<
|
|
19
|
+
ReturnType<Wings.CatalogManager.CatalogManagerService["getIcebergCatalog"]>
|
|
20
|
+
>;
|
|
21
|
+
|
|
22
|
+
type TableOptions = {
|
|
23
|
+
readonly catalog: Catalog;
|
|
24
|
+
readonly identifier: TableIdentifier;
|
|
25
|
+
readonly location?: string | undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type WingsTableOptions = {
|
|
29
|
+
readonly wingsUri: string;
|
|
30
|
+
readonly catalogId: string;
|
|
31
|
+
readonly identifier: TableIdentifier;
|
|
32
|
+
readonly location?: string | undefined;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const tableError = (cause: unknown): ConnectorError => {
|
|
36
|
+
if (cause instanceof ConnectorError) return cause;
|
|
37
|
+
|
|
38
|
+
const message = cause instanceof Error ? cause.message : "Failed to prepare Iceberg table";
|
|
39
|
+
return new ConnectorError({
|
|
40
|
+
message: cause instanceof IcebergSchemaError ? `${cause.path}: ${message}` : message,
|
|
41
|
+
cause,
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const compareTypes = (
|
|
46
|
+
expected: IcebergType,
|
|
47
|
+
actual: IcebergType,
|
|
48
|
+
path: string,
|
|
49
|
+
): string | undefined => {
|
|
50
|
+
if (typeof expected === "string" || typeof actual === "string") {
|
|
51
|
+
return expected === actual ? undefined : path;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (expected.type !== actual.type) return path;
|
|
55
|
+
|
|
56
|
+
switch (expected.type) {
|
|
57
|
+
case "struct":
|
|
58
|
+
if (actual.type !== "struct") return path;
|
|
59
|
+
return compareFields(expected.fields, actual.fields, path);
|
|
60
|
+
case "list":
|
|
61
|
+
if (
|
|
62
|
+
actual.type !== "list" ||
|
|
63
|
+
expected["element-id"] !== actual["element-id"] ||
|
|
64
|
+
expected["element-required"] !== actual["element-required"]
|
|
65
|
+
) {
|
|
66
|
+
return path;
|
|
67
|
+
}
|
|
68
|
+
return compareTypes(expected.element, actual.element, `${path}.element`);
|
|
69
|
+
case "map":
|
|
70
|
+
if (
|
|
71
|
+
actual.type !== "map" ||
|
|
72
|
+
expected["key-id"] !== actual["key-id"] ||
|
|
73
|
+
expected["value-id"] !== actual["value-id"] ||
|
|
74
|
+
expected["value-required"] !== actual["value-required"]
|
|
75
|
+
) {
|
|
76
|
+
return path;
|
|
77
|
+
}
|
|
78
|
+
return (
|
|
79
|
+
compareTypes(expected.key, actual.key, `${path}.key`) ??
|
|
80
|
+
compareTypes(expected.value, actual.value, `${path}.value`)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const compareFields = (
|
|
86
|
+
expected: ReadonlyArray<StructField>,
|
|
87
|
+
actual: ReadonlyArray<StructField>,
|
|
88
|
+
path: string,
|
|
89
|
+
): string | undefined => {
|
|
90
|
+
if (expected.length !== actual.length) return path;
|
|
91
|
+
|
|
92
|
+
for (const field of expected) {
|
|
93
|
+
const fieldPath = `${path}.${field.name}`;
|
|
94
|
+
const other = actual.find((candidate) => candidate.name === field.name);
|
|
95
|
+
|
|
96
|
+
if (!other || field.id !== other.id || field.required !== other.required) return fieldPath;
|
|
97
|
+
|
|
98
|
+
const mismatch = compareTypes(field.type, other.type, fieldPath);
|
|
99
|
+
if (mismatch) return mismatch;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const verifyTable = (
|
|
104
|
+
metadata: TableMetadata,
|
|
105
|
+
expected: TableSchema,
|
|
106
|
+
): Effect.Effect<TableSchema, ConnectorError> =>
|
|
107
|
+
Effect.try({
|
|
108
|
+
try: () => {
|
|
109
|
+
if (!Array.isArray(metadata.schemas)) throw new Error("Invalid table metadata");
|
|
110
|
+
const actual = getCurrentSchema(metadata);
|
|
111
|
+
if (!actual) throw new Error("Table has no current schema");
|
|
112
|
+
|
|
113
|
+
const path = compareFields(expected.fields, actual.fields, "$");
|
|
114
|
+
if (path) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Table schema does not match at ${path}. Migrate the table or revert the schema change.`,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const expectedIds = new Set(expected["identifier-field-ids"]);
|
|
121
|
+
const actualIds = new Set(actual["identifier-field-ids"]);
|
|
122
|
+
if (
|
|
123
|
+
expectedIds.size !== actualIds.size ||
|
|
124
|
+
[...expectedIds].some((id) => !actualIds.has(id))
|
|
125
|
+
) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
"Table identifier fields do not match. Recreate the table or revert the schema change.",
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return actual;
|
|
132
|
+
},
|
|
133
|
+
catch: (cause) =>
|
|
134
|
+
new ConnectorError({
|
|
135
|
+
message: cause instanceof Error ? cause.message : "Invalid table metadata",
|
|
136
|
+
cause,
|
|
137
|
+
}),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
/** Creates a table with the IDs from its Effect schema, then checks the loaded schema. */
|
|
141
|
+
export const createTable = (
|
|
142
|
+
schema: Schema.Top,
|
|
143
|
+
options: TableOptions,
|
|
144
|
+
): Effect.Effect<TableSchema, ConnectorError> =>
|
|
145
|
+
Effect.gen(function* () {
|
|
146
|
+
const table = yield* compileTable(schema);
|
|
147
|
+
yield* options.catalog.commitTable(
|
|
148
|
+
options.identifier,
|
|
149
|
+
makeCommitRequest(table, { location: options.location }),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const metadata = yield* options.catalog.loadTable(options.identifier);
|
|
153
|
+
return yield* verifyTable(metadata, table.schema);
|
|
154
|
+
}).pipe(Effect.mapError(tableError));
|
|
155
|
+
|
|
156
|
+
/** Creates a table through a Wings catalog. */
|
|
157
|
+
export const createTableFromWings = (
|
|
158
|
+
schema: Schema.Top,
|
|
159
|
+
options: WingsTableOptions,
|
|
160
|
+
): Effect.Effect<TableSchema, ConnectorError, HttpClient.HttpClient> =>
|
|
161
|
+
Effect.scoped(
|
|
162
|
+
Effect.gen(function* () {
|
|
163
|
+
const manager = yield* Wings.CatalogManager.make({ baseUrl: options.wingsUri });
|
|
164
|
+
const catalog = yield* manager.getIcebergCatalog(options.catalogId);
|
|
165
|
+
return yield* createTable(schema, {
|
|
166
|
+
catalog,
|
|
167
|
+
identifier: options.identifier,
|
|
168
|
+
location: options.location,
|
|
169
|
+
});
|
|
170
|
+
}),
|
|
171
|
+
).pipe(Effect.mapError(tableError));
|
|
172
|
+
|
|
173
|
+
/** Loads a table, creating it when it is missing. */
|
|
174
|
+
export const ensureTable = (
|
|
175
|
+
schema: Schema.Top,
|
|
176
|
+
options: TableOptions,
|
|
177
|
+
): Effect.Effect<TableSchema, ConnectorError> =>
|
|
178
|
+
Effect.gen(function* () {
|
|
179
|
+
const table = yield* compileTable(schema);
|
|
180
|
+
const metadata = yield* options.catalog.loadTable(options.identifier).pipe(
|
|
181
|
+
Effect.catchIf(
|
|
182
|
+
(error) => error.status === 404,
|
|
183
|
+
() =>
|
|
184
|
+
options.catalog
|
|
185
|
+
.commitTable(
|
|
186
|
+
options.identifier,
|
|
187
|
+
makeCommitRequest(table, { location: options.location }),
|
|
188
|
+
)
|
|
189
|
+
.pipe(
|
|
190
|
+
// Another instance may have created the table. Reload and check it.
|
|
191
|
+
Effect.catchIf(
|
|
192
|
+
(error) => error.status === 409 || error.status === 412,
|
|
193
|
+
() => Effect.void,
|
|
194
|
+
),
|
|
195
|
+
Effect.flatMap(() => options.catalog.loadTable(options.identifier)),
|
|
196
|
+
),
|
|
197
|
+
),
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
return yield* verifyTable(metadata, table.schema);
|
|
201
|
+
}).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 { createTableFromWings } from "../catalog/table";
|
|
5
|
+
import { wingsUri } from "../runtime-config/config";
|
|
6
|
+
|
|
7
|
+
/** Creates the table command for a 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* wingsUri;
|
|
39
|
+
yield* createTableFromWings(schema, {
|
|
40
|
+
wingsUri: uri.href,
|
|
41
|
+
catalogId: catalog,
|
|
42
|
+
identifier: { namespace, name },
|
|
43
|
+
location: Option.getOrUndefined(location),
|
|
44
|
+
});
|
|
45
|
+
yield* Effect.log(`Created ${table}`);
|
|
46
|
+
}),
|
|
47
|
+
).pipe(Command.withDescription("Create an Iceberg table for a connector resource"));
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { ByteSize, DateTime, Effect, Option } from "effect";
|
|
2
|
+
import { Command, Flag } from "effect/cli";
|
|
3
|
+
|
|
4
|
+
import { run, type DemoDefinition } from "../demo";
|
|
5
|
+
import { layerWingsConfig } from "../ingestor/wings";
|
|
6
|
+
|
|
7
|
+
/** Reads sizes such as `512KiB`, `128MiB` or `1GiB`. */
|
|
8
|
+
export const parseSize = (input: string): Option.Option<number> => {
|
|
9
|
+
if (!/^\d+(KiB|MiB|GiB)$/.test(input)) return Option.none();
|
|
10
|
+
|
|
11
|
+
return ByteSize.fromString(input).pipe(
|
|
12
|
+
Option.flatMap(ByteSize.toNumber),
|
|
13
|
+
Option.filter((bytes) => bytes > 0),
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates the command that writes the demo data to Wings, then exits.
|
|
19
|
+
* It reads Wings and table bindings from Config, so provide `RuntimeConfig.layerHosted()`.
|
|
20
|
+
*/
|
|
21
|
+
export const makeDemoCommand = (demo: DemoDefinition) =>
|
|
22
|
+
Command.make(
|
|
23
|
+
"demo",
|
|
24
|
+
{
|
|
25
|
+
size: Flag.String("size").pipe(
|
|
26
|
+
Flag.filterMap(parseSize, () => "a size like 512KiB, 128MiB or 1GiB"),
|
|
27
|
+
),
|
|
28
|
+
seed: Flag.Int("seed").pipe(Flag.withDefault(1)),
|
|
29
|
+
},
|
|
30
|
+
({ size, seed }) =>
|
|
31
|
+
Effect.gen(function* () {
|
|
32
|
+
const now = yield* DateTime.nowAsDate;
|
|
33
|
+
|
|
34
|
+
yield* run(demo, { bytes: size, seed, now }).pipe(
|
|
35
|
+
Effect.provide(
|
|
36
|
+
layerWingsConfig({
|
|
37
|
+
resources: [...demo.setup.tables, ...demo.group.tables],
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
}),
|
|
42
|
+
).pipe(Command.withDescription("Write demo data for the bound tables, then exit"));
|
|
@@ -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";
|
package/src/core/builder.ts
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
|
-
import { DateTime, Effect } from "effect";
|
|
1
|
+
import { DateTime, Effect, Option } from "effect";
|
|
2
2
|
|
|
3
3
|
import type {
|
|
4
|
+
ChangesFeed,
|
|
4
5
|
ChangesFetch,
|
|
5
6
|
ConnectorDefinition,
|
|
6
7
|
Cursor as CursorType,
|
|
7
|
-
DeleteValue,
|
|
8
8
|
PageFetch,
|
|
9
9
|
ResourceDefinition,
|
|
10
|
-
|
|
10
|
+
ResourceRequiredField,
|
|
11
11
|
ResourceSchema,
|
|
12
|
-
WebhookHandler,
|
|
13
12
|
} from "./types";
|
|
14
13
|
|
|
15
14
|
import { ConnectorError } from "../errors";
|
|
16
15
|
|
|
17
16
|
export const Connector = {
|
|
18
|
-
define: <
|
|
19
|
-
|
|
17
|
+
define: <
|
|
18
|
+
const Resources extends ReadonlyArray<ResourceDefinition>,
|
|
19
|
+
const FeedResources extends ReadonlyArray<Resources[number]> = Resources,
|
|
20
|
+
>(
|
|
21
|
+
definition: ConnectorDefinition<Resources, FeedResources>,
|
|
20
22
|
) => definition,
|
|
21
23
|
};
|
|
22
24
|
|
|
@@ -26,28 +28,18 @@ export const Resource = {
|
|
|
26
28
|
Payload = never,
|
|
27
29
|
R = never,
|
|
28
30
|
const Name extends string = string,
|
|
31
|
+
const Version extends ResourceRequiredField<S> = ResourceRequiredField<S>,
|
|
29
32
|
>(
|
|
30
|
-
definition: ResourceDefinition<S, Payload, R, Name>,
|
|
31
|
-
): ResourceDefinition<S, Payload, R, Name> => definition,
|
|
32
|
-
|
|
33
|
-
webhook: <Payload, Row extends object = never>(definition: WebhookHandler<Row, Payload>) =>
|
|
34
|
-
definition,
|
|
35
|
-
|
|
36
|
-
upsert: <Row extends object>(row: Row): ResourceMutation<Row> => ({ op: "upsert", row }),
|
|
37
|
-
|
|
38
|
-
delete: (options: {
|
|
39
|
-
readonly key: DeleteValue;
|
|
40
|
-
readonly version: DeleteValue;
|
|
41
|
-
}): ResourceMutation<never> => ({
|
|
42
|
-
op: "delete",
|
|
43
|
-
key: options.key,
|
|
44
|
-
version: options.version,
|
|
45
|
-
}),
|
|
33
|
+
definition: ResourceDefinition<S, Payload, R, Name, Version>,
|
|
34
|
+
): ResourceDefinition<S, Payload, R, Name, Version> => definition,
|
|
46
35
|
};
|
|
47
36
|
|
|
48
37
|
export const Fetch = {
|
|
49
38
|
page: <Row extends object = never, R = never>(definition: PageFetch<Row, R>) => definition,
|
|
50
39
|
changes: <Row extends object = never, R = never>(definition: ChangesFetch<Row, R>) => definition,
|
|
40
|
+
feed: <const Resources extends ReadonlyArray<ResourceDefinition>, R = never>(
|
|
41
|
+
definition: ChangesFeed<Resources, R>,
|
|
42
|
+
) => definition,
|
|
51
43
|
};
|
|
52
44
|
|
|
53
45
|
const decodeFailure = (kind: string, value: unknown) =>
|
|
@@ -73,7 +65,7 @@ export const Cursor = {
|
|
|
73
65
|
isoDateTime: (): CursorType.Definition<string> => ({
|
|
74
66
|
kind: "isoDateTime",
|
|
75
67
|
decode: (value) => {
|
|
76
|
-
if (typeof value !== "string" ||
|
|
68
|
+
if (typeof value !== "string" || Option.isNone(DateTime.make(value))) {
|
|
77
69
|
return Effect.fail(decodeFailure("ISO datetime", value));
|
|
78
70
|
}
|
|
79
71
|
return Effect.succeed(value);
|
package/src/core/index.ts
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
export { Connector, Cursor, Fetch, Resource } from "./builder";
|
|
2
|
+
export type { Identified } from "../iceberg/annotations";
|
|
2
3
|
export type {
|
|
4
|
+
ChangesFeed,
|
|
3
5
|
ChangesFetch,
|
|
4
6
|
ConnectorDefinition,
|
|
5
7
|
Cursor as CursorTypes,
|
|
8
|
+
FeedRows,
|
|
6
9
|
FetchChangesResult,
|
|
10
|
+
FetchFeedResult,
|
|
7
11
|
FetchPageResult,
|
|
8
12
|
PageFetch,
|
|
9
13
|
ResourceBatch,
|
|
10
14
|
ResourceDefinition,
|
|
15
|
+
ResourceDelete,
|
|
11
16
|
ResourceField,
|
|
12
|
-
|
|
17
|
+
ResourceKey,
|
|
13
18
|
ResourcePayload,
|
|
19
|
+
ResourceRequiredField,
|
|
14
20
|
ResourceRow,
|
|
15
21
|
ResourceRows,
|
|
16
22
|
ResourceSchema,
|
|
17
23
|
ResourceState,
|
|
24
|
+
ResourceUpdate,
|
|
25
|
+
ResourceUpdateOf,
|
|
26
|
+
TableResource,
|
|
27
|
+
TableSource,
|
|
18
28
|
WebhookAckMode,
|
|
19
29
|
WebhookHandler,
|
|
20
30
|
WebhookRoute,
|
package/src/core/types.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Duration, Effect, Schema } from "effect";
|
|
2
|
-
import type { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/
|
|
2
|
+
import type { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
3
3
|
|
|
4
4
|
import type { ConnectorError } from "../errors";
|
|
5
|
+
import type { Identified, RequiredField } from "../iceberg/annotations";
|
|
5
6
|
|
|
6
7
|
export namespace Cursor {
|
|
7
8
|
export type Value = string | number | Date;
|
|
@@ -14,22 +15,10 @@ export namespace Cursor {
|
|
|
14
15
|
};
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
export type ResourceMutation<Row extends object = object> =
|
|
20
|
-
| {
|
|
21
|
-
readonly op: "upsert";
|
|
22
|
-
readonly row: Row;
|
|
23
|
-
}
|
|
24
|
-
| {
|
|
25
|
-
readonly op: "delete";
|
|
26
|
-
readonly key: DeleteValue;
|
|
27
|
-
readonly version: DeleteValue;
|
|
28
|
-
};
|
|
29
|
-
|
|
18
|
+
/** Rows and an optional cursor from one resource. */
|
|
30
19
|
export type ResourceBatch<Row extends object = object> = {
|
|
31
20
|
readonly cursor?: Cursor.Value;
|
|
32
|
-
readonly
|
|
21
|
+
readonly rows: ReadonlyArray<Row>;
|
|
33
22
|
};
|
|
34
23
|
|
|
35
24
|
export type ResourceState = {
|
|
@@ -45,8 +34,8 @@ export type ResourceState = {
|
|
|
45
34
|
};
|
|
46
35
|
readonly lastError?: {
|
|
47
36
|
readonly source: "backfill" | "changes";
|
|
48
|
-
readonly operation: "fetch" | "
|
|
49
|
-
readonly code: "fetch_failed" | "
|
|
37
|
+
readonly operation: "fetch" | "ingest" | "checkpoint";
|
|
38
|
+
readonly code: "fetch_failed" | "ingest_failed" | "checkpoint_failed";
|
|
50
39
|
readonly message: string;
|
|
51
40
|
readonly at: string;
|
|
52
41
|
};
|
|
@@ -54,14 +43,17 @@ export type ResourceState = {
|
|
|
54
43
|
|
|
55
44
|
export type SyncState = "pending" | "backfilling" | "live" | "error";
|
|
56
45
|
|
|
57
|
-
|
|
46
|
+
/** A row schema with its key, set by `Iceberg.identifier`. */
|
|
47
|
+
export type ResourceSchema = Schema.Decoder<object> & Identified<string>;
|
|
58
48
|
|
|
59
49
|
/**
|
|
60
50
|
* Takes a resource schema and returns its decoded row object type.
|
|
61
51
|
*
|
|
62
52
|
* @example
|
|
63
53
|
* ```ts
|
|
64
|
-
* const ProductSchema = Schema.Struct({ id: Schema.String, updatedAt: Schema.String })
|
|
54
|
+
* const ProductSchema = Schema.Struct({ id: Schema.String, updatedAt: Schema.String }).pipe(
|
|
55
|
+
* Iceberg.identifier("id"),
|
|
56
|
+
* );
|
|
65
57
|
* type Product = ResourceRow<typeof ProductSchema>;
|
|
66
58
|
* // Result:
|
|
67
59
|
* // type Product = { readonly id: string; readonly updatedAt: string }
|
|
@@ -74,18 +66,43 @@ export type ResourceRow<S extends ResourceSchema> = Schema.Schema.Type<S>;
|
|
|
74
66
|
*
|
|
75
67
|
* @example
|
|
76
68
|
* ```ts
|
|
77
|
-
* const ProductSchema = Schema.Struct({ id: Schema.String, updatedAt: Schema.String })
|
|
69
|
+
* const ProductSchema = Schema.Struct({ id: Schema.String, updatedAt: Schema.String }).pipe(
|
|
70
|
+
* Iceberg.identifier("id"),
|
|
71
|
+
* );
|
|
78
72
|
* type ProductField = ResourceField<typeof ProductSchema>;
|
|
79
73
|
* // Result:
|
|
80
74
|
* // type ProductField = "id" | "updatedAt"
|
|
81
75
|
*
|
|
82
|
-
* const
|
|
76
|
+
* const version: ResourceField<typeof ProductSchema> = "updatedAt";
|
|
83
77
|
* ```
|
|
84
78
|
*/
|
|
85
|
-
|
|
79
|
+
type KnownResourceField<S extends ResourceSchema> = keyof ResourceRow<S> & string;
|
|
80
|
+
|
|
81
|
+
export type ResourceField<S extends ResourceSchema> = [KnownResourceField<S>] extends [never]
|
|
82
|
+
? string
|
|
83
|
+
: KnownResourceField<S>;
|
|
84
|
+
|
|
85
|
+
/** A row field that cannot be null or undefined. */
|
|
86
|
+
export type ResourceRequiredField<S extends ResourceSchema> = RequiredField<S>;
|
|
87
|
+
|
|
88
|
+
/** The fields that identify a row, from `Iceberg.identifier`. */
|
|
89
|
+
export type ResourceKey<S extends ResourceSchema> = S extends Identified<infer Key> ? Key : never;
|
|
90
|
+
|
|
91
|
+
/** Deletes a row: its key, version, and `_deleted: true`. */
|
|
92
|
+
export type ResourceDelete<S extends ResourceSchema, Version extends ResourceField<S>> = Pick<
|
|
93
|
+
ResourceRow<S>,
|
|
94
|
+
Extract<ResourceKey<S> | Version, keyof ResourceRow<S>>
|
|
95
|
+
> & {
|
|
96
|
+
readonly _deleted: true;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** A full row, or a delete. Wings replaces the whole row. */
|
|
100
|
+
export type ResourceUpdate<S extends ResourceSchema, Version extends ResourceField<S>> =
|
|
101
|
+
| (ResourceRow<S> & { readonly _deleted?: boolean })
|
|
102
|
+
| ResourceDelete<S, Version>;
|
|
86
103
|
|
|
87
104
|
export type FetchPageResult<Row extends object> = {
|
|
88
|
-
readonly
|
|
105
|
+
readonly rows: ReadonlyArray<Row>;
|
|
89
106
|
readonly nextPageCursor?: Cursor.Value;
|
|
90
107
|
readonly hasMore: boolean;
|
|
91
108
|
};
|
|
@@ -100,8 +117,10 @@ export type PageFetch<Row extends object, R = never> = {
|
|
|
100
117
|
};
|
|
101
118
|
|
|
102
119
|
export type FetchChangesResult<Row extends object> = {
|
|
103
|
-
readonly
|
|
120
|
+
readonly rows: ReadonlyArray<Row>;
|
|
104
121
|
readonly cursor: Cursor.Value;
|
|
122
|
+
/** When true, the engine runs the changes again without waiting for the interval. */
|
|
123
|
+
readonly hasMore?: boolean;
|
|
105
124
|
};
|
|
106
125
|
|
|
107
126
|
export type ChangesFetch<Row extends object, R = never> = {
|
|
@@ -113,10 +132,9 @@ export type ChangesFetch<Row extends object, R = never> = {
|
|
|
113
132
|
};
|
|
114
133
|
|
|
115
134
|
export type WebhookHandler<Row extends object, Payload> = {
|
|
135
|
+
/** Types the payload. The route decodes it before `to`. */
|
|
116
136
|
readonly schema: Schema.Decoder<Payload>;
|
|
117
|
-
handler(input: {
|
|
118
|
-
readonly payload: Payload;
|
|
119
|
-
}): Effect.Effect<ReadonlyArray<ResourceMutation<Row>>, ConnectorError>;
|
|
137
|
+
handler(input: { readonly payload: Payload }): Effect.Effect<ReadonlyArray<Row>, ConnectorError>;
|
|
120
138
|
};
|
|
121
139
|
|
|
122
140
|
export type ResourceDefinition<
|
|
@@ -124,19 +142,28 @@ export type ResourceDefinition<
|
|
|
124
142
|
Payload = unknown,
|
|
125
143
|
R = never,
|
|
126
144
|
Name extends string = string,
|
|
145
|
+
Version extends ResourceRequiredField<S> = ResourceRequiredField<S>,
|
|
127
146
|
> = {
|
|
128
147
|
readonly name: Name;
|
|
129
|
-
|
|
130
|
-
readonly
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
148
|
+
/** Types the rows, and names the key. The Iceberg table decides how they are encoded. */
|
|
149
|
+
readonly rowSchema: S;
|
|
150
|
+
|
|
151
|
+
/** Orders updates for the entity. */
|
|
152
|
+
readonly version: Version;
|
|
153
|
+
|
|
135
154
|
/** Read-only validation used before provisioning this resource. */
|
|
136
155
|
readonly check: Effect.Effect<void, ConnectorError>;
|
|
137
156
|
readonly backfill?: PageFetch<ResourceRow<NoInfer<S>>, R>;
|
|
138
|
-
readonly changes?: ChangesFetch<
|
|
139
|
-
readonly webhook?: WebhookHandler<
|
|
157
|
+
readonly changes?: ChangesFetch<ResourceUpdate<NoInfer<S>, NoInfer<Version>>, R>;
|
|
158
|
+
readonly webhook?: WebhookHandler<ResourceUpdate<NoInfer<S>, NoInfer<Version>>, Payload>;
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** The parts of a resource its table needs. */
|
|
162
|
+
export type TableResource = Pick<ResourceDefinition, "name" | "rowSchema" | "version">;
|
|
163
|
+
|
|
164
|
+
/** Resources that write to tables, such as a connector or a demo. */
|
|
165
|
+
export type TableSource = {
|
|
166
|
+
readonly resources: ReadonlyArray<TableResource>;
|
|
140
167
|
};
|
|
141
168
|
|
|
142
169
|
/**
|
|
@@ -161,7 +188,9 @@ export type ResourceRows<Resources extends ReadonlyArray<ResourceDefinition>> =
|
|
|
161
188
|
readonly [Resource in Resources[number] as Resource["name"]]: Resource extends ResourceDefinition<
|
|
162
189
|
infer S,
|
|
163
190
|
unknown,
|
|
164
|
-
unknown
|
|
191
|
+
unknown,
|
|
192
|
+
string,
|
|
193
|
+
infer _Version
|
|
165
194
|
>
|
|
166
195
|
? ResourceRow<S>
|
|
167
196
|
: never;
|
|
@@ -183,9 +212,11 @@ export type ResourceName<Resources extends ReadonlyArray<ResourceDefinition>> =
|
|
|
183
212
|
* ```
|
|
184
213
|
*/
|
|
185
214
|
export type ResourcePayload<R> =
|
|
186
|
-
R extends ResourceDefinition<ResourceSchema, infer Payload, unknown
|
|
215
|
+
R extends ResourceDefinition<ResourceSchema, infer Payload, unknown, string, string>
|
|
216
|
+
? Payload
|
|
217
|
+
: never;
|
|
187
218
|
|
|
188
|
-
export type WebhookAckMode = "after-enqueue" | "after-
|
|
219
|
+
export type WebhookAckMode = "after-enqueue" | "after-ingest";
|
|
189
220
|
|
|
190
221
|
export type WebhookRouteContext<
|
|
191
222
|
Resources extends ReadonlyArray<ResourceDefinition> = ReadonlyArray<ResourceDefinition>,
|
|
@@ -194,7 +225,7 @@ export type WebhookRouteContext<
|
|
|
194
225
|
readonly request: HttpServerRequest.HttpServerRequest;
|
|
195
226
|
readonly rawBody: Uint8Array;
|
|
196
227
|
readonly payload: Payload;
|
|
197
|
-
/**
|
|
228
|
+
/** Runs a resource's webhook handler on a decoded payload and collects its rows. */
|
|
198
229
|
readonly to: <Resource extends Resources[number]>(
|
|
199
230
|
resource: Resource,
|
|
200
231
|
payload: ResourcePayload<Resource>,
|
|
@@ -218,11 +249,55 @@ export type WebhookRouteInput<
|
|
|
218
249
|
Payload,
|
|
219
250
|
> = WebhookRoute<Resources, Payload>;
|
|
220
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Takes a resource definition and returns the rows its changes and webhooks may
|
|
254
|
+
* return: full rows or deletes.
|
|
255
|
+
*/
|
|
256
|
+
export type ResourceUpdateOf<R> =
|
|
257
|
+
R extends ResourceDefinition<infer S, unknown, unknown, string, infer Version>
|
|
258
|
+
? ResourceUpdate<S, Version>
|
|
259
|
+
: never;
|
|
260
|
+
|
|
261
|
+
/** Rows from one feed run, by resource name. A missing resource has no rows. */
|
|
262
|
+
export type FeedRows<Resources extends ReadonlyArray<ResourceDefinition>> = {
|
|
263
|
+
readonly [Resource in Resources[number] as Resource["name"]]?: ReadonlyArray<
|
|
264
|
+
ResourceUpdateOf<Resource>
|
|
265
|
+
>;
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
export type FetchFeedResult<Resources extends ReadonlyArray<ResourceDefinition>> = {
|
|
269
|
+
readonly rows: FeedRows<Resources>;
|
|
270
|
+
readonly cursor: Cursor.Value;
|
|
271
|
+
/** When true, the engine runs the feed again without waiting for the interval. */
|
|
272
|
+
readonly hasMore?: boolean;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* One changes source for several resources, with one cursor. Use it when one
|
|
277
|
+
* provider log, such as an events API, covers them all.
|
|
278
|
+
*/
|
|
279
|
+
export type ChangesFeed<
|
|
280
|
+
Resources extends ReadonlyArray<ResourceDefinition> = ReadonlyArray<ResourceDefinition>,
|
|
281
|
+
R = never,
|
|
282
|
+
> = {
|
|
283
|
+
readonly resources: Resources;
|
|
284
|
+
readonly cursor: Cursor.Definition;
|
|
285
|
+
readonly interval?: Duration.Input;
|
|
286
|
+
readonly fetch: (input: {
|
|
287
|
+
readonly cursor: Cursor.Value;
|
|
288
|
+
/** The resources that run. Skip the work for the others. */
|
|
289
|
+
readonly selected: ReadonlySet<string>;
|
|
290
|
+
}) => Effect.Effect<FetchFeedResult<Resources>, ConnectorError, R>;
|
|
291
|
+
};
|
|
292
|
+
|
|
221
293
|
export type ConnectorDefinition<
|
|
222
294
|
Resources extends ReadonlyArray<ResourceDefinition> = ReadonlyArray<ResourceDefinition>,
|
|
295
|
+
FeedResources extends ReadonlyArray<ResourceDefinition> = ReadonlyArray<ResourceDefinition>,
|
|
223
296
|
> = {
|
|
224
297
|
readonly name: string;
|
|
225
298
|
readonly title?: string;
|
|
226
299
|
readonly resources: Resources;
|
|
300
|
+
/** Shared changes for resources that do not define their own `changes`. */
|
|
301
|
+
readonly changes?: ChangesFeed<FeedResources>;
|
|
227
302
|
readonly webhooks?: ReadonlyArray<WebhookRoute>;
|
|
228
303
|
};
|