votive 0.0.3 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,11 @@
1
1
  # Votive
2
2
 
3
- TK
3
+ ## Roadmap
4
+
5
+ - [ ] ReadPaths
6
+ - [ ] Flesh out job runners
7
+ - [ ] Destination query filters
8
+
9
+ ## Helpers
10
+
11
+ - Read
package/lib/bundle.js CHANGED
@@ -1,9 +1,10 @@
1
- import { default as createDatabase } from "./createDatabase.js"
2
- import readSources from "./readSources.js"
1
+ import path from "node:path"
3
2
  import readAbstracts from "./readAbstracts.js"
4
3
  import readFolders from "./readFolders.js"
5
- import writeDestinations from "./writeDestinations.js"
4
+ import readSources from "./readSources.js"
6
5
  import runJobs from "./runJobs.js"
6
+ import writeDestinations from "./writeDestinations.js"
7
+ import { default as createDatabase } from "./createDatabase.js"
7
8
 
8
9
  /** @import {Database} from "./createDatabase.js" */
9
10
 
@@ -54,6 +55,7 @@ import runJobs from "./runJobs.js"
54
55
  /**
55
56
  * @callback ReadText
56
57
  * @param {string} text
58
+ * @param {string} filePath
57
59
  * @param {Database} database
58
60
  * @param {VotiveConfig} config
59
61
  * @returns {ReadTextResult | undefined}
@@ -101,6 +103,15 @@ import runJobs from "./runJobs.js"
101
103
  * @param {object} Folder
102
104
  * @param {Database} database
103
105
  * @param {VotiveConfig} config
106
+ * @returns {{ jobs?: Jobs, destinations?: Destination[] }}
107
+ */
108
+
109
+ /**
110
+ * @typedef {object} Destination
111
+ * @property {string} path
112
+ * @property {object} metadata
113
+ * @property {Abstract} abstract
114
+ * @property {string} syntax
104
115
  */
105
116
 
106
117
  /**
@@ -158,21 +169,54 @@ import runJobs from "./runJobs.js"
158
169
 
159
170
  /**
160
171
  * @param {VotiveConfig} config
172
+ * @param {Database | undefined} [cache]
161
173
  */
