diff --git a/src/components/gui/export/export-result-button.tsx b/src/components/gui/export/export-result-button.tsx index 1085ed20..df5f2359 100644 --- a/src/components/gui/export/export-result-button.tsx +++ b/src/components/gui/export/export-result-button.tsx @@ -4,7 +4,6 @@ import OptimizeTableState, { TableSelectionRange, } from "../table-optimized/OptimizeTableState"; import { useCallback, useEffect, useMemo, useState } from "react"; -import { getFormatHandlers } from "@/components/lib/export-helper"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Select, @@ -14,6 +13,7 @@ import { SelectValue, } from "@/components/ui/select"; import { Label } from "@/components/ui/label"; +import { getFormatHandlers } from "@/lib/export-helper"; export type ExportTarget = "clipboard" | "file"; type ExportFormat = "csv" | "delimited" | "json" | "sql" | "xlsx"; diff --git a/src/components/gui/query-progress-log.tsx b/src/components/gui/query-progress-log.tsx index 508d1690..1e5baf1c 100644 --- a/src/components/gui/query-progress-log.tsx +++ b/src/components/gui/query-progress-log.tsx @@ -1,8 +1,8 @@ -import { MultipleQueryProgress } from "@/components/lib/multiple-query"; import { useEffect, useState } from "react"; import CodePreview from "./code-preview"; import ResultStats from "./result-stat"; -import isEmptyResultStats from "@/components/lib/empty-stats"; +import { MultipleQueryProgress } from "@/lib/sql/multiple-query"; +import isEmptyResultStats from "@/lib/empty-state"; function formatTimeAgo(ms: number) { if (ms < 1000) { diff --git a/src/components/gui/schema-editor/index.tsx b/src/components/gui/schema-editor/index.tsx index 88e4f3cf..2cc81146 100644 --- a/src/components/gui/schema-editor/index.tsx +++ b/src/components/gui/schema-editor/index.tsx @@ -4,7 +4,6 @@ import { Dispatch, SetStateAction, useCallback, useMemo } from "react"; import { Button, buttonVariants } from "../../ui/button"; import SchemaEditorColumnList from "./schema-editor-column-list"; import { Input } from "../../ui/input"; -import { checkSchemaChange } from "@/components/lib/sql-generate.schema"; import SchemaEditorConstraintList from "./schema-editor-constraint-list"; import { ColumnsProvider } from "./column-provider"; import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover"; @@ -13,6 +12,7 @@ import { toast } from "sonner"; import { DatabaseTableSchemaChange } from "@/drivers/base-driver"; import { useDatabaseDriver } from "@/context/driver-provider"; import SchemaNameSelect from "./schema-name-select"; +import { checkSchemaChange } from "@/lib/sql/sql-generate.schema"; interface Props { onSave: () => void; diff --git a/src/components/gui/schema-editor/schema-editor-column-list.tsx b/src/components/gui/schema-editor/schema-editor-column-list.tsx index d56946ff..d30858f5 100644 --- a/src/components/gui/schema-editor/schema-editor-column-list.tsx +++ b/src/components/gui/schema-editor/schema-editor-column-list.tsx @@ -18,7 +18,6 @@ import { import { CSS } from "@dnd-kit/utilities"; import { Checkbox } from "@/components/ui/checkbox"; import ColumnDefaultValueInput from "./column-default-value-input"; -import { checkSchemaColumnChange } from "@/components/lib/sql-generate.schema"; import { DatabaseTableColumn, DatabaseTableColumnChange, @@ -43,6 +42,7 @@ import { restrictToVerticalAxis } from "@dnd-kit/modifiers"; import { useDatabaseDriver } from "@/context/driver-provider"; import ColumnTypeSelector from "./column-type-selector"; import ColumnCollation from "./column-collation"; +import { checkSchemaColumnChange } from "@/lib/sql/sql-generate.schema"; export type ColumnChangeEvent = ( newValue: Partial | null diff --git a/src/components/gui/sortable-tab.tsx b/src/components/gui/sortable-tab.tsx index d4bbb669..1e3d9327 100644 --- a/src/components/gui/sortable-tab.tsx +++ b/src/components/gui/sortable-tab.tsx @@ -4,7 +4,7 @@ import { WindowTabItemProps } from "./windows-tab"; import { cn } from "@/lib/utils"; import { forwardRef } from "react"; import { ButtonProps } from "../ui/button"; -import { CSS } from "../lib/dnd-kit"; +import { CSS } from "@/lib/dnd-kit"; interface SortableTabProps { tab: WindowTabItemProps; diff --git a/src/components/gui/table-optimized/OptimizeTableState.tsx b/src/components/gui/table-optimized/OptimizeTableState.tsx index 218acfd0..8f775ad9 100644 --- a/src/components/gui/table-optimized/OptimizeTableState.tsx +++ b/src/components/gui/table-optimized/OptimizeTableState.tsx @@ -1,4 +1,3 @@ -import { selectArrayFromIndexList } from "@/components/lib/export-helper"; import { OptimizeTableHeaderProps } from "."; import { LucideKey, LucideKeySquare, LucideSigma } from "lucide-react"; import { @@ -10,6 +9,7 @@ import { import { ReactElement } from "react"; import deepEqual from "deep-equal"; import { formatNumber } from "@/lib/convertNumber"; +import { selectArrayFromIndexList } from "@/lib/export-helper"; export interface OptimizeTableRowValue { raw: Record; diff --git a/src/components/gui/table-result/context-menu.tsx b/src/components/gui/table-result/context-menu.tsx index 0f8db49c..627d1484 100644 --- a/src/components/gui/table-result/context-menu.tsx +++ b/src/components/gui/table-result/context-menu.tsx @@ -6,7 +6,7 @@ import { exportRowsToExcel, exportRowsToJson, exportRowsToSqlInsert, -} from "@/components/lib/export-helper"; +} from "@/lib/export-helper"; import { LucidePlus, LucideTrash2 } from "lucide-react"; import TableStateActions from "../table-optimized/table-state-actions"; import { openContextMenuFromEvent } from "@/core/channel-builtin"; diff --git a/src/components/gui/tabs-result/query-result-tab.tsx b/src/components/gui/tabs-result/query-result-tab.tsx index 5cc51790..096acf62 100644 --- a/src/components/gui/tabs-result/query-result-tab.tsx +++ b/src/components/gui/tabs-result/query-result-tab.tsx @@ -1,11 +1,11 @@ import { useMemo } from "react"; -import { MultipleQueryResult } from "../../lib/multiple-query"; import ExportResultButton from "../export/export-result-button"; import ResultTable from "../query-result-table"; import ResultStats from "../result-stat"; import OptimizeTableState from "../table-optimized/OptimizeTableState"; import { useDatabaseDriver } from "@/context/driver-provider"; import AggregateResultButton from "../aggregate-result/aggregate-result-button"; +import { MultipleQueryResult } from "@/lib/sql/multiple-query"; export default function QueryResult({ result, diff --git a/src/components/gui/tabs/query-tab.tsx b/src/components/gui/tabs/query-tab.tsx index 28ce784e..be0ec566 100644 --- a/src/components/gui/tabs/query-tab.tsx +++ b/src/components/gui/tabs/query-tab.tsx @@ -20,7 +20,7 @@ import { MultipleQueryProgress, MultipleQueryResult, multipleQuery, -} from "@/components/lib/multiple-query"; +} from "@/lib/sql/multiple-query"; import WindowTabs, { useTabsContext, WindowTabItemProps } from "../windows-tab"; import QueryResult from "../tabs-result/query-result-tab"; import { useSchema } from "@/context/schema-provider"; diff --git a/src/components/gui/tabs/schema-editor-tab.tsx b/src/components/gui/tabs/schema-editor-tab.tsx index f22a6dbc..872c4fe5 100644 --- a/src/components/gui/tabs/schema-editor-tab.tsx +++ b/src/components/gui/tabs/schema-editor-tab.tsx @@ -4,7 +4,7 @@ import { useDatabaseDriver } from "@/context/driver-provider"; import SchemaSaveDialog from "../schema-editor/schema-save-dialog"; import { DatabaseTableSchemaChange } from "@/drivers/base-driver"; import SchemaEditor from "../schema-editor"; -import { createTableSchemaDraft } from "@/components/lib/sql-generate.schema"; +import { createTableSchemaDraft } from "@/lib/sql/sql-generate.schema"; import { cloneDeep } from "lodash"; interface SchemaEditorTabProps { diff --git a/src/components/gui/tabs/table-data-tab.tsx b/src/components/gui/tabs/table-data-tab.tsx index af6ecc7a..e4da2c69 100644 --- a/src/components/gui/tabs/table-data-tab.tsx +++ b/src/components/gui/tabs/table-data-tab.tsx @@ -16,7 +16,7 @@ import { TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; -import { commitChange } from "@/components/lib/sql-execute-helper"; +import { commitChange } from "@/lib/sql/sql-execute-helper"; import { AlertDialog, AlertDialogAction, diff --git a/src/components/gui/windows-tab.tsx b/src/components/gui/windows-tab.tsx index 58961f86..7d2be446 100644 --- a/src/components/gui/windows-tab.tsx +++ b/src/components/gui/windows-tab.tsx @@ -29,7 +29,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from "../ui/dropdown-menu"; -import { restrictToHorizontalAxis } from "../lib/dnd-kit"; +import { restrictToHorizontalAxis } from "@/lib/dnd-kit"; export interface WindowTabItemProps { component: JSX.Element; diff --git a/src/components/lib/api-database-response.ts b/src/components/lib/api-database-response.ts deleted file mode 100644 index 52d488cb..00000000 --- a/src/components/lib/api-database-response.ts +++ /dev/null @@ -1,25 +0,0 @@ -export interface ApiRole { - id: string; - name: string; -} - -export interface ApiUser { - id: string; - name: string; -} - -export interface ApiUserRole { - id: string; - name: string; - role: ApiRole; - createdAt: number; - assignedBy: ApiUser; -} - -export interface ApiRolesResponse { - roles: ApiRole[]; -} - -export interface ApiUserListResponse { - users: ApiUserRole[]; -} diff --git a/src/components/lib/bit-operation.ts b/src/components/lib/bit-operation.ts deleted file mode 100644 index cf81910d..00000000 --- a/src/components/lib/bit-operation.ts +++ /dev/null @@ -1,15 +0,0 @@ -const byteToHex: string[] = []; - -for (let n = 0; n <= 0xff; ++n) { - const hexOctet = n.toString(16).padStart(2, "0"); - byteToHex.push(hexOctet.toUpperCase()); -} - -// https://stackoverflow.com/questions/40031688/javascript-arraybuffer-to-hex -export function hex(arrayBuffer: ArrayBuffer) { - const buff = new Uint8Array(arrayBuffer); - let hexOctets = ""; - - for (const b of buff) hexOctets += byteToHex[b]; - return hexOctets; -} diff --git a/src/components/lib/json-safe.ts b/src/components/lib/json-safe.ts deleted file mode 100644 index 3adc2eb6..00000000 --- a/src/components/lib/json-safe.ts +++ /dev/null @@ -1,13 +0,0 @@ -export default function parseSafeJson( - str: string | null | undefined, - defaultValue: T -): T { - if (!str) return defaultValue; - - try { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return JSON.parse(str); - } catch { - return defaultValue; - } -} diff --git a/src/components/lib/key-matcher.ts b/src/components/lib/key-matcher.ts deleted file mode 100644 index 626263b4..00000000 --- a/src/components/lib/key-matcher.ts +++ /dev/null @@ -1,79 +0,0 @@ -interface KeyMatcherProps { - ctrl?: boolean; - key?: string; - shift?: boolean; -} - -export default class KeyMatcher { - protected key: KeyMatcherProps; - - constructor(props: KeyMatcherProps) { - this.key = props; - } - - static capture(e: KeyboardEvent | React.KeyboardEvent) { - const isCtrlKey = e.ctrlKey || e.metaKey; - - let key: string | undefined = e.key; - - if (key === "Shift") key = undefined; - if (key === "Control") key = undefined; - - return new KeyMatcher({ - ctrl: isCtrlKey, - shift: e.shiftKey, - key, - }); - } - - match(e: KeyboardEvent | React.KeyboardEvent) { - let isMatched = true; - const isCtrlKey = e.ctrlKey || e.metaKey; - - if (this.key.ctrl && !isCtrlKey) { - isMatched = false; - } - - if (this.key.key && e.key !== this.key.key) { - isMatched = false; - } - - if (this.key.shift && !e.shiftKey) { - isMatched = false; - } - - return isMatched; - } - - toJson(): KeyMatcherProps { - return { ...this.key }; - } - - toString() { - const isMac = navigator.userAgent.toLowerCase().indexOf("mac") > -1; - return [ - this.key.ctrl ? (isMac ? "⌘" : "Ctrl") : undefined, - this.key.shift ? "Shift" : undefined, - this.key?.key?.toUpperCase(), - ] - .filter(Boolean) - .join(" + "); - } - - toCodeMirrorKey() { - const isMac = navigator.userAgent.toLowerCase().indexOf("mac") > -1; - return [ - this.key.ctrl ? (isMac ? "Cmd" : "Ctrl") : undefined, - this.key.shift ? "Shift" : undefined, - this.key?.key, - ] - .filter(Boolean) - .join("-"); - } -} - -export const KEY_BINDING = { - run: new KeyMatcher({ ctrl: true, key: "Enter" }), - copy: new KeyMatcher({ ctrl: true, key: "c" }), - paste: new KeyMatcher({ ctrl: true, key: "v" }), -}; diff --git a/src/components/lib/validation.ts b/src/components/lib/validation.ts deleted file mode 100644 index 5de96ab5..00000000 --- a/src/components/lib/validation.ts +++ /dev/null @@ -1,79 +0,0 @@ -import type { - DatabaseTableOperation, - DatabaseTableSchema, -} from "@/drivers/base-driver"; - -export function validateOperation( - op: DatabaseTableOperation, - validateSchema: DatabaseTableSchema -): { valid: boolean; reason?: string } { - const operation = op.operation; - const primaryKey = validateSchema.pk; - const originalValue = op.operation !== "INSERT" ? op.where : {}; - const changeValue = op.operation !== "DELETE" ? op.values : {}; - const autoIncrement = validateSchema.autoIncrement; - - const hasAnyPrimaryKeyNull = primaryKey - .map((pkColumnName) => originalValue[pkColumnName]) - .some((value) => value === null || value === undefined); - - const hasAnyUpdatePrimaryKeyNull = primaryKey - .map((pkColumnName) => - changeValue[pkColumnName] === undefined - ? originalValue[pkColumnName] - : changeValue[pkColumnName] - ) - .some((value) => value === null || value === undefined); - - if (primaryKey.length === 0) { - return { - valid: false, - reason: - "This table does not have any primary key. It is not safe to perform any update/delete/insert operation.", - }; - } - - if (operation === "DELETE") { - if (hasAnyPrimaryKeyNull) - return { - valid: false, - reason: "It is not safe to remove row with NULL in primary key column", - }; - } else if (operation === "UPDATE") { - if (hasAnyPrimaryKeyNull) { - return { - valid: false, - reason: "It is not safe to update row with NULL in primary key column", - }; - } else if (hasAnyUpdatePrimaryKeyNull) { - return { - valid: false, - reason: "It is not safe to update row to NULL in primary key column", - }; - } - } else { - // If it is INSERT operation and also have auto increment - if (autoIncrement && primaryKey[0] && changeValue[primaryKey[0]] === null) { - return { - valid: false, - reason: "It is not safe to insert row with NULL in primary key column", - }; - } else if (!autoIncrement && hasAnyUpdatePrimaryKeyNull) { - return { - valid: false, - reason: "It is not safe to insert row with NULL in primary key column", - }; - } - } - - return { valid: true }; -} - -export function isLinkString(str: string) { - if (str.length > 200) return false; - try { - return Boolean(new URL(str)); - } catch { - return false; - } -} diff --git a/src/drivers/common-sql-imp.ts b/src/drivers/common-sql-imp.ts index d807676d..4cee75ed 100644 --- a/src/drivers/common-sql-imp.ts +++ b/src/drivers/common-sql-imp.ts @@ -1,4 +1,4 @@ -import { validateOperation } from "@/components/lib/validation"; +import { validateOperation } from "@/lib/validation"; import { BaseDriver, DatabaseResultSet, diff --git a/src/drivers/sqlite/sqlite-generate-schema.test.ts b/src/drivers/sqlite/sqlite-generate-schema.test.ts index ca4b1c54..125f92b0 100644 --- a/src/drivers/sqlite/sqlite-generate-schema.test.ts +++ b/src/drivers/sqlite/sqlite-generate-schema.test.ts @@ -1,7 +1,7 @@ -import { createTableSchemaDraft } from "@/components/lib/sql-generate.schema"; import { parseCreateTableScript } from "./sql-parse-table"; import { produce } from "immer"; import generateSqlSchemaChange from "./sqlite-generate-schema"; +import { createTableSchemaDraft } from "@/lib/sql/sql-generate.schema"; function c(sql: string) { return createTableSchemaDraft("main", parseCreateTableScript("main", sql)); diff --git a/src/components/lib/dnd-kit.ts b/src/lib/dnd-kit.ts similarity index 100% rename from src/components/lib/dnd-kit.ts rename to src/lib/dnd-kit.ts diff --git a/src/components/lib/empty-stats.ts b/src/lib/empty-state.ts similarity index 100% rename from src/components/lib/empty-stats.ts rename to src/lib/empty-state.ts diff --git a/src/components/lib/export-helper.ts b/src/lib/export-helper.ts similarity index 96% rename from src/components/lib/export-helper.ts rename to src/lib/export-helper.ts index c46c79ea..28db6fa3 100644 --- a/src/components/lib/export-helper.ts +++ b/src/lib/export-helper.ts @@ -1,16 +1,12 @@ +import { ExportOptions, ExportSelection, ExportTarget } from "@/components/gui/export/export-result-button"; +import OptimizeTableState from "@/components/gui/table-optimized/OptimizeTableState"; +import { getSingleTableName } from "@/components/gui/tabs/query-tab"; import { escapeDelimitedValue, escapeIdentity, escapeSqlValue, } from "@/drivers/sqlite/sql-helper"; -import OptimizeTableState from "../gui/table-optimized/OptimizeTableState"; -import { - ExportOptions, - ExportSelection, - ExportTarget, -} from "../gui/export/export-result-button"; import { toast } from "sonner"; -import { getSingleTableName } from "../gui/tabs/query-tab"; export function selectArrayFromIndexList( data: T[], diff --git a/src/components/lib/internal-pubsub.ts b/src/lib/internal-pubsub.ts similarity index 100% rename from src/components/lib/internal-pubsub.ts rename to src/lib/internal-pubsub.ts diff --git a/src/components/lib/multiple-query.ts b/src/lib/sql/multiple-query.ts similarity index 100% rename from src/components/lib/multiple-query.ts rename to src/lib/sql/multiple-query.ts diff --git a/src/components/lib/sql-execute-helper.ts b/src/lib/sql/sql-execute-helper.ts similarity index 100% rename from src/components/lib/sql-execute-helper.ts rename to src/lib/sql/sql-execute-helper.ts diff --git a/src/components/lib/sql-generate.schema.ts b/src/lib/sql/sql-generate.schema.ts similarity index 100% rename from src/components/lib/sql-generate.schema.ts rename to src/lib/sql/sql-generate.schema.ts diff --git a/src/components/lib/validation.test.ts b/src/lib/validation.test.ts similarity index 100% rename from src/components/lib/validation.test.ts rename to src/lib/validation.test.ts diff --git a/src/lib/validation.ts b/src/lib/validation.ts index eaefbbd6..f6437cf9 100644 --- a/src/lib/validation.ts +++ b/src/lib/validation.ts @@ -1,3 +1,74 @@ +import type { + DatabaseTableOperation, + DatabaseTableSchema, +} from "@/drivers/base-driver"; + +export function validateOperation( + op: DatabaseTableOperation, + validateSchema: DatabaseTableSchema +): { valid: boolean; reason?: string } { + const operation = op.operation; + const primaryKey = validateSchema.pk; + const originalValue = op.operation !== "INSERT" ? op.where : {}; + const changeValue = op.operation !== "DELETE" ? op.values : {}; + const autoIncrement = validateSchema.autoIncrement; + + const hasAnyPrimaryKeyNull = primaryKey + .map((pkColumnName) => originalValue[pkColumnName]) + .some((value) => value === null || value === undefined); + + const hasAnyUpdatePrimaryKeyNull = primaryKey + .map((pkColumnName) => + changeValue[pkColumnName] === undefined + ? originalValue[pkColumnName] + : changeValue[pkColumnName] + ) + .some((value) => value === null || value === undefined); + + if (primaryKey.length === 0) { + return { + valid: false, + reason: + "This table does not have any primary key. It is not safe to perform any update/delete/insert operation.", + }; + } + + if (operation === "DELETE") { + if (hasAnyPrimaryKeyNull) + return { + valid: false, + reason: "It is not safe to remove row with NULL in primary key column", + }; + } else if (operation === "UPDATE") { + if (hasAnyPrimaryKeyNull) { + return { + valid: false, + reason: "It is not safe to update row with NULL in primary key column", + }; + } else if (hasAnyUpdatePrimaryKeyNull) { + return { + valid: false, + reason: "It is not safe to update row to NULL in primary key column", + }; + } + } else { + // If it is INSERT operation and also have auto increment + if (autoIncrement && primaryKey[0] && changeValue[primaryKey[0]] === null) { + return { + valid: false, + reason: "It is not safe to insert row with NULL in primary key column", + }; + } else if (!autoIncrement && hasAnyUpdatePrimaryKeyNull) { + return { + valid: false, + reason: "It is not safe to insert row with NULL in primary key column", + }; + } + } + + return { valid: true }; +} + export function isLinkString(str: string) { if (str.length > 200) return false;