From 2d2cc95caa9e9e200521425faad7abc63aa715d3 Mon Sep 17 00:00:00 2001 From: Charis <26616127+charislam@users.noreply.github.com> Date: Wed, 13 May 2026 14:57:08 -0400 Subject: [PATCH] studio: add SafeSql foundation utilities (1/7) (#45897) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary First in a stack of seven PRs migrating the remaining `executeSql` call sites to `SafeSqlFragment`. This PR lands pure additions — shared types and helpers that later PRs import. - `apps/studio/lib/postgres-types.ts` — `SafePostgresColumn` / `SafePostgresTable` wrappers over `PGColumn` / `PGTable` that brand `check` as `SafeSqlFragment` and add the optional `format_schema` field - `apps/studio/lib/sql.ts` — `trimSafeSqlFragment` overload set that preserves the brand through `.trim()` - `apps/studio/lib/type-helpers.ts` — refine `DeepReadonly` so primitives pass through unchanged - `apps/studio/components/ui/SafeSqlInput.tsx` — `` wrapper that emits `SafeSqlFragment` via `rawSql` No consumers in this PR. The next PRs in the stack pick these up. ## Stack This is PR 1 of 7. The full sequence: 1. **This PR** — Foundation utilities 2. `charis/safe-sql-last/2` — pg-meta columns + ColumnTypeRef cascade 3. `charis/safe-sql-last/3` — pg-meta non-column SafeSql (functions/policies/triggers) 4. `charis/safe-sql-last/4` — Studio reports / query performance / privileges 5. `charis/safe-sql-last/6` — Stragglers + remaining tests 6. `charis/safe-sql-last/7` — Flip `executeSql` signature from `string` to `SafeSqlFragment` ## Test plan - [x] `pnpm typecheck` passes for the Studio target on this branch - [x] No runtime behavior changes — only type-level additions and a new unused component ## Summary by CodeRabbit ## Release Notes * **New Features** * Introduced SafeSqlInput component providing secure and validated SQL query input handling with built-in constraints and error prevention. * **Refactor** * Enhanced internal type definitions and utilities for Postgres metadata handling and SQL operations to improve code reliability and maintainability. [![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/45897) --- apps/studio/components/ui/SafeSqlInput.tsx | 15 +++++++++++++++ apps/studio/lib/postgres-types.ts | 12 ++++++++++++ apps/studio/lib/sql.ts | 13 +++++++++++++ apps/studio/lib/type-helpers.ts | 14 +++++++++----- 4 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 apps/studio/components/ui/SafeSqlInput.tsx create mode 100644 apps/studio/lib/postgres-types.ts create mode 100644 apps/studio/lib/sql.ts diff --git a/apps/studio/components/ui/SafeSqlInput.tsx b/apps/studio/components/ui/SafeSqlInput.tsx new file mode 100644 index 00000000000..1dbb6631051 --- /dev/null +++ b/apps/studio/components/ui/SafeSqlInput.tsx @@ -0,0 +1,15 @@ +import { rawSql, type SafeSqlFragment } from '@supabase/pg-meta' +import type { ChangeEvent, ComponentProps } from 'react' +import { Input } from 'ui-patterns/DataInputs/Input' + +type InputProps = ComponentProps + +export type SafeSqlInputProps = Omit & { + placeholder?: SafeSqlFragment + value: SafeSqlFragment + onChange?: (event: ChangeEvent, value: SafeSqlFragment) => void +} + +export const SafeSqlInput = ({ onChange, ...props }: SafeSqlInputProps) => ( + onChange?.(event, rawSql(event.target.value))} /> +) diff --git a/apps/studio/lib/postgres-types.ts b/apps/studio/lib/postgres-types.ts new file mode 100644 index 00000000000..50a5c01572a --- /dev/null +++ b/apps/studio/lib/postgres-types.ts @@ -0,0 +1,12 @@ +import type { PGColumn, PGTable, SafeSqlFragment } from '@supabase/pg-meta' + +export type SafePostgresColumn = PGColumn & { + check: SafeSqlFragment | null + // Present when sourced from pg-meta's SQL queries (e.g. `pgMeta.tables.retrieve`), absent + // from the legacy `/platform/pg-meta/{ref}/tables` REST endpoint. + format_schema?: string +} + +export type SafePostgresTable = Omit & { + columns?: SafePostgresColumn[] | undefined +} diff --git a/apps/studio/lib/sql.ts b/apps/studio/lib/sql.ts new file mode 100644 index 00000000000..45a76ac9778 --- /dev/null +++ b/apps/studio/lib/sql.ts @@ -0,0 +1,13 @@ +import type { SafeSqlFragment } from '@supabase/pg-meta' + +export function trimSafeSqlFragment(fragment: SafeSqlFragment): SafeSqlFragment +export function trimSafeSqlFragment(fragment: SafeSqlFragment | null): SafeSqlFragment | null +export function trimSafeSqlFragment( + fragment: SafeSqlFragment | undefined +): SafeSqlFragment | undefined +export function trimSafeSqlFragment( + fragment: SafeSqlFragment | null | undefined +): SafeSqlFragment | null | undefined { + if (fragment == null) return fragment + return fragment.trim() as SafeSqlFragment +} diff --git a/apps/studio/lib/type-helpers.ts b/apps/studio/lib/type-helpers.ts index 02b486eac21..a7310684875 100644 --- a/apps/studio/lib/type-helpers.ts +++ b/apps/studio/lib/type-helpers.ts @@ -2,8 +2,12 @@ export type PlainObject = Record = { [K in keyof T]: T[K] } & {} -export type DeepReadonly = T extends (infer R)[] - ? ReadonlyArray> - : T extends Object - ? { readonly [K in keyof T]: DeepReadonly } - : T +type Primitive = string | number | bigint | boolean | symbol | null | undefined + +export type DeepReadonly = T extends Primitive + ? T + : T extends (infer R)[] + ? ReadonlyArray> + : T extends object + ? { readonly [K in keyof T]: DeepReadonly } + : T