Skip to content

Commit

Permalink
edit tables : awful code, untested, but nearly working
Browse files Browse the repository at this point in the history
  • Loading branch information
jdeniau committed Apr 14, 2024
1 parent 84adab0 commit 61f59be
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 37 deletions.
99 changes: 75 additions & 24 deletions src/contexts/PendingEditContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,39 +5,41 @@ import {
useContext,
useMemo,
useReducer,
useState,
} from 'react';
import { Button } from 'antd';
import { FieldPacket, RowDataPacket } from 'mysql2';
import invariant from 'tiny-invariant';
import { PendingEdit, PendingEditState } from '../sql/types';
import { useConnectionContext } from './ConnectionContext';

enum PendingEditState {
Pending = 'pending',
Applied = 'applied',
}

type PendingEdit = {
connectionSlug: string;
tableName: string;
primaryKeys: Record<string, unknown>;
values: Record<string, unknown>;
state: PendingEditState;
};

type PendingEditContextType = {
pendingEdits: Array<PendingEdit>;
addPendingEdit: (edit: Omit<PendingEdit, 'connectionSlug' | 'state'>) => void;
findPendingEdit: (
record: RowDataPacket,
field: FieldPacket
) => PendingEdit | undefined;
findPendingEdits: (
record: RowDataPacket,
tableName: string
) => Array<PendingEdit>;
markAllAsApplied: () => void;
};
const PendingEditContext = createContext<PendingEditContextType | null>(null);

type State = Array<PendingEdit>;

type Action = { type: 'add'; edit: PendingEdit };
type Action = { type: 'add'; edit: PendingEdit } | { type: 'markAllAsApplied' };

