@useairfoil/connector-kit 0.4.0 → 0.5.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/README.md +283 -90
- package/dist/chunk-Bp6m_JJh.js +13 -0
- package/dist/connector-app/index.d.ts +9 -0
- package/dist/connector-app/index.js +8 -0
- package/dist/connector-app-CvqbfN6R.js +32 -0
- package/dist/connector-app-CvqbfN6R.js.map +1 -0
- package/dist/errors/index.d.ts +2 -0
- package/dist/errors/index.js +3 -0
- package/dist/errors-DHWylqRZ.js +8 -0
- package/dist/errors-DHWylqRZ.js.map +1 -0
- package/dist/index-8KnVpoZp.d.ts +32 -0
- package/dist/index-BlhJCpEk.d.ts +124 -0
- package/dist/index-CQJsFJs3.d.ts +23 -0
- package/dist/index-DK9YcR6W.d.ts +25 -0
- package/dist/index-DaNW63Mv.d.ts +21 -0
- package/dist/index-DyuT2jzP.d.ts +14 -0
- package/dist/index.d.ts +9 -165
- package/dist/index.js +44 -228
- package/dist/index.js.map +1 -1
- package/dist/ingestion/index.d.ts +6 -0
- package/dist/ingestion/index.js +7 -0
- package/dist/ingestion-CMCroK7Y.js +171 -0
- package/dist/ingestion-CMCroK7Y.js.map +1 -0
- package/dist/publisher/index.d.ts +5 -0
- package/dist/publisher/index.js +5 -0
- package/dist/publisher-DusgYvaV.js +131 -0
- package/dist/publisher-DusgYvaV.js.map +1 -0
- package/dist/server-DAeFWT4o.js +113 -0
- package/dist/server-DAeFWT4o.js.map +1 -0
- package/dist/service-B7g_lHMI.d.ts +32 -0
- package/dist/state-store/index.d.ts +4 -0
- package/dist/state-store/index.js +3 -0
- package/dist/state-store-DG_4QpWG.js +31 -0
- package/dist/state-store-DG_4QpWG.js.map +1 -0
- package/dist/telemetry-p0dE5Mdu.js +171 -0
- package/dist/telemetry-p0dE5Mdu.js.map +1 -0
- package/dist/types-ZNrA-Dn0.d.ts +160 -0
- package/dist/webhook/index.d.ts +5 -0
- package/dist/webhook/index.js +6 -0
- package/dist/webhook-BSA-tXtU.js +13 -0
- package/dist/webhook-BSA-tXtU.js.map +1 -0
- package/package.json +36 -7
- package/src/connector-app/index.ts +1 -0
- package/src/connector-app/start.ts +38 -0
- package/src/core/builder.ts +76 -9
- package/src/core/index.ts +22 -0
- package/src/core/types.ts +185 -75
- package/src/errors/index.ts +1 -0
- package/src/index.ts +8 -28
- package/src/ingestion/engine.ts +301 -179
- package/src/ingestion/index.ts +1 -0
- package/src/publisher/console.ts +18 -0
- package/src/publisher/index.ts +7 -0
- package/src/publisher/service.ts +30 -13
- package/src/publisher/wings.ts +159 -72
- package/src/runtime/context.ts +15 -0
- package/src/state-store/index.ts +2 -0
- package/src/state-store/memory.ts +25 -0
- package/src/state-store/service.ts +18 -0
- package/src/telemetry.ts +246 -0
- package/src/webhook/index.ts +8 -0
- package/src/webhook/server.ts +172 -37
- package/src/webhook/types.ts +6 -17
- package/src/ingestion/state-store.ts +0 -36
- package/src/streams/pull-stream.ts +0 -54
- package/src/streams/webhook-queue.ts +0 -14
- /package/src/{core/errors.ts → errors/connector-error.ts} +0 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { t as __export } from "./chunk-Bp6m_JJh.js";
|
|
2
|
+
import { Cause, Config, Context, Effect, Layer, Option } from "effect";
|
|
3
|
+
import { Headers } from "effect/unstable/http";
|
|
4
|
+
import * as Observability from "effect/unstable/observability";
|
|
5
|
+
|
|
6
|
+
//#region src/publisher/service.ts
|
|
7
|
+
var Publisher = class extends Context.Service()("@useairfoil/connector-kit/Publisher") {};
|
|
8
|
+
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/telemetry.ts
|
|
11
|
+
var telemetry_exports = /* @__PURE__ */ __export({
|
|
12
|
+
Attr: () => Attr,
|
|
13
|
+
EventAttr: () => EventAttr,
|
|
14
|
+
EventName: () => EventName,
|
|
15
|
+
SpanName: () => SpanName,
|
|
16
|
+
addCurrentSpanEvent: () => addCurrentSpanEvent,
|
|
17
|
+
annotateError: () => annotateError,
|
|
18
|
+
layer: () => layer,
|
|
19
|
+
layerConfig: () => layerConfig,
|
|
20
|
+
layerOtlpTracing: () => layerOtlpTracing
|
|
21
|
+
});
|
|
22
|
+
/** Canonical span names emitted by connector-kit runtime instrumentation. */
|
|
23
|
+
const SpanName = {
|
|
24
|
+
batchProcess: "connector.batch.process",
|
|
25
|
+
webhookDecode: "connector.webhook.decode",
|
|
26
|
+
webhookHandle: "connector.webhook.handle",
|
|
27
|
+
publish: "connector.publish",
|
|
28
|
+
stateSet: "connector.state.set",
|
|
29
|
+
apiFetch: "connector.api.fetch"
|
|
30
|
+
};
|
|
31
|
+
/** Canonical span attribute names. Keep these stable for collector queries. */
|
|
32
|
+
const Attr = {
|
|
33
|
+
connectorName: "airfoil.connector.name",
|
|
34
|
+
connectorResourcesCount: "airfoil.connector.resources.count",
|
|
35
|
+
resourceName: "airfoil.resource.name",
|
|
36
|
+
resourceSource: "airfoil.resource.source",
|
|
37
|
+
batchMutations: "airfoil.batch.mutations",
|
|
38
|
+
webhookPath: "airfoil.webhook.path",
|
|
39
|
+
publisherSuccess: "airfoil.publisher.success",
|
|
40
|
+
stateKey: "airfoil.state.key",
|
|
41
|
+
apiPath: "airfoil.api.path",
|
|
42
|
+
errorPhase: "airfoil.error.phase",
|
|
43
|
+
errorType: "airfoil.error.type",
|
|
44
|
+
errorMessage: "airfoil.error.message",
|
|
45
|
+
errorDetails: "airfoil.error.details"
|
|
46
|
+
};
|
|
47
|
+
/** Span event names for high-cardinality reproduction details. */
|
|
48
|
+
const EventName = { batchCheckpoint: "airfoil.batch.checkpoint" };
|
|
49
|
+
/** Event-only attributes. These are intentionally not span attributes. */
|
|
50
|
+
const EventAttr = { batchCursor: "airfoil.batch.cursor" };
|
|
51
|
+
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
52
|
+
const maxErrorMessageLength = 500;
|
|
53
|
+
const OtlpEnvConfig = Config.all({
|
|
54
|
+
enabled: Config.boolean("OTEL_ENABLED").pipe(Config.withDefault(false)),
|
|
55
|
+
baseUrl: Config.option(Config.string("OTEL_EXPORTER_OTLP_ENDPOINT")),
|
|
56
|
+
rawHeaders: Config.option(Config.string("OTEL_EXPORTER_OTLP_HEADERS"))
|
|
57
|
+
});
|
|
58
|
+
const defaultRedactedHeaders = [
|
|
59
|
+
"authorization",
|
|
60
|
+
"cookie",
|
|
61
|
+
"set-cookie",
|
|
62
|
+
"x-api-key",
|
|
63
|
+
/api[-_]?key/i,
|
|
64
|
+
/secret/i,
|
|
65
|
+
/signature/i,
|
|
66
|
+
/token/i
|
|
67
|
+
];
|
|
68
|
+
const parseOtelHeaders = (value) => Object.fromEntries(value.split(",").map((entry) => entry.trim()).filter(Boolean).flatMap((entry) => {
|
|
69
|
+
const separator = entry.indexOf("=");
|
|
70
|
+
if (separator < 1) return [];
|
|
71
|
+
return [[entry.slice(0, separator).trim(), entry.slice(separator + 1).trim()]];
|
|
72
|
+
}));
|
|
73
|
+
const appendPath = (baseUrl, path) => `${baseUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
74
|
+
/**
|
|
75
|
+
* Best-effort stable error classifier for span metadata.
|
|
76
|
+
*
|
|
77
|
+
* Effect/Data tagged errors expose `_tag`; that is the most useful value for
|
|
78
|
+
* connector bugs (`ConnectorError`, `InvalidWebhookPayloadError`, etc.). For
|
|
79
|
+
* non-tagged errors we fall back to the JS `Error.name`, then constructor name,
|
|
80
|
+
* then the primitive type.
|
|
81
|
+
*/
|
|
82
|
+
const errorType = (error) => {
|
|
83
|
+
if (isRecord(error) && typeof error._tag === "string") return String(error._tag);
|
|
84
|
+
if (error instanceof Error) return error.name;
|
|
85
|
+
if (isRecord(error) && typeof error.constructor?.name === "string") return error.constructor.name;
|
|
86
|
+
return typeof error;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Human-readable error summary for span metadata.
|
|
90
|
+
*
|
|
91
|
+
* Tagged errors in this package carry a `message` field, so this preserves the
|
|
92
|
+
* important diagnostic text. Unknown object failures are intentionally reduced
|
|
93
|
+
* to their type; full structured error payloads belong in logs, not span attrs.
|
|
94
|
+
*/
|
|
95
|
+
const errorMessage = (error) => {
|
|
96
|
+
const message = (() => {
|
|
97
|
+
if (isRecord(error) && typeof error.message === "string") return error.message;
|
|
98
|
+
if (typeof error === "string") return error;
|
|
99
|
+
return errorType(error);
|
|
100
|
+
})();
|
|
101
|
+
if (message.length > maxErrorMessageLength) return `${message.slice(0, maxErrorMessageLength)}...`;
|
|
102
|
+
return message;
|
|
103
|
+
};
|
|
104
|
+
const errorDetails = (error) => {
|
|
105
|
+
if (!isRecord(error) || error.cause === void 0) return void 0;
|
|
106
|
+
return Cause.pretty(Cause.die(error.cause));
|
|
107
|
+
};
|
|
108
|
+
/** Annotates the active span with phase-aware error metadata before re-failing. */
|
|
109
|
+
const annotateError = (phase, error) => {
|
|
110
|
+
const details = errorDetails(error);
|
|
111
|
+
return Effect.annotateCurrentSpan({
|
|
112
|
+
[Attr.errorPhase]: phase,
|
|
113
|
+
[Attr.errorType]: errorType(error),
|
|
114
|
+
[Attr.errorMessage]: errorMessage(error),
|
|
115
|
+
...details !== void 0 ? { [Attr.errorDetails]: details } : {}
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
/** Adds an event to the active span, or no-ops when no span is active. */
|
|
119
|
+
const addCurrentSpanEvent = (name, attributes) => Effect.currentSpan.pipe(Effect.flatMap((span) => Effect.clockWith((clock) => Effect.sync(() => span.event(name, clock.currentTimeNanosUnsafe(), attributes)))), Effect.ignore);
|
|
120
|
+
const buildOtlpTracingLayer = (config, options) => Effect.gen(function* () {
|
|
121
|
+
const allRedacted = [...defaultRedactedHeaders, ...options.redactedHeaders ?? []];
|
|
122
|
+
const RedactionLayer = Layer.succeed(Headers.CurrentRedactedNames)(allRedacted);
|
|
123
|
+
if (!config.enabled) return RedactionLayer;
|
|
124
|
+
if (!config.endpoint) return yield* Effect.fail(/* @__PURE__ */ new Error("enabled=true requires endpoint"));
|
|
125
|
+
yield* Effect.logInfo("telemetry enabled").pipe(Effect.annotateLogs({
|
|
126
|
+
endpoint: config.endpoint,
|
|
127
|
+
headers: config.headers ? Object.keys(config.headers) : []
|
|
128
|
+
}));
|
|
129
|
+
return Layer.mergeAll(RedactionLayer, Observability.OtlpTracer.layer({
|
|
130
|
+
url: appendPath(config.endpoint, "/v1/traces"),
|
|
131
|
+
headers: config.headers
|
|
132
|
+
}).pipe(Layer.provide(Observability.OtlpSerialization.layerJson)));
|
|
133
|
+
});
|
|
134
|
+
/**
|
|
135
|
+
* Trace-only OTLP export with sensitive HTTP header redaction.
|
|
136
|
+
* Takes direct config values — no `ConfigProvider` required.
|
|
137
|
+
*/
|
|
138
|
+
const layer = (config = {}, options = {}) => Layer.unwrap(buildOtlpTracingLayer(config, options));
|
|
139
|
+
/**
|
|
140
|
+
* Trace-only OTLP export with sensitive HTTP header redaction.
|
|
141
|
+
* Takes Effect Config-wrapped values — reads from `ConfigProvider`.
|
|
142
|
+
*
|
|
143
|
+
* @example
|
|
144
|
+
* Telemetry.layerConfig({
|
|
145
|
+
* enabled: Config.boolean("MY_OTEL_ENABLED"),
|
|
146
|
+
* endpoint: Config.string("MY_COLLECTOR_URL"),
|
|
147
|
+
* })
|
|
148
|
+
*/
|
|
149
|
+
const layerConfig = (config, options = {}) => Layer.unwrap(Effect.gen(function* () {
|
|
150
|
+
return yield* buildOtlpTracingLayer(yield* Config.unwrap(config), options);
|
|
151
|
+
}));
|
|
152
|
+
/**
|
|
153
|
+
* Trace-only OTLP export with sensitive HTTP header redaction.
|
|
154
|
+
* Zero-config shortcut that reads the standard `OTEL_*` env var names.
|
|
155
|
+
*
|
|
156
|
+
* Reads `OTEL_ENABLED`, `OTEL_EXPORTER_OTLP_ENDPOINT`, and
|
|
157
|
+
* `OTEL_EXPORTER_OTLP_HEADERS`. Effect's OTLP resource handling reads
|
|
158
|
+
* `OTEL_SERVICE_NAME`, `OTEL_SERVICE_VERSION`, and `OTEL_RESOURCE_ATTRIBUTES`.
|
|
159
|
+
*/
|
|
160
|
+
const layerOtlpTracing = (options = {}) => Layer.unwrap(Effect.gen(function* () {
|
|
161
|
+
const raw = yield* OtlpEnvConfig;
|
|
162
|
+
return yield* buildOtlpTracingLayer({
|
|
163
|
+
enabled: raw.enabled,
|
|
164
|
+
endpoint: Option.getOrUndefined(raw.baseUrl),
|
|
165
|
+
headers: Option.isSome(raw.rawHeaders) ? parseOtelHeaders(raw.rawHeaders.value) : void 0
|
|
166
|
+
}, options);
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
//#endregion
|
|
170
|
+
export { addCurrentSpanEvent as a, Publisher as c, SpanName as i, EventAttr as n, annotateError as o, EventName as r, telemetry_exports as s, Attr as t };
|
|
171
|
+
//# sourceMappingURL=telemetry-p0dE5Mdu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry-p0dE5Mdu.js","names":["defaultRedactedHeaders: ReadonlyArray<string | RegExp>"],"sources":["../src/publisher/service.ts","../src/telemetry.ts"],"sourcesContent":["import type * as Wings from \"@useairfoil/wings\";\nimport type * as Effect from \"effect/Effect\";\n\nimport { Context } from \"effect\";\n\nimport type { ResourceBatch } from \"../core/types\";\nimport type { ConnectorError } from \"../errors\";\n\nexport type PublishSource = \"backfill\" | \"changes\" | \"webhook\";\n\nexport type PublishAck =\n | {\n readonly status: \"accepted\";\n readonly resource: string;\n readonly partition?: Wings.PartitionValue.PartitionValue;\n }\n | {\n readonly status: \"rejected\";\n readonly resource: string;\n readonly reason: string;\n readonly rejectedRows?: number;\n readonly partition?: Wings.PartitionValue.PartitionValue;\n };\n\nexport type PublishOptions = {\n readonly resource: string;\n readonly source: PublishSource;\n readonly batch: ResourceBatch;\n};\n\nexport interface PublisherService {\n readonly publish: (options: PublishOptions) => Effect.Effect<PublishAck, ConnectorError>;\n}\n\nexport class Publisher extends Context.Service<Publisher, PublisherService>()(\n \"@useairfoil/connector-kit/Publisher\",\n) {}\n","import { Cause, Config, Effect, Layer, Option } from \"effect\";\nimport { Headers } from \"effect/unstable/http\";\nimport * as Observability from \"effect/unstable/observability\";\n\n/** Canonical span names emitted by connector-kit runtime instrumentation. */\nexport const SpanName = {\n batchProcess: \"connector.batch.process\",\n webhookDecode: \"connector.webhook.decode\",\n webhookHandle: \"connector.webhook.handle\",\n publish: \"connector.publish\",\n stateSet: \"connector.state.set\",\n apiFetch: \"connector.api.fetch\",\n} as const;\n\n/** Canonical span attribute names. Keep these stable for collector queries. */\nexport const Attr = {\n connectorName: \"airfoil.connector.name\",\n connectorResourcesCount: \"airfoil.connector.resources.count\",\n resourceName: \"airfoil.resource.name\",\n resourceSource: \"airfoil.resource.source\",\n batchMutations: \"airfoil.batch.mutations\",\n webhookPath: \"airfoil.webhook.path\",\n publisherSuccess: \"airfoil.publisher.success\",\n stateKey: \"airfoil.state.key\",\n apiPath: \"airfoil.api.path\",\n errorPhase: \"airfoil.error.phase\",\n errorType: \"airfoil.error.type\",\n errorMessage: \"airfoil.error.message\",\n errorDetails: \"airfoil.error.details\",\n} as const;\n\n/** Span event names for high-cardinality reproduction details. */\nexport const EventName = {\n batchCheckpoint: \"airfoil.batch.checkpoint\",\n} as const;\n\n/** Event-only attributes. These are intentionally not span attributes. */\nexport const EventAttr = {\n batchCursor: \"airfoil.batch.cursor\",\n} as const;\n\nconst isRecord = (value: unknown): value is Record<string, unknown> =>\n typeof value === \"object\" && value !== null;\n\nconst maxErrorMessageLength = 500;\n\nconst OtlpEnvConfig = Config.all({\n enabled: Config.boolean(\"OTEL_ENABLED\").pipe(Config.withDefault(false)),\n baseUrl: Config.option(Config.string(\"OTEL_EXPORTER_OTLP_ENDPOINT\")),\n rawHeaders: Config.option(Config.string(\"OTEL_EXPORTER_OTLP_HEADERS\")),\n});\n\nconst defaultRedactedHeaders: ReadonlyArray<string | RegExp> = [\n \"authorization\",\n \"cookie\",\n \"set-cookie\",\n \"x-api-key\",\n /api[-_]?key/i,\n /secret/i,\n /signature/i,\n /token/i,\n];\n\nconst parseOtelHeaders = (value: string): Record<string, string> =>\n Object.fromEntries(\n value\n .split(\",\")\n .map((entry) => entry.trim())\n .filter(Boolean)\n .flatMap((entry) => {\n const separator = entry.indexOf(\"=\");\n if (separator < 1) return [];\n return [[entry.slice(0, separator).trim(), entry.slice(separator + 1).trim()]];\n }),\n );\n\nconst appendPath = (baseUrl: string, path: string) =>\n `${baseUrl.replace(/\\/$/, \"\")}/${path.replace(/^\\//, \"\")}`;\n\n/**\n * Best-effort stable error classifier for span metadata.\n *\n * Effect/Data tagged errors expose `_tag`; that is the most useful value for\n * connector bugs (`ConnectorError`, `InvalidWebhookPayloadError`, etc.). For\n * non-tagged errors we fall back to the JS `Error.name`, then constructor name,\n * then the primitive type.\n */\nconst errorType = (error: unknown) => {\n if (isRecord(error) && typeof error._tag === \"string\") {\n return String(error._tag);\n }\n if (error instanceof Error) {\n return error.name;\n }\n if (isRecord(error) && typeof error.constructor?.name === \"string\") {\n return error.constructor.name;\n }\n return typeof error;\n};\n\n/**\n * Human-readable error summary for span metadata.\n *\n * Tagged errors in this package carry a `message` field, so this preserves the\n * important diagnostic text. Unknown object failures are intentionally reduced\n * to their type; full structured error payloads belong in logs, not span attrs.\n */\nconst errorMessage = (error: unknown) => {\n const message = (() => {\n if (isRecord(error) && typeof error.message === \"string\") {\n return error.message;\n }\n if (typeof error === \"string\") {\n return error;\n }\n return errorType(error);\n })();\n\n if (message.length > maxErrorMessageLength) {\n return `${message.slice(0, maxErrorMessageLength)}...`;\n }\n\n return message;\n};\n\nconst errorDetails = (error: unknown): string | undefined => {\n if (!isRecord(error) || error.cause === undefined) return undefined;\n return Cause.pretty(Cause.die(error.cause));\n};\n\n/** Annotates the active span with phase-aware error metadata before re-failing. */\nexport const annotateError = (phase: string, error: unknown) => {\n const details = errorDetails(error);\n return Effect.annotateCurrentSpan({\n [Attr.errorPhase]: phase,\n [Attr.errorType]: errorType(error),\n [Attr.errorMessage]: errorMessage(error),\n ...(details !== undefined ? { [Attr.errorDetails]: details } : {}),\n });\n};\n\n/** Adds an event to the active span, or no-ops when no span is active. */\nexport const addCurrentSpanEvent = (name: string, attributes?: Record<string, unknown>) =>\n Effect.currentSpan.pipe(\n Effect.flatMap((span) =>\n Effect.clockWith((clock) =>\n Effect.sync(() => span.event(name, clock.currentTimeNanosUnsafe(), attributes)),\n ),\n ),\n Effect.ignore,\n );\n\n/** Transport configuration for OTLP trace export. All fields are direct values. */\nexport type OtlpTracingConfig = {\n readonly enabled?: boolean;\n readonly endpoint?: string;\n readonly headers?: Record<string, string>;\n};\n\n/** Runtime options — not config-wrappable since RegExp cannot live in Config. */\nexport type OtlpTracingOptions = {\n /** Additional connector-specific header names or regexes to redact from HTTP logs/traces. */\n readonly redactedHeaders?: ReadonlyArray<string | RegExp>;\n};\n\nconst buildOtlpTracingLayer = (config: OtlpTracingConfig, options: OtlpTracingOptions) =>\n Effect.gen(function* () {\n const allRedacted = [...defaultRedactedHeaders, ...(options.redactedHeaders ?? [])];\n const RedactionLayer = Layer.succeed(Headers.CurrentRedactedNames)(allRedacted);\n\n if (!config.enabled) {\n return RedactionLayer;\n }\n\n if (!config.endpoint) {\n return yield* Effect.fail(new Error(\"enabled=true requires endpoint\"));\n }\n\n yield* Effect.logInfo(\"telemetry enabled\").pipe(\n Effect.annotateLogs({\n endpoint: config.endpoint,\n headers: config.headers ? Object.keys(config.headers) : [],\n }),\n );\n\n return Layer.mergeAll(\n RedactionLayer,\n Observability.OtlpTracer.layer({\n url: appendPath(config.endpoint, \"/v1/traces\"),\n headers: config.headers,\n }).pipe(Layer.provide(Observability.OtlpSerialization.layerJson)),\n );\n });\n\n/**\n * Trace-only OTLP export with sensitive HTTP header redaction.\n * Takes direct config values — no `ConfigProvider` required.\n */\nexport const layer = (config: OtlpTracingConfig = {}, options: OtlpTracingOptions = {}) =>\n Layer.unwrap(buildOtlpTracingLayer(config, options));\n\n/**\n * Trace-only OTLP export with sensitive HTTP header redaction.\n * Takes Effect Config-wrapped values — reads from `ConfigProvider`.\n *\n * @example\n * Telemetry.layerConfig({\n * enabled: Config.boolean(\"MY_OTEL_ENABLED\"),\n * endpoint: Config.string(\"MY_COLLECTOR_URL\"),\n * })\n */\nexport const layerConfig = (\n config: Config.Wrap<OtlpTracingConfig>,\n options: OtlpTracingOptions = {},\n) =>\n Layer.unwrap(\n Effect.gen(function* () {\n const resolved = yield* Config.unwrap(config);\n return yield* buildOtlpTracingLayer(resolved, options);\n }),\n );\n\n/**\n * Trace-only OTLP export with sensitive HTTP header redaction.\n * Zero-config shortcut that reads the standard `OTEL_*` env var names.\n *\n * Reads `OTEL_ENABLED`, `OTEL_EXPORTER_OTLP_ENDPOINT`, and\n * `OTEL_EXPORTER_OTLP_HEADERS`. Effect's OTLP resource handling reads\n * `OTEL_SERVICE_NAME`, `OTEL_SERVICE_VERSION`, and `OTEL_RESOURCE_ATTRIBUTES`.\n */\nexport const layerOtlpTracing = (options: OtlpTracingOptions = {}) =>\n Layer.unwrap(\n Effect.gen(function* () {\n const raw = yield* OtlpEnvConfig;\n return yield* buildOtlpTracingLayer(\n {\n enabled: raw.enabled,\n endpoint: Option.getOrUndefined(raw.baseUrl),\n headers: Option.isSome(raw.rawHeaders)\n ? parseOtelHeaders(raw.rawHeaders.value)\n : undefined,\n },\n options,\n );\n }),\n );\n"],"mappings":"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"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { t as ConnectorError } from "./index-DyuT2jzP.js";
|
|
2
|
+
import { Duration, Effect, Schema } from "effect";
|
|
3
|
+
import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
|
|
4
|
+
|
|
5
|
+
//#region src/core/types.d.ts
|
|
6
|
+
declare namespace Cursor {
|
|
7
|
+
type Value = string | number | Date;
|
|
8
|
+
type Kind = "string" | "number" | "isoDateTime";
|
|
9
|
+
type Definition<T extends Value = Value> = {
|
|
10
|
+
readonly kind: Kind;
|
|
11
|
+
readonly decode: (value: unknown) => Effect.Effect<T, ConnectorError>;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
type DeleteValue = string | number | boolean | Date;
|
|
15
|
+
type ResourceMutation<Row extends object = object> = {
|
|
16
|
+
readonly op: "upsert";
|
|
17
|
+
readonly row: Row;
|
|
18
|
+
} | {
|
|
19
|
+
readonly op: "delete";
|
|
20
|
+
readonly key: DeleteValue;
|
|
21
|
+
readonly version: DeleteValue;
|
|
22
|
+
};
|
|
23
|
+
type ResourceBatch<Row extends object = object> = {
|
|
24
|
+
readonly cursor?: Cursor.Value;
|
|
25
|
+
readonly mutations: ReadonlyArray<ResourceMutation<Row>>;
|
|
26
|
+
};
|
|
27
|
+
type ResourceState = {
|
|
28
|
+
readonly backfill?: {
|
|
29
|
+
readonly cutoff: Cursor.Value;
|
|
30
|
+
readonly pageCursor?: Cursor.Value;
|
|
31
|
+
readonly completed: boolean;
|
|
32
|
+
};
|
|
33
|
+
readonly changes?: {
|
|
34
|
+
readonly cursor: Cursor.Value;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
type ResourceSchema = Schema.Decoder<object>;
|
|
38
|
+
/**
|
|
39
|
+
* Takes a resource schema and returns its decoded row object type.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* ```ts
|
|
43
|
+
* const ProductSchema = Schema.Struct({ id: Schema.String, updatedAt: Schema.String });
|
|
44
|
+
* type Product = ResourceRow<typeof ProductSchema>;
|
|
45
|
+
* // Result:
|
|
46
|
+
* // type Product = { readonly id: string; readonly updatedAt: string }
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
type ResourceRow<S$1 extends ResourceSchema> = Schema.Schema.Type<S$1>;
|
|
50
|
+
/**
|
|
51
|
+
* Takes a resource schema and returns the string keys available on its row type.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```ts
|
|
55
|
+
* const ProductSchema = Schema.Struct({ id: Schema.String, updatedAt: Schema.String });
|
|
56
|
+
* type ProductField = ResourceField<typeof ProductSchema>;
|
|
57
|
+
* // Result:
|
|
58
|
+
* // type ProductField = "id" | "updatedAt"
|
|
59
|
+
*
|
|
60
|
+
* const key: ResourceField<typeof ProductSchema> = "id";
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
type ResourceField<S$1 extends ResourceSchema> = keyof ResourceRow<S$1> & string;
|
|
64
|
+
type FetchPageResult<Row extends object> = {
|
|
65
|
+
readonly mutations: ReadonlyArray<ResourceMutation<Row>>;
|
|
66
|
+
readonly nextPageCursor?: Cursor.Value;
|
|
67
|
+
readonly hasMore: boolean;
|
|
68
|
+
};
|
|
69
|
+
type PageFetch<Row extends object, R = never> = {
|
|
70
|
+
readonly pageCursor: Cursor.Definition;
|
|
71
|
+
readonly cutoff: Cursor.Definition;
|
|
72
|
+
readonly fetch: (input: {
|
|
73
|
+
readonly pageCursor?: Cursor.Value;
|
|
74
|
+
readonly cutoff: Cursor.Value;
|
|
75
|
+
}) => Effect.Effect<FetchPageResult<Row>, ConnectorError, R>;
|
|
76
|
+
};
|
|
77
|
+
type FetchChangesResult<Row extends object> = {
|
|
78
|
+
readonly mutations: ReadonlyArray<ResourceMutation<Row>>;
|
|
79
|
+
readonly cursor: Cursor.Value;
|
|
80
|
+
};
|
|
81
|
+
type ChangesFetch<Row extends object, R = never> = {
|
|
82
|
+
readonly cursor: Cursor.Definition;
|
|
83
|
+
readonly interval?: Duration.Input;
|
|
84
|
+
readonly fetch: (input: {
|
|
85
|
+
readonly cursor: Cursor.Value;
|
|
86
|
+
}) => Effect.Effect<FetchChangesResult<Row>, ConnectorError, R>;
|
|
87
|
+
};
|
|
88
|
+
type WebhookHandler<Row extends object, Payload$1> = {
|
|
89
|
+
readonly schema: Schema.Decoder<Payload$1>;
|
|
90
|
+
handler(input: {
|
|
91
|
+
readonly payload: Payload$1;
|
|
92
|
+
}): Effect.Effect<ReadonlyArray<ResourceMutation<Row>>, ConnectorError>;
|
|
93
|
+
};
|
|
94
|
+
type ResourceDefinition<S$1 extends ResourceSchema = ResourceSchema, Payload$1 = unknown, R = never> = {
|
|
95
|
+
readonly name: string;
|
|
96
|
+
readonly schema: S$1;
|
|
97
|
+
readonly key: ResourceField<NoInfer<S$1>>;
|
|
98
|
+
readonly version: ResourceField<NoInfer<S$1>>;
|
|
99
|
+
readonly partition?: {
|
|
100
|
+
readonly required: boolean;
|
|
101
|
+
};
|
|
102
|
+
readonly backfill?: PageFetch<ResourceRow<NoInfer<S$1>>, R>;
|
|
103
|
+
readonly changes?: ChangesFetch<ResourceRow<NoInfer<S$1>>, R>;
|
|
104
|
+
readonly webhook?: WebhookHandler<ResourceRow<NoInfer<S$1>>, Payload$1>;
|
|
105
|
+
};
|
|
106
|
+
/**
|
|
107
|
+
* Takes a connector resource tuple and returns a `{ [resourceName]: rowType }`
|
|
108
|
+
* map.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* type Rows = ResourceRows<[typeof Products, typeof Orders]>;
|
|
113
|
+
* // Result:
|
|
114
|
+
* // type Rows = {
|
|
115
|
+
* // readonly products: Product;
|
|
116
|
+
* // readonly orders: Order;
|
|
117
|
+
* // }
|
|
118
|
+
*
|
|
119
|
+
* type ProductRow = Rows["products"];
|
|
120
|
+
* // Result:
|
|
121
|
+
* // type ProductRow = Product
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
type ResourceRows<Resources extends ReadonlyArray<ResourceDefinition>> = { readonly [Resource in Resources[number] as Resource["name"]]: Resource extends ResourceDefinition<infer S, unknown, unknown> ? ResourceRow<S> : never };
|
|
125
|
+
/**
|
|
126
|
+
* Takes a resource definition and returns the payload type accepted by
|
|
127
|
+
* `to(resource, payload)`.
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* type ProductPayload = ResourcePayload<typeof Products>;
|
|
132
|
+
* // Result, if Products was defined with a ProductEvent webhook schema:
|
|
133
|
+
* // type ProductPayload = ProductEvent
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
type ResourcePayload<R> = R extends ResourceDefinition<ResourceSchema, infer Payload, unknown> ? Payload : never;
|
|
137
|
+
type WebhookAckMode = "after-enqueue" | "after-publish";
|
|
138
|
+
type WebhookRouteContext<Resources extends ReadonlyArray<ResourceDefinition> = ReadonlyArray<ResourceDefinition>, Payload$1 = unknown> = {
|
|
139
|
+
readonly request: HttpServerRequest.HttpServerRequest;
|
|
140
|
+
readonly rawBody: Uint8Array;
|
|
141
|
+
readonly payload: Payload$1;
|
|
142
|
+
/** Decodes and collects mutations for a resource-owned webhook handler. */
|
|
143
|
+
readonly to: <Resource$1 extends Resources[number]>(resource: Resource$1, payload: ResourcePayload<Resource$1>) => Effect.Effect<void, ConnectorError>;
|
|
144
|
+
};
|
|
145
|
+
type WebhookRoute<Resources extends ReadonlyArray<ResourceDefinition> = ReadonlyArray<ResourceDefinition>, Payload$1 = unknown> = {
|
|
146
|
+
readonly path: HttpRouter.PathInput;
|
|
147
|
+
readonly ackMode: WebhookAckMode;
|
|
148
|
+
readonly schema: Schema.Decoder<Payload$1>;
|
|
149
|
+
handler(context: WebhookRouteContext<Resources, Payload$1>): Effect.Effect<HttpServerResponse.HttpServerResponse, unknown>;
|
|
150
|
+
};
|
|
151
|
+
type WebhookRouteInput<Resources extends ReadonlyArray<ResourceDefinition>, Payload$1> = WebhookRoute<Resources, Payload$1>;
|
|
152
|
+
type ConnectorDefinition<Resources extends ReadonlyArray<ResourceDefinition> = ReadonlyArray<ResourceDefinition>> = {
|
|
153
|
+
readonly name: string;
|
|
154
|
+
readonly title?: string;
|
|
155
|
+
readonly resources: Resources;
|
|
156
|
+
readonly webhooks?: ReadonlyArray<WebhookRoute>;
|
|
157
|
+
};
|
|
158
|
+
//#endregion
|
|
159
|
+
export { WebhookAckMode as _, FetchChangesResult as a, WebhookRouteContext as b, ResourceBatch as c, ResourceMutation as d, ResourcePayload as f, ResourceState as g, ResourceSchema as h, DeleteValue as i, ResourceDefinition as l, ResourceRows as m, ConnectorDefinition as n, FetchPageResult as o, ResourceRow as p, Cursor as r, PageFetch as s, ChangesFetch as t, ResourceField as u, WebhookHandler as v, WebhookRouteInput as x, WebhookRoute as y };
|
|
160
|
+
//# sourceMappingURL=types-ZNrA-Dn0.d.ts.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import "../index-DyuT2jzP.js";
|
|
2
|
+
import { _ as WebhookAckMode, b as WebhookRouteContext, x as WebhookRouteInput, y as WebhookRoute } from "../types-ZNrA-Dn0.js";
|
|
3
|
+
import "../service-B7g_lHMI.js";
|
|
4
|
+
import { n as route, r as router } from "../index-CQJsFJs3.js";
|
|
5
|
+
export { WebhookAckMode, WebhookRoute, WebhookRouteContext, WebhookRouteInput, route, router };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { t as __export } from "./chunk-Bp6m_JJh.js";
|
|
2
|
+
import { n as router } from "./server-DAeFWT4o.js";
|
|
3
|
+
|
|
4
|
+
//#region src/webhook/index.ts
|
|
5
|
+
var webhook_exports = /* @__PURE__ */ __export({
|
|
6
|
+
route: () => route,
|
|
7
|
+
router: () => router
|
|
8
|
+
});
|
|
9
|
+
const route = (definition) => definition;
|
|
10
|
+
|
|
11
|
+
//#endregion
|
|
12
|
+
export { webhook_exports as n, route as t };
|
|
13
|
+
//# sourceMappingURL=webhook-BSA-tXtU.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhook-BSA-tXtU.js","names":[],"sources":["../src/webhook/index.ts"],"sourcesContent":["import type { ResourceDefinition, WebhookRoute, WebhookRouteInput } from \"../core/types\";\n\nexport { router } from \"./server\";\nexport type { WebhookAckMode, WebhookRoute, WebhookRouteContext, WebhookRouteInput } from \"./types\";\n\nexport const route = <const Resources extends ReadonlyArray<ResourceDefinition>, Payload>(\n definition: WebhookRouteInput<Resources, Payload>,\n): WebhookRoute<Resources, Payload> => definition;\n"],"mappings":";;;;;;;;AAKA,MAAa,SACX,eACqC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@useairfoil/connector-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"repository": "useairfoil/typescript-sdk",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -15,6 +15,36 @@
|
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
16
|
"import": "./dist/index.js",
|
|
17
17
|
"default": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./connector-app": {
|
|
20
|
+
"types": "./dist/connector-app/index.d.ts",
|
|
21
|
+
"import": "./dist/connector-app/index.js",
|
|
22
|
+
"default": "./dist/connector-app/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./ingestion": {
|
|
25
|
+
"types": "./dist/ingestion/index.d.ts",
|
|
26
|
+
"import": "./dist/ingestion/index.js",
|
|
27
|
+
"default": "./dist/ingestion/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./publisher": {
|
|
30
|
+
"types": "./dist/publisher/index.d.ts",
|
|
31
|
+
"import": "./dist/publisher/index.js",
|
|
32
|
+
"default": "./dist/publisher/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./state-store": {
|
|
35
|
+
"types": "./dist/state-store/index.d.ts",
|
|
36
|
+
"import": "./dist/state-store/index.js",
|
|
37
|
+
"default": "./dist/state-store/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./webhook": {
|
|
40
|
+
"types": "./dist/webhook/index.d.ts",
|
|
41
|
+
"import": "./dist/webhook/index.js",
|
|
42
|
+
"default": "./dist/webhook/index.js"
|
|
43
|
+
},
|
|
44
|
+
"./errors": {
|
|
45
|
+
"types": "./dist/errors/index.d.ts",
|
|
46
|
+
"import": "./dist/errors/index.js",
|
|
47
|
+
"default": "./dist/errors/index.js"
|
|
18
48
|
}
|
|
19
49
|
},
|
|
20
50
|
"scripts": {
|
|
@@ -24,19 +54,18 @@
|
|
|
24
54
|
"typecheck": "tsc --noEmit"
|
|
25
55
|
},
|
|
26
56
|
"dependencies": {
|
|
27
|
-
"@
|
|
28
|
-
"@useairfoil/
|
|
29
|
-
"
|
|
30
|
-
"json-stable-stringify": "^1.3.0"
|
|
57
|
+
"@effect/platform-node": "4.0.0-beta.83",
|
|
58
|
+
"@useairfoil/flight": "0.4.3",
|
|
59
|
+
"@useairfoil/wings": "0.4.1"
|
|
31
60
|
},
|
|
32
61
|
"devDependencies": {
|
|
33
|
-
"@effect/vitest": "4.0.0-beta.
|
|
62
|
+
"@effect/vitest": "4.0.0-beta.83",
|
|
34
63
|
"@types/node": "^24.2.0",
|
|
35
64
|
"tsdown": "^0.15.6",
|
|
36
65
|
"vitest": "^3.2.4"
|
|
37
66
|
},
|
|
38
67
|
"peerDependencies": {
|
|
39
|
-
"effect": "4.0.0-beta.
|
|
68
|
+
"effect": "4.0.0-beta.83"
|
|
40
69
|
},
|
|
41
70
|
"peerDependenciesMeta": {
|
|
42
71
|
"effect": {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { start, type App, type StartOptions } from "./start";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { NodeHttpServer } from "@effect/platform-node";
|
|
2
|
+
import { DateTime, Effect } from "effect";
|
|
3
|
+
import { HttpRouter } from "effect/unstable/http";
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
|
|
6
|
+
import type { ConnectorDefinition } from "../core";
|
|
7
|
+
import type { Cursor } from "../core/types";
|
|
8
|
+
|
|
9
|
+
import { run as runIngestion } from "../ingestion";
|
|
10
|
+
|
|
11
|
+
export type App = ConnectorDefinition;
|
|
12
|
+
|
|
13
|
+
export type StartOptions = {
|
|
14
|
+
readonly port: number;
|
|
15
|
+
readonly initialCutoff?: Cursor.Value;
|
|
16
|
+
readonly healthPath?: HttpRouter.PathInput;
|
|
17
|
+
readonly disableHttpLogger?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const start = (app: App, options: StartOptions) =>
|
|
21
|
+
Effect.gen(function* () {
|
|
22
|
+
const initialCutoff =
|
|
23
|
+
options.initialCutoff ?? (yield* DateTime.now.pipe(Effect.map(DateTime.formatIso)));
|
|
24
|
+
const routes = app.webhooks?.map((route) => route.path) ?? [];
|
|
25
|
+
|
|
26
|
+
yield* Effect.logInfo("webhook server ready").pipe(
|
|
27
|
+
Effect.annotateLogs({ port: options.port, routes }),
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
return yield* runIngestion(app, {
|
|
31
|
+
initialCutoff,
|
|
32
|
+
webhook: {
|
|
33
|
+
routes: app.webhooks ?? [],
|
|
34
|
+
healthPath: options.healthPath ?? "/health",
|
|
35
|
+
disableHttpLogger: options.disableHttpLogger ?? true,
|
|
36
|
+
},
|
|
37
|
+
}).pipe(Effect.provide(NodeHttpServer.layer(createServer, { port: options.port })));
|
|
38
|
+
});
|
package/src/core/builder.ts
CHANGED
|
@@ -1,12 +1,79 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { DateTime, Effect } from "effect";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import type {
|
|
4
|
+
ChangesFetch,
|
|
5
|
+
ConnectorDefinition,
|
|
6
|
+
Cursor as CursorType,
|
|
7
|
+
DeleteValue,
|
|
8
|
+
PageFetch,
|
|
9
|
+
ResourceDefinition,
|
|
10
|
+
ResourceMutation,
|
|
11
|
+
ResourceSchema,
|
|
12
|
+
WebhookHandler,
|
|
13
|
+
} from "./types";
|
|
9
14
|
|
|
10
|
-
|
|
15
|
+
import { ConnectorError } from "../errors";
|
|
11
16
|
|
|
12
|
-
export const
|
|
17
|
+
export const Connector = {
|
|
18
|
+
define: <const Resources extends ReadonlyArray<ResourceDefinition>>(
|
|
19
|
+
definition: ConnectorDefinition<Resources>,
|
|
20
|
+
) => definition,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const Resource = {
|
|
24
|
+
entity: <S extends ResourceSchema, Payload = never, R = never>(
|
|
25
|
+
definition: ResourceDefinition<S, Payload, R>,
|
|
26
|
+
): ResourceDefinition<S, Payload, R> => definition,
|
|
27
|
+
|
|
28
|
+
webhook: <Payload, Row extends object = never>(definition: WebhookHandler<Row, Payload>) =>
|
|
29
|
+
definition,
|
|
30
|
+
|
|
31
|
+
upsert: <Row extends object>(row: Row): ResourceMutation<Row> => ({ op: "upsert", row }),
|
|
32
|
+
|
|
33
|
+
delete: (options: {
|
|
34
|
+
readonly key: DeleteValue;
|
|
35
|
+
readonly version: DeleteValue;
|
|
36
|
+
}): ResourceMutation<never> => ({
|
|
37
|
+
op: "delete",
|
|
38
|
+
key: options.key,
|
|
39
|
+
version: options.version,
|
|
40
|
+
}),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const Fetch = {
|
|
44
|
+
page: <Row extends object = never, R = never>(definition: PageFetch<Row, R>) => definition,
|
|
45
|
+
changes: <Row extends object = never, R = never>(definition: ChangesFetch<Row, R>) => definition,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const decodeFailure = (kind: string, value: unknown) =>
|
|
49
|
+
new ConnectorError({ message: `Invalid ${kind} cursor value: ${String(value)}` });
|
|
50
|
+
|
|
51
|
+
export const Cursor = {
|
|
52
|
+
string: (): CursorType.Definition<string> => ({
|
|
53
|
+
kind: "string",
|
|
54
|
+
decode: (value) =>
|
|
55
|
+
typeof value === "string"
|
|
56
|
+
? Effect.succeed(value)
|
|
57
|
+
: Effect.fail(decodeFailure("string", value)),
|
|
58
|
+
}),
|
|
59
|
+
|
|
60
|
+
number: (): CursorType.Definition<number> => ({
|
|
61
|
+
kind: "number",
|
|
62
|
+
decode: (value) =>
|
|
63
|
+
typeof value === "number" && Number.isFinite(value)
|
|
64
|
+
? Effect.succeed(value)
|
|
65
|
+
: Effect.fail(decodeFailure("number", value)),
|
|
66
|
+
}),
|
|
67
|
+
|
|
68
|
+
isoDateTime: (): CursorType.Definition<string> => ({
|
|
69
|
+
kind: "isoDateTime",
|
|
70
|
+
decode: (value) => {
|
|
71
|
+
if (typeof value !== "string" || Number.isNaN(Date.parse(value))) {
|
|
72
|
+
return Effect.fail(decodeFailure("ISO datetime", value));
|
|
73
|
+
}
|
|
74
|
+
return Effect.succeed(value);
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
|
|
78
|
+
nowIsoDateTime: Effect.map(DateTime.now, DateTime.formatIso),
|
|
79
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export { Connector, Cursor, Fetch, Resource } from "./builder";
|
|
2
|
+
export type {
|
|
3
|
+
ChangesFetch,
|
|
4
|
+
ConnectorDefinition,
|
|
5
|
+
Cursor as CursorTypes,
|
|
6
|
+
FetchChangesResult,
|
|
7
|
+
FetchPageResult,
|
|
8
|
+
PageFetch,
|
|
9
|
+
ResourceBatch,
|
|
10
|
+
ResourceDefinition,
|
|
11
|
+
ResourceField,
|
|
12
|
+
ResourceMutation,
|
|
13
|
+
ResourcePayload,
|
|
14
|
+
ResourceRow,
|
|
15
|
+
ResourceRows,
|
|
16
|
+
ResourceSchema,
|
|
17
|
+
ResourceState,
|
|
18
|
+
WebhookAckMode,
|
|
19
|
+
WebhookHandler,
|
|
20
|
+
WebhookRoute,
|
|
21
|
+
WebhookRouteContext,
|
|
22
|
+
} from "./types";
|