@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.
Files changed (179) hide show
  1. package/METRICS.md +16 -17
  2. package/README.md +127 -442
  3. package/dist/annotations-BE74YmAO.d.mts +39 -0
  4. package/dist/auth/index.d.mts +2 -0
  5. package/dist/{auth-DvU_CZ7C.js → auth/index.mjs} +6 -8
  6. package/dist/auth/index.mjs.map +1 -0
  7. package/dist/connector-app/index.d.mts +2 -0
  8. package/dist/connector-app/index.mjs +2 -0
  9. package/dist/connector-app-C-L4QqG6.mjs +123 -0
  10. package/dist/connector-app-C-L4QqG6.mjs.map +1 -0
  11. package/dist/{constants-D113Ut1x.js → constants-D-GQzpHZ.mjs} +4 -5
  12. package/dist/constants-D-GQzpHZ.mjs.map +1 -0
  13. package/dist/demo/index.d.mts +2 -0
  14. package/dist/demo/index.mjs +2 -0
  15. package/dist/demo-B30Vlkk2.mjs +191 -0
  16. package/dist/demo-B30Vlkk2.mjs.map +1 -0
  17. package/dist/errors/index.d.mts +2 -0
  18. package/dist/errors/index.mjs +2 -0
  19. package/dist/{errors-DHWylqRZ.js → errors-pT0YVJU5.mjs} +2 -3
  20. package/dist/errors-pT0YVJU5.mjs.map +1 -0
  21. package/dist/iceberg/index.d.mts +3 -0
  22. package/dist/iceberg/index.mjs +3 -0
  23. package/dist/iceberg-D6L8I6vA.mjs +39 -0
  24. package/dist/iceberg-D6L8I6vA.mjs.map +1 -0
  25. package/dist/{index-VpHaZXZw.d.ts → index-BUHSGm9Q.d.mts} +11 -13
  26. package/dist/{index-DXa26Ry5.d.ts → index-BX8G7d41.d.mts} +1 -2
  27. package/dist/{index-bfjH7Yb0.d.ts → index-BfXpgxk9.d.mts} +9 -10
  28. package/dist/{index-BKw7jA2a.d.ts → index-CRVAwJ5-.d.mts} +10 -8
  29. package/dist/index-CUPP2A_m.d.mts +27 -0
  30. package/dist/{index-DYpEMJf-.d.ts → index-CgvFomO6.d.mts} +6 -7
  31. package/dist/index-DJhDp4Xc.d.mts +58 -0
  32. package/dist/index-Dk7urvpF.d.mts +76 -0
  33. package/dist/{index-DKoRwyFn.d.ts → index-DkVNO6ks.d.mts} +2 -5
  34. package/dist/{index-C5Ma7V7t.d.ts → index-NnCutIqM.d.mts} +62 -37
  35. package/dist/{index-CR9HJDdr.d.ts → index-eFuoMZZ3.d.mts} +18 -20
  36. package/dist/{index-ZSuiZX5H.d.ts → index-ygpml2kC.d.mts} +19 -31
  37. package/dist/index.d.mts +16 -0
  38. package/dist/index.mjs +45 -0
  39. package/dist/index.mjs.map +1 -0
  40. package/dist/ingestion/index.d.mts +2 -0
  41. package/dist/ingestion/index.mjs +2 -0
  42. package/dist/ingestion-q0WwqUSt.mjs +275 -0
  43. package/dist/ingestion-q0WwqUSt.mjs.map +1 -0
  44. package/dist/ingestor/index.d.mts +3 -0
  45. package/dist/ingestor/index.mjs +4 -0
  46. package/dist/ingestor-D4L-HPkg.mjs +26 -0
  47. package/dist/ingestor-D4L-HPkg.mjs.map +1 -0
  48. package/dist/instrumented-Bhp7fyvh.mjs +29 -0
  49. package/dist/instrumented-Bhp7fyvh.mjs.map +1 -0
  50. package/dist/manifest/index.d.mts +2 -0
  51. package/dist/manifest/{index.js → index.mjs} +2 -4
  52. package/dist/{manifest-P7kBnch9.js → manifest-BGKxmKLy.mjs} +14 -19
  53. package/dist/manifest-BGKxmKLy.mjs.map +1 -0
  54. package/dist/{telemetry-j_nxwYfh.js → metrics-BaEX__MN.mjs} +170 -19
  55. package/dist/metrics-BaEX__MN.mjs.map +1 -0
  56. package/dist/{metrics-C4V1pm6v.d.ts → metrics-CVXFL7YT.d.mts} +18 -13
  57. package/dist/metrics.d.mts +2 -0
  58. package/dist/metrics.mjs +2 -0
  59. package/dist/{chunk-Bp6m_JJh.js → rolldown-runtime-D7D4PA-g.mjs} +4 -4
  60. package/dist/runtime-config/index.d.mts +2 -0
  61. package/dist/runtime-config/index.mjs +3 -0
  62. package/dist/runtime-config-Oa55gAew.mjs +97 -0
  63. package/dist/runtime-config-Oa55gAew.mjs.map +1 -0
  64. package/dist/server-CMnxz3h0.mjs +144 -0
  65. package/dist/server-CMnxz3h0.mjs.map +1 -0
  66. package/dist/service-DLkhuxYl.d.mts +27 -0
  67. package/dist/service-jWxxfEht.mjs +7 -0
  68. package/dist/service-jWxxfEht.mjs.map +1 -0
  69. package/dist/state-store/index.d.mts +2 -0
  70. package/dist/state-store/index.mjs +2 -0
  71. package/dist/{state-store-BdYulQOY.js → state-store-uFvHY7Ax.mjs} +15 -22
  72. package/dist/state-store-uFvHY7Ax.mjs.map +1 -0
  73. package/dist/status/{index.d.ts → index.d.mts} +1 -4
  74. package/dist/status/{index.js → index.mjs} +2 -6
  75. package/dist/{status-BTTd7AUR.js → status-CKb9IhQ9.mjs} +14 -16
  76. package/dist/status-CKb9IhQ9.mjs.map +1 -0
  77. package/dist/table-V0tgUC5i.mjs +370 -0
  78. package/dist/table-V0tgUC5i.mjs.map +1 -0
  79. package/dist/types-xZtSmrGA.d.mts +220 -0
  80. package/dist/webhook/index.d.mts +3 -0
  81. package/dist/webhook/index.mjs +12 -0
  82. package/dist/webhook/index.mjs.map +1 -0
  83. package/dist/wings-I56hGtNv.mjs +325 -0
  84. package/dist/wings-I56hGtNv.mjs.map +1 -0
  85. package/package.json +64 -51
  86. package/src/catalog/table.ts +201 -0
  87. package/src/connector-app/create-table.ts +47 -0
  88. package/src/connector-app/demo.ts +42 -0
  89. package/src/connector-app/index.ts +2 -0
  90. package/src/connector-app/start.ts +1 -1
  91. package/src/core/builder.ts +15 -23
  92. package/src/core/index.ts +11 -1
  93. package/src/core/types.ts +114 -39
  94. package/src/demo/definition.ts +68 -0
  95. package/src/demo/generate.ts +167 -0
  96. package/src/demo/index.ts +3 -0
  97. package/src/demo/run.ts +142 -0
  98. package/src/iceberg/annotations.ts +62 -0
  99. package/src/iceberg/error.ts +9 -0
  100. package/src/iceberg/index.ts +3 -0
  101. package/src/iceberg/table.ts +511 -0
  102. package/src/index.ts +3 -1
  103. package/src/ingestion/engine.ts +240 -60
  104. package/src/ingestor/arrow.ts +248 -0
  105. package/src/ingestor/console.ts +13 -0
  106. package/src/ingestor/index.ts +9 -0
  107. package/src/ingestor/instrumented.ts +61 -0
  108. package/src/ingestor/service.ts +30 -0
  109. package/src/ingestor/wings.ts +143 -0
  110. package/src/manifest/config.ts +3 -3
  111. package/src/metrics.ts +66 -20
  112. package/src/runtime-config/config.ts +6 -8
  113. package/src/runtime-config/constants.ts +2 -2
  114. package/src/runtime-config/index.ts +2 -7
  115. package/src/runtime-config/layer.ts +1 -1
  116. package/src/runtime-config/table-bindings.ts +48 -136
  117. package/src/state-store/layer-key-value-store.ts +8 -3
  118. package/src/state-store/layer-memory.ts +1 -1
  119. package/src/state-store/layer-sql.ts +4 -4
  120. package/src/state-store/schema.ts +2 -2
  121. package/src/state-store/state.ts +14 -2
  122. package/src/status/route.ts +2 -2
  123. package/src/telemetry.ts +10 -10
  124. package/src/webhook/server.ts +39 -40
  125. package/dist/auth/index.d.ts +0 -2
  126. package/dist/auth/index.js +0 -3
  127. package/dist/auth-DvU_CZ7C.js.map +0 -1
  128. package/dist/connector-app/index.d.ts +0 -14
  129. package/dist/connector-app/index.js +0 -12
  130. package/dist/connector-app-BljHd0Pe.js +0 -64
  131. package/dist/connector-app-BljHd0Pe.js.map +0 -1
  132. package/dist/constants-D113Ut1x.js.map +0 -1
  133. package/dist/errors/index.d.ts +0 -2
  134. package/dist/errors/index.js +0 -3
  135. package/dist/errors-DHWylqRZ.js.map +0 -1
  136. package/dist/index-BA8ASHKr.d.ts +0 -31
  137. package/dist/index.d.ts +0 -14
  138. package/dist/index.js +0 -60
  139. package/dist/index.js.map +0 -1
  140. package/dist/ingestion/index.d.ts +0 -6
  141. package/dist/ingestion/index.js +0 -11
  142. package/dist/ingestion-QBGKrFr9.js +0 -200
  143. package/dist/ingestion-QBGKrFr9.js.map +0 -1
  144. package/dist/manifest/index.d.ts +0 -2
  145. package/dist/manifest-P7kBnch9.js.map +0 -1
  146. package/dist/metrics-inkCAxKq.js +0 -136
  147. package/dist/metrics-inkCAxKq.js.map +0 -1
  148. package/dist/metrics.d.ts +0 -4
  149. package/dist/metrics.js +0 -5
  150. package/dist/publisher/index.d.ts +0 -6
  151. package/dist/publisher/index.js +0 -8
  152. package/dist/publisher-BdWJOfer.js +0 -138
  153. package/dist/publisher-BdWJOfer.js.map +0 -1
  154. package/dist/runtime-config/index.d.ts +0 -4
  155. package/dist/runtime-config/index.js +0 -5
  156. package/dist/runtime-config-IbyCVpei.js +0 -60
  157. package/dist/runtime-config-IbyCVpei.js.map +0 -1
  158. package/dist/server-D-eLX3yN.js +0 -183
  159. package/dist/server-D-eLX3yN.js.map +0 -1
  160. package/dist/service-CUjGhe-7.d.ts +0 -32
  161. package/dist/service-DiP-XETo.js +0 -8
  162. package/dist/service-DiP-XETo.js.map +0 -1
  163. package/dist/state-store/index.d.ts +0 -3
  164. package/dist/state-store/index.js +0 -5
  165. package/dist/state-store-BdYulQOY.js.map +0 -1
  166. package/dist/status-BTTd7AUR.js.map +0 -1
  167. package/dist/table-bindings-Dp1DhTeS.js +0 -106
  168. package/dist/table-bindings-Dp1DhTeS.js.map +0 -1
  169. package/dist/telemetry-j_nxwYfh.js.map +0 -1
  170. package/dist/types-jVD5Ys43.d.ts +0 -174
  171. package/dist/webhook/index.d.ts +0 -7
  172. package/dist/webhook/index.js +0 -9
  173. package/dist/webhook-MhFcLW-o.js +0 -13
  174. package/dist/webhook-MhFcLW-o.js.map +0 -1
  175. package/src/publisher/console.ts +0 -18
  176. package/src/publisher/index.ts +0 -7
  177. package/src/publisher/instrumented.ts +0 -75
  178. package/src/publisher/service.ts +0 -37
  179. 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,2 +1,4 @@
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";
@@ -1,6 +1,6 @@
1
1
  import { NodeHttpServer } from "@effect/platform-node";