function reducer(state: State, action: Action): State {
switch (action.type) {
case 'add':
return [...state, action.edit];
case 'markAllAsApplied':
return state.map((edit) => ({
...edit,
state: PendingEditState.Applied,
}));
default:
return state;
}
Expand Down Expand Up @@ -67,9 +69,51 @@ export function PendingEditContextProvider({
[currentConnectionSlug]
);

const markAllAsApplied = useCallback(
() => dispatch({ type: 'markAllAsApplied' }),
[]
);

const findPendingEdits = useCallback(
(record: RowDataPacket, tableName: string) =>
pendingEdits.filter(
(edit) =>
edit.tableName === tableName &&
Object.entries(edit.primaryKeys).every(
([columnName, pkValue]) => record[columnName] === pkValue
)
),
[pendingEdits]
);

const findPendingEdit = useCallback(
(record: RowDataPacket, field: FieldPacket) =>
pendingEdits.findLast(
(edit) =>
edit.tableName === field.table &&
field.name in edit.values &&
Object.entries(edit.primaryKeys).every(
([columnName, pkValue]) => record[columnName] === pkValue
)
),
[pendingEdits]
);

const value = useMemo(
() => ({ pendingEdits, addPendingEdit }),
[addPendingEdit, pendingEdits]
() => ({
pendingEdits,
addPendingEdit,
markAllAsApplied,
findPendingEdit,
findPendingEdits,
}),
[
addPendingEdit,
pendingEdits,
markAllAsApplied,
findPendingEdit,
findPendingEdits,
]
);

return (
Expand All @@ -91,21 +135,28 @@ export function usePendingEditContext() {
return context;
}
export function PendingEditDebug() {
const { pendingEdits } = usePendingEditContext();
const { pendingEdits, markAllAsApplied } = usePendingEditContext();

const unappliedPendingEdits = pendingEdits.filter(
(edit) => edit.state === PendingEditState.Pending
);

return (
<Button
title="Synchronize"
danger={
pendingEdits.filter((edit) => edit.state === PendingEditState.Pending)
.length > 0
}
danger={unappliedPendingEdits.length > 0}
onClick={() => {
alert(JSON.stringify(pendingEdits, null, 2));
window.sql.handlePendingEdits(pendingEdits).then((r) => {

Check failure on line 149 in src/contexts/PendingEditContext.tsx

View workflow job for this annotation

GitHub Actions / test (18.x)

Property 'then' does not exist on type 'QueryResult<ResultSetHeader>[]'.

Check failure on line 149 in src/contexts/PendingEditContext.tsx

View workflow job for this annotation

GitHub Actions / test (18.x)

Parameter 'r' implicitly has an 'any' type.
console.log(r);

// Mark all as applied
markAllAsApplied();
});
// alert(JSON.stringify(pendingEdits, null, 2));
}}
>
🔃
{pendingEdits.length}
{unappliedPendingEdits.length}
</Button>
);
}
7 changes: 7 additions & 0 deletions src/preload/sql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ipcRenderer } from 'electron';
import { decodeError } from '../sql/errorSerializer';
import type {
KeyColumnUsageRow,
PendingEdit,
QueryResult,
QueryReturnType,
ShowDatabasesResult,
Expand All @@ -10,6 +11,7 @@ import type {
} from '../sql/types';
import { bindChannel, bindEvent } from './bindChannel';
import { SQL_CHANNEL } from './sqlChannel';
import { ResultSetHeader } from 'mysql2';

interface Sql {
executeQuery<T extends QueryReturnType>(query: string): QueryResult<T>;
Expand All @@ -22,6 +24,9 @@ interface Sql {
showDatabases(): QueryResult<ShowDatabasesResult>;
getPrimaryKeys(tableName: string): QueryResult<ShowKeyRow[]>;
showTableStatus(): QueryResult<ShowTableStatus[]>;
handlePendingEdits(
PendingEdit: Array<PendingEdit>
): Array<QueryResult<ResultSetHeader>>;
}

async function doInvokeQuery(sqlChannel: SQL_CHANNEL, ...params: unknown[]) {
Expand All @@ -48,6 +53,8 @@ export const sql: Sql = {

showTableStatus: async () => doInvokeQuery(SQL_CHANNEL.SHOW_TABLE_STATUS),

handlePendingEdits: bindChannel(SQL_CHANNEL.HANDLE_PENDING_EDITS),

Check failure on line 56 in src/preload/sql.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

Type '(...args: unknown[]) => Promise<any>' is not assignable to type '(PendingEdit: PendingEdit[]) => QueryResult<ResultSetHeader>[]'.

closeAllConnections: bindChannel(SQL_CHANNEL.CLOSE_ALL),

// events
Expand Down
1 change: 1 addition & 0 deletions src/preload/sqlChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ export enum SQL_CHANNEL {
SHOW_TABLE_STATUS = 'sql:showTableStatus',
CLOSE_ALL = 'sql:closeAll',
ON_CONNECTION_CHANGED = 'sql:onConnectionChanged',
HANDLE_PENDING_EDITS = 'sql:handlePendingEdits',
}
22 changes: 11 additions & 11 deletions src/renderer/component/TableGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { Input, InputRef, Table } from 'antd';
import type { FieldPacket, RowDataPacket } from 'mysql2/promise';
import { usePendingEditContext } from '../../contexts/PendingEditContext';
import { PendingEditState } from '../../sql/types';
import Cell from './Cell';
import ForeignKeyLink from './ForeignKeyLink';
import { useTableHeight } from './TableLayout/useTableHeight';
Expand Down Expand Up @@ -100,22 +101,21 @@ function CellWithPendingValue({
field: FieldPacket;
record: RowDataPacket;
}) {
const { pendingEdits } = usePendingEditContext();

const pendingEdit = pendingEdits.findLast(
(edit) =>
edit.tableName === field.table &&
field.name in edit.values &&
Object.entries(edit.primaryKeys).every(
([columnName, pkValue]) => record[columnName] === pkValue
)
);
const { findPendingEdit } = usePendingEditContext();

const pendingEdit = findPendingEdit(record, field);

const pendingEditValue = pendingEdit?.values[field.name];
const futureValue = pendingEditValue ?? value;

return (
<div style={pendingEditValue ? { background: 'orange' } : undefined}>
<div
style={
pendingEditValue && pendingEdit.state === PendingEditState.Pending
? { background: 'orange' }
: undefined
}
>
<Cell
type={field.type}
value={futureValue}
Expand Down
17 changes: 16 additions & 1 deletion src/renderer/component/TableLayout/TableLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { ReactElement, useCallback, useEffect, useState } from 'react';
import { Button, Flex } from 'antd';
import type { FieldPacket, RowDataPacket } from 'mysql2/promise';
import { useConnectionContext } from '../../../contexts/ConnectionContext';
import { usePendingEditContext } from '../../../contexts/PendingEditContext';
import { useTranslation } from '../../../i18n';
import ButtonLink from '../ButtonLink';
import WhereFilter from '../Query/WhereFilter';
Expand All @@ -28,6 +29,7 @@ export function TableLayout({
const [error, setError] = useState<null | Error>(null);
const [currentOffset, setCurrentOffset] = useState<number>(0);
const [where, setWhere] = useState<string>(defaultWhere ?? '');
const { findPendingEdits } = usePendingEditContext();

const fetchTableData = useCallback(
(offset: number) => {
Expand Down Expand Up @@ -59,6 +61,19 @@ export function TableLayout({
return <div>{error.message}</div>;
}

const resultWithActiveEdits = result?.map((row) => {
const pendingEdits = findPendingEdits(row, tableName);

return pendingEdits.reduce((acc, pendingEdit) => {
const { values } = pendingEdit;

return {
...acc,
...values,
};
}, row);
});

return (
<Flex vertical gap="small" style={{ height: '100%' }}>
<div>
Expand All @@ -75,7 +90,7 @@ export function TableLayout({
<TableGrid
editable
fields={fields}
result={result}
result={resultWithActiveEdits}

Check failure on line 93 in src/renderer/component/TableLayout/TableLayout.tsx

View workflow job for this annotation

GitHub Actions / test (18.x)

Type 'RowDataPacket[] | undefined' is not assignable to type 'RowDataPacket[] | null'.
primaryKeys={primaryKeys}
title={() => (
<>
Expand Down
51 changes: 50 additions & 1 deletion src/sql/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import log from 'electron-log';
import { Connection, createConnection } from 'mysql2/promise';
import { Connection, ResultSetHeader, createConnection } from 'mysql2/promise';
import invariant from 'tiny-invariant';
import { getConfiguration } from '../configuration';
import { SQL_CHANNEL } from '../preload/sqlChannel';
import { QueryResultOrError, encodeError } from './errorSerializer';
import {
ConnectionObject,
KeyColumnUsageRow,
PendingEdit,
PendingEditState,
QueryResult,
QueryReturnType,
ShowDatabasesResult,
ShowKeyRow,
Expand All @@ -27,6 +30,7 @@ class ConnectionStack {
[SQL_CHANNEL.GET_PRIMARY_KEYS]: this.getPrimaryKeys,
[SQL_CHANNEL.SHOW_DATABASES]: this.showDatabases,
[SQL_CHANNEL.SHOW_TABLE_STATUS]: this.showTableStatus,
[SQL_CHANNEL.HANDLE_PENDING_EDITS]: this.handlePendingEdits,
[SQL_CHANNEL.CLOSE_ALL]: this.closeAllConnections,
};

Expand Down Expand Up @@ -143,6 +147,51 @@ class ConnectionStack {
}
}

async handlePendingEdits(
pendingEdits: Array<PendingEdit>
): Promise<Array<QueryResult<ResultSetHeader>>> {
invariant(this.#currentConnectionSlug, 'Connection slug is required');

const connection = await this.#getConnection(this.#currentConnectionSlug);

return await Promise.all(

Check failure on line 157 in src/sql/index.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

Type '({ result: [RowDataPacket[] | OkPacket | ProcedureCallPacket | ResultSetHeader[] | RowDataPacket[][] | OkPacket[], FieldPacket[]]; error: undefined; } | { ...; })[]' is not assignable to type 'QueryResult<ResultSetHeader>[]'.
pendingEdits
.filter(
(edit) =>
edit.connectionSlug === this.#currentConnectionSlug &&
edit.state === PendingEditState.Pending
)
.map(async (edit) => {
const { tableName, primaryKeys, values } = edit;

const keys = Object.keys(primaryKeys);
const where = keys
.map((key) => `${key} = ${connection.escape(primaryKeys[key])}`)
.join(' AND ');

const set = Object.keys(values)
.map((key) => `${key} = ${connection.escape(values[key])}`)
.join(', ');

const query = `
UPDATE ${this.#databaseName}.${tableName}
SET ${set}
WHERE ${where}
`;

log.debug(
`Execute pending edit on "${this.#currentConnectionSlug}": "${query}"`
);

try {
return { result: await connection.query(query), error: undefined };
} catch (error) {
return { result: undefined, error: encodeError(error) };
}
})
);
}

async onConnectionSlugChanged(
connectionSlug: string | undefined,
databaseName: string | undefined
Expand Down
13 changes: 13 additions & 0 deletions src/sql/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,16 @@ export interface ForeignKeyRow extends KeyColumnUsageRow {
export interface ShowKeyRow extends RowDataPacket {
Column_name: string;
}

export enum PendingEditState {
Pending = 'pending',
Applied = 'applied',
}

export type PendingEdit = {
connectionSlug: string;
tableName: string;
primaryKeys: Record<string, unknown>;
values: Record<string, unknown>;
state: PendingEditState;
};

0 comments on commit 61f59be

Please sign in to comment.