162
- async function bundle(config) {
174
+ async function bundle(config, cache) {
175
+
176
+ // Map out all processors
163
177
  const processors = config.plugins
164
178
  && config.plugins.flatMap(plugin => plugin.processors && plugin.processors.map(processor => ({ plugin, processor })))
165
179
 
166
- const database = createDatabase()
180
+ // Create database
181
+ const database = cache || createDatabase(path.join(config.sourceFolder, ".votive.db"))
182
+
183
+ /*
184
+ Note: If no cache is provided or located, the database
185
+ will automatically run in memory, based on the assumption
186
+ that Votive is running for the first time. The in-
187
+ memory database will run much faster and then back up
188
+ to the file system. When Votive next launches from the
189
+ cached disk database, the read/writes will be a little
190
+ slower, but startup will be much faster, so it should
191
+ even out.
192
+ */
193
+
194
+ // Read folders and source files
167
195
  const { folders, sources } = await readSources(config, database, processors)
168
196
 
197
+ // Map out jobs from source files
169
198
  const sourcesJobs = sources.flatMap(source => source.jobs) || []
170
- const { processedAbstracts, abstractsJobs } = readAbstracts(sources, config, database, processors)
199
+
200
+ // Process source file abstracts and map jobs
201
+ const { abstractsJobs } = readAbstracts(sources, config, database, processors)
202
+
203
+ // Scan folders and map out jobs
171
204
  const foldersJobs = readFolders(folders, config, database, processors) || []
205
+
206
+ // Write destination files
172
207
  await writeDestinations(config, database)
173
208
 
174
- const jobs = [...sourcesJobs, ...abstractsJobs, ...foldersJobs]
175
- await runJobs(jobs, config, database)
209
+ // Run all jobs
210
+ await runJobs([
211
+ ...sourcesJobs,
212
+ ...abstractsJobs,
213
+ ...foldersJobs
214
+ ], config, database)
215
+
216
+ // Back up database (only if in-memory first run)
217
+ await database.saveDB()
218
+
219
+ return database
176
220
  }
177
221
 
178
222
  export default bundle
@@ -1,23 +1,23 @@
1
1
  import { DatabaseSync, backup } from "node:sqlite"
2
- import { splitURL } from "./utils/index.js"
2
+ import { splitURL, checkFile } from "./utils/index.js"
3
3
  import { statSync } from "node:fs"
4
+ import path from "node:path"
4
5
 
5
6
  /**
6
7
  * @typedef {ReturnType<createDatabase>} Database
7
8
  */
8
9
 
9
10
 
10
- function checkFile(path) {
11
- try {
12
- return statSync(path)
13
- } catch(e) {
14
- return null
15
- }
11
+ /** @param {string} dbPath */
12
+ function loadDB(dbPath) {
13
+ if (checkFile(dbPath)) return new DatabaseSync(dbPath)
14
+ return new DatabaseSync(":memory:")
16
15
  }
17
16
 
18
17
 
19
- function createDatabase() {
20
- const databaseSync = new DatabaseSync(".votive.db")
18
+ /** @param {string} dbPath */
19
+ function createDatabase(dbPath = ".votive.db") {
20
+ const databaseSync = loadDB(dbPath)
21
21
 
22
22
  const store = databaseSync.createTagStore()
23
23
 
@@ -25,6 +25,11 @@ function createDatabase() {
25
25
 
26
26
  const database = {}
27
27
 
28
+ database.saveDB = async () => {
29
+ if (databaseSync.location()) return // Only save if running in memory
30
+ await backup(databaseSync, dbPath)
31
+ }
32
+
28
33
  /**
29
34
  * @param {string} folderPath
30
35
  * @param {string} urlPath
@@ -42,9 +47,21 @@ function createDatabase() {
42
47
  */
43
48
  database.createSource = (source, destination, lastModified) => {
44
49
  databaseSync.prepare(``) // SQLite bug. Query fails without this.
45
- const inserted = createSource.get(source, destination, lastModified)
50
+ createSource.get(source, destination, lastModified)
51
+ }
52
+
53
+ const updateSource = databaseSync.prepare(`UPDATE sources SET lastModified = ? WHERE path = ?`)
54
+
55
+ /**
56
+ * @param {string} source
57
+ * @param {number} lastModified
58
+ */
59
+ database.updateSource = (source, lastModified) => {
60
+ updateSource.get(lastModified, source)
46
61
  }
47
62
 
63
+ const getSettingsBySource = databaseSync.prepare(`SELECT * FROM settings WHERE source = ?`)
64
+
48
65
 
49
66
  // These statements are cached, no need to refactor
50
67
  const createDest = databaseSync.prepare(`INSERT INTO destinations (path, dir, syntax, stale, abstract) VALUES (?, ?, ?, 1, ?)`)
@@ -52,8 +69,25 @@ function createDatabase() {
52
69
  const createMeta = databaseSync.prepare(`INSERT INTO metadata (destination, label, value, type) VALUES (?, ?, ?, ?)`)
53
70
  const updateMeta = databaseSync.prepare(`UPDATE metadata SET value = ? WHERE destination = ? AND label = ?`)
54
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`)
55
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 *`)
56
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
+ }
57
91
 
58
92
  /**
59
93
  * @param {object} params
@@ -75,10 +109,18 @@ function createDatabase() {
75
109
  }
76
110
 
77
111
  const changedAbstract = JSON.stringify(dest.abstract) !== JSON.stringify(extant.abstract)
78
- const changedMetadata = []
112
+
113
+ let changedMetadata = false
114
+
115
+
116
+ // TODO: This is redundant with `getDestinationIndependently`
117
+ const cachedMetadata = getMetadataByPath.all(dest.path)
79
118
 
80
119
  for (const key in metadata) {
120
+ const index = cachedMetadata.findIndex(el => el.label === key)
121
+ cachedMetadata.splice(index, 1)
81
122
  if (JSON.stringify(metadata[key]) !== JSON.stringify(extant.metadata[key])) {
123
+ changedMetadata = true
82
124
  updateMeta.get(metadata[key], dest.path, key)
83
125
  const dependencies = getDepends.all(dest.path, key)
84
126
  dependencies.forEach(({ dependent }) => {
@@ -87,8 +129,19 @@ function createDatabase() {
87
129
  }
88
130
  }
89
131
 
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
+
90
143
  if (changedAbstract) {
91
- const updated = updateDest.get(JSON.stringify(dest.abstract), dest.path)
144
+ updateDest.get(JSON.stringify(dest.abstract), dest.path)
92
145
  const dependencies = getDepends.all(dest.path, "abstract")
93
146
  dependencies.forEach(({ dependent }) => {
94
147
  staleDepen.get(dependent)
@@ -96,17 +149,43 @@ function createDatabase() {
96
149
  }
97
150
  }
98
151
 
152
+ const getMetadataByPath = databaseSync.prepare(`
153
+ SELECT * FROM metadata WHERE destination = ?
154
+ `)
155
+
156
+ /** @param {string} path */
157
+ database.getAllMetadataByPath = (path) => {
158
+ return getMetadataByPath.get(path)
159
+ }
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
+ }
169
+
170
+ database.getDestinationWithoutMetadata = () => {
171
+ return databaseSync.prepare(`
172
+ SELECT * FROM destinations
173
+ WHERE path = ?
174
+ `)
175
+ }
176
+
99
177
  /**
100
178
  * @param {string} path
101
- * @param {string[]} params
179
+ * @param {string[]} [params]
102
180
  */
103
181
  database.getDestinationIndependently = (path, params) => {
104
182
  // TODO: Could potentially speed up the next two db queries by using the tag store
105
- const metadatas = databaseSync.prepare(`
106
- SELECT * FROM destinations d
107
- LEFT JOIN metadata m ON d.path = m.destination AND m.label IN (${params.map(p => `'${p}'`).join(", ")})
108
- WHERE d.path = ?
109
- `).all(path)
183
+
184
+ const metadatas = (params && params.length)
185
+ ? database.getMetadataIndependently(params).all(path)
186
+ : [database.getDestinationWithoutMetadata().get(path)]
187
+
188
+ if (!metadatas) return
110
189
 
111
190
  const [first] = metadatas
112
191
 
@@ -123,7 +202,7 @@ function createDatabase() {
123
202
  : typeof value !== "string"
124
203
  ? value
125
204
  : JSON.parse(value)
126
- return [label, value]
205
+ return [label, originalValue]
127
206
  })
128
207
  )
129
208
 
@@ -135,7 +214,7 @@ function createDatabase() {
135
214
  metadata
136
215
  }
137
216
 
138
- const includeAbstract = params.includes("abstract")
217
+ const includeAbstract = params && params.includes("abstract")
139
218
  if (includeAbstract) result.abstract = JSON.parse(first.abstract)
140
219
 
141
220
  return result
@@ -157,7 +236,7 @@ function createDatabase() {
157
236
 
158
237
  const result = database.getDestinationIndependently(path, params)
159
238
  if (result.abstract) deps.push(`('${path}, 'abstract', '${dependent}')`)
160
- const createDepp = databaseSync.prepare(`INSERT INTO dependencies (destination, property, dependent) VALUES ${deps.join(", ")}`).all()
239
+ databaseSync.prepare(`INSERT INTO dependencies (destination, property, dependent) VALUES ${deps.join(", ")} RETURNING *`).all()
161
240
  }
162
241
 
163
242
  const selectSource = databaseSync.prepare(`SELECT * FROM sources WHERE path = ?`)
@@ -175,45 +254,138 @@ function createDatabase() {
175
254
  return sources
176
255
  }
177
256
 
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
+ }
275
+
276
+ // TODO: DELETE SETTING
277
+
178
278
  database.getAllDestinations = () => {
179
279
  return store.all`SELECT * FROM destinations`
180
280
  }
181
281
 
182
282
  database.getStaleDestinations = () => {
183
- return store.all`SELECT * FROM destinations WHERE stale = 1`
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
+ console.log({ metadatas })
289
+
290
+ const grouped = metadatas.map((value, index, array) => {
291
+ if (array.slice(0, index)
292
+ .find(prior => prior.path === value.path)
293
+ ) {
294
+ return null
295
+ } else {
296
+ return {
297
+ path: value.path,
298
+ dir: value.dir,
299
+ syntax: value.syntax,
300
+ stale: value.stale,
301
+ metadata: Object.fromEntries(
302
+ array.slice(index)
303
+ .filter(following => following.path === value.path)
304
+ .map(succeeding => {
305
+ return reconstituteMetadata(succeeding)
306
+ })
307
+ )
308
+
309
+ }
310
+ }
311
+ }).filter(a => a)
312
+
313
+ function reconstituteMetadata({ label, value, type }) {
314
+ const originalValue = type === "undefined"
315
+ ? undefined
316
+ : type === "string"
317
+ ? String(value)
318
+ : type === "boolean"
319
+ ? Boolean(value)
320
+ : typeof value !== "string"
321
+ ? value
322
+ : JSON.parse(value)
323
+ return [label, originalValue]
324
+ }
325
+
326
+ return grouped
184
327
  }
185
328
 
186
- const createSetting = databaseSync.prepare(`INSERT INTO settings (destination, label, value) VALUES (?, ?, ?) RETURNING *`)
329
+ const createSetting = databaseSync.prepare(`INSERT INTO settings (destination, label, value, source) VALUES (?, ?, ?, ?) RETURNING *`)
187
330
  const selectSetting = databaseSync.prepare(`SELECT * FROM settings WHERE label = ? AND destination = ?`)
188
- const updateSetting = databaseSync.prepare(`UPDATE settings SET value = ? WHERE destination = ? AND label = ?`)
331
+ const updateSetting = databaseSync.prepare(`UPDATE settings SET value = ? WHERE destination = ? AND label = ? RETURNING *`)
332
+ const deleteSettings = databaseSync.prepare(`DELETE FROM settings WHERE source = ?`)
189
333
 
190
334
  // const updateDest = databaseSync.prepare(`UPDATE destinations SET abstract = ? WHERE path = ?`)
191
335
 
336
+ // WRITE AN UPDATE SETTINGS FUNCTION FOR WHEN A SOURCE CHANGES
337
+
338
+ /** @param {string} source */
339
+ database.deleteSettings = (source) => {
340
+ const deleted = deleteSettings.all(source)
341
+ deleted.forEach(setting => {
342
+ staleDepen.get(setting.destination)
343
+ staleDescendents.all(`${setting.destination}/%`)
344
+ })
345
+ }
346
+
192
347
  /**
193
- * @param {string} destination
348
+ * @param {string} destinationFolder
194
349
  * @param {string} key
195
350
  * @param {string} value
351
+ * @param {string} [source]
352
+ * @example
353
+ * setSetting("", "theme", "summer", "settings.md")
354
+ * setSetting("data", "format", "csv", "/config.yaml")
196
355
  */
197
- database.setSetting = (destination, key, value) => {
198
- // TODO: Check if already exists
199
- const extant = selectSetting.get(key, destination)
356
+ database.setSetting = (destinationFolder, key, value, source = "") => {
357
+ const extant = selectSetting.get(key, destinationFolder)
200
358
 
201
- if (!extant) return createSetting.get(destination, key, value)
359
+ if (!extant) return createSetting.get(destinationFolder, key, value, source)
202
360
  if (extant.value === value) return
361
+ updateSetting.get(value, destinationFolder, key)
203
362
 
204
- updateSetting.get(value, destination, key)
205
- const stale = staleDescendents.all(`${destination}/%`)
363
+ destinationFolder === ""
364
+ ? staleDescendents.all("%")
365
+ : staleDescendents.all(`${destinationFolder}/%`)
206
366
  }
207
367
 
208
368
 
209
369
  /**
210
- * @param {string} destination
211
- * @param {string} dependent
370
+ * @param {string} destinationFolder
212
371
  */
213
- database.getSettings = (destination, dependent) => {
214
- return Object.fromEntries(databaseSync.prepare(`SELECT * FROM settings WHERE destination = '${destination}' OR destination LIKE '${destination}/%';`)
215
- .all()
216
- .map(({ label, value }) => [label, value]))
372
+ database.getSettings = (destinationFolder) => {
373
+ const segments = destinationFolder.split(path.sep)
374
+ .map((_, index, array) => `'${array.slice(0, index + 1).join(path.sep)}'`)
375
+
376
+ segments.unshift("''")
377
+ const statement = segments.join(", ")
378
+
379
+ const records = databaseSync.prepare(`SELECT * FROM settings WHERE destination IN (${statement})`).all()
380
+
381
+ const grouped = {}
382
+ records.sort((a, b) => a.length - b.length)
383
+ .forEach(record => {
384
+ if (!grouped[record.label]) grouped[record.label] = [record.value]
385
+ else grouped[record.label].push(record.value)
386
+ })
387
+
388
+ return grouped
217
389
  }
218
390
 
219
391
  database.getEverything = () => {
@@ -230,50 +402,7 @@ function createDatabase() {
230
402
  return database
231
403
  }
232
404
 
233
- /**
234
- * @param {object} data
235
- * @param {string} table
236
- * @param {DatabaseSync} databaseSync
237
- */
238
- function updateColumns(data, table, databaseSync) {
239
- const keys = new Set(Object.keys(data))
240
- const columns = listColumns(table, databaseSync)
241
- const newColumns = keys.difference(columns)
242
- for (const column of newColumns) addColumn(column, table, databaseSync)
243
- }
244
-
245
- /**
246
- * @param {string} table
247
- * @param {DatabaseSync} databaseSync
248
- * @description list the columns on a table.
249
- */
250
- function listColumns(table, databaseSync) {
251
- const columns = new Set()
252
- const tableInfo = databaseSync.prepare(`PRAGMA table_info(${table})`).all()
253
- for (const column of tableInfo) columns.add(column.name)
254
- return columns
255
- }
256
-
257
- /**
258
- * @param {string} column
259
- * @param {string} table
260
- * @param {DatabaseSync} databaseSync
261
- * @description Add a column to a table and update binary.
262
- */
263
- function addColumn(column, table, databaseSync) {
264
- databaseSync.exec(`ALTER TABLE ${table} ADD COLUMN ${column} STRING;`)
265
- }
266
-
267
405
  const sqlCreateTables = `
268
- CREATE TABLE IF NOT EXISTS jobs (
269
- id INTEGER PRIMARY KEY,
270
- pluginName TEXT NOT NULL,
271
- data STRING NOT NULL,
272
- done INTEGER NOT NULL DEFAULT 0,
273
- runner STRING NOT NULL
274
- );
275
-
276
- -- Source/destination relationships
277
406
  CREATE TABLE IF NOT EXISTS sources (
278
407
  id INTEGER PRIMARY KEY,
279
408
  destination STRING,
@@ -297,11 +426,6 @@ CREATE TABLE IF NOT EXISTS destinations (
297
426
  abstract
298
427
  );
299
428
 
300
- CREATE TABLE IF NOT EXISTS folders (
301
- path STRING PRIMARY KEY,
302
- urlPath STRING
303
- );
304
-
305
429
  CREATE TABLE IF NOT EXISTS metadata (
306
430
  id INTEGER PRIMARY KEY,
307
431
  destination STRING,
@@ -314,6 +438,7 @@ CREATE TABLE IF NOT EXISTS metadata (
314
438
  CREATE TABLE IF NOT EXISTS settings (
315
439
  id INTEGER PRIMARY KEY,
316
440
  destination STRING,
441
+ source STRING,
317
442
  label STRING,
318
443
  value STRING
319
444
  );
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { default as bundle } from "./bundle.js"
2
2
  export { default as createDatabase } from "./createDatabase.js"
3
+ export { default as pruneSources } from "./pruneSources.js"
3
4
  export { default as readAbstracts } from "./readAbstracts.js"
4
5
  export { default as readFolders } from "./readFolders.js"
5
6
  export { default as readSources } from "./readSources.js"
@@ -0,0 +1,28 @@
1
+ import { readdir } from "node:fs/promises"
2
+ import path from "node:path"
3
+ /** @import {VotiveConfig} from "./bundle.js" */
4
+ /** @import {Database} from "./createDatabase.js" */
5
+
6
+
7
+
8
+ /**
9
+ * @param {VotiveConfig} config
10
+ * @param {Database} database
11
+ */
12
+ async function pruneSources(config, database) {
13
+ const sources = database.getAllSources()
14
+ const files = (await readdir(config.sourceFolder, {
15
+ withFileTypes: true,
16
+ recursive: true
17
+ })).map(file => {
18
+ return file.isFile() ? path.join(file.parentPath, file.name) : null
19
+ }).filter(a => typeof a === "string")
20
+
21
+ sources.forEach(source => {
22
+ if(files.includes(String(source.path))) return
23
+ database.deleteSource(String(source.path))
24
+ })
25
+
26
+ }
27
+
28
+ export default pruneSources
@@ -48,9 +48,9 @@ function readAbstracts(abstracts, config, database, processors) {
48
48
  const processedAbstracts = []
49
49
  const abstractsJobs = []
50
50
 
51
- processed.forEach(({ abstract, job}) => {
51
+ processed.forEach(({ abstract, jobs }) => {
52
52
  processedAbstracts.push(abstract)
53
- abstractsJobs.push(job)
53
+ abstractsJobs.push(...jobs)
54
54
  })
55
55
 
56
56
  return { processedAbstracts, abstractsJobs }
@@ -1,4 +1,4 @@
1
- /** @import {VotiveConfig, FlatProcessors, Jobs} from "./bundle.js" */
1
+ /** @import {VotiveConfig, FlatProcessors} from "./bundle.js" */
2
2
  /** @import {Database} from "./createDatabase.js" */
3
3
  /** @import {Dirent} from "node:fs" */
4
4
 
@@ -9,20 +9,42 @@ import path from "node:path"
9
9
  * @param {VotiveConfig} config
10
10
  * @param {Database} database
11
11
  * @param {FlatProcessors} processors
12
- * @returns {Jobs}
13
12
  */
14
- function readFolders(folders, config, database, processors) {
15
- if(!folders) return
16
- return folders.flatMap(folder => {
17
- return config.plugins.flatMap(plugin => {
18
- return plugin.processors.flatMap(processor => {
19
- const folderPath = path.relative(config.sourceFolder, path.join(folder.parentPath, folder.name))
20
- return processor.read
21
- && processor.read.folder
22
- && processor.read.folder(folderPath, database, config)
23
- })
13
+ function readFolders(folders = [], config, database, processors) {
14
+
15
+ const folderProcessors = processors.filter(({ processor }) => processor.read && processor.read.folder)
16
+
17
+ const processed = folderProcessors.flatMap(({ processor, plugin }) => {
18
+
19
+ const jobs = folders.flatMap(folder => {
20
+ const folderPath = path.relative(config.sourceFolder, path.join(folder.parentPath, folder.name))
21
+
22
+ /** @ts-ignore `.read` is throwing a warning, but it's guarded above */
23
+ const { destinations, jobs } = processor.read.folder(folderPath, database, config)
24
+ jobs.forEach(job => job.plugin = plugin.name)
25
+ if (destinations) {
26
+ destinations.forEach(destination => {
27
+ database.createOrUpdateDestination(destination)
28
+ })
29
+ return jobs
30
+ }
24
31
  })
32
+
33
+ const rootFolder = processor.read.folder("", database, config)
34
+
35
+ if (rootFolder.destinations) {
36
+ rootFolder.destinations.forEach(destination => {
37
+ database.createOrUpdateDestination(destination)
38
+ })
39
+ }
40
+
41
+ rootFolder.jobs.forEach(job => job.plugin = plugin.name)
42
+
43
+ if(rootFolder.jobs) jobs.push(... rootFolder.jobs)
44
+ return jobs
25
45
  })
46
+
47
+ return processed
26
48
  }
27
49
 
28
50
  export default readFolders
@@ -25,8 +25,6 @@ async function readSources(config, database, processors) {
25
25
  recursive: true
26
26
  })
27
27
 
28
- // TODO: Check file modified time
29
-
30
28
  const filteredDirents = (dirents || []).filter(fileFilter(config, database))
31
29
  const { files, folders } = Object.groupBy(filteredDirents, (dirent) => dirent.isFile() ? "files" : "folders")
32
30
  if (!files) return { folders, sources: [] }
@@ -101,7 +99,9 @@ function readSourceFile(processors, database, config) {
101
99
  const source = database.getSource(filePath)
102
100
 
103
101
 
104
- if (source.lastModified === Number(stat.mtimeMs.toFixed())) return { jobs: [] }
102
+ if (source && source.lastModified === Number(stat.mtimeMs.toFixed())) return { jobs: [] }
103
+
104
+ database.deleteSettings(filePath)
105
105
 
106
106
  // Get destination route
107
107
  const destinationPath = route(filePath, plugin)
@@ -109,12 +109,17 @@ function readSourceFile(processors, database, config) {
109
109
  // Set URL if exists
110
110
  const buffer = await fs.readFile(path.format(fileInfo))
111
111
  const data = decodeBuffer(buffer)
112
- const { jobs, abstract, metadata } = read.text(data, database, config)
112
+ const { jobs, abstract, metadata } = read.text(data, filePath, database, config)
113
113
  jobs && jobs.forEach(job => job.plugin = plugin.name)
114
- // TODO: Cache: Check if file already exists
115
114
 
116
115
  const timeStamp = stat.mtimeMs.toFixed()
117
- database.createSource(filePath, destinationPath, Number(timeStamp))
116
+
117
+ if(source) {
118
+ database.updateSource(filePath, Number(timeStamp))
119
+ } else {
120
+ database.createSource(filePath, destinationPath, Number(timeStamp))
121
+ }
122
+
118
123
  database.createOrUpdateDestination({
119
124
  metadata,
120
125
  path: destinationPath,
@@ -1,4 +1,5 @@
1
1
  import { styleText } from "node:util"
2
+ import { statSync } from "node:fs"
2
3
 
3
4
  /** @param {string} label */
4
5
  export function stopwatch(label) {
@@ -20,3 +21,12 @@ export function splitURL(urlPath) {
20
21
  const [urlFileName, ...urlDirSegmentsReversed] = urlPath.split("/").reverse()
21
22
  return [urlDirSegmentsReversed.reverse().join("/") || "/", urlFileName]
22
23
  }
24
+
25
+ /** @param {string} dbPath */
26
+ export function checkFile(dbPath) {
27
+ try {
28
+ return statSync(path.normalize(dbPath))
29
+ } catch (e) {
30
+ return null
31
+ }
32
+ }
@@ -29,6 +29,7 @@ async function writeDestinations(config, database) {
29
29
  async function write() {
30
30
  await mkdir(dir, { recursive: true })
31
31
  await writeFile(destinationPath, data, encoding)
32
+ database.freshenDependency(destination.path)
32
33
  }
33
34
 
34
35
  return write()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "votive",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "A file processor.",
5
5
  "homepage": "https://github.com/samlfair/votive#readme",
6
6
  "bugs": {
@@ -19,7 +19,8 @@
19
19
  "./internals": "./lib/index.js"
20
20
  },
21
21
  "scripts": {
22
- "test": "node --test --watch tests/*.js"
22
+ "test": "node --test --watch tests/*.js",
23
+ "coverage": "node --test --experimental-test-coverage --watch tests/*.js"
23
24
  },
24
25
  "dependencies": {
25
26
  "encoding-sniffer": "^0.2.1",
package/tests/.votive.db CHANGED
Binary file
package/tests/index.js CHANGED
@@ -5,148 +5,313 @@ import path from "node:path"
5
5
  import * as votive from "votive/internals"
6
6
  import { stopwatch } from "./../lib/utils/index.js"
7
7
  import { readFolders, runJobs } from "../lib/index.js"
8
+ import { readdir, rm } from "node:fs/promises"
9
+ import { checkFile } from "./../lib/utils/index.js"
8
10
 
9
11
  /** @import {VotiveConfig, VotivePlugin, FlatProcessors, VotiveProcessor, Runner, Job} from "./../lib/bundle.js" */
12
+ /** @import {ProcessorRead, ProcessorWrite, ReadAbstract, ReadText, ReadTextResult, ReadFolder} from "./../lib/bundle.js" */
10
13
 
11
14
  process.chdir("./tests")
12
15
 
13
16
  test("empty directory", async () => {
14
- const temp = fs.mkdtempDisposableSync("destination-")
17
+ const tempDest = fs.mkdtempDisposableSync("destination-")
18
+ const tempSource = fs.mkdtempDisposableSync("source-")
15
19
 
16
20
  try {
17
21
  const stop = stopwatch("Bundle empty folder")
18
22
  await votive.bundle({
19
- sourceFolder: "./empty",
20
- destinationFolder: temp.path
23
+ sourceFolder: tempSource.path,
24
+ destinationFolder: tempDest.path,
25
+ plugins: []
21
26
  })
22
27
 
23
28
  stop()
24
29
 
25
- const dir = fs.readdirSync(temp.path)
30
+ const dir = fs.readdirSync(tempDest.path)
26
31
  assert(dir.length === 0, "Destination directory is not empty.")
27
32
  } catch (error) {
28
- temp.remove()
33
+ tempDest.remove()
34
+ tempSource.remove()
29
35
  console.error(error)
30
36
 
31
37
  }
32
38
 
33
- temp.remove()
39
+ tempDest.remove()
40
+ tempSource.remove()
34
41
  })
35
42
 
36
- test("create empty database", () => {
37
- const stop = stopwatch("Create empty database")
38
- const database = votive.createDatabase()
39
- stop()
43
+ test("internals", async (t) => {
44
+ const dbExists = checkFile(".votive.db")
45
+ if (dbExists) await rm(".votive.db")
40
46
 
41
- database.createSource('exampleSource', 'exampleDestination', 123)
42
- const [source] = database.getAllSources()
43
-
44
- assert(source.destination === 'exampleDestination')
45
- })
46
-
47
- test("read sources", async () => {
48
47
  const temp = fs.mkdtempDisposableSync("destination-")
49
48
 
50
- /** @type {Job} */
51
- const testJob = {
52
- data: "123",
53
- runner: "testRunner"
49
+ /** @returns {Job} */
50
+ function createExampleJob() {
51
+ return {
52
+ data: Math.floor(Math.random() * 1000),
53
+ runner: "exampleRunner"
54
+ }
55
+ }
56
+
57
+
58
+ /** @type {ReadText} */
59
+ function exampleTextReader(text, filePath, database, config) {
60
+ const matches = text.match(/\b\w+\b/)
61
+ const title = matches ? matches[0] : "Untitled"
62
+
63
+ /** @type {ReadTextResult} */
64
+ return {
65
+ abstract: {
66
+ content: text,
67
+ },
68
+ metadata: {
69
+ title
70
+ },
71
+ jobs: [
72
+ createExampleJob()
73
+ ]
74
+ }
75
+ }
76
+
77
+ /** @type {ReadAbstract} */
78
+ function exampleAbstractReader(abstract, database, config) {
79
+ abstract.exampleAppend = true
80
+ return { abstract, jobs: [createExampleJob()] }
81
+ }
82
+
83
+ /** @type {ReadFolder} */
84
+ function exampleFolderReader(folder, database, config) {
85
+ return {
86
+ jobs: [createExampleJob()],
87
+ destinations: [
88
+ {
89
+ path: "/index.html",
90
+ abstract: { content: "" },
91
+ metadata: {
92
+ title: "home"
93
+ },
94
+ syntax: "md"
95
+ }
96
+ ]
97
+ }
54
98
  }
55
99
 
56
100
  /** @type {VotiveProcessor} */
57
- const testProcessor = {
58
- syntax: "txt",
101
+ const exampleProcessor = {
102
+ syntax: "md",
59
103
  filter: {
60
104
  extensions: [".md"]
61
105
  },
62
106
  read: {
63
- path: (filePath, database, config) => undefined,
64
- text: (text, database, config) => ({ metadata: { foo: "bar", bang: "baz" }, abstract: { baz: "bang" } }),
65
- abstract: (abstract, database, config) => ({ abstract: { ...abstract, newAddition: true }, jobs: [testJob] }),
66
- folder: (folder, database, config) => [testJob]
107
+ text: exampleTextReader,
108
+ abstract: exampleAbstractReader,
109
+ folder: exampleFolderReader
67
110
  },
68
111
  write: (destination, database, config) => {
69
112
  return {
70
- data: "yo there",
113
+ data: "lorem ipsum",
71
114
  }
72
115
  }
73
116
  }
74
117
 
118
+ /** @param {string} sourcePath */
119
+ function router(sourcePath) {
120
+ const { base, ...parsed }= path.parse(sourcePath)
121
+ return path.format({ ...parsed, ext: ".html" })
122
+ }
123
+
75
124
  /** @type {VotivePlugin} */
76
- const testPlugin = {
77
- name: "test plugin",
125
+ const examplePlugin = {
126
+ name: "example plugin",
78
127
  runners: {
79
- testRunner
128
+ exampleRunner: exampleRunner
80
129
  },
81
- router: (path) => path,
82
- processors: [testProcessor]
130
+ router,
131
+ processors: [exampleProcessor]
83
132
  }
84
133
 
85
134
  /** @type {Runner} */
86
- async function testRunner(data, database) {
87
- const waiting = await new Promise((resolve) => setTimeout(() => resolve(data), 1000))
88
-
89
- return waiting
135
+ async function exampleRunner(data, database) {
136
+ return await new Promise((resolve) => setTimeout(() => resolve(data), 1))
90
137
  }
91
138
 
92
139
  /** @type {VotiveConfig} */
93
140
  const config = {
94
141
  sourceFolder: "./markdown",
95
142
  destinationFolder: temp.path,
96
- plugins: [testPlugin]
143
+ plugins: [examplePlugin]
97
144
  }
98
145
 
99
146
  /** @type {FlatProcessors} */
100
147
  const processors = [
101
148
  {
102
- plugin: testPlugin,
103
- processor: testProcessor
149
+ plugin: examplePlugin,
150
+ processor: exampleProcessor
104
151
  }
105
152
  ]
106
153
 
107
154
  try {
155
+ fs.writeFileSync("markdown/prunee.md", "A little content")
156
+
108
157
  const database = votive.createDatabase()
158
+ const sourcesOne = database.getAllSources()
159
+
160
+ t.test("no sources on startup", () => {
161
+ const isArray = Array.isArray(sourcesOne)
162
+ const isEmpty = !sourcesOne.length
163
+ assert(isArray && isEmpty)
164
+ })
165
+
109
166
 
110
- const moresources = database.getAllSources()
111
- console.log({ moresources })
112
167
 
113
168
  const stop = stopwatch("Read sources")
169
+
114
170
  const { folders, sources } = await votive.readSources(config, database, processors)
171
+
172
+ t.test('one folder exists', () => {
173
+ assert(folders.length === 1)
174
+ })
175
+
176
+ t.test('three sources exist', () => {
177
+ assert(sources.length === 4)
178
+ })
179
+
115
180
  stop()
116
181
 
117
182
  const sourcesJobs = sources.flatMap(source => source.jobs)
183
+
118
184
  const { processedAbstracts, abstractsJobs } = votive.readAbstracts(sources, config, database, processors)
119
185
 
186
+ t.test('transformation succeeded', () => {
187
+ assert(processedAbstracts.find(abstract => abstract.exampleAppend))
188
+ })
189
+
190
+ t.test('abstract jobs exist', () => {
191
+ assert(abstractsJobs.length > 0)
192
+ assert(abstractsJobs.every(job => job.data && job.runner))
193
+ })
194
+
120
195
  const foldersJobs = readFolders(folders, config, database, processors)
121
196
 
122
- database.createOrUpdateDestination({ metadata: { a: 1, b: 2 }, path: "abc.txt", abstract: { c: 3 }, syntax: "txt" })
123
- const destination = database.getDestinationDependently("abc.txt", ["a"], "def")
124
- database.createOrUpdateDestination({ metadata: { a: 3, b: 4 }, path: "def.txt", abstract: { c: 5 }, syntax: "txt" })
125
- database.createOrUpdateDestination({ metadata: { a: 3, b: 9 }, path: "abc.txt", abstract: { c: 4 }, syntax: "txt" })
126
- database.createOrUpdateDestination({ metadata: { a: 3, b: 9 }, path: "abc/def.txt", abstract: { c: 4 }, syntax: "txt" })
197
+ t.test("folders jobs exist", () => {
198
+ assert(foldersJobs.length > 0)
199
+ assert(foldersJobs.every(job => job.data && job.runner))
200
+ })
201
+
202
+ database.createOrUpdateDestination({ metadata: { a: 1, b: 2 }, path: "abc.html", abstract: { c: 3 }, syntax: "md" })
203
+ const firstDestination = database.getDestinationIndependently("abc.html", [])
204
+
205
+ t.test('first destination created', () => {
206
+ assert(firstDestination.path === 'abc.html')
207
+ })
208
+
209
+ database.createOrUpdateDestination({ metadata: { a: 3, b: 4 }, path: "abc/def.html", abstract: { c: 5 }, syntax: "md" })
210
+ const destination = database.getDestinationDependently("abc.html", ["a"], "abc/def.html")
127
211
 
212
+ const dependencies = database.getDependencies()
213
+
214
+ t.test('dependency created', () => {
215
+ assert(dependencies[0].dependent === 'abc/def.html')
216
+ })
217
+
218
+
219
+ const setting = database.setSetting("abc.html", "theme", "blue", "markdown/prunee.md")
220
+ const newSetting = database.setSetting("abc", "category", "Dog", "markdown/prunee.md")
221
+ const sameSetting = database.setSetting("abc", "category", "Dog", "markdown/prunee.md")
222
+ const folderSetting = database.setSetting("abc", "theme", "red", "markdown/prunee.md")
223
+
224
+ t.test('settings created', () => {
225
+ assert(setting.value === 'blue')
226
+ assert(newSetting.value === 'Dog')
227
+ assert(!sameSetting)
228
+ })
229
+
230
+ const abcSettings = database.getSettings("abc.html")
231
+
232
+ t.test('settings retrieved', () => {
233
+ assert(abcSettings.theme[0] === "blue")
234
+ })
235
+
236
+ const descendentSetting = database.setSetting("abc/def.html", "theme", "green", "abc.md")
237
+ const defSettings = database.getSettings("abc/def.html")
238
+
239
+ t.test('settings retrieved', () => {
240
+ assert(abcSettings.theme[0] === "blue")
241
+ assert(defSettings.theme[1] === "green")
242
+ })
243
+
244
+ const staleDestinations = database.getStaleDestinations()
245
+
246
+ t.test('all destinations are stale', () => {
247
+ assert(staleDestinations.length === 7)
248
+ })
249
+
250
+ const written = await votive.writeDestinations(config, database)
251
+
252
+ const staleDestinationsAfterWriting = database.getStaleDestinations()
253
+
254
+ t.test('all destinations are fresh', () => {
255
+ assert(staleDestinationsAfterWriting.length === 0)
256
+ })
257
+
258
+ const updated = database.createOrUpdateDestination({ metadata: { a: 1, b: 9 }, path: "abc.html", abstract: { c: 4 }, syntax: "md" })
259
+ const staleAfterAbstractUpdate = database.getStaleDestinations()
260
+
261
+ t.test('updated document with no side effects is stale', () => {
262
+ assert(staleAfterAbstractUpdate.length === 1)
263
+ })
264
+
265
+ await votive.writeDestinations(config, database)
266
+ const staleAfterSecondWrite = database.getStaleDestinations()
267
+
268
+ t.test('everything fresh again', () => {
269
+ assert(staleAfterSecondWrite.length === 0)
270
+ })
271
+
272
+ const updatedWithSideEffects = database.createOrUpdateDestination({ metadata: { a: 4, b: 9 }, path: "abc.html", abstract: { c: 4 }, syntax: "md" })
273
+ const staleAfterSideEffects = database.getStaleDestinations()
274
+
275
+ t.test('side effects work', () => {
276
+ assert(staleAfterSideEffects.length === 2)
277
+ })
278
+
279
+ const freshDestinations = database.getAllDestinations()
280
+
281
+ await votive.writeDestinations(config, database)
128
282
 
129
- const setting = database.setSetting("abc", "theme", "blue")
130
- const newSetting = database.setSetting("def", "category", "Dog")
131
- const sameSetting = database.setSetting("def", "category", "Dog")
132
283
  const oldSetting = database.setSetting("abc", "theme", "green")
133
284
 
134
- const settings = database.getSettings("abc", "def")
285
+ const staleAfterSettingsChange = database.getStaleDestinations()
286
+
287
+ t.test('setting affects descendents', () => {
288
+ assert(staleAfterSettingsChange.length === 1)
289
+ })
135
290
 
291
+ // TODO: Write tests for jobs
136
292
  const jobs = [...sourcesJobs, ...abstractsJobs, ...foldersJobs]
137
293
  runJobs(jobs, config, database)
138
294
 
139
- const dbResults = database.getAllSources()
295
+ const destinations = database.getAllDestinations()
296
+ database.getDestinationDependently("markdown/prunee.html", ["title"], "abc/def.html")
297
+ const newDependencies = database.getDependencies()
140
298
 
141
- const written = await votive.writeDestinations(config, database)
299
+ fs.rmSync("markdown/prunee.md")
300
+
301
+ await votive.pruneSources(config, database)
142
302
 
143
- // const everything = database.getEverything()
144
- // console.log(everything)
303
+ const result = database.getDestinationIndependently("markdown/prunee.html")
304
+ const prunedDependencies = database.getDependencies()
305
+ const prunedMetadata = database.getAllMetadataByPath("markdown/prunee.html")
306
+
307
+ t.test('source pruned', () => {
308
+ assert(!result)
309
+ assert(newDependencies.length - prunedDependencies.length === 1)
310
+ assert(!prunedMetadata)
311
+ })
145
312
 
146
- const newsources = database.getAllSources()
147
- console.log({ newsources })
313
+ await database.saveDB()
148
314
 
149
- assert(true, "Read test tk")
150
315
  temp.remove()
151
316
  } catch (error) {
152
317
  temp.remove()
@@ -0,0 +1,3 @@
1
+ # Dog
2
+
3
+ Here is a page inside a folder.
@@ -0,0 +1,3 @@
1
+ # Hey
2
+
3
+ Here is some content.
@@ -0,0 +1 @@
1
+ This is an index root.
File without changes
File without changes
File without changes