@amerged/ohmyhost-mcp 0.1.44 → 0.1.46

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.
@@ -6,10 +6,17 @@ import {
6
6
  parseDatabaseListLimit,
7
7
  parseDatabaseScheduleName,
8
8
  parseDatabaseUsageWindow,
9
+ parseDatabaseWatchCreateRequest,
10
+ parseDatabaseWatchId,
9
11
  parseDatabaseWebhookCreateRequest,
10
12
  parseDatabaseWebhookId,
11
- type DatabaseWebhookEventType,
13
+ type DatabaseWatchRequest,
14
+ type DatabaseWebhookEventPattern,
12
15
  } from "@ohmyhost/contracts/database-events";
16
+ import {
17
+ DATABASE_TABLE_MAX_PRIMARY_KEY_COLUMNS,
18
+ isDatabaseTableIdentifier,
19
+ } from "@ohmyhost/contracts/database-tables";
13
20
  import {
14
21
  databaseSizeForMemory,
15
22
  type DatabaseComputeSelection,
@@ -404,12 +411,47 @@ export type ProductCliCommand =
404
411
  readonly yes: true;
405
412
  readonly credentialStore: "native";
406
413
  }
414
+ | {
415
+ readonly kind: "database-watch-create";
416
+ readonly projectId: string;
417
+ readonly environment: "dev" | "prod";
418
+ readonly request: DatabaseWatchRequest;
419
+ readonly idempotencyKey: string;
420
+ readonly yes: true;
421
+ readonly credentialStore: "native";
422
+ }
423
+ | {
424
+ readonly kind: "database-table-create";
425
+ readonly projectId: string;
426
+ readonly environment: "dev" | "prod";
427
+ readonly table: string;
428
+ readonly columnsFile: string;
429
+ readonly primaryKey: readonly string[];
430
+ readonly idempotencyKey: string;
431
+ readonly yes: true;
432
+ readonly credentialStore: "native";
433
+ }
434
+ | {
435
+ readonly kind: "database-watch-list";
436
+ readonly projectId: string;
437
+ readonly environment: "dev" | "prod";
438
+ readonly credentialStore: "native";
439
+ }
440
+ | {
441
+ readonly kind: "database-watch-delete";
442
+ readonly projectId: string;
443
+ readonly environment: "dev" | "prod";
444
+ readonly watchId: string;
445
+ readonly idempotencyKey: string;
446
+ readonly yes: true;
447
+ readonly credentialStore: "native";
448
+ }
407
449
  | {
408
450
  readonly kind: "database-webhook-create";
409
451
  readonly projectId: string;
410
452
  readonly environment: "dev" | "prod";
411
453
  readonly url: string;
412
- readonly eventTypes: readonly DatabaseWebhookEventType[];
454
+ readonly eventTypes: readonly DatabaseWebhookEventPattern[];
413
455
  readonly headersFile: string | null;
414
456
  readonly idempotencyKey: string;
415
457
  readonly yes: true;
@@ -1003,6 +1045,10 @@ export type RecognizedCommandLabel =
1003
1045
  | "database access create"
1004
1046
  | "database access list"
1005
1047
  | "database access revoke"
1048
+ | "database watch create"
1049
+ | "database watch list"
1050
+ | "database watch delete"
1051
+ | "database table create"
1006
1052
  | "database webhook create"
1007
1053
  | "database webhook list"
1008
1054
  | "database webhook get"
@@ -1174,6 +1220,13 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
1174
1220
  if (values[1] === "list") return "database access list";
1175
1221
  if (values[1] === "revoke") return "database access revoke";
1176
1222
  }
1223
+ if (value === "database" && values[0] === "watch") {
1224
+ if (values[1] === "create") return "database watch create";
1225
+ if (values[1] === "list") return "database watch list";
1226
+ if (values[1] === "delete") return "database watch delete";
1227
+ }
1228
+ if (value === "database" && values[0] === "table" && values[1] === "create")
1229
+ return "database table create";
1177
1230
  if (value === "database" && values[0] === "webhook") {
1178
1231
  if (values[1] === "create") return "database webhook create";
1179
1232
  if (values[1] === "list") return "database webhook list";
@@ -2063,6 +2116,137 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
2063
2116
  }
2064
2117
  throw new InvalidCommandError();
2065
2118
  }
