-
Notifications
You must be signed in to change notification settings - Fork 1
/
database.ts
138 lines (129 loc) · 3.91 KB
/
database.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
// This is an "promisified" version of sqlite3
import * as sqlite from 'sqlite';
// This is only need for constants declarations
import { OPEN_CREATE, OPEN_READWRITE } from 'sqlite3';
import { FieldsMap, ParsedFieldsMap, ParsedFields } from './configTypes';
const BLOGSEARCH = 'blogsearch';
const BLOGSEARCH_EXT_CONTENT = 'blogsearch_ext_content';
export class Database {
private constructor (
private readonly db: sqlite.Database,
private readonly columns: FieldsMap,
) {}
public static async create ({
filename,
columns,
debug = false,
}: {
filename: string;
columns: FieldsMap;
debug?: boolean;
}) {
const db = await sqlite.open(
filename,
{
mode: (OPEN_CREATE | OPEN_READWRITE),
verbose: true
}
);
columns = filterMap(columns, field => field.enabled);
return (new Database(db, columns)).init({ debug });
}
private async init ({ debug = false }) {
if (debug) {
const thisFunc = this.db.run.bind(this.db);
// @ts-ignore
this.db.run = async (...args: any[]) => {
console.log(args[0].toString());
await thisFunc(...args);
};
}
// Reference: https://www.sqlite.org/fts5.html
// Create a virtual table 'blogsearch'
// We use the fts5 extension to support full-text search out of the box.
await this.db.run(`
CREATE VIRTUAL TABLE ${BLOGSEARCH}
USING fts5(
${[...this.columns.entries()]
.map(([field, config]) => `${field}${config.indexed ? '' : ' UNINDEXED'}`)
.join(',')},
tokenize = 'porter unicode61 remove_diacritics 1',
content=${BLOGSEARCH_EXT_CONTENT},
content_rowid=rowid
);
`);
// Create table 'blogsearch_ext_content'. This is an external content table
// for FTS5. External content table is used to support "hasConent" feature
// that helps reducing the size of the output db file.
await this.db.run(`
CREATE TABLE ${BLOGSEARCH_EXT_CONTENT} (
rowid INTEGER PRIMARY KEY,
${[...this.columns.entries()]
.map(([field, _]) => field)
.join(',')}
);
`);
return this;
}
public async insert (
rowid: number,
columns: ParsedFields | ParsedFieldsMap
) {
if (columns instanceof Map) {
// If map, filter out disabled keys
const originalColumns = columns;
columns = new Map();
for (const [field] of this.columns) {
columns.set(field, originalColumns.get(field)!);
}
} else {
// If object, construct a map with the correct order.
const columnsObj = columns;
columns = new Map();
for (const [field, config] of this.columns) {
columns.set(field, {
value: columnsObj[field],
config
});
}
}
// Insert the parsed content first.
await this.db.run(`
INSERT INTO ${BLOGSEARCH_EXT_CONTENT}
VALUES (
${rowid},
${[...columns.values()]
.map(({ value, config }) => config.hasContent ? value : '')
.map(value => `'${value.replace(/'/g, "''")}'`) // A single quote should be encoded to two single quotes: https://www.sqlite.org/lang_expr.html
.join(',')}
);
`);
// When inserting a row in the fts table, it only indexes content but does
// not actually store content.
await this.db.run(`
INSERT INTO ${BLOGSEARCH} (
rowid,
${[...columns.keys()].join(',')}
)
VALUES (
${rowid},
${[...columns.values()]
.map(({ value }) => value)
.map(value => `'${value.replace(/'/g, "''")}'`) // The same as above
.join(',')}
);
`);
}
public async close () {
await this.db.close();
}
}
function filterMap<K, V> (map: Map<K, V>, callback: (value: V) => boolean) {
const newMap = new Map();
for (const [key, value] of map) {
if (callback(value)) {
newMap.set(key, value);
}
}
return newMap;
}