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;
|
max-width: 360px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Preferences__ReadonlySqlPlayground__Textarea {
|
||||||
|
&__input {
|
||||||
|
font-family: variables.$monospace;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -37,6 +37,7 @@ export type PropsType = {
|
||||||
onFocus?: () => unknown;
|
onFocus?: () => unknown;
|
||||||
onEnter?: () => unknown;
|
onEnter?: () => unknown;
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
|
readOnly?: boolean;
|
||||||
value?: string;
|
value?: string;
|
||||||
whenToShowRemainingCount?: number;
|
whenToShowRemainingCount?: number;
|
||||||
whenToWarnRemainingCount?: number;
|
whenToWarnRemainingCount?: number;
|
||||||
|
@ -84,6 +85,7 @@ export const Input = forwardRef<
|
||||||
onFocus,
|
onFocus,
|
||||||
onEnter,
|
onEnter,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
readOnly,
|
||||||
value = '',
|
value = '',
|
||||||
whenToShowRemainingCount = Infinity,
|
whenToShowRemainingCount = Infinity,
|
||||||
whenToWarnRemainingCount = Infinity,
|
whenToWarnRemainingCount = Infinity,
|
||||||
|
@ -226,6 +228,7 @@ export const Input = forwardRef<
|
||||||
onKeyDown: handleKeyDown,
|
onKeyDown: handleKeyDown,
|
||||||
onPaste: handlePaste,
|
onPaste: handlePaste,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
readOnly,
|
||||||
ref: refMerger<HTMLInputElement | HTMLTextAreaElement | null>(
|
ref: refMerger<HTMLInputElement | HTMLTextAreaElement | null>(
|
||||||
ref,
|
ref,
|
||||||
innerRef
|
innerRef
|
||||||
|
|
|
@ -497,6 +497,9 @@ export default {
|
||||||
action('generateDonationReceiptBlob')();
|
action('generateDonationReceiptBlob')();
|
||||||
return new Blob();
|
return new Blob();
|
||||||
},
|
},
|
||||||
|
__dangerouslyRunAbitraryReadOnlySqlQuery: async () => {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
},
|
||||||
} satisfies PropsType,
|
} satisfies PropsType,
|
||||||
} satisfies Meta<PropsType>;
|
} satisfies Meta<PropsType>;
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@ import { isNumber, noop, partition } from 'lodash';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import * as LocaleMatcher from '@formatjs/intl-localematcher';
|
import * as LocaleMatcher from '@formatjs/intl-localematcher';
|
||||||
import type { MutableRefObject, ReactNode } from 'react';
|
import type { MutableRefObject, ReactNode } from 'react';
|
||||||
|
import type { RowType } from '@signalapp/sqlcipher';
|
||||||
import { Button, ButtonVariant } from './Button';
|
import { Button, ButtonVariant } from './Button';
|
||||||
import { ChatColorPicker } from './ChatColorPicker';
|
import { ChatColorPicker } from './ChatColorPicker';
|
||||||
import { Checkbox } from './Checkbox';
|
import { Checkbox } from './Checkbox';
|
||||||
|
@ -312,6 +313,9 @@ type PropsFunctionType = {
|
||||||
onWhoCanSeeMeChange: SelectChangeHandlerType<PhoneNumberSharingMode>;
|
onWhoCanSeeMeChange: SelectChangeHandlerType<PhoneNumberSharingMode>;
|
||||||
onWhoCanFindMeChange: SelectChangeHandlerType<PhoneNumberDiscoverability>;
|
onWhoCanFindMeChange: SelectChangeHandlerType<PhoneNumberDiscoverability>;
|
||||||
onZoomFactorChange: SelectChangeHandlerType<ZoomFactorType>;
|
onZoomFactorChange: SelectChangeHandlerType<ZoomFactorType>;
|
||||||
|
__dangerouslyRunAbitraryReadOnlySqlQuery: (
|
||||||
|
readonlySqlQuery: string
|
||||||
|
) => Promise<ReadonlyArray<RowType<object>>>;
|
||||||
|
|
||||||
// Localization
|
// Localization
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
|
@ -511,6 +515,7 @@ export function Preferences({
|
||||||
internalAddDonationReceipt,
|
internalAddDonationReceipt,
|
||||||
saveAttachmentToDisk,
|
saveAttachmentToDisk,
|
||||||
generateDonationReceiptBlob,
|
generateDonationReceiptBlob,
|
||||||
|
__dangerouslyRunAbitraryReadOnlySqlQuery,
|
||||||
}: PropsType): JSX.Element {
|
}: PropsType): JSX.Element {
|
||||||
const storiesId = useId();
|
const storiesId = useId();
|
||||||
const themeSelectId = useId();
|
const themeSelectId = useId();
|
||||||
|
@ -2198,6 +2203,9 @@ export function Preferences({
|
||||||
internalAddDonationReceipt={internalAddDonationReceipt}
|
internalAddDonationReceipt={internalAddDonationReceipt}
|
||||||
saveAttachmentToDisk={saveAttachmentToDisk}
|
saveAttachmentToDisk={saveAttachmentToDisk}
|
||||||
generateDonationReceiptBlob={generateDonationReceiptBlob}
|
generateDonationReceiptBlob={generateDonationReceiptBlob}
|
||||||
|
__dangerouslyRunAbitraryReadOnlySqlQuery={
|
||||||
|
__dangerouslyRunAbitraryReadOnlySqlQuery
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
contentsRef={settingsPaneRef}
|
contentsRef={settingsPaneRef}
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
// Copyright 2025 Signal Messenger, LLC
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 classNames from 'classnames';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
import type { RowType } from '@signalapp/sqlcipher';
|
||||||
import type { LocalizerType } from '../types/I18N';
|
import type { LocalizerType } from '../types/I18N';
|
||||||
import { toLogFormat } from '../types/errors';
|
import { toLogFormat } from '../types/errors';
|
||||||
import { formatFileSize } from '../util/formatFileSize';
|
import { formatFileSize } from '../util/formatFileSize';
|
||||||
|
@ -19,6 +20,7 @@ import type { DonationReceipt } from '../types/Donations';
|
||||||
import { createLogger } from '../logging/log';
|
import { createLogger } from '../logging/log';
|
||||||
import { isStagingServer } from '../util/isStagingServer';
|
import { isStagingServer } from '../util/isStagingServer';
|
||||||
import { getHumanDonationAmount } from '../util/currency';
|
import { getHumanDonationAmount } from '../util/currency';
|
||||||
|
import { AutoSizeTextArea } from './AutoSizeTextArea';
|
||||||
|
|
||||||
const log = createLogger('PreferencesInternal');
|
const log = createLogger('PreferencesInternal');
|
||||||
|
|
||||||
|
@ -32,6 +34,7 @@ export function PreferencesInternal({
|
||||||
internalAddDonationReceipt,
|
internalAddDonationReceipt,
|
||||||
saveAttachmentToDisk,
|
saveAttachmentToDisk,
|
||||||
generateDonationReceiptBlob,
|
generateDonationReceiptBlob,
|
||||||
|
__dangerouslyRunAbitraryReadOnlySqlQuery,
|
||||||
}: {
|
}: {
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
exportLocalBackup: () => Promise<BackupValidationResultType>;
|
exportLocalBackup: () => Promise<BackupValidationResultType>;
|
||||||
|
@ -51,6 +54,9 @@ export function PreferencesInternal({
|
||||||
receipt: DonationReceipt,
|
receipt: DonationReceipt,
|
||||||
i18n: LocalizerType
|
i18n: LocalizerType
|
||||||
) => Promise<Blob>;
|
) => Promise<Blob>;
|
||||||
|
__dangerouslyRunAbitraryReadOnlySqlQuery: (
|
||||||
|
readonlySqlQuery: string
|
||||||
|
) => Promise<ReadonlyArray<RowType<object>>>;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const [isExportPending, setIsExportPending] = useState(false);
|
const [isExportPending, setIsExportPending] = useState(false);
|
||||||
const [exportResult, setExportResult] = useState<
|
const [exportResult, setExportResult] = useState<
|
||||||
|
@ -68,6 +74,11 @@ export function PreferencesInternal({
|
||||||
BackupValidationResultType | undefined
|
BackupValidationResultType | undefined
|
||||||
>();
|
>();
|
||||||
|
|
||||||
|
const [readOnlySqlInput, setReadOnlySqlInput] = useState('');
|
||||||
|
const [readOnlySqlResults, setReadOnlySqlResults] = useState<ReadonlyArray<
|
||||||
|
RowType<object>
|
||||||
|
> | null>(null);
|
||||||
|
|
||||||
const validateBackup = useCallback(async () => {
|
const validateBackup = useCallback(async () => {
|
||||||
setIsValidationPending(true);
|
setIsValidationPending(true);
|
||||||
setValidationResult(undefined);
|
setValidationResult(undefined);
|
||||||
|
@ -184,6 +195,34 @@ export function PreferencesInternal({
|
||||||
[i18n, saveAttachmentToDisk, generateDonationReceiptBlob]
|
[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 (
|
return (
|
||||||
<div className="Preferences--internal">
|
<div className="Preferences--internal">
|
||||||
<SettingsRow
|
<SettingsRow
|
||||||
|
@ -437,6 +476,34 @@ export function PreferencesInternal({
|
||||||
)}
|
)}
|
||||||
</SettingsRow>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright 2020 Signal Messenger, LLC
|
// Copyright 2020 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 type { ReadonlyDeep } from 'type-fest';
|
||||||
|
|
||||||
import { strictAssert } from '../util/assert';
|
import { strictAssert } from '../util/assert';
|
||||||
|
@ -894,6 +894,10 @@ type ReadableInterface = {
|
||||||
getMessageSampleForSchemaVersion: (
|
getMessageSampleForSchemaVersion: (
|
||||||
version: number
|
version: number
|
||||||
) => Array<MessageAttributesType>;
|
) => Array<MessageAttributesType>;
|
||||||
|
|
||||||
|
__dangerouslyRunAbitraryReadOnlySqlQuery: (
|
||||||
|
readOnlySqlQuery: string
|
||||||
|
) => ReadonlyArray<RowType<object>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type WritableInterface = {
|
type WritableInterface = {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
/* eslint-disable camelcase */
|
/* eslint-disable camelcase */
|
||||||
|
|
||||||
// TODO(indutny): format queries
|
// TODO(indutny): format queries
|
||||||
|
import type { RowType } from '@signalapp/sqlcipher';
|
||||||
import SQL, { setLogger as setSqliteLogger } from '@signalapp/sqlcipher';
|
import SQL, { setLogger as setSqliteLogger } from '@signalapp/sqlcipher';
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { mkdirSync, rmSync } from 'node:fs';
|
import { mkdirSync, rmSync } from 'node:fs';
|
||||||
|
@ -477,6 +478,8 @@ export const DataReader: ServerReadableInterface = {
|
||||||
finishPageMessages,
|
finishPageMessages,
|
||||||
getKnownDownloads,
|
getKnownDownloads,
|
||||||
getKnownConversationAttachments,
|
getKnownConversationAttachments,
|
||||||
|
|
||||||
|
__dangerouslyRunAbitraryReadOnlySqlQuery,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DataWriter: ServerWritableInterface = {
|
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
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// 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 { useSelector } from 'react-redux';
|
||||||
|
|
||||||
import type { AudioDevice } from '@signalapp/ringrtc';
|
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) {
|
if (currentLocation.tab !== NavTab.Settings) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -883,6 +892,9 @@ export function SmartPreferences(): JSX.Element | null {
|
||||||
internalAddDonationReceipt={internalAddDonationReceipt}
|
internalAddDonationReceipt={internalAddDonationReceipt}
|
||||||
saveAttachmentToDisk={window.Signal.Migrations.saveAttachmentToDisk}
|
saveAttachmentToDisk={window.Signal.Migrations.saveAttachmentToDisk}
|
||||||
generateDonationReceiptBlob={generateDonationReceiptBlob}
|
generateDonationReceiptBlob={generateDonationReceiptBlob}
|
||||||
|
__dangerouslyRunAbitraryReadOnlySqlQuery={
|
||||||
|
__dangerouslyRunAbitraryReadOnlySqlQuery
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1501,6 +1501,13 @@
|
||||||
"updated": "2025-05-30T22:48:14.420Z",
|
"updated": "2025-05-30T22:48:14.420Z",
|
||||||
"reasonDetail": "For focusing the settings backup key viewer textarea"
|
"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",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/ProfileEditor.tsx",
|
"path": "ts/components/ProfileEditor.tsx",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue