@exposify/supabase 0.3.13 → 0.3.14

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
@@ -28,15 +28,18 @@ npx @exposify/supabase init
28
28
 
29
29
  This is safe to re-run; see "Re-running `init`" below. It writes:
30
30
 
31
- - `supabase/migrations/<timestamp>_exposify_meta_v2.sql` — a SQL migration that
31
+ - `supabase/migrations/<timestamp>_exposify_meta_v3.sql` — a SQL migration that
32
32
  adds `exposify_meta()`, a `SECURITY DEFINER` function returning, per table,
33
33
  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.
34
+ (select/insert/update/delete), whether each column is an identity or
35
+ generated column (so `insert_<table>` doesn't ask for a `generated always as
36
+ identity` key the database rejects), and any `COMMENT ON TABLE`/`COMMENT ON
37
+ COLUMN` text you've written. It only ever returns catalog metadata
38
+ (table/column names, flags, comments) — never row data — which is what makes
39
+ it safe to grant to `anon`/`authenticated` and call with a low-privilege key.
40
+ A project on an older migration (`_exposify_meta.sql` with no grants, or
41
+ `_exposify_meta_v2.sql` with no identity/generated columns) gets the `_v3`
42
+ one on the next `init`, which replaces the function.
40
43
  - `supabase/functions/mcp/index.ts` — the edge function itself. `init` owns
41
44
  the part between `// exposify:begin` and `// exposify:end` (imports and a
42
45
  `generated` options object). Everything after that is yours, starting with
@@ -252,6 +255,12 @@ Deno.serve(aiReady({
252
255
  ```
253
256
 
254
257
  Notes:
258
+ - `insert_<table>` takes a `body` with one field per column, but only the
259
+ columns the caller actually supplies: a column with a `DEFAULT` or a `BY
260
+ DEFAULT` identity is optional (the database fills it in), and a `GENERATED
261
+ ALWAYS AS IDENTITY` column or a generated column is left out entirely (the
262
+ database computes it and rejects any value). So a table whose only
263
+ caller-supplied column is `body` is inserted with just `{ "body": "..." }`.
255
264
  - `update_<table>` and `delete_<table>` change one row: the filter must
256
265
  include `eq` on every primary-key column (more filters only narrow it).
257
266
  A filter that happens to match everything (`id neq -1`, `item like %`) is
@@ -357,9 +366,9 @@ exits 1 if anything failed, so you can run it in CI.
357
366
  readable. It lists which tables became tools, and warns about tables that
358
367
  never will: RLS off, or RLS on but no `SELECT` for `authenticated` (with the
359
368
  `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.
369
+ isn't granted, about an older `exposify_meta()` that doesn't report
370
+ grants or identity/generated columns, and about tables whose missing
371
+ comments leave tools with thin descriptions.
363
372
  - **Deployed function:** speaks MCP to `<project>/functions/v1/mcp`
364
373
  (`initialize`, then `tools/list`) and reports the tool count. If the
365
374
  function registered `exposify://diagnostics`, which it only does when
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;
@@ -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
@@ -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,
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
@@ -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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exposify/supabase",
3
- "version": "0.3.13",
3
+ "version": "0.3.14",
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.18",
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