votive 0.0.8 → 0.0.9

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