Create internal db debugger
This commit is contained in:
parent
31544d68a2
commit
ae7c2c09a4
9 changed files with 130 additions and 3 deletions
|
@ -1462,3 +1462,9 @@ $secondary-text-color: light-dark(
|
|||
max-width: 360px;
|
||||
}
|
||||
}
|
||||
|
||||
.Preferences__ReadonlySqlPlayground__Textarea {
|
||||
&__input {
|
||||
font-family: variables.$monospace;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ export type PropsType = {
|
|||
onFocus?: () => unknown;
|
||||
onEnter?: () => unknown;
|
||||
placeholder: string;
|
||||
readOnly?: boolean;
|
||||
value?: string;
|
||||
whenToShowRemainingCount?: number;
|
||||
whenToWarnRemainingCount?: number;
|
||||
|
@ -84,6 +85,7 @@ export const Input = forwardRef<
|
|||
onFocus,
|
||||
onEnter,
|
||||
placeholder,
|
||||
readOnly,
|
||||
value = '',
|
||||
whenToShowRemainingCount = Infinity,
|
||||
whenToWarnRemainingCount = Infinity,
|
||||
|
@ -226,6 +228,7 @@ export const Input = forwardRef<
|
|||
onKeyDown: handleKeyDown,
|
||||
onPaste: handlePaste,
|
||||
placeholder,
|
||||
readOnly,
|
||||
ref: refMerger<HTMLInputElement | HTMLTextAreaElement | null>(
|
||||
ref,
|
||||
innerRef
|
||||
|
|
|
@ -497,6 +497,9 @@ export default {
|
|||
action('generateDonationReceiptBlob')();
|
||||
return new Blob();
|
||||
},
|
||||
__dangerouslyRunAbitraryReadOnlySqlQuery: async () => {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
} satisfies PropsType,
|
||||
} satisfies Meta<PropsType>;
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import { isNumber, noop, partition } from 'lodash';
|
|||
import classNames from 'classnames';
|
||||
import * as LocaleMatcher from '@formatjs/intl-localematcher';
|
||||
import type { MutableRefObject, ReactNode } from 'react';
|
||||
import type { RowType } from '@signalapp/sqlcipher';
|
||||
import { Button, ButtonVariant } from './Button';
|
||||
import { ChatColorPicker } from './ChatColorPicker';
|
||||
import { Checkbox } from './Checkbox';
|
||||
|
@ -312,6 +313,9 @@ type PropsFunctionType = {
|
|||
onWhoCanSeeMeChange: SelectChangeHandlerType<PhoneNumberSharingMode>;
|
||||
onWhoCanFindMeChange: SelectChangeHandlerType<PhoneNumberDiscoverability>;
|
||||
onZoomFactorChange: SelectChangeHandlerType<ZoomFactorType>;
|
||||
__dangerouslyRunAbitraryReadOnlySqlQuery: (
|
||||
readonlySqlQuery: string
|
||||
) => Promise<ReadonlyArray<RowType<object>>>;
|
||||
|
||||
// Localization
|
||||
i18n: LocalizerType;
|
||||
|
@ -511,6 +515,7 @@ export function Preferences({
|
|||
internalAddDonationReceipt,
|
||||
saveAttachmentToDisk,
|
||||
generateDonationReceiptBlob,
|
||||
__dangerouslyRunAbitraryReadOnlySqlQuery,
|
||||
}: PropsType): JSX.Element {
|
||||
const storiesId = useId();
|
||||
const themeSelectId = useId();
|
||||
|
@ -2198,6 +2203,9 @@ export function Preferences({
|
|||
internalAddDonationReceipt={internalAddDonationReceipt}
|
||||
saveAttachmentToDisk={saveAttachmentToDisk}
|
||||
generateDonationReceiptBlob={generateDonationReceiptBlob}
|
||||
__dangerouslyRunAbitraryReadOnlySqlQuery={
|
||||
__dangerouslyRunAbitraryReadOnlySqlQuery
|
||||
}
|
||||
/>
|
||||
}
|
||||
contentsRef={settingsPaneRef}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { useState, useCallback } from 'react';
|
||||
import React, { useState, useCallback, useRef } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import type { RowType } from '@signalapp/sqlcipher';
|
||||
import type { LocalizerType } from '../types/I18N';
|
||||
import { toLogFormat } from '../types/errors';
|
||||
import { formatFileSize } from '../util/formatFileSize';
|
||||
|
@ -19,6 +20,7 @@ import type { DonationReceipt } from '../types/Donations';
|
|||
import { createLogger } from '../logging/log';
|
||||
import { isStagingServer } from '../util/isStagingServer';
|
||||
import { getHumanDonationAmount } from '../util/currency';
|
||||
import { AutoSizeTextArea } from './AutoSizeTextArea';
|
||||
|
||||
const log = createLogger('PreferencesInternal');
|
||||
|
||||
|
@ -32,6 +34,7 @@ export function PreferencesInternal({
|
|||
internalAddDonationReceipt,
|
||||
saveAttachmentToDisk,
|
||||
generateDonationReceiptBlob,
|
||||
__dangerouslyRunAbitraryReadOnlySqlQuery,
|
||||
}: {
|
||||
i18n: LocalizerType;
|
||||
exportLocalBackup: () => Promise<BackupValidationResultType>;
|
||||
|
@ -51,6 +54,9 @@ export function PreferencesInternal({
|
|||
receipt: DonationReceipt,
|
||||
i18n: LocalizerType
|
||||
) => Promise<Blob>;
|
||||
__dangerouslyRunAbitraryReadOnlySqlQuery: (
|
||||
readonlySqlQuery: string
|
||||
) => Promise<ReadonlyArray<RowType<object>>>;
|
||||
}): JSX.Element {
|
||||
const [isExportPending, setIsExportPending] = useState(false);
|
||||
const [exportResult, setExportResult] = useState<
|
||||
|
@ -68,6 +74,11 @@ export function PreferencesInternal({
|
|||
BackupValidationResultType | undefined
|
||||
>();
|
||||
|
||||
const [readOnlySqlInput, setReadOnlySqlInput] = useState('');
|
||||
const [readOnlySqlResults, setReadOnlySqlResults] = useState<ReadonlyArray<
|
||||
RowType<object>
|
||||
> | null>(null);
|
||||
|
||||
const validateBackup = useCallback(async () => {
|
||||
setIsValidationPending(true);
|
||||
setValidationResult(undefined);
|
||||
|
@ -184,6 +195,34 @@ export function PreferencesInternal({
|
|||
[i18n, saveAttachmentToDisk, generateDonationReceiptBlob]
|
||||
);
|
||||
|
||||
const handleReadonlySqlInputChange = useCallback(
|
||||
(newReadonlySqlInput: string) => {
|
||||
setReadOnlySqlInput(newReadonlySqlInput);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const prevAbortControlerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const handleReadOnlySqlInputSubmit = useCallback(async () => {
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
|
||||
prevAbortControlerRef.current?.abort();
|
||||
prevAbortControlerRef.current = controller;
|
||||
|
||||
setReadOnlySqlResults(null);
|
||||
|
||||
const result =
|
||||
await __dangerouslyRunAbitraryReadOnlySqlQuery(readOnlySqlInput);
|
||||
|
||||
if (signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setReadOnlySqlResults(result);
|
||||
}, [readOnlySqlInput, __dangerouslyRunAbitraryReadOnlySqlQuery]);
|
||||
|
||||
return (
|
||||
<div className="Preferences--internal">
|
||||
<SettingsRow
|
||||
|
@ -437,6 +476,34 @@ export function PreferencesInternal({
|
|||
)}
|
||||
</SettingsRow>
|
||||
)}
|
||||
|
||||
<SettingsRow title="Readonly SQL Playground">
|
||||
<FlowingSettingsControl>
|
||||
<AutoSizeTextArea
|
||||
i18n={i18n}
|
||||
value={readOnlySqlInput}
|
||||
onChange={handleReadonlySqlInputChange}
|
||||
placeholder="SELECT * FROM items"
|
||||
moduleClassName="Preferences__ReadonlySqlPlayground__Textarea"
|
||||
/>
|
||||
<Button
|
||||
variant={ButtonVariant.Destructive}
|
||||
onClick={handleReadOnlySqlInputSubmit}
|
||||
>
|
||||
Run Query
|
||||
</Button>
|
||||
{readOnlySqlResults != null && (
|
||||
<AutoSizeTextArea
|
||||
i18n={i18n}
|
||||
value={JSON.stringify(readOnlySqlResults, null, 2)}
|
||||
onChange={() => null}
|
||||
readOnly
|
||||
placeholder=""
|
||||
moduleClassName="Preferences__ReadonlySqlPlayground__Textarea"
|
||||
/>
|
||||
)}
|
||||
</FlowingSettingsControl>
|
||||
</SettingsRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2020 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import type { Database } from '@signalapp/sqlcipher';
|
||||
import type { Database, RowType } from '@signalapp/sqlcipher';
|
||||
import type { ReadonlyDeep } from 'type-fest';
|
||||
|
||||
import { strictAssert } from '../util/assert';
|
||||
|
@ -894,6 +894,10 @@ type ReadableInterface = {
|
|||
getMessageSampleForSchemaVersion: (
|
||||
version: number
|
||||
) => Array<MessageAttributesType>;
|
||||
|
||||
__dangerouslyRunAbitraryReadOnlySqlQuery: (
|
||||
readOnlySqlQuery: string
|
||||
) => ReadonlyArray<RowType<object>>;
|
||||
};
|
||||
|
||||
type WritableInterface = {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
/* eslint-disable camelcase */
|
||||
|
||||
// TODO(indutny): format queries
|
||||
import type { RowType } from '@signalapp/sqlcipher';
|
||||
import SQL, { setLogger as setSqliteLogger } from '@signalapp/sqlcipher';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { mkdirSync, rmSync } from 'node:fs';
|
||||
|
@ -477,6 +478,8 @@ export const DataReader: ServerReadableInterface = {
|
|||
finishPageMessages,
|
||||
getKnownDownloads,
|
||||
getKnownConversationAttachments,
|
||||
|
||||
__dangerouslyRunAbitraryReadOnlySqlQuery,
|
||||
};
|
||||
|
||||
export const DataWriter: ServerWritableInterface = {
|
||||
|
@ -8717,3 +8720,17 @@ function ensureMessageInsertTriggersAreEnabled(db: WritableDB): void {
|
|||
}
|
||||
})();
|
||||
}
|
||||
|
||||
function __dangerouslyRunAbitraryReadOnlySqlQuery(
|
||||
db: ReadableDB,
|
||||
readOnlySqlQuery: string
|
||||
): ReadonlyArray<RowType<object>> {
|
||||
let results: ReadonlyArray<RowType<object>>;
|
||||
try {
|
||||
db.pragma('query_only = on');
|
||||
results = db.prepare(readOnlySqlQuery).all();
|
||||
} finally {
|
||||
db.pragma('query_only = off');
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright 2024 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
import React, { StrictMode, useEffect } from 'react';
|
||||
import React, { StrictMode, useCallback, useEffect } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import type { AudioDevice } from '@signalapp/ringrtc';
|
||||
|
@ -686,6 +686,15 @@ export function SmartPreferences(): JSX.Element | null {
|
|||
}
|
||||
);
|
||||
|
||||
const __dangerouslyRunAbitraryReadOnlySqlQuery = useCallback(
|
||||
(readOnlySqlQuery: string) => {
|
||||
return DataReader.__dangerouslyRunAbitraryReadOnlySqlQuery(
|
||||
readOnlySqlQuery
|
||||
);
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
if (currentLocation.tab !== NavTab.Settings) {
|
||||
return null;
|
||||
}
|
||||
|
@ -883,6 +892,9 @@ export function SmartPreferences(): JSX.Element | null {
|
|||
internalAddDonationReceipt={internalAddDonationReceipt}
|
||||
saveAttachmentToDisk={window.Signal.Migrations.saveAttachmentToDisk}
|
||||
generateDonationReceiptBlob={generateDonationReceiptBlob}
|
||||
__dangerouslyRunAbitraryReadOnlySqlQuery={
|
||||
__dangerouslyRunAbitraryReadOnlySqlQuery
|
||||
}
|
||||
/>
|
||||
</StrictMode>
|
||||
);
|
||||
|
|
|
@ -1501,6 +1501,13 @@
|
|||
"updated": "2025-05-30T22:48:14.420Z",
|
||||
"reasonDetail": "For focusing the settings backup key viewer textarea"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/PreferencesInternal.tsx",
|
||||
"line": " const prevAbortControlerRef = useRef<AbortController | null>(null);",
|
||||
"reasonCategory": "usageTrusted",
|
||||
"updated": "2025-08-20T18:18:34.081Z"
|
||||
},
|
||||
{
|
||||
"rule": "React-useRef",
|
||||
"path": "ts/components/ProfileEditor.tsx",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue