Refactor: Move data-access code to Typescript w/ shared interface

This commit is contained in:
Scott Nonnenberg 2020-04-01 11:59:11 -07:00
parent 9ab54b9b83
commit 6b094e1514
35 changed files with 1695 additions and 598 deletions

1426
ts/sql/Client.ts Normal file

File diff suppressed because it is too large Load diff

382
ts/sql/Interface.ts Normal file
View file

@ -0,0 +1,382 @@
import { LocaleMessagesType } from '../types/I18N';
export type AttachmentDownloadJobType = any;
export type ConverationMetricsType = any;
export type ConversationType = any;
export type EmojiType = any;
export type IdentityKeyType = any;
export type ItemType = any;
export type MessageType = any;
export type MessageTypeUnhydrated = any;
export type PreKeyType = any;
export type SearchResultMessageType = any;
export type SessionType = any;
export type SignedPreKeyType = any;
export type StickerPackStatusType = string;
export type StickerPackType = any;
export type StickerType = any;
export type UnprocessedType = any;
export type BackboneConversationModelType = any;
export type BackboneConversationCollectionType = any;
export type BackboneMessageModelType = any;
export type BackboneMessageCollectionType = any;
export interface DataInterface {
close: () => Promise<void>;
removeDB: () => Promise<void>;
removeIndexedDBFiles: () => Promise<void>;
createOrUpdateIdentityKey: (data: IdentityKeyType) => Promise<void>;
getIdentityKeyById: (id: string) => Promise<IdentityKeyType | undefined>;
bulkAddIdentityKeys: (array: Array<IdentityKeyType>) => Promise<void>;
removeIdentityKeyById: (id: string) => Promise<void>;
removeAllIdentityKeys: () => Promise<void>;
getAllIdentityKeys: () => Promise<Array<IdentityKeyType>>;
createOrUpdatePreKey: (data: PreKeyType) => Promise<void>;
getPreKeyById: (id: number) => Promise<PreKeyType | undefined>;
bulkAddPreKeys: (array: Array<PreKeyType>) => Promise<void>;
removePreKeyById: (id: number) => Promise<void>;
removeAllPreKeys: () => Promise<void>;
getAllPreKeys: () => Promise<Array<PreKeyType>>;
createOrUpdateSignedPreKey: (data: SignedPreKeyType) => Promise<void>;
getSignedPreKeyById: (id: number) => Promise<SignedPreKeyType | undefined>;
bulkAddSignedPreKeys: (array: Array<SignedPreKeyType>) => Promise<void>;
removeSignedPreKeyById: (id: number) => Promise<void>;
removeAllSignedPreKeys: () => Promise<void>;
getAllSignedPreKeys: () => Promise<Array<SignedPreKeyType>>;
createOrUpdateItem: (data: ItemType) => Promise<void>;
getItemById: (id: string) => Promise<ItemType | undefined>;
bulkAddItems: (array: Array<ItemType>) => Promise<void>;
removeItemById: (id: string) => Promise<void>;
removeAllItems: () => Promise<void>;
getAllItems: () => Promise<Array<ItemType>>;
createOrUpdateSession: (data: SessionType) => Promise<void>;
createOrUpdateSessions: (array: Array<SessionType>) => Promise<void>;
getSessionById: (id: string) => Promise<SessionType | undefined>;
getSessionsById: (conversationId: string) => Promise<Array<SessionType>>;
bulkAddSessions: (array: Array<SessionType>) => Promise<void>;
removeSessionById: (id: string) => Promise<void>;
removeSessionsByConversation: (conversationId: string) => Promise<void>;
removeAllSessions: () => Promise<void>;
getAllSessions: () => Promise<Array<SessionType>>;
getConversationCount: () => Promise<number>;
saveConversation: (data: ConversationType) => Promise<void>;
saveConversations: (array: Array<ConversationType>) => Promise<void>;
updateConversations: (array: Array<ConversationType>) => Promise<void>;
getAllConversationIds: () => Promise<Array<string>>;
searchConversations: (
query: string,
options?: { limit?: number }
) => Promise<Array<ConversationType>>;
searchMessages: (
query: string,
options?: { limit?: number }
) => Promise<Array<SearchResultMessageType>>;
searchMessagesInConversation: (
query: string,
conversationId: string,
options?: { limit?: number }
) => Promise<Array<SearchResultMessageType>>;
getMessageCount: () => Promise<number>;
saveMessages: (
arrayOfMessages: Array<MessageType>,
options: { forceSave?: boolean }
) => Promise<void>;
getAllMessageIds: () => Promise<Array<string>>;
getMessageMetricsForConversation: (
conversationId: string
) => Promise<ConverationMetricsType>;
getUnprocessedCount: () => Promise<number>;
getAllUnprocessed: () => Promise<Array<UnprocessedType>>;
saveUnprocessed: (
data: UnprocessedType,
options?: { forceSave?: boolean }
) => Promise<number>;
updateUnprocessedAttempts: (id: string, attempts: number) => Promise<void>;
updateUnprocessedWithData: (
id: string,
data: UnprocessedType
) => Promise<void>;
updateUnprocessedsWithData: (array: Array<UnprocessedType>) => Promise<void>;
getUnprocessedById: (id: string) => Promise<UnprocessedType | undefined>;
saveUnprocesseds: (
arrayOfUnprocessed: Array<UnprocessedType>,
options?: { forceSave?: boolean }
) => Promise<void>;
removeUnprocessed: (id: string) => Promise<void>;
removeAllUnprocessed: () => Promise<void>;
getNextAttachmentDownloadJobs: (
limit?: number,
options?: { timestamp?: number }
) => Promise<Array<AttachmentDownloadJobType>>;
saveAttachmentDownloadJob: (job: AttachmentDownloadJobType) => Promise<void>;
setAttachmentDownloadJobPending: (
id: string,
pending: boolean
) => Promise<void>;
resetAttachmentDownloadPending: () => Promise<void>;
removeAttachmentDownloadJob: (id: string) => Promise<void>;
removeAllAttachmentDownloadJobs: () => Promise<void>;
createOrUpdateStickerPack: (pack: StickerPackType) => Promise<void>;
updateStickerPackStatus: (
id: string,
status: StickerPackStatusType,
options?: { timestamp: number }
) => Promise<void>;
createOrUpdateSticker: (sticker: StickerType) => Promise<void>;
updateStickerLastUsed: (
packId: string,
stickerId: number,
lastUsed: number
) => Promise<void>;
addStickerPackReference: (messageId: string, packId: string) => Promise<void>;
deleteStickerPackReference: (
messageId: string,
packId: string
) => Promise<Array<string>>;
getStickerCount: () => Promise<number>;
deleteStickerPack: (packId: string) => Promise<Array<string>>;
getAllStickerPacks: () => Promise<Array<StickerPackType>>;
getAllStickers: () => Promise<Array<StickerType>>;
getRecentStickers: (options?: {
limit?: number;
}) => Promise<Array<StickerType>>;
updateEmojiUsage: (shortName: string, timeUsed?: number) => Promise<void>;
getRecentEmojis: (limit?: number) => Promise<Array<EmojiType>>;
removeAll: () => Promise<void>;
removeAllConfiguration: () => Promise<void>;
getMessagesNeedingUpgrade: (
limit: number,
options: { maxVersion: number }
) => Promise<Array<MessageType>>;
getMessagesWithVisualMediaAttachments: (
conversationId: string,
options: { limit: number }
) => Promise<Array<MessageType>>;
getMessagesWithFileAttachments: (
conversationId: string,
options: { limit: number }
) => Promise<Array<MessageType>>;
}
// The reason for client/server divergence is the need to inject Backbone models and
// collections into data calls so those are the objects returned. This was necessary in
// July 2018 when creating the Data API as a drop-in replacement for previous database
// requests via ORM.
// Note: It is extremely important that items are duplicated between these two. Client.js
// loops over all of its local functions to generate the server-side IPC-based API.
export type ServerInterface = DataInterface & {
getAllConversations: () => Promise<Array<ConversationType>>;
getAllGroupsInvolvingId: (id: string) => Promise<Array<ConversationType>>;
getAllPrivateConversations: () => Promise<Array<ConversationType>>;
getConversationById: (id: string) => Promise<ConversationType>;
getExpiredMessages: () => Promise<Array<MessageType>>;
getMessageById: (id: string) => Promise<MessageType | undefined>;
getMessageBySender: (options: {
source: string;
sourceUuid: string;
sourceDevice: string;
sent_at: number;
}) => Promise<Array<MessageType>>;
getMessagesBySentAt: (sentAt: number) => Promise<Array<MessageType>>;
getOlderMessagesByConversation: (
conversationId: string,
options?: { limit?: number; receivedAt?: number }
) => Promise<Array<MessageTypeUnhydrated>>;
getNewerMessagesByConversation: (
conversationId: string,
options?: { limit?: number; receivedAt?: number }
) => Promise<Array<MessageTypeUnhydrated>>;
getNextExpiringMessage: () => Promise<MessageType>;
getNextTapToViewMessageToAgeOut: () => Promise<MessageType>;
getOutgoingWithoutExpiresAt: () => Promise<Array<MessageType>>;
getTapToViewMessagesNeedingErase: () => Promise<Array<MessageType>>;
getUnreadByConversation: (
conversationId: string
) => Promise<Array<MessageType>>;
removeConversation: (id: Array<string> | string) => Promise<void>;
removeMessage: (id: Array<string> | string) => Promise<void>;
saveMessage: (
data: MessageType,
options: { forceSave?: boolean }
) => Promise<number>;
updateConversation: (data: ConversationType) => Promise<void>;
// For testing only
_getAllMessages: () => Promise<Array<MessageType>>;
// Server-only
initialize: (options: {
configDir: string;
key: string;
messages: LocaleMessagesType;
}) => Promise<boolean>;
removeKnownAttachments: (
allAttachments: Array<string>
) => Promise<Array<string>>;
removeKnownStickers: (allStickers: Array<string>) => Promise<Array<string>>;
removeKnownDraftAttachments: (
allStickers: Array<string>
) => Promise<Array<string>>;
};
export type ClientInterface = DataInterface & {
getAllConversations: ({
ConversationCollection,
}: {
ConversationCollection: BackboneConversationCollectionType;
}) => Promise<Array<ConversationType>>;
getAllGroupsInvolvingId: (
id: string,
{
ConversationCollection,
}: {
ConversationCollection: BackboneConversationCollectionType;
}
) => Promise<Array<ConversationType>>;
getAllPrivateConversations: ({
ConversationCollection,
}: {
ConversationCollection: BackboneConversationCollectionType;
}) => Promise<Array<ConversationType>>;
getConversationById: (
id: string,
{ Conversation }: { Conversation: BackboneConversationModelType }
) => Promise<ConversationType>;
getExpiredMessages: ({
MessageCollection,
}: {
MessageCollection: BackboneMessageCollectionType;
}) => Promise<Array<MessageType>>;
getMessageById: (
id: string,
{ Message }: { Message: BackboneMessageModelType }
) => Promise<MessageType | undefined>;
getMessageBySender: (
options: {
source: string;
sourceUuid: string;
sourceDevice: string;
sent_at: number;
},
{ Message }: { Message: BackboneMessageModelType }
) => Promise<Array<MessageType>>;
getMessagesBySentAt: (
sentAt: number,
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType }
) => Promise<Array<MessageType>>;
getOlderMessagesByConversation: (
conversationId: string,
options: {
limit?: number;
receivedAt?: number;
MessageCollection: BackboneMessageCollectionType;
}
) => Promise<Array<MessageTypeUnhydrated>>;
getNewerMessagesByConversation: (
conversationId: string,
options: {
limit?: number;
receivedAt?: number;
MessageCollection: BackboneMessageCollectionType;
}
) => Promise<Array<MessageTypeUnhydrated>>;
getNextExpiringMessage: ({
Message,
}: {
Message: BackboneMessageModelType;
}) => Promise<MessageType>;
getNextTapToViewMessageToAgeOut: ({
Message,
}: {
Message: BackboneMessageModelType;
}) => Promise<MessageType>;
getOutgoingWithoutExpiresAt: ({
MessageCollection,
}: {
MessageCollection: BackboneMessageCollectionType;
}) => Promise<Array<MessageType>>;
getTapToViewMessagesNeedingErase: ({
MessageCollection,
}: {
MessageCollection: BackboneMessageCollectionType;
}) => Promise<Array<MessageType>>;
getUnreadByConversation: (
conversationId: string,
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType }
) => Promise<Array<MessageType>>;
removeConversation: (
id: string,
{ Conversation }: { Conversation: BackboneConversationModelType }
) => Promise<void>;
removeMessage: (
id: string,
{ Message }: { Message: BackboneMessageModelType }
) => Promise<void>;
saveMessage: (
data: MessageType,
options: { forceSave?: boolean; Message: BackboneMessageModelType }
) => Promise<number>;
updateConversation: (data: ConversationType) => void;
// Test-only
_getAllMessages: ({
MessageCollection,
}: {
MessageCollection: BackboneMessageCollectionType;
}) => Promise<Array<MessageType>>;
// Client-side only
shutdown: () => Promise<void>;
removeAllMessagesInConversation: (
conversationId: string,
{ MessageCollection }: { MessageCollection: BackboneMessageCollectionType }
) => Promise<void>;
removeOtherData: () => Promise<void>;
cleanupOrphanedAttachments: () => Promise<void>;
ensureFilePermissions: () => Promise<void>;
getLegacyMessagesNeedingUpgrade: (
limit: number,
options: { maxVersion: number }
) => Promise<Array<MessageType>>;
saveLegacyMessage: (data: MessageType) => Promise<void>;
// Client-side only, and test-only
_removeConversations: (ids: Array<string>) => Promise<void>;
_removeMessages: (ids: Array<string>) => Promise<void>;
_cleanData: (data: any, path?: string) => any;
_jobs: { [id: string]: ClientJobType };
};
export type ClientJobType = {
fnName: string;
start: number;
resolve?: Function;
reject?: Function;
// Only in DEBUG mode
complete?: boolean;
args?: Array<any>;
};

