@amerged/ohmyhost-cli 0.1.41 → 0.1.42
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 +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-SB6RMO3M.js → chunk-HBM2X2MV.js} +1592 -214
- package/dist/index.js +5 -3
- package/package.json +1 -1
- package/src/apps/product-cli/package.json +1 -1
- package/src/apps/product-cli/src/cli.ts +438 -38
- package/src/apps/product-cli/src/command.ts +490 -3
- package/src/apps/product-cli/src/composition.ts +6 -3
- package/src/apps/product-cli/src/product-api.ts +363 -0
- package/src/packages/contracts/package.json +5 -0
- package/src/packages/contracts/src/database-access.ts +24 -45
- package/src/packages/contracts/src/database-events.ts +697 -0
- package/src/packages/contracts/src/problem-policy.ts +28 -0
- package/src/packages/postgres-bridge/src/bridge.ts +0 -4
- package/src/packages/sdk-ts/package.json +1 -1
- package/src/packages/sdk-ts/src/generated/index.ts +109 -0
- package/src/packages/sdk-ts/src/generated/sdk.gen.ts +696 -0
- package/src/packages/sdk-ts/src/generated/types.gen.ts +1153 -25
|
@@ -0,0 +1,697 @@
|
|
|
1
|
+
import type { DatabaseEnvironment } from "./database-access.js";
|
|
2
|
+
|
|
3
|
+
/** Customer webhooks may subscribe only to these database lifecycle events. */
|
|
4
|
+
export const DATABASE_WEBHOOK_EVENT_TYPES = [
|
|
5
|
+
"database.created",
|
|
6
|
+
"database.ready",
|
|
7
|
+
"database.woke",
|
|
8
|
+
"database.slept",
|
|
9
|
+
"database.suspended",
|
|
10
|
+
"database.resized",
|
|
11
|
+
"database.deleted",
|
|
12
|
+
] as const;
|
|
13
|
+
export type DatabaseWebhookEventType = (typeof DATABASE_WEBHOOK_EVENT_TYPES)[number];
|
|
14
|
+
/** Per environment; the platform-wide provider cap is shared by every customer. */
|
|
15
|
+
export const DATABASE_WEBHOOK_LIMIT = 5;
|
|
16
|
+
export const DATABASE_WEBHOOK_MAX_EVENT_TYPES = 16;
|
|
17
|
+
export const DATABASE_WEBHOOK_MAX_HEADERS = 8;
|
|
18
|
+
export const DATABASE_WEBHOOK_URL_MAX_LENGTH = 2048;
|
|
19
|
+
|
|
20
|
+
export const DATABASE_SCHEDULE_MIN_EVERY_SECONDS = 60;
|
|
21
|
+
export const DATABASE_SCHEDULE_MAX_EVERY_SECONDS = 31 * 86_400;
|
|
22
|
+
export const DATABASE_SCHEDULE_CRON_MIN_LENGTH = 9;
|
|
23
|
+
export const DATABASE_SCHEDULE_CRON_MAX_LENGTH = 120;
|
|
24
|
+
export const DATABASE_SCHEDULE_MAX_STATEMENTS = 20;
|
|
25
|
+
export const DATABASE_SCHEDULE_STATEMENT_MAX_BYTES = 16 * 1024;
|
|
26
|
+
export const DATABASE_SCHEDULE_STATEMENTS_MAX_BYTES = 64 * 1024;
|
|
27
|
+
export const DATABASE_SCHEDULE_MIN_TIMEOUT_MS = 1_000;
|
|
28
|
+
export const DATABASE_SCHEDULE_DEFAULT_TIMEOUT_MS = 10_000;
|
|
29
|
+
export const DATABASE_SCHEDULE_MAX_TIMEOUT_MS = 30_000;
|
|
30
|
+
|
|
31
|
+
export const DATABASE_SCHEDULE_OUTCOMES = [
|
|
32
|
+
"succeeded",
|
|
33
|
+
"failed",
|
|
34
|
+
"timed_out",
|
|
35
|
+
"outcome_unknown",
|
|
36
|
+
"rejected",
|
|
37
|
+
"skipped_capacity",
|
|
38
|
+
"skipped_suspended",
|
|
39
|
+
"skipped_deleted",
|
|
40
|
+
"skipped_missed",
|
|
41
|
+
] as const;
|
|
42
|
+
export type DatabaseScheduleOutcome = (typeof DATABASE_SCHEDULE_OUTCOMES)[number];
|
|
43
|
+
|
|
44
|
+
export const DATABASE_EXECUTION_METRICS = [
|
|
45
|
+
"schedule_runs_succeeded",
|
|
46
|
+
"schedule_runs_failed",
|
|
47
|
+
"schedule_runs_timed_out",
|
|
48
|
+
"schedule_runs_outcome_unknown",
|
|
49
|
+
"schedule_runs_rejected",
|
|
50
|
+
"schedule_runs_skipped",
|
|
51
|
+
"events_emitted",
|
|
52
|
+
"event_bytes",
|
|
53
|
+
"webhook_attempts",
|
|
54
|
+
"webhook_deliveries_delivered",
|
|
55
|
+
"webhook_deliveries_dead_lettered",
|
|
56
|
+
] as const;
|
|
57
|
+
export type DatabaseExecutionMetric = (typeof DATABASE_EXECUTION_METRICS)[number];
|
|
58
|
+
export type DatabaseExecutionMetrics = Readonly<Record<DatabaseExecutionMetric, number>>;
|
|
59
|
+
|
|
60
|
+
/** Upper bound for every list page, whatever the route default is. */
|
|
61
|
+
export const DATABASE_PAGE_MAX_ITEMS = 100;
|
|
62
|
+
const USAGE_HOUR_MS = 3_600_000;
|
|
63
|
+
const USAGE_MAX_RANGE_MS = 31 * 24 * USAGE_HOUR_MS;
|
|
64
|
+
|
|
65
|
+
export type DatabaseWebhookDeliveryState = "pending" | "delivered" | "dead_lettered";
|
|
66
|
+
export type DatabaseWebhookDeliveryError = "timeout" | "network" | "http" | "expired" | "permanent";
|
|
67
|
+
export type DatabaseWebhookTestError = "timeout" | "network" | "http";
|
|
68
|
+
|
|
69
|
+
export interface DatabaseWebhookCreateRequest {
|
|
70
|
+
readonly url: string;
|
|
71
|
+
readonly event_types: readonly DatabaseWebhookEventType[];
|
|
72
|
+
readonly headers: Readonly<Record<string, string>>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface DatabaseScheduleSetRequest {
|
|
76
|
+
readonly cron: string | null;
|
|
77
|
+
readonly every_seconds: number | null;
|
|
78
|
+
readonly statements: readonly string[];
|
|
79
|
+
readonly timeout_ms: number;
|
|
80
|
+
readonly enabled: boolean;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface DatabaseUsageWindow {
|
|
84
|
+
readonly from: string;
|
|
85
|
+
readonly to: string;
|
|
86
|
+
readonly granularity: "hour" | "day";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface DatabaseWebhook {
|
|
90
|
+
readonly id: string;
|
|
91
|
+
readonly environment: DatabaseEnvironment;
|
|
92
|
+
readonly url: string;
|
|
93
|
+
readonly event_types: readonly DatabaseWebhookEventType[];
|
|
94
|
+
readonly header_names: readonly string[];
|
|
95
|
+
readonly created_at: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** The creation answer is the only place that shows the signing secret. */
|
|
99
|
+
export interface DatabaseWebhookCreated extends DatabaseWebhook {
|
|
100
|
+
readonly secret: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface DatabaseWebhookPage {
|
|
104
|
+
readonly items: readonly DatabaseWebhook[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface DatabaseWebhookDeleted {
|
|
108
|
+
readonly id: string;
|
|
109
|
+
readonly deleted: true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface DatabaseWebhookDelivery {
|
|
113
|
+
readonly id: string;
|
|
114
|
+
readonly event_id: string;
|
|
115
|
+
readonly event_type: DatabaseWebhookEventType;
|
|
116
|
+
readonly webhook_id: string;
|
|
117
|
+
readonly state: DatabaseWebhookDeliveryState;
|
|
118
|
+
readonly attempts: number;
|
|
119
|
+
readonly created_at: string;
|
|
120
|
+
readonly next_attempt_at: string | null;
|
|
121
|
+
readonly delivered_at: string | null;
|
|
122
|
+
readonly last_status: number | null;
|
|
123
|
+
readonly last_error: DatabaseWebhookDeliveryError | null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface DatabaseWebhookDeliveryPage {
|
|
127
|
+
readonly items: readonly DatabaseWebhookDelivery[];
|
|
128
|
+
readonly next_cursor: string | null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface DatabaseWebhookTestResult {
|
|
132
|
+
readonly delivered: boolean;
|
|
133
|
+
readonly status: number | null;
|
|
134
|
+
readonly error: DatabaseWebhookTestError | null;
|
|
135
|
+
readonly duration_ms: number;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface DatabaseWebhookReplayAccepted {
|
|
139
|
+
readonly delivery_id: string;
|
|
140
|
+
readonly state: "pending";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface DatabaseSchedule {
|
|
144
|
+
readonly name: string;
|
|
145
|
+
readonly environment: DatabaseEnvironment;
|
|
146
|
+
readonly cron: string | null;
|
|
147
|
+
readonly every_seconds: number | null;
|
|
148
|
+
readonly statements: readonly string[];
|
|
149
|
+
readonly timeout_ms: number;
|
|
150
|
+
readonly enabled: boolean;
|
|
151
|
+
readonly next_run_at: string | null;
|
|
152
|
+
readonly created_at: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface DatabaseSchedulePage {
|
|
156
|
+
readonly items: readonly DatabaseSchedule[];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface DatabaseScheduleDeleted {
|
|
160
|
+
readonly name: string;
|
|
161
|
+
readonly deleted: true;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface DatabaseScheduleRun {
|
|
165
|
+
readonly id: number;
|
|
166
|
+
readonly schedule: string;
|
|
167
|
+
readonly scheduled_at: string;
|
|
168
|
+
readonly started_at: string | null;
|
|
169
|
+
readonly finished_at: string;
|
|
170
|
+
readonly outcome: DatabaseScheduleOutcome;
|
|
171
|
+
readonly sqlstate: string | null;
|
|
172
|
+
readonly attempts: number;
|
|
173
|
+
readonly manual: boolean;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export interface DatabaseScheduleRunPage {
|
|
177
|
+
readonly items: readonly DatabaseScheduleRun[];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
export interface DatabaseScheduleRunAccepted {
|
|
181
|
+
readonly schedule: string;
|
|
182
|
+
readonly state: "queued";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface DatabaseExecutionUsageRow {
|
|
186
|
+
readonly start: string;
|
|
187
|
+
readonly end: string;
|
|
188
|
+
readonly final: boolean;
|
|
189
|
+
readonly metrics: DatabaseExecutionMetrics;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface DatabaseExecutionUsagePage {
|
|
193
|
+
readonly items: readonly DatabaseExecutionUsageRow[];
|
|
194
|
+
readonly next_cursor: string | null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const WEBHOOK_KEYS = ["id", "environment", "url", "event_types", "header_names", "created_at"];
|
|
198
|
+
const WEBHOOK_ID_PATTERN = /^whk_[a-z0-9]{20}$/u;
|
|
199
|
+
const DELIVERY_ID_PATTERN = /^dlv_[0-9a-f]{20}$/u;
|
|
200
|
+
const EVENT_ID_PATTERN = /^evt_[0-9a-f]{20}$/u;
|
|
201
|
+
const SCHEDULE_NAME_PATTERN = /^[a-z][a-z0-9_-]{0,62}$/u;
|
|
202
|
+
const CURSOR_PATTERN = /^[A-Za-z0-9._|:=+/-]{1,200}$/u;
|
|
203
|
+
const HEADER_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9-]{0,63}$/u;
|
|
204
|
+
const HEADER_VALUE_PATTERN = /^[\x21-\x7e]([\x20-\x7e]*[\x21-\x7e])?$/u;
|
|
205
|
+
const HEADER_FORBIDDEN_NAMES = [
|
|
206
|
+
"host",
|
|
207
|
+
"content-length",
|
|
208
|
+
"content-type",
|
|
209
|
+
"connection",
|
|
210
|
+
"transfer-encoding",
|
|
211
|
+
"user-agent",
|
|
212
|
+
"te",
|
|
213
|
+
"upgrade",
|
|
214
|
+
"expect",
|
|
215
|
+
];
|
|
216
|
+
const HEADER_FORBIDDEN_PREFIXES = ["pgcf-", "cf-", "x-forwarded-"];
|
|
217
|
+
|
|
218
|
+
export function parseDatabaseEnvironment(value: unknown): DatabaseEnvironment {
|
|
219
|
+
if (value !== "dev" && value !== "prod") throw new TypeError("Invalid database environment");
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function parseDatabaseWebhookId(value: unknown): string {
|
|
224
|
+
if (!matches(value, WEBHOOK_ID_PATTERN)) throw new TypeError("Invalid database webhook id");
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function parseDatabaseDeliveryId(value: unknown): string {
|
|
229
|
+
if (!matches(value, DELIVERY_ID_PATTERN)) throw new TypeError("Invalid database delivery id");
|
|
230
|
+
return value;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function parseDatabaseScheduleName(value: unknown): string {
|
|
234
|
+
if (!matches(value, SCHEDULE_NAME_PATTERN)) throw new TypeError("Invalid database schedule name");
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** A missing value takes the fallback; query strings are decimal digits only. */
|
|
239
|
+
export function parseDatabaseListLimit(value: unknown, max: number, fallback: number): number {
|
|
240
|
+
if (value === undefined) return fallback;
|
|
241
|
+
const limit =
|
|
242
|
+
typeof value === "string" && /^[1-9][0-9]{0,3}$/u.test(value) ? Number(value) : value;
|
|
243
|
+
if (typeof limit !== "number" || !Number.isSafeInteger(limit) || limit < 1 || limit > max)
|
|
244
|
+
throw new TypeError("Invalid database list limit");
|
|
245
|
+
return limit;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function parseDatabaseCursor(value: unknown): string | null {
|
|
249
|
+
if (value === undefined || value === null) return null;
|
|
250
|
+
if (!matches(value, CURSOR_PATTERN)) throw new TypeError("Invalid database cursor");
|
|
251
|
+
return value;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Usage windows are UTC, aligned to the granularity and at most 31 days long. */
|
|
255
|
+
export function parseDatabaseUsageWindow(
|
|
256
|
+
from: unknown,
|
|
257
|
+
to: unknown,
|
|
258
|
+
granularity: unknown,
|
|
259
|
+
): DatabaseUsageWindow {
|
|
260
|
+
if (!timestamp(from) || !timestamp(to) || !oneOf(["hour", "day"] as const, granularity))
|
|
261
|
+
throw new TypeError("Invalid database usage window");
|
|
262
|
+
const start = Date.parse(from),
|
|
263
|
+
end = Date.parse(to),
|
|
264
|
+
unit = granularity === "hour" ? USAGE_HOUR_MS : 24 * USAGE_HOUR_MS;
|
|
265
|
+
if (end <= start || end - start > USAGE_MAX_RANGE_MS || start % unit !== 0 || end % unit !== 0)
|
|
266
|
+
throw new TypeError("Invalid database usage window");
|
|
267
|
+
return { from, to, granularity };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** Request bodies carry only the customer's fields; project, database and role are never accepted. */
|
|
271
|
+
export function parseDatabaseWebhookCreateRequest(value: unknown): DatabaseWebhookCreateRequest {
|
|
272
|
+
if (!record(value) || !onlyKeys(value, ["url", "event_types", "headers"]))
|
|
273
|
+
throw new TypeError("Invalid database webhook request");
|
|
274
|
+
return {
|
|
275
|
+
url: parseHttpsUrl(value["url"]),
|
|
276
|
+
event_types: parseEventTypes(value["event_types"]),
|
|
277
|
+
headers: value["headers"] === undefined ? {} : parseHeaders(value["headers"]),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function parseDatabaseScheduleSetRequest(value: unknown): DatabaseScheduleSetRequest {
|
|
282
|
+
if (
|
|
283
|
+
!record(value) ||
|
|
284
|
+
!onlyKeys(value, ["cron", "every_seconds", "statements", "timeout_ms", "enabled", "confirm"]) ||
|
|
285
|
+
value["confirm"] !== true ||
|
|
286
|
+
(value["cron"] === undefined) === (value["every_seconds"] === undefined)
|
|
287
|
+
)
|
|
288
|
+
throw new TypeError("Invalid database schedule request");
|
|
289
|
+
const enabled = value["enabled"] === undefined ? true : value["enabled"];
|
|
290
|
+
if (typeof enabled !== "boolean") throw new TypeError("Invalid database schedule request");
|
|
291
|
+
return {
|
|
292
|
+
cron: value["cron"] === undefined ? null : parseCron(value["cron"]),
|
|
293
|
+
every_seconds:
|
|
294
|
+
value["every_seconds"] === undefined ? null : parseEverySeconds(value["every_seconds"]),
|
|
295
|
+
statements: parseStatements(value["statements"]),
|
|
296
|
+
timeout_ms:
|
|
297
|
+
value["timeout_ms"] === undefined
|
|
298
|
+
? DATABASE_SCHEDULE_DEFAULT_TIMEOUT_MS
|
|
299
|
+
: parseTimeout(value["timeout_ms"]),
|
|
300
|
+
enabled,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function assertDatabaseWebhook(value: unknown): asserts value is DatabaseWebhook {
|
|
305
|
+
if (!record(value) || !exact(value, WEBHOOK_KEYS))
|
|
306
|
+
throw new TypeError("Invalid database webhook");
|
|
307
|
+
assertWebhookFields(value);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function assertDatabaseWebhookCreated(
|
|
311
|
+
value: unknown,
|
|
312
|
+
): asserts value is DatabaseWebhookCreated {
|
|
313
|
+
if (
|
|
314
|
+
!record(value) ||
|
|
315
|
+
!exact(value, [...WEBHOOK_KEYS, "secret"]) ||
|
|
316
|
+
typeof value["secret"] !== "string" ||
|
|
317
|
+
value["secret"].length === 0
|
|
318
|
+
)
|
|
319
|
+
throw new TypeError("Invalid database webhook");
|
|
320
|
+
assertWebhookFields(value);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function assertDatabaseWebhookPage(value: unknown): asserts value is DatabaseWebhookPage {
|
|
324
|
+
if (!record(value) || !exact(value, ["items"]) || !pageItems(value["items"]))
|
|
325
|
+
throw new TypeError("Invalid database webhook page");
|
|
326
|
+
for (const item of value["items"]) assertDatabaseWebhook(item);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function assertDatabaseWebhookDeleted(
|
|
330
|
+
value: unknown,
|
|
331
|
+
): asserts value is DatabaseWebhookDeleted {
|
|
332
|
+
if (
|
|
333
|
+
!record(value) ||
|
|
334
|
+
!exact(value, ["id", "deleted"]) ||
|
|
335
|
+
!matches(value["id"], WEBHOOK_ID_PATTERN) ||
|
|
336
|
+
value["deleted"] !== true
|
|
337
|
+
)
|
|
338
|
+
throw new TypeError("Invalid database webhook deletion");
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function assertDatabaseWebhookDelivery(
|
|
342
|
+
value: unknown,
|
|
343
|
+
): asserts value is DatabaseWebhookDelivery {
|
|
344
|
+
if (
|
|
345
|
+
!record(value) ||
|
|
346
|
+
!exact(value, [
|
|
347
|
+
"id",
|
|
348
|
+
"event_id",
|
|
349
|
+
"event_type",
|
|
350
|
+
"webhook_id",
|
|
351
|
+
"state",
|
|
352
|
+
"attempts",
|
|
353
|
+
"created_at",
|
|
354
|
+
"next_attempt_at",
|
|
355
|
+
"delivered_at",
|
|
356
|
+
"last_status",
|
|
357
|
+
"last_error",
|
|
358
|
+
]) ||
|
|
359
|
+
!matches(value["id"], DELIVERY_ID_PATTERN) ||
|
|
360
|
+
!matches(value["event_id"], EVENT_ID_PATTERN) ||
|
|
361
|
+
!oneOf(DATABASE_WEBHOOK_EVENT_TYPES, value["event_type"]) ||
|
|
362
|
+
!matches(value["webhook_id"], WEBHOOK_ID_PATTERN) ||
|
|
363
|
+
!oneOf(["pending", "delivered", "dead_lettered"] as const, value["state"]) ||
|
|
364
|
+
!nonNegativeInteger(value["attempts"]) ||
|
|
365
|
+
!timestamp(value["created_at"]) ||
|
|
366
|
+
!nullable(value["next_attempt_at"], timestamp) ||
|
|
367
|
+
!nullable(value["delivered_at"], timestamp) ||
|
|
368
|
+
!nullable(value["last_status"], Number.isSafeInteger) ||
|
|
369
|
+
!nullable(value["last_error"], (error) =>
|
|
370
|
+
oneOf(["timeout", "network", "http", "expired", "permanent"] as const, error),
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
throw new TypeError("Invalid database webhook delivery");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function assertDatabaseWebhookDeliveryPage(
|
|
377
|
+
value: unknown,
|
|
378
|
+
): asserts value is DatabaseWebhookDeliveryPage {
|
|
379
|
+
if (!record(value) || !exact(value, ["items", "next_cursor"]) || !pageItems(value["items"]))
|
|
380
|
+
throw new TypeError("Invalid database webhook delivery page");
|
|
381
|
+
for (const item of value["items"]) assertDatabaseWebhookDelivery(item);
|
|
382
|
+
if (!nullable(value["next_cursor"], isCursor))
|
|
383
|
+
throw new TypeError("Invalid database webhook delivery page");
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function assertDatabaseWebhookTest(
|
|
387
|
+
value: unknown,
|
|
388
|
+
): asserts value is DatabaseWebhookTestResult {
|
|
389
|
+
if (
|
|
390
|
+
!record(value) ||
|
|
391
|
+
!exact(value, ["delivered", "status", "error", "duration_ms"]) ||
|
|
392
|
+
typeof value["delivered"] !== "boolean" ||
|
|
393
|
+
!nullable(value["status"], Number.isSafeInteger) ||
|
|
394
|
+
!nullable(value["error"], (error) => oneOf(["timeout", "network", "http"] as const, error)) ||
|
|
395
|
+
!nonNegativeInteger(value["duration_ms"])
|
|
396
|
+
)
|
|
397
|
+
throw new TypeError("Invalid database webhook test result");
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export function assertDatabaseWebhookReplayAccepted(
|
|
401
|
+
value: unknown,
|
|
402
|
+
): asserts value is DatabaseWebhookReplayAccepted {
|
|
403
|
+
if (
|
|
404
|
+
!record(value) ||
|
|
405
|
+
!exact(value, ["delivery_id", "state"]) ||
|
|
406
|
+
!matches(value["delivery_id"], DELIVERY_ID_PATTERN) ||
|
|
407
|
+
value["state"] !== "pending"
|
|
408
|
+
)
|
|
409
|
+
throw new TypeError("Invalid database webhook replay");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function assertDatabaseSchedule(value: unknown): asserts value is DatabaseSchedule {
|
|
413
|
+
if (
|
|
414
|
+
!record(value) ||
|
|
415
|
+
!exact(value, [
|
|
416
|
+
"name",
|
|
417
|
+
"environment",
|
|
418
|
+
"cron",
|
|
419
|
+
"every_seconds",
|
|
420
|
+
"statements",
|
|
421
|
+
"timeout_ms",
|
|
422
|
+
"enabled",
|
|
423
|
+
"next_run_at",
|
|
424
|
+
"created_at",
|
|
425
|
+
]) ||
|
|
426
|
+
!matches(value["name"], SCHEDULE_NAME_PATTERN) ||
|
|
427
|
+
!oneOf(["dev", "prod"] as const, value["environment"]) ||
|
|
428
|
+
(value["cron"] === null) === (value["every_seconds"] === null) ||
|
|
429
|
+
!nullable(value["cron"], isCronText) ||
|
|
430
|
+
!nullable(value["every_seconds"], isEverySeconds) ||
|
|
431
|
+
!Array.isArray(value["statements"]) ||
|
|
432
|
+
value["statements"].length > DATABASE_SCHEDULE_MAX_STATEMENTS ||
|
|
433
|
+
!value["statements"].every((statement) => typeof statement === "string") ||
|
|
434
|
+
!isTimeout(value["timeout_ms"]) ||
|
|
435
|
+
typeof value["enabled"] !== "boolean" ||
|
|
436
|
+
!nullable(value["next_run_at"], timestamp) ||
|
|
437
|
+
!timestamp(value["created_at"])
|
|
438
|
+
)
|
|
439
|
+
throw new TypeError("Invalid database schedule");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function assertDatabaseSchedulePage(value: unknown): asserts value is DatabaseSchedulePage {
|
|
443
|
+
if (!record(value) || !exact(value, ["items"]) || !pageItems(value["items"]))
|
|
444
|
+
throw new TypeError("Invalid database schedule page");
|
|
445
|
+
for (const item of value["items"]) assertDatabaseSchedule(item);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function assertDatabaseScheduleDeleted(
|
|
449
|
+
value: unknown,
|
|
450
|
+
): asserts value is DatabaseScheduleDeleted {
|
|
451
|
+
if (
|
|
452
|
+
!record(value) ||
|
|
453
|
+
!exact(value, ["name", "deleted"]) ||
|
|
454
|
+
!matches(value["name"], SCHEDULE_NAME_PATTERN) ||
|
|
455
|
+
value["deleted"] !== true
|
|
456
|
+
)
|
|
457
|
+
throw new TypeError("Invalid database schedule deletion");
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function assertDatabaseScheduleRunPage(
|
|
461
|
+
value: unknown,
|
|
462
|
+
): asserts value is DatabaseScheduleRunPage {
|
|
463
|
+
if (!record(value) || !exact(value, ["items"]) || !pageItems(value["items"]))
|
|
464
|
+
throw new TypeError("Invalid database schedule run page");
|
|
465
|
+
for (const item of value["items"]) assertDatabaseScheduleRun(item);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function assertDatabaseScheduleRunAccepted(
|
|
469
|
+
value: unknown,
|
|
470
|
+
): asserts value is DatabaseScheduleRunAccepted {
|
|
471
|
+
if (
|
|
472
|
+
!record(value) ||
|
|
473
|
+
!exact(value, ["schedule", "state"]) ||
|
|
474
|
+
!matches(value["schedule"], SCHEDULE_NAME_PATTERN) ||
|
|
475
|
+
value["state"] !== "queued"
|
|
476
|
+
)
|
|
477
|
+
throw new TypeError("Invalid database schedule run");
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
export function assertDatabaseExecutionUsagePage(
|
|
481
|
+
value: unknown,
|
|
482
|
+
): asserts value is DatabaseExecutionUsagePage {
|
|
483
|
+
if (!record(value) || !exact(value, ["items", "next_cursor"]) || !pageItems(value["items"]))
|
|
484
|
+
throw new TypeError("Invalid database execution usage page");
|
|
485
|
+
for (const row of value["items"]) assertUsageRow(row);
|
|
486
|
+
if (!nullable(value["next_cursor"], isCursor))
|
|
487
|
+
throw new TypeError("Invalid database execution usage page");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function assertDatabaseScheduleRun(value: unknown): asserts value is DatabaseScheduleRun {
|
|
491
|
+
if (
|
|
492
|
+
!record(value) ||
|
|
493
|
+
!exact(value, [
|
|
494
|
+
"id",
|
|
495
|
+
"schedule",
|
|
496
|
+
"scheduled_at",
|
|
497
|
+
"started_at",
|
|
498
|
+
"finished_at",
|
|
499
|
+
"outcome",
|
|
500
|
+
"sqlstate",
|
|
501
|
+
"attempts",
|
|
502
|
+
"manual",
|
|
503
|
+
]) ||
|
|
504
|
+
!nonNegativeInteger(value["id"]) ||
|
|
505
|
+
!matches(value["schedule"], SCHEDULE_NAME_PATTERN) ||
|
|
506
|
+
!timestamp(value["scheduled_at"]) ||
|
|
507
|
+
!nullable(value["started_at"], timestamp) ||
|
|
508
|
+
!timestamp(value["finished_at"]) ||
|
|
509
|
+
!oneOf(DATABASE_SCHEDULE_OUTCOMES, value["outcome"]) ||
|
|
510
|
+
!nullable(value["sqlstate"], (state) => typeof state === "string") ||
|
|
511
|
+
!nonNegativeInteger(value["attempts"]) ||
|
|
512
|
+
typeof value["manual"] !== "boolean"
|
|
513
|
+
)
|
|
514
|
+
throw new TypeError("Invalid database schedule run");
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function assertUsageRow(value: unknown): asserts value is DatabaseExecutionUsageRow {
|
|
518
|
+
if (
|
|
519
|
+
!record(value) ||
|
|
520
|
+
!exact(value, ["start", "end", "final", "metrics"]) ||
|
|
521
|
+
!timestamp(value["start"]) ||
|
|
522
|
+
!timestamp(value["end"]) ||
|
|
523
|
+
typeof value["final"] !== "boolean" ||
|
|
524
|
+
!record(value["metrics"]) ||
|
|
525
|
+
!exact(value["metrics"], DATABASE_EXECUTION_METRICS)
|
|
526
|
+
)
|
|
527
|
+
throw new TypeError("Invalid database execution usage row");
|
|
528
|
+
const metrics = value["metrics"];
|
|
529
|
+
if (!DATABASE_EXECUTION_METRICS.every((metric) => nonNegativeInteger(metrics[metric])))
|
|
530
|
+
throw new TypeError("Invalid database execution usage row");
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function assertWebhookFields(value: Record<string, unknown>): void {
|
|
534
|
+
if (
|
|
535
|
+
!matches(value["id"], WEBHOOK_ID_PATTERN) ||
|
|
536
|
+
!oneOf(["dev", "prod"] as const, value["environment"]) ||
|
|
537
|
+
!isHttpsUrl(value["url"]) ||
|
|
538
|
+
!Array.isArray(value["event_types"]) ||
|
|
539
|
+
value["event_types"].length < 1 ||
|
|
540
|
+
value["event_types"].length > DATABASE_WEBHOOK_MAX_EVENT_TYPES ||
|
|
541
|
+
!value["event_types"].every((type) => oneOf(DATABASE_WEBHOOK_EVENT_TYPES, type)) ||
|
|
542
|
+
!Array.isArray(value["header_names"]) ||
|
|
543
|
+
value["header_names"].length > DATABASE_WEBHOOK_MAX_HEADERS ||
|
|
544
|
+
!value["header_names"].every((name) => matches(name, HEADER_NAME_PATTERN)) ||
|
|
545
|
+
!timestamp(value["created_at"])
|
|
546
|
+
)
|
|
547
|
+
throw new TypeError("Invalid database webhook");
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function parseHttpsUrl(value: unknown): string {
|
|
551
|
+
if (!isHttpsUrl(value)) throw new TypeError("Invalid database webhook url");
|
|
552
|
+
return value;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function isHttpsUrl(value: unknown): value is string {
|
|
556
|
+
if (typeof value !== "string" || value.length > DATABASE_WEBHOOK_URL_MAX_LENGTH) return false;
|
|
557
|
+
try {
|
|
558
|
+
return new URL(value).protocol === "https:";
|
|
559
|
+
} catch {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function parseEventTypes(value: unknown): readonly DatabaseWebhookEventType[] {
|
|
565
|
+
if (
|
|
566
|
+
!Array.isArray(value) ||
|
|
567
|
+
value.length < 1 ||
|
|
568
|
+
value.length > DATABASE_WEBHOOK_MAX_EVENT_TYPES ||
|
|
569
|
+
!value.every((type) => oneOf(DATABASE_WEBHOOK_EVENT_TYPES, type)) ||
|
|
570
|
+
new Set(value).size !== value.length
|
|
571
|
+
)
|
|
572
|
+
throw new TypeError("Invalid database webhook event types");
|
|
573
|
+
return value as DatabaseWebhookEventType[];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function parseHeaders(value: unknown): Record<string, string> {
|
|
577
|
+
if (
|
|
578
|
+
!record(value) ||
|
|
579
|
+
Object.keys(value).length > DATABASE_WEBHOOK_MAX_HEADERS ||
|
|
580
|
+
new Set(Object.keys(value).map((name) => name.toLowerCase())).size !== Object.keys(value).length
|
|
581
|
+
)
|
|
582
|
+
throw new TypeError("Invalid database webhook headers");
|
|
583
|
+
const headers: Record<string, string> = {};
|
|
584
|
+
for (const [name, headerValue] of Object.entries(value)) {
|
|
585
|
+
const lower = name.toLowerCase();
|
|
586
|
+
if (
|
|
587
|
+
!HEADER_NAME_PATTERN.test(name) ||
|
|
588
|
+
HEADER_FORBIDDEN_NAMES.includes(lower) ||
|
|
589
|
+
HEADER_FORBIDDEN_PREFIXES.some((prefix) => lower.startsWith(prefix)) ||
|
|
590
|
+
typeof headerValue !== "string" ||
|
|
591
|
+
headerValue.length > 512 ||
|
|
592
|
+
!HEADER_VALUE_PATTERN.test(headerValue)
|
|
593
|
+
)
|
|
594
|
+
throw new TypeError("Invalid database webhook headers");
|
|
595
|
+
headers[name] = headerValue;
|
|
596
|
+
}
|
|
597
|
+
return headers;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function parseCron(value: unknown): string {
|
|
601
|
+
if (!isCronText(value)) throw new TypeError("Invalid database schedule cron");
|
|
602
|
+
return value;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function isCronText(value: unknown): value is string {
|
|
606
|
+
return (
|
|
607
|
+
typeof value === "string" &&
|
|
608
|
+
value.length >= DATABASE_SCHEDULE_CRON_MIN_LENGTH &&
|
|
609
|
+
value.length <= DATABASE_SCHEDULE_CRON_MAX_LENGTH
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function parseEverySeconds(value: unknown): number {
|
|
614
|
+
if (!isEverySeconds(value)) throw new TypeError("Invalid database schedule interval");
|
|
615
|
+
return value;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function isEverySeconds(value: unknown): value is number {
|
|
619
|
+
return (
|
|
620
|
+
Number.isSafeInteger(value) &&
|
|
621
|
+
(value as number) >= DATABASE_SCHEDULE_MIN_EVERY_SECONDS &&
|
|
622
|
+
(value as number) <= DATABASE_SCHEDULE_MAX_EVERY_SECONDS
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
function parseTimeout(value: unknown): number {
|
|
627
|
+
if (!isTimeout(value)) throw new TypeError("Invalid database schedule timeout");
|
|
628
|
+
return value;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function isTimeout(value: unknown): value is number {
|
|
632
|
+
return (
|
|
633
|
+
Number.isSafeInteger(value) &&
|
|
634
|
+
(value as number) >= DATABASE_SCHEDULE_MIN_TIMEOUT_MS &&
|
|
635
|
+
(value as number) <= DATABASE_SCHEDULE_MAX_TIMEOUT_MS
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function parseStatements(value: unknown): readonly string[] {
|
|
640
|
+
if (
|
|
641
|
+
!Array.isArray(value) ||
|
|
642
|
+
value.length < 1 ||
|
|
643
|
+
value.length > DATABASE_SCHEDULE_MAX_STATEMENTS ||
|
|
644
|
+
!value.every(
|
|
645
|
+
(statement) =>
|
|
646
|
+
typeof statement === "string" &&
|
|
647
|
+
statement.trim().length > 0 &&
|
|
648
|
+
!statement.includes("\0") &&
|
|
649
|
+
new TextEncoder().encode(statement).byteLength <= DATABASE_SCHEDULE_STATEMENT_MAX_BYTES,
|
|
650
|
+
) ||
|
|
651
|
+
new TextEncoder().encode(value.join("")).byteLength > DATABASE_SCHEDULE_STATEMENTS_MAX_BYTES
|
|
652
|
+
)
|
|
653
|
+
throw new TypeError("Invalid database schedule statements");
|
|
654
|
+
return value as string[];
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function isCursor(value: unknown): value is string {
|
|
658
|
+
return matches(value, CURSOR_PATTERN);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function pageItems(value: unknown): value is unknown[] {
|
|
662
|
+
return Array.isArray(value) && value.length <= DATABASE_PAGE_MAX_ITEMS;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function nonNegativeInteger(value: unknown): value is number {
|
|
666
|
+
return Number.isSafeInteger(value) && (value as number) >= 0;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function nullable(value: unknown, check: (value: unknown) => boolean): boolean {
|
|
670
|
+
return value === null || check(value);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function matches(value: unknown, pattern: RegExp): value is string {
|
|
674
|
+
return typeof value === "string" && pattern.test(value);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function oneOf<T extends string>(allowed: readonly T[], value: unknown): value is T {
|
|
678
|
+
return typeof value === "string" && (allowed as readonly string[]).includes(value);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function timestamp(value: unknown): value is string {
|
|
682
|
+
if (typeof value !== "string") return false;
|
|
683
|
+
const time = Date.parse(value);
|
|
684
|
+
return Number.isFinite(time) && new Date(time).toISOString() === value;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function record(value: unknown): value is Record<string, unknown> {
|
|
688
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function onlyKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
|
|
692
|
+
return Object.keys(value).every((key) => allowed.includes(key));
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function exact(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
696
|
+
return Object.keys(value).sort().join(",") === [...keys].sort().join(",");
|
|
697
|
+
}
|