2119
+ if (first === "database" && values[0] === "watch") {
2120
+ values.shift();
2121
+ const subcommand = values.shift();
2122
+ if (subcommand === "create") {
2123
+ if (!yes || wait || follow || revoke || stdin) throw new InvalidCommandError();
2124
+ const options = parseOptionalOptions(values, [
2125
+ "--project",
2126
+ "--environment",
2127
+ "--table",
2128
+ "--schema",
2129
+ "--columns",
2130
+ "--operations",
2131
+ "--idempotency-key",
2132
+ ]);
2133
+ const projectId = options["--project"],
2134
+ environment = options["--environment"],
2135
+ table = options["--table"],
2136
+ schema = options["--schema"],
2137
+ columns = options["--columns"],
2138
+ operations = options["--operations"],
2139
+ idempotencyKey = options["--idempotency-key"];
2140
+ if (
2141
+ !isValue(projectId) ||
2142
+ !isValue(idempotencyKey) ||
2143
+ !isValue(table) ||
2144
+ !isValue(columns) ||
2145
+ (environment !== "dev" && environment !== "prod")
2146
+ )
2147
+ throw new InvalidCommandError();
2148
+ const request = contractValue(() =>
2149
+ parseDatabaseWatchCreateRequest({
2150
+ schema,
2151
+ table,
2152
+ columns: columns.split(","),
2153
+ ...(operations === undefined ? {} : { operations: operations.split(",") }),
2154
+ }),
2155
+ );
2156
+ return {
2157
+ kind: "database-watch-create",
2158
+ projectId,
2159
+ environment,
2160
+ request,
2161
+ idempotencyKey,
2162
+ yes: true,
2163
+ credentialStore,
2164
+ };
2165
+ }
2166
+ if (subcommand === "list") {
2167
+ if (yes || wait || follow || revoke || stdin) throw new InvalidCommandError();
2168
+ const options = parseOptionalOptions(values, ["--project", "--environment"]);
2169
+ const projectId = options["--project"],
2170
+ environment = options["--environment"] ?? "dev";
2171
+ if (!isValue(projectId) || (environment !== "dev" && environment !== "prod"))
2172
+ throw new InvalidCommandError();
2173
+ return { kind: "database-watch-list", projectId, environment, credentialStore };
2174
+ }
2175
+ if (subcommand === "delete") {
2176
+ if (!yes || wait || follow || revoke || stdin) throw new InvalidCommandError();
2177
+ const options = parseOptionalOptions(values, [
2178
+ "--project",
2179
+ "--environment",
2180
+ "--watch",
2181
+ "--idempotency-key",
2182
+ ]);
2183
+ const projectId = options["--project"],
2184
+ environment = options["--environment"],
2185
+ watchId = options["--watch"],
2186
+ idempotencyKey = options["--idempotency-key"];
2187
+ if (
2188
+ !isValue(projectId) ||
2189
+ !isValue(idempotencyKey) ||
2190
+ (environment !== "dev" && environment !== "prod")
2191
+ )
2192
+ throw new InvalidCommandError();
2193
+ return {
2194
+ kind: "database-watch-delete",
2195
+ projectId,
2196
+ environment,
2197
+ watchId: contractValue(() => parseDatabaseWatchId(watchId)),
2198
+ idempotencyKey,
2199
+ yes: true,
2200
+ credentialStore,
2201
+ };
2202
+ }
2203
+ throw new InvalidCommandError();
2204
+ }
2205
+ if (first === "database" && values[0] === "table") {
2206
+ values.shift();
2207
+ const subcommand = values.shift();
2208
+ if (subcommand === "create") {
2209
+ if (!yes || wait || follow || revoke || stdin) throw new InvalidCommandError();
2210
+ const options = parseOptionalOptions(values, [
2211
+ "--project",
2212
+ "--environment",
2213
+ "--table",
2214
+ "--columns-file",
2215
+ "--primary-key",
2216
+ "--idempotency-key",
2217
+ ]);
2218
+ const projectId = options["--project"],
2219
+ environment = options["--environment"],
2220
+ table = options["--table"],
2221
+ columnsFile = options["--columns-file"],
2222
+ primaryKey = options["--primary-key"],
2223
+ idempotencyKey = options["--idempotency-key"];
2224
+ const keyNames = primaryKey === undefined ? [] : primaryKey.split(",");
2225
+ if (
2226
+ !isValue(projectId) ||
2227
+ !isValue(idempotencyKey) ||
2228
+ !isValue(table) ||
2229
+ !isValue(columnsFile) ||
2230
+ (primaryKey !== undefined && !keyNames.every(isDatabaseTableIdentifier)) ||
2231
+ keyNames.length > DATABASE_TABLE_MAX_PRIMARY_KEY_COLUMNS ||
2232
+ !isDatabaseTableIdentifier(table) ||
2233
+ (environment !== "dev" && environment !== "prod")
2234
+ )
2235
+ throw new InvalidCommandError();
2236
+ return {
2237
+ kind: "database-table-create",
2238
+ projectId,
2239
+ environment,
2240
+ table,
2241
+ columnsFile,
2242
+ primaryKey: keyNames,
2243
+ idempotencyKey,
2244
+ yes: true,
2245
+ credentialStore,
2246
+ };
2247
+ }
2248
+ throw new InvalidCommandError();
2249
+ }
2066
2250
  if (first === "database" && values[0] === "webhook") {
2067
2251
  values.shift();
2068
2252
  const subcommand = values.shift();
@@ -20,6 +20,13 @@ import {
20
20
  type ProjectDatabaseWriteReceipt,
21
21
  } from "@ohmyhost/contracts/database-access";
22
22
  import { assertDatabaseCompute, type DatabaseCompute } from "@ohmyhost/contracts/database-compute";
23
+ import {
24
+ DATABASE_TABLE_COLUMN_TYPES,
25
+ DATABASE_TABLE_DEFAULTS,
26
+ isDatabaseTableIdentifier,
27
+ type DatabaseTableColumnType,
28
+ type DatabaseTableDefault,
29
+ } from "@ohmyhost/contracts/database-tables";
23
30
  import {
24
31
  assertDatabaseExecutionUsagePage,
25
32
  assertDatabaseSchedule,
@@ -33,6 +40,9 @@ import {
33
40
  assertDatabaseWebhookDeliveryPage,
34
41
  assertDatabaseWebhookPage,
35
42
  assertDatabaseWebhookReplayAccepted,
43
+ assertDatabaseWatch,
44
+ assertDatabaseWatchDeleted,
45
+ assertDatabaseWatchPage,
36
46
  assertDatabaseWebhookTest,
37
47
  type DatabaseExecutionUsagePage,
38
48
  type DatabaseSchedule,
@@ -45,7 +55,11 @@ import {
45
55
  type DatabaseWebhookCreated,
46
56
  type DatabaseWebhookDeleted,
47
57
  type DatabaseWebhookDeliveryPage,
48
- type DatabaseWebhookEventType,
58
+ type DatabaseWatch,
59
+ type DatabaseWatchDeleted,
60
+ type DatabaseWatchPage,
61
+ type DatabaseWatchRequest,
62
+ type DatabaseWebhookEventPattern,
49
63
  type DatabaseWebhookPage,
50
64
  type DatabaseWebhookReplayAccepted,
51
65
  type DatabaseWebhookTestResult,
@@ -152,6 +166,10 @@ import {
152
166
  getProjectDatabaseWebhook,
153
167
  deleteProjectDatabaseWebhook,
154
168
  testProjectDatabaseWebhook,
169
+ createProjectDatabaseWatch,
170
+ listProjectDatabaseWatches,
171
+ deleteProjectDatabaseWatch,
172
+ createProjectDatabaseTable,
155
173
  listProjectDatabaseWebhookDeliveries,
156
174
  replayProjectDatabaseWebhookDelivery,
157
175
  setProjectDatabaseSchedule,
@@ -205,6 +223,39 @@ export interface ProjectCreateInput {
205
223
  readonly idempotencyKey: string;
206
224
  }
207
225
 
226
+ /** One column of a table to create; nullable, identity and default are optional as the REST body allows. */
227
+ export interface DatabaseTableColumnInput {
228
+ readonly name: string;
229
+ readonly type: DatabaseTableColumnType;
230
+ readonly nullable?: boolean;
231
+ readonly identity?: boolean;
232
+ readonly default?: DatabaseTableDefault;
233
+ }
234
+
235
+ export interface DatabaseTableCreateInput {
236
+ readonly table: string;
237
+ readonly columns: readonly DatabaseTableColumnInput[];
238
+ readonly primaryKey: readonly string[];
239
+ }
240
+
241
+ /** The created or identical existing table, every column with its concrete nullability and default. */
242
+ export interface DatabaseTableAnswer {
243
+ readonly table: {
244
+ readonly environment: "dev" | "prod";
245
+ readonly schema: "public";
246
+ readonly table: string;
247
+ readonly columns: readonly {
248
+ readonly name: string;
249
+ readonly type: DatabaseTableColumnType;
250
+ readonly nullable: boolean;
251
+ readonly identity: boolean;
252
+ readonly default: DatabaseTableDefault | null;
253
+ }[];
254
+ readonly primary_key: readonly string[];
255
+ readonly created: boolean;
256
+ };
257
+ }
258
+
208
259
  export interface ProductApi {
209
260
  readonly managedSource?: ManagedSourceApi;
210
261
  planProjectDataChange(input: {
@@ -328,7 +379,7 @@ export interface ProductApi {
328
379
  readonly environment: "dev" | "prod";
329
380
  readonly idempotencyKey: string;
330
381
  readonly url: string;
331
- readonly eventTypes: readonly DatabaseWebhookEventType[];
382
+ readonly eventTypes: readonly DatabaseWebhookEventPattern[];
332
383
  readonly headers: Readonly<Record<string, string>>;
333
384
  }): Promise<DatabaseWebhookCreated>;
334
385
  listProjectDatabaseWebhooks(input: {
@@ -350,6 +401,28 @@ export interface ProductApi {
350
401
  readonly environment: "dev" | "prod";
351
402
  readonly webhookId: string;
352
403
  }): Promise<DatabaseWebhookTestResult>;
404
+ createProjectDatabaseWatch(input: {
405
+ readonly projectId: string;
406
+ readonly environment: "dev" | "prod";
407
+ readonly idempotencyKey: string;
408
+ readonly request: DatabaseWatchRequest;
409
+ }): Promise<DatabaseWatch>;
410
+ listProjectDatabaseWatches(input: {
411
+ readonly projectId: string;
412
+ readonly environment: "dev" | "prod";
413
+ }): Promise<DatabaseWatchPage>;
414
+ deleteProjectDatabaseWatch(input: {
415
+ readonly projectId: string;
416
+ readonly environment: "dev" | "prod";
417
+ readonly idempotencyKey: string;
418
+ readonly watchId: string;
419
+ }): Promise<DatabaseWatchDeleted>;
420
+ createProjectDatabaseTable(input: {
421
+ readonly projectId: string;
422
+ readonly environment: "dev" | "prod";
423
+ readonly idempotencyKey: string;
424
+ readonly request: DatabaseTableCreateInput;
425
+ }): Promise<DatabaseTableAnswer>;
353
426
  listProjectDatabaseWebhookDeliveries(input: {
354
427
  readonly projectId: string;
355
428
  readonly environment: "dev" | "prod";
@@ -566,9 +639,55 @@ export interface ProductApi {
566
639
 
567
640
  /** A webhook answer that names another environment, webhook or delivery than the request did. */
568
641
  const WEBHOOK_SCOPE_MISMATCH = "Webhook response scope mismatch";
642
+ const WATCH_SCOPE_MISMATCH = "Watch response scope mismatch";
643
+ const TABLE_SCOPE_MISMATCH = "Table response scope mismatch";
569
644
  /** A schedule answer that names another environment or schedule than the request did. */
570
645
  const SCHEDULE_SCOPE_MISMATCH = "Schedule response scope mismatch";
571
646
 
647
+ /** The REST answer read exactly: no extra keys, closed column types and defaults, keys among the columns. */
648
+ export function assertDatabaseTableAnswer(value: unknown): asserts value is DatabaseTableAnswer {
649
+ const table = isRecordValue(value) ? value["table"] : undefined;
650
+ const columns = isRecordValue(table) ? table["columns"] : undefined;
651
+ const primaryKey = isRecordValue(table) ? table["primary_key"] : undefined;
652
+ if (
653
+ !isRecordValue(table) ||
654
+ !hasExactKeys(table, ["environment", "schema", "table", "columns", "primary_key", "created"]) ||
655
+ (table["environment"] !== "dev" && table["environment"] !== "prod") ||
656
+ table["schema"] !== "public" ||
657
+ !isDatabaseTableIdentifier(table["table"]) ||
658
+ typeof table["created"] !== "boolean" ||
659
+ !Array.isArray(columns) ||
660
+ columns.length < 1 ||
661
+ !columns.every(isDatabaseTableAnswerColumn) ||
662
+ !Array.isArray(primaryKey) ||
663
+ !primaryKey.every((name: unknown) =>
664
+ columns.some((column: Record<string, unknown>) => column["name"] === name),
665
+ )
666
+ )
667
+ throw new TypeError("Invalid database table answer");
668
+ }
669
+
670
+ function isDatabaseTableAnswerColumn(value: unknown): boolean {
671
+ return (
672
+ isRecordValue(value) &&
673
+ hasExactKeys(value, ["name", "type", "nullable", "identity", "default"]) &&
674
+ isDatabaseTableIdentifier(value["name"]) &&
675
+ (DATABASE_TABLE_COLUMN_TYPES as readonly unknown[]).includes(value["type"]) &&
676
+ typeof value["nullable"] === "boolean" &&
677
+ typeof value["identity"] === "boolean" &&
678
+ (value["default"] === null ||
679
+ (DATABASE_TABLE_DEFAULTS as readonly unknown[]).includes(value["default"]))
680
+ );
681
+ }
682
+
683
+ function isRecordValue(value: unknown): value is Record<string, unknown> {
684
+ return typeof value === "object" && value !== null && !Array.isArray(value);
685
+ }
686
+
687
+ function hasExactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
688
+ return Object.keys(value).sort().join(",") === [...keys].sort().join(",");
689
+ }
690
+
572
691
  export class GeneratedSdkProductApi implements ProductApi {
573
692
  readonly #client: OhMyHostClient;
574
693
  public readonly managedSource: ManagedSourceApi;
@@ -889,6 +1008,87 @@ export class GeneratedSdkProductApi implements ProductApi {
889
1008
  return value;
890
1009
  }
891
1010
 
1011
+ public async createProjectDatabaseWatch(
1012
+ input: Parameters<ProductApi["createProjectDatabaseWatch"]>[0],
1013
+ ): Promise<DatabaseWatch> {
1014
+ const value = await createProjectDatabaseWatch(
1015
+ {
1016
+ project_id: input.projectId,
1017
+ "Idempotency-Key": input.idempotencyKey,
1018
+ databaseWatchCreateRequest: {
1019
+ environment: input.environment,
1020
+ schema: input.request.schema,
1021
+ table: input.request.table,
1022
+ columns: [...input.request.columns],
1023
+ operations: [...input.request.operations],
1024
+ },
1025
+ },
1026
+ { client: this.#client },
1027
+ );
1028
+ assertDatabaseWatch(value);
1029
+ if (
1030
+ value.environment !== input.environment ||
1031
+ value.schema !== input.request.schema ||
1032
+ value.table !== input.request.table
1033
+ )
1034
+ throw new TypeError(WATCH_SCOPE_MISMATCH);
1035
+ return value;
1036
+ }
1037
+
1038
+ public async listProjectDatabaseWatches(
1039
+ input: Parameters<ProductApi["listProjectDatabaseWatches"]>[0],
1040
+ ): Promise<DatabaseWatchPage> {
1041
+ const value = await listProjectDatabaseWatches(
1042
+ { project_id: input.projectId, environment: input.environment },
1043
+ { client: this.#client },
1044
+ );
1045
+ assertDatabaseWatchPage(value);
1046
+ if (value.items.some((item) => item.environment !== input.environment))
1047
+ throw new TypeError(WATCH_SCOPE_MISMATCH);
1048
+ return value;
1049
+ }
1050
+
1051
+ public async deleteProjectDatabaseWatch(
1052
+ input: Parameters<ProductApi["deleteProjectDatabaseWatch"]>[0],
1053
+ ): Promise<DatabaseWatchDeleted> {
1054
+ const value = await deleteProjectDatabaseWatch(
1055
+ {
1056
+ project_id: input.projectId,
1057
+ environment: input.environment,
1058
+ "Idempotency-Key": input.idempotencyKey,
1059
+ watch_id: input.watchId,
1060
+ },
1061
+ { client: this.#client },
1062
+ );
1063
+ assertDatabaseWatchDeleted(value);
1064
+ if (value.id !== input.watchId) throw new TypeError(WATCH_SCOPE_MISMATCH);
1065
+ return value;
1066
+ }
1067
+
1068
+ public async createProjectDatabaseTable(
1069
+ input: Parameters<ProductApi["createProjectDatabaseTable"]>[0],
1070
+ ): Promise<DatabaseTableAnswer> {
1071
+ const value = await createProjectDatabaseTable(
1072
+ {
1073
+ project_id: input.projectId,
1074
+ "Idempotency-Key": input.idempotencyKey,
1075
+ databaseTableCreateRequest: {
1076
+ environment: input.environment,
1077
+ table: input.request.table,
1078
+ columns: [...input.request.columns],
1079
+ ...(input.request.primaryKey.length === 0
1080
+ ? {}
1081
+ : { primary_key: [...input.request.primaryKey] }),
1082
+ },
1083
+ },
1084
+ { client: this.#client },
1085
+ );
1086
+ assertDatabaseTableAnswer(value);
1087
+ if (value.table.environment !== input.environment || value.table.table !== input.request.table)
1088
+ throw new TypeError(TABLE_SCOPE_MISMATCH);
1089
+ return value;
1090
+ }
1091
+
892
1092
  /** The signing secret exists only in this answer; the CLI prints it once and stores nothing. */
893
1093
  public async createProjectDatabaseWebhook(
894
1094
  input: Parameters<ProductApi["createProjectDatabaseWebhook"]>[0],