| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- import dagre from '@dagrejs/dagre'
- import type { PGSchema, PGTable } from '@supabase/pg-meta'
- import { Edge, Node, Position } from '@xyflow/react'
- import { uniqBy } from 'lodash'
- import '@xyflow/react/dist/style.css'
- import { LOCAL_STORAGE_KEYS } from 'common'
- import { TableNodeData } from './Schemas.constants'
- import { TABLE_NODE_ROW_HEIGHT, TABLE_NODE_WIDTH } from './SchemaTableNode'
- import { tryParseJson } from '@/lib/helpers'
- const NODE_SEP = 25
- const RANK_SEP = 50
- export async function getGraphDataFromTables(
- ref?: string,
- schema?: PGSchema,
- tables?: PGTable[]
- ): Promise<{
- nodes: Node<TableNodeData>[]
- edges: Edge[]
- }> {
- if (!tables?.length) {
- return { nodes: [], edges: [] }
- }
- const nodes = tables.map((table) => {
- const columns = (table.columns || []).map((column) => {
- return {
- id: column.id,
- isPrimary: table.primary_keys.some((pk) => pk.name === column.name),
- name: column.name,
- format: column.format,
- isNullable: column.is_nullable,
- isUnique: column.is_unique,
- isUpdateable: column.is_updatable,
- isIdentity: column.is_identity,
- description: column.comment ?? '',
- }
- })
- const data: TableNodeData = {
- ref,
- id: table.id,
- name: table.name,
- description: table.comment ?? '',
- schema: table.schema,
- isForeign: false,
- columns,
- }
- return {
- data,
- id: `${table.id}`,
- type: 'table',
- position: { x: 0, y: 0 },
- }
- })
- const edges: Edge[] = []
- const currentSchema = tables[0].schema
- const uniqueRelationships = uniqBy(
- tables.flatMap((t) => t.relationships),
- 'id'
- )
- for (const rel of uniqueRelationships) {
- // TODO: Support [external->this] relationship?
- if (rel.source_schema !== currentSchema) {
- continue
- }
- // Create additional [this->foreign] node that we can point to on the graph.
- if (rel.target_table_schema !== currentSchema) {
- const targetId = `${rel.target_table_schema}.${rel.target_table_name}.${rel.target_column_name}`
- const targetNode = nodes.find((n) => n.id === targetId)
- if (!targetNode) {
- const data: TableNodeData = {
- id: rel.id,
- ref: ref!,
- schema: rel.target_table_schema,
- name: targetId,
- description: '',
- isForeign: true,
- columns: [],
- }
- nodes.push({
- id: targetId,
- type: 'table',
- data: data,
- position: { x: 0, y: 0 },
- })
- }
- const [source, sourceHandle] = findTablesHandleIds(
- tables,
- rel.source_table_name,
- rel.source_column_name
- )
- if (source) {
- edges.push({
- id: String(rel.id),
- source,
- sourceHandle,
- target: targetId,
- targetHandle: targetId,
- deletable: false,
- data: {
- sourceName: rel.source_table_name,
- sourceSchemaName: rel.source_schema,
- sourceColumnName: rel.source_column_name,
- targetName: rel.target_table_name,
- targetSchemaName: rel.target_table_schema,
- targetColumnName: rel.target_column_name,
- },
- })
- }
- continue
- }
- const [source, sourceHandle] = findTablesHandleIds(
- tables,
- rel.source_table_name,
- rel.source_column_name
- )
- const [target, targetHandle] = findTablesHandleIds(
- tables,
- rel.target_table_name,
- rel.target_column_name
- )
- // We do not support [external->this] flow currently.
- if (source && target) {
- edges.push({
- id: String(rel.id),
- source,
- sourceHandle,
- target,
- targetHandle,
- type: 'default',
- data: {
- sourceName: rel.source_table_name,
- sourceSchemaName: rel.source_schema,
- sourceColumnName: rel.source_column_name,
- targetName: rel.target_table_name,
- targetSchemaName: rel.target_table_schema,
- targetColumnName: rel.target_column_name,
- },
- })
- }
- }
- const savedPositionsLocalStorage = localStorage.getItem(
- LOCAL_STORAGE_KEYS.SCHEMA_VISUALIZER_POSITIONS(ref ?? 'project', schema?.id ?? 0)
- )
- const savedPositions = tryParseJson(savedPositionsLocalStorage)
- return !!savedPositions
- ? getLayoutedElementsViaLocalStorage(nodes, edges, savedPositions)
- : getLayoutedElementsViaDagre(nodes, edges)
- }
- function findTablesHandleIds(
- tables: PGTable[],
- table_name: string,
- column_name: string
- ): [string?, string?] {
- for (const table of tables) {
- if (table_name !== table.name) continue
- for (const column of table.columns || []) {
- if (column_name !== column.name) continue
- return [String(table.id), column.id]
- }
- }
- return []
- }
- export const getLayoutedElementsViaDagre = (nodes: Node<TableNodeData>[], edges: Edge[]) => {
- const dagreGraph = new dagre.graphlib.Graph()
- dagreGraph.setDefaultEdgeLabel(() => ({}))
- dagreGraph.setGraph({
- rankdir: 'LR',
- align: 'UR',
- nodesep: NODE_SEP,
- ranksep: RANK_SEP,
- })
- nodes.forEach((node) => {
- dagreGraph.setNode(node.id, {
- width: TABLE_NODE_WIDTH / 2,
- height: (TABLE_NODE_ROW_HEIGHT / 2) * (node.data.columns.length + 1), // columns + header
- })
- })
- edges.forEach((edge) => {
- dagreGraph.setEdge(edge.source, edge.target)
- })
- dagre.layout(dagreGraph)
- nodes.forEach((node) => {
- const nodeWithPosition = dagreGraph.node(node.id)
- node.targetPosition = Position.Left
- node.sourcePosition = Position.Right
- // We are shifting the dagre node position (anchor=center center) to the top left
- // so it matches the React Flow node anchor point (top left).
- node.position = {
- x: nodeWithPosition.x - nodeWithPosition.width / 2,
- y: nodeWithPosition.y - nodeWithPosition.height / 2,
- }
- return node
- })
- return { nodes, edges }
- }
- const getLayoutedElementsViaLocalStorage = (
- nodes: Node<TableNodeData>[],
- edges: Edge[],
- positions: { [key: string]: { x: number; y: number } }
- ) => {
- // [Joshen] Potentially look into auto fitting new nodes?
- // https://github.com/xyflow/xyflow/issues/1113
- const nodesWithNoSavedPositons = nodes.filter((n) => !(n.id in positions))
- let newNodeCount = 0
- let basePosition = {
- x: 0,
- y: -(NODE_SEP + TABLE_NODE_ROW_HEIGHT + nodesWithNoSavedPositons.length * 10),
- }
- nodes.forEach((node) => {
- const existingPosition = positions?.[node.id]
- node.targetPosition = Position.Left
- node.sourcePosition = Position.Right
- if (existingPosition) {
- node.position = existingPosition
- } else {
- node.position = {
- x: basePosition.x + newNodeCount * 10,
- y: basePosition.y + newNodeCount * 10,
- }
- newNodeCount += 1
- }
- })
- return { nodes, edges }
- }
- export const getTableDefinitionAsMarkdown = (table: TableNodeData) => {
- let markdown = `## Table \`${escapeForMarkdown(table.name)}\`\n\n`
- if (table.description) {
- markdown += `${table.description}\n\n`
- }
- markdown += `### Columns\n\n`
- markdown += `| Name | Type | Constraints |\n`
- markdown += `|------|------|-------------|\n`
- return table.columns.reduce((current, column) => {
- current += `| \`${escapeForMarkdown(column.name)}\` | \`${escapeForMarkdown(column.format)}\` | ${column.isPrimary ? 'Primary' : ''}${column.isNullable ? ' Nullable' : ''}${column.isUnique ? ' Unique' : ''}${column.isIdentity ? ' Identity' : ''} |\n`
- return current
- }, markdown)
- }
- export const getSchemaAsMarkdown = (schema: string, tables: TableNodeData[]) => {
- return tables.reduce((current, table) => {
- if (table.schema === schema) {
- current += `${getTableDefinitionAsMarkdown(table)}\n`
- }
- return current
- }, '')
- }
- const escapeForMarkdown = (str: string) => {
- return (
- str
- // Escape backslashes first so later escapes are not ambiguous
- .replace(/\\/g, '\\\\')
- // Escape backticks and pipes for markdown tables
- .replace(/([|`])/g, '\\$1')
- // Remove new lines
- .replace(/\n/g, ' ')
- )
- }
|