2
2
  import { DateTime, Effect } from "effect";
3
- import { HttpRouter } from "effect/unstable/http";
3
+ import { HttpRouter } from "effect/http";
4
4
  import { createServer } from "node:http";
5
5
 
6
6
  import type { ConnectorDefinition } from "../core";
@@ -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
- ResourceMutation,
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: <const Resources extends ReadonlyArray<ResourceDefinition>>(
19
- definition: ConnectorDefinition<Resources>,
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" || Number.isNaN(Date.parse(value))) {
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
- ResourceMutation,
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/unstable/http";
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
- export type DeleteValue = string | number | boolean | Date;
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 mutations: ReadonlyArray<ResourceMutation<Row>>;
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" | "publish" | "checkpoint";
49
- readonly code: "fetch_failed" | "publish_failed" | "checkpoint_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
- export type ResourceSchema = Schema.Decoder<object>;
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 key: ResourceField<typeof ProductSchema> = "id";
76
+ * const version: ResourceField<typeof ProductSchema> = "updatedAt";
83
77
  * ```
84
78
  */
85
- export type ResourceField<S extends ResourceSchema> = keyof ResourceRow<S> & string;
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 mutations: ReadonlyArray<ResourceMutation<Row>>;
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 mutations: ReadonlyArray<ResourceMutation<Row>>;
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
- readonly schema: S;
130
- readonly key: ResourceField<NoInfer<S>>;
131
- readonly version: ResourceField<NoInfer<S>>;
132
- readonly partition?: {
133
- readonly required: boolean;
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<ResourceRow<NoInfer<S>>, R>;
139
- readonly webhook?: WebhookHandler<ResourceRow<NoInfer<S>>, Payload>;
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> ? Payload : never;
215
+ R extends ResourceDefinition<ResourceSchema, infer Payload, unknown, string, string>
216
+ ? Payload
217
+ : never;
187
218
 
188
- export type WebhookAckMode = "after-enqueue" | "after-publish";
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
- /** Decodes and collects mutations for a resource-owned webhook handler. */
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
  };