3953
ts/sql/Server.ts Normal file

File diff suppressed because it is too large Load diff

181
ts/sqlcipher.d.ts vendored Normal file
View file

@ -0,0 +1,181 @@
// Taken from:
// https://github.com/DefinitelyTyped/DefinitelyTyped/blob/8bf8aedba75ada257428c4846d2bc7d14e3b4be8/types/sqlite3/index.d.ts
declare module '@journeyapps/sqlcipher' {
// Type definitions for sqlite3 3.1
// Project: http://github.com/mapbox/node-sqlite3
// Definitions by: Nick Malaguti <https://github.com/nmalaguti>
// Sumant Manne <https://github.com/dpyro>
// Behind The Math <https://github.com/BehindTheMath>
// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped
/// <reference types="node" />
import events = require('events');
export const OPEN_READONLY: number;
export const OPEN_READWRITE: number;
export const OPEN_CREATE: number;
export const OPEN_SHAREDCACHE: number;
export const OPEN_PRIVATECACHE: number;
export const OPEN_URI: number;
export const cached: {
Database(
filename: string,
callback?: (this: Database, err: Error | null) => void
): Database;
Database(
filename: string,
mode?: number,
callback?: (this: Database, err: Error | null) => void
): Database;
};
export interface RunResult extends Statement {
lastID: number;
changes: number;
}
export class Statement {
bind(callback?: (err: Error | null) => void): this;
bind(...params: any[]): this;
reset(callback?: (err: null) => void): this;
finalize(callback?: (err: Error) => void): Database;
run(callback?: (err: Error | null) => void): this;
run(
params: any,
callback?: (this: RunResult, err: Error | null) => void
): this;
run(...params: any[]): this;
get(callback?: (err: Error | null, row?: any) => void): this;
get(
params: any,
callback?: (this: RunResult, err: Error | null, row?: any) => void
): this;
get(...params: any[]): this;
all(callback?: (err: Error | null, rows: any[]) => void): this;
all(
params: any,
callback?: (this: RunResult, err: Error | null, rows: any[]) => void
): this;
all(...params: any[]): this;
each(
callback?: (err: Error | null, row: any) => void,
complete?: (err: Error | null, count: number) => void
): this;
each(
params: any,
callback?: (this: RunResult, err: Error | null, row: any) => void,
complete?: (err: Error | null, count: number) => void
): this;
each(...params: any[]): this;
}
export class Database extends events.EventEmitter {
constructor(filename: string, callback?: (err: Error | null) => void);
constructor(
filename: string,
mode?: number,
callback?: (err: Error | null) => void
);
close(callback?: (err: Error | null) => void): void;
run(
sql: string,
callback?: (this: RunResult, err: Error | null) => void
): this;
run(
sql: string,
params: any,
callback?: (this: RunResult, err: Error | null) => void
): this;
run(sql: string, ...params: any[]): this;
get(
sql: string,
callback?: (this: Statement, err: Error | null, row: any) => void
): this;
get(
sql: string,
params: any,
callback?: (this: Statement, err: Error | null, row: any) => void
): this;
get(sql: string, ...params: any[]): this;
all(
sql: string,
callback?: (this: Statement, err: Error | null, rows: any[]) => void
): this;
all(
sql: string,
params: any,
callback?: (this: Statement, err: Error | null, rows: any[]) => void
): this;
all(sql: string, ...params: any[]): this;
each(
sql: string,
callback?: (this: Statement, err: Error | null, row: any) => void,
complete?: (err: Error | null, count: number) => void
): this;
each(
sql: string,
params: any,
callback?: (this: Statement, err: Error | null, row: any) => void,
complete?: (err: Error | null, count: number) => void
): this;
each(sql: string, ...params: any[]): this;
exec(
sql: string,
callback?: (this: Statement, err: Error | null) => void
): this;
prepare(
sql: string,
callback?: (this: Statement, err: Error | null) => void
): Statement;
prepare(
sql: string,
params: any,
callback?: (this: Statement, err: Error | null) => void
): Statement;
prepare(sql: string, ...params: any[]): Statement;
serialize(callback?: () => void): void;
parallelize(callback?: () => void): void;
on(event: 'trace', listener: (sql: string) => void): this;
on(event: 'profile', listener: (sql: string, time: number) => void): this;
on(event: 'error', listener: (err: Error) => void): this;
on(event: 'open' | 'close', listener: () => void): this;
on(event: string, listener: (...args: any[]) => void): this;
configure(option: 'busyTimeout', value: number): void;
interrupt(): void;
}
export function verbose(): sqlite3;
export interface sqlite3 {
OPEN_READONLY: number;
OPEN_READWRITE: number;
OPEN_CREATE: number;
OPEN_SHAREDCACHE: number;
OPEN_PRIVATECACHE: number;
OPEN_URI: number;
cached: typeof cached;
RunResult: RunResult;
Statement: typeof Statement;
Database: typeof Database;
verbose(): this;
}
}

