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.
@@ -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 {object} TargetInput
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 {DatabaseSync} database */
58
- function prepareStatements(database) {
18
+ /** @param {string} dbPath */
19
+ function createDatabase(dbPath = ".votive.db") {
20
+ const databaseSync = loadDB(dbPath)
59
21
 
60
- const { prepare } = database
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
- * @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
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
- * @typedef {object} SQLiteDependency
87
- * @property {number} key
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
- * @typedef {object} SQLiteMetadata
95
- * @property {number} id
96
- * @property {string} destination
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
- * @typedef {object} SQLiteSetting
104
- * @typedef {number} id
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
- * @typedef {object} SQLiteURL
113
- * @typedef {string} url
114
- * @typedef {string} data
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
- return {
118
-
119
- /* SOURCES */
120
- source: {
121
-
122
- /**
123
- * @callback SQLiteSourcesCreate
124
- * @param {string} path
125
- * @param {string} destination
126
- * @param {number} timestamp
127
- * @returns {SQLiteSource}
128
- */
129
-
130
- create: /** @type {{get: SQLiteSourcesCreate}} */ (/** @type {unknown} */ (database.prepare(`
131
- INSERT INTO sources (path, destination, lastModified) VALUES (?, ?, ?)
132
- RETURNING *
133
- `))),
134
-
135
- /**
136
- * @callback SQLiteSourcesDelete
137
- * @param {string} filePath
138
- * @returns {SQLiteSource}
139
- */
140
-
141
- delete: /** @type {{get: SQLiteSourcesDelete}} */ (/** @type {unknown} */ (database.prepare(`
142
- DELETE FROM sources WHERE path = ? RETURNING *
143
- `))),
144
-
145
- /**
146
- * @callback SQLiteSourcesGet
147
- * @param {string} path
148
- * @returns {SQLiteSource}
149
- */
150
-
151
- get: /** @type {{get: SQLiteSourcesGet}} */ (/** @type {unknown} */ (database.prepare(`
152
- SELECT * FROM sources WHERE path = ?
153
- `))),
154
-
155
- /**
156
- * @callback SQLiteSourcesGetAll
157
- * @returns {SQLiteSource[]}
158
- */
159
-
160
- getAll: /** @type {{all: SQLiteSourcesGetAll}} */ (/** @type {unknown} */ (database.prepare(`
161
- SELECT * FROM sources
162
- `))),
163
-
164
-
165
- /**
166
- * @callback SQLiteSourcesUpdate
167
- * @param {number} timestamp
168
- * @param {string} filePath
169
- * @returns {void}
170
- */
171
-
172
- update: /** @type {{get: SQLiteSourcesUpdate}} */ (/** @type {unknown} */ (database.prepare(`
173
- UPDATE sources SET lastModified = ? WHERE path = ?
174
- `))),
175
- },
176
-
177
- /* TARGETS */
178
- target: {
179
-
180
- /**
181
- * @callback SQLiteTargetCreate
182
- * @param {string} path
183
- * @param {string} dir
184
- * @param {string} syntax
185
- * @param {string} abstract
186
- * @returns {SQLiteTarget}
187
- */
188
-
189
- create: /** @type {{get: SQLiteTargetCreate}} */ (/** @type {unknown} */ (database.prepare(`
190
- INSERT OR IGNORE INTO destinations (path, dir, syntax, stale, abstract)
191
- VALUES (?, ?, ?, 1, ?)
192
- RETURNING *
193
- `))),
194
-
195
-
196
- /**
197
- * @callback SQLiteTargetDelete
198
- * @param {string} targetFilePath
199
- * @returns {void}
200
- */
201
-
202
- delete: /** @type {{get: SQLiteTargetDelete}} */ (/** @type {unknown} */ (database.prepare(`
203
- DELETE FROM destinations WHERE path = ?
204
- `))),
205
-
206
- /**
207
- * @callback SQLiteTargetGet
208
- * @param {string} targetFilePath
209
- * @returns {SQLiteTarget}
210
- */
211
-
212
- get: /** @type {{get: SQLiteTargetGet}} */ (/** @type {unknown} */ (database.prepare(`
213
- WITH destination AS (
214
- SELECT * FROM destinations
215
- LEFT JOIN metadata ON destinations.path = metadata.destination
216
- WHERE path = ?
217
- )
218
- SELECT destination.path, destination.dir, destination.syntax, destination.abstract, json_group_object(destination.label, destination.value) AS metadata
219
- FROM destination
220
- GROUP BY destination.path
221
- `))),
222
-
223
- /**
224
- * @callback SQLiteTargetGetMany
225
- * @param {{ filter: JSONString, folder: string, recursivePath: string }} params
226
- * @returns {SQLiteTarget[]}
227
- */
228
-
229
- getMany: /** @type {{all: SQLiteTargetGetMany}} */ (/** @type {unknown} */ (database.prepare(`
230
- WITH matches AS (
231
- SELECT m.destination, COUNT(*) AS match_count
232
- FROM metadata m
233
- INNER JOIN json_each(:filter) j ON j.key = m.label AND j.value = m.value
234
- GROUP BY m.destination
235
- ),
236
- filter_count AS (
237
- SELECT COUNT(*) AS total FROM json_each(:filter)
238
- )
239
- SELECT d.*, json_group_object(i.label, i.value) AS metadata
240
- FROM destinations d
241
- INNER JOIN metadata i ON d.path = i.destination
242
- LEFT JOIN matches ON matches.destination = d.path
243
- CROSS JOIN filter_count f
244
- WHERE (
245
- f.total = 0
246
- OR matches.match_count = f.total
247
- )
248
- AND (
249
- d.dir = :folder
250
- OR d.dir LIKE :recursivePath
251
- )
252
- GROUP BY d.path
253
- ORDER BY d.path ASC
254
- `))),
255
-
256
-
257
- /**
258
- * @callback SQLiteTargetGetAll
259
- * @returns {SQLiteTarget[]}
260
- */
261
-
262
- getAll: /** @type {{all: SQLiteTargetGetAll}} */ (/** @type {unknown} */ (database.prepare(`
263
- SELECT destinations.*, json_group_object(i.label, i.value) AS metadata
264
- FROM destinations
265
- LEFT JOIN metadata i ON destinations.path = i.destination
266
- GROUP BY destinations.path
267
- `))),
268
-
269
- /**
270
- * @callback SQLiteTargetGetAllStale
271
- * @returns {SQLiteTarget[]}
272
- */
273
-
274
- getAllStale: /** @type {{all: SQLiteTargetGetAllStale}} */ (/** @type {unknown} */ (database.prepare(`
275
- WITH destination AS (
276
- SELECT * FROM destinations
277
- LEFT JOIN metadata ON destinations.path = metadata.destination
278
- )
279
- SELECT destination.path, destination.dir, destination.syntax, destination.abstract, json_group_object(destination.label, destination.value) AS metadata
280
- FROM destination
281
- WHERE stale = 1
282
- GROUP BY destination.path
283
- `))),
284
-
285
- /**
286
- * @callback SQLiteTargetMarkDescendentsStale
287
- * @param {string} targetFilePathPattern
288
- * @returns {SQLiteTarget[]}
289
- */
290
-
291
- markDescendentsStale: /** @type {{all: SQLiteTargetMarkDescendentsStale}} */ (/** @type {unknown} */ (database.prepare(`
292
- UPDATE destinations SET stale = 1 WHERE path LIKE ? RETURNING *
293
- `))),
294
-
295
- /**
296
- * @callback SQLiteTargetMarkFresh
297
- * @param {string} targetFilePath
298
- * @returns {SQLiteTarget[]}
299
- */
300
-
301
- markFresh: /** @type {{get: SQLiteTargetMarkFresh}} */ (/** @type {unknown} */ (database.prepare(`
302
- UPDATE destinations SET stale = 0 WHERE path = ? RETURNING *
303
- `))),
304
-
305
- /**
306
- * @callback SQLiteTargetMarkStale
307
- * @param {string} targetFilePath
308
- * @returns {SQLiteTarget}
309
- */
310
-
311
- markStale: /** @type {{get: SQLiteTargetMarkStale}} */ (/** @type {unknown} */ (database.prepare(`
312
- UPDATE destinations SET stale = 1 WHERE path = ? RETURNING *
313
- `))),
314
-
315
- /**
316
- * @callback SQLiteTargetUpdate
317
- * @param {string} abstract
318
- * @param {string} targetFilePath
319
- * @returns {SQLiteTarget}
320
- */
321
- update: /** @type {{get: SQLiteTargetUpdate}} */ (/** @type {unknown} */ (database.prepare(`
322
- UPDATE destinations
323
- SET abstract = ?
324
- WHERE path = ?
325
- `))),
326
- },
327
-
328
- /* DEPENDENCIES */
329
- dependency: {
330
-
331
- /**
332
- * @callback SQLiteDependencyCreate
333
- * @param {string} destination
334
- * @param {string} property
335
- * @param {string} dependent
336
- * @returns {void}
337
- */
338
-
339
- create: /** @type {{get: SQLiteDependencyCreate}} */ (/** @type {unknown} */ (database.prepare(`
340
- INSERT OR IGNORE INTO dependencies (destination, property, dependent)
341
- VALUES (?, ?, ?)
342
- `))),
343
-
344
- /**
345
- * @callback SQLiteDependencyDeleteByTarget
346
- * @param {string} targetFilePath
347
- * @returns {SQLiteDependency[]}
348
- */
349
-
350
- deleteByTarget: /** @type {{all: SQLiteDependencyDeleteByTarget}} */ (/** @type {unknown} */ (database.prepare(`
351
- DELETE FROM dependencies WHERE destination = ? RETURNING dependent
352
- `))),
353
-
354
- /**
355
- * @callback SQLiteDependencyGetAll
356
- * @returns {SQLiteDependency[]}
357
- */
358
-
359
- getAll: /** @type {{all: SQLiteDependencyGetAll}} */ (/** @type {unknown} */ (database.prepare(`
360
- SELECT * FROM dependencies
361
- `))),
362
-
363
- /**
364
- * @callback SQLiteDependencyGetByTarget
365
- * @param {string} targetFilePath
366
- * @returns {SQLiteDependency[]}
367
- */
368
-
369
- 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
- target: {
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
- if (!target) return
116
+ // TODO: This is redundant with `getDestinationIndependently`
117
+ const cachedMetadata = getMetadataByPath.all(dest.path)
705
118
 
706
- const { metadata, abstract, ...rest } = target
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
- if (!abstract) return
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
- /** @type {TargetOutput} */
711
- const copy = {
712
- abstract: coerceJSON(abstract),
713
- metadata: coerceJSON(metadata),
714
- ...rest
715
- }
152
+ const getMetadataByPath = databaseSync.prepare(`
153
+ SELECT * FROM metadata WHERE destination = ?
154
+ `)
716
155
 
717
- return copy
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
- * @returns {TargetOutput[]}
723
- */
724
- getAll() {
725
- const targets = prepared.target.getAll.all()
170
+ database.getDestinationWithoutMetadata = () => {
171
+ return databaseSync.prepare(`
172
+ SELECT * FROM destinations
173
+ WHERE path = ?
174
+ `)
175
+ }
726
176
 
727
- return targets.map(({ metadata, abstract, ...rest }) => {
728
- return {
729
- abstract: coerceJSON(abstract),
730
- metadata: coerceJSON(metadata),
731
- ...rest
732
- }
733
- })
734
- },
735
-
736
- /**
737
- * @returns {TargetOutput[]}
738
- */
739
- getStale() {
740
- const targets = prepared.target.getAllStale.all()
741
-
742
- return targets.map(({ metadata, abstract, ...rest }) => {
743
- return {
744
- abstract: coerceJSON(abstract),
745
- metadata: coerceJSON(metadata),
746
- ...rest
747
- }
748
- })
749
- },
750
-
751
- /**
752
- * @param {string} filePath
753
- * @param {string} dependent
754
- */
755
- getWithTrackers(filePath, dependent) {
756
- const target = queries.target.get(filePath)
757
-
758
- if(!target) return
759
-
760
- const trackedTarget = { metadata: {} }
761
-
762
- queries.dependency.track(
763
- trackedTarget,
764
- "abstract",
765
- target.abstract,
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
- return target
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
- return many
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
- const extant = queries.target.get(target.path)
244
+ /**
245
+ * @param {string} path
246
+ */
247
+ database.getSource = (path) => {
248
+ const source = selectSource.get(path)
249
+ return source
250
+ }
854
251
 
855
- if (!extant) {
856
- const created = prepared.target.create.get(
857
- relativePath,
858
- dir,
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
- const keys = Object.keys(target.metadata)
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
- for (const key in target.metadata) {
869
- if (target.metadata[key] !== extant.metadata[key]) {
276
+ // TODO: DELETE SETTING
870
277
 
871
- // TODO use triggers to mark dependencies as stale
872
- prepared.metadata.create.get(relativePath, JSON.stringify({
873
- [key]: target.metadata[key]
874
- }))
278
+ database.getAllDestinations = () => {
279
+ return store.all`SELECT * FROM destinations`
280
+ }
875
281
 
876
- // TODO delete dependencies after marking stale
877
- prepared.dependency
878
- .getByTargetAndProperty
879
- .all(relativePath, key)
880
- .forEach(dependency => {
881
- prepared.target.markStale.get(dependency.dependent)
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
- for (const key in extant.metadata) {
887
- if (!target.metadata[key]) {
888
- prepared.metadata.delete.get(relativePath, key)
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
- // TODO delete dependencies after marking stale
891
- prepared.dependency
892
- .getByTargetAndProperty
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
- const changedAbstract = JSON.stringify(target.abstract) !== JSON.stringify(extant.abstract)
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
- if (changedAbstract) {
903
- prepared.target.update.get(JSON.stringify(target.abstract), relativePath)
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
- prepared.dependency
906
- .getByTargetAndProperty
907
- .all(relativePath, "abstract")
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
- return extant
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
- /** @param {string} filePath */
922
- markStale(filePath) {
923
- return prepared.target.markStale.get(filePath)
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
- url: {
378
+ const records = databaseSync.prepare(`SELECT * FROM settings WHERE destination IN (${statement})`).all()
929
379
 
930
- /**
931
- * @param {string} url
932
- * @param {string} data
933
- * @param {string} destination
934
- */
935
- create(url, data, destination) {
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
- /** @param {string} url */
940
- get(url) {
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 Object.freeze(queries)
401
+ return database
950
402
  }
951
403
 
952
- /** @param {DatabaseSync} databaseSync */
953
- function createTables(databaseSync) {
954
- databaseSync.exec(`
955
- CREATE TABLE IF NOT EXISTS sources (
956
- id INTEGER PRIMARY KEY,
957
- destination STRING,
958
- path STRING,
959
- lastModified INTEGER
960
- );
961
-
962
- CREATE TABLE IF NOT EXISTS dependencies (
963
- key INTEGER PRIMARY KEY,
964
- destination STRING NOT NULL,
965
- property STRING NOT NULL,
966
- dependent STRING NOT NULL,
967
- UNIQUE(destination, property, dependent)
968
- );
969
-
970
- CREATE TABLE IF NOT EXISTS destinations (
971
- key INTEGER PRIMARY KEY,
972
- path STRING UNIQUE,
973
- dir TEXT,
974
- syntax TEXT,
975
- stale INTEGER,
976
- abstract STRING
977
- );
978
-
979
- CREATE TABLE IF NOT EXISTS metadata (
980
- id INTEGER PRIMARY KEY,
981
- destination STRING,
982
- label STRING,
983
- value STRING,
984
- type STRING,
985
- UNIQUE(destination, label)
986
- );
987
-
988
- CREATE TABLE IF NOT EXISTS settings (
989
- id INTEGER PRIMARY KEY,
990
- destination STRING,
991
- source STRING,
992
- label STRING,
993
- value STRING,
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