Files
supabase/apps/studio/components/interfaces/Database/Schemas/Schemas.utils.test.ts
seungjae 77953e7a1f feat(studio): include enums and RLS policies in Schema Visualizer Cop… (#46189)
Extend the "Copy as Markdown" feature in the Schema Visualizer to
include Custom Types/Enums and Row Level Security (RLS) Policies in the
generated output.

Closes https://github.com/orgs/supabase/discussions/46108

## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

Feature — extend Copy as Markdown to include enums and RLS policies

## What is the current behavior?

The "Copy as Markdown" function in the Schema Visualizer generates a
SCHEMA.md that only includes tables, columns, and entity relationships.
Custom Types/Enums and RLS Policies are missing, making the output an
incomplete representation of the schema.

Related: https://github.com/orgs/supabase/discussions/46108

## What is the new behavior?

The generated markdown now includes two additional sections:

**Custom Types / Enums:**
- Lists each enum type with its ordered permitted values
- Filtered by the currently selected schema

**RLS Policies:**
- Grouped by table for self-contained readability
- Includes policy name, command (SELECT/INSERT/UPDATE/DELETE/ALL),
roles, action (PERMISSIVE/RESTRICTIVE), USING expression, and WITH CHECK
expression

**Example output:**
```markdown
## Custom Types / Enums

### `order_status`

`pending` | `processing` | `shipped` | `delivered`

## RLS Policies

### `orders`

| Policy | Command | Roles | Action | USING | WITH CHECK |
|--------|---------|-------|--------|-------|------------|
| `users_own_orders` | SELECT | authenticated | PERMISSIVE | `auth.uid() = user_id` | — |
```

## Additional context

- 3 files changed: `Schemas.utils.ts`, `SchemaGraph.tsx`,
`Schemas.utils.test.ts`
- New utility functions `getEnumsAsMarkdown()` and
`getPoliciesAsMarkdown()` with unit tests
- Reuses existing `useEnumeratedTypesQuery` and
`useDatabasePoliciesQuery` hooks
- No breaking changes — existing markdown output is preserved, new
sections are appended

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* "Copy as Markdown" now includes enumerated types and database policies
alongside existing schema/table content; policies are included with
their rule details and grouped by table.

* **Tests**
* Added tests covering enum and policy markdown generation, including
matching/non-matching schema cases and policy formatting.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/46189?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Gildas Garcia <1122076+djhi@users.noreply.github.com>
2026-07-17 16:14:31 +02:00

257 lines
6.4 KiB
TypeScript

import { describe, expect, test } from 'vitest'
import {
getEnumsAsMarkdown,
getPoliciesAsMarkdown,
getSchemaAsMarkdown,
getTableDefinitionAsMarkdown,
} from './Schemas.utils'
describe('Schemas.utils', () => {
test('getSchemaAsMarkdown returns properly formatted markdown', () => {
const schema = 'public'
const tables = [
{
ref: 'default',
id: 20999,
name: 'test',
description: 'An excellent description',
schema: 'public',
isForeign: false,
columns: [
{
id: '20999.1',
isPrimary: true,
name: 'id',
format: 'int8',
isNullable: false,
isUnique: false,
isUpdateable: true,
isIdentity: true,
description: '',
},
{
id: '20999.2',
isPrimary: false,
name: 'created_at',
format: 'timestamptz',
isNullable: false,
isUnique: false,
isUpdateable: true,
isIdentity: false,
description: '',
},
{
id: '20999.3',
isPrimary: false,
name: 'user_id',
format: 'uuid',
isNullable: true,
isUnique: false,
isUpdateable: true,
isIdentity: false,
description: '',
},
],
},
{
ref: 'default',
id: 21049,
name: 'test2',
description: '',
schema: 'public',
isForeign: false,
columns: [
{
id: '21049.1',
isPrimary: true,
name: 'id',
format: 'int8',
isNullable: false,
isUnique: false,
isUpdateable: true,
isIdentity: true,
description: '',
},
{
id: '21049.2',
isPrimary: false,
name: 'created_at',
format: 'timestamptz',
isNullable: false,
isUnique: false,
isUpdateable: true,
isIdentity: false,
description: '',
},
],
},
{
id: 21009,
ref: 'default',
schema: 'auth',
name: 'auth.users.id',
description: '',
isForeign: true,
columns: [],
},
]
const result = getSchemaAsMarkdown(schema, tables)
expect(result).toBe(`## Table \`test\`
An excellent description
### Columns
| Name | Type | Constraints |
|------|------|-------------|
| \`id\` | \`int8\` | Primary Identity |
| \`created_at\` | \`timestamptz\` | |
| \`user_id\` | \`uuid\` | Nullable |
## Table \`test2\`
### Columns
| Name | Type | Constraints |
|------|------|-------------|
| \`id\` | \`int8\` | Primary Identity |
| \`created_at\` | \`timestamptz\` | |
`)
})
test('getTableDefinitionAsMarkdown returns properly formatted markdown', () => {
const table = {
ref: 'default',
id: 20999,
name: 'test',
description: 'An excellent description',
schema: 'public',
isForeign: false,
columns: [
{
id: '20999.1',
isPrimary: true,
name: 'id',
format: 'int8',
isNullable: false,
isUnique: false,
isUpdateable: true,
isIdentity: true,
description: '',
},
{
id: '20999.2',
isPrimary: false,
name: 'created_at',
format: 'timestamptz',
isNullable: false,
isUnique: false,
isUpdateable: true,
isIdentity: false,
description: '',
},
{
id: '20999.3',
isPrimary: false,
name: 'user_id',
format: 'uuid',
isNullable: true,
isUnique: false,
isUpdateable: true,
isIdentity: false,
description: '',
},
],
}
const result = getTableDefinitionAsMarkdown(table)
expect(result).toBe(`## Table \`test\`
An excellent description
### Columns
| Name | Type | Constraints |
|------|------|-------------|
| \`id\` | \`int8\` | Primary Identity |
| \`created_at\` | \`timestamptz\` | |
| \`user_id\` | \`uuid\` | Nullable |
`)
})
test('getEnumsAsMarkdown returns formatted enum section', () => {
const enums = [
{ name: 'order_status', schema: 'public', enums: ['pending', 'processing', 'shipped'] },
{ name: 'role_type', schema: 'public', enums: ['admin', 'user'] },
{ name: 'other_enum', schema: 'auth', enums: ['a', 'b'] },
]
const result = getEnumsAsMarkdown('public', enums)
expect(result).toBe(`## Custom Types / Enums
### \`order_status\`
\`pending\` | \`processing\` | \`shipped\`
### \`role_type\`
\`admin\` | \`user\`
`)
})
test('getEnumsAsMarkdown returns empty string when no enums match schema', () => {
const enums = [{ name: 'other_enum', schema: 'auth', enums: ['a', 'b'] }]
const result = getEnumsAsMarkdown('public', enums)
expect(result).toBe('')
})
test('getPoliciesAsMarkdown returns formatted policy table grouped by table', () => {
const policies = [
{
name: 'users_select',
schema: 'public',
table: 'users',
command: 'SELECT',
roles: ['authenticated'],
action: 'PERMISSIVE',
definition: 'auth.uid() = id',
check: null,
},
{
name: 'users_insert',
schema: 'public',
table: 'users',
command: 'INSERT',
roles: ['authenticated'],
action: 'PERMISSIVE',
definition: null,
check: 'auth.uid() = id',
},
]
const result = getPoliciesAsMarkdown('public', policies)
expect(result).toContain('## RLS Policies')
expect(result).toContain('### `users`')
expect(result).toContain('`users_select`')
expect(result).toContain('`users_insert`')
expect(result).toContain('`auth.uid() = id`')
expect(result).toContain('—')
})
test('getPoliciesAsMarkdown returns empty string when no policies match schema', () => {
const policies = [
{
name: 'test',
schema: 'auth',
table: 'users',
command: 'SELECT',
roles: ['anon'],
action: 'PERMISSIVE',
definition: 'true',
check: null,
},
]
const result = getPoliciesAsMarkdown('public', policies)
expect(result).toBe('')
})
})