votive 0.3.0 → 0.3.2
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/lib/bundle.js +119 -26
- package/lib/createDatabase.js +993 -374
- package/lib/writeDestinations.js +40 -12
- package/package.json +1 -1
package/lib/createDatabase.js
CHANGED
|
@@ -1,12 +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
|
+
|
|
6
15
|
/**
|
|
7
|
-
* @typedef {
|
|
16
|
+
* @typedef {Record<string, MetadataProperty>} Metadata
|
|
8
17
|
*/
|
|
9
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
|
+
/**
|
|
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
|
+
}
|
|
10
49
|
|
|
11
50
|
/** @param {string} dbPath */
|
|
12
51
|
function loadDB(dbPath) {
|
|
@@ -15,433 +54,1013 @@ function loadDB(dbPath) {
|
|
|
15
54
|
}
|
|
16
55
|
|
|
17
56
|
|
|
18
|
-
/** @param {
|
|
19
|
-
function
|
|
20
|
-
const databaseSync = loadDB(dbPath)
|
|
57
|
+
/** @param {DatabaseSync} database */
|
|
58
|
+
function prepareStatements(database) {
|
|
21
59
|
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
databaseSync.exec(sqlCreateTables)
|
|
25
|
-
|
|
26
|
-
const database = {}
|
|
27
|
-
|
|
28
|
-
database.saveDB = async () => {
|
|
29
|
-
if (databaseSync.location()) return // Only save if running in memory
|
|
30
|
-
await backup(databaseSync, dbPath)
|
|
31
|
-
}
|
|
60
|
+
const { prepare } = database
|
|
32
61
|
|
|
33
62
|
/**
|
|
34
|
-
* @
|
|
35
|
-
* @param {string} urlPath
|
|
63
|
+
* @typedef {string} JSONString - A string that is actually JSON.
|
|
36
64
|
*/
|
|
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 (?, ?, ?)`)
|
|
42
65
|
|
|
43
66
|
/**
|
|
44
|
-
* @
|
|
45
|
-
* @
|
|
46
|
-
* @
|
|
67
|
+
* @typedef {object} SQLiteSource
|
|
68
|
+
* @property {number} id
|
|
69
|
+
* @property {string} destination
|
|
70
|
+
* @property {string} filePath
|
|
71
|
+
* @property {number} lastModified
|
|
47
72
|
*/
|
|
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 = ?`)
|
|
54
73
|
|
|
55
74
|
/**
|
|
56
|
-
* @
|
|
57
|
-
* @
|
|
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
|
|
58
83
|
*/
|
|
59
|
-
database.updateSource = (source, lastModified) => {
|
|
60
|
-
updateSource.get(lastModified, source)
|
|
61
|
-
}
|
|
62
84
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
}
|
|
85
|
+
/**
|
|
86
|
+
* @typedef {object} SQLiteDependency
|
|
87
|
+
* @property {number} key
|
|
88
|
+
* @property {string} destination
|
|
89
|
+
* @property {string} property
|
|
90
|
+
* @property {string} dependent
|
|
91
|
+
*/
|
|
82
92
|
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
+
*/
|
|
87
101
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
*/
|
|
91
110
|
|
|
92
111
|
/**
|
|
93
|
-
* @
|
|
94
|
-
* @
|
|
95
|
-
* @
|
|
96
|
-
* @param {object} params.metadata
|
|
97
|
-
* @param {string} params.syntax - Destination abstract syntax
|
|
112
|
+
* @typedef {object} SQLiteURL
|
|
113
|
+
* @typedef {string} url
|
|
114
|
+
* @typedef {string} data
|
|
98
115
|
*/
|
|
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
|
|
109
|
-
}
|
|
110
116
|
|
|
111
|
-
|
|
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 (
|
|
234
|
+
j.value = m.value
|
|
235
|
+
OR (
|
|
236
|
+
json_valid(m.value)
|
|
237
|
+
AND json_type(m.value) = 'array'
|
|
238
|
+
AND EXISTS (
|
|
239
|
+
SELECT 1
|
|
240
|
+
FROM json_each(m.value)
|
|
241
|
+
WHERE value = j.value
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
GROUP BY m.destination
|
|
246
|
+
),
|
|
247
|
+
filter_count AS (
|
|
248
|
+
SELECT COUNT(*) AS total FROM json_each(:filter)
|
|
249
|
+
)
|
|
250
|
+
SELECT d.*, json_group_object(i.label, i.value) AS metadata
|
|
251
|
+
FROM destinations d
|
|
252
|
+
INNER JOIN metadata i ON d.path = i.destination
|
|
253
|
+
LEFT JOIN matches ON matches.destination = d.path
|
|
254
|
+
CROSS JOIN filter_count f
|
|
255
|
+
WHERE (
|
|
256
|
+
f.total = 0
|
|
257
|
+
OR matches.match_count = f.total
|
|
258
|
+
)
|
|
259
|
+
AND (
|
|
260
|
+
d.dir = :folder
|
|
261
|
+
OR d.dir LIKE :recursivePath
|
|
262
|
+
)
|
|
263
|
+
GROUP BY d.path
|
|
264
|
+
ORDER BY d.path ASC;
|
|
265
|
+
`))),
|
|
266
|
+
|
|
267
|
+
/*
|
|
268
|
+
WITH matches AS (
|
|
269
|
+
SELECT m.destination, COUNT(*) AS match_count
|
|
270
|
+
FROM metadata m
|
|
271
|
+
INNER JOIN json_each('{ "breadcrumb": "Hello World" }') j ON j.key = m.label AND (
|
|
272
|
+
j.value = m.value
|
|
273
|
+
OR (
|
|
274
|
+
json_valid(m.value)
|
|
275
|
+
AND json_type(m.value) = 'array'
|
|
276
|
+
AND EXISTS (
|
|
277
|
+
SELECT 1
|
|
278
|
+
FROM json_each(m.value)
|
|
279
|
+
WHERE value = j.value
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
GROUP BY m.destination
|
|
284
|
+
),
|
|
285
|
+
filter_count AS (
|
|
286
|
+
SELECT COUNT(*) AS total FROM json_each('{ "breadcrumb": "Hello World" }')
|
|
287
|
+
)
|
|
288
|
+
SELECT d.*, json_group_object(i.label, i.value) AS metadata
|
|
289
|
+
FROM destinations d
|
|
290
|
+
INNER JOIN metadata i ON d.path = i.destination
|
|
291
|
+
LEFT JOIN matches ON matches.destination = d.path
|
|
292
|
+
CROSS JOIN filter_count f
|
|
293
|
+
WHERE (
|
|
294
|
+
f.total = 0
|
|
295
|
+
OR matches.match_count = f.total
|
|
296
|
+
)
|
|
297
|
+
AND (
|
|
298
|
+
d.dir = '/'
|
|
299
|
+
OR d.dir LIKE '%'
|
|
300
|
+
)
|
|
301
|
+
GROUP BY d.path
|
|
302
|
+
ORDER BY d.path ASC;
|
|
303
|
+
*/
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* @callback SQLiteTargetGetAll
|
|
307
|
+
* @returns {SQLiteTarget[]}
|
|
308
|
+
*/
|
|
309
|
+
|
|
310
|
+
getAll: /** @type {{all: SQLiteTargetGetAll}} */ (/** @type {unknown} */ (database.prepare(`
|
|
311
|
+
SELECT destinations.*, json_group_object(i.label, i.value) AS metadata
|
|
312
|
+
FROM destinations
|
|
313
|
+
LEFT JOIN metadata i ON destinations.path = i.destination
|
|
314
|
+
GROUP BY destinations.path
|
|
315
|
+
`))),
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* @callback SQLiteTargetGetAllStale
|
|
319
|
+
* @returns {SQLiteTarget[]}
|
|
320
|
+
*/
|
|
321
|
+
|
|
322
|
+
getAllStale: /** @type {{all: SQLiteTargetGetAllStale}} */ (/** @type {unknown} */ (database.prepare(`
|
|
323
|
+
WITH destination AS (
|
|
324
|
+
SELECT * FROM destinations
|
|
325
|
+
LEFT JOIN metadata ON destinations.path = metadata.destination
|
|
326
|
+
)
|
|
327
|
+
SELECT destination.path, destination.dir, destination.syntax, destination.abstract, json_group_object(destination.label, destination.value) AS metadata
|
|
328
|
+
FROM destination
|
|
329
|
+
WHERE stale = 1
|
|
330
|
+
GROUP BY destination.path
|
|
331
|
+
`))),
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* @callback SQLiteTargetMarkDescendentsStale
|
|
335
|
+
* @param {string} targetFilePathPattern
|
|
336
|
+
* @returns {SQLiteTarget[]}
|
|
337
|
+
*/
|
|
338
|
+
|
|
339
|
+
markDescendentsStale: /** @type {{all: SQLiteTargetMarkDescendentsStale}} */ (/** @type {unknown} */ (database.prepare(`
|
|
340
|
+
UPDATE destinations SET stale = 1 WHERE path LIKE ? RETURNING *
|
|
341
|
+
`))),
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* @callback SQLiteTargetMarkFresh
|
|
345
|
+
* @param {string} targetFilePath
|
|
346
|
+
* @returns {SQLiteTarget[]}
|
|
347
|
+
*/
|
|
348
|
+
|
|
349
|
+
markFresh: /** @type {{get: SQLiteTargetMarkFresh}} */ (/** @type {unknown} */ (database.prepare(`
|
|
350
|
+
UPDATE destinations SET stale = 0 WHERE path = ? RETURNING *
|
|
351
|
+
`))),
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* @callback SQLiteTargetMarkStale
|
|
355
|
+
* @param {string} targetFilePath
|
|
356
|
+
* @returns {SQLiteTarget}
|
|
357
|
+
*/
|
|
358
|
+
|
|
359
|
+
markStale: /** @type {{get: SQLiteTargetMarkStale}} */ (/** @type {unknown} */ (database.prepare(`
|
|
360
|
+
UPDATE destinations SET stale = 1 WHERE path = ? RETURNING *
|
|
361
|
+
`))),
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* @callback SQLiteTargetUpdate
|
|
365
|
+
* @param {string} abstract
|
|
366
|
+
* @param {string} targetFilePath
|
|
367
|
+
* @returns {SQLiteTarget}
|
|
368
|
+
*/
|
|
369
|
+
update: /** @type {{get: SQLiteTargetUpdate}} */ (/** @type {unknown} */ (database.prepare(`
|
|
370
|
+
UPDATE destinations
|
|
371
|
+
SET abstract = ?
|
|
372
|
+
WHERE path = ?
|
|
373
|
+
`))),
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
/* DEPENDENCIES */
|
|
377
|
+
dependency: {
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* @callback SQLiteDependencyCreate
|
|
381
|
+
* @param {string} destination
|
|
382
|
+
* @param {string} property
|
|
383
|
+
* @param {string} dependent
|
|
384
|
+
* @returns {void}
|
|
385
|
+
*/
|
|
386
|
+
|
|
387
|
+
create: /** @type {{get: SQLiteDependencyCreate}} */ (/** @type {unknown} */ (database.prepare(`
|
|
388
|
+
INSERT OR IGNORE INTO dependencies (destination, property, dependent)
|
|
389
|
+
VALUES (?, ?, ?)
|
|
390
|
+
`))),
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* @callback SQLiteDependencyDeleteByTarget
|
|
394
|
+
* @param {string} targetFilePath
|
|
395
|
+
* @returns {SQLiteDependency[]}
|
|
396
|
+
*/
|
|
397
|
+
|
|
398
|
+
deleteByTarget: /** @type {{all: SQLiteDependencyDeleteByTarget}} */ (/** @type {unknown} */ (database.prepare(`
|
|
399
|
+
DELETE FROM dependencies WHERE destination = ? RETURNING dependent
|
|
400
|
+
`))),
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* @callback SQLiteDependencyGetAll
|
|
404
|
+
* @returns {SQLiteDependency[]}
|
|
405
|
+
*/
|
|
406
|
+
|
|
407
|
+
getAll: /** @type {{all: SQLiteDependencyGetAll}} */ (/** @type {unknown} */ (database.prepare(`
|
|
408
|
+
SELECT * FROM dependencies
|
|
409
|
+
`))),
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* @callback SQLiteDependencyGetByTarget
|
|
413
|
+
* @param {string} targetFilePath
|
|
414
|
+
* @returns {SQLiteDependency[]}
|
|
415
|
+
*/
|
|
416
|
+
|
|
417
|
+
getAllByTarget: /** @type {{all: SQLiteDependencyGetByTarget}} */ (/** @type {unknown} */ (database.prepare(`
|
|
418
|
+
SELECT * FROM dependencies WHERE destination = ?
|
|
419
|
+
`))),
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* @callback SQLiteDependencyGetByTargetAndProperty
|
|
423
|
+
* @param {string} targetFilePath
|
|
424
|
+
* @param {string} property
|
|
425
|
+
* @returns {SQLiteDependency[]}
|
|
426
|
+
*/
|
|
427
|
+
|
|
428
|
+
getByTargetAndProperty: /** @type {{all: SQLiteDependencyGetByTargetAndProperty}} */ (/** @type {unknown} */ (database.prepare(`
|
|
429
|
+
SELECT * FROM dependencies WHERE destination = ? AND property = ?
|
|
430
|
+
`))),
|
|
431
|
+
|
|
432
|
+
},
|
|
433
|
+
|
|
434
|
+
/* METADATA */
|
|
435
|
+
metadata: {
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* @callback SQLiteMetadataCreate
|
|
439
|
+
* @param {JSONString} metadataJSON
|
|
440
|
+
* @param {string} targetPath
|
|
441
|
+
* @returns {void}
|
|
442
|
+
*/
|
|
443
|
+
|
|
444
|
+
create: /** @type {{get: SQLiteMetadataCreate}} */ (/** @type {unknown} */ (database.prepare(`
|
|
445
|
+
INSERT OR REPLACE INTO metadata (label, value, type, destination)
|
|
446
|
+
SELECT
|
|
447
|
+
json_each.key,
|
|
448
|
+
json_each.value,
|
|
449
|
+
json_each.type,
|
|
450
|
+
?
|
|
451
|
+
FROM json_each(?);
|
|
452
|
+
`))),
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* @callback SQLiteMetadataDelete
|
|
456
|
+
* @param {string} targetFilePath
|
|
457
|
+
* @param {string} label
|
|
458
|
+
* @returns {void}
|
|
459
|
+
*/
|
|
460
|
+
|
|
461
|
+
delete: /** @type {{get: SQLiteMetadataDelete}} */ (/** @type {unknown} */ (database.prepare(`
|
|
462
|
+
DELETE FROM metadata WHERE destination = ? AND label = ?
|
|
463
|
+
`))),
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* @callback SQLiteMetadataDeleteByTarget
|
|
467
|
+
* @param {string} targetFilePath
|
|
468
|
+
* @returns {void}
|
|
469
|
+
*/
|
|
470
|
+
|
|
471
|
+
deleteByTarget: /** @type {{all: SQLiteMetadataDeleteByTarget}} */ (/** @type {unknown} */ (database.prepare(`
|
|
472
|
+
DELETE FROM metadata WHERE destination = ?
|
|
473
|
+
`)))
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
/* SETTINGS */
|
|
477
|
+
settings: {
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* @callback SQLiteSettingsCreate
|
|
481
|
+
* @param {string} targetFilePath
|
|
482
|
+
* @param {string} label
|
|
483
|
+
* @param {string} value
|
|
484
|
+
* @param {string} sourceFilePath
|
|
485
|
+
* @returns {SQLiteSetting[]}
|
|
486
|
+
*/
|
|
487
|
+
|
|
488
|
+
create: /** @type {{get: SQLiteSettingsCreate}} */ (/** @type {unknown} */ (database.prepare(`
|
|
489
|
+
INSERT OR IGNORE INTO settings (destination, label, value, source) VALUES (?, ?, ?, ?) RETURNING *
|
|
490
|
+
`))),
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* @callback SQLiteSettingsDelete
|
|
494
|
+
* @param {string} sourceFilePath
|
|
495
|
+
* @returns {SQLiteSetting[]}
|
|
496
|
+
*/
|
|
497
|
+
|
|
498
|
+
delete: /** @type {{all: SQLiteSettingsDelete}} */ (/** @type {unknown} */ (database.prepare(`
|
|
499
|
+
DELETE FROM settings WHERE source = ?
|
|
500
|
+
RETURNING *
|
|
501
|
+
`))),
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* @callback SQLiteSettingsGet
|
|
505
|
+
* @param {string} label
|
|
506
|
+
* @param {string} targetFilePath
|
|
507
|
+
* @returns {SQLiteSettings[]}
|
|
508
|
+
*/
|
|
509
|
+
|
|
510
|
+
get: /** @type {{all: SQLiteSettingsGet}} */ (/** @type {unknown} */ (database.prepare(`
|
|
511
|
+
SELECT * FROM settings WHERE label = ? AND destination = ?
|
|
512
|
+
`))),
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* @callback SQLiteSettingsGetAll
|
|
516
|
+
* @returns {SQLiteSetting[]}
|
|
517
|
+
*/
|
|
518
|
+
|
|
519
|
+
getAll: /** @type {{all: SQLiteSettingsGetAll}} */ (/** @type {unknown} */ (database.prepare(`
|
|
520
|
+
SELECT * FROM settings
|
|
521
|
+
`))),
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* @callback SQLiteSettingsGetByFolder
|
|
525
|
+
* @param {JSONString} JSONarray - JSON string of an array of target file paths
|
|
526
|
+
* @returns {SQLiteSetting[]}
|
|
527
|
+
*/
|
|
528
|
+
|
|
529
|
+
getByFolder: /** @type {{all: SQLiteSettingsGetByFolder}} */ (/** @type {unknown} */ (database.prepare(`
|
|
530
|
+
SELECT label, json_group_array(json_object('destination', destination, 'value', value)) AS settings
|
|
531
|
+
FROM settings
|
|
532
|
+
WHERE destination IN (SELECT value FROM json_each(?))
|
|
533
|
+
GROUP BY label
|
|
534
|
+
`))),
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* @callback SQLiteSettingsUpdate
|
|
538
|
+
* @param {string} value
|
|
539
|
+
* @param {string} targetFilePath
|
|
540
|
+
* @param {string} label
|
|
541
|
+
* @returns {SQLiteSettings}
|
|
542
|
+
*/
|
|
543
|
+
|
|
544
|
+
update: /** @type {{get: SQLiteSettingsUpdate}} */ (/** @type {unknown} */ (database.prepare(`
|
|
545
|
+
UPDATE settings SET value = ? WHERE destination = ? AND label = ? RETURNING *
|
|
546
|
+
`)))
|
|
547
|
+
},
|
|
548
|
+
|
|
549
|
+
/* URLS */
|
|
550
|
+
url: {
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* @callback SQLiteURLCreate
|
|
554
|
+
* @param {string} url
|
|
555
|
+
* @param {string} data
|
|
556
|
+
* @returns {SQLiteURL}
|
|
557
|
+
*/
|
|
558
|
+
|
|
559
|
+
create: /** @type {{get: SQLiteURLCreate}} */ (/** @type {unknown} */ (database.prepare(`
|
|
560
|
+
INSERT OR IGNORE INTO urls (url, data) VALUES (?, ?) RETURNING *
|
|
561
|
+
`))),
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* @callback SQLiteURLGet
|
|
565
|
+
* @param {string} url
|
|
566
|
+
* @returns {SQLiteURL}
|
|
567
|
+
*/
|
|
568
|
+
|
|
569
|
+
get: /** @type {{get: SQLiteURLGet}} */ (/** @type {unknown} */ (database.prepare(`
|
|
570
|
+
SELECT data FROM urls WHERE url = ?
|
|
571
|
+
`)))
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
112
575
|
|
|
113
|
-
|
|
576
|
+
/**
|
|
577
|
+
* @param {string} databasePath
|
|
578
|
+
*/
|
|
579
|
+
function createDatabase(databasePath = ".votive.db") {
|
|
580
|
+
|
|
581
|
+
const database = loadDB(databasePath)
|
|
582
|
+
createTables(database)
|
|
583
|
+
const prepared = prepareStatements(database)
|
|
584
|
+
|
|
585
|
+
const queries = {
|
|
586
|
+
begin: () => database.exec("BEGIN TRANSACTION"),
|
|
587
|
+
commit: () => database.exec("COMMIT"),
|
|
588
|
+
restore: () => database.exec("RESTORE"),
|
|
589
|
+
raw: database,
|
|
590
|
+
|
|
591
|
+
/** @param {object[]} sources */
|
|
592
|
+
async saveDB(sources) {
|
|
593
|
+
/*
|
|
594
|
+
FIXME This seems to throw an error sometimes if the backup
|
|
595
|
+
runs too quickly after writing, which maybe happens when
|
|
596
|
+
Votive runs with no changes. To guard against this, I check
|
|
597
|
+
to see if any sources have changed. With no source changes,
|
|
598
|
+
the backup should theoretically be unnecessary.
|
|
599
|
+
*/
|
|
600
|
+
if (database.location() || !sources.length) return // Only save if running in memory
|
|
601
|
+
await backup(database, databasePath)
|
|
602
|
+
},
|
|
603
|
+
|
|
604
|
+
source: {
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* @param {string} source - Source file path.
|
|
608
|
+
* @param {string} target - Destination file path.
|
|
609
|
+
* @param {number} lastModified - Source file date last modified.
|
|
610
|
+
*/
|
|
611
|
+
create(source, target, lastModified) {
|
|
612
|
+
const created = prepared.source.create.get(source, target, lastModified)
|
|
613
|
+
prepared.target.markDescendentsStale.all("%")
|
|
614
|
+
return created
|
|
615
|
+
},
|
|
616
|
+
|
|
617
|
+
/** @param {string} filePath */
|
|
618
|
+
delete(filePath) {
|
|
619
|
+
const deletedSource = prepared.source.delete.get(filePath)
|
|
620
|
+
prepared.settings.delete.all(filePath)
|
|
621
|
+
prepared.dependency.deleteByTarget.all(deletedSource.destination)
|
|
622
|
+
.forEach(({ dependent }) => {
|
|
623
|
+
prepared.target.markStale.get(dependent)
|
|
624
|
+
})
|
|
625
|
+
prepared.metadata.deleteByTarget.all(deletedSource.destination)
|
|
626
|
+
prepared.target.delete.get(deletedSource.destination)
|
|
627
|
+
return deletedSource
|
|
628
|
+
},
|
|
629
|
+
|
|
630
|
+
getAll() {
|
|
631
|
+
return prepared.source.getAll.all()
|
|
632
|
+
},
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* @param {string} filePath
|
|
636
|
+
*/
|
|
637
|
+
get(filePath) {
|
|
638
|
+
return prepared.source.get.get(filePath)
|
|
639
|
+
},
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* @param {string} source
|
|
643
|
+
* @param {number} timestamp
|
|
644
|
+
*/
|
|
645
|
+
updateTimestamp(source, timestamp) {
|
|
646
|
+
return prepared.source.update.get(timestamp, source)
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
|
|
650
|
+
setting: {
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* @param {string} folder
|
|
654
|
+
* @param {string} key
|
|
655
|
+
* @param {string} value
|
|
656
|
+
* @param {string} [source]
|
|
657
|
+
*/
|
|
658
|
+
create(folder, key, value, source = "") {
|
|
659
|
+
const type = typeof value
|
|
660
|
+
const safeValue = type === "object"
|
|
661
|
+
? JSON.stringify(value)
|
|
662
|
+
: value
|
|
663
|
+
|
|
664
|
+
const descendents = folder === ""
|
|
665
|
+
? "%"
|
|
666
|
+
: `${folder}/%`
|
|
667
|
+
|
|
668
|
+
prepared.target.markDescendentsStale.all(descendents)
|
|
669
|
+
|
|
670
|
+
return prepared.settings.create.get(folder, key, safeValue, source)
|
|
671
|
+
},
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
/** @param {string} source */
|
|
675
|
+
deleteBySource(source) {
|
|
676
|
+
const deletedSettings = prepared.settings.delete.all(source)
|
|
677
|
+
|
|
678
|
+
deletedSettings.forEach(setting => {
|
|
679
|
+
prepared.dependency.markStale.get(setting.destination)
|
|
680
|
+
prepared.dependency.markDescendentTargetsStale.all(setting.destination + "/%")
|
|
681
|
+
})
|
|
682
|
+
},
|
|
683
|
+
|
|
684
|
+
getAll() {
|
|
685
|
+
return prepared.settings.getAll.all()
|
|
686
|
+
},
|
|
687
|
+
|
|
688
|
+
/** @param {string} folder */
|
|
689
|
+
getByFolder(folder) {
|
|
690
|
+
const segments = ["", ...(folder).split(path.sep)
|
|
691
|
+
.map((_, index, array) => `${array.slice(0, index + 1).join(path.sep)}`)
|
|
692
|
+
.filter(a => a)]
|
|
693
|
+
|
|
694
|
+
const settings = prepared.settings.getByFolder.all(JSON.stringify(segments))
|
|
695
|
+
const grouped = Object.fromEntries(
|
|
696
|
+
prepared.settings.getByFolder.all(JSON.stringify(segments)).
|
|
697
|
+
map(({ label, settings }) => {
|
|
698
|
+
const targets = Object.groupBy(JSON.parse(settings), ({ destination }) => destination)
|
|
699
|
+
for (const key in targets) {
|
|
700
|
+
targets[key] = targets[key].map(({ destination, value }) => value)
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
return [label, targets]
|
|
704
|
+
})
|
|
705
|
+
)
|
|
706
|
+
|
|
707
|
+
return grouped
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
|
|
711
|
+
dependency: {
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
getAll() {
|
|
715
|
+
return prepared.dependency.getAll.all()
|
|
716
|
+
},
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* @param {string} target
|
|
720
|
+
*/
|
|
721
|
+
getAllByTarget(target) {
|
|
722
|
+
return prepared.dependency.getAllByTarget.all(target)
|
|
723
|
+
},
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* @param {object} dependencyFile
|
|
727
|
+
* @param {string} dependencyKey
|
|
728
|
+
* @param {any} dependencyValue
|
|
729
|
+
* @param {string} dependencyPath
|
|
730
|
+
* @param {string} dependentPath
|
|
731
|
+
*/
|
|
732
|
+
track(dependencyFile, dependencyKey, dependencyValue, dependencyPath, dependentPath) {
|
|
733
|
+
Object.defineProperty(dependencyFile, dependencyKey, {
|
|
734
|
+
enumerable: true,
|
|
735
|
+
get() {
|
|
736
|
+
prepared.dependency.create.get(dependencyPath, dependencyKey, dependentPath)
|
|
737
|
+
return dependencyValue
|
|
738
|
+
}
|
|
739
|
+
})
|
|
740
|
+
}
|
|
741
|
+
},
|
|
742
|
+
|
|
743
|
+
target: {
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* @param {string} filePath
|
|
747
|
+
*/
|
|
748
|
+
delete(filePath) {
|
|
749
|
+
// Note: Need to run clean-up after
|
|
750
|
+
prepared.target.delete.get(filePath)
|
|
751
|
+
prepared.metadata.deleteByTarget.all(filePath)
|
|
752
|
+
const deps = queries.dependency.getAllByTarget(filePath)
|
|
753
|
+
deps.forEach(dep => {
|
|
754
|
+
queries.target.markStale(dep.dependent)
|
|
755
|
+
})
|
|
756
|
+
},
|
|
114
757
|
|
|
758
|
+
/**
|
|
759
|
+
* @param {string} filePath
|
|
760
|
+
* @returns {TargetOutput | undefined}
|
|
761
|
+
*/
|
|
762
|
+
get(filePath) {
|
|
763
|
+
const target = prepared.target.get.get(filePath)
|
|
115
764
|
|
|
116
|
-
|
|
117
|
-
const cachedMetadata = getMetadataByPath.all(dest.path)
|
|
765
|
+
if (!target) return
|
|
118
766
|
|
|
119
|
-
|
|
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
|
-
}
|
|
767
|
+
const { metadata, abstract, ...rest } = target
|
|
131
768
|
|
|
132
|
-
|
|
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
|
-
}
|
|
769
|
+
if (!abstract) return
|
|
151
770
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
771
|
+
/** @type {TargetOutput} */
|
|
772
|
+
const copy = {
|
|
773
|
+
abstract: coerceJSON(abstract),
|
|
774
|
+
metadata: coerceJSON(metadata),
|
|
775
|
+
...rest
|
|
776
|
+
}
|
|
155
777
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return getMetadataByPath.get(path)
|
|
159
|
-
}
|
|
778
|
+
return copy
|
|
779
|
+
},
|
|
160
780
|
|
|
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
|
-
}
|
|
169
781
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
782
|
+
/**
|
|
783
|
+
* @returns {TargetOutput[]}
|
|
784
|
+
*/
|
|
785
|
+
getAll() {
|
|
786
|
+
const targets = prepared.target.getAll.all()
|
|
176
787
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
788
|
+
return targets.map(({ metadata, abstract, ...rest }) => {
|
|
789
|
+
return {
|
|
790
|
+
abstract: coerceJSON(abstract),
|
|
791
|
+
metadata: coerceJSON(metadata),
|
|
792
|
+
...rest
|
|
793
|
+
}
|
|
794
|
+
})
|
|
795
|
+
},
|
|
796
|
+
|
|
797
|
+
/**
|
|
798
|
+
* @returns {TargetOutput[]}
|
|
799
|
+
*/
|
|
800
|
+
getStale() {
|
|
801
|
+
const targets = prepared.target.getAllStale.all()
|
|
802
|
+
|
|
803
|
+
return targets.map(({ metadata, abstract, ...rest }) => {
|
|
804
|
+
return {
|
|
805
|
+
abstract: coerceJSON(abstract),
|
|
806
|
+
metadata: coerceJSON(metadata),
|
|
807
|
+
...rest
|
|
808
|
+
}
|
|
809
|
+
})
|
|
810
|
+
},
|
|
811
|
+
|
|
812
|
+
/**
|
|
813
|
+
* @param {string} filePath
|
|
814
|
+
* @param {string} dependent
|
|
815
|
+
*/
|
|
816
|
+
getWithTrackers(filePath, dependent) {
|
|
817
|
+
const target = queries.target.get(filePath)
|
|
818
|
+
|
|
819
|
+
if (!target) return
|
|
820
|
+
|
|
821
|
+
const trackedTarget = { metadata: {} }
|
|
822
|
+
|
|
823
|
+
queries.dependency.track(
|
|
824
|
+
trackedTarget,
|
|
825
|
+
"abstract",
|
|
826
|
+
target.abstract,
|
|
827
|
+
filePath,
|
|
828
|
+
dependent
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
for (const key in target.metadata) {
|
|
832
|
+
queries.dependency.track(
|
|
833
|
+
trackedTarget.metadata,
|
|
834
|
+
key,
|
|
835
|
+
target.metadata[key],
|
|
836
|
+
filePath,
|
|
837
|
+
dependent
|
|
838
|
+
)
|
|
839
|
+
}
|
|
216
840
|
|
|
217
|
-
|
|
218
|
-
|
|
841
|
+
return target
|
|
842
|
+
},
|
|
843
|
+
|
|
844
|
+
/**
|
|
845
|
+
* @typedef {object} TargetGetManyWithTrackersParams
|
|
846
|
+
* @property {string | undefined} [folder]
|
|
847
|
+
* @property {boolean | undefined} [recursive]
|
|
848
|
+
* @property {string | undefined} [dependent]
|
|
849
|
+
* @property {Record<string, string> | undefined} [query]
|
|
850
|
+
*/
|
|
851
|
+
|
|
852
|
+
/** @param {TargetGetManyWithTrackersParams | undefined} params */
|
|
853
|
+
getManyWithTrackers(params) {
|
|
854
|
+
const { folder = "", recursive, dependent, query = {} } = params
|
|
855
|
+
|
|
856
|
+
const recursivePath = [folder, "%"].filter(a => a).join("/")
|
|
857
|
+
|
|
858
|
+
const many = prepared.target.getMany
|
|
859
|
+
.all({ folder, recursivePath: recursive ? recursivePath : folder, filter: JSON.stringify(query) })
|
|
860
|
+
.map(({ abstract, metadata, ...rest }) => {
|
|
861
|
+
|
|
862
|
+
const trackedTarget = {
|
|
863
|
+
abstract: coerceJSON(abstract),
|
|
864
|
+
metadata: coerceJSON(metadata),
|
|
865
|
+
...rest
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
queries.dependency.track(
|
|
869
|
+
trackedTarget,
|
|
870
|
+
"abstract",
|
|
871
|
+
trackedTarget.abstract,
|
|
872
|
+
trackedTarget.path,
|
|
873
|
+
dependent
|
|
874
|
+
)
|
|
875
|
+
|
|
876
|
+
for (const key in trackedTarget.metadata) {
|
|
877
|
+
queries.dependency.track(
|
|
878
|
+
trackedTarget.metadata,
|
|
879
|
+
key,
|
|
880
|
+
trackedTarget.metadata[key],
|
|
881
|
+
trackedTarget.path,
|
|
882
|
+
dependent
|
|
883
|
+
)
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
return trackedTarget
|
|
887
|
+
})
|
|
888
|
+
|
|
889
|
+
|
|
890
|
+
many.sort((a, b) => {
|
|
891
|
+
if (a.metadata.date) {
|
|
892
|
+
if (b.metadata.date) {
|
|
893
|
+
return (new Date(b.metadata.date)).getTime() - (new Date(a.metadata.date)).getTime()
|
|
894
|
+
} else {
|
|
895
|
+
return -1
|
|
896
|
+
}
|
|
897
|
+
} else {
|
|
898
|
+
return 1
|
|
899
|
+
}
|
|
900
|
+
})
|
|
219
901
|
|
|
220
|
-
|
|
221
|
-
|
|
902
|
+
return many
|
|
903
|
+
},
|
|
222
904
|
|
|
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
|
-
}
|
|
241
905
|
|
|
242
|
-
|
|
906
|
+
/**
|
|
907
|
+
* @param {TargetInput} target
|
|
908
|
+
*/
|
|
909
|
+
create(target) {
|
|
910
|
+
const dir = target.path && splitURL(target.path)
|
|
911
|
+
const ext = path.extname(target.path)
|
|
912
|
+
const relativePath = path.relative("", target.path).toLowerCase()
|
|
243
913
|
|
|
244
|
-
|
|
245
|
-
* @param {string} path
|
|
246
|
-
*/
|
|
247
|
-
database.getSource = (path) => {
|
|
248
|
-
const source = selectSource.get(path)
|
|
249
|
-
return source
|
|
250
|
-
}
|
|
914
|
+
const extant = queries.target.get(target.path)
|
|
251
915
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
916
|
+
if (!extant) {
|
|
917
|
+
const created = prepared.target.create.get(
|
|
918
|
+
relativePath,
|
|
919
|
+
dir,
|
|
920
|
+
ext,
|
|
921
|
+
JSON.stringify(target.abstract)
|
|
922
|
+
)
|
|
923
|
+
prepared.metadata.create.get(relativePath, JSON.stringify(target.metadata))
|
|
924
|
+
return created
|
|
925
|
+
}
|
|
256
926
|
|
|
257
|
-
|
|
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
|
-
}
|
|
927
|
+
const keys = Object.keys(target.metadata)
|
|
275
928
|
|
|
276
|
-
|
|
929
|
+
for (const key in target.metadata) {
|
|
930
|
+
if (target.metadata[key] !== extant.metadata[key]) {
|
|
277
931
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
932
|
+
// TODO use triggers to mark dependencies as stale
|
|
933
|
+
prepared.metadata.create.get(relativePath, JSON.stringify({
|
|
934
|
+
[key]: target.metadata[key]
|
|
935
|
+
}))
|
|
281
936
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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)
|
|
937
|
+
// TODO delete dependencies after marking stale
|
|
938
|
+
prepared.dependency
|
|
939
|
+
.getByTargetAndProperty
|
|
940
|
+
.all(relativePath, key)
|
|
941
|
+
.forEach(dependency => {
|
|
942
|
+
prepared.target.markStale.get(dependency.dependent)
|
|
305
943
|
})
|
|
306
|
-
|
|
307
|
-
|
|
944
|
+
}
|
|
308
945
|
}
|
|
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
|
-
}
|
|
327
|
-
|
|
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 = ?`)
|
|
332
946
|
|
|
333
|
-
|
|
947
|
+
for (const key in extant.metadata) {
|
|
948
|
+
if (!target.metadata[key]) {
|
|
949
|
+
prepared.metadata.delete.get(relativePath, key)
|
|
334
950
|
|
|
335
|
-
|
|
951
|
+
// TODO delete dependencies after marking stale
|
|
952
|
+
prepared.dependency
|
|
953
|
+
.getByTargetAndProperty
|
|
954
|
+
.all(relativePath, key)
|
|
955
|
+
.forEach(dependency => {
|
|
956
|
+
prepared.target.markStale.get(dependency.dependent)
|
|
957
|
+
})
|
|
958
|
+
}
|
|
959
|
+
}
|
|
336
960
|
|
|
337
|
-
|
|
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
|
-
}
|
|
961
|
+
const changedAbstract = JSON.stringify(target.abstract) !== JSON.stringify(extant.abstract)
|
|
345
962
|
|
|
346
|
-
|
|
347
|
-
|
|
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)
|
|
963
|
+
if (changedAbstract) {
|
|
964
|
+
prepared.target.update.get(JSON.stringify(target.abstract), relativePath)
|
|
357
965
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
966
|
+
prepared.dependency
|
|
967
|
+
.getByTargetAndProperty
|
|
968
|
+
.all(relativePath, "abstract")
|
|
969
|
+
.forEach(dependency => {
|
|
970
|
+
prepared.target.markStale.get(dependency.dependent)
|
|
971
|
+
})
|
|
972
|
+
}
|
|
361
973
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
: staleDescendents.all(`${destinationFolder}/%`)
|
|
365
|
-
}
|
|
974
|
+
return extant
|
|
975
|
+
},
|
|
366
976
|
|
|
977
|
+
/** @param {string} filePath */
|
|
978
|
+
markFresh(filePath) {
|
|
979
|
+
return prepared.target.markFresh.get(filePath)
|
|
980
|
+
},
|
|
367
981
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const segments = destinationFolder.split(path.sep)
|
|
373
|
-
.map((_, index, array) => `'${array.slice(0, index + 1).join(path.sep)}'`)
|
|
982
|
+
/** @param {string} filePath */
|
|
983
|
+
markStale(filePath) {
|
|
984
|
+
return prepared.target.markStale.get(filePath)
|
|
985
|
+
}
|
|
374
986
|
|
|
375
|
-
|
|
376
|
-
const statement = segments.join(", ")
|
|
987
|
+
},
|
|
377
988
|
|
|
378
|
-
|
|
989
|
+
url: {
|
|
379
990
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
991
|
+
/**
|
|
992
|
+
* @param {string} url
|
|
993
|
+
* @param {string} data
|
|
994
|
+
* @param {string} destination
|
|
995
|
+
*/
|
|
996
|
+
create(url, data, destination) {
|
|
997
|
+
prepared.url.create.get(url, JSON.stringify(data))
|
|
998
|
+
},
|
|
386
999
|
|
|
387
|
-
|
|
388
|
-
|
|
1000
|
+
/** @param {string} url */
|
|
1001
|
+
get(url) {
|
|
1002
|
+
const { data } = prepared.url.get.get(url) || {}
|
|
1003
|
+
if (!data) return
|
|
1004
|
+
return JSON.parse(data)
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
389
1007
|
|
|
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
|
-
)
|
|
399
1008
|
}
|
|
400
1009
|
|
|
401
|
-
return
|
|
1010
|
+
return Object.freeze(queries)
|
|
402
1011
|
}
|
|
403
1012
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
1013
|
+
/** @param {DatabaseSync} databaseSync */
|
|
1014
|
+
function createTables(databaseSync) {
|
|
1015
|
+
databaseSync.exec(`
|
|
1016
|
+
CREATE TABLE IF NOT EXISTS sources (
|
|
1017
|
+
id INTEGER PRIMARY KEY,
|
|
1018
|
+
destination STRING,
|
|
1019
|
+
path STRING,
|
|
1020
|
+
lastModified INTEGER
|
|
1021
|
+
);
|
|
1022
|
+
|
|
1023
|
+
CREATE TABLE IF NOT EXISTS dependencies (
|
|
1024
|
+
key INTEGER PRIMARY KEY,
|
|
1025
|
+
destination STRING NOT NULL,
|
|
1026
|
+
property STRING NOT NULL,
|
|
1027
|
+
dependent STRING NOT NULL,
|
|
1028
|
+
UNIQUE(destination, property, dependent)
|
|
1029
|
+
);
|
|
1030
|
+
|
|
1031
|
+
CREATE TABLE IF NOT EXISTS destinations (
|
|
1032
|
+
key INTEGER PRIMARY KEY,
|
|
1033
|
+
path STRING UNIQUE,
|
|
1034
|
+
dir TEXT,
|
|
1035
|
+
syntax TEXT,
|
|
1036
|
+
stale INTEGER,
|
|
1037
|
+
abstract STRING
|
|
1038
|
+
);
|
|
1039
|
+
|
|
1040
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
|
1041
|
+
id INTEGER PRIMARY KEY,
|
|
1042
|
+
destination STRING,
|
|
1043
|
+
label STRING,
|
|
1044
|
+
value STRING,
|
|
1045
|
+
type STRING,
|
|
1046
|
+
UNIQUE(destination, label)
|
|
1047
|
+
);
|
|
1048
|
+
|
|
1049
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
1050
|
+
id INTEGER PRIMARY KEY,
|
|
1051
|
+
destination STRING,
|
|
1052
|
+
source STRING,
|
|
1053
|
+
label STRING,
|
|
1054
|
+
value STRING,
|
|
1055
|
+
UNIQUE(destination, label, value, source)
|
|
1056
|
+
);
|
|
1057
|
+
|
|
1058
|
+
CREATE TABLE IF NOT EXISTS urls (
|
|
1059
|
+
url STRING PRIMARY KEY,
|
|
1060
|
+
data STRING
|
|
1061
|
+
);
|
|
1062
|
+
|
|
1063
|
+
`)
|
|
1064
|
+
}
|
|
446
1065
|
|
|
447
1066
|
export default createDatabase
|