effect-cf 0.14.0 → 0.16.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/CHANGELOG.md +12 -0
- package/README.md +110 -1
- package/dist/index.d.mts +322 -100
- package/dist/index.mjs +414 -103
- package/package.json +1 -1
- package/src/AnalyticsEngine.ts +846 -0
- package/src/Email.ts +134 -0
- package/src/index.ts +2 -0
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AnalyticsEngineDataPoint as CloudflareAnalyticsEngineDataPoint,
|
|
3
|
+
AnalyticsEngineDataset as CloudflareAnalyticsEngineDataset,
|
|
4
|
+
} from "@cloudflare/workers-types";
|
|
5
|
+
import {
|
|
6
|
+
Config,
|
|
7
|
+
Context,
|
|
8
|
+
Data,
|
|
9
|
+
Effect,
|
|
10
|
+
Layer,
|
|
11
|
+
Option,
|
|
12
|
+
Redacted,
|
|
13
|
+
Result,
|
|
14
|
+
Schema as S,
|
|
15
|
+
} from "effect";
|
|
16
|
+
import {
|
|
17
|
+
FetchHttpClient,
|
|
18
|
+
type Headers,
|
|
19
|
+
HttpClient,
|
|
20
|
+
type HttpClientResponse,
|
|
21
|
+
HttpClientRequest,
|
|
22
|
+
} from "effect/unstable/http";
|
|
23
|
+
|
|
24
|
+
import * as Binding from "./Binding";
|
|
25
|
+
import type { WorkerEnvironment } from "./Environment";
|
|
26
|
+
|
|
27
|
+
const expectedAnalyticsEngineDataset = "Analytics Engine dataset binding with writeDataPoint()";
|
|
28
|
+
const defaultQueryApiBaseUrl = "https://api.cloudflare.com/client/v4";
|
|
29
|
+
const textEncoder = new TextEncoder();
|
|
30
|
+
|
|
31
|
+
export const writeLimits = {
|
|
32
|
+
maxBlobs: 20,
|
|
33
|
+
maxDoubles: 20,
|
|
34
|
+
maxIndexes: 1,
|
|
35
|
+
maxBlobBytes: 16 * 1024,
|
|
36
|
+
maxIndexBytes: 96,
|
|
37
|
+
maxDataPointsPerInvocation: 250,
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
40
|
+
const queryColumnSchema = S.Struct({
|
|
41
|
+
name: S.String,
|
|
42
|
+
type: S.String,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const queryRowSchema = S.Record(S.String, S.Unknown);
|
|
46
|
+
|
|
47
|
+
const queryResponseSchema = S.Struct({
|
|
48
|
+
meta: S.Array(queryColumnSchema),
|
|
49
|
+
data: S.Array(queryRowSchema),
|
|
50
|
+
rows: S.Number,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const AnalyticsEngineFieldValueSchema = S.NullOr(
|
|
54
|
+
S.Union([S.String, S.instanceOf(ArrayBuffer)]),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
export const AnalyticsEngineDataPointSchema = S.Struct({
|
|
58
|
+
indexes: S.optional(S.Array(AnalyticsEngineFieldValueSchema)),
|
|
59
|
+
doubles: S.optional(S.Array(S.Finite)),
|
|
60
|
+
blobs: S.optional(S.Array(AnalyticsEngineFieldValueSchema)),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const decodeQueryResponse = S.decodeUnknownEffect(queryResponseSchema);
|
|
64
|
+
const decodeDataPoint = S.decodeUnknownEffect(AnalyticsEngineDataPointSchema);
|
|
65
|
+
|
|
66
|
+
/** Error raised when an Analytics Engine operation fails. */
|
|
67
|
+
export class AnalyticsEngineOperationError extends Data.TaggedError(
|
|
68
|
+
"AnalyticsEngineOperationError",
|
|
69
|
+
)<{
|
|
70
|
+
readonly binding: string;
|
|
71
|
+
readonly operation: string;
|
|
72
|
+
readonly cause: unknown;
|
|
73
|
+
}> {}
|
|
74
|
+
|
|
75
|
+
export interface AnalyticsEngineWriteViolation {
|
|
76
|
+
readonly path: string;
|
|
77
|
+
readonly message: string;
|
|
78
|
+
readonly limit?: number;
|
|
79
|
+
readonly actual?: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Error raised when an Analytics Engine write input violates Cloudflare limits. */
|
|
83
|
+
export class AnalyticsEngineWriteValidationError extends Data.TaggedError(
|
|
84
|
+
"AnalyticsEngineWriteValidationError",
|
|
85
|
+
)<{
|
|
86
|
+
readonly binding: string;
|
|
87
|
+
readonly operation: string;
|
|
88
|
+
readonly violations: ReadonlyArray<AnalyticsEngineWriteViolation>;
|
|
89
|
+
readonly cause?: unknown;
|
|
90
|
+
}> {}
|
|
91
|
+
|
|
92
|
+
/** Error raised when an Analytics Engine SQL API query fails. */
|
|
93
|
+
export class AnalyticsEngineQueryError extends Data.TaggedError("AnalyticsEngineQueryError")<{
|
|
94
|
+
readonly operation: string;
|
|
95
|
+
readonly accountId: string;
|
|
96
|
+
readonly message: string;
|
|
97
|
+
readonly status?: number;
|
|
98
|
+
readonly body?: string;
|
|
99
|
+
readonly cause?: unknown;
|
|
100
|
+
}> {}
|
|
101
|
+
|
|
102
|
+
/** Typed Analytics Engine dataset binding definition. */
|
|
103
|
+
export interface AnalyticsEngineDefinition {
|
|
104
|
+
/** Binding name as configured in `wrangler.jsonc`. */
|
|
105
|
+
readonly binding: string;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export type AnalyticsEngineBinding = CloudflareAnalyticsEngineDataset;
|
|
109
|
+
export type AnalyticsEngineDataPoint = CloudflareAnalyticsEngineDataPoint;
|
|
110
|
+
export type AnalyticsEngineFieldValue = ArrayBuffer | string | null;
|
|
111
|
+
export type AnalyticsEngineWriteError =
|
|
112
|
+
| AnalyticsEngineOperationError
|
|
113
|
+
| AnalyticsEngineWriteValidationError;
|
|
114
|
+
export type AnalyticsEngineInvalidWritePolicy = "error" | "drop";
|
|
115
|
+
export type AnalyticsEngineQueryColumn = S.Schema.Type<typeof queryColumnSchema>;
|
|
116
|
+
export type AnalyticsEngineQueryRow = S.Schema.Type<typeof queryRowSchema>;
|
|
117
|
+
|
|
118
|
+
export interface AnalyticsEngineWriteOptions {
|
|
119
|
+
/**
|
|
120
|
+
* Controls invalid write behavior. The default is `"error"`, which fails with
|
|
121
|
+
* `AnalyticsEngineWriteValidationError`. `"drop"` skips invalid points.
|
|
122
|
+
*/
|
|
123
|
+
readonly onInvalid?: AnalyticsEngineInvalidWritePolicy;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface AnalyticsEngineWriteBatchOptions extends AnalyticsEngineWriteOptions {
|
|
127
|
+
/**
|
|
128
|
+
* Maximum points per native batch call. Values are clamped to Cloudflare's
|
|
129
|
+
* per-invocation limit.
|
|
130
|
+
*/
|
|
131
|
+
readonly batchSize?: number;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export type AnalyticsEngineWritePolicy = AnalyticsEngineWriteBatchOptions;
|
|
135
|
+
|
|
136
|
+
export interface AnalyticsEngineQueryResult<Row = AnalyticsEngineQueryRow> {
|
|
137
|
+
readonly meta: ReadonlyArray<AnalyticsEngineQueryColumn>;
|
|
138
|
+
readonly data: ReadonlyArray<Row>;
|
|
139
|
+
readonly rows: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export interface AnalyticsEngineQueryDefinition {
|
|
143
|
+
/** Cloudflare account id that owns the Analytics Engine datasets. */
|
|
144
|
+
readonly accountId: string;
|
|
145
|
+
/** API token with Account Analytics Read permission. */
|
|
146
|
+
readonly apiToken: Redacted.Redacted<string>;
|
|
147
|
+
/** Base Cloudflare API URL. Defaults to `https://api.cloudflare.com/client/v4`. */
|
|
148
|
+
readonly apiBaseUrl?: string | URL;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export interface AnalyticsEngineQueryOptions {
|
|
152
|
+
/** Additional request headers. Authorization is always derived from `apiToken`. */
|
|
153
|
+
readonly headers?: Headers.Input;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface AnalyticsEngineClient {
|
|
157
|
+
readonly writeDataPoint: (
|
|
158
|
+
dataPoint?: AnalyticsEngineDataPoint,
|
|
159
|
+
options?: AnalyticsEngineWriteOptions,
|
|
160
|
+
) => Effect.Effect<void, AnalyticsEngineWriteError>;
|
|
161
|
+
readonly write: (
|
|
162
|
+
dataPoint?: AnalyticsEngineDataPoint,
|
|
163
|
+
options?: AnalyticsEngineWriteOptions,
|
|
164
|
+
) => Effect.Effect<void, AnalyticsEngineWriteError>;
|
|
165
|
+
readonly writeDataPoints: (
|
|
166
|
+
dataPoints: ReadonlyArray<AnalyticsEngineDataPoint>,
|
|
167
|
+
options?: AnalyticsEngineWriteBatchOptions,
|
|
168
|
+
) => Effect.Effect<void, AnalyticsEngineWriteError>;
|
|
169
|
+
readonly writeBatch: (
|
|
170
|
+
dataPoints: ReadonlyArray<AnalyticsEngineDataPoint>,
|
|
171
|
+
options?: AnalyticsEngineWriteBatchOptions,
|
|
172
|
+
) => Effect.Effect<void, AnalyticsEngineWriteError>;
|
|
173
|
+
readonly unsafeRaw: Effect.Effect<AnalyticsEngineBinding>;
|
|
174
|
+
readonly definition: AnalyticsEngineDefinition;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface AnalyticsEngineQueryClient {
|
|
178
|
+
readonly query: (
|
|
179
|
+
sql: string,
|
|
180
|
+
options?: AnalyticsEngineQueryOptions,
|
|
181
|
+
) => Effect.Effect<AnalyticsEngineQueryResult, AnalyticsEngineQueryError | S.SchemaError>;
|
|
182
|
+
readonly queryResult: <Row>(
|
|
183
|
+
row: S.Codec<Row, unknown>,
|
|
184
|
+
sql: string,
|
|
185
|
+
options?: AnalyticsEngineQueryOptions,
|
|
186
|
+
) => Effect.Effect<AnalyticsEngineQueryResult<Row>, AnalyticsEngineQueryError | S.SchemaError>;
|
|
187
|
+
readonly queryRows: <Row>(
|
|
188
|
+
row: S.Codec<Row, unknown>,
|
|
189
|
+
sql: string,
|
|
190
|
+
options?: AnalyticsEngineQueryOptions,
|
|
191
|
+
) => Effect.Effect<ReadonlyArray<Row>, AnalyticsEngineQueryError | S.SchemaError>;
|
|
192
|
+
readonly queryOne: <Row>(
|
|
193
|
+
row: S.Codec<Row, unknown>,
|
|
194
|
+
sql: string,
|
|
195
|
+
options?: AnalyticsEngineQueryOptions,
|
|
196
|
+
) => Effect.Effect<Option.Option<Row>, AnalyticsEngineQueryError | S.SchemaError>;
|
|
197
|
+
readonly queryText: (
|
|
198
|
+
sql: string,
|
|
199
|
+
options?: AnalyticsEngineQueryOptions,
|
|
200
|
+
) => Effect.Effect<string, AnalyticsEngineQueryError>;
|
|
201
|
+
readonly raw: (
|
|
202
|
+
sql: string,
|
|
203
|
+
options?: AnalyticsEngineQueryOptions,
|
|
204
|
+
) => Effect.Effect<HttpClientResponse.HttpClientResponse, AnalyticsEngineQueryError>;
|
|
205
|
+
readonly definition: AnalyticsEngineQueryDefinition;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
declare const AnalyticsEngineServiceTypeId: unique symbol;
|
|
209
|
+
declare const AnalyticsEngineQueryServiceTypeId: unique symbol;
|
|
210
|
+
|
|
211
|
+
/** Nominal service marker for Analytics Engine services created with {@link make}. */
|
|
212
|
+
export interface AnalyticsEngineService<Id extends string> {
|
|
213
|
+
readonly [AnalyticsEngineServiceTypeId]: {
|
|
214
|
+
readonly id: Id;
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Nominal service marker for Analytics Engine query services created with {@link makeQuery}. */
|
|
219
|
+
export interface AnalyticsEngineQueryService<Id extends string> {
|
|
220
|
+
readonly [AnalyticsEngineQueryServiceTypeId]: {
|
|
221
|
+
readonly id: Id;
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export type LayerOptions = {
|
|
226
|
+
readonly binding: string;
|
|
227
|
+
readonly write?: AnalyticsEngineWritePolicy;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export type QueryConfigOptions = {
|
|
231
|
+
readonly accountId?: Config.Config<string>;
|
|
232
|
+
readonly apiToken?: Config.Config<Redacted.Redacted<string>>;
|
|
233
|
+
readonly apiBaseUrl?: Config.Config<string>;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export interface TagClass<Self, Id extends string> extends Context.ServiceClass<
|
|
237
|
+
Self,
|
|
238
|
+
Id,
|
|
239
|
+
AnalyticsEngineClient
|
|
240
|
+
> {
|
|
241
|
+
readonly id: Id;
|
|
242
|
+
readonly layer: (
|
|
243
|
+
options: LayerOptions,
|
|
244
|
+
) => Layer.Layer<
|
|
245
|
+
Self,
|
|
246
|
+
Binding.BindingNotFoundError | Binding.BindingValidationError,
|
|
247
|
+
WorkerEnvironment
|
|
248
|
+
>;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface QueryTagClass<Self, Id extends string> extends Context.ServiceClass<
|
|
252
|
+
Self,
|
|
253
|
+
Id,
|
|
254
|
+
AnalyticsEngineQueryClient
|
|
255
|
+
> {
|
|
256
|
+
readonly id: Id;
|
|
257
|
+
readonly layer: (
|
|
258
|
+
definition: AnalyticsEngineQueryDefinition,
|
|
259
|
+
) => Layer.Layer<Self, never, HttpClient.HttpClient>;
|
|
260
|
+
readonly fetchLayer: (definition: AnalyticsEngineQueryDefinition) => Layer.Layer<Self>;
|
|
261
|
+
readonly layerConfig: (
|
|
262
|
+
config?: Config.Config<AnalyticsEngineQueryDefinition>,
|
|
263
|
+
) => Layer.Layer<Self, Config.ConfigError, HttpClient.HttpClient>;
|
|
264
|
+
readonly fetchLayerConfig: (
|
|
265
|
+
config?: Config.Config<AnalyticsEngineQueryDefinition>,
|
|
266
|
+
) => Layer.Layer<Self, Config.ConfigError>;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const analyticsEngineError = (binding: string, operation: string, cause: unknown) =>
|
|
270
|
+
new AnalyticsEngineOperationError({ binding, operation, cause });
|
|
271
|
+
|
|
272
|
+
const analyticsEngineWriteValidationError = (
|
|
273
|
+
binding: string,
|
|
274
|
+
operation: string,
|
|
275
|
+
violations: ReadonlyArray<AnalyticsEngineWriteViolation>,
|
|
276
|
+
cause?: unknown,
|
|
277
|
+
) =>
|
|
278
|
+
new AnalyticsEngineWriteValidationError({
|
|
279
|
+
binding,
|
|
280
|
+
operation,
|
|
281
|
+
violations,
|
|
282
|
+
cause,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const analyticsEngineQueryError = (
|
|
286
|
+
definition: AnalyticsEngineQueryDefinition,
|
|
287
|
+
operation: string,
|
|
288
|
+
message: string,
|
|
289
|
+
options?: {
|
|
290
|
+
readonly status?: number;
|
|
291
|
+
readonly body?: string;
|
|
292
|
+
readonly cause?: unknown;
|
|
293
|
+
},
|
|
294
|
+
) =>
|
|
295
|
+
new AnalyticsEngineQueryError({
|
|
296
|
+
operation,
|
|
297
|
+
accountId: definition.accountId,
|
|
298
|
+
message,
|
|
299
|
+
status: options?.status,
|
|
300
|
+
body: options?.body,
|
|
301
|
+
cause: options?.cause,
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
const tryAnalyticsEngineSync = <A>(
|
|
305
|
+
binding: string,
|
|
306
|
+
operation: string,
|
|
307
|
+
evaluate: () => A,
|
|
308
|
+
): Effect.Effect<A, AnalyticsEngineOperationError> =>
|
|
309
|
+
Effect.try({
|
|
310
|
+
try: evaluate,
|
|
311
|
+
catch: (cause) => analyticsEngineError(binding, operation, cause),
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const normalizeBatchSize = (batchSize: number | undefined) => {
|
|
315
|
+
if (batchSize === undefined || !Number.isFinite(batchSize) || batchSize <= 0) {
|
|
316
|
+
return writeLimits.maxDataPointsPerInvocation;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return Math.min(Math.floor(batchSize), writeLimits.maxDataPointsPerInvocation);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
const resolveWritePolicy = (
|
|
323
|
+
defaults: AnalyticsEngineWritePolicy | undefined,
|
|
324
|
+
options: AnalyticsEngineWriteBatchOptions | undefined,
|
|
325
|
+
) => ({
|
|
326
|
+
onInvalid: options?.onInvalid ?? defaults?.onInvalid ?? "error",
|
|
327
|
+
batchSize: normalizeBatchSize(options?.batchSize ?? defaults?.batchSize),
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const fieldPath = (prefix: string, field: string) => (prefix === "" ? field : `${prefix}.${field}`);
|
|
331
|
+
|
|
332
|
+
const indexedPath = (prefix: string, field: string, index: number) =>
|
|
333
|
+
`${fieldPath(prefix, field)}[${index}]`;
|
|
334
|
+
|
|
335
|
+
const byteLength = (value: AnalyticsEngineFieldValue) => {
|
|
336
|
+
if (value === null) {
|
|
337
|
+
return 0;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return typeof value === "string" ? textEncoder.encode(value).byteLength : value.byteLength;
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const lengthViolation = (
|
|
344
|
+
path: string,
|
|
345
|
+
label: string,
|
|
346
|
+
actual: number,
|
|
347
|
+
limit: number,
|
|
348
|
+
): AnalyticsEngineWriteViolation => ({
|
|
349
|
+
path,
|
|
350
|
+
message: `${label} exceeds Cloudflare Analytics Engine limit`,
|
|
351
|
+
actual,
|
|
352
|
+
limit,
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const validateDataPointLimits = (
|
|
356
|
+
dataPoint: AnalyticsEngineDataPoint,
|
|
357
|
+
pathPrefix = "",
|
|
358
|
+
): ReadonlyArray<AnalyticsEngineWriteViolation> => {
|
|
359
|
+
const violations: Array<AnalyticsEngineWriteViolation> = [];
|
|
360
|
+
|
|
361
|
+
if (dataPoint.blobs !== undefined && dataPoint.blobs.length > writeLimits.maxBlobs) {
|
|
362
|
+
violations.push(
|
|
363
|
+
lengthViolation(
|
|
364
|
+
fieldPath(pathPrefix, "blobs"),
|
|
365
|
+
"blobs",
|
|
366
|
+
dataPoint.blobs.length,
|
|
367
|
+
writeLimits.maxBlobs,
|
|
368
|
+
),
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (dataPoint.doubles !== undefined && dataPoint.doubles.length > writeLimits.maxDoubles) {
|
|
373
|
+
violations.push(
|
|
374
|
+
lengthViolation(
|
|
375
|
+
fieldPath(pathPrefix, "doubles"),
|
|
376
|
+
"doubles",
|
|
377
|
+
dataPoint.doubles.length,
|
|
378
|
+
writeLimits.maxDoubles,
|
|
379
|
+
),
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (dataPoint.indexes !== undefined && dataPoint.indexes.length > writeLimits.maxIndexes) {
|
|
384
|
+
violations.push(
|
|
385
|
+
lengthViolation(
|
|
386
|
+
fieldPath(pathPrefix, "indexes"),
|
|
387
|
+
"indexes",
|
|
388
|
+
dataPoint.indexes.length,
|
|
389
|
+
writeLimits.maxIndexes,
|
|
390
|
+
),
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (dataPoint.blobs !== undefined) {
|
|
395
|
+
const blobBytes = dataPoint.blobs.reduce((total, value) => total + byteLength(value), 0);
|
|
396
|
+
|
|
397
|
+
if (blobBytes > writeLimits.maxBlobBytes) {
|
|
398
|
+
violations.push(
|
|
399
|
+
lengthViolation(
|
|
400
|
+
fieldPath(pathPrefix, "blobs"),
|
|
401
|
+
"blob bytes",
|
|
402
|
+
blobBytes,
|
|
403
|
+
writeLimits.maxBlobBytes,
|
|
404
|
+
),
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (dataPoint.indexes !== undefined) {
|
|
410
|
+
for (let index = 0; index < dataPoint.indexes.length; index++) {
|
|
411
|
+
const indexBytes = byteLength(dataPoint.indexes[index] ?? null);
|
|
412
|
+
|
|
413
|
+
if (indexBytes > writeLimits.maxIndexBytes) {
|
|
414
|
+
violations.push(
|
|
415
|
+
lengthViolation(
|
|
416
|
+
indexedPath(pathPrefix, "indexes", index),
|
|
417
|
+
"index bytes",
|
|
418
|
+
indexBytes,
|
|
419
|
+
writeLimits.maxIndexBytes,
|
|
420
|
+
),
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return violations;
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const schemaViolation = (
|
|
430
|
+
pathPrefix: string,
|
|
431
|
+
cause: S.SchemaError,
|
|
432
|
+
): AnalyticsEngineWriteViolation => ({
|
|
433
|
+
path: pathPrefix === "" ? "$" : pathPrefix,
|
|
434
|
+
message: cause.message,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const toAnalyticsEngineDataPoint = (
|
|
438
|
+
dataPoint: S.Schema.Type<typeof AnalyticsEngineDataPointSchema>,
|
|
439
|
+
): AnalyticsEngineDataPoint => ({
|
|
440
|
+
...(dataPoint.indexes === undefined ? {} : { indexes: [...dataPoint.indexes] }),
|
|
441
|
+
...(dataPoint.doubles === undefined ? {} : { doubles: [...dataPoint.doubles] }),
|
|
442
|
+
...(dataPoint.blobs === undefined ? {} : { blobs: [...dataPoint.blobs] }),
|
|
443
|
+
});
|
|
444
|
+
|
|
445
|
+
const validateDataPoint = (
|
|
446
|
+
binding: string,
|
|
447
|
+
operation: string,
|
|
448
|
+
dataPoint: AnalyticsEngineDataPoint,
|
|
449
|
+
pathPrefix = "",
|
|
450
|
+
): Effect.Effect<AnalyticsEngineDataPoint, AnalyticsEngineWriteValidationError> =>
|
|
451
|
+
Effect.gen(function* () {
|
|
452
|
+
const decoded = yield* decodeDataPoint(dataPoint).pipe(
|
|
453
|
+
Effect.mapError((cause) =>
|
|
454
|
+
analyticsEngineWriteValidationError(
|
|
455
|
+
binding,
|
|
456
|
+
operation,
|
|
457
|
+
[schemaViolation(pathPrefix, cause)],
|
|
458
|
+
cause,
|
|
459
|
+
),
|
|
460
|
+
),
|
|
461
|
+
);
|
|
462
|
+
const validated = toAnalyticsEngineDataPoint(decoded);
|
|
463
|
+
const violations = validateDataPointLimits(validated, pathPrefix);
|
|
464
|
+
|
|
465
|
+
if (violations.length > 0) {
|
|
466
|
+
return yield* Effect.fail(
|
|
467
|
+
analyticsEngineWriteValidationError(binding, operation, violations),
|
|
468
|
+
);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return validated;
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
const validateOptionalDataPoint = (
|
|
475
|
+
binding: string,
|
|
476
|
+
operation: string,
|
|
477
|
+
dataPoint: AnalyticsEngineDataPoint | undefined,
|
|
478
|
+
): Effect.Effect<AnalyticsEngineDataPoint | undefined, AnalyticsEngineWriteValidationError> =>
|
|
479
|
+
dataPoint === undefined
|
|
480
|
+
? Effect.succeed(undefined)
|
|
481
|
+
: validateDataPoint(binding, operation, dataPoint);
|
|
482
|
+
|
|
483
|
+
const validateDataPoints = (
|
|
484
|
+
binding: string,
|
|
485
|
+
operation: string,
|
|
486
|
+
dataPoints: ReadonlyArray<AnalyticsEngineDataPoint>,
|
|
487
|
+
policy: ReturnType<typeof resolveWritePolicy>,
|
|
488
|
+
): Effect.Effect<ReadonlyArray<AnalyticsEngineDataPoint>, AnalyticsEngineWriteValidationError> =>
|
|
489
|
+
Effect.gen(function* () {
|
|
490
|
+
const points =
|
|
491
|
+
dataPoints.length > writeLimits.maxDataPointsPerInvocation && policy.onInvalid === "drop"
|
|
492
|
+
? dataPoints.slice(0, writeLimits.maxDataPointsPerInvocation)
|
|
493
|
+
: dataPoints;
|
|
494
|
+
const violations: Array<AnalyticsEngineWriteViolation> = [];
|
|
495
|
+
|
|
496
|
+
if (
|
|
497
|
+
dataPoints.length > writeLimits.maxDataPointsPerInvocation &&
|
|
498
|
+
policy.onInvalid === "error"
|
|
499
|
+
) {
|
|
500
|
+
violations.push(
|
|
501
|
+
lengthViolation(
|
|
502
|
+
"dataPoints",
|
|
503
|
+
"data points per Worker invocation",
|
|
504
|
+
dataPoints.length,
|
|
505
|
+
writeLimits.maxDataPointsPerInvocation,
|
|
506
|
+
),
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const validPoints: Array<AnalyticsEngineDataPoint> = [];
|
|
511
|
+
|
|
512
|
+
for (let index = 0; index < points.length; index++) {
|
|
513
|
+
const result = yield* Effect.result(
|
|
514
|
+
validateDataPoint(binding, operation, points[index], `dataPoints[${index}]`),
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
if (Result.isSuccess(result)) {
|
|
518
|
+
validPoints.push(result.success);
|
|
519
|
+
} else if (policy.onInvalid === "error") {
|
|
520
|
+
violations.push(...result.failure.violations);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (violations.length > 0) {
|
|
525
|
+
return yield* Effect.fail(
|
|
526
|
+
analyticsEngineWriteValidationError(binding, operation, violations),
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return validPoints;
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
const writeChunks = (
|
|
534
|
+
binding: string,
|
|
535
|
+
operation: string,
|
|
536
|
+
dataset: AnalyticsEngineBinding,
|
|
537
|
+
dataPoints: ReadonlyArray<AnalyticsEngineDataPoint>,
|
|
538
|
+
batchSize: number,
|
|
539
|
+
): Effect.Effect<void, AnalyticsEngineOperationError> =>
|
|
540
|
+
Effect.gen(function* () {
|
|
541
|
+
const writeDataPoints = Reflect.get(dataset, "writeDataPoints");
|
|
542
|
+
|
|
543
|
+
for (let index = 0; index < dataPoints.length; index += batchSize) {
|
|
544
|
+
const chunk = dataPoints.slice(index, index + batchSize);
|
|
545
|
+
|
|
546
|
+
if (typeof writeDataPoints === "function") {
|
|
547
|
+
yield* tryAnalyticsEngineSync(binding, operation, () =>
|
|
548
|
+
writeDataPoints.call(dataset, chunk),
|
|
549
|
+
);
|
|
550
|
+
} else {
|
|
551
|
+
for (const point of chunk) {
|
|
552
|
+
yield* tryAnalyticsEngineSync(binding, operation, () => dataset.writeDataPoint(point));
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
const queryApiUrl = (definition: AnalyticsEngineQueryDefinition) => {
|
|
559
|
+
const baseUrl = new URL(definition.apiBaseUrl ?? defaultQueryApiBaseUrl);
|
|
560
|
+
const pathname = baseUrl.pathname.replace(/\/+$/, "");
|
|
561
|
+
baseUrl.pathname = `${pathname}/accounts/${definition.accountId}/analytics_engine/sql`;
|
|
562
|
+
return baseUrl.href;
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
const queryRequest = (
|
|
566
|
+
definition: AnalyticsEngineQueryDefinition,
|
|
567
|
+
sql: string,
|
|
568
|
+
options?: AnalyticsEngineQueryOptions,
|
|
569
|
+
) => {
|
|
570
|
+
let request = HttpClientRequest.post(queryApiUrl(definition)).pipe(
|
|
571
|
+
HttpClientRequest.bodyText(sql, "text/plain;charset=UTF-8"),
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
if (options?.headers !== undefined) {
|
|
575
|
+
request = HttpClientRequest.setHeaders(request, options.headers);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return HttpClientRequest.bearerToken(request, definition.apiToken);
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const responseText = (
|
|
582
|
+
definition: AnalyticsEngineQueryDefinition,
|
|
583
|
+
response: HttpClientResponse.HttpClientResponse,
|
|
584
|
+
operation: string,
|
|
585
|
+
) =>
|
|
586
|
+
response.text.pipe(
|
|
587
|
+
Effect.catch((cause) =>
|
|
588
|
+
Effect.fail(
|
|
589
|
+
analyticsEngineQueryError(
|
|
590
|
+
definition,
|
|
591
|
+
operation,
|
|
592
|
+
"Failed to read Analytics Engine SQL API response body",
|
|
593
|
+
{ cause },
|
|
594
|
+
),
|
|
595
|
+
),
|
|
596
|
+
),
|
|
597
|
+
);
|
|
598
|
+
|
|
599
|
+
const executeQueryRequest = (
|
|
600
|
+
definition: AnalyticsEngineQueryDefinition,
|
|
601
|
+
httpClient: HttpClient.HttpClient,
|
|
602
|
+
sql: string,
|
|
603
|
+
options?: AnalyticsEngineQueryOptions,
|
|
604
|
+
) =>
|
|
605
|
+
Effect.gen(function* () {
|
|
606
|
+
const response = yield* httpClient.execute(queryRequest(definition, sql, options)).pipe(
|
|
607
|
+
Effect.mapError((cause) =>
|
|
608
|
+
analyticsEngineQueryError(definition, "query", "Analytics Engine SQL API request failed", {
|
|
609
|
+
cause,
|
|
610
|
+
}),
|
|
611
|
+
),
|
|
612
|
+
);
|
|
613
|
+
|
|
614
|
+
if (response.status < 200 || response.status >= 300) {
|
|
615
|
+
const body = yield* responseText(definition, response, "queryErrorBody");
|
|
616
|
+
return yield* Effect.fail(
|
|
617
|
+
analyticsEngineQueryError(
|
|
618
|
+
definition,
|
|
619
|
+
"query",
|
|
620
|
+
`Analytics Engine SQL API returned HTTP ${response.status}`,
|
|
621
|
+
{ status: response.status, body },
|
|
622
|
+
),
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return response;
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
const makeQueryClientWith = (
|
|
630
|
+
definition: AnalyticsEngineQueryDefinition,
|
|
631
|
+
httpClient: HttpClient.HttpClient,
|
|
632
|
+
): AnalyticsEngineQueryClient => {
|
|
633
|
+
const raw = (sql: string, options?: AnalyticsEngineQueryOptions) =>
|
|
634
|
+
executeQueryRequest(definition, httpClient, sql, options);
|
|
635
|
+
const query = (sql: string, options?: AnalyticsEngineQueryOptions) =>
|
|
636
|
+
Effect.gen(function* () {
|
|
637
|
+
const response = yield* raw(sql, options);
|
|
638
|
+
const json = yield* response.json.pipe(
|
|
639
|
+
Effect.catch((cause) =>
|
|
640
|
+
Effect.fail(
|
|
641
|
+
analyticsEngineQueryError(
|
|
642
|
+
definition,
|
|
643
|
+
"json",
|
|
644
|
+
"Failed to read Analytics Engine SQL API JSON response body",
|
|
645
|
+
{ cause },
|
|
646
|
+
),
|
|
647
|
+
),
|
|
648
|
+
),
|
|
649
|
+
);
|
|
650
|
+
return yield* decodeQueryResponse(json);
|
|
651
|
+
});
|
|
652
|
+
const queryResult = <Row>(
|
|
653
|
+
row: S.Codec<Row, unknown>,
|
|
654
|
+
sql: string,
|
|
655
|
+
options?: AnalyticsEngineQueryOptions,
|
|
656
|
+
) =>
|
|
657
|
+
Effect.gen(function* () {
|
|
658
|
+
const result = yield* query(sql, options);
|
|
659
|
+
const decodeRow = S.decodeUnknownEffect(row);
|
|
660
|
+
const data: Array<Row> = [];
|
|
661
|
+
|
|
662
|
+
for (const value of result.data) {
|
|
663
|
+
data.push(yield* decodeRow(value));
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
return {
|
|
667
|
+
...result,
|
|
668
|
+
data,
|
|
669
|
+
rows: result.rows,
|
|
670
|
+
} satisfies AnalyticsEngineQueryResult<Row>;
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
return {
|
|
674
|
+
definition,
|
|
675
|
+
raw,
|
|
676
|
+
query,
|
|
677
|
+
queryResult,
|
|
678
|
+
queryRows: (row, sql, options) =>
|
|
679
|
+
queryResult(row, sql, options).pipe(Effect.map((result) => result.data)),
|
|
680
|
+
queryOne: (row, sql, options) =>
|
|
681
|
+
queryResult(row, sql, options).pipe(
|
|
682
|
+
Effect.map((result) =>
|
|
683
|
+
result.data[0] === undefined ? Option.none() : Option.some(result.data[0]),
|
|
684
|
+
),
|
|
685
|
+
),
|
|
686
|
+
queryText: (sql, options) =>
|
|
687
|
+
raw(sql, options).pipe(
|
|
688
|
+
Effect.flatMap((response) => responseText(definition, response, "text")),
|
|
689
|
+
),
|
|
690
|
+
};
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
export const makeQueryClient = (definition: AnalyticsEngineQueryDefinition) =>
|
|
694
|
+
Effect.map(HttpClient.HttpClient, (httpClient) => makeQueryClientWith(definition, httpClient));
|
|
695
|
+
|
|
696
|
+
export const isAnalyticsEngineDataset = (value: unknown): value is AnalyticsEngineBinding => {
|
|
697
|
+
if (typeof value !== "object" || value === null) {
|
|
698
|
+
return false;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return typeof Reflect.get(value, "writeDataPoint") === "function";
|
|
702
|
+
};
|
|
703
|
+
|
|
704
|
+
export const makeClient =
|
|
705
|
+
(definition: AnalyticsEngineDefinition, defaults?: AnalyticsEngineWritePolicy) =>
|
|
706
|
+
(dataset: AnalyticsEngineBinding): AnalyticsEngineClient => {
|
|
707
|
+
const writeDataPoint = (
|
|
708
|
+
dataPoint?: AnalyticsEngineDataPoint,
|
|
709
|
+
options?: AnalyticsEngineWriteOptions,
|
|
710
|
+
) => {
|
|
711
|
+
const policy = resolveWritePolicy(defaults, options);
|
|
712
|
+
|
|
713
|
+
return validateOptionalDataPoint(definition.binding, "writeDataPoint", dataPoint).pipe(
|
|
714
|
+
Effect.matchEffect({
|
|
715
|
+
onFailure: (error) =>
|
|
716
|
+
policy.onInvalid === "drop" ? Effect.succeed(undefined) : Effect.fail(error),
|
|
717
|
+
onSuccess: (point) =>
|
|
718
|
+
tryAnalyticsEngineSync(definition.binding, "writeDataPoint", () =>
|
|
719
|
+
dataset.writeDataPoint(point),
|
|
720
|
+
),
|
|
721
|
+
}),
|
|
722
|
+
);
|
|
723
|
+
};
|
|
724
|
+
|
|
725
|
+
const writeDataPoints = (
|
|
726
|
+
dataPoints: ReadonlyArray<AnalyticsEngineDataPoint>,
|
|
727
|
+
options?: AnalyticsEngineWriteBatchOptions,
|
|
728
|
+
) => {
|
|
729
|
+
const policy = resolveWritePolicy(defaults, options);
|
|
730
|
+
|
|
731
|
+
return Effect.gen(function* () {
|
|
732
|
+
const validPoints = yield* validateDataPoints(
|
|
733
|
+
definition.binding,
|
|
734
|
+
"writeDataPoints",
|
|
735
|
+
dataPoints,
|
|
736
|
+
policy,
|
|
737
|
+
);
|
|
738
|
+
|
|
739
|
+
yield* writeChunks(
|
|
740
|
+
definition.binding,
|
|
741
|
+
"writeDataPoints",
|
|
742
|
+
dataset,
|
|
743
|
+
validPoints,
|
|
744
|
+
policy.batchSize,
|
|
745
|
+
);
|
|
746
|
+
});
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
return {
|
|
750
|
+
definition,
|
|
751
|
+
writeDataPoint,
|
|
752
|
+
write: writeDataPoint,
|
|
753
|
+
writeDataPoints,
|
|
754
|
+
writeBatch: writeDataPoints,
|
|
755
|
+
unsafeRaw: Effect.succeed(dataset),
|
|
756
|
+
};
|
|
757
|
+
};
|
|
758
|
+
|
|
759
|
+
export const queryConfig = (options?: QueryConfigOptions) =>
|
|
760
|
+
Config.all({
|
|
761
|
+
accountId: options?.accountId ?? Config.string("CLOUDFLARE_ACCOUNT_ID"),
|
|
762
|
+
apiToken: options?.apiToken ?? Config.redacted("CLOUDFLARE_API_TOKEN"),
|
|
763
|
+
apiBaseUrl:
|
|
764
|
+
options?.apiBaseUrl ??
|
|
765
|
+
Config.string("CLOUDFLARE_API_BASE_URL").pipe(Config.withDefault(defaultQueryApiBaseUrl)),
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
export const queryLayer = <Self>(
|
|
769
|
+
tag: Context.Service<Self, AnalyticsEngineQueryClient>,
|
|
770
|
+
definition: AnalyticsEngineQueryDefinition,
|
|
771
|
+
) => Layer.effect(tag, makeQueryClient(definition));
|
|
772
|
+
|
|
773
|
+
export const queryFetchLayer = <Self>(
|
|
774
|
+
tag: Context.Service<Self, AnalyticsEngineQueryClient>,
|
|
775
|
+
definition: AnalyticsEngineQueryDefinition,
|
|
776
|
+
) => queryLayer(tag, definition).pipe(Layer.provide(FetchHttpClient.layer));
|
|
777
|
+
|
|
778
|
+
export const queryLayerConfig = <Self>(
|
|
779
|
+
tag: Context.Service<Self, AnalyticsEngineQueryClient>,
|
|
780
|
+
config: Config.Config<AnalyticsEngineQueryDefinition> = queryConfig(),
|
|
781
|
+
) =>
|
|
782
|
+
Layer.effect(
|
|
783
|
+
tag,
|
|
784
|
+
Effect.gen(function* () {
|
|
785
|
+
const definition = yield* config;
|
|
786
|
+
return yield* makeQueryClient(definition);
|
|
787
|
+
}),
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
export const queryFetchLayerConfig = <Self>(
|
|
791
|
+
tag: Context.Service<Self, AnalyticsEngineQueryClient>,
|
|
792
|
+
config: Config.Config<AnalyticsEngineQueryDefinition> = queryConfig(),
|
|
793
|
+
) => queryLayerConfig(tag, config).pipe(Layer.provide(FetchHttpClient.layer));
|
|
794
|
+
|
|
795
|
+
export const layer = <Self>(
|
|
796
|
+
tag: Context.Service<Self, AnalyticsEngineClient>,
|
|
797
|
+
definition: LayerOptions,
|
|
798
|
+
) =>
|
|
799
|
+
Binding.layer(
|
|
800
|
+
tag,
|
|
801
|
+
definition.binding,
|
|
802
|
+
isAnalyticsEngineDataset,
|
|
803
|
+
makeClient(definition, definition.write),
|
|
804
|
+
{
|
|
805
|
+
expected: expectedAnalyticsEngineDataset,
|
|
806
|
+
},
|
|
807
|
+
);
|
|
808
|
+
|
|
809
|
+
export const make = <Id extends string>(id: Id) => Tag<AnalyticsEngineService<Id>>()<Id>(id);
|
|
810
|
+
|
|
811
|
+
export const makeQuery = <Id extends string>(id: Id) =>
|
|
812
|
+
QueryTag<AnalyticsEngineQueryService<Id>>()<Id>(id);
|
|
813
|
+
|
|
814
|
+
export const Tag =
|
|
815
|
+
<Self>() =>
|
|
816
|
+
<Id extends string>(id: Id) => {
|
|
817
|
+
const tag = Context.Service<Self, AnalyticsEngineClient>()(id);
|
|
818
|
+
|
|
819
|
+
const makeLayer = (definition: LayerOptions) => layer(tag, definition);
|
|
820
|
+
|
|
821
|
+
return Object.assign(tag, {
|
|
822
|
+
id,
|
|
823
|
+
layer: makeLayer,
|
|
824
|
+
}) as TagClass<Self, Id>;
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
export const QueryTag =
|
|
828
|
+
<Self>() =>
|
|
829
|
+
<Id extends string>(id: Id) => {
|
|
830
|
+
const tag = Context.Service<Self, AnalyticsEngineQueryClient>()(id);
|
|
831
|
+
const makeLayer = (definition: AnalyticsEngineQueryDefinition) => queryLayer(tag, definition);
|
|
832
|
+
const makeFetchLayer = (definition: AnalyticsEngineQueryDefinition) =>
|
|
833
|
+
queryFetchLayer(tag, definition);
|
|
834
|
+
const makeLayerConfig = (config?: Config.Config<AnalyticsEngineQueryDefinition>) =>
|
|
835
|
+
queryLayerConfig(tag, config);
|
|
836
|
+
const makeFetchLayerConfig = (config?: Config.Config<AnalyticsEngineQueryDefinition>) =>
|
|
837
|
+
queryFetchLayerConfig(tag, config);
|
|
838
|
+
|
|
839
|
+
return Object.assign(tag, {
|
|
840
|
+
id,
|
|
841
|
+
layer: makeLayer,
|
|
842
|
+
fetchLayer: makeFetchLayer,
|
|
843
|
+
layerConfig: makeLayerConfig,
|
|
844
|
+
fetchLayerConfig: makeFetchLayerConfig,
|
|
845
|
+
}) as QueryTagClass<Self, Id>;
|
|
846
|
+
};
|