votive 0.0.8 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -5
- package/lib/bundle.js +121 -55
- package/lib/createDatabase.js +895 -450
- package/lib/readAbstracts.js +12 -9
- package/lib/readBuffers.js +10 -10
- package/lib/readFilePaths.js +12 -12
- package/lib/readFolders.js +10 -17
- package/lib/readSources.js +107 -55
- package/lib/runJobs.js +1 -3
- package/lib/sqlite.js +12 -14
- package/lib/utils/index.js +8 -4
- package/lib/writeDestinations.js +23 -18
- package/package.json +2 -1
- package/tests/index.js +303 -263
- package/tests/markdown/prunee.md +1 -0
- package/tests/.votive.db +0 -0
package/lib/createDatabase.js
CHANGED
|
@@ -1,13 +1,51 @@
|
|
|
1
1
|
import { DatabaseSync, backup } from "node:sqlite"
|
|
2
2
|
import { splitURL, checkFile } from "./utils/index.js"
|
|
3
|
-
import { statSync } from "node:fs"
|
|
4
3
|
import path from "node:path"
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
/** @typedef {ReturnType<createDatabase>} Database */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object | array} Abstract
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {any} MetadataProperty
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Record<string, MetadataProperty>} Metadata
|
|
17
|
+
*/
|
|
6
18
|
|
|
7
19
|
/**
|
|
8
|
-
* @typedef {
|
|
20
|
+
* @typedef {object} TargetOutput
|
|
21
|
+
* @property {number} key
|
|
22
|
+
* @property {string} path
|
|
23
|
+
* @property {string} dir
|
|
24
|
+
* @property {string} syntax
|
|
25
|
+
* @property {number} stale
|
|
26
|
+
* @property {Abstract} abstract
|
|
27
|
+
* @property {Metadata} metadata
|
|
9
28
|
*/
|
|
10
29
|
|
|
30
|
+
/**
|
|
31
|
+
* @typedef {object} TargetInput
|
|
32
|
+
* @property {string} path
|
|
33
|
+
* @property {Abstract} abstract
|
|
34
|
+
* @property {Metadata} metadata
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @param {string} json
|
|
40
|
+
* @returns {object | array}
|
|
41
|
+
*/
|
|
42
|
+
function coerceJSON(json) {
|
|
43
|
+
const parsed = JSON.parse(json)
|
|
44
|
+
if (!json) return {}
|
|
45
|
+
if (Array.isArray(json)) return parsed
|
|
46
|
+
if (typeof parsed === "object") return parsed
|
|
47
|
+
else return {}
|
|
48
|
+
}
|
|
11
49
|
|
|
12
50
|
/** @param {string} dbPath */
|
|
13
51
|
function loadDB(dbPath) {
|
|
@@ -16,529 +54,936 @@ function loadDB(dbPath) {
|
|
|
16
54
|
}
|
|
17
55
|
|
|
18
56
|
|
|
19
|
-
/** @param {
|
|
20
|
-
function
|
|
21
|
-
const databaseSync = loadDB(dbPath)
|
|
57
|
+
/** @param {DatabaseSync} database */
|
|
58
|
+
function prepareStatements(database) {
|
|
22
59
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
databaseSync.exec(sqlCreateTables)
|
|
26
|
-
|
|
27
|
-
const database = {}
|
|
28
|
-
|
|
29
|
-
database.saveDB = async () => {
|
|
30
|
-
if (databaseSync.location()) return // Only save if running in memory
|
|
31
|
-
await backup(databaseSync, dbPath)
|
|
32
|
-
}
|
|
60
|
+
const { prepare } = database
|
|
33
61
|
|
|
34
62
|
/**
|
|
35
|
-
* @
|
|
36
|
-
* @param {string} urlPath
|
|
63
|
+
* @typedef {string} JSONString - A string that is actually JSON.
|
|
37
64
|
*/
|
|
38
|
-
database.createFolder = (folderPath, urlPath) => {
|
|
39
|
-
return store.get`INSERT OR IGNORE INTO folders (folderPath, urlPath) VALUES :${folderPath} :${urlPath} RETURNING *`
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const createSource = databaseSync.prepare(`INSERT INTO sources (path, destination, lastModified) VALUES (?, ?, ?)`)
|
|
43
65
|
|
|
44
66
|
/**
|
|
45
|
-
* @
|
|
46
|
-
* @
|
|
47
|
-
* @
|
|
67
|
+
* @typedef {object} SQLiteSource
|
|
68
|
+
* @property {number} id
|
|
69
|
+
* @property {string} destination
|
|
70
|
+
* @property {string} filePath
|
|
71
|
+
* @property {number} lastModified
|
|
48
72
|
*/
|
|
49
|
-
database.createSource = (source, destination, lastModified) => {
|
|
50
|
-
databaseSync.prepare(``) // SQLite bug. Query fails without this.
|
|
51
|
-
createSource.get(source, destination, lastModified)
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const updateSource = databaseSync.prepare(`UPDATE sources SET lastModified = ? WHERE path = ?`)
|
|
55
73
|
|
|
56
74
|
/**
|
|
57
|
-
* @
|
|
58
|
-
* @
|
|
75
|
+
* @typedef {object} SQLiteTarget
|
|
76
|
+
* @property {number} key
|
|
77
|
+
* @property {string} path
|
|
78
|
+
* @property {string} dir
|
|
79
|
+
* @property {string} syntax
|
|
80
|
+
* @property {number} stale
|
|
81
|
+
* @property {JSONString} abstract
|
|
82
|
+
* @property {JSONString} metadata
|
|
59
83
|
*/
|
|
60
|
-
database.updateSource = (source, lastModified) => {
|
|
61
|
-
updateSource.get(lastModified, source)
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const getSettingsBySource = databaseSync.prepare(`SELECT * FROM settings WHERE source = ?`)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
// These statements are cached, no need to refactor
|
|
68
|
-
const createDest = databaseSync.prepare(`INSERT INTO destinations (path, dir, syntax, stale, abstract) VALUES (?, ?, ?, 1, ?)`)
|
|
69
|
-
const updateDest = databaseSync.prepare(`UPDATE destinations SET abstract = ? WHERE path = ?`)
|
|
70
|
-
const createMeta = databaseSync.prepare(`INSERT OR REPLACE INTO metadata (destination, label, value, type) VALUES (?, ?, ?, ?)`)
|
|
71
|
-
const updateMeta = databaseSync.prepare(`UPDATE metadata SET value = ? WHERE destination = ? AND label = ?`)
|
|
72
|
-
const getDepends = databaseSync.prepare(`SELECT * FROM dependencies WHERE destination = ? AND property = ?`)
|
|
73
|
-
const getDependenciesByDestination = databaseSync.prepare(`SELECT * FROM dependencies WHERE destination = ?`)
|
|
74
|
-
const getAllDeps = databaseSync.prepare(`SELECT * FROM dependencies`)
|
|
75
|
-
const staleDepen = databaseSync.prepare(`UPDATE destinations SET stale = 1 WHERE path = ? RETURNING *`)
|
|
76
|
-
const freshDepen = databaseSync.prepare(`UPDATE destinations SET stale = 0 WHERE path = ? RETURNING *`)
|
|
77
|
-
const staleDescendents = databaseSync.prepare(`UPDATE destinations SET stale = 1 WHERE path LIKE ? RETURNING *`)
|
|
78
|
-
const getAllSettings = databaseSync.prepare(`SELECT * FROM settings`)
|
|
79
|
-
|
|
80
|
-
database.getAllSettings = () => {
|
|
81
|
-
return getAllSettings.all()
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** @param {string} path */
|
|
85
|
-
database.freshenDependency = (path) => {
|
|
86
|
-
freshDepen.get(path)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
database.getDependencies = () => {
|
|
90
|
-
return getAllDeps.all()
|
|
91
|
-
}
|
|
92
84
|
|
|
93
85
|
/**
|
|
94
|
-
* @
|
|
95
|
-
* @
|
|
96
|
-
* @
|
|
97
|
-
* @
|
|
98
|
-
* @
|
|
86
|
+
* @typedef {object} SQLiteDependency
|
|
87
|
+
* @property {number} key
|
|
88
|
+
* @property {string} destination
|
|
89
|
+
* @property {string} property
|
|
90
|
+
* @property {string} dependent
|
|
99
91
|
*/
|
|
100
|
-
database.createOrUpdateDestination = ({ metadata, ...dest }) => {
|
|
101
|
-
const dir = dest.path && splitURL(dest.path)
|
|
102
|
-
const params = Object.keys(metadata)
|
|
103
|
-
if (dest.abstract) params.push("abstract")
|
|
104
|
-
const extant = database.getDestinationIndependently(dest.path, params)
|
|
105
|
-
|
|
106
|
-
if (!extant) {
|
|
107
|
-
createDest.get(dest.path, dir, dest.syntax, JSON.stringify(dest.abstract))
|
|
108
|
-
Object.entries(metadata).map(([k, v]) => {
|
|
109
|
-
const type = typeof v
|
|
110
|
-
const value = type === "object"
|
|
111
|
-
? JSON.stringify(v)
|
|
112
|
-
: v
|
|
113
|
-
|
|
114
|
-
createMeta.get(dest.path, k, value, typeof v)
|
|
115
|
-
})
|
|
116
|
-
return
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const changedAbstract = JSON.stringify(dest.abstract) !== JSON.stringify(extant.abstract)
|
|
120
|
-
|
|
121
|
-
let changedMetadata = false
|
|
122
92
|
|
|
93
|
+
/**
|
|
94
|
+
* @typedef {object} SQLiteMetadata
|
|
95
|
+
* @property {number} id
|
|
96
|
+
* @property {string} destination
|
|
97
|
+
* @property {string} label
|
|
98
|
+
* @property {string} value
|
|
99
|
+
* @property {string} type
|
|
100
|
+
*/
|
|
123
101
|
|
|
124
|
-
|
|
125
|
-
|
|
102
|
+
/**
|
|
103
|
+
* @typedef {object} SQLiteSetting
|
|
104
|
+
* @typedef {number} id
|
|
105
|
+
* @typedef {string} destination
|
|
106
|
+
* @typedef {string} source
|
|
107
|
+
* @typedef {string} label
|
|
108
|
+
* @typedef {string} value
|
|
109
|
+
*/
|
|
126
110
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
updateMeta.get(JSON.stringify(metadata[key]), dest.path, key)
|
|
133
|
-
const dependencies = getDepends.all(dest.path, key)
|
|
134
|
-
dependencies.forEach(({ dependent }) => {
|
|
135
|
-
staleDepen.get(dependent)
|
|
136
|
-
})
|
|
137
|
-
}
|
|
138
|
-
}
|
|
111
|
+
/**
|
|
112
|
+
* @typedef {object} SQLiteURL
|
|
113
|
+
* @typedef {string} url
|
|
114
|
+
* @typedef {string} data
|
|
115
|
+
*/
|
|
139
116
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
117
|
+
return {
|
|
118
|
+
|
|
119
|
+
/* SOURCES */
|
|
120
|
+
source: {
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* @callback SQLiteSourcesCreate
|
|
124
|
+
* @param {string} path
|
|
125
|
+
* @param {string} destination
|
|
126
|
+
* @param {number} timestamp
|
|
127
|
+
* @returns {SQLiteSource}
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
create: /** @type {{get: SQLiteSourcesCreate}} */ (/** @type {unknown} */ (database.prepare(`
|
|
131
|
+
INSERT INTO sources (path, destination, lastModified) VALUES (?, ?, ?)
|
|
132
|
+
RETURNING *
|
|
133
|
+
`))),
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* @callback SQLiteSourcesDelete
|
|
137
|
+
* @param {string} filePath
|
|
138
|
+
* @returns {SQLiteSource}
|
|
139
|
+
*/
|
|
140
|
+
|
|
141
|
+
delete: /** @type {{get: SQLiteSourcesDelete}} */ (/** @type {unknown} */ (database.prepare(`
|
|
142
|
+
DELETE FROM sources WHERE path = ? RETURNING *
|
|
143
|
+
`))),
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @callback SQLiteSourcesGet
|
|
147
|
+
* @param {string} path
|
|
148
|
+
* @returns {SQLiteSource}
|
|
149
|
+
*/
|
|
150
|
+
|
|
151
|
+
get: /** @type {{get: SQLiteSourcesGet}} */ (/** @type {unknown} */ (database.prepare(`
|
|
152
|
+
SELECT * FROM sources WHERE path = ?
|
|
153
|
+
`))),
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @callback SQLiteSourcesGetAll
|
|
157
|
+
* @returns {SQLiteSource[]}
|
|
158
|
+
*/
|
|
159
|
+
|
|
160
|
+
getAll: /** @type {{all: SQLiteSourcesGetAll}} */ (/** @type {unknown} */ (database.prepare(`
|
|
161
|
+
SELECT * FROM sources
|
|
162
|
+
`))),
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* @callback SQLiteSourcesUpdate
|
|
167
|
+
* @param {number} timestamp
|
|
168
|
+
* @param {string} filePath
|
|
169
|
+
* @returns {void}
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
update: /** @type {{get: SQLiteSourcesUpdate}} */ (/** @type {unknown} */ (database.prepare(`
|
|
173
|
+
UPDATE sources SET lastModified = ? WHERE path = ?
|
|
174
|
+
`))),
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
/* TARGETS */
|
|
178
|
+
target: {
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @callback SQLiteTargetCreate
|
|
182
|
+
* @param {string} path
|
|
183
|
+
* @param {string} dir
|
|
184
|
+
* @param {string} syntax
|
|
185
|
+
* @param {string} abstract
|
|
186
|
+
* @returns {SQLiteTarget}
|
|
187
|
+
*/
|
|
188
|
+
|
|
189
|
+
create: /** @type {{get: SQLiteTargetCreate}} */ (/** @type {unknown} */ (database.prepare(`
|
|
190
|
+
INSERT OR IGNORE INTO destinations (path, dir, syntax, stale, abstract)
|
|
191
|
+
VALUES (?, ?, ?, 1, ?)
|
|
192
|
+
RETURNING *
|
|
193
|
+
`))),
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* @callback SQLiteTargetDelete
|
|
198
|
+
* @param {string} targetFilePath
|
|
199
|
+
* @returns {void}
|
|
200
|
+
*/
|
|
201
|
+
|
|
202
|
+
delete: /** @type {{get: SQLiteTargetDelete}} */ (/** @type {unknown} */ (database.prepare(`
|
|
203
|
+
DELETE FROM destinations WHERE path = ?
|
|
204
|
+
`))),
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* @callback SQLiteTargetGet
|
|
208
|
+
* @param {string} targetFilePath
|
|
209
|
+
* @returns {SQLiteTarget}
|
|
210
|
+
*/
|
|
211
|
+
|
|
212
|
+
get: /** @type {{get: SQLiteTargetGet}} */ (/** @type {unknown} */ (database.prepare(`
|
|
213
|
+
WITH destination AS (
|
|
214
|
+
SELECT * FROM destinations
|
|
215
|
+
LEFT JOIN metadata ON destinations.path = metadata.destination
|
|
216
|
+
WHERE path = ?
|
|
217
|
+
)
|
|
218
|
+
SELECT destination.path, destination.dir, destination.syntax, destination.abstract, json_group_object(destination.label, destination.value) AS metadata
|
|
219
|
+
FROM destination
|
|
220
|
+
GROUP BY destination.path
|
|
221
|
+
`))),
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @callback SQLiteTargetGetMany
|
|
225
|
+
* @param {{ filter: JSONString, folder: string, recursivePath: string }} params
|
|
226
|
+
* @returns {SQLiteTarget[]}
|
|
227
|
+
*/
|
|
228
|
+
|
|
229
|
+
getMany: /** @type {{all: SQLiteTargetGetMany}} */ (/** @type {unknown} */ (database.prepare(`
|
|
230
|
+
WITH matches AS (
|
|
231
|
+
SELECT m.destination, COUNT(*) AS match_count
|
|
232
|
+
FROM metadata m
|
|
233
|
+
INNER JOIN json_each(:filter) j ON j.key = m.label AND j.value = m.value
|
|
234
|
+
GROUP BY m.destination
|
|
235
|
+
),
|
|
236
|
+
filter_count AS (
|
|
237
|
+
SELECT COUNT(*) AS total FROM json_each(:filter)
|
|
238
|
+
)
|
|
239
|
+
SELECT d.*, json_group_object(i.label, i.value) AS metadata
|
|
240
|
+
FROM destinations d
|
|
241
|
+
INNER JOIN metadata i ON d.path = i.destination
|
|
242
|
+
LEFT JOIN matches ON matches.destination = d.path
|
|
243
|
+
CROSS JOIN filter_count f
|
|
244
|
+
WHERE (
|
|
245
|
+
f.total = 0
|
|
246
|
+
OR matches.match_count = f.total
|
|
247
|
+
)
|
|
248
|
+
AND (
|
|
249
|
+
d.dir = :folder
|
|
250
|
+
OR d.dir LIKE :recursivePath
|
|
251
|
+
)
|
|
252
|
+
GROUP BY d.path
|
|
253
|
+
ORDER BY d.path ASC
|
|
254
|
+
`))),
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* @callback SQLiteTargetGetAll
|
|
259
|
+
* @returns {SQLiteTarget[]}
|
|
260
|
+
*/
|
|
261
|
+
|
|
262
|
+
getAll: /** @type {{all: SQLiteTargetGetAll}} */ (/** @type {unknown} */ (database.prepare(`
|
|
263
|
+
SELECT destinations.*, json_group_object(i.label, i.value) AS metadata
|
|
264
|
+
FROM destinations
|
|
265
|
+
LEFT JOIN metadata i ON destinations.path = i.destination
|
|
266
|
+
GROUP BY destinations.path
|
|
267
|
+
`))),
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* @callback SQLiteTargetGetAllStale
|
|
271
|
+
* @returns {SQLiteTarget[]}
|
|
272
|
+
*/
|
|
273
|
+
|
|
274
|
+
getAllStale: /** @type {{all: SQLiteTargetGetAllStale}} */ (/** @type {unknown} */ (database.prepare(`
|
|
275
|
+
WITH destination AS (
|
|
276
|
+
SELECT * FROM destinations
|
|
277
|
+
LEFT JOIN metadata ON destinations.path = metadata.destination
|
|
278
|
+
)
|
|
279
|
+
SELECT destination.path, destination.dir, destination.syntax, destination.abstract, json_group_object(destination.label, destination.value) AS metadata
|
|
280
|
+
FROM destination
|
|
281
|
+
WHERE stale = 1
|
|
282
|
+
GROUP BY destination.path
|
|
283
|
+
`))),
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* @callback SQLiteTargetMarkDescendentsStale
|
|
287
|
+
* @param {string} targetFilePathPattern
|
|
288
|
+
* @returns {SQLiteTarget[]}
|
|
289
|
+
*/
|
|
290
|
+
|
|
291
|
+
markDescendentsStale: /** @type {{all: SQLiteTargetMarkDescendentsStale}} */ (/** @type {unknown} */ (database.prepare(`
|
|
292
|
+
UPDATE destinations SET stale = 1 WHERE path LIKE ? RETURNING *
|
|
293
|
+
`))),
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* @callback SQLiteTargetMarkFresh
|
|
297
|
+
* @param {string} targetFilePath
|
|
298
|
+
* @returns {SQLiteTarget[]}
|
|
299
|
+
*/
|
|
300
|
+
|
|
301
|
+
markFresh: /** @type {{get: SQLiteTargetMarkFresh}} */ (/** @type {unknown} */ (database.prepare(`
|
|
302
|
+
UPDATE destinations SET stale = 0 WHERE path = ? RETURNING *
|
|
303
|
+
`))),
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @callback SQLiteTargetMarkStale
|
|
307
|
+
* @param {string} targetFilePath
|
|
308
|
+
* @returns {SQLiteTarget}
|
|
309
|
+
*/
|
|
310
|
+
|
|
311
|
+
markStale: /** @type {{get: SQLiteTargetMarkStale}} */ (/** @type {unknown} */ (database.prepare(`
|
|
312
|
+
UPDATE destinations SET stale = 1 WHERE path = ? RETURNING *
|
|
313
|
+
`))),
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* @callback SQLiteTargetUpdate
|
|
317
|
+
* @param {string} abstract
|
|
318
|
+
* @param {string} targetFilePath
|
|
319
|
+
* @returns {SQLiteTarget}
|
|
320
|
+
*/
|
|
321
|
+
update: /** @type {{get: SQLiteTargetUpdate}} */ (/** @type {unknown} */ (database.prepare(`
|
|
322
|
+
UPDATE destinations
|
|
323
|
+
SET abstract = ?
|
|
324
|
+
WHERE path = ?
|
|
325
|
+
`))),
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
/* DEPENDENCIES */
|
|
329
|
+
dependency: {
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* @callback SQLiteDependencyCreate
|
|
333
|
+
* @param {string} destination
|
|
334
|
+
* @param {string} property
|
|
335
|
+
* @param {string} dependent
|
|
336
|
+
* @returns {void}
|
|
337
|
+
*/
|
|
338
|
+
|
|
339
|
+
create: /** @type {{get: SQLiteDependencyCreate}} */ (/** @type {unknown} */ (database.prepare(`
|
|
340
|
+
INSERT OR IGNORE INTO dependencies (destination, property, dependent)
|
|
341
|
+
VALUES (?, ?, ?)
|
|
342
|
+
`))),
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* @callback SQLiteDependencyDeleteByTarget
|
|
346
|
+
* @param {string} targetFilePath
|
|
347
|
+
* @returns {SQLiteDependency[]}
|
|
348
|
+
*/
|
|
349
|
+
|
|
350
|
+
deleteByTarget: /** @type {{all: SQLiteDependencyDeleteByTarget}} */ (/** @type {unknown} */ (database.prepare(`
|
|
351
|
+
DELETE FROM dependencies WHERE destination = ? RETURNING dependent
|
|
352
|
+
`))),
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* @callback SQLiteDependencyGetAll
|
|
356
|
+
* @returns {SQLiteDependency[]}
|
|
357
|
+
*/
|
|
358
|
+
|
|
359
|
+
getAll: /** @type {{all: SQLiteDependencyGetAll}} */ (/** @type {unknown} */ (database.prepare(`
|
|
360
|
+
SELECT * FROM dependencies
|
|
361
|
+
`))),
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* @callback SQLiteDependencyGetByTarget
|
|
365
|
+
* @param {string} targetFilePath
|
|
366
|
+
* @returns {SQLiteDependency[]}
|
|
367
|
+
*/
|
|
368
|
+
|
|
369
|
+
getByTarget: /** @type {{all: SQLiteDependencyGetByTarget}} */ (/** @type {unknown} */ (database.prepare(`
|
|
370
|
+
SELECT * FROM dependencies WHERE destination = ?
|
|
371
|
+
`))),
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* @callback SQLiteDependencyGetByTargetAndProperty
|
|
375
|
+
* @param {string} targetFilePath
|
|
376
|
+
* @param {string} property
|
|
377
|
+
* @returns {SQLiteDependency[]}
|
|
378
|
+
*/
|
|
379
|
+
|
|
380
|
+
getByTargetAndProperty: /** @type {{all: SQLiteDependencyGetByTargetAndProperty}} */ (/** @type {unknown} */ (database.prepare(`
|
|
381
|
+
SELECT * FROM dependencies WHERE destination = ? AND property = ?
|
|
382
|
+
`))),
|
|
383
|
+
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
/* METADATA */
|
|
387
|
+
metadata: {
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* @callback SQLiteMetadataCreate
|
|
391
|
+
* @param {JSONString} metadataJSON
|
|
392
|
+
* @param {string} targetPath
|
|
393
|
+
* @returns {void}
|
|
394
|
+
*/
|
|
395
|
+
|
|
396
|
+
create: /** @type {{get: SQLiteMetadataCreate}} */ (/** @type {unknown} */ (database.prepare(`
|
|
397
|
+
INSERT OR REPLACE INTO metadata (label, value, type, destination)
|
|
398
|
+
SELECT
|
|
399
|
+
json_each.key,
|
|
400
|
+
json_each.value,
|
|
401
|
+
json_each.type,
|
|
402
|
+
?
|
|
403
|
+
FROM json_each(?);
|
|
404
|
+
`))),
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* @callback SQLiteMetadataDelete
|
|
408
|
+
* @param {string} targetFilePath
|
|
409
|
+
* @param {string} label
|
|
410
|
+
* @returns {void}
|
|
411
|
+
*/
|
|
412
|
+
|
|
413
|
+
delete: /** @type {{get: SQLiteMetadataDelete}} */ (/** @type {unknown} */ (database.prepare(`
|
|
414
|
+
DELETE FROM metadata WHERE destination = ? AND label = ?
|
|
415
|
+
`))),
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* @callback SQLiteMetadataDeleteByTarget
|
|
419
|
+
* @param {string} targetFilePath
|
|
420
|
+
* @returns {void}
|
|
421
|
+
*/
|
|
422
|
+
|
|
423
|
+
deleteByTarget: /** @type {{all: SQLiteMetadataDeleteByTarget}} */ (/** @type {unknown} */ (database.prepare(`
|
|
424
|
+
DELETE FROM metadata WHERE destination = ?
|
|
425
|
+
`)))
|
|
426
|
+
},
|
|
427
|
+
|
|
428
|
+
/* SETTINGS */
|
|
429
|
+
settings: {
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* @callback SQLiteSettingsCreate
|
|
433
|
+
* @param {string} targetFilePath
|
|
434
|
+
* @param {string} label
|
|
435
|
+
* @param {string} value
|
|
436
|
+
* @param {string} sourceFilePath
|
|
437
|
+
* @returns {SQLiteSetting[]}
|
|
438
|
+
*/
|
|
439
|
+
|
|
440
|
+
create: /** @type {{get: SQLiteSettingsCreate}} */ (/** @type {unknown} */ (database.prepare(`
|
|
441
|
+
INSERT OR IGNORE INTO settings (destination, label, value, source) VALUES (?, ?, ?, ?) RETURNING *
|
|
442
|
+
`))),
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* @callback SQLiteSettingsDelete
|
|
446
|
+
* @param {string} sourceFilePath
|
|
447
|
+
* @returns {SQLiteSetting[]}
|
|
448
|
+
*/
|
|
449
|
+
|
|
450
|
+
delete: /** @type {{all: SQLiteSettingsDelete}} */ (/** @type {unknown} */ (database.prepare(`
|
|
451
|
+
DELETE FROM settings WHERE source = ?
|
|
452
|
+
RETURNING *
|
|
453
|
+
`))),
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* @callback SQLiteSettingsGet
|
|
457
|
+
* @param {string} label
|
|
458
|
+
* @param {string} targetFilePath
|
|
459
|
+
* @returns {SQLiteSettings[]}
|
|
460
|
+
*/
|
|
461
|
+
|
|
462
|
+
get: /** @type {{all: SQLiteSettingsGet}} */ (/** @type {unknown} */ (database.prepare(`
|
|
463
|
+
SELECT * FROM settings WHERE label = ? AND destination = ?
|
|
464
|
+
`))),
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* @callback SQLiteSettingsGetAll
|
|
468
|
+
* @returns {SQLiteSetting[]}
|
|
469
|
+
*/
|
|
470
|
+
|
|
471
|
+
getAll: /** @type {{all: SQLiteSettingsGetAll}} */ (/** @type {unknown} */ (database.prepare(`
|
|
472
|
+
SELECT * FROM settings
|
|
473
|
+
`))),
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* @callback SQLiteSettingsGetByFolder
|
|
477
|
+
* @param {JSONString} JSONarray - JSON string of an array of target file paths
|
|
478
|
+
* @returns {SQLiteSetting[]}
|
|
479
|
+
*/
|
|
480
|
+
|
|
481
|
+
getByFolder: /** @type {{all: SQLiteSettingsGetByFolder}} */ (/** @type {unknown} */ (database.prepare(`
|
|
482
|
+
SELECT label, json_group_array(json_object('destination', destination, 'value', value)) AS settings
|
|
483
|
+
FROM settings
|
|
484
|
+
WHERE destination IN (SELECT value FROM json_each(?))
|
|
485
|
+
GROUP BY label
|
|
486
|
+
`))),
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* @callback SQLiteSettingsUpdate
|
|
490
|
+
* @param {string} value
|
|
491
|
+
* @param {string} targetFilePath
|
|
492
|
+
* @param {string} label
|
|
493
|
+
* @returns {SQLiteSettings}
|
|
494
|
+
*/
|
|
495
|
+
|
|
496
|
+
update: /** @type {{get: SQLiteSettingsUpdate}} */ (/** @type {unknown} */ (database.prepare(`
|
|
497
|
+
UPDATE settings SET value = ? WHERE destination = ? AND label = ? RETURNING *
|
|
498
|
+
`)))
|
|
499
|
+
},
|
|
500
|
+
|
|
501
|
+
/* URLS */
|
|
502
|
+
url: {
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* @callback SQLiteURLCreate
|
|
506
|
+
* @param {string} url
|
|
507
|
+
* @param {string} data
|
|
508
|
+
* @returns {SQLiteURL}
|
|
509
|
+
*/
|
|
510
|
+
|
|
511
|
+
create: /** @type {{get: SQLiteURLCreate}} */ (/** @type {unknown} */ (database.prepare(`
|
|
512
|
+
INSERT OR IGNORE INTO urls (url, data) VALUES (?, ?) RETURNING *
|
|
513
|
+
`))),
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* @callback SQLiteURLGet
|
|
517
|
+
* @param {string} url
|
|
518
|
+
* @returns {SQLiteURL}
|
|
519
|
+
*/
|
|
520
|
+
|
|
521
|
+
get: /** @type {{get: SQLiteURLGet}} */ (/** @type {unknown} */ (database.prepare(`
|
|
522
|
+
SELECT data FROM urls WHERE url = ?
|
|
523
|
+
`)))
|
|
157
524
|
}
|
|
158
525
|
}
|
|
526
|
+
}
|
|
159
527
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
528
|
+
/**
|
|
529
|
+
* @param {string} databasePath
|
|
530
|
+
*/
|
|
531
|
+
function createDatabase(databasePath = ".votive.db") {
|
|
532
|
+
|
|
533
|
+
const database = loadDB(databasePath)
|
|
534
|
+
createTables(database)
|
|
535
|
+
const prepared = prepareStatements(database)
|
|
536
|
+
|
|
537
|
+
const queries = {
|
|
538
|
+
begin: () => database.exec("BEGIN TRANSACTION"),
|
|
539
|
+
commit: () => database.exec("COMMIT"),
|
|
540
|
+
restore: () => database.exec("RESTORE"),
|
|
541
|
+
raw: database,
|
|
542
|
+
|
|
543
|
+
/** @param {object[]} sources */
|
|
544
|
+
async saveDB(sources) {
|
|
545
|
+
/*
|
|
546
|
+
FIXME This seems to throw an error sometimes if the backup
|
|
547
|
+
runs too quickly after writing, which maybe happens when
|
|
548
|
+
Votive runs with no changes. To guard against this, I check
|
|
549
|
+
to see if any sources have changed. With no source changes,
|
|
550
|
+
the backup should theoretically be unnecessary.
|
|
551
|
+
*/
|
|
552
|
+
if (database.location() || !sources.length) return // Only save if running in memory
|
|
553
|
+
await backup(database, databasePath)
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
source: {
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* @param {string} source - Source file path.
|
|
560
|
+
* @param {string} target - Destination file path.
|
|
561
|
+
* @param {number} lastModified - Source file date last modified.
|
|
562
|
+
*/
|
|
563
|
+
create(source, target, lastModified) {
|
|
564
|
+
const created = prepared.source.create.get(source, target, lastModified)
|
|
565
|
+
prepared.target.markDescendentsStale.all("%")
|
|
566
|
+
return created
|
|
567
|
+
},
|
|
568
|
+
|
|
569
|
+
/** @param {string} filePath */
|
|
570
|
+
delete(filePath) {
|
|
571
|
+
const deletedSource = prepared.source.delete.get(filePath)
|
|
572
|
+
prepared.settings.delete.all(filePath)
|
|
573
|
+
prepared.dependency.deleteByTarget.all(deletedSource.destination)
|
|
574
|
+
.forEach(({ dependent }) => {
|
|
575
|
+
prepared.dependency.markStale.get(dependent)
|
|
576
|
+
})
|
|
577
|
+
prepared.metadata.deleteByTarget.all(deletedSource.destination)
|
|
578
|
+
prepared.target.delete.get(deletedSource.destination)
|
|
579
|
+
},
|
|
580
|
+
|
|
581
|
+
getAll() {
|
|
582
|
+
return prepared.source.getAll.all()
|
|
583
|
+
},
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* @param {string} filePath
|
|
587
|
+
*/
|
|
588
|
+
get(filePath) {
|
|
589
|
+
return prepared.source.get.get(filePath)
|
|
590
|
+
},
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* @param {string} source
|
|
594
|
+
* @param {number} timestamp
|
|
595
|
+
*/
|
|
596
|
+
updateTimestamp(source, timestamp) {
|
|
597
|
+
return prepared.source.update.get(timestamp, source)
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
|
|
601
|
+
setting: {
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* @param {string} folder
|
|
605
|
+
* @param {string} key
|
|
606
|
+
* @param {string} value
|
|
607
|
+
* @param {string} [source]
|
|
608
|
+
*/
|
|
609
|
+
create(folder, key, value, source = "") {
|
|
610
|
+
const type = typeof value
|
|
611
|
+
const safeValue = type === "object"
|
|
612
|
+
? JSON.stringify(value)
|
|
613
|
+
: value
|
|
163
614
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
615
|
+
const descendents = folder === ""
|
|
616
|
+
? "%"
|
|
617
|
+
: `${folder}/%`
|
|
168
618
|
|
|
169
|
-
|
|
170
|
-
return databaseSync.prepare(`
|
|
171
|
-
SELECT * FROM destinations d
|
|
172
|
-
LEFT JOIN metadata m ON d.path = m.destination
|
|
173
|
-
WHERE d.path = ?
|
|
174
|
-
`)
|
|
175
|
-
}
|
|
619
|
+
prepared.target.markDescendentsStale.all(descendents)
|
|
176
620
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
SELECT * FROM destinations
|
|
180
|
-
WHERE path = ?
|
|
181
|
-
`)
|
|
182
|
-
}
|
|
621
|
+
return prepared.settings.create.get(folder, key, safeValue, source)
|
|
622
|
+
},
|
|
183
623
|
|
|
184
|
-
function castMetadata({ label, value, type }) {
|
|
185
|
-
return [label,
|
|
186
|
-
type === "undefined"
|
|
187
|
-
? undefined
|
|
188
|
-
: type === "boolean"
|
|
189
|
-
? Boolean(value)
|
|
190
|
-
: value
|
|
191
|
-
]
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function parseMetadatas(metadatas) {
|
|
195
|
-
return Object.fromEntries(
|
|
196
|
-
metadatas.map(castMetadata)
|
|
197
|
-
)
|
|
198
|
-
}
|
|
199
624
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
database.getDestinationIndependently = (path) => {
|
|
204
|
-
// TODO: Could potentially speed up the next two db queries by using the tag store
|
|
625
|
+
/** @param {string} source */
|
|
626
|
+
deleteBySource(source) {
|
|
627
|
+
const deletedSettings = prepared.settings.delete.all(source)
|
|
205
628
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
629
|
+
deletedSettings.forEach(setting => {
|
|
630
|
+
prepared.dependency.markStale.get(setting.destination)
|
|
631
|
+
prepared.dependency.markDescendentTargetsStale.all(setting.destination + "/%")
|
|
632
|
+
})
|
|
633
|
+
},
|
|
634
|
+
|
|
635
|
+
getAll() {
|
|
636
|
+
return prepared.settings.getAll.all()
|
|
637
|
+
},
|
|
638
|
+
|
|
639
|
+
/** @param {string} folder */
|
|
640
|
+
getByFolder(folder) {
|
|
641
|
+
const segments = ["", ...(folder).split(path.sep)
|
|
642
|
+
.map((_, index, array) => `${array.slice(0, index + 1).join(path.sep)}`)
|
|
643
|
+
.filter(a => a)]
|
|
644
|
+
|
|
645
|
+
const settings = prepared.settings.getByFolder.all(JSON.stringify(segments))
|
|
646
|
+
const grouped = Object.fromEntries(
|
|
647
|
+
prepared.settings.getByFolder.all(JSON.stringify(segments)).
|
|
648
|
+
map(({ label, settings }) => {
|
|
649
|
+
const targets = Object.groupBy(JSON.parse(settings), ({ destination }) => destination)
|
|
650
|
+
for (const key in targets) {
|
|
651
|
+
targets[key] = targets[key].map(({ destination, value }) => value)
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
return [label, targets]
|
|
655
|
+
})
|
|
656
|
+
)
|
|
209
657
|
|
|
210
|
-
|
|
658
|
+
return grouped
|
|
659
|
+
}
|
|
660
|
+
},
|
|
211
661
|
|
|
212
|
-
|
|
662
|
+
dependency: {
|
|
213
663
|
|
|
214
|
-
const metadata = parseMetadatas(metadatas)
|
|
215
664
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
dir: first.dir,
|
|
220
|
-
syntax: first.syntax,
|
|
221
|
-
metadata
|
|
222
|
-
}
|
|
665
|
+
getAll() {
|
|
666
|
+
return prepared.dependency.getAll.all()
|
|
667
|
+
},
|
|
223
668
|
|
|
224
|
-
|
|
225
|
-
|
|
669
|
+
/**
|
|
670
|
+
* @param {object} dependencyFile
|
|
671
|
+
* @param {string} dependencyKey
|
|
672
|
+
* @param {any} dependencyValue
|
|
673
|
+
* @param {string} dependencyPath
|
|
674
|
+
* @param {string} dependentPath
|
|
675
|
+
*/
|
|
676
|
+
track(dependencyFile, dependencyKey, dependencyValue, dependencyPath, dependentPath) {
|
|
677
|
+
Object.defineProperty(dependencyFile, dependencyKey, {
|
|
678
|
+
enumerable: true,
|
|
679
|
+
get() {
|
|
680
|
+
prepared.dependency.create.get(dependencyPath, dependencyKey, dependentPath)
|
|
681
|
+
return dependencyValue
|
|
682
|
+
}
|
|
683
|
+
})
|
|
684
|
+
}
|
|
685
|
+
},
|
|
226
686
|
|
|
227
|
-
|
|
228
|
-
const getDependency = databaseSync.prepare("SELECT * FROM dependencies WHERE destination = ? AND property = ?")
|
|
229
|
-
/**
|
|
230
|
-
* @param {string} path
|
|
231
|
-
* @param {string} dependent
|
|
232
|
-
*/
|
|
233
|
-
database.getDestinationDependently = (path, dependent) => {
|
|
234
|
-
const result = database.getDestinationIndependently(path)
|
|
687
|
+
target: {
|
|
235
688
|
|
|
236
|
-
|
|
689
|
+
/**
|
|
690
|
+
* @param {string} filePath
|
|
691
|
+
* @returns {TargetOutput}
|
|
692
|
+
*/
|
|
693
|
+
get(filePath) {
|
|
694
|
+
const target = prepared.target.get.get(filePath)
|
|
237
695
|
|
|
238
|
-
|
|
696
|
+
if (!target) return
|
|
239
697
|
|
|
240
|
-
|
|
241
|
-
enumerable: true,
|
|
242
|
-
get() {
|
|
243
|
-
createDependency.get(path, "abstract", dependent)
|
|
244
|
-
return abstract
|
|
245
|
-
}
|
|
246
|
-
})
|
|
698
|
+
const { metadata, abstract, ...rest } = target
|
|
247
699
|
|
|
248
|
-
|
|
700
|
+
if (!abstract) return
|
|
249
701
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
return metadata[key]
|
|
702
|
+
/** @type {TargetOutput} */
|
|
703
|
+
const copy = {
|
|
704
|
+
abstract: coerceJSON(abstract),
|
|
705
|
+
metadata: coerceJSON(metadata),
|
|
706
|
+
...rest
|
|
256
707
|
}
|
|
257
|
-
})
|
|
258
|
-
})
|
|
259
708
|
|
|
709
|
+
return copy
|
|
710
|
+
},
|
|
260
711
|
|
|
261
|
-
return copy
|
|
262
|
-
}
|
|
263
712
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
if (!pv || !pv[cv.path]) {
|
|
270
|
-
pv[cv.path] = {
|
|
271
|
-
dir: cv.dir,
|
|
272
|
-
path: cv.path,
|
|
273
|
-
syntax: cv.syntax,
|
|
274
|
-
metadata: {}
|
|
275
|
-
}
|
|
713
|
+
/**
|
|
714
|
+
* @returns {TargetOutput[]}
|
|
715
|
+
*/
|
|
716
|
+
getAll() {
|
|
717
|
+
const targets = prepared.target.getAll.all()
|
|
276
718
|
|
|
277
|
-
|
|
278
|
-
{
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
719
|
+
return targets.map(({ metadata, abstract, ...rest }) => {
|
|
720
|
+
return {
|
|
721
|
+
abstract: coerceJSON(abstract),
|
|
722
|
+
metadata: coerceJSON(metadata),
|
|
723
|
+
...rest
|
|
724
|
+
}
|
|
725
|
+
})
|
|
726
|
+
},
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* @returns {TargetOutput[]}
|
|
730
|
+
*/
|
|
731
|
+
getStale() {
|
|
732
|
+
const targets = prepared.target.getAllStale.all()
|
|
733
|
+
|
|
734
|
+
return targets.map(({ metadata, abstract, ...rest }) => {
|
|
735
|
+
return {
|
|
736
|
+
abstract: coerceJSON(abstract),
|
|
737
|
+
metadata: coerceJSON(metadata),
|
|
738
|
+
...rest
|
|
284
739
|
}
|
|
740
|
+
})
|
|
741
|
+
},
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* @param {string} filePath
|
|
745
|
+
* @param {string} dependent
|
|
746
|
+
*/
|
|
747
|
+
getWithTrackers(filePath, dependent) {
|
|
748
|
+
const target = queries.target.get(filePath)
|
|
749
|
+
|
|
750
|
+
const trackedTarget = { metadata: {} }
|
|
751
|
+
|
|
752
|
+
queries.dependency.track(
|
|
753
|
+
trackedTarget,
|
|
754
|
+
"abstract",
|
|
755
|
+
target.abstract,
|
|
756
|
+
filePath,
|
|
757
|
+
dependent
|
|
285
758
|
)
|
|
286
|
-
}
|
|
287
759
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
760
|
+
for (const key in target.metadata) {
|
|
761
|
+
queries.dependency.track(
|
|
762
|
+
trackedTarget.metadata,
|
|
763
|
+
key,
|
|
764
|
+
target.metadata[key],
|
|
765
|
+
filePath,
|
|
766
|
+
dependent
|
|
767
|
+
)
|
|
295
768
|
}
|
|
296
|
-
)
|
|
297
769
|
|
|
298
|
-
|
|
299
|
-
|
|
770
|
+
return target
|
|
771
|
+
},
|
|
300
772
|
|
|
301
|
-
|
|
302
|
-
|
|
773
|
+
/**
|
|
774
|
+
* @typedef {object} TargetGetManyWithTrackersParams
|
|
775
|
+
* @property {string | undefined} [folder]
|
|
776
|
+
* @property {boolean | undefined} [recursive]
|
|
777
|
+
* @property {string | undefined} [dependent]
|
|
778
|
+
* @property {Record<string, string> | undefined} [query]
|
|
779
|
+
*/
|
|
303
780
|
|
|
304
|
-
|
|
781
|
+
/** @param {TargetGetManyWithTrackersParams | undefined} params */
|
|
782
|
+
getManyWithTrackers(params) {
|
|
783
|
+
const { folder = "%", recursive, dependent, query = {} } = params
|
|
305
784
|
|
|
306
|
-
|
|
307
|
-
* @param {string} path
|
|
308
|
-
*/
|
|
309
|
-
database.getSource = (path) => {
|
|
310
|
-
const source = selectSource.get(path)
|
|
311
|
-
return source
|
|
312
|
-
}
|
|
785
|
+
const recursivePath = [folder, "%"].filter(a => a).join("/")
|
|
313
786
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const deleteSource = databaseSync.prepare(`DELETE FROM sources WHERE path = ? RETURNING *`)
|
|
320
|
-
const deleteAllDependenciesByDestination = databaseSync.prepare(`DELETE FROM dependencies WHERE destination = ? RETURNING dependent`)
|
|
321
|
-
const deleteAllMetadataByDestination = databaseSync.prepare(`DELETE FROM metadata WHERE destination = ?`)
|
|
322
|
-
const deleteMetadata = databaseSync.prepare(`DELETE FROM metadata WHERE destination = ? AND label = ?`)
|
|
323
|
-
const deleteDestination = databaseSync.prepare(`DELETE FROM destinations WHERE path = ?`)
|
|
324
|
-
|
|
325
|
-
/** @param {string} path */
|
|
326
|
-
database.deleteSource = (path) => {
|
|
327
|
-
const deleted = deleteSource.all(path)
|
|
328
|
-
deleted.forEach(source => {
|
|
329
|
-
database.deleteSettings(String(source.path))
|
|
330
|
-
getDependenciesByDestination.all(source.destination)
|
|
331
|
-
deleteAllDependenciesByDestination.all(source.destination)
|
|
332
|
-
.forEach(({ dependent }) => staleDepen.get(dependent))
|
|
333
|
-
deleteAllMetadataByDestination.all(source.destination)
|
|
334
|
-
deleteDestination.get(source.destination)
|
|
335
|
-
})
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// TODO: DELETE SETTING
|
|
787
|
+
const many = prepared.target.getMany
|
|
788
|
+
.all({ folder, recursivePath: recursive ? recursivePath : folder, filter: JSON.stringify(query) })
|
|
789
|
+
.map(({ abstract, metadata, ...rest }) => {
|
|
339
790
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
791
|
+
const trackedTarget = {
|
|
792
|
+
abstract: coerceJSON(abstract),
|
|
793
|
+
metadata: coerceJSON(metadata),
|
|
794
|
+
...rest
|
|
795
|
+
}
|
|
343
796
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
abstract: JSON.parse(value.abstract),
|
|
362
|
-
metadata: Object.fromEntries(
|
|
363
|
-
array.slice(index)
|
|
364
|
-
.filter(following => following.path === value.path)
|
|
365
|
-
.map(succeeding => {
|
|
366
|
-
return reconstituteMetadata(succeeding)
|
|
367
|
-
})
|
|
368
|
-
)
|
|
797
|
+
queries.dependency.track(
|
|
798
|
+
trackedTarget,
|
|
799
|
+
"abstract",
|
|
800
|
+
trackedTarget.abstract,
|
|
801
|
+
trackedTarget.path,
|
|
802
|
+
dependent
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
for (const key in trackedTarget.metadata) {
|
|
806
|
+
queries.dependency.track(
|
|
807
|
+
trackedTarget.metadata,
|
|
808
|
+
key,
|
|
809
|
+
trackedTarget.metadata[key],
|
|
810
|
+
trackedTarget.path,
|
|
811
|
+
dependent
|
|
812
|
+
)
|
|
813
|
+
}
|
|
369
814
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
}).filter(a => a)
|
|
373
|
-
|
|
374
|
-
function reconstituteMetadata({ label, value, type }) {
|
|
375
|
-
const originalValue = type === "undefined"
|
|
376
|
-
? undefined
|
|
377
|
-
: type === "string"
|
|
378
|
-
? String(value)
|
|
379
|
-
: type === "boolean"
|
|
380
|
-
? Boolean(value)
|
|
381
|
-
: typeof value !== "string"
|
|
382
|
-
? value
|
|
383
|
-
: JSON.parse(value)
|
|
384
|
-
return [label, originalValue]
|
|
385
|
-
}
|
|
815
|
+
return trackedTarget
|
|
816
|
+
})
|
|
386
817
|
|
|
387
|
-
return grouped
|
|
388
|
-
}
|
|
389
818
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
819
|
+
many.sort((a, b) => {
|
|
820
|
+
if (a.metadata.date) {
|
|
821
|
+
if (b.metadata.date) {
|
|
822
|
+
return (new Date(b.metadata.date)).getTime() - (new Date(a.metadata.date)).getTime()
|
|
823
|
+
} else {
|
|
824
|
+
return -1
|
|
825
|
+
}
|
|
826
|
+
} else {
|
|
827
|
+
return 1
|
|
828
|
+
}
|
|
829
|
+
})
|
|
394
830
|
|
|
395
|
-
|
|
831
|
+
return many
|
|
832
|
+
},
|
|
396
833
|
|
|
397
|
-
// WRITE AN UPDATE SETTINGS FUNCTION FOR WHEN A SOURCE CHANGES
|
|
398
834
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
}
|
|
835
|
+
/**
|
|
836
|
+
* @param {TargetInput} target
|
|
837
|
+
*/
|
|
838
|
+
create(target) {
|
|
839
|
+
const dir = target.path && splitURL(target.path)
|
|
840
|
+
const ext = path.extname(target.path)
|
|
841
|
+
const relativePath = path.relative("", target.path).toLowerCase()
|
|
407
842
|
|
|
408
|
-
|
|
409
|
-
* @param {string} destinationFolder
|
|
410
|
-
* @param {string} key
|
|
411
|
-
* @param {string} value
|
|
412
|
-
* @param {string} [source]
|
|
413
|
-
* @example
|
|
414
|
-
* setSetting("", "theme", "summer", "settings.md")
|
|
415
|
-
* setSetting("data", "format", "csv", "/config.yaml")
|
|
416
|
-
*/
|
|
417
|
-
database.setSetting = (destinationFolder, key, value, source = "") => {
|
|
418
|
-
const extant = selectSetting.get(key, destinationFolder)
|
|
843
|
+
const extant = queries.target.get(target.path)
|
|
419
844
|
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
845
|
+
if (!extant) {
|
|
846
|
+
const created = prepared.target.create.get(
|
|
847
|
+
relativePath,
|
|
848
|
+
dir,
|
|
849
|
+
ext,
|
|
850
|
+
JSON.stringify(target.abstract)
|
|
851
|
+
)
|
|
852
|
+
prepared.metadata.create.get(relativePath, JSON.stringify(target.metadata))
|
|
853
|
+
return created
|
|
854
|
+
}
|
|
424
855
|
|
|
425
|
-
|
|
426
|
-
if (extant.value === safeValue) return
|
|
427
|
-
updateSetting.get(safeValue, destinationFolder, key)
|
|
856
|
+
const keys = Object.keys(target.metadata)
|
|
428
857
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
: staleDescendents.all(`${destinationFolder}/%`)
|
|
432
|
-
}
|
|
858
|
+
for (const key in target.metadata) {
|
|
859
|
+
if (target.metadata[key] !== extant.metadata[key]) {
|
|
433
860
|
|
|
861
|
+
// TODO use triggers to mark dependencies as stale
|
|
862
|
+
prepared.metadata.create.get(relativePath, JSON.stringify({
|
|
863
|
+
[key]: target.metadata[key]
|
|
864
|
+
}))
|
|
434
865
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
866
|
+
// TODO delete dependencies after marking stale
|
|
867
|
+
prepared.dependency
|
|
868
|
+
.getByTargetAndProperty
|
|
869
|
+
.all(relativePath, key)
|
|
870
|
+
.forEach(dependency => {
|
|
871
|
+
prepared.target.markStale.get(dependency.dependent)
|
|
872
|
+
})
|
|
873
|
+
}
|
|
874
|
+
}
|
|
441
875
|
|
|
442
|
-
|
|
443
|
-
|
|
876
|
+
for (const key in extant.metadata) {
|
|
877
|
+
if (!target.metadata[key]) {
|
|
878
|
+
prepared.metadata.delete.get(relativePath, key)
|
|
444
879
|
|
|
445
|
-
|
|
880
|
+
// TODO delete dependencies after marking stale
|
|
881
|
+
prepared.dependency
|
|
882
|
+
.getByTargetAndProperty
|
|
883
|
+
.all(relativePath, key)
|
|
884
|
+
.forEach(dependency => {
|
|
885
|
+
prepared.target.markStale.get(dependency.dependent)
|
|
886
|
+
})
|
|
887
|
+
}
|
|
888
|
+
}
|
|
446
889
|
|
|
447
|
-
|
|
448
|
-
records.sort((a, b) => a.length - b.length)
|
|
449
|
-
.forEach(record => {
|
|
450
|
-
if (!grouped[record.label]) grouped[record.label] = [record.value]
|
|
451
|
-
else grouped[record.label].push(record.value)
|
|
452
|
-
})
|
|
890
|
+
const changedAbstract = JSON.stringify(target.abstract) !== JSON.stringify(extant.abstract)
|
|
453
891
|
|
|
454
|
-
|
|
455
|
-
|
|
892
|
+
if (changedAbstract) {
|
|
893
|
+
prepared.target.update.get(JSON.stringify(target.abstract), relativePath)
|
|
456
894
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
])
|
|
465
|
-
)
|
|
466
|
-
}
|
|
895
|
+
prepared.dependency
|
|
896
|
+
.getByTargetAndProperty
|
|
897
|
+
.all(relativePath, "abstract")
|
|
898
|
+
.forEach(dependency => {
|
|
899
|
+
prepared.target.markStale.get(dependency.dependent)
|
|
900
|
+
})
|
|
901
|
+
}
|
|
467
902
|
|
|
468
|
-
|
|
469
|
-
|
|
903
|
+
return extant
|
|
904
|
+
},
|
|
470
905
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
906
|
+
/** @param {string} filePath */
|
|
907
|
+
markFresh(filePath) {
|
|
908
|
+
return prepared.target.markFresh.get(filePath)
|
|
909
|
+
}
|
|
910
|
+
},
|
|
911
|
+
|
|
912
|
+
url: {
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* @param {string} url
|
|
916
|
+
* @param {string} data
|
|
917
|
+
* @param {string} destination
|
|
918
|
+
*/
|
|
919
|
+
create(url, data, destination) {
|
|
920
|
+
prepared.url.create.get(url, JSON.stringify(data))
|
|
921
|
+
},
|
|
922
|
+
|
|
923
|
+
/** @param {string} url */
|
|
924
|
+
get(url) {
|
|
925
|
+
const { data } = prepared.url.get.get(url) || {}
|
|
926
|
+
if (!data) return
|
|
927
|
+
return JSON.parse(data)
|
|
928
|
+
}
|
|
929
|
+
}
|
|
481
930
|
|
|
482
|
-
/**
|
|
483
|
-
* @param {string} url
|
|
484
|
-
*/
|
|
485
|
-
database.getURL = (url) => {
|
|
486
|
-
const { data } = getURL.get(url) || {}
|
|
487
|
-
if(!data) return
|
|
488
|
-
return JSON.parse(data)
|
|
489
931
|
}
|
|
490
932
|
|
|
491
|
-
return
|
|
933
|
+
return Object.freeze(queries)
|
|
492
934
|
}
|
|
493
935
|
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
936
|
+
/** @param {DatabaseSync} databaseSync */
|
|
937
|
+
function createTables(databaseSync) {
|
|
938
|
+
databaseSync.exec(`
|
|
939
|
+
CREATE TABLE IF NOT EXISTS sources (
|
|
940
|
+
id INTEGER PRIMARY KEY,
|
|
941
|
+
destination STRING,
|
|
942
|
+
path STRING,
|
|
943
|
+
lastModified INTEGER
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
CREATE TABLE IF NOT EXISTS dependencies (
|
|
947
|
+
key INTEGER PRIMARY KEY,
|
|
948
|
+
destination STRING NOT NULL,
|
|
949
|
+
property STRING NOT NULL,
|
|
950
|
+
dependent STRING NOT NULL,
|
|
951
|
+
UNIQUE(destination, property, dependent)
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
CREATE TABLE IF NOT EXISTS destinations (
|
|
955
|
+
key INTEGER PRIMARY KEY,
|
|
956
|
+
path STRING UNIQUE,
|
|
957
|
+
dir TEXT,
|
|
958
|
+
syntax TEXT,
|
|
959
|
+
stale INTEGER,
|
|
960
|
+
abstract STRING
|
|
961
|
+
);
|
|
962
|
+
|
|
963
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
964
|
+
id INTEGER PRIMARY KEY,
|
|
965
|
+
destination STRING,
|
|
966
|
+
label STRING,
|
|
967
|
+
value STRING,
|
|
968
|
+
type STRING,
|
|
969
|
+
UNIQUE(destination, label)
|
|
970
|
+
);
|
|
971
|
+
|
|
972
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
973
|
+
id INTEGER PRIMARY KEY,
|
|
974
|
+
destination STRING,
|
|
975
|
+
source STRING,
|
|
976
|
+
label STRING,
|
|
977
|
+
value STRING,
|
|
978
|
+
UNIQUE(destination, label, value, source)
|
|
979
|
+
);
|
|
980
|
+
|
|
981
|
+
CREATE TABLE IF NOT EXISTS urls (
|
|
982
|
+
url STRING PRIMARY KEY,
|
|
983
|
+
data STRING
|
|
984
|
+
);
|
|
985
|
+
|
|
986
|
+
`)
|
|
987
|
+
}
|
|
543
988
|
|
|
544
989
|
export default createDatabase
|