votive 0.2.0 → 0.3.0
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 -11
- package/lib/bundle.js +26 -119
- package/lib/cleanDeletions.js +32 -0
- package/lib/createDatabase.js +373 -931
- package/lib/readAbstracts.js +9 -12
- package/lib/readFolders.js +15 -18
- package/lib/readSources.js +41 -176
- package/lib/runJobs.js +8 -26
- package/lib/utils/index.js +5 -13
- package/lib/writeDestinations.js +11 -31
- package/package.json +1 -2
- package/tests/index.js +245 -302
package/lib/createDatabase.js
CHANGED
|
@@ -1,52 +1,13 @@
|
|
|
1
1
|
import { DatabaseSync, backup } from "node:sqlite"
|
|
2
2
|
import { splitURL, checkFile } from "./utils/index.js"
|
|
3
|
+
import { statSync } from "node:fs"
|
|
3
4
|
import path from "node:path"
|
|
4
5
|
|
|
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
|
-
*/
|
|
18
|
-
|
|
19
|
-
/**
|
|
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
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
6
|
/**
|
|
31
|
-
* @typedef {
|
|
32
|
-
* @property {string} path
|
|
33
|
-
* @property {Abstract} abstract
|
|
34
|
-
* @property {Metadata} metadata
|
|
7
|
+
* @typedef {ReturnType<createDatabase>} Database
|
|
35
8
|
*/
|
|
36
9
|
|
|
37
10
|
|
|
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
|
-
}
|
|
49
|
-
|
|
50
11
|
/** @param {string} dbPath */
|
|
51
12
|
function loadDB(dbPath) {
|
|
52
13
|
if (checkFile(dbPath)) return new DatabaseSync(dbPath)
|
|
@@ -54,952 +15,433 @@ function loadDB(dbPath) {
|
|
|
54
15
|
}
|
|
55
16
|
|
|
56
17
|
|
|
57
|
-
/** @param {
|
|
58
|
-
function
|
|
18
|
+
/** @param {string} dbPath */
|
|
19
|
+
function createDatabase(dbPath = ".votive.db") {
|
|
20
|
+
const databaseSync = loadDB(dbPath)
|
|
59
21
|
|
|
60
|
-
const
|
|
22
|
+
const store = databaseSync.createTagStore()
|
|
61
23
|
|
|
62
|
-
|
|
63
|
-
* @typedef {string} JSONString - A string that is actually JSON.
|
|
64
|
-
*/
|
|
24
|
+
databaseSync.exec(sqlCreateTables)
|
|
65
25
|
|
|
66
|
-
|
|
67
|
-
* @typedef {object} SQLiteSource
|
|
68
|
-
* @property {number} id
|
|
69
|
-
* @property {string} destination
|
|
70
|
-
* @property {string} filePath
|
|
71
|
-
* @property {number} lastModified
|
|
72
|
-
*/
|
|
26
|
+
const database = {}
|
|
73
27
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
* @property {string} dir
|
|
79
|
-
* @property {string} syntax
|
|
80
|
-
* @property {number} stale
|
|
81
|
-
* @property {JSONString} abstract
|
|
82
|
-
* @property {JSONString} metadata
|
|
83
|
-
*/
|
|
28
|
+
database.saveDB = async () => {
|
|
29
|
+
if (databaseSync.location()) return // Only save if running in memory
|
|
30
|
+
await backup(databaseSync, dbPath)
|
|
31
|
+
}
|
|
84
32
|
|
|
85
33
|
/**
|
|
86
|
-
* @
|
|
87
|
-
* @
|
|
88
|
-
* @property {string} destination
|
|
89
|
-
* @property {string} property
|
|
90
|
-
* @property {string} dependent
|
|
34
|
+
* @param {string} folderPath
|
|
35
|
+
* @param {string} urlPath
|
|
91
36
|
*/
|
|
37
|
+
database.createFolder = (folderPath, urlPath) => {
|
|
38
|
+
return store.get`INSERT INTO folders (folderPath, urlPath) VALUES :${folderPath} :${urlPath} RETURNING *`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const createSource = databaseSync.prepare(`INSERT INTO sources (path, destination, lastModified) VALUES (?, ?, ?)`)
|
|
92
42
|
|
|
93
43
|
/**
|
|
94
|
-
* @
|
|
95
|
-
* @
|
|
96
|
-
* @
|
|
97
|
-
* @property {string} label
|
|
98
|
-
* @property {string} value
|
|
99
|
-
* @property {string} type
|
|
44
|
+
* @param {string} source - Source file path.
|
|
45
|
+
* @param {string} destination - Destination file path.
|
|
46
|
+
* @param {number} lastModified - Source file date last modified.
|
|
100
47
|
*/
|
|
48
|
+
database.createSource = (source, destination, lastModified) => {
|
|
49
|
+
databaseSync.prepare(``) // SQLite bug. Query fails without this.
|
|
50
|
+
createSource.get(source, destination, lastModified)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const updateSource = databaseSync.prepare(`UPDATE sources SET lastModified = ? WHERE path = ?`)
|
|
101
54
|
|
|
102
55
|
/**
|
|
103
|
-
* @
|
|
104
|
-
* @
|
|
105
|
-
* @typedef {string} destination
|
|
106
|
-
* @typedef {string} source
|
|
107
|
-
* @typedef {string} label
|
|
108
|
-
* @typedef {string} value
|
|
56
|
+
* @param {string} source
|
|
57
|
+
* @param {number} lastModified
|
|
109
58
|
*/
|
|
59
|
+
database.updateSource = (source, lastModified) => {
|
|
60
|
+
updateSource.get(lastModified, source)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const getSettingsBySource = databaseSync.prepare(`SELECT * FROM settings WHERE source = ?`)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
// These statements are cached, no need to refactor
|
|
67
|
+
const createDest = databaseSync.prepare(`INSERT INTO destinations (path, dir, syntax, stale, abstract) VALUES (?, ?, ?, 1, ?)`)
|
|
68
|
+
const updateDest = databaseSync.prepare(`UPDATE destinations SET abstract = ? WHERE path = ?`)
|
|
69
|
+
const createMeta = databaseSync.prepare(`INSERT INTO metadata (destination, label, value, type) VALUES (?, ?, ?, ?)`)
|
|
70
|
+
const updateMeta = databaseSync.prepare(`UPDATE metadata SET value = ? WHERE destination = ? AND label = ?`)
|
|
71
|
+
const getDepends = databaseSync.prepare(`SELECT * FROM dependencies WHERE destination = ? AND property = ?`)
|
|
72
|
+
const getDependenciesByDestination = databaseSync.prepare(`SELECT * FROM dependencies WHERE destination = ?`)
|
|
73
|
+
const getAllDeps = databaseSync.prepare(`SELECT * FROM dependencies`)
|
|
74
|
+
const staleDepen = databaseSync.prepare(`UPDATE destinations SET stale = 1 WHERE path = ?`)
|
|
75
|
+
const freshDepen = databaseSync.prepare(`UPDATE destinations SET stale = 0 WHERE path = ? RETURNING *`)
|
|
76
|
+
const staleDescendents = databaseSync.prepare(`UPDATE destinations SET stale = 1 WHERE path LIKE ? RETURNING *`)
|
|
77
|
+
const getAllSettings = databaseSync.prepare(`SELECT * FROM settings`)
|
|
78
|
+
|
|
79
|
+
database.getAllSettings = () => {
|
|
80
|
+
return getAllSettings.all()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @param {string} path */
|
|
84
|
+
database.freshenDependency = (path) => {
|
|
85
|
+
freshDepen.get(path)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
database.getDependencies = () => {
|
|
89
|
+
return getAllDeps.all()
|
|
90
|
+
}
|
|
110
91
|
|
|
111
92
|
/**
|
|
112
|
-
* @
|
|
113
|
-
* @
|
|
114
|
-
* @
|
|
93
|
+
* @param {object} params
|
|
94
|
+
* @param {string} params.path - Destination file path
|
|
95
|
+
* @param {object} params.abstract
|
|
96
|
+
* @param {object} params.metadata
|
|
97
|
+
* @param {string} params.syntax - Destination abstract syntax
|
|
115
98
|
*/
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
getAllByTarget: /** @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
|
-
`)))
|
|
99
|
+
database.createOrUpdateDestination = ({ metadata, ...dest }) => {
|
|
100
|
+
const [dir] = dest.path ? splitURL(dest.path) : []
|
|
101
|
+
const params = Object.keys(metadata)
|
|
102
|
+
if (dest.abstract) params.push("abstract")
|
|
103
|
+
const extant = database.getDestinationIndependently(dest.path, params)
|
|
104
|
+
|
|
105
|
+
if (!extant) {
|
|
106
|
+
createDest.get(dest.path, dir, dest.syntax, JSON.stringify(dest.abstract))
|
|
107
|
+
Object.entries(metadata).map(([k, v]) => createMeta.get(dest.path, k, v, typeof v))
|
|
108
|
+
return
|
|
524
109
|
}
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
110
|
|
|
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.target.markStale.get(dependent)
|
|
576
|
-
})
|
|
577
|
-
prepared.metadata.deleteByTarget.all(deletedSource.destination)
|
|
578
|
-
prepared.target.delete.get(deletedSource.destination)
|
|
579
|
-
return deletedSource
|
|
580
|
-
},
|
|
581
|
-
|
|
582
|
-
getAll() {
|
|
583
|
-
return prepared.source.getAll.all()
|
|
584
|
-
},
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* @param {string} filePath
|
|
588
|
-
*/
|
|
589
|
-
get(filePath) {
|
|
590
|
-
return prepared.source.get.get(filePath)
|
|
591
|
-
},
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
* @param {string} source
|
|
595
|
-
* @param {number} timestamp
|
|
596
|
-
*/
|
|
597
|
-
updateTimestamp(source, timestamp) {
|
|
598
|
-
return prepared.source.update.get(timestamp, source)
|
|
599
|
-
},
|
|
600
|
-
},
|
|
601
|
-
|
|
602
|
-
setting: {
|
|
603
|
-
|
|
604
|
-
/**
|
|
605
|
-
* @param {string} folder
|
|
606
|
-
* @param {string} key
|
|
607
|
-
* @param {string} value
|
|
608
|
-
* @param {string} [source]
|
|
609
|
-
*/
|
|
610
|
-
create(folder, key, value, source = "") {
|
|
611
|
-
const type = typeof value
|
|
612
|
-
const safeValue = type === "object"
|
|
613
|
-
? JSON.stringify(value)
|
|
614
|
-
: value
|
|
615
|
-
|
|
616
|
-
const descendents = folder === ""
|
|
617
|
-
? "%"
|
|
618
|
-
: `${folder}/%`
|
|
619
|
-
|
|
620
|
-
prepared.target.markDescendentsStale.all(descendents)
|
|
621
|
-
|
|
622
|
-
return prepared.settings.create.get(folder, key, safeValue, source)
|
|
623
|
-
},
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
/** @param {string} source */
|
|
627
|
-
deleteBySource(source) {
|
|
628
|
-
const deletedSettings = prepared.settings.delete.all(source)
|
|
629
|
-
|
|
630
|
-
deletedSettings.forEach(setting => {
|
|
631
|
-
prepared.dependency.markStale.get(setting.destination)
|
|
632
|
-
prepared.dependency.markDescendentTargetsStale.all(setting.destination + "/%")
|
|
633
|
-
})
|
|
634
|
-
},
|
|
635
|
-
|
|
636
|
-
getAll() {
|
|
637
|
-
return prepared.settings.getAll.all()
|
|
638
|
-
},
|
|
639
|
-
|
|
640
|
-
/** @param {string} folder */
|
|
641
|
-
getByFolder(folder) {
|
|
642
|
-
const segments = ["", ...(folder).split(path.sep)
|
|
643
|
-
.map((_, index, array) => `${array.slice(0, index + 1).join(path.sep)}`)
|
|
644
|
-
.filter(a => a)]
|
|
645
|
-
|
|
646
|
-
const settings = prepared.settings.getByFolder.all(JSON.stringify(segments))
|
|
647
|
-
const grouped = Object.fromEntries(
|
|
648
|
-
prepared.settings.getByFolder.all(JSON.stringify(segments)).
|
|
649
|
-
map(({ label, settings }) => {
|
|
650
|
-
const targets = Object.groupBy(JSON.parse(settings), ({ destination }) => destination)
|
|
651
|
-
for (const key in targets) {
|
|
652
|
-
targets[key] = targets[key].map(({ destination, value }) => value)
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
return [label, targets]
|
|
656
|
-
})
|
|
657
|
-
)
|
|
658
|
-
|
|
659
|
-
return grouped
|
|
660
|
-
}
|
|
661
|
-
},
|
|
662
|
-
|
|
663
|
-
dependency: {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
getAll() {
|
|
667
|
-
return prepared.dependency.getAll.all()
|
|
668
|
-
},
|
|
669
|
-
|
|
670
|
-
/**
|
|
671
|
-
* @param {string} target
|
|
672
|
-
*/
|
|
673
|
-
getAllByTarget(target) {
|
|
674
|
-
return prepared.dependency.getAllByTarget.all(target)
|
|
675
|
-
},
|
|
676
|
-
|
|
677
|
-
/**
|
|
678
|
-
* @param {object} dependencyFile
|
|
679
|
-
* @param {string} dependencyKey
|
|
680
|
-
* @param {any} dependencyValue
|
|
681
|
-
* @param {string} dependencyPath
|
|
682
|
-
* @param {string} dependentPath
|
|
683
|
-
*/
|
|
684
|
-
track(dependencyFile, dependencyKey, dependencyValue, dependencyPath, dependentPath) {
|
|
685
|
-
Object.defineProperty(dependencyFile, dependencyKey, {
|
|
686
|
-
enumerable: true,
|
|
687
|
-
get() {
|
|
688
|
-
prepared.dependency.create.get(dependencyPath, dependencyKey, dependentPath)
|
|
689
|
-
return dependencyValue
|
|
690
|
-
}
|
|
691
|
-
})
|
|
692
|
-
}
|
|
693
|
-
},
|
|
111
|
+
const changedAbstract = JSON.stringify(dest.abstract) !== JSON.stringify(extant.abstract)
|
|
694
112
|
|
|
695
|
-
|
|
113
|
+
let changedMetadata = false
|
|
696
114
|
|
|
697
|
-
/**
|
|
698
|
-
* @param {string} filePath
|
|
699
|
-
* @returns {TargetOutput | undefined}
|
|
700
|
-
*/
|
|
701
|
-
get(filePath) {
|
|
702
|
-
const target = prepared.target.get.get(filePath)
|
|
703
115
|
|
|
704
|
-
|
|
116
|
+
// TODO: This is redundant with `getDestinationIndependently`
|
|
117
|
+
const cachedMetadata = getMetadataByPath.all(dest.path)
|
|
705
118
|
|
|
706
|
-
|
|
119
|
+
for (const key in metadata) {
|
|
120
|
+
const index = cachedMetadata.findIndex(el => el.label === key)
|
|
121
|
+
cachedMetadata.splice(index, 1)
|
|
122
|
+
if (JSON.stringify(metadata[key]) !== JSON.stringify(extant.metadata[key])) {
|
|
123
|
+
changedMetadata = true
|
|
124
|
+
updateMeta.get(metadata[key], dest.path, key)
|
|
125
|
+
const dependencies = getDepends.all(dest.path, key)
|
|
126
|
+
dependencies.forEach(({ dependent }) => {
|
|
127
|
+
staleDepen.get(dependent)
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
}
|
|
707
131
|
|
|
708
|
-
|
|
132
|
+
cachedMetadata.forEach(deletedDatum => {
|
|
133
|
+
changedMetadata = true
|
|
134
|
+
const dependencies = getDepends.all(dest.path, deletedDatum.label)
|
|
135
|
+
dependencies.forEach(({ dependent }) => {
|
|
136
|
+
staleDepen.get(dependent)
|
|
137
|
+
})
|
|
138
|
+
deleteMetadata.get(deletedDatum.destination, deletedDatum.label)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
if (changedAbstract || changedMetadata) staleDepen.get(dest.path)
|
|
142
|
+
|
|
143
|
+
if (changedAbstract) {
|
|
144
|
+
updateDest.get(JSON.stringify(dest.abstract), dest.path)
|
|
145
|
+
const dependencies = getDepends.all(dest.path, "abstract")
|
|
146
|
+
dependencies.forEach(({ dependent }) => {
|
|
147
|
+
staleDepen.get(dependent)
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
}
|
|
709
151
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
metadata: coerceJSON(metadata),
|
|
714
|
-
...rest
|
|
715
|
-
}
|
|
152
|
+
const getMetadataByPath = databaseSync.prepare(`
|
|
153
|
+
SELECT * FROM metadata WHERE destination = ?
|
|
154
|
+
`)
|
|
716
155
|
|
|
717
|
-
|
|
718
|
-
|
|
156
|
+
/** @param {string} path */
|
|
157
|
+
database.getAllMetadataByPath = (path) => {
|
|
158
|
+
return getMetadataByPath.get(path)
|
|
159
|
+
}
|
|
719
160
|
|
|
161
|
+
/** @param {string[]} params */
|
|
162
|
+
database.getMetadataIndependently = (params) => {
|
|
163
|
+
return databaseSync.prepare(`
|
|
164
|
+
SELECT * FROM destinations d
|
|
165
|
+
LEFT JOIN metadata m ON d.path = m.destination AND m.label IN (${params.map(p => `'${p}'`).join(", ")})
|
|
166
|
+
WHERE d.path = ?
|
|
167
|
+
`)
|
|
168
|
+
}
|
|
720
169
|
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
170
|
+
database.getDestinationWithoutMetadata = () => {
|
|
171
|
+
return databaseSync.prepare(`
|
|
172
|
+
SELECT * FROM destinations
|
|
173
|
+
WHERE path = ?
|
|
174
|
+
`)
|
|
175
|
+
}
|
|
726
176
|
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
filePath,
|
|
767
|
-
dependent
|
|
768
|
-
)
|
|
769
|
-
|
|
770
|
-
for (const key in target.metadata) {
|
|
771
|
-
queries.dependency.track(
|
|
772
|
-
trackedTarget.metadata,
|
|
773
|
-
key,
|
|
774
|
-
target.metadata[key],
|
|
775
|
-
filePath,
|
|
776
|
-
dependent
|
|
777
|
-
)
|
|
778
|
-
}
|
|
177
|
+
/**
|
|
178
|
+
* @param {string} path
|
|
179
|
+
* @param {string[]} [params]
|
|
180
|
+
*/
|
|
181
|
+
database.getDestinationIndependently = (path, params) => {
|
|
182
|
+
// TODO: Could potentially speed up the next two db queries by using the tag store
|
|
183
|
+
|
|
184
|
+
const metadatas = (params && params.length)
|
|
185
|
+
? database.getMetadataIndependently(params).all(path)
|
|
186
|
+
: [database.getDestinationWithoutMetadata().get(path)]
|
|
187
|
+
|
|
188
|
+
if (!metadatas) return
|
|
189
|
+
|
|
190
|
+
const [first] = metadatas
|
|
191
|
+
|
|
192
|
+
if (!first) return
|
|
193
|
+
|
|
194
|
+
const metadata = Object.fromEntries(
|
|
195
|
+
metadatas.map(({ label, value, type }) => {
|
|
196
|
+
const originalValue = type === "undefined"
|
|
197
|
+
? undefined
|
|
198
|
+
: type === "string"
|
|
199
|
+
? String(value)
|
|
200
|
+
: type === "boolean"
|
|
201
|
+
? Boolean(value)
|
|
202
|
+
: typeof value !== "string"
|
|
203
|
+
? value
|
|
204
|
+
: JSON.parse(value)
|
|
205
|
+
return [label, originalValue]
|
|
206
|
+
})
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
const result = {
|
|
211
|
+
path: first.path,
|
|
212
|
+
dir: first.dir,
|
|
213
|
+
syntax: first.syntax,
|
|
214
|
+
metadata
|
|
215
|
+
}
|
|
779
216
|
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
/**
|
|
784
|
-
* @typedef {object} TargetGetManyWithTrackersParams
|
|
785
|
-
* @property {string | undefined} [folder]
|
|
786
|
-
* @property {boolean | undefined} [recursive]
|
|
787
|
-
* @property {string | undefined} [dependent]
|
|
788
|
-
* @property {Record<string, string> | undefined} [query]
|
|
789
|
-
*/
|
|
790
|
-
|
|
791
|
-
/** @param {TargetGetManyWithTrackersParams | undefined} params */
|
|
792
|
-
getManyWithTrackers(params) {
|
|
793
|
-
const { folder = "%", recursive, dependent, query = {} } = params
|
|
794
|
-
|
|
795
|
-
const recursivePath = [folder, "%"].filter(a => a).join("/")
|
|
796
|
-
|
|
797
|
-
const many = prepared.target.getMany
|
|
798
|
-
.all({ folder, recursivePath: recursive ? recursivePath : folder, filter: JSON.stringify(query) })
|
|
799
|
-
.map(({ abstract, metadata, ...rest }) => {
|
|
800
|
-
|
|
801
|
-
const trackedTarget = {
|
|
802
|
-
abstract: coerceJSON(abstract),
|
|
803
|
-
metadata: coerceJSON(metadata),
|
|
804
|
-
...rest
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
queries.dependency.track(
|
|
808
|
-
trackedTarget,
|
|
809
|
-
"abstract",
|
|
810
|
-
trackedTarget.abstract,
|
|
811
|
-
trackedTarget.path,
|
|
812
|
-
dependent
|
|
813
|
-
)
|
|
814
|
-
|
|
815
|
-
for (const key in trackedTarget.metadata) {
|
|
816
|
-
queries.dependency.track(
|
|
817
|
-
trackedTarget.metadata,
|
|
818
|
-
key,
|
|
819
|
-
trackedTarget.metadata[key],
|
|
820
|
-
trackedTarget.path,
|
|
821
|
-
dependent
|
|
822
|
-
)
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
return trackedTarget
|
|
826
|
-
})
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
many.sort((a, b) => {
|
|
830
|
-
if (a.metadata.date) {
|
|
831
|
-
if (b.metadata.date) {
|
|
832
|
-
return (new Date(b.metadata.date)).getTime() - (new Date(a.metadata.date)).getTime()
|
|
833
|
-
} else {
|
|
834
|
-
return -1
|
|
835
|
-
}
|
|
836
|
-
} else {
|
|
837
|
-
return 1
|
|
838
|
-
}
|
|
839
|
-
})
|
|
217
|
+
const includeAbstract = params && params.includes("abstract")
|
|
218
|
+
if (includeAbstract) result.abstract = JSON.parse(first.abstract)
|
|
840
219
|
|
|
841
|
-
|
|
842
|
-
|
|
220
|
+
return result
|
|
221
|
+
}
|
|
843
222
|
|
|
223
|
+
/**
|
|
224
|
+
* @param {string} path
|
|
225
|
+
* @param {string[]} properties
|
|
226
|
+
* @param {string} dependent
|
|
227
|
+
*/
|
|
228
|
+
database.getDestinationDependently = (path, properties, dependent) => {
|
|
229
|
+
const params = []
|
|
230
|
+
const deps = []
|
|
231
|
+
|
|
232
|
+
properties.forEach(p => {
|
|
233
|
+
params.push(p)
|
|
234
|
+
deps.push(`('${path}', '${p}', '${dependent}')`)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
const result = database.getDestinationIndependently(path, params)
|
|
238
|
+
if (result.abstract) deps.push(`('${path}, 'abstract', '${dependent}')`)
|
|
239
|
+
databaseSync.prepare(`INSERT INTO dependencies (destination, property, dependent) VALUES ${deps.join(", ")} RETURNING *`).all()
|
|
240
|
+
}
|
|
844
241
|
|
|
845
|
-
|
|
846
|
-
* @param {TargetInput} target
|
|
847
|
-
*/
|
|
848
|
-
create(target) {
|
|
849
|
-
const dir = target.path && splitURL(target.path)
|
|
850
|
-
const ext = path.extname(target.path)
|
|
851
|
-
const relativePath = path.relative("", target.path).toLowerCase()
|
|
242
|
+
const selectSource = databaseSync.prepare(`SELECT * FROM sources WHERE path = ?`)
|
|
852
243
|
|
|
853
|
-
|
|
244
|
+
/**
|
|
245
|
+
* @param {string} path
|
|
246
|
+
*/
|
|
247
|
+
database.getSource = (path) => {
|
|
248
|
+
const source = selectSource.get(path)
|
|
249
|
+
return source
|
|
250
|
+
}
|
|
854
251
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
ext,
|
|
860
|
-
JSON.stringify(target.abstract)
|
|
861
|
-
)
|
|
862
|
-
prepared.metadata.create.get(relativePath, JSON.stringify(target.metadata))
|
|
863
|
-
return created
|
|
864
|
-
}
|
|
252
|
+
database.getAllSources = () => {
|
|
253
|
+
const sources = store.all`SELECT * FROM sources`
|
|
254
|
+
return sources
|
|
255
|
+
}
|
|
865
256
|
|
|
866
|
-
|
|
257
|
+
const deleteSource = databaseSync.prepare(`DELETE FROM sources WHERE path = ? RETURNING *`)
|
|
258
|
+
const deleteAllDependenciesByDestination = databaseSync.prepare(`DELETE FROM dependencies WHERE destination = ? RETURNING dependent`)
|
|
259
|
+
const deleteAllMetadataByDestination = databaseSync.prepare(`DELETE FROM metadata WHERE destination = ?`)
|
|
260
|
+
const deleteMetadata = databaseSync.prepare(`DELETE FROM metadata WHERE destination = ? AND label = ?`)
|
|
261
|
+
const deleteDestination = databaseSync.prepare(`DELETE FROM destinations WHERE path = ?`)
|
|
262
|
+
|
|
263
|
+
/** @param {string} path */
|
|
264
|
+
database.deleteSource = (path) => {
|
|
265
|
+
const deleted = deleteSource.all(path)
|
|
266
|
+
deleted.forEach(source => {
|
|
267
|
+
database.deleteSettings(String(source.path))
|
|
268
|
+
getDependenciesByDestination.all(source.destination)
|
|
269
|
+
deleteAllDependenciesByDestination.all(source.destination)
|
|
270
|
+
.forEach(({ dependent }) => staleDepen.get(dependent))
|
|
271
|
+
deleteAllMetadataByDestination.all(source.destination)
|
|
272
|
+
deleteDestination.get(source.destination)
|
|
273
|
+
})
|
|
274
|
+
}
|
|
867
275
|
|
|
868
|
-
|
|
869
|
-
if (target.metadata[key] !== extant.metadata[key]) {
|
|
276
|
+
// TODO: DELETE SETTING
|
|
870
277
|
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
}))
|
|
278
|
+
database.getAllDestinations = () => {
|
|
279
|
+
return store.all`SELECT * FROM destinations`
|
|
280
|
+
}
|
|
875
281
|
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
282
|
+
database.getStaleDestinations = () => {
|
|
283
|
+
const metadatas = store.all`
|
|
284
|
+
SELECT * FROM destinations d
|
|
285
|
+
LEFT JOIN metadata m on d.path = m.destination
|
|
286
|
+
WHERE stale = 1`
|
|
287
|
+
|
|
288
|
+
const grouped = metadatas.map((value, index, array) => {
|
|
289
|
+
if (array.slice(0, index)
|
|
290
|
+
.find(prior => prior.path === value.path)
|
|
291
|
+
) {
|
|
292
|
+
return null
|
|
293
|
+
} else {
|
|
294
|
+
return {
|
|
295
|
+
path: value.path,
|
|
296
|
+
dir: value.dir,
|
|
297
|
+
syntax: value.syntax,
|
|
298
|
+
stale: value.stale,
|
|
299
|
+
abstract: JSON.parse(value.abstract),
|
|
300
|
+
metadata: Object.fromEntries(
|
|
301
|
+
array.slice(index)
|
|
302
|
+
.filter(following => following.path === value.path)
|
|
303
|
+
.map(succeeding => {
|
|
304
|
+
return reconstituteMetadata(succeeding)
|
|
882
305
|
})
|
|
883
|
-
|
|
306
|
+
)
|
|
307
|
+
|
|
884
308
|
}
|
|
309
|
+
}
|
|
310
|
+
}).filter(a => a)
|
|
311
|
+
|
|
312
|
+
function reconstituteMetadata({ label, value, type }) {
|
|
313
|
+
const originalValue = type === "undefined"
|
|
314
|
+
? undefined
|
|
315
|
+
: type === "string"
|
|
316
|
+
? String(value)
|
|
317
|
+
: type === "boolean"
|
|
318
|
+
? Boolean(value)
|
|
319
|
+
: typeof value !== "string"
|
|
320
|
+
? value
|
|
321
|
+
: JSON.parse(value)
|
|
322
|
+
return [label, originalValue]
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return grouped
|
|
326
|
+
}
|
|
885
327
|
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
328
|
+
const createSetting = databaseSync.prepare(`INSERT INTO settings (destination, label, value, source) VALUES (?, ?, ?, ?) RETURNING *`)
|
|
329
|
+
const selectSetting = databaseSync.prepare(`SELECT * FROM settings WHERE label = ? AND destination = ?`)
|
|
330
|
+
const updateSetting = databaseSync.prepare(`UPDATE settings SET value = ? WHERE destination = ? AND label = ? RETURNING *`)
|
|
331
|
+
const deleteSettings = databaseSync.prepare(`DELETE FROM settings WHERE source = ?`)
|
|
889
332
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
.all(relativePath, key)
|
|
894
|
-
.forEach(dependency => {
|
|
895
|
-
prepared.target.markStale.get(dependency.dependent)
|
|
896
|
-
})
|
|
897
|
-
}
|
|
898
|
-
}
|
|
333
|
+
// const updateDest = databaseSync.prepare(`UPDATE destinations SET abstract = ? WHERE path = ?`)
|
|
334
|
+
|
|
335
|
+
// WRITE AN UPDATE SETTINGS FUNCTION FOR WHEN A SOURCE CHANGES
|
|
899
336
|
|
|
900
|
-
|
|
337
|
+
/** @param {string} source */
|
|
338
|
+
database.deleteSettings = (source) => {
|
|
339
|
+
const deleted = deleteSettings.all(source)
|
|
340
|
+
deleted.forEach(setting => {
|
|
341
|
+
staleDepen.get(setting.destination)
|
|
342
|
+
staleDescendents.all(`${setting.destination}/%`)
|
|
343
|
+
})
|
|
344
|
+
}
|
|
901
345
|
|
|
902
|
-
|
|
903
|
-
|
|
346
|
+
/**
|
|
347
|
+
* @param {string} destinationFolder
|
|
348
|
+
* @param {string} key
|
|
349
|
+
* @param {string} value
|
|
350
|
+
* @param {string} [source]
|
|
351
|
+
* @example
|
|
352
|
+
* setSetting("", "theme", "summer", "settings.md")
|
|
353
|
+
* setSetting("data", "format", "csv", "/config.yaml")
|
|
354
|
+
*/
|
|
355
|
+
database.setSetting = (destinationFolder, key, value, source = "") => {
|
|
356
|
+
const extant = selectSetting.get(key, destinationFolder)
|
|
904
357
|
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
.forEach(dependency => {
|
|
909
|
-
prepared.target.markStale.get(dependency.dependent)
|
|
910
|
-
})
|
|
911
|
-
}
|
|
358
|
+
if (!extant) return createSetting.get(destinationFolder, key, value, source)
|
|
359
|
+
if (extant.value === value) return
|
|
360
|
+
updateSetting.get(value, destinationFolder, key)
|
|
912
361
|
|
|
913
|
-
|
|
914
|
-
|
|
362
|
+
destinationFolder === ""
|
|
363
|
+
? staleDescendents.all("%")
|
|
364
|
+
: staleDescendents.all(`${destinationFolder}/%`)
|
|
365
|
+
}
|
|
915
366
|
|
|
916
|
-
/** @param {string} filePath */
|
|
917
|
-
markFresh(filePath) {
|
|
918
|
-
return prepared.target.markFresh.get(filePath)
|
|
919
|
-
},
|
|
920
367
|
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
368
|
+
/**
|
|
369
|
+
* @param {string} destinationFolder
|
|
370
|
+
*/
|
|
371
|
+
database.getSettings = (destinationFolder) => {
|
|
372
|
+
const segments = destinationFolder.split(path.sep)
|
|
373
|
+
.map((_, index, array) => `'${array.slice(0, index + 1).join(path.sep)}'`)
|
|
925
374
|
|
|
926
|
-
|
|
375
|
+
segments.unshift("''")
|
|
376
|
+
const statement = segments.join(", ")
|
|
927
377
|
|
|
928
|
-
|
|
378
|
+
const records = databaseSync.prepare(`SELECT * FROM settings WHERE destination IN (${statement})`).all()
|
|
929
379
|
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
prepared.url.create.get(url, JSON.stringify(data))
|
|
937
|
-
},
|
|
380
|
+
const grouped = {}
|
|
381
|
+
records.sort((a, b) => a.length - b.length)
|
|
382
|
+
.forEach(record => {
|
|
383
|
+
if (!grouped[record.label]) grouped[record.label] = [record.value]
|
|
384
|
+
else grouped[record.label].push(record.value)
|
|
385
|
+
})
|
|
938
386
|
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
const { data } = prepared.url.get.get(url) || {}
|
|
942
|
-
if (!data) return
|
|
943
|
-
return JSON.parse(data)
|
|
944
|
-
}
|
|
945
|
-
}
|
|
387
|
+
return grouped
|
|
388
|
+
}
|
|
946
389
|
|
|
390
|
+
database.getEverything = () => {
|
|
391
|
+
return Object.fromEntries(
|
|
392
|
+
databaseSync.prepare(`SELECT name FROM sqlite_master WHERE type = 'table'`)
|
|
393
|
+
.all()
|
|
394
|
+
.map(table => [
|
|
395
|
+
table.name,
|
|
396
|
+
databaseSync.prepare(`SELECT * FROM ${table.name}`).all()
|
|
397
|
+
])
|
|
398
|
+
)
|
|
947
399
|
}
|
|
948
400
|
|
|
949
|
-
return
|
|
401
|
+
return database
|
|
950
402
|
}
|
|
951
403
|
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
UNIQUE(destination, label, value, source)
|
|
995
|
-
);
|
|
996
|
-
|
|
997
|
-
CREATE TABLE IF NOT EXISTS urls (
|
|
998
|
-
url STRING PRIMARY KEY,
|
|
999
|
-
data STRING
|
|
1000
|
-
);
|
|
1001
|
-
|
|
1002
|
-
`)
|
|
1003
|
-
}
|
|
404
|
+
const sqlCreateTables = `
|
|
405
|
+
CREATE TABLE IF NOT EXISTS sources (
|
|
406
|
+
id INTEGER PRIMARY KEY,
|
|
407
|
+
destination STRING,
|
|
408
|
+
path STRING,
|
|
409
|
+
lastModified INTEGER
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
CREATE TABLE IF NOT EXISTS dependencies (
|
|
413
|
+
key INTEGER PRIMARY KEY,
|
|
414
|
+
destination STRING NOT NULL,
|
|
415
|
+
property STRING NOT NULL,
|
|
416
|
+
dependent STRING NOT NULL
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
CREATE TABLE IF NOT EXISTS destinations (
|
|
420
|
+
key INTEGER PRIMARY KEY,
|
|
421
|
+
path STRING UNIQUE,
|
|
422
|
+
dir TEXT,
|
|
423
|
+
syntax TEXT,
|
|
424
|
+
stale INTEGER,
|
|
425
|
+
abstract
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
429
|
+
id INTEGER PRIMARY KEY,
|
|
430
|
+
destination STRING,
|
|
431
|
+
label STRING,
|
|
432
|
+
value STRING,
|
|
433
|
+
type STRING,
|
|
434
|
+
UNIQUE(destination, label)
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
438
|
+
id INTEGER PRIMARY KEY,
|
|
439
|
+
destination STRING,
|
|
440
|
+
source STRING,
|
|
441
|
+
label STRING,
|
|
442
|
+
value STRING
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
`
|
|
1004
446
|
|
|
1005
447
|
export default createDatabase
|