@amerged/ohmyhost-mcp 0.1.45 → 0.1.47

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.
@@ -132,6 +132,11 @@
132
132
  "types": "./dist/database-events.d.ts",
133
133
  "import": "./dist/database-events.js"
134
134
  },
135
+ "./database-tables": {
136
+ "source": "./src/database-tables.ts",
137
+ "types": "./dist/database-tables.d.ts",
138
+ "import": "./dist/database-tables.js"
139
+ },
135
140
  "./functions": {
136
141
  "source": "./src/functions.ts",
137
142
  "types": "./dist/functions.d.ts",
@@ -0,0 +1,317 @@
1
+ /** Column types a created table may declare; the closed list keeps type text out of the DDL. */
2
+ export const DATABASE_TABLE_COLUMN_TYPES = [
3
+ "text",
4
+ "integer",
5
+ "bigint",
6
+ "boolean",
7
+ "numeric",
8
+ "double precision",
9
+ "timestamptz",
10
+ "date",
11
+ "uuid",
12
+ "jsonb",
13
+ ] as const;
14
+ export type DatabaseTableColumnType = (typeof DATABASE_TABLE_COLUMN_TYPES)[number];
15
+ /** `now` is DEFAULT now() on timestamptz; `uuid` is DEFAULT gen_random_uuid() on uuid. */
16
+ export const DATABASE_TABLE_DEFAULTS = ["now", "uuid"] as const;
17
+ export type DatabaseTableDefault = (typeof DATABASE_TABLE_DEFAULTS)[number];
18
+ export const DATABASE_TABLE_MAX_COLUMNS = 64;
19
+ export const DATABASE_TABLE_MAX_PRIMARY_KEY_COLUMNS = 8;
20
+ /** Ordinary and partitioned tables that a database may hold in its public schema. */
21
+ export const DATABASE_TABLE_LIMIT = 100;
22
+ export const DATABASE_TABLE_SCHEMA = "public";
23
+
24
+ const DATABASE_TABLE_IDENTIFIER_PATTERN = /^[a-z_][a-z0-9_]{0,62}$/u;
25
+ /** PostgreSQL reserved words: a table or column of such a name would need quoting everywhere it is used. */
26
+ const RESERVED_WORDS: ReadonlySet<string> = new Set([
27
+ "all",
28
+ "analyse",
29
+ "analyze",
30
+ "and",
31
+ "any",
32
+ "array",
33
+ "as",
34
+ "asc",
35
+ "asymmetric",
36
+ "both",
37
+ "case",
38
+ "cast",
39
+ "check",
40
+ "collate",
41
+ "column",
42
+ "constraint",
43
+ "create",
44
+ "current_catalog",
45
+ "current_date",
46
+ "current_role",
47
+ "current_time",
48
+ "current_timestamp",
49
+ "current_user",
50
+ "default",
51
+ "deferrable",
52
+ "desc",
53
+ "distinct",
54
+ "do",
55
+ "else",
56
+ "end",
57
+ "except",
58
+ "false",
59
+ "fetch",
60
+ "for",
61
+ "foreign",
62
+ "from",
63
+ "grant",
64
+ "group",
65
+ "having",
66
+ "in",
67
+ "initially",
68
+ "intersect",
69
+ "into",
70
+ "lateral",
71
+ "leading",
72
+ "limit",
73
+ "localtime",
74
+ "localtimestamp",
75
+ "not",
76
+ "null",
77
+ "offset",
78
+ "on",
79
+ "only",
80
+ "or",
81
+ "order",
82
+ "placing",
83
+ "primary",
84
+ "references",
85
+ "returning",
86
+ "select",
87
+ "session_user",
88
+ "some",
89
+ "symmetric",
90
+ "table",
91
+ "then",
92
+ "to",
93
+ "trailing",
94
+ "true",
95
+ "union",
96
+ "unique",
97
+ "user",
98
+ "using",
99
+ "variadic",
100
+ "when",
101
+ "where",
102
+ "window",
103
+ "with",
104
+ ]);
105
+
106
+ export interface DatabaseTableColumn {
107
+ readonly name: string;
108
+ readonly type: DatabaseTableColumnType;
109
+ readonly nullable: boolean;
110
+ readonly identity: boolean;
111
+ readonly default: DatabaseTableDefault | null;
112
+ }
113
+
114
+ /** A create request after parsing: nullable is concrete and primaryKey is empty when the request names none. */
115
+ export interface DatabaseTableRequest {
116
+ readonly table: string;
117
+ readonly columns: readonly DatabaseTableColumn[];
118
+ readonly primaryKey: readonly string[];
119
+ }
120
+
121
+ export interface DatabaseTable {
122
+ readonly schema: typeof DATABASE_TABLE_SCHEMA;
123
+ readonly table: string;
124
+ readonly columns: readonly DatabaseTableColumn[];
125
+ readonly primaryKey: readonly string[];
126
+ }
127
+
128
+ export interface DatabaseTableCreated {
129
+ readonly table: DatabaseTable;
130
+ readonly created: boolean;
131
+ }
132
+
133
+ interface ColumnInput {
134
+ readonly name: string;
135
+ readonly type: DatabaseTableColumnType;
136
+ readonly nullable: boolean | undefined;
137
+ readonly identity: boolean;
138
+ readonly default: DatabaseTableDefault | null;
139
+ }
140
+
141
+ /** Identifiers are plain lower-case SQL names; the renderer quotes them and no other caller text reaches the DDL. */
142
+ export function isDatabaseTableIdentifier(value: unknown): value is string {
143
+ return (
144
+ typeof value === "string" &&
145
+ DATABASE_TABLE_IDENTIFIER_PATTERN.test(value) &&
146
+ !value.startsWith("pg_") &&
147
+ !RESERVED_WORDS.has(value)
148
+ );
149
+ }
150
+
151
+ export function parseDatabaseTableCreateRequest(value: unknown): DatabaseTableRequest {
152
+ if (
153
+ !record(value) ||
154
+ !onlyKeys(value, ["table", "columns", "primaryKey"]) ||
155
+ !isDatabaseTableIdentifier(value["table"]) ||
156
+ !Array.isArray(value["columns"]) ||
157
+ value["columns"].length < 1 ||
158
+ value["columns"].length > DATABASE_TABLE_MAX_COLUMNS
159
+ )
160
+ throw new TypeError("Invalid database table request");
161
+ const inputs = value["columns"].map(parseColumn);
162
+ const names = inputs.map((column) => column.name);
163
+ if (new Set(names).size !== names.length || inputs.filter((column) => column.identity).length > 1)
164
+ throw new TypeError("Invalid database table request");
165
+ const primaryKey = parsePrimaryKey(value["primaryKey"], names);
166
+ return {
167
+ table: value["table"],
168
+ columns: inputs.map((column) => completeColumn(column, primaryKey.includes(column.name))),
169
+ primaryKey,
170
+ };
171
+ }
172
+
173
+ /**
174
+ * The one CREATE TABLE statement of a request, in the public schema. Identifiers are validated again here, so the
175
+ * renderer is safe for any caller; the statement has no trailing semicolon.
176
+ */
177
+ export function renderCreateTableStatement(request: DatabaseTableRequest): string {
178
+ const definitions = request.columns.map(renderColumn);
179
+ if (request.primaryKey.length > 0)
180
+ definitions.push(`PRIMARY KEY (${request.primaryKey.map(quoted).join(",")})`);
181
+ return `CREATE TABLE ${databaseTableReference(request.table)} (${definitions.join(", ")})`;
182
+ }
183
+
184
+ /** The quoted, schema-qualified name of a table of the public schema, as SQL statements use it. */
185
+ export function databaseTableReference(table: string): string {
186
+ return `${quoted(DATABASE_TABLE_SCHEMA)}.${quoted(table)}`;
187
+ }
188
+
189
+ export function assertDatabaseTableCreated(value: unknown): asserts value is DatabaseTableCreated {
190
+ if (
191
+ !record(value) ||
192
+ !exact(value, ["table", "created"]) ||
193
+ typeof value["created"] !== "boolean" ||
194
+ !isDatabaseTable(value["table"])
195
+ )
196
+ throw new TypeError("Invalid database table");
197
+ }
198
+
199
+ function parseColumn(value: unknown): ColumnInput {
200
+ if (
201
+ !record(value) ||
202
+ !onlyKeys(value, ["name", "type", "nullable", "identity", "default"]) ||
203
+ !isDatabaseTableIdentifier(value["name"]) ||
204
+ !oneOf(DATABASE_TABLE_COLUMN_TYPES, value["type"]) ||
205
+ (value["nullable"] !== undefined && typeof value["nullable"] !== "boolean") ||
206
+ (value["identity"] !== undefined && typeof value["identity"] !== "boolean") ||
207
+ (value["default"] !== undefined && !oneOf(DATABASE_TABLE_DEFAULTS, value["default"]))
208
+ )
209
+ throw new TypeError("Invalid database table request");
210
+ const type = value["type"];
211
+ const identity = value["identity"] === true;
212
+ const defaultValue = value["default"] ?? null;
213
+ if (identity && type !== "integer" && type !== "bigint")
214
+ throw new TypeError("Invalid database table request");
215
+ if (identity && defaultValue !== null) throw new TypeError("Invalid database table request");
216
+ if (defaultValue === "now" && type !== "timestamptz")
217
+ throw new TypeError("Invalid database table request");
218
+ if (defaultValue === "uuid" && type !== "uuid")
219
+ throw new TypeError("Invalid database table request");
220
+ return {
221
+ name: value["name"],
222
+ type,
223
+ nullable: value["nullable"],
224
+ identity,
225
+ default: defaultValue,
226
+ };
227
+ }
228
+
229
+ function parsePrimaryKey(value: unknown, names: readonly string[]): readonly string[] {
230
+ if (value === undefined) return [];
231
+ if (
232
+ !Array.isArray(value) ||
233
+ value.length < 1 ||
234
+ value.length > DATABASE_TABLE_MAX_PRIMARY_KEY_COLUMNS ||
235
+ new Set(value).size !== value.length ||
236
+ !value.every((name) => typeof name === "string" && names.includes(name))
237
+ )
238
+ throw new TypeError("Invalid database table request");
239
+ return value as string[];
240
+ }
241
+
242
+ /** Identity and primary key columns are NOT NULL; asking for a nullable one of them is a contradiction. */
243
+ function completeColumn(column: ColumnInput, keyed: boolean): DatabaseTableColumn {
244
+ const required = column.identity || keyed;
245
+ if (required && column.nullable === true) throw new TypeError("Invalid database table request");
246
+ return {
247
+ name: column.name,
248
+ type: column.type,
249
+ nullable: required ? false : (column.nullable ?? true),
250
+ identity: column.identity,
251
+ default: column.default,
252
+ };
253
+ }
254
+
255
+ function renderColumn(column: DatabaseTableColumn): string {
256
+ if (!oneOf(DATABASE_TABLE_COLUMN_TYPES, column.type))
257
+ throw new TypeError("Invalid database table column type");
258
+ const parts = [quoted(column.name), column.type];
259
+ if (!column.nullable) parts.push("NOT NULL");
260
+ if (column.identity) parts.push("GENERATED ALWAYS AS IDENTITY");
261
+ if (column.default === "now") parts.push("DEFAULT now()");
262
+ if (column.default === "uuid") parts.push("DEFAULT gen_random_uuid()");
263
+ return parts.join(" ");
264
+ }
265
+
266
+ function quoted(identifier: string): string {
267
+ if (!isDatabaseTableIdentifier(identifier))
268
+ throw new TypeError("Invalid database table identifier");
269
+ return `"${identifier}"`;
270
+ }
271
+
272
+ function isDatabaseTable(value: unknown): boolean {
273
+ if (!record(value) || !exact(value, ["schema", "table", "columns", "primaryKey"])) return false;
274
+ const columns = value["columns"];
275
+ const primaryKey = value["primaryKey"];
276
+ if (
277
+ value["schema"] !== DATABASE_TABLE_SCHEMA ||
278
+ !isDatabaseTableIdentifier(value["table"]) ||
279
+ !Array.isArray(columns) ||
280
+ columns.length < 1 ||
281
+ columns.length > DATABASE_TABLE_MAX_COLUMNS ||
282
+ !columns.every(isDatabaseTableColumn) ||
283
+ !Array.isArray(primaryKey) ||
284
+ primaryKey.length > DATABASE_TABLE_MAX_PRIMARY_KEY_COLUMNS
285
+ )
286
+ return false;
287
+ const names = columns.map((column) => column.name);
288
+ return primaryKey.every((name) => typeof name === "string" && names.includes(name));
289
+ }
290
+
291
+ function isDatabaseTableColumn(value: unknown): value is DatabaseTableColumn {
292
+ return (
293
+ record(value) &&
294
+ exact(value, ["name", "type", "nullable", "identity", "default"]) &&
295
+ isDatabaseTableIdentifier(value["name"]) &&
296
+ oneOf(DATABASE_TABLE_COLUMN_TYPES, value["type"]) &&
297
+ typeof value["nullable"] === "boolean" &&
298
+ typeof value["identity"] === "boolean" &&
299
+ (value["default"] === null || oneOf(DATABASE_TABLE_DEFAULTS, value["default"]))
300
+ );
301
+ }
302
+
303
+ function oneOf<T extends string>(allowed: readonly T[], value: unknown): value is T {
304
+ return typeof value === "string" && (allowed as readonly string[]).includes(value);
305
+ }
306
+
307
+ function record(value: unknown): value is Record<string, unknown> {
308
+ return typeof value === "object" && value !== null && !Array.isArray(value);
309
+ }
310
+
311
+ function onlyKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
312
+ return Object.keys(value).every((key) => allowed.includes(key));
313
+ }
314
+
315
+ function exact(value: Record<string, unknown>, keys: readonly string[]): boolean {
316
+ return Object.keys(value).sort().join(",") === [...keys].sort().join(",");
317
+ }
@@ -509,8 +509,8 @@ const FRAMEWORK_CONVERSION_DETAILS = Object.freeze({
509
509
  text: {
510
510
  nextjs: "runtime.mode: edge",
511
511
  "vite-static":
512
- "runtime.mode: edge with src/ohmyhost/companion.ts when database, auth, mail, storage or functions.crons is set, and static otherwise",
513
- "tanstack-start": "runtime.mode: edge, or static without functions.crons and storage",
512
+ "runtime.mode: edge with src/ohmyhost/companion.ts when database, auth, mail, ai, storage or functions.crons is set, and static otherwise",
513
+ "tanstack-start": "runtime.mode: edge, or static without functions.crons, storage and ai",
514
514
  astro: "runtime.mode: static for prerendered sites, or edge with the Cloudflare adapter",
515
515
  },
516
516
  },
@@ -580,7 +580,7 @@ const FRAMEWORK_CONVERSION_DETAILS = Object.freeze({
580
580
  companion_entry_required: {
581
581
  paths: ["module"],
582
582
  template:
583
- '"{{path}}" is missing. A Vite app with runtime.mode: edge or with database, auth, mail, storage or functions.crons needs this Worker module with export default { fetch }. Add it, or use runtime.mode: static without those settings; commit and push the change, then plan the new commit.',
583
+ '"{{path}}" is missing. A Vite app with runtime.mode: edge or with database, auth, mail, ai, storage or functions.crons needs this Worker module with export default { fetch }. Add it, or use runtime.mode: static without those settings; commit and push the change, then plan the new commit.',
584
584
  },
585
585
  worker_module_required: {
586
586
  paths: ["module"],
@@ -209,6 +209,7 @@ export function viteCompanionRequired(configuration: OhmyhostConfig): boolean {
209
209
  configuration.database?.enabled === true ||
210
210
  configuration.auth?.provider === "better-auth" ||
211
211
  configuration.mail?.enabled === true ||
212
+ configuration.ai?.enabled === true ||
212
213
  configuration.storage !== undefined ||
213
214
  (configuration.functions?.crons.length ?? 0) > 0
214
215
  );
@@ -637,12 +638,19 @@ function runtimeModeAdmitted(
637
638
  configuration.storage === undefined &&
638
639
  configuration.database?.enabled !== true &&
639
640
  configuration.mail?.enabled !== true &&
641
+ configuration.ai?.enabled !== true &&
640
642
  configuration.auth?.provider !== "better-auth")
641
643
  );
642
644
  if (framework === "nextjs") return mode === "edge";
643
645
  if (framework === "vite-static")
644
646
  return mode === (viteCompanionRequired(configuration) ? "edge" : "static");
645
- return mode === "edge" || (mode === "static" && !crons && configuration.storage === undefined);
647
+ return (
648
+ mode === "edge" ||
649
+ (mode === "static" &&
650
+ !crons &&
651
+ configuration.storage === undefined &&
652
+ configuration.ai?.enabled !== true)
653
+ );
646
654
  }
647
655
 
648
656
  function betterAuthFindings(
@@ -56,6 +56,11 @@ export interface OhmyhostConfig {
56
56
  jurisdiction: "us" | "eu";
57
57
  public: false;
58
58
  }>;
59
+ /** `jurisdiction` is required when `enabled` is true; it has no default. */
60
+ readonly ai?: Readonly<{
61
+ enabled: boolean;
62
+ jurisdiction?: "eu" | "any";
63
+ }>;
59
64
  readonly functions?: Readonly<{ crons: readonly string[] }>;
60
65
  readonly credits?: Readonly<{
61
66
  monthly_budget: number;
@@ -125,6 +130,14 @@ export function parseOhmyhostConfig(value: unknown): OhmyhostConfig {
125
130
  public: input.storage.public,
126
131
  }),
127
132
  }),
133
+ ...(input.ai === undefined
134
+ ? {}
135
+ : {
136
+ ai: Object.freeze({
137
+ enabled: input.ai.enabled,
138
+ ...(input.ai.jurisdiction === undefined ? {} : { jurisdiction: input.ai.jurisdiction }),
139
+ }),
140
+ }),
128
141
  ...(input.functions === undefined
129
142
  ? {}
130
143
  : {
@@ -227,6 +227,20 @@ export const PROBLEM_POLICY = {
227
227
  "A migration already attaches a capture trigger to this table: keep it and skip the watch, or remove it from the migrations, deploy and create the watch again. A table carries one capture trigger.",
228
228
  title: "Table already has a capture trigger",
229
229
  },
230
+ database_table_conflict: {
231
+ status: 409,
232
+ retryable: false,
233
+ action:
234
+ "A table with this name already exists with another definition. Choose another table name, or create the table again with the same columns, types, nullability, identity, defaults and primary key it has.",
235
+ title: "Table already exists with another definition",
236
+ },
237
+ database_table_limit: {
238
+ status: 409,
239
+ retryable: false,
240
+ action:
241
+ "This database already holds the maximum number of tables in its public schema, so no table can be added. Use an existing table, or remove a table you no longer need through a deployed migration, then create the table again.",
242
+ title: "Database table limit reached",
243
+ },
230
244
  project_notes_conflict: {
231
245
  status: 409,
232
246
  retryable: false,
@@ -13,6 +13,7 @@ const FIXED_PLATFORM_NAMES = new Set([
13
13
  const INSTALLED_PLATFORM_KEYS = new Set([
14
14
  "OHMYHOST_STORAGE_KEY",
15
15
  "OHMYHOST_MAIL_KEY",
16
+ "OHMYHOST_AI_KEY",
16
17
  "BETTER_AUTH_SECRET",
17
18
  ]);
18
19
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ohmyhost/sdk-ts",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -23,6 +23,7 @@ export {
23
23
  createOrganization,
24
24
  createProject,
25
25
  createProjectDatabaseAccess,
26
+ createProjectDatabaseTable,
26
27
  createProjectDatabaseWatch,
27
28
  createProjectDatabaseWebhook,
28
29
  createProjectDataChange,
@@ -274,6 +275,11 @@ export type {
274
275
  CreateProjectDatabaseAccessErrors,
275
276
  CreateProjectDatabaseAccessResponse,
276
277
  CreateProjectDatabaseAccessResponses,
278
+ CreateProjectDatabaseTableData,
279
+ CreateProjectDatabaseTableError,
280
+ CreateProjectDatabaseTableErrors,
281
+ CreateProjectDatabaseTableResponse,
282
+ CreateProjectDatabaseTableResponses,
277
283
  CreateProjectDatabaseWatchData,
278
284
  CreateProjectDatabaseWatchError,
279
285
  CreateProjectDatabaseWatchErrors,
@@ -333,6 +339,12 @@ export type {
333
339
  DatabaseScheduleRunAccepted,
334
340
  DatabaseScheduleRunPage,
335
341
  DatabaseScheduleSetRequest,
342
+ DatabaseTable,
343
+ DatabaseTableColumn,
344
+ DatabaseTableColumnRequest,
345
+ DatabaseTableColumnType,
346
+ DatabaseTableCreated,
347
+ DatabaseTableCreateRequest,
336
348
  DatabaseWatch,
337
349
  DatabaseWatchCreateRequest,
338
350
  DatabaseWatchDeleted,
@@ -60,6 +60,8 @@ import type {
60
60
  CreateOrganizationResponses,
61
61
  CreateProjectDatabaseAccessErrors,
62
62
  CreateProjectDatabaseAccessResponses,
63
+ CreateProjectDatabaseTableErrors,
64
+ CreateProjectDatabaseTableResponses,
63
65
  CreateProjectDatabaseWatchErrors,
64
66
  CreateProjectDatabaseWatchResponses,
65
67
  CreateProjectDatabaseWebhookErrors,
@@ -79,6 +81,7 @@ import type {
79
81
  CreateWorkspaceMailDomainRequest,
80
82
  CreateWorkspaceMailDomainResponses,
81
83
  DatabaseScheduleSetRequest,
84
+ DatabaseTableCreateRequest,
82
85
  DatabaseWatchCreateRequest,
83
86
  DatabaseWebhookCreateRequest,
84
87
  DeclareApplicationDomainIdentityErrors,
@@ -4766,6 +4769,57 @@ export const revokeProjectDatabaseAccess = <ThrowOnError extends boolean = true>
4766
4769
  });
4767
4770
  };
4768
4771
 
4772
+ /**
4773
+ * Create a table in the project's database
4774
+ *
4775
+ * Creates one ordinary table in the public schema of one environment's database, with the columns and the primary key the request names. Repeating an identical request answers the existing table with 200; a table of the same name with another definition is refused with database_table_conflict (409). The request wakes a sleeping database. A reached Stop budget refuses with project_budget_exceeded (402). At most 100 tables exist per database.
4776
+ */
4777
+ export const createProjectDatabaseTable = <ThrowOnError extends boolean = true>(
4778
+ parameters: {
4779
+ "X-Request-Id"?: string;
4780
+ "Idempotency-Key": string;
4781
+ project_id: Ulid;
4782
+ databaseTableCreateRequest: DatabaseTableCreateRequest;
4783
+ },
4784
+ options?: Options<never, ThrowOnError>,
4785
+ ): RequestResult<
4786
+ CreateProjectDatabaseTableResponses,
4787
+ CreateProjectDatabaseTableErrors,
4788
+ ThrowOnError,
4789
+ "data"
4790
+ > => {
4791
+ const params = buildClientParams(
4792
+ [parameters],
4793
+ [
4794
+ {
4795
+ args: [
4796
+ { in: "headers", key: "X-Request-Id" },
4797
+ { in: "headers", key: "Idempotency-Key" },
4798
+ { in: "path", key: "project_id" },
4799
+ { key: "databaseTableCreateRequest", map: "body" },
4800
+ ],
4801
+ },
4802
+ ],
4803
+ );
4804
+ return (options?.client ?? client).post<
4805
+ CreateProjectDatabaseTableResponses,
4806
+ CreateProjectDatabaseTableErrors,
4807
+ ThrowOnError,
4808
+ "data"
4809
+ >({
4810
+ responseStyle: "data",
4811
+ security: [{ scheme: "bearer", type: "http" }],
4812
+ url: "/v1/projects/{project_id}/database/tables",
4813
+ ...options,
4814
+ ...params,
4815
+ headers: {
4816
+ "Content-Type": "application/json",
4817
+ ...options?.headers,
4818
+ ...params.headers,
4819
+ },
4820
+ });
4821
+ };
4822
+
4769
4823
  /**
4770
4824
  * List the project's database watches
4771
4825
  *