@amerged/ohmyhost-cli 0.1.44 → 0.1.45

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/dist/index.js CHANGED
@@ -100,7 +100,7 @@ import {
100
100
  selectWorkspace,
101
101
  verifiedUserId,
102
102
  workspaceOf
103
- } from "./chunk-VJWPDFZ5.js";
103
+ } from "./chunk-C6KEAOJP.js";
104
104
  import {
105
105
  CredentialStoreIntegrityError,
106
106
  CredentialStoreUnavailableError,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amerged/ohmyhost-cli",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
4
4
  "type": "module",
5
5
  "engines": {
6
6
  "node": ">=22"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohmyhost/product-cli",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
4
4
  "private": true,
5
5
  "ohmyhost": {
6
6
  "deployment": "production",
@@ -419,8 +419,14 @@ const HELP_USAGE = Object.freeze({
419
419
  "ohmyhost database access list --project ULID [--environment dev|prod] --json",
420
420
  "database access revoke":
421
421
  "ohmyhost database access revoke --project ULID --access ULID --yes --json",
422
+ "database watch create":
423
+ "ohmyhost database watch create --project ULID --environment dev|prod --table NAME [--schema public] --columns COL[,COL] [--operations insert,update,delete] --idempotency-key KEY --yes --json (publishes only the listed columns, at most 32, of each row change as the event type data.<topic> printed in this answer; the table must exist as an ordinary table of the application; a sleeping database wakes; next, subscribe with 'ohmyhost database webhook create ... --event-types data.<topic>')",
424
+ "database watch list":
425
+ "ohmyhost database watch list --project ULID [--environment dev|prod] --json (dev when omitted; a sleeping database wakes)",
426
+ "database watch delete":
427
+ "ohmyhost database watch delete --project ULID --environment dev|prod --watch ID --idempotency-key KEY --yes --json",
422
428
  "database webhook create":
423
- "ohmyhost database webhook create --project ULID --environment dev|prod --url HTTPS_URL --event-types TYPE[,TYPE] [--headers-file PATH] --idempotency-key KEY --yes --json (the signing secret is shown once in this answer; types: database.created, database.ready, database.woke, database.slept, database.suspended, database.resized, database.deleted; the headers file is a UTF-8 JSON object of at most 8 KiB)",
429
+ "ohmyhost database webhook create --project ULID --environment dev|prod --url HTTPS_URL --event-types TYPE[,TYPE] [--headers-file PATH] --idempotency-key KEY --yes --json (the signing secret is shown once in this answer; types: database.created, database.ready, database.woke, database.slept, database.suspended, database.resized, database.deleted, data.<topic> for the rows of one watched table printed by database watch create, or data.* for every watched table; a bare * is not accepted and does not include data events; the headers file is a UTF-8 JSON object of at most 8 KiB)",
424
430
  "database webhook list":
425
431
  "ohmyhost database webhook list --project ULID [--environment dev|prod] --json (dev when omitted; the secret is never listed)",
426
432
  "database webhook get":
@@ -1191,6 +1197,12 @@ export class ProductCli {
1191
1197
  return await this.listDatabaseAccess(command, signal);
1192
1198
  if (command.kind === "database-access-revoke")
1193
1199
  return await this.revokeDatabaseAccess(command, signal);
1200
+ if (command.kind === "database-watch-create")
1201
+ return await this.createDatabaseWatch(command, signal);
1202
+ if (command.kind === "database-watch-list")
1203
+ return await this.listDatabaseWatches(command, signal);
1204
+ if (command.kind === "database-watch-delete")
1205
+ return await this.deleteDatabaseWatch(command, signal);
1194
1206
  if (command.kind === "database-webhook-create")
1195
1207
  return await this.createDatabaseWebhook(command, signal);
1196
1208
  if (command.kind === "database-webhook-list")
@@ -1952,6 +1964,51 @@ export class ProductCli {
1952
1964
  return success("database access revoke", { status: "succeeded", access });
1953
1965
  }
1954
1966
 
1967
+ /** The answer names the event type to subscribe to; the watch already carries the columns and the topic. */
1968
+ private async createDatabaseWatch(
1969
+ command: Extract<ProductCliCommand, { kind: "database-watch-create" }>,
1970
+ signal: AbortSignal,
1971
+ ): Promise<CliResult> {
1972
+ const api = await this.authenticatedApi(command.credentialStore, signal);
1973
+ const watch = await api.createProjectDatabaseWatch({
1974
+ projectId: command.projectId,
1975
+ environment: command.environment,
1976
+ idempotencyKey: command.idempotencyKey,
1977
+ request: command.request,
1978
+ });
1979
+ return success("database watch create", {
1980
+ status: "succeeded",
1981
+ watch,
1982
+ next_action: `Run '${this.dependencies.commandPrefix} database webhook create --project ${command.projectId} --environment ${command.environment} --url HTTPS_URL --event-types ${watch.event_type} --idempotency-key KEY --yes --json' to receive ${watch.event_type} events at an HTTPS receiver.`,
1983
+ });
1984
+ }
1985
+
1986
+ private async listDatabaseWatches(
1987
+ command: Extract<ProductCliCommand, { kind: "database-watch-list" }>,
1988
+ signal: AbortSignal,
1989
+ ): Promise<CliResult> {
1990
+ const api = await this.authenticatedApi(command.credentialStore, signal);
1991
+ const page = await api.listProjectDatabaseWatches({
1992
+ projectId: command.projectId,
1993
+ environment: command.environment,
1994
+ });
1995
+ return success("database watch list", { status: "succeeded", watch: page.items });
1996
+ }
1997
+
1998
+ private async deleteDatabaseWatch(
1999
+ command: Extract<ProductCliCommand, { kind: "database-watch-delete" }>,
2000
+ signal: AbortSignal,
2001
+ ): Promise<CliResult> {
2002
+ const api = await this.authenticatedApi(command.credentialStore, signal);
2003
+ const deleted = await api.deleteProjectDatabaseWatch({
2004
+ projectId: command.projectId,
2005
+ environment: command.environment,
2006
+ idempotencyKey: command.idempotencyKey,
2007
+ watchId: command.watchId,
2008
+ });
2009
+ return success("database watch delete", { status: "succeeded", deleted });
2010
+ }
2011
+
1955
2012
  /** The signing secret is returned by create only; every other answer carries no secret. */
1956
2013
  private async createDatabaseWebhook(
1957
2014
  command: Extract<ProductCliCommand, { kind: "database-webhook-create" }>,
@@ -3526,6 +3583,9 @@ const parsedCommandName = (command: ProductCliCommand): CommandLabel => {
3526
3583
  if (command.kind === "database-access-list") return "database access list";
3527
3584
  if (command.kind === "database-access-revoke") return "database access revoke";
3528
3585
  if (command.kind === "database-psql") return "database psql";
3586
+ if (command.kind === "database-watch-create") return "database watch create";
3587
+ if (command.kind === "database-watch-list") return "database watch list";
3588
+ if (command.kind === "database-watch-delete") return "database watch delete";
3529
3589
  if (command.kind === "database-webhook-create") return "database webhook create";
3530
3590
  if (command.kind === "database-webhook-list") return "database webhook list";
3531
3591
  if (command.kind === "database-webhook-get") return "database webhook get";
@@ -6,9 +6,12 @@ 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";
13
16
  import {
14
17
  databaseSizeForMemory,
@@ -404,12 +407,36 @@ export type ProductCliCommand =
404
407
  readonly yes: true;
405
408
  readonly credentialStore: "native";
406
409
  }
410
+ | {
411
+ readonly kind: "database-watch-create";
412
+ readonly projectId: string;
413
+ readonly environment: "dev" | "prod";
414
+ readonly request: DatabaseWatchRequest;
415
+ readonly idempotencyKey: string;
416
+ readonly yes: true;
417
+ readonly credentialStore: "native";
418
+ }
419
+ | {
420
+ readonly kind: "database-watch-list";
421
+ readonly projectId: string;
422
+ readonly environment: "dev" | "prod";
423
+ readonly credentialStore: "native";
424
+ }
425
+ | {
426
+ readonly kind: "database-watch-delete";
427
+ readonly projectId: string;
428
+ readonly environment: "dev" | "prod";
429
+ readonly watchId: string;
430
+ readonly idempotencyKey: string;
431
+ readonly yes: true;
432
+ readonly credentialStore: "native";
433
+ }
407
434
  | {
408
435
  readonly kind: "database-webhook-create";
409
436
  readonly projectId: string;
410
437
  readonly environment: "dev" | "prod";
411
438
  readonly url: string;
412
- readonly eventTypes: readonly DatabaseWebhookEventType[];
439
+ readonly eventTypes: readonly DatabaseWebhookEventPattern[];
413
440
  readonly headersFile: string | null;
414
441
  readonly idempotencyKey: string;
415
442
  readonly yes: true;
@@ -1003,6 +1030,9 @@ export type RecognizedCommandLabel =
1003
1030
  | "database access create"
1004
1031
  | "database access list"
1005
1032
  | "database access revoke"
1033
+ | "database watch create"
1034
+ | "database watch list"
1035
+ | "database watch delete"
1006
1036
  | "database webhook create"
1007
1037
  | "database webhook list"
1008
1038
  | "database webhook get"
@@ -1174,6 +1204,11 @@ export const recognizedCommandLabel = (argv: readonly string[]): RecognizedComma
1174
1204
  if (values[1] === "list") return "database access list";
1175
1205
  if (values[1] === "revoke") return "database access revoke";
1176
1206
  }
1207
+ if (value === "database" && values[0] === "watch") {
1208
+ if (values[1] === "create") return "database watch create";
1209
+ if (values[1] === "list") return "database watch list";
1210
+ if (values[1] === "delete") return "database watch delete";
1211
+ }
1177
1212
  if (value === "database" && values[0] === "webhook") {
1178
1213
  if (values[1] === "create") return "database webhook create";
1179
1214
  if (values[1] === "list") return "database webhook list";
@@ -2063,6 +2098,92 @@ export const parseProductCliCommand = (argv: readonly string[]): ProductCliComma
2063
2098
  }
2064
2099
  throw new InvalidCommandError();
2065
2100
  }
2101
+ if (first === "database" && values[0] === "watch") {
2102
+ values.shift();
2103
+ const subcommand = values.shift();
2104
+ if (subcommand === "create") {
2105
+ if (!yes || wait || follow || revoke || stdin) throw new InvalidCommandError();
2106
+ const options = parseOptionalOptions(values, [
2107
+ "--project",
2108
+ "--environment",
2109
+ "--table",
2110
+ "--schema",
2111
+ "--columns",
2112
+ "--operations",
2113
+ "--idempotency-key",
2114
+ ]);
2115
+ const projectId = options["--project"],
2116
+ environment = options["--environment"],
2117
+ table = options["--table"],
2118
+ schema = options["--schema"],
2119
+ columns = options["--columns"],
2120
+ operations = options["--operations"],
2121
+ idempotencyKey = options["--idempotency-key"];
2122
+ if (
2123
+ !isValue(projectId) ||
2124
+ !isValue(idempotencyKey) ||
2125
+ !isValue(table) ||
2126
+ !isValue(columns) ||
2127
+ (environment !== "dev" && environment !== "prod")
2128
+ )
2129
+ throw new InvalidCommandError();
2130
+ const request = contractValue(() =>
2131
+ parseDatabaseWatchCreateRequest({
2132
+ schema,
2133
+ table,
2134
+ columns: columns.split(","),
2135
+ ...(operations === undefined ? {} : { operations: operations.split(",") }),
2136
+ }),
2137
+ );
2138
+ return {
2139
+ kind: "database-watch-create",
2140
+ projectId,
2141
+ environment,
2142
+ request,
2143
+ idempotencyKey,
2144
+ yes: true,
2145
+ credentialStore,
2146
+ };
2147
+ }
2148
+ if (subcommand === "list") {
2149
+ if (yes || wait || follow || revoke || stdin) throw new InvalidCommandError();
2150
+ const options = parseOptionalOptions(values, ["--project", "--environment"]);
2151
+ const projectId = options["--project"],
2152
+ environment = options["--environment"] ?? "dev";
2153
+ if (!isValue(projectId) || (environment !== "dev" && environment !== "prod"))
2154
+ throw new InvalidCommandError();
2155
+ return { kind: "database-watch-list", projectId, environment, credentialStore };
2156
+ }
2157
+ if (subcommand === "delete") {
2158
+ if (!yes || wait || follow || revoke || stdin) throw new InvalidCommandError();
2159
+ const options = parseOptionalOptions(values, [
2160
+ "--project",
2161
+ "--environment",
2162
+ "--watch",
2163
+ "--idempotency-key",
2164
+ ]);
2165
+ const projectId = options["--project"],
2166
+ environment = options["--environment"],
2167
+ watchId = options["--watch"],
2168
+ idempotencyKey = options["--idempotency-key"];
2169
+ if (
2170
+ !isValue(projectId) ||
2171
+ !isValue(idempotencyKey) ||
2172
+ (environment !== "dev" && environment !== "prod")
2173
+ )
2174
+ throw new InvalidCommandError();
2175
+ return {
2176
+ kind: "database-watch-delete",
2177
+ projectId,
2178
+ environment,
2179
+ watchId: contractValue(() => parseDatabaseWatchId(watchId)),
2180
+ idempotencyKey,
2181
+ yes: true,
2182
+ credentialStore,
2183
+ };
2184
+ }
2185
+ throw new InvalidCommandError();
2186
+ }
2066
2187
  if (first === "database" && values[0] === "webhook") {
2067
2188
  values.shift();
2068
2189
  const subcommand = values.shift();
@@ -33,6 +33,9 @@ import {
33
33
  assertDatabaseWebhookDeliveryPage,
34
34
  assertDatabaseWebhookPage,
35
35
  assertDatabaseWebhookReplayAccepted,
36
+ assertDatabaseWatch,
37
+ assertDatabaseWatchDeleted,
38
+ assertDatabaseWatchPage,
36
39
  assertDatabaseWebhookTest,
37
40
  type DatabaseExecutionUsagePage,
38
41
  type DatabaseSchedule,
@@ -45,7 +48,11 @@ import {
45
48
  type DatabaseWebhookCreated,
46
49
  type DatabaseWebhookDeleted,
47
50
  type DatabaseWebhookDeliveryPage,
48
- type DatabaseWebhookEventType,
51
+ type DatabaseWatch,
52
+ type DatabaseWatchDeleted,
53
+ type DatabaseWatchPage,
54
+ type DatabaseWatchRequest,
55
+ type DatabaseWebhookEventPattern,
49
56
  type DatabaseWebhookPage,
50
57
  type DatabaseWebhookReplayAccepted,
51
58
  type DatabaseWebhookTestResult,
@@ -152,6 +159,9 @@ import {
152
159
  getProjectDatabaseWebhook,
153
160
  deleteProjectDatabaseWebhook,
154
161
  testProjectDatabaseWebhook,
162
+ createProjectDatabaseWatch,
163
+ listProjectDatabaseWatches,
164
+ deleteProjectDatabaseWatch,
155
165
  listProjectDatabaseWebhookDeliveries,
156
166
  replayProjectDatabaseWebhookDelivery,
157
167
  setProjectDatabaseSchedule,
@@ -328,7 +338,7 @@ export interface ProductApi {
328
338
  readonly environment: "dev" | "prod";
329
339
  readonly idempotencyKey: string;
330
340
  readonly url: string;
331
- readonly eventTypes: readonly DatabaseWebhookEventType[];
341
+ readonly eventTypes: readonly DatabaseWebhookEventPattern[];
332
342
  readonly headers: Readonly<Record<string, string>>;
333
343
  }): Promise<DatabaseWebhookCreated>;
334
344
  listProjectDatabaseWebhooks(input: {
@@ -350,6 +360,22 @@ export interface ProductApi {
350
360
  readonly environment: "dev" | "prod";
351
361
  readonly webhookId: string;
352
362
  }): Promise<DatabaseWebhookTestResult>;
363
+ createProjectDatabaseWatch(input: {
364
+ readonly projectId: string;
365
+ readonly environment: "dev" | "prod";
366
+ readonly idempotencyKey: string;
367
+ readonly request: DatabaseWatchRequest;
368
+ }): Promise<DatabaseWatch>;
369
+ listProjectDatabaseWatches(input: {
370
+ readonly projectId: string;
371
+ readonly environment: "dev" | "prod";
372
+ }): Promise<DatabaseWatchPage>;
373
+ deleteProjectDatabaseWatch(input: {
374
+ readonly projectId: string;
375
+ readonly environment: "dev" | "prod";
376
+ readonly idempotencyKey: string;
377
+ readonly watchId: string;
378
+ }): Promise<DatabaseWatchDeleted>;
353
379
  listProjectDatabaseWebhookDeliveries(input: {
354
380
  readonly projectId: string;
355
381
  readonly environment: "dev" | "prod";
@@ -566,6 +592,7 @@ export interface ProductApi {
566
592
 
567
593
  /** A webhook answer that names another environment, webhook or delivery than the request did. */
568
594
  const WEBHOOK_SCOPE_MISMATCH = "Webhook response scope mismatch";
595
+ const WATCH_SCOPE_MISMATCH = "Watch response scope mismatch";
569
596
  /** A schedule answer that names another environment or schedule than the request did. */
570
597
  const SCHEDULE_SCOPE_MISMATCH = "Schedule response scope mismatch";
571
598
 
@@ -889,6 +916,63 @@ export class GeneratedSdkProductApi implements ProductApi {
889
916
  return value;
890
917
  }
891
918
 
919
+ public async createProjectDatabaseWatch(
920
+ input: Parameters<ProductApi["createProjectDatabaseWatch"]>[0],
921
+ ): Promise<DatabaseWatch> {
922
+ const value = await createProjectDatabaseWatch(
923
+ {
924
+ project_id: input.projectId,
925
+ "Idempotency-Key": input.idempotencyKey,
926
+ databaseWatchCreateRequest: {
927
+ environment: input.environment,
928
+ schema: input.request.schema,
929
+ table: input.request.table,
930
+ columns: [...input.request.columns],
931
+ operations: [...input.request.operations],
932
+ },
933
+ },
934
+ { client: this.#client },
935
+ );
936
+ assertDatabaseWatch(value);
937
+ if (
938
+ value.environment !== input.environment ||
939
+ value.schema !== input.request.schema ||
940
+ value.table !== input.request.table
941
+ )
942
+ throw new TypeError(WATCH_SCOPE_MISMATCH);
943
+ return value;
944
+ }
945
+
946
+ public async listProjectDatabaseWatches(
947
+ input: Parameters<ProductApi["listProjectDatabaseWatches"]>[0],
948
+ ): Promise<DatabaseWatchPage> {
949
+ const value = await listProjectDatabaseWatches(
950
+ { project_id: input.projectId, environment: input.environment },
951
+ { client: this.#client },
952
+ );
953
+ assertDatabaseWatchPage(value);
954
+ if (value.items.some((item) => item.environment !== input.environment))
955
+ throw new TypeError(WATCH_SCOPE_MISMATCH);
956
+ return value;
957
+ }
958
+
959
+ public async deleteProjectDatabaseWatch(
960
+ input: Parameters<ProductApi["deleteProjectDatabaseWatch"]>[0],
961
+ ): Promise<DatabaseWatchDeleted> {
962
+ const value = await deleteProjectDatabaseWatch(
963
+ {
964
+ project_id: input.projectId,
965
+ environment: input.environment,
966
+ "Idempotency-Key": input.idempotencyKey,
967
+ watch_id: input.watchId,
968
+ },
969
+ { client: this.#client },
970
+ );
971
+ assertDatabaseWatchDeleted(value);
972
+ if (value.id !== input.watchId) throw new TypeError(WATCH_SCOPE_MISMATCH);
973
+ return value;
974
+ }
975
+
892
976
  /** The signing secret exists only in this answer; the CLI prints it once and stores nothing. */
893
977
  public async createProjectDatabaseWebhook(
894
978
  input: Parameters<ProductApi["createProjectDatabaseWebhook"]>[0],