@exposify/supabase 0.3.13 → 0.3.15

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 CHANGED
@@ -1,4 +1,4 @@
1
- # @exposify/supabase — setup guide
1
+ # @exposify/supabase
2
2
 
3
3
  Make a Supabase-backed app's database and edge functions usable by AI agents:
4
4
  one edge function serves MCP, with tools generated from your database
@@ -7,9 +7,42 @@ register. The caller's JWT passes straight through to PostgREST — **RLS is
7
7
  the authorization layer**. The agent can do exactly what that logged-in user
8
8
  can do, nothing more.
9
9
 
10
- This guide walks through setup end to end. For the design rationale, see
11
- `docs/superpowers/specs/2026-08-15-agentify-supabase-design.md` in the repo
12
- root.
10
+ > **Which package?** Use **`@exposify/supabase`** when you want an MCP
11
+ > endpoint served straight from a Supabase **edge function** over PostgREST,
12
+ > with RLS as the authorization layer. If you have a **Next.js app** (even one
13
+ > whose users sign in with Supabase Auth), use
14
+ > [**`@exposify/next`**](https://github.com/ayadagain/agentready/tree/main/packages/next)
15
+ > instead, so tool calls run through your Next server and your own middleware
16
+ > and checks apply.
17
+
18
+ ## Quickstart
19
+
20
+ From your Supabase project's root (the directory containing `supabase/`):
21
+
22
+ ```bash
23
+ npx @exposify/supabase init # scaffold the migration + mcp function
24
+ supabase db push # apply the exposify_meta() migration
25
+ supabase functions deploy mcp # deploy the MCP edge function
26
+ ```
27
+
28
+ Then point an MCP client at the function's URL with a user's JWT — e.g. with
29
+ the Claude Code CLI:
30
+
31
+ ```bash
32
+ claude mcp add --transport http my-app \
33
+ https://<project-ref>.supabase.co/functions/v1/mcp \
34
+ --header "Authorization: Bearer <user-jwt>"
35
+ ```
36
+
37
+ Every RLS-enabled table you can `SELECT` becomes a `query_<table>` tool
38
+ automatically; writes and RPCs are opt-in. `init` is interactive and can run
39
+ `db push`/`functions deploy` for you, wire in the hosted broker or Supabase
40
+ Auth OAuth (so clients sign in by themselves instead of pasting an hourly
41
+ JWT), and write tool descriptions from your SQL comments — see the sections
42
+ below. Check the whole setup any time with `npx @exposify/supabase doctor`.
43
+
44
+ The rest of this guide is reference, end to end. For the design rationale, see
45
+ the [design spec](https://github.com/ayadagain/agentready/blob/main/docs/superpowers/specs/2026-08-15-agentify-supabase-design.md).
13
46
 
14
47
  ## Prerequisites
15
48
 
@@ -28,15 +61,18 @@ npx @exposify/supabase init
28
61
 
29
62
  This is safe to re-run; see "Re-running `init`" below. It writes:
30
63
 
31
- - `supabase/migrations/<timestamp>_exposify_meta_v2.sql` — a SQL migration that
64
+ - `supabase/migrations/<timestamp>_exposify_meta_v3.sql` — a SQL migration that
32
65
  adds `exposify_meta()`, a `SECURITY DEFINER` function returning, per table,
33
66
  whether RLS is enabled, what the `authenticated` role is granted on it
34
- (select/insert/update/delete), and any `COMMENT ON TABLE`/`COMMENT ON COLUMN` text
35
- you've written. It only ever returns catalog metadata (table/column names,
36
- flags, comments) — never row data — which is what makes it safe to grant
37
- to `anon`/`authenticated` and call with a low-privilege key. A project with
38
- the older `<timestamp>_exposify_meta.sql` (no grants) gets the `_v2` one on
39
- the next `init`, which replaces the function.
67
+ (select/insert/update/delete), whether each column is an identity or
68
+ generated column (so `insert_<table>` doesn't ask for a `generated always as
69
+ identity` key the database rejects), and any `COMMENT ON TABLE`/`COMMENT ON
70
+ COLUMN` text you've written. It only ever returns catalog metadata
71
+ (table/column names, flags, comments) — never row data — which is what makes
72
+ it safe to grant to `anon`/`authenticated` and call with a low-privilege key.
73
+ A project on an older migration (`_exposify_meta.sql` with no grants, or
74
+ `_exposify_meta_v2.sql` with no identity/generated columns) gets the `_v3`
75
+ one on the next `init`, which replaces the function.
40
76
  - `supabase/functions/mcp/index.ts` — the edge function itself. `init` owns
41
77
  the part between `// exposify:begin` and `// exposify:end` (imports and a
42
78
  `generated` options object). Everything after that is yours, starting with
@@ -196,7 +232,7 @@ things:
196
232
  Until the project has a sign-in method (the Supabase preset in the
197
233
  dashboard), agents use the function signed out, as `anon`, with no sign-in
198
234
  asked: there's nothing to sign in with. The log says which it is
199
- (`[agentready] How agents sign in: ...`), and adding the method takes effect
235
+ (`[exposify] How agents sign in: ...`), and adding the method takes effect
200
236
  within a minute.
201
237
 
202
238
  MCP clients then click Connect and sign in through the broker (or send a
@@ -252,6 +288,12 @@ Deno.serve(aiReady({
252
288
  ```
253
289
 
254
290
  Notes:
291
+ - `insert_<table>` takes a `body` with one field per column, but only the
292
+ columns the caller actually supplies: a column with a `DEFAULT` or a `BY
293
+ DEFAULT` identity is optional (the database fills it in), and a `GENERATED
294
+ ALWAYS AS IDENTITY` column or a generated column is left out entirely (the
295
+ database computes it and rejects any value). So a table whose only
296
+ caller-supplied column is `body` is inserted with just `{ "body": "..." }`.
255
297
  - `update_<table>` and `delete_<table>` change one row: the filter must
256
298
  include `eq` on every primary-key column (more filters only narrow it).
257
299
  A filter that happens to match everything (`id neq -1`, `item like %`) is
@@ -357,9 +399,9 @@ exits 1 if anything failed, so you can run it in CI.
357
399
  readable. It lists which tables became tools, and warns about tables that
358
400
  never will: RLS off, or RLS on but no `SELECT` for `authenticated` (with the
359
401
  `grant` to run). It warns about writes in the function's config the role
360
- isn't granted, about the older `exposify_meta()` that doesn't report
361
- grants, and about tables whose missing comments leave tools with thin
362
- descriptions.
402
+ isn't granted, about an older `exposify_meta()` that doesn't report
403
+ grants or identity/generated columns, and about tables whose missing
404
+ comments leave tools with thin descriptions.
363
405
  - **Deployed function:** speaks MCP to `<project>/functions/v1/mcp`
364
406
  (`initialize`, then `tools/list`) and reports the tool count. If the
365
407
  function registered `exposify://diagnostics`, which it only does when
@@ -481,7 +523,7 @@ Supabase user JWTs expire (default 1h) and their refresh tokens rotate; a plain
481
523
  MCP client can hold a bearer token but can't refresh it. Exposify's hosted
482
524
  broker holds each user's session, refreshes it, and swaps the agent's
483
525
  long-lived connection token (`extb_...`) for a live Supabase JWT on every
484
- call ([`docs/hosted-broker.md`](../../docs/hosted-broker.md)). `init` sets it
526
+ call (see the [hosted broker guide](https://github.com/ayadagain/agentready/blob/main/docs/hosted-broker.md)). `init` sets it
485
527
  up (see "Using the hosted broker" above); by hand:
486
528
 
487
529
  ```sh
@@ -580,7 +622,7 @@ the function prints whether MCP is up. It shows in `supabase functions serve`
580
622
  locally and in the function's logs in production:
581
623
 
582
624
  ```
583
- [@exposify/supabase] MCP ready at https://<ref>.supabase.co/functions/v1/mcp: 9 tools (5 query, 2 write, 1 rpc, search_docs), 3 docs indexed, auth: passthrough (caller's JWT)
625
+ [exposify] MCP ready at https://<ref>.supabase.co/functions/v1/mcp: 9 tools (5 query, 2 write, 1 rpc, search_docs), 3 docs indexed, auth: passthrough (caller's JWT)
584
626
  ```
585
627
 
586
628
  - **Manifest fallback:** the same line, marked "from the bundled manifest",
package/dist/cli.js CHANGED
@@ -76,6 +76,10 @@ function columnsFromOpenApi(doc, table) {
76
76
  type: TYPE_MAP[prop.type ?? "string"] ?? "string",
77
77
  format: prop.format,
78
78
  required: required.has(name),
79
+ // A column with a DEFAULT doesn't have to be supplied on insert even when
80
+ // it's NOT NULL (the database fills it in). identity/generated aren't in
81
+ // PostgREST's OpenAPI -- introspect.ts adds those from exposify_meta().
82
+ hasDefault: prop.default !== void 0,
79
83
  isPrimaryKey: isPrimaryKey(prop),
80
84
  // Falls back to the OpenAPI-embedded comment (PostgREST mirrors `COMMENT
81
85
  // ON COLUMN` into `description`, then appends its own "Note:\nThis is a
@@ -110,7 +114,13 @@ function introspectTables(openapi, metaRows) {
110
114
  ...fromOpenApi,
111
115
  // exposify_meta()'s comment wins over OpenAPI's mirrored copy (spec:
112
116
  // "Column comments → param descriptions" sourced from the migration).
113
- description: row.column_comment ?? fromOpenApi.description
117
+ description: row.column_comment ?? fromOpenApi.description,
118
+ // identity/generated aren't in PostgREST's OpenAPI; they come from
119
+ // exposify_meta() (the `_v3` migration). Absent on the older function,
120
+ // leaving these unset -- the insert/update shapes then fall back to
121
+ // defaults alone, which is the pre-fix behaviour.
122
+ ...row.identity_kind ? { identity: row.identity_kind } : {},
123
+ ...row.is_generated === true ? { generated: true } : {}
114
124
  });
115
125
  }
116
126
  if (columns.length === 0) continue;
@@ -569,7 +579,7 @@ function warnOnce(message) {
569
579
  if (warned.has(message)) return;
570
580
  warned.add(message);
571
581
  try {
572
- console.warn(`[@exposify/supabase] ${message}`);
582
+ console.warn(`[exposify] ${message}`);
573
583
  } catch {
574
584
  }
575
585
  }
@@ -741,14 +751,14 @@ async function runDoctor(deps) {
741
751
  sections.push({ title: "Project", checks: project });
742
752
  const files = [];
743
753
  const migrations = await deps.listDir(MIGRATIONS_DIR);
744
- const migration = findExistingMigration(migrations, "_exposify_meta_v2.sql");
745
- const legacyMigration = findExistingMigration(migrations, "_exposify_meta.sql");
754
+ const migration = findExistingMigration(migrations, "_exposify_meta_v3.sql");
755
+ const legacyMigration = findExistingMigration(migrations, "_exposify_meta_v2.sql") || findExistingMigration(migrations, "_exposify_meta.sql");
746
756
  files.push(
747
757
  migration ? { status: "pass", message: `migration ${MIGRATIONS_DIR}/${migration}` } : legacyMigration ? {
748
758
  status: "warn",
749
- message: `migration ${MIGRATIONS_DIR}/${legacyMigration} is the older exposify_meta(), which doesn't report grants`,
750
- fix: "npx @exposify/supabase init (it adds *_exposify_meta_v2.sql), then supabase db push"
751
- } : { status: "fail", message: "no *_exposify_meta_v2.sql migration", fix: "npx @exposify/supabase init" }
759
+ message: `migration ${MIGRATIONS_DIR}/${legacyMigration} is an older exposify_meta(), which doesn't report identity/generated columns`,
760
+ fix: "npx @exposify/supabase init (it adds *_exposify_meta_v3.sql), then supabase db push"
761
+ } : { status: "fail", message: "no *_exposify_meta_v3.sql migration", fix: "npx @exposify/supabase init" }
752
762
  );
753
763
  const functionSource = await deps.readText(FUNCTION_PATH);
754
764
  const authMode = functionSource === null ? null : detectAuthMode(functionSource);
@@ -1000,7 +1010,8 @@ var FUNCTION_DIR = "supabase/functions/mcp";
1000
1010
  var FUNCTION_PATH2 = `${FUNCTION_DIR}/index.ts`;
1001
1011
  var MIGRATIONS_DIR2 = "supabase/migrations";
1002
1012
  var CONFIG_TOML2 = "supabase/config.toml";
1003
- var META_SUFFIX = "_exposify_meta_v2.sql";
1013
+ var META_SUFFIX = "_exposify_meta_v3.sql";
1014
+ var META_SUFFIX_V2 = "_exposify_meta_v2.sql";
1004
1015
  var LEGACY_META_SUFFIX = "_exposify_meta.sql";
1005
1016
  var COMMENTS_SUFFIX = "_exposify_comments.sql";
1006
1017
  function authLabel(config) {
@@ -1036,7 +1047,7 @@ function planInit(input) {
1036
1047
  changes.push({
1037
1048
  path: `${MIGRATIONS_DIR2}/${name}`,
1038
1049
  action: "create",
1039
- summary: findExistingMigration(input.migrations, LEGACY_META_SUFFIX) ? "exposify_meta() update: it also reports the authenticated role's grants" : "exposify_meta() migration",
1050
+ summary: findExistingMigration(input.migrations, LEGACY_META_SUFFIX) || findExistingMigration(input.migrations, META_SUFFIX_V2) ? "exposify_meta() update: it also reports identity and generated columns" : "exposify_meta() migration",
1040
1051
  content: input.metaMigrationSql,
1041
1052
  effect: "push"
1042
1053
  });
package/dist/index.cjs CHANGED
@@ -107,7 +107,7 @@ function warnOnce(message) {
107
107
  if (warned.has(message)) return;
108
108
  warned.add(message);
109
109
  try {
110
- console.warn(`[@exposify/supabase] ${message}`);
110
+ console.warn(`[exposify] ${message}`);
111
111
  } catch {
112
112
  }
113
113
  }
@@ -258,6 +258,10 @@ function columnsFromOpenApi(doc, table) {
258
258
  type: TYPE_MAP[prop.type ?? "string"] ?? "string",
259
259
  format: prop.format,
260
260
  required: required.has(name),
261
+ // A column with a DEFAULT doesn't have to be supplied on insert even when
262
+ // it's NOT NULL (the database fills it in). identity/generated aren't in
263
+ // PostgREST's OpenAPI -- introspect.ts adds those from exposify_meta().
264
+ hasDefault: prop.default !== void 0,
261
265
  isPrimaryKey: isPrimaryKey(prop),
262
266
  // Falls back to the OpenAPI-embedded comment (PostgREST mirrors `COMMENT
263
267
  // ON COLUMN` into `description`, then appends its own "Note:\nThis is a
@@ -302,7 +306,13 @@ function introspectTables(openapi, metaRows) {
302
306
  ...fromOpenApi,
303
307
  // exposify_meta()'s comment wins over OpenAPI's mirrored copy (spec:
304
308
  // "Column comments → param descriptions" sourced from the migration).
305
- description: row.column_comment ?? fromOpenApi.description
309
+ description: row.column_comment ?? fromOpenApi.description,
310
+ // identity/generated aren't in PostgREST's OpenAPI; they come from
311
+ // exposify_meta() (the `_v3` migration). Absent on the older function,
312
+ // leaving these unset -- the insert/update shapes then fall back to
313
+ // defaults alone, which is the pre-fix behaviour.
314
+ ...row.identity_kind ? { identity: row.identity_kind } : {},
315
+ ...row.is_generated === true ? { generated: true } : {}
306
316
  });
307
317
  }
308
318
  if (columns.length === 0) continue;
@@ -527,11 +537,16 @@ function registerQueryTool(server, table, client, cfg, taken) {
527
537
  }
528
538
  );
529
539
  }
540
+ function isDatabaseGenerated(col) {
541
+ return col.identity === "always" || col.generated === true;
542
+ }
530
543
  function insertBodyShape(columns) {
531
544
  const shape = {};
532
545
  for (const col of columns) {
546
+ if (isDatabaseGenerated(col)) continue;
533
547
  let field = columnToZod(col);
534
- if (!col.required) field = field.optional();
548
+ const databaseFillsIn = col.hasDefault === true || col.identity === "default";
549
+ if (!col.required || databaseFillsIn) field = field.optional();
535
550
  shape[col.name] = field;
536
551
  }
537
552
  return shape;
@@ -582,7 +597,10 @@ function keyFilterShape(table, verb) {
582
597
  function registerUpdateTool(server, table, client, taken) {
583
598
  const name = toolName("update", table.name, taken);
584
599
  const setShape = {};
585
- for (const col of table.columns) setShape[col.name] = columnToZod(col).optional();
600
+ for (const col of table.columns) {
601
+ if (isDatabaseGenerated(col)) continue;
602
+ setShape[col.name] = columnToZod(col).optional();
603
+ }
586
604
  const keys = keyColumns(table);
587
605
  server.registerTool(
588
606
  name,
@@ -737,7 +755,7 @@ function createSupabaseOAuthGate(oauth, supabaseUrl, fetchImpl) {
737
755
 
738
756
  // src/startup.ts
739
757
  var import_core4 = require("@exposify/core");
740
- var LOG_PREFIX = "[@exposify/supabase]";
758
+ var LOG_PREFIX = "[exposify]";
741
759
  var INTROSPECTION_FIX = "check that SUPABASE_SERVICE_ROLE_KEY is available to the function and the exposify_meta migration is applied (`supabase db push`); the reason is logged above. To survive this, bundle a manifest with `npx @exposify/supabase generate`";
742
760
  function supabaseToolsPhrase(ladder, cfg, hasDocs) {
743
761
  const query = ladder.tables.length;
@@ -856,7 +874,7 @@ function aiReady(options = {}) {
856
874
  manifest: options.manifest
857
875
  }).then((ladder) => {
858
876
  for (const w of ladder.warnings) {
859
- console.error(`[@exposify/supabase] degradation ladder: ${w.rung}: ${w.reason}`);
877
+ console.error(`[exposify] degradation ladder: ${w.rung}: ${w.reason}`);
860
878
  }
861
879
  return ladder;
862
880
  });
package/dist/index.d.cts CHANGED
@@ -7,7 +7,19 @@ interface ColumnSchema {
7
7
  type: ColumnType;
8
8
  /** OpenAPI `format`, e.g. "uuid", "timestamp with time zone", "bigint". */
9
9
  format?: string;
10
+ /** NOT NULL per PostgREST's OpenAPI `required`. Whether it must be *given* on insert is a separate question -- see `hasDefault`/`identity` and server.ts's `insertBodyShape`. */
10
11
  required: boolean;
12
+ /** The OpenAPI property carried a `default`, so the database fills the column in when it's omitted -- optional on insert even when NOT NULL. */
13
+ hasDefault?: boolean;
14
+ /**
15
+ * An identity column (`GENERATED ... AS IDENTITY`), from `exposify_meta()`.
16
+ * `"always"` rejects any supplied value (left out of insert/update entirely);
17
+ * `"default"` is accepted but optional. Absent on a non-identity column, and
18
+ * on every column when the database still has the pre-identity `exposify_meta()`.
19
+ */
20
+ identity?: "always" | "default";
21
+ /** A generated column (`GENERATED ALWAYS AS (...) STORED`), from `exposify_meta()`: the database computes it, so it's left out of insert/update. */
22
+ generated?: boolean;
11
23
  /** From `exposify_meta()`'s `column_comment` (a `COMMENT ON COLUMN`), not OpenAPI. */
12
24
  description?: string;
13
25
  isPrimaryKey?: boolean;
@@ -43,6 +55,10 @@ interface MetaRow {
43
55
  can_insert?: boolean;
44
56
  can_update?: boolean;
45
57
  can_delete?: boolean;
58
+ /** `pg_attribute.attidentity` mapped: `"always"` ('a'), `"default"` ('d'), or null/absent. Absent before the `_v3` migration. */
59
+ identity_kind?: "always" | "default" | null;
60
+ /** `pg_attribute.attgenerated <> ''` -- a generated (stored) column. Absent before the `_v3` migration. */
61
+ is_generated?: boolean;
46
62
  }
47
63
  /** Registered sibling edge function exposed as a tool. */
48
64
  interface FunctionToolConfig {
@@ -189,6 +205,8 @@ interface PostgrestColumnProperty {
189
205
  type?: string;
190
206
  format?: string;
191
207
  description?: string;
208
+ /** PostgREST emits the column's `DEFAULT` expression here (e.g. `"now()"`, `"auth.uid()"`) when it has one. We only care whether it's present. */
209
+ default?: unknown;
192
210
  }
193
211
  /**
194
212
  * Extracts column schemas for one table from a PostgREST OpenAPI document.
package/dist/index.d.ts CHANGED
@@ -7,7 +7,19 @@ interface ColumnSchema {
7
7
  type: ColumnType;
8
8
  /** OpenAPI `format`, e.g. "uuid", "timestamp with time zone", "bigint". */
9
9
  format?: string;
10
+ /** NOT NULL per PostgREST's OpenAPI `required`. Whether it must be *given* on insert is a separate question -- see `hasDefault`/`identity` and server.ts's `insertBodyShape`. */
10
11
  required: boolean;
12
+ /** The OpenAPI property carried a `default`, so the database fills the column in when it's omitted -- optional on insert even when NOT NULL. */
13
+ hasDefault?: boolean;
14
+ /**
15
+ * An identity column (`GENERATED ... AS IDENTITY`), from `exposify_meta()`.
16
+ * `"always"` rejects any supplied value (left out of insert/update entirely);
17
+ * `"default"` is accepted but optional. Absent on a non-identity column, and
18
+ * on every column when the database still has the pre-identity `exposify_meta()`.
19
+ */
20
+ identity?: "always" | "default";
21
+ /** A generated column (`GENERATED ALWAYS AS (...) STORED`), from `exposify_meta()`: the database computes it, so it's left out of insert/update. */
22
+ generated?: boolean;
11
23
  /** From `exposify_meta()`'s `column_comment` (a `COMMENT ON COLUMN`), not OpenAPI. */
12
24
  description?: string;
13
25
  isPrimaryKey?: boolean;
@@ -43,6 +55,10 @@ interface MetaRow {
43
55
  can_insert?: boolean;
44
56
  can_update?: boolean;
45
57
  can_delete?: boolean;
58
+ /** `pg_attribute.attidentity` mapped: `"always"` ('a'), `"default"` ('d'), or null/absent. Absent before the `_v3` migration. */
59
+ identity_kind?: "always" | "default" | null;
60
+ /** `pg_attribute.attgenerated <> ''` -- a generated (stored) column. Absent before the `_v3` migration. */
61
+ is_generated?: boolean;
46
62
  }
47
63
  /** Registered sibling edge function exposed as a tool. */
48
64
  interface FunctionToolConfig {
@@ -189,6 +205,8 @@ interface PostgrestColumnProperty {
189
205
  type?: string;
190
206
  format?: string;
191
207
  description?: string;
208
+ /** PostgREST emits the column's `DEFAULT` expression here (e.g. `"now()"`, `"auth.uid()"`) when it has one. We only care whether it's present. */
209
+ default?: unknown;
192
210
  }
193
211
  /**
194
212
  * Extracts column schemas for one table from a PostgREST OpenAPI document.
package/dist/index.js CHANGED
@@ -70,7 +70,7 @@ function warnOnce(message) {
70
70
  if (warned.has(message)) return;
71
71
  warned.add(message);
72
72
  try {
73
- console.warn(`[@exposify/supabase] ${message}`);
73
+ console.warn(`[exposify] ${message}`);
74
74
  } catch {
75
75
  }
76
76
  }
@@ -221,6 +221,10 @@ function columnsFromOpenApi(doc, table) {
221
221
  type: TYPE_MAP[prop.type ?? "string"] ?? "string",
222
222
  format: prop.format,
223
223
  required: required.has(name),
224
+ // A column with a DEFAULT doesn't have to be supplied on insert even when
225
+ // it's NOT NULL (the database fills it in). identity/generated aren't in
226
+ // PostgREST's OpenAPI -- introspect.ts adds those from exposify_meta().
227
+ hasDefault: prop.default !== void 0,
224
228
  isPrimaryKey: isPrimaryKey(prop),
225
229
  // Falls back to the OpenAPI-embedded comment (PostgREST mirrors `COMMENT
226
230
  // ON COLUMN` into `description`, then appends its own "Note:\nThis is a
@@ -265,7 +269,13 @@ function introspectTables(openapi, metaRows) {
265
269
  ...fromOpenApi,
266
270
  // exposify_meta()'s comment wins over OpenAPI's mirrored copy (spec:
267
271
  // "Column comments → param descriptions" sourced from the migration).
268
- description: row.column_comment ?? fromOpenApi.description
272
+ description: row.column_comment ?? fromOpenApi.description,
273
+ // identity/generated aren't in PostgREST's OpenAPI; they come from
274
+ // exposify_meta() (the `_v3` migration). Absent on the older function,
275
+ // leaving these unset -- the insert/update shapes then fall back to
276
+ // defaults alone, which is the pre-fix behaviour.
277
+ ...row.identity_kind ? { identity: row.identity_kind } : {},
278
+ ...row.is_generated === true ? { generated: true } : {}
269
279
  });
270
280
  }
271
281
  if (columns.length === 0) continue;
@@ -494,11 +504,16 @@ function registerQueryTool(server, table, client, cfg, taken) {
494
504
  }
495
505
  );
496
506
  }
507
+ function isDatabaseGenerated(col) {
508
+ return col.identity === "always" || col.generated === true;
509
+ }
497
510
  function insertBodyShape(columns) {
498
511
  const shape = {};
499
512
  for (const col of columns) {
513
+ if (isDatabaseGenerated(col)) continue;
500
514
  let field = columnToZod(col);
501
- if (!col.required) field = field.optional();
515
+ const databaseFillsIn = col.hasDefault === true || col.identity === "default";
516
+ if (!col.required || databaseFillsIn) field = field.optional();
502
517
  shape[col.name] = field;
503
518
  }
504
519
  return shape;
@@ -549,7 +564,10 @@ function keyFilterShape(table, verb) {
549
564
  function registerUpdateTool(server, table, client, taken) {
550
565
  const name = toolName("update", table.name, taken);
551
566
  const setShape = {};
552
- for (const col of table.columns) setShape[col.name] = columnToZod(col).optional();
567
+ for (const col of table.columns) {
568
+ if (isDatabaseGenerated(col)) continue;
569
+ setShape[col.name] = columnToZod(col).optional();
570
+ }
553
571
  const keys = keyColumns(table);
554
572
  server.registerTool(
555
573
  name,
@@ -704,7 +722,7 @@ function createSupabaseOAuthGate(oauth, supabaseUrl, fetchImpl) {
704
722
 
705
723
  // src/startup.ts
706
724
  import { formatFailed, formatReady, formatWarning } from "@exposify/core";
707
- var LOG_PREFIX = "[@exposify/supabase]";
725
+ var LOG_PREFIX = "[exposify]";
708
726
  var INTROSPECTION_FIX = "check that SUPABASE_SERVICE_ROLE_KEY is available to the function and the exposify_meta migration is applied (`supabase db push`); the reason is logged above. To survive this, bundle a manifest with `npx @exposify/supabase generate`";
709
727
  function supabaseToolsPhrase(ladder, cfg, hasDocs) {
710
728
  const query = ladder.tables.length;
@@ -823,7 +841,7 @@ function aiReady(options = {}) {
823
841
  manifest: options.manifest
824
842
  }).then((ladder) => {
825
843
  for (const w of ladder.warnings) {
826
- console.error(`[@exposify/supabase] degradation ladder: ${w.rung}: ${w.reason}`);
844
+ console.error(`[exposify] degradation ladder: ${w.rung}: ${w.reason}`);
827
845
  }
828
846
  return ladder;
829
847
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exposify/supabase",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -27,7 +27,7 @@
27
27
  "prepack": "yarn build"
28
28
  },
29
29
  "dependencies": {
30
- "@exposify/core": "0.3.17",
30
+ "@exposify/core": "0.3.19",
31
31
  "@modelcontextprotocol/sdk": "^1.12.0",
32
32
  "zod": "^3.24.0"
33
33
  },
@@ -28,6 +28,12 @@
28
28
  -- The grant columns were added later, as a new migration
29
29
  -- (`<timestamp>_exposify_meta_v2.sql`): adding output columns changes the
30
30
  -- function's return type, which `create or replace` can't do, hence the drop.
31
+ -- The `identity_kind`/`is_generated` columns were added later still, as a
32
+ -- third migration (`<timestamp>_exposify_meta_v3.sql`), for the same reason:
33
+ -- they tell the insert/update tools which columns the database computes
34
+ -- itself (an identity or generated column) so they aren't demanded or even
35
+ -- offered -- otherwise `insert_<table>` asks for a `generated always as
36
+ -- identity` primary key and PostgREST rejects any value for it.
31
37
  drop function if exists public.exposify_meta();
32
38
  create function public.exposify_meta()
33
39
  returns table (
@@ -39,7 +45,9 @@ returns table (
39
45
  can_select boolean,
40
46
  can_insert boolean,
41
47
  can_update boolean,
42
- can_delete boolean
48
+ can_delete boolean,
49
+ identity_kind text,
50
+ is_generated boolean
43
51
  )
44
52
  language sql
45
53
  security definer
@@ -55,7 +63,11 @@ as $$
55
63
  has_any_column_privilege('authenticated', c.oid, 'SELECT') as can_select,
56
64
  has_any_column_privilege('authenticated', c.oid, 'INSERT') as can_insert,
57
65
  has_any_column_privilege('authenticated', c.oid, 'UPDATE') as can_update,
58
- has_table_privilege('authenticated', c.oid, 'DELETE') as can_delete
66
+ has_table_privilege('authenticated', c.oid, 'DELETE') as can_delete,
67
+ -- 'a' = GENERATED ALWAYS AS IDENTITY, 'd' = ... BY DEFAULT, '' = not an identity column.
68
+ case a.attidentity when 'a' then 'always' when 'd' then 'default' else null end as identity_kind,
69
+ -- 's' = GENERATED ALWAYS AS (...) STORED; '' = ordinary column.
70
+ (a.attgenerated <> '') as is_generated
59
71
  from pg_catalog.pg_class c
60
72
  join pg_catalog.pg_namespace n on n.oid = c.relnamespace
61
73
  join pg_catalog.pg_attribute a on a.attrelid = c.oid