From 580983730505de2e736f5fcb131bb86edbda4c43 Mon Sep 17 00:00:00 2001 From: Faqih Muntashir Date: Fri, 10 Feb 2023 21:50:57 +0700 Subject: [PATCH] fix: circular dependency in schema --- src/components/Canvas.tsx | 3 +- src/components/CreateRelationDialog.tsx | 2 +- src/components/EditorPropsPanel.tsx | 2 +- src/components/GeneralPropsPanel.tsx | 2 +- .../ReactFlow/SimpleFloatingEdge.tsx | 2 +- src/contexts/EditorStateContext.tsx | 2 +- src/flow-hooks/useCreateColumn.spec.tsx | 84 +++++++++++++++++++ src/flow-hooks/useCreateRelation.ts | 2 +- src/flow-hooks/useHandleEdgeMarker.ts | 3 +- src/flow-hooks/useHandleSaveLocalSchema.ts | 4 +- src/flow-hooks/useReverseRelation.ts | 2 +- .../useTransformSchemaToReactFlowData.ts | 3 +- src/mutations/useSaveLocalSchema.ts | 2 +- src/pages/index.ts | 2 +- src/queries/useSchemaQuery.ts | 3 +- src/schemas/base.ts | 78 ----------------- src/schemas/group.ts | 12 +++ src/schemas/position.ts | 10 +++ src/schemas/relation.ts | 46 ++++++++++ src/schemas/schema.ts | 17 ++++ src/test/setup.ts | 49 +++++++++++ src/test/utils.tsx | 26 ++++++ src/utils/reactflow.ts | 3 +- src/utils/schema.ts | 2 +- 24 files changed, 266 insertions(+), 95 deletions(-) create mode 100644 src/flow-hooks/useCreateColumn.spec.tsx create mode 100644 src/schemas/group.ts create mode 100644 src/schemas/position.ts create mode 100644 src/schemas/relation.ts create mode 100644 src/schemas/schema.ts create mode 100644 src/test/setup.ts create mode 100644 src/test/utils.tsx diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx index de9654e..db23209 100644 --- a/src/components/Canvas.tsx +++ b/src/components/Canvas.tsx @@ -14,7 +14,6 @@ import { GeneralPropsPanel } from './GeneralPropsPanel'; import { EditorPropsPanel } from './EditorPropsPanel'; import { ToolbarPanel } from './ToolbarPanel'; import { SimpleFloatingEdge } from './ReactFlow/SimpleFloatingEdge'; -import { EdgeType, SchemaType } from '@/schemas/base'; import { useAddCreateTableShortcut } from '@/flow-hooks/useAddCreateTableShortcut'; import { useHandleSaveLocalSchema } from '@/flow-hooks/useHandleSaveLocalSchema'; import { isUuid } from '@/utils/zod'; @@ -29,6 +28,8 @@ import { UtilsPanel } from './UtilsPanel'; import { AddTableIcon } from './Icon/AddTableIcon'; import { ClipboardIcon } from '@heroicons/react/20/solid'; import { TableWithoutIdType } from '@/schemas/table'; +import { SchemaType } from '@/schemas/schema'; +import { EdgeType } from '@/schemas/relation'; const nodeTypes: NodeTypes = { table: TableNode } as unknown as NodeTypes; diff --git a/src/components/CreateRelationDialog.tsx b/src/components/CreateRelationDialog.tsx index fdfed11..48bca71 100644 --- a/src/components/CreateRelationDialog.tsx +++ b/src/components/CreateRelationDialog.tsx @@ -1,5 +1,5 @@ import { EditorStateContext } from '@/contexts/EditorStateContext'; -import { CreateRelationSchema, CreateRelationType, RelationActionEnum } from '@/schemas/base'; +import { CreateRelationSchema, CreateRelationType, RelationActionEnum } from '@/schemas/relation'; import * as Accordion from '@radix-ui/react-accordion'; import { zodResolver } from '@hookform/resolvers/zod'; import { useContext, useEffect, useState } from 'react'; diff --git a/src/components/EditorPropsPanel.tsx b/src/components/EditorPropsPanel.tsx index d532fef..5b929b2 100644 --- a/src/components/EditorPropsPanel.tsx +++ b/src/components/EditorPropsPanel.tsx @@ -5,7 +5,7 @@ import { useCopyToClipboard } from 'usehooks-ts'; import { useRouter } from '@tanstack/react-router'; import { EditorPanelContainer } from './EditorPanelContainer'; import { IconButton } from './IconButton'; -import { SchemaType } from '@/schemas/base'; +import { SchemaType } from '@/schemas/schema'; import { schemaToBase64Url } from '@/utils/schema'; import { useEffect } from 'react'; import { InformationDialog } from './InformationDialog'; diff --git a/src/components/GeneralPropsPanel.tsx b/src/components/GeneralPropsPanel.tsx index 5baff8d..f97789e 100644 --- a/src/components/GeneralPropsPanel.tsx +++ b/src/components/GeneralPropsPanel.tsx @@ -6,7 +6,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { IconButton } from './IconButton'; import { useSaveLocalSchema } from '@/mutations/useSaveLocalSchema'; import { EditorPanelContainer } from './EditorPanelContainer'; -import { SchemaSchema, SchemaType } from '@/schemas/base'; +import { SchemaSchema, SchemaType } from '@/schemas/schema'; import { Textbox } from './Textbox'; import FocusLock from 'react-focus-lock'; diff --git a/src/components/ReactFlow/SimpleFloatingEdge.tsx b/src/components/ReactFlow/SimpleFloatingEdge.tsx index 453c6c4..7ed525a 100644 --- a/src/components/ReactFlow/SimpleFloatingEdge.tsx +++ b/src/components/ReactFlow/SimpleFloatingEdge.tsx @@ -1,5 +1,5 @@ import { useReverseRelation } from '@/flow-hooks/useReverseRelation'; -import { RelationEdgeType } from '@/schemas/base'; +import { RelationEdgeType } from '@/schemas/relation'; import { TableWithoutIdType } from '@/schemas/table'; import { getColumnIdFromHandleId } from '@/utils/reactflow'; import { diff --git a/src/contexts/EditorStateContext.tsx b/src/contexts/EditorStateContext.tsx index de3629f..73891b2 100644 --- a/src/contexts/EditorStateContext.tsx +++ b/src/contexts/EditorStateContext.tsx @@ -2,7 +2,7 @@ import { createContext, ReactNode, useMemo, useState } from 'react'; import { useInterpret } from '@xstate/react'; import { copyPasteMachine } from '@/machines/copy-paste-machine'; import { InterpreterFrom } from 'xstate'; -import { SchemaType } from '@/schemas/base'; +import { SchemaType } from '@/schemas/schema'; import { Edge, useReactFlow } from 'reactflow'; import useUndoable from 'use-undoable'; import { useTransformSchemaToReactFlowData } from '@/flow-hooks/useTransformSchemaToReactFlowData'; diff --git a/src/flow-hooks/useCreateColumn.spec.tsx b/src/flow-hooks/useCreateColumn.spec.tsx new file mode 100644 index 0000000..d1ea0c5 --- /dev/null +++ b/src/flow-hooks/useCreateColumn.spec.tsx @@ -0,0 +1,84 @@ +import { ReactFlowProvider, useReactFlow } from 'reactflow'; +import { renderHook, act } from '@/test/utils'; +import { useCreateColumn } from './useCreateColumn'; +import { EditorStateProvider } from '@/contexts/EditorStateContext'; +import { generateMock } from '@anatine/zod-mock'; +import crypto from 'crypto'; +import { expect, it } from 'vitest'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { TableSchema, TableWithoutIdType } from '@/schemas/table'; +import { CreateVarcharColumnSchema } from '@/schemas/varchar'; +import { EdgeType } from '@/schemas/relation'; +import { SchemaType, SchemaSchema } from '@/schemas/schema'; + +const tableId = crypto.randomUUID(); + +const schemaMock: SchemaType = { + ...generateMock( + SchemaSchema.omit({ positions: true, relations: true, tables: true, groups: true }), + ), + tables: [ + { + ...generateMock(TableSchema.omit({ id: true, columns: true, indexes: true })), + id: tableId, + columns: [], + indexes: [], + }, + ], + groups: [], + relations: [], + positions: [ + { + itemId: tableId, + x: 0, + y: 0, + }, + ], +}; + +const queryClient = new QueryClient(); + +queryClient.setQueryData([`schema-${schemaMock.id}`], schemaMock); + +it('should create a column', () => { + const { result } = renderHook( + () => { + return { + createColumn: useCreateColumn(), + reactFlowInstance: useReactFlow(), + }; + }, + { + wrapper({ children }) { + return ( + + + {children} + + + ); + }, + }, + ); + + const columnData = generateMock(CreateVarcharColumnSchema); + + act(() => { + const { createColumn } = result.current; + + createColumn(columnData); + }); + + expect( + result.current.reactFlowInstance.getNodes().find((node) => node.id === tableId)?.data.columns ?? + [], + ).toHaveLength(1); + expect( + result.current.reactFlowInstance.getNodes().find((node) => node.id === tableId)?.data.columns[0] + .name, + ).toEqual(columnData.name); + expect( + result.current.reactFlowInstance.getNodes().find((node) => node.id === tableId)?.data.columns[0] + .type, + ).toEqual(columnData.type); +}); diff --git a/src/flow-hooks/useCreateRelation.ts b/src/flow-hooks/useCreateRelation.ts index 0c61cdf..e6da94b 100644 --- a/src/flow-hooks/useCreateRelation.ts +++ b/src/flow-hooks/useCreateRelation.ts @@ -1,9 +1,9 @@ import { useReactFlow } from 'reactflow'; -import { CreateRelationType, EdgeType, RelationEdgeType } from '@/schemas/base'; import { useContext } from 'react'; import { EditorStateContext } from '@/contexts/EditorStateContext'; import { useHandleEdgeMarker } from './useHandleEdgeMarker'; import { TableWithoutIdType } from '@/schemas/table'; +import { EdgeType, CreateRelationType, RelationEdgeType } from '@/schemas/relation'; export function useCreateRelation() { const { undoableService } = useContext(EditorStateContext); diff --git a/src/flow-hooks/useHandleEdgeMarker.ts b/src/flow-hooks/useHandleEdgeMarker.ts index d760689..0147c0d 100644 --- a/src/flow-hooks/useHandleEdgeMarker.ts +++ b/src/flow-hooks/useHandleEdgeMarker.ts @@ -1,4 +1,5 @@ -import { EdgeType, IndexType } from '@/schemas/base'; +import { IndexType } from '@/schemas/base'; +import { EdgeType } from '@/schemas/relation'; import { TableWithoutIdType } from '@/schemas/table'; import { getColumnIdFromHandleId } from '@/utils/reactflow'; import { Edge, useReactFlow } from 'reactflow'; diff --git a/src/flow-hooks/useHandleSaveLocalSchema.ts b/src/flow-hooks/useHandleSaveLocalSchema.ts index 133a270..f1a5850 100644 --- a/src/flow-hooks/useHandleSaveLocalSchema.ts +++ b/src/flow-hooks/useHandleSaveLocalSchema.ts @@ -1,5 +1,7 @@ import { useSaveLocalSchema } from '@/mutations/useSaveLocalSchema'; -import { PositionType, RelationType, SchemaType } from '@/schemas/base'; +import { PositionType } from '@/schemas/position'; +import { RelationType } from '@/schemas/relation'; +import { SchemaType } from '@/schemas/schema'; import { TableType } from '@/schemas/table'; import { nodeToTable, nodeToPosition, edgeToRelation } from '@/utils/reactflow'; import { useReactFlow } from 'reactflow'; diff --git a/src/flow-hooks/useReverseRelation.ts b/src/flow-hooks/useReverseRelation.ts index 71cc3e3..aa8a4ba 100644 --- a/src/flow-hooks/useReverseRelation.ts +++ b/src/flow-hooks/useReverseRelation.ts @@ -1,8 +1,8 @@ import { useReactFlow } from 'reactflow'; -import { EdgeType, RelationType } from '@/schemas/base'; import { useContext } from 'react'; import { EditorStateContext } from '@/contexts/EditorStateContext'; import { TableWithoutIdType } from '@/schemas/table'; +import { EdgeType, RelationType } from '@/schemas/relation'; export function useReverseRelation() { const { undoableService } = useContext(EditorStateContext); diff --git a/src/flow-hooks/useTransformSchemaToReactFlowData.ts b/src/flow-hooks/useTransformSchemaToReactFlowData.ts index 7356219..33b5dac 100644 --- a/src/flow-hooks/useTransformSchemaToReactFlowData.ts +++ b/src/flow-hooks/useTransformSchemaToReactFlowData.ts @@ -1,7 +1,8 @@ import { z } from 'zod'; -import { RelationEdgeType, SchemaSchema } from '@/schemas/base'; import { useHandleEdgeMarker } from './useHandleEdgeMarker'; import { TableNodeType } from '@/schemas/table'; +import { RelationEdgeType } from '@/schemas/relation'; +import { SchemaSchema } from '@/schemas/schema'; export function useTransformSchemaToReactFlowData() { const handleEdgeMarker = useHandleEdgeMarker(); diff --git a/src/mutations/useSaveLocalSchema.ts b/src/mutations/useSaveLocalSchema.ts index f2b5cd4..f4a1686 100644 --- a/src/mutations/useSaveLocalSchema.ts +++ b/src/mutations/useSaveLocalSchema.ts @@ -1,5 +1,5 @@ import { localSchemaQuery } from '@/queries/useSchemaQuery'; -import { SchemaType } from '@/schemas/base'; +import { SchemaType } from '@/schemas/schema'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import localforage from 'localforage'; diff --git a/src/pages/index.ts b/src/pages/index.ts index 90afed3..d5f32d3 100644 --- a/src/pages/index.ts +++ b/src/pages/index.ts @@ -1,4 +1,4 @@ -import { SchemaType } from '@/schemas/base'; +import { SchemaType } from '@/schemas/schema'; import { base64UrlToSchema } from '@/utils/schema'; import { QueryClient } from '@tanstack/react-query'; import { createRouteConfig, createReactRouter } from '@tanstack/react-router'; diff --git a/src/queries/useSchemaQuery.ts b/src/queries/useSchemaQuery.ts index d92d401..2497bfb 100644 --- a/src/queries/useSchemaQuery.ts +++ b/src/queries/useSchemaQuery.ts @@ -1,5 +1,4 @@ -import { SchemaType } from '@/schemas/base'; -import { emptySchemaFactory } from '@/utils/schema'; +import { SchemaType } from '@/schemas/schema'; import { useQuery } from '@tanstack/react-query'; import axios from 'axios'; import localforage from 'localforage'; diff --git a/src/schemas/base.ts b/src/schemas/base.ts index 79dd12c..662cbba 100644 --- a/src/schemas/base.ts +++ b/src/schemas/base.ts @@ -1,7 +1,4 @@ import { z } from 'zod'; -import { Edge } from 'reactflow'; -import { ColumnType } from './column'; -import { TableSchema } from './table'; export const IndexSchema = z.object({ id: z.string().uuid(), @@ -11,40 +8,6 @@ export const IndexSchema = z.object({ export type IndexType = z.infer; -export const RelationActionEnum = z.enum(['CASCADE', 'RESTRICT', 'SET_NULL', 'NO_ACTION']); - -export const RelationSchema = z.object({ - id: z.string().uuid(), - // catch for backward compat - name: z.string().catch('fk'), - source: z.object({ - columnId: z.string().uuid(), - tableId: z.string().uuid(), - }), - target: z.object({ - columnId: z.string().uuid(), - tableId: z.string().uuid(), - }), - // catch for backward compat - actions: z - .object({ - onDelete: RelationActionEnum, - onUpdate: RelationActionEnum, - }) - .catch({ - onDelete: 'RESTRICT', - onUpdate: 'RESTRICT', - }), -}); - -export type RelationType = z.infer; - -export const CreateRelationSchema = RelationSchema.omit({ - id: true, -}); - -export type CreateRelationType = z.infer; - export const VisibilityAttribute = z.union([z.literal('INVISIBLE'), z.literal('VISIBLE')]); export const NullabilityAttribute = z.union([z.literal('NULLABLE'), z.literal('NOT_NULL')]); export const SignabilityAttribute = z.union([z.literal('UNSIGNED'), z.literal('SIGNED')]); @@ -68,44 +31,3 @@ export const BaseUpdateColumnSchema = BaseColumnSchema.extend({ isUniqueIndex: z.boolean(), tableId: z.string().uuid(), }); - -export type RelationEdgeType = Edge; - -export const GroupSchema = z.object({ - id: z.string().uuid(), - name: z.string(), - width: z.number().int(), - height: z.number().int(), - color: z.object({ - background: z.string(), - border: z.string(), - }), -}); - -export const PositionSchema = z.object({ - itemId: z.string().uuid(), - groupId: z.string().uuid().optional(), - x: z.number(), - y: z.number(), -}); - -export type PositionType = z.infer; - -export const SchemaSchema = z.object({ - id: z.string().uuid(), - name: z.string().trim().max(64), - vendor: z.enum(['MYSQL:8.0']), - tables: TableSchema.array(), - groups: GroupSchema.array(), - positions: PositionSchema.array(), - relations: RelationSchema.array(), -}); - -export type SchemaType = z.infer; - -export type EdgeType = { - name: RelationType['name']; - sourceColumnId: ColumnType['id']; - targetColumnId: ColumnType['id']; - actions: RelationType['actions']; -}; diff --git a/src/schemas/group.ts b/src/schemas/group.ts new file mode 100644 index 0000000..435a2a2 --- /dev/null +++ b/src/schemas/group.ts @@ -0,0 +1,12 @@ +import { z } from 'zod'; + +export const GroupSchema = z.object({ + id: z.string().uuid(), + name: z.string(), + width: z.number().int(), + height: z.number().int(), + color: z.object({ + background: z.string(), + border: z.string(), + }), +}); diff --git a/src/schemas/position.ts b/src/schemas/position.ts new file mode 100644 index 0000000..e68c41e --- /dev/null +++ b/src/schemas/position.ts @@ -0,0 +1,10 @@ +import { z } from 'zod'; + +export const PositionSchema = z.object({ + itemId: z.string().uuid(), + groupId: z.string().uuid().optional(), + x: z.number(), + y: z.number(), +}); + +export type PositionType = z.infer; diff --git a/src/schemas/relation.ts b/src/schemas/relation.ts new file mode 100644 index 0000000..51cfae9 --- /dev/null +++ b/src/schemas/relation.ts @@ -0,0 +1,46 @@ +import { Edge } from 'reactflow'; +import { z } from 'zod'; +import { ColumnType } from './column'; + +export const RelationActionEnum = z.enum(['CASCADE', 'RESTRICT', 'SET_NULL', 'NO_ACTION']); + +export const RelationSchema = z.object({ + id: z.string().uuid(), + // catch for backward compat + name: z.string().catch('fk'), + source: z.object({ + columnId: z.string().uuid(), + tableId: z.string().uuid(), + }), + target: z.object({ + columnId: z.string().uuid(), + tableId: z.string().uuid(), + }), + // catch for backward compat + actions: z + .object({ + onDelete: RelationActionEnum, + onUpdate: RelationActionEnum, + }) + .catch({ + onDelete: 'RESTRICT', + onUpdate: 'RESTRICT', + }), +}); + +export type RelationType = z.infer; + +export const CreateRelationSchema = RelationSchema.omit({ + id: true, +}); + +export type CreateRelationType = z.infer; + +export type EdgeType = { + name: RelationType['name']; + sourceColumnId: ColumnType['id']; + targetColumnId: ColumnType['id']; + actions: RelationType['actions']; +}; + +export type RelationEdgeType = Edge; diff --git a/src/schemas/schema.ts b/src/schemas/schema.ts new file mode 100644 index 0000000..b2db24a --- /dev/null +++ b/src/schemas/schema.ts @@ -0,0 +1,17 @@ +import { z } from 'zod'; +import { GroupSchema } from './group'; +import { PositionSchema } from './position'; +import { RelationSchema } from './relation'; +import { TableSchema } from './table'; + +export const SchemaSchema = z.object({ + id: z.string().uuid(), + name: z.string().trim().max(64), + vendor: z.enum(['MYSQL:8.0']), + tables: TableSchema.array(), + groups: GroupSchema.array(), + positions: PositionSchema.array(), + relations: RelationSchema.array(), +}); + +export type SchemaType = z.infer; diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..f4096d0 --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1,49 @@ +class ResizeObserver { + callback: globalThis.ResizeObserverCallback; + + constructor(callback: globalThis.ResizeObserverCallback) { + this.callback = callback; + } + + observe(target: Element) { + this.callback([{ target } as globalThis.ResizeObserverEntry], this); + } + + unobserve() {} + + disconnect() {} +} + +global.ResizeObserver = ResizeObserver; + +class DOMMatrixReadOnly { + m22: number; + constructor(transform: string) { + const scale = transform?.match(/scale\(([1-9.])\)/)?.[1]; + this.m22 = scale !== undefined ? +scale : 1; + } +} +// @ts-ignore +global.DOMMatrixReadOnly = DOMMatrixReadOnly; + +// Object.defineProperties(global.HTMLElement.prototype, { +// offsetHeight: { +// get() { +// return parseFloat(this.style.height) || 1; +// }, +// }, +// offsetWidth: { +// get() { +// return parseFloat(this.style.width) || 1; +// }, +// }, +// }); + +// (global.SVGElement as any).prototype.getBBox = () => ({ +// x: 0, +// y: 0, +// width: 0, +// height: 0, +// }); + +export {}; diff --git a/src/test/utils.tsx b/src/test/utils.tsx new file mode 100644 index 0000000..1bfc6dd --- /dev/null +++ b/src/test/utils.tsx @@ -0,0 +1,26 @@ +import { cleanup, render, RenderOptions } from '@testing-library/react'; +import { afterEach } from 'vitest'; +import { ReactElement, ReactNode } from 'react'; +import { ReactFlowProvider } from 'reactflow'; + +afterEach(() => { + cleanup(); +}); + +const Providers = ({ children }: { children: ReactNode }) => { + return {children}; +}; + +const customRender = (ui: ReactElement, options: RenderOptions = {}) => { + return render(ui, { + wrapper({ children }) { + return {children}; + }, + ...options, + }); +}; + +export * from '@testing-library/react'; +export { default as userEvent } from '@testing-library/user-event'; + +export { customRender as render }; diff --git a/src/utils/reactflow.ts b/src/utils/reactflow.ts index 516e9dd..f29e200 100644 --- a/src/utils/reactflow.ts +++ b/src/utils/reactflow.ts @@ -1,4 +1,5 @@ -import { PositionType, RelationEdgeType, RelationType } from '@/schemas/base'; +import { PositionType } from '@/schemas/position'; +import { RelationEdgeType, RelationType } from '@/schemas/relation'; import { TableNodeType, TableType, TableWithoutIdType } from '@/schemas/table'; import { VarcharColumnType } from '@/schemas/varchar'; diff --git a/src/utils/schema.ts b/src/utils/schema.ts index 553e896..ff284b0 100644 --- a/src/utils/schema.ts +++ b/src/utils/schema.ts @@ -1,5 +1,5 @@ import { encode, decode } from 'universal-base64url'; -import { SchemaSchema, SchemaType } from '@/schemas/base'; +import { SchemaSchema, SchemaType } from '@/schemas/schema'; export function emptySchemaFactory(): SchemaType { return {