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.
@@ -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 {ReturnType<createDatabase>} Database
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 {string} dbPath */
19
- function createDatabase(dbPath = ".votive.db") {
20
- const databaseSync = loadDB(dbPath)
57
+ /** @param {DatabaseSync} database */
58
+ function prepareStatements(database) {
21
59
 
22
- const store = databaseSync.createTagStore()
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
- * @param {string} folderPath
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
- * @param {string} source - Source file path.
45
- * @param {string} destination - Destination file path.
46
- * @param {number} lastModified - Source file date last modified.
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
- * @param {string} source
57
- * @param {number} lastModified
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
- 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
- }
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
- /** @param {string} path */
84
- database.freshenDependency = (path) => {
85
- freshDepen.get(path)
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
- database.getDependencies = () => {
89
- return getAllDeps.all()
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
- * @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
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
- const changedAbstract = JSON.stringify(dest.abstract) !== JSON.stringify(extant.abstract)
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
- let changedMetadata = false
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
- // TODO: This is redundant with `getDestinationIndependently`
117
- const cachedMetadata = getMetadataByPath.all(dest.path)
765
+ if (!target) return
118
766
 
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
- }
767
+ const { metadata, abstract, ...rest } = target
131
768
 
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
- }
769
+ if (!abstract) return
151
770
 
152
- const getMetadataByPath = databaseSync.prepare(`
153
- SELECT * FROM metadata WHERE destination = ?
154
- `)
771
+ /** @type {TargetOutput} */
772
+ const copy = {
773
+ abstract: coerceJSON(abstract),
774
+ metadata: coerceJSON(metadata),
775
+ ...rest
776
+ }
155
777
 
156
- /** @param {string} path */
157
- database.getAllMetadataByPath = (path) => {
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
- database.getDestinationWithoutMetadata = () => {
171
- return databaseSync.prepare(`
172
- SELECT * FROM destinations
173
- WHERE path = ?
174
- `)
175
- }
782
+ /**
783
+ * @returns {TargetOutput[]}
784
+ */
785
+ getAll() {
786
+ const targets = prepared.target.getAll.all()
176
787
 
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
- }
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
- const includeAbstract = params && params.includes("abstract")
218
- if (includeAbstract) result.abstract = JSON.parse(first.abstract)
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
- return result
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
- const selectSource = databaseSync.prepare(`SELECT * FROM sources WHERE path = ?`)
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
- database.getAllSources = () => {
253
- const sources = store.all`SELECT * FROM sources`
254
- return sources
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
- 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
- }
927
+ const keys = Object.keys(target.metadata)
275
928
 
276
- // TODO: DELETE SETTING
929
+ for (const key in target.metadata) {
930
+ if (target.metadata[key] !== extant.metadata[key]) {
277
931
 
278
- database.getAllDestinations = () => {
279
- return store.all`SELECT * FROM destinations`
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
- 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)
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
- // const updateDest = databaseSync.prepare(`UPDATE destinations SET abstract = ? WHERE path = ?`)
947
+ for (const key in extant.metadata) {
948
+ if (!target.metadata[key]) {
949
+ prepared.metadata.delete.get(relativePath, key)
334
950
 
335
- // WRITE AN UPDATE SETTINGS FUNCTION FOR WHEN A SOURCE CHANGES
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
- /** @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
- }
961
+ const changedAbstract = JSON.stringify(target.abstract) !== JSON.stringify(extant.abstract)
345
962
 
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)
963
+ if (changedAbstract) {
964
+ prepared.target.update.get(JSON.stringify(target.abstract), relativePath)
357
965
 
358
- if (!extant) return createSetting.get(destinationFolder, key, value, source)
359
- if (extant.value === value) return
360
- updateSetting.get(value, destinationFolder, key)
966
+ prepared.dependency
967
+ .getByTargetAndProperty
968
+ .all(relativePath, "abstract")
969
+ .forEach(dependency => {
970
+ prepared.target.markStale.get(dependency.dependent)
971
+ })
972
+ }
361
973
 
362
- destinationFolder === ""
363
- ? staleDescendents.all("%")
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
- * @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)}'`)
982
+ /** @param {string} filePath */
983
+ markStale(filePath) {
984
+ return prepared.target.markStale.get(filePath)
985
+ }
374
986
 
375
- segments.unshift("''")
376
- const statement = segments.join(", ")
987
+ },
377
988
 
378
- const records = databaseSync.prepare(`SELECT * FROM settings WHERE destination IN (${statement})`).all()
989
+ url: {
379
990
 
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
- })
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
- return grouped
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 database
1010
+ return Object.freeze(queries)
402
1011
  }
403
1012
 
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
- `
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