View file

@ -1,6 +1,8 @@
import { take, uniq } from 'lodash';
import { EmojiPickDataType } from '../../components/emoji/EmojiPicker';
import { updateEmojiUsage } from '../../../js/modules/data';
import dataInterface from '../../sql/Client';
const { updateEmojiUsage } = dataInterface;
// State

View file

@ -3,11 +3,7 @@ import { omit, reject } from 'lodash';
import { normalize } from '../../types/PhoneNumber';
import { trigger } from '../../shims/events';
import { cleanSearchTerm } from '../../util/cleanSearchTerm';
import {
searchConversations as dataSearchConversations,
searchMessages as dataSearchMessages,
searchMessagesInConversation,
} from '../../../js/modules/data';
import dataInterface from '../../sql/Client';
import { makeLookup } from '../../util/makeLookup';
import {
@ -20,6 +16,12 @@ import {
ShowArchivedConversationsActionType,
} from './conversations';
const {
searchConversations: dataSearchConversations,
searchMessages: dataSearchMessages,
searchMessagesInConversation,
} = dataInterface;
// State
export type MessageSearchResultType = MessageType & {

View file

@ -1,9 +1,5 @@
import { Dictionary, omit, reject } from 'lodash';
import {
getRecentStickers,
updateStickerLastUsed,
updateStickerPackStatus,
} from '../../../js/modules/data';
import dataInterface from '../../sql/Client';
import {
downloadStickerPack as externalDownloadStickerPack,
maybeDeletePack,
@ -13,6 +9,12 @@ import { trigger } from '../../shims/events';
import { NoopActionType } from './noop';
const {
getRecentStickers,
updateStickerLastUsed,
updateStickerPackStatus,
} = dataInterface;
// State
export type StickerDBType = {

11
ts/types/I18N.ts Normal file
View file

@ -0,0 +1,11 @@
export type LocaleMessagesType = {
[key: string]: {
message: string;
description?: string;
};
};
export type LocaleType = {
i18n: (key: string, placeholders: Array<string>) => string;
messages: LocaleMessagesType;
};

10
ts/types/Logging.ts Normal file
View file

@ -0,0 +1,10 @@
type LogFunction = (...args: Array<any>) => void;
export type LoggerType = {
fatal: LogFunction;
error: LogFunction;
warn: LogFunction;
info: LogFunction;
debug: LogFunction;
trace: LogFunction;
};

View file

@ -27,26 +27,8 @@ import { Dialogs } from '../types/Dialogs';
import * as packageJson from '../../package.json';
import { getSignatureFileName } from './signature';
export type LocaleType = {
i18n: (key: string, placeholders: Array<string>) => string;
messages: {
[key: string]: {
message: string;
description?: string;
};
};
};
type LogFunction = (...args: Array<any>) => void;
export type LoggerType = {
fatal: LogFunction;
error: LogFunction;
warn: LogFunction;
info: LogFunction;
debug: LogFunction;
trace: LogFunction;
};
import { LocaleType } from '../types/I18N';
import { LoggerType } from '../types/Logging';
const writeFile = pify(writeFileCallback);
const mkdirpPromise = pify(mkdirp);

View file

@ -3,7 +3,8 @@ import { BrowserWindow } from 'electron';
import { start as startMacOS } from './macos';
import { start as startWindows } from './windows';
import { LocaleType, LoggerType } from './common';
import { LocaleType } from '../types/I18N';
import { LoggerType } from '../types/Logging';
let initialized = false;

View file

@ -15,11 +15,11 @@ import {
deleteTempDir,
downloadUpdate,
getPrintableError,
LocaleType,
LoggerType,
showCannotUpdateDialog,
showUpdateDialog,
} from './common';
import { LocaleType } from '../types/I18N';
import { LoggerType } from '../types/Logging';
import { hexToBinary, verifySignature } from './signature';
import { markShouldQuit } from '../../app/window_state';
import { Dialogs } from '../types/Dialogs';

View file

@ -12,11 +12,11 @@ import {
deleteTempDir,
downloadUpdate,
getPrintableError,
LocaleType,
LoggerType,
showCannotUpdateDialog,
showUpdateDialog,
} from './common';
import { LocaleType } from '../types/I18N';
import { LoggerType } from '../types/Logging';
import { hexToBinary, verifySignature } from './signature';
import { markShouldQuit } from '../../app/window_state';

View file

@ -172,7 +172,7 @@
"rule": "jQuery-load(",
"path": "js/conversation_controller.js",
"line": " this._initialPromise = load();",
"lineNumber": 260,
"lineNumber": 257,
"reasonCategory": "falseMatch",
"updated": "2020-03-24T20:06:31.391Z"
},

10
ts/window.d.ts vendored
View file

@ -12,6 +12,7 @@ declare global {
warn: LoggerType;
error: LoggerType;
};
restart: () => void;
storage: {
put: (key: string, value: any) => void;
remove: (key: string) => void;
@ -25,6 +26,7 @@ declare global {
}
export type ConversationControllerType = {
getConversationId: (identifier: string) => string | null;
prepareForSend: (
id: string,
options: Object
@ -95,4 +97,12 @@ export type WhisperType = {
events: {
trigger: (name: string, param1: any, param2: any) => void;
};
Database: {
open: () => Promise<IDBDatabase>;
handleDOMException: (
context: string,
error: DOMException | null,
reject: Function
) => void;
};
};