mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Related to FE-4109. ## Summary - **API codegen workaround**: Platform API's `notebook` content type hasn't shipped to the OpenAPI spec yet, so `pnpm api:codegen` can't be run. Locally widened `ContentBase.type` to include `'notebook'` (marked with TODO for removal once spec publishes). - **Notebook schema & type system**: Introduced Zod schemas mirroring RFC-defined notebook shape (`schema_version: 1, cells: Cell[]`). Maintains wire/domain boundary (cell `sql` → `unchecked_sql` branded for security). Agent-writable schema for `create_notebook` tool omits cell IDs (backend-generated); future update operations will require them. All TypeScript types are `z.infer`'d from schemas (no hand-written parallel interfaces). - **IsoDateTimeString moved**: Extracted ISO datetime validator from `querySource.ts` to `lib/iso-datetime.ts` (data layer shouldn't import from components layer). Needed by notebook `time_range` fields. ## Test plan - [x] Unit tests: `notebook-schema.test.ts` (9 tests), `iso-datetime.test.ts` (3 tests), `querySource.test.ts` updated and passing (26 tests) - [x] Typecheck: no new errors - [x] Prettier: formatting clean <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added support for validating and processing notebook content, including markdown, database, log cells, time ranges, and chart configurations. - Added compatibility for notebook content types in content handling. - Added reliable ISO date-time validation for notebook data and related features. - **Tests** - Expanded coverage for valid and invalid notebook structures, cell requirements, time ranges, chart settings, and date-time values. <!-- end of auto-generated comment: release notes by coderabbit.ai --> Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
122 lines
3.5 KiB
TypeScript
122 lines
3.5 KiB
TypeScript
import { untrustedSql } from '@supabase/pg-meta'
|
|
import * as z from 'zod'
|
|
|
|
import { untrustedLogSql } from '@/data/logs/safe-analytics-sql'
|
|
import { isoDateTimeString } from '@/lib/iso-datetime'
|
|
|
|
const isoDateTimeSchema = z.string().transform((raw, ctx) => {
|
|
const parsed = isoDateTimeString(raw)
|
|
if (parsed === null) {
|
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'must be a valid ISO-8601 datetime' })
|
|
return z.NEVER
|
|
}
|
|
return parsed
|
|
})
|
|
|
|
const chartConfigSchema = z.object({
|
|
x_column: z.string(),
|
|
y_column: z.string(),
|
|
cumulative: z.boolean(),
|
|
})
|
|
|
|
const absoluteTimeRangeSchema = z.object({
|
|
_tag: z.literal('absolute_time_range'),
|
|
start: isoDateTimeSchema,
|
|
end: isoDateTimeSchema,
|
|
})
|
|
|
|
const relativeTimeRangeSchema = z.object({
|
|
_tag: z.literal('relative_time_range'),
|
|
unit: z.enum(['minute', 'hour', 'day', 'week', 'month', 'year']),
|
|
amount: z.number(),
|
|
})
|
|
|
|
const timeRangeSchema = z.discriminatedUnion('_tag', [
|
|
absoluteTimeRangeSchema,
|
|
relativeTimeRangeSchema,
|
|
])
|
|
|
|
const markdownCellSchema = z.object({
|
|
_tag: z.literal('markdown_cell'),
|
|
id: z.string(),
|
|
text: z.string(),
|
|
})
|
|
|
|
const databaseCellSchema = z.object({
|
|
_tag: z.literal('database_cell'),
|
|
id: z.string(),
|
|
sql: z.string(),
|
|
row_limit: z.number(),
|
|
chart: chartConfigSchema.optional(),
|
|
})
|
|
|
|
const logCellSchema = z.object({
|
|
_tag: z.literal('log_cell'),
|
|
id: z.string(),
|
|
sql: z.string(),
|
|
time_range: timeRangeSchema,
|
|
chart: chartConfigSchema.optional(),
|
|
})
|
|
|
|
const cellSchema = z.discriminatedUnion('_tag', [
|
|
markdownCellSchema,
|
|
databaseCellSchema,
|
|
logCellSchema,
|
|
])
|
|
|
|
// The wire shape: every notebook fetched from the API has this shape, with backend-
|
|
// generated cell `id`s and plaintext `sql`.
|
|
export const notebookSchema = z.object({
|
|
schema_version: z.literal(1),
|
|
cells: z.array(cellSchema),
|
|
})
|
|
|
|
export type NotebookWire = z.infer<typeof notebookSchema>
|
|
export type CellWire = z.infer<typeof cellSchema>
|
|
|
|
// Agents have restrictions on writing IDs to preserve guarantees about ID
|
|
// uniqueness.
|
|
export const agentCellSchema = z.discriminatedUnion('_tag', [
|
|
markdownCellSchema.omit({ id: true }).strict(),
|
|
databaseCellSchema.omit({ id: true }).strict(),
|
|
logCellSchema.omit({ id: true }).strict(),
|
|
])
|
|
|
|
export const agentNotebookSchema = z.object({
|
|
schema_version: z.literal(1),
|
|
cells: z.array(agentCellSchema),
|
|
})
|
|
|
|
export type AgentNotebook = z.infer<typeof agentNotebookSchema>
|
|
export type AgentCell = z.infer<typeof agentCellSchema>
|
|
|
|
// The domain shape: parses the same wire cell (`cellSchema`) and transforms `sql` into a
|
|
// branded `unchecked_sql`.
|
|
const cellDomainSchema = cellSchema.transform((cell) => {
|
|
switch (cell._tag) {
|
|
case 'markdown_cell':
|
|
return cell
|
|
case 'database_cell': {
|
|
const { sql, ...rest } = cell
|
|
return { ...rest, unchecked_sql: untrustedSql(sql) }
|
|
}
|
|
case 'log_cell': {
|
|
const { sql, ...rest } = cell
|
|
return { ...rest, unchecked_sql: untrustedLogSql(sql) }
|
|
}
|
|
}
|
|
})
|
|
|
|
export const notebookDomainSchema = z.object({
|
|
schema_version: z.literal(1),
|
|
cells: z.array(cellDomainSchema),
|
|
})
|
|
|
|
export type NotebookContent = z.infer<typeof notebookDomainSchema>
|
|
export type Cell = z.infer<typeof cellDomainSchema>
|
|
export type MarkdownCell = Extract<Cell, { _tag: 'markdown_cell' }>
|
|
export type DatabaseCell = Extract<Cell, { _tag: 'database_cell' }>
|
|
export type LogCell = Extract<Cell, { _tag: 'log_cell' }>
|
|
export type TimeRange = z.infer<typeof timeRangeSchema>
|
|
export type ChartConfig = z.infer<typeof chartConfigSchema>
|