votive 0.0.5 → 0.0.7

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
@@ -4,7 +4,8 @@
4
4
 
5
5
  - [ ] ReadPaths
6
6
  - [ ] Flesh out job runners
7
- - [ ] Destination query filters
7
+ - [x] Destination query filters
8
+ - [x] Setters for metadata
8
9
 
9
10
  ## Helpers
10
11
 
package/lib/bundle.js CHANGED
@@ -7,6 +7,7 @@ import writeDestinations from "./writeDestinations.js"
7
7
  import { default as createDatabase } from "./createDatabase.js"
8
8
 
9
9
  /** @import {Database} from "./createDatabase.js" */
10
+ /** @import {ParsedPath} from "node:path" */
10
11
 
11
12
  /**
12
13
  * @typedef {object} VotivePlugin
@@ -38,6 +39,7 @@ import { default as createDatabase } from "./createDatabase.js"
38
39
  /**
39
40
  * @typedef {object} ProcessorRead
40
41
  * @property {ReadPath} [path]
42
+ * @property {ReadURL} [url]
41
43
  * @property {ReadText} [text]
42
44
  * @property {ReadAbstract} [abstract]
43
45
  * @property {ReadFolder} [folder]
@@ -49,23 +51,31 @@ import { default as createDatabase } from "./createDatabase.js"
49
51
  * @param {string} filePath
50
52
  * @param {Database} database
51
53
  * @param {VotiveConfig} config
52
- * @returns {Job[] | undefined}
54
+ * @returns {Promise<ReadTextResult>}
55
+ */
56
+
57
+ /**
58
+ * Reads a the response from a URL and returns arbitrary data.
59
+ * @callback ReadURL
60
+ * @param {Response} response
61
+ * @returns {object}
53
62
  */
54
63
 
55
64
  /**
56
65
  * @callback ReadText
57
66
  * @param {string} text
58
67
  * @param {string} filePath
68
+ * @param {string} destinationPath
59
69
  * @param {Database} database
60
70
  * @param {VotiveConfig} config
61
- * @returns {ReadTextResult | undefined}
71
+ * @returns {ReadTextResult}
62
72
  */
63
73
 
64
74
  /**
65
75
  * @typedef {object} ReadTextResult
66
76
  * @property {Jobs} [jobs]
67
- * @property {object} [metadata]
68
- * @property {Abstract} [abstract]
77
+ * @property {object} metadata
78
+ * @property {Abstract} abstract
69
79
  */
70
80
 
71
81
  /**
@@ -100,9 +110,10 @@ import { default as createDatabase } from "./createDatabase.js"
100
110
 
101
111
  /**
102
112
  * @callback ReadFolder
103
- * @param {object} Folder
113
+ * @param {object} folder
104
114
  * @param {Database} database
105
115
  * @param {VotiveConfig} config
116
+ * @param {boolean} isRoot
106
117
  * @returns {{ jobs?: Jobs, destinations?: Destination[] }}
107
118
  */
108
119
 
@@ -125,7 +136,7 @@ import { default as createDatabase } from "./createDatabase.js"
125
136
  * @param {object} destination
126
137
  * @param {Database} database
127
138
  * @param {VotiveConfig} config
128
- * @returns {{ data: string, encoding?: BufferEncoding = 'utf-8' }}
139
+ * @returns {{ data: string, buffer: Buffer, encoding?: BufferEncoding = 'utf-8' }}
129
140
  */
130
141
 
131
142
  /**
@@ -140,14 +151,22 @@ import { default as createDatabase } from "./createDatabase.js"
140
151
  * Run a job.
141
152
  * @typedef {object} Job
142
153
  * @property {object} data
143
- * @property {string} runner
144
- * @property {string} [plugin]
154
+ * @property {"text" | "blob" | "json"} format
155
+ * @property {string} [syntax]
156
+ */
157
+
158
+ /**
159
+ * @typedef {object} PathInfo
160
+ * @property {string[]} dir
161
+ * @property {ParsedPath["name"]} name
162
+ * @property {ParsedPath["ext"]} ext
163
+ * @property {boolean} [inRootDir]
145
164
  */
146
165
 
147
166
  /**
148
167
  * @callback Router
149
- * @param {string} path
150
- * @returns {string | false | undefined}
168
+ * @param {PathInfo} path
169
+ * @returns {PathInfo | false | undefined}
151
170
  */
152
171
 
153
172
  /**
@@ -172,6 +191,7 @@ import { default as createDatabase } from "./createDatabase.js"
172
191
  * @param {Database | undefined} [cache]
173
192
  */
174
193
  async function bundle(config, cache) {
194
+ // TODO: Ensure all cached destinations exist as expected
175
195
 
176
196
  // Map out all processors
177
197
  const processors = config.plugins
@@ -213,9 +233,15 @@ async function bundle(config, cache) {
213
233
  ...foldersJobs
214
234
  ], config, database)
215
235
 
236
+
237
+ await writeDestinations(config, database)
238
+
216
239
  // Back up database (only if in-memory first run)
217
240
  await database.saveDB()
218
241
 
242
+ const stale = database.getStaleDestinations()
243
+ if(stale.length > 0) await bundle(config, cache)
244
+
219
245
  return database
220
246
  }
221
247
 
@@ -2,6 +2,7 @@ import { DatabaseSync, backup } from "node:sqlite"
2
2
  import { splitURL, checkFile } from "./utils/index.js"
3
3
  import { statSync } from "node:fs"
4
4
  import path from "node:path"
5
+ import createStatement from "./sqlite.js"
5
6
 
6
7
  /**
7
8
  * @typedef {ReturnType<createDatabase>} Database
@@ -35,7 +36,7 @@ function createDatabase(dbPath = ".votive.db") {
35
36
  * @param {string} urlPath
36
37
  */
37
38
  database.createFolder = (folderPath, urlPath) => {
38
- return store.get`INSERT INTO folders (folderPath, urlPath) VALUES :${folderPath} :${urlPath} RETURNING *`
39
+ return store.get`INSERT OR IGNORE INTO folders (folderPath, urlPath) VALUES :${folderPath} :${urlPath} RETURNING *`
39
40
  }
40
41
 
41
42
  const createSource = databaseSync.prepare(`INSERT INTO sources (path, destination, lastModified) VALUES (?, ?, ?)`)
@@ -66,12 +67,12 @@ function createDatabase(dbPath = ".votive.db") {
66
67
  // These statements are cached, no need to refactor
67
68
  const createDest = databaseSync.prepare(`INSERT INTO destinations (path, dir, syntax, stale, abstract) VALUES (?, ?, ?, 1, ?)`)
68
69
  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 createMeta = databaseSync.prepare(`INSERT OR REPLACE INTO metadata (destination, label, value, type) VALUES (?, ?, ?, ?)`)
70
71
  const updateMeta = databaseSync.prepare(`UPDATE metadata SET value = ? WHERE destination = ? AND label = ?`)
71
72
  const getDepends = databaseSync.prepare(`SELECT * FROM dependencies WHERE destination = ? AND property = ?`)
72
73
  const getDependenciesByDestination = databaseSync.prepare(`SELECT * FROM dependencies WHERE destination = ?`)
73
74
  const getAllDeps = databaseSync.prepare(`SELECT * FROM dependencies`)
74
- const staleDepen = databaseSync.prepare(`UPDATE destinations SET stale = 1 WHERE path = ?`)
75
+ const staleDepen = databaseSync.prepare(`UPDATE destinations SET stale = 1 WHERE path = ? RETURNING *`)
75
76
  const freshDepen = databaseSync.prepare(`UPDATE destinations SET stale = 0 WHERE path = ? RETURNING *`)
76
77
  const staleDescendents = databaseSync.prepare(`UPDATE destinations SET stale = 1 WHERE path LIKE ? RETURNING *`)
77
78
  const getAllSettings = databaseSync.prepare(`SELECT * FROM settings`)
@@ -97,14 +98,21 @@ function createDatabase(dbPath = ".votive.db") {
97
98
  * @param {string} params.syntax - Destination abstract syntax
98
99
  */
99
100
  database.createOrUpdateDestination = ({ metadata, ...dest }) => {
100
- const [dir] = dest.path ? splitURL(dest.path) : []
101
+ const dir = dest.path && splitURL(dest.path)
101
102
  const params = Object.keys(metadata)
102
103
  if (dest.abstract) params.push("abstract")
103
104
  const extant = database.getDestinationIndependently(dest.path, params)
104
105
 
105
106
  if (!extant) {
106
107
  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
+ 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
+ })
108
116
  return
109
117
  }
110
118
 
@@ -121,7 +129,7 @@ function createDatabase(dbPath = ".votive.db") {
121
129
  cachedMetadata.splice(index, 1)
122
130
  if (JSON.stringify(metadata[key]) !== JSON.stringify(extant.metadata[key])) {
123
131
  changedMetadata = true
124
- updateMeta.get(metadata[key], dest.path, key)
132
+ updateMeta.get(JSON.stringify(metadata[key]), dest.path, key)
125
133
  const dependencies = getDepends.all(dest.path, key)
126
134
  dependencies.forEach(({ dependent }) => {
127
135
  staleDepen.get(dependent)
@@ -158,11 +166,10 @@ function createDatabase(dbPath = ".votive.db") {
158
166
  return getMetadataByPath.get(path)
159
167
  }
160
168
 
161
- /** @param {string[]} params */
162
- database.getMetadataIndependently = (params) => {
169
+ database.getMetadataIndependently = () => {
163
170
  return databaseSync.prepare(`
164
171
  SELECT * FROM destinations d
165
- LEFT JOIN metadata m ON d.path = m.destination AND m.label IN (${params.map(p => `'${p}'`).join(", ")})
172
+ LEFT JOIN metadata m ON d.path = m.destination
166
173
  WHERE d.path = ?
167
174
  `)
168
175
  }
@@ -174,16 +181,29 @@ function createDatabase(dbPath = ".votive.db") {
174
181
  `)
175
182
  }
176
183
 
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
+
177
200
  /**
178
201
  * @param {string} path
179
- * @param {string[]} [params]
180
202
  */
181
- database.getDestinationIndependently = (path, params) => {
203
+ database.getDestinationIndependently = (path) => {
182
204
  // TODO: Could potentially speed up the next two db queries by using the tag store
183
205
 
184
- const metadatas = (params && params.length)
185
- ? database.getMetadataIndependently(params).all(path)
186
- : [database.getDestinationWithoutMetadata().get(path)]
206
+ const metadatas = database.getMetadataIndependently().all(path)
187
207
 
188
208
  if (!metadatas) return
189
209
 
@@ -191,52 +211,94 @@ function createDatabase(dbPath = ".votive.db") {
191
211
 
192
212
  if (!first) return
193
213
 
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
-
214
+ const metadata = parseMetadatas(metadatas)
209
215
 
210
216
  const result = {
217
+ abstract: JSON.parse(first.abstract),
211
218
  path: first.path,
212
219
  dir: first.dir,
213
220
  syntax: first.syntax,
214
221
  metadata
215
222
  }
216
223
 
217
- const includeAbstract = params && params.includes("abstract")
218
- if (includeAbstract) result.abstract = JSON.parse(first.abstract)
219
-
220
224
  return result
221
225
  }
222
226
 
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 = ?")
223
229
  /**
224
230
  * @param {string} path
225
- * @param {string[]} properties
226
231
  * @param {string} dependent
227
232
  */
228
- database.getDestinationDependently = (path, properties, dependent) => {
229
- const params = []
230
- const deps = []
233
+ database.getDestinationDependently = (path, dependent) => {
234
+ const result = database.getDestinationIndependently(path)
235
+
236
+ const { abstract, metadata, ...copy } = result
237
+
238
+ copy.metadata = {}
231
239
 
232
- properties.forEach(p => {
233
- params.push(p)
234
- deps.push(`('${path}', '${p}', '${dependent}')`)
240
+ Object.defineProperty(copy, "abstract", {
241
+ enumerable: true,
242
+ get() {
243
+ createDependency.get(path, "abstract", dependent)
244
+ return abstract
245
+ }
235
246
  })
236
247
 
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()
248
+ const keys = Object.keys(metadata)
249
+
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]
256
+ }
257
+ })
258
+ })
259
+
260
+
261
+ return copy
262
+ }
263
+
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
+ }
276
+
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
+ }
284
+ }
285
+ )
286
+ }
287
+
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
+ }
295
+ }
296
+ )
297
+
298
+ return pv
299
+ }, {}))
300
+
301
+ return destinations
240
302
  }
241
303
 
242
304
  const selectSource = databaseSync.prepare(`SELECT * FROM sources WHERE path = ?`)
@@ -285,8 +347,6 @@ function createDatabase(dbPath = ".votive.db") {
285
347
  LEFT JOIN metadata m on d.path = m.destination
286
348
  WHERE stale = 1`
287
349
 
288
- console.log({ metadatas })
289
-
290
350
  const grouped = metadatas.map((value, index, array) => {
291
351
  if (array.slice(0, index)
292
352
  .find(prior => prior.path === value.path)
@@ -298,6 +358,7 @@ function createDatabase(dbPath = ".votive.db") {
298
358
  dir: value.dir,
299
359
  syntax: value.syntax,
300
360
  stale: value.stale,
361
+ abstract: JSON.parse(value.abstract),
301
362
  metadata: Object.fromEntries(
302
363
  array.slice(index)
303
364
  .filter(following => following.path === value.path)
@@ -326,7 +387,7 @@ function createDatabase(dbPath = ".votive.db") {
326
387
  return grouped
327
388
  }
328
389
 
329
- const createSetting = databaseSync.prepare(`INSERT INTO settings (destination, label, value, source) VALUES (?, ?, ?, ?) RETURNING *`)
390
+ const createSetting = databaseSync.prepare(`INSERT OR IGNORE INTO settings (destination, label, value, source) VALUES (?, ?, ?, ?) RETURNING *`)
330
391
  const selectSetting = databaseSync.prepare(`SELECT * FROM settings WHERE label = ? AND destination = ?`)
331
392
  const updateSetting = databaseSync.prepare(`UPDATE settings SET value = ? WHERE destination = ? AND label = ? RETURNING *`)
332
393
  const deleteSettings = databaseSync.prepare(`DELETE FROM settings WHERE source = ?`)
@@ -356,9 +417,14 @@ function createDatabase(dbPath = ".votive.db") {
356
417
  database.setSetting = (destinationFolder, key, value, source = "") => {
357
418
  const extant = selectSetting.get(key, destinationFolder)
358
419
 
359
- if (!extant) return createSetting.get(destinationFolder, key, value, source)
360
- if (extant.value === value) return
361
- updateSetting.get(value, destinationFolder, key)
420
+ const type = typeof value
421
+ const safeValue = type === "object"
422
+ ? JSON.stringify(value)
423
+ : value
424
+
425
+ if (!extant) return createSetting.get(destinationFolder, key, safeValue, source)
426
+ if (extant.value === safeValue) return
427
+ updateSetting.get(safeValue, destinationFolder, key)
362
428
 
363
429
  destinationFolder === ""
364
430
  ? staleDescendents.all("%")
@@ -399,6 +465,29 @@ function createDatabase(dbPath = ".votive.db") {
399
465
  )
400
466
  }
401
467
 
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 = ?`)
470
+
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
+ }
481
+
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
+ }
490
+
402
491
  return database
403
492
  }
404
493
 
@@ -414,7 +503,8 @@ CREATE TABLE IF NOT EXISTS dependencies (
414
503
  key INTEGER PRIMARY KEY,
415
504
  destination STRING NOT NULL,
416
505
  property STRING NOT NULL,
417
- dependent STRING NOT NULL
506
+ dependent STRING NOT NULL,
507
+ UNIQUE(destination, property, dependent)
418
508
  );
419
509
 
420
510
  CREATE TABLE IF NOT EXISTS destinations (
@@ -440,7 +530,13 @@ CREATE TABLE IF NOT EXISTS settings (
440
530
  destination STRING,
441
531
  source STRING,
442
532
  label STRING,
443
- value 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
444
540
  );
445
541
 
446
542
  `
@@ -38,7 +38,7 @@ function readAbstracts(abstracts, config, database, processors) {
38
38
  ) return recursiveProcess(rest, abstract, jobs)
39
39
 
40
40
  const processed = processor.read.abstract(abstract, database, config)
41
- processed.jobs && processed.jobs.forEach(job => job.plugin = plugin.name)
41
+ processed.jobs && processed.jobs.forEach(job => job.syntax = processor.syntax)
42
42
  return recursiveProcess(rest, processed.abstract, [...jobs, ...(processed.jobs || [])])
43
43
  }
44
44
 
@@ -0,0 +1,59 @@
1
+ /** @import {VotiveConfig, FlatProcessors, Abstract, AbstractsWithSyntax, Jobs, FlatProcessor, Abstracts} from "./bundle.js" */
2
+ /** @import {Database} from "./createDatabase.js" */
3
+ /** @import {ReadSourceFileResult} from "./readSources.js"
4
+
5
+ /**
6
+ * @typedef {object} ReadAbstractsResult
7
+ * @property {Jobs} jobs
8
+ * @property {ReadAbstractResult[]} abstracts
9
+ */
10
+
11
+ /**
12
+ * @typedef {object} ReadAbstractResult
13
+ */
14
+
15
+ /**
16
+ * @param {ReadSourceFileResult} abstracts
17
+ * @param {VotiveConfig} config
18
+ * @param {Database} database
19
+ * @param {FlatProcessors} processors
20
+ * @returns {{processedAbstracts: Abstracts, abstractsJobs: Jobs}}
21
+ */
22
+ function readBuffers(abstracts, config, database, processors) {
23
+
24
+ const processed = abstracts.flatMap(({ abstract: unprocessedAbstract, syntax }) => {
25
+
26
+ /**
27
+ * @param {FlatProcessors} processors
28
+ * @param {Abstract} abstract
29
+ * @param {Jobs} jobs
30
+ */
31
+ function recursiveProcess(processors, abstract, jobs = []) {
32
+ const [flatProcessor, ...rest] = processors
33
+ if (!flatProcessor) return { abstract, jobs }
34
+ const { processor, plugin } = flatProcessor
35
+ if (processor.syntax !== syntax
36
+ || !processor.read
37
+ || !processor.read.abstract
38
+ ) return recursiveProcess(rest, abstract, jobs)
39
+
40
+ const processed = processor.read.abstract(abstract, database, config)
41
+ processed.jobs && processed.jobs.forEach(job => job.syntax = processor.syntax)
42
+ return recursiveProcess(rest, processed.abstract, [...jobs, ...(processed.jobs || [])])
43
+ }
44
+
45
+ return recursiveProcess(processors, unprocessedAbstract)
46
+ })
47
+
48
+ const processedAbstracts = []
49
+ const abstractsJobs = []
50
+
51
+ processed.forEach(({ abstract, jobs }) => {
52
+ processedAbstracts.push(abstract)
53
+ abstractsJobs.push(...jobs)
54
+ })
55
+
56
+ return { processedAbstracts, abstractsJobs }
57
+ }
58
+
59
+ export default readAbstracts
@@ -0,0 +1,59 @@
1
+ /** @import {VotiveConfig, FlatProcessors, Abstract, AbstractsWithSyntax, Jobs, FlatProcessor, Abstracts} from "./bundle.js" */
2
+ /** @import {Database} from "./createDatabase.js" */
3
+ /** @import {ReadSourceFileResult} from "./readSources.js"
4
+
5
+ /**
6
+ * @typedef {object} ReadAbstractsResult
7
+ * @property {Jobs} jobs
8
+ * @property {ReadAbstractResult[]} abstracts
9
+ */
10
+
11
+ /**
12
+ * @typedef {object} ReadAbstractResult
13
+ */
14
+
15
+ /**
16
+ * @param {ReadSourceFileResult} abstracts
17
+ * @param {VotiveConfig} config
18
+ * @param {Database} database
19
+ * @param {FlatProcessors} processors
20
+ * @returns {{processedAbstracts: Abstracts, abstractsJobs: Jobs}}
21
+ */
22
+ function readFilePaths(abstracts, config, database, processors) {
23
+
24
+ const processed = abstracts.flatMap(({ abstract: unprocessedAbstract, syntax }) => {
25
+
26
+ /**
27
+ * @param {FlatProcessors} processors
28
+ * @param {Abstract} abstract
29
+ * @param {Jobs} jobs
30
+ */
31
+ function recursiveProcess(processors, abstract, jobs = []) {
32
+ const [flatProcessor, ...rest] = processors
33
+ if (!flatProcessor) return { abstract, jobs }
34
+ const { processor, plugin } = flatProcessor
35
+ if (processor.syntax !== syntax
36
+ || !processor.read
37
+ || !processor.read.abstract
38
+ ) return recursiveProcess(rest, abstract, jobs)
39
+
40
+ const processed = processor.read.abstract(abstract, database, config)
41
+ processed.jobs && processed.jobs.forEach(job => job.syntax = processor.syntax)
42
+ return recursiveProcess(rest, processed.abstract, [...jobs, ...(processed.jobs || [])])
43
+ }
44
+
45
+ return recursiveProcess(processors, unprocessedAbstract)
46
+ })
47
+
48
+ const processedAbstracts = []
49
+ const abstractsJobs = []
50
+
51
+ processed.forEach(({ abstract, jobs }) => {
52
+ processedAbstracts.push(abstract)
53
+ abstractsJobs.push(...jobs)
54
+ })
55
+
56
+ return { processedAbstracts, abstractsJobs }
57
+ }
58
+
59
+ export default readFilePaths
@@ -1,8 +1,9 @@
1
+ import path from "node:path"
2
+
1
3
  /** @import {VotiveConfig, FlatProcessors} from "./bundle.js" */
2
4
  /** @import {Database} from "./createDatabase.js" */
3
5
  /** @import {Dirent} from "node:fs" */
4
6
 
5
- import path from "node:path"
6
7
 
7
8
  /**
8
9
  * @param {Dirent[]} folders
@@ -17,11 +18,13 @@ function readFolders(folders = [], config, database, processors) {
17
18
  const processed = folderProcessors.flatMap(({ processor, plugin }) => {
18
19
 
19
20
  const jobs = folders.flatMap(folder => {
20
- const folderPath = path.relative(config.sourceFolder, path.join(folder.parentPath, folder.name))
21
+ let folderPath = path.join(folder.parentPath, folder.name)
22
+ if(folderPath) folderPath += path.sep
23
+
21
24
 
22
25
  /** @ts-ignore `.read` is throwing a warning, but it's guarded above */
23
26
  const { destinations, jobs } = processor.read.folder(folderPath, database, config)
24
- jobs.forEach(job => job.plugin = plugin.name)
27
+ jobs.forEach(job => job.syntax = processor.syntax)
25
28
  if (destinations) {
26
29
  destinations.forEach(destination => {
27
30
  database.createOrUpdateDestination(destination)
@@ -30,7 +33,14 @@ function readFolders(folders = [], config, database, processors) {
30
33
  }
31
34
  })
32
35
 
33
- const rootFolder = processor.read.folder("", database, config)
36
+
37
+ const rootInfo = path.parse(config.sourceFolder)
38
+ const rootPath = rootInfo.name
39
+ ? path.format(rootInfo) + path.sep
40
+ : ""
41
+
42
+ // TODO: Add isRoot to type definition
43
+ const rootFolder = processor.read.folder(rootPath, database, config, true)
34
44
 
35
45
  if (rootFolder.destinations) {
36
46
  rootFolder.destinations.forEach(destination => {
@@ -38,7 +48,7 @@ function readFolders(folders = [], config, database, processors) {
38
48
  })
39
49
  }
40
50
 
41
- rootFolder.jobs.forEach(job => job.plugin = plugin.name)
51
+ rootFolder.jobs.forEach(job => job.syntax = processor.syntax)
42
52
 
43
53
  if(rootFolder.jobs) jobs.push(... rootFolder.jobs)
44
54
  return jobs
@@ -29,7 +29,7 @@ async function readSources(config, database, processors) {
29
29
  const { files, folders } = Object.groupBy(filteredDirents, (dirent) => dirent.isFile() ? "files" : "folders")
30
30
  if (!files) return { folders, sources: [] }
31
31
  const readingSourceFiles = files.flatMap(readSourceFile(processors, database, config))
32
- const sources = readingSourceFiles && await Promise.all(readingSourceFiles)
32
+ const sources = readingSourceFiles && (await Promise.all(readingSourceFiles)).filter(a => a)
33
33
  return {
34
34
  folders,
35
35
  sources
@@ -43,6 +43,8 @@ async function readSources(config, database, processors) {
43
43
  function fileFilter(config, database) {
44
44
  /** @param {Dirent} dirent */
45
45
  return (dirent) => {
46
+ const isDestinationFolder = !path.relative(config.destinationFolder, path.join(dirent.parentPath, dirent.name))
47
+
46
48
  if (dirent.parentPath === config.destinationFolder) {
47
49
  return false
48
50
  } else if (dirent.parentPath.startsWith(config.destinationFolder + path.sep)) {
@@ -51,6 +53,10 @@ function fileFilter(config, database) {
51
53
  return false // Ignore hidden files
52
54
  } else if (dirent.parentPath.includes(path.sep + ".")) {
53
55
  return false // Ignore hidden folders
56
+ } else if(dirent.parentPath.match(/^\.\w/)) {
57
+ return false // Ignore hidden folders
58
+ } if(isDestinationFolder) {
59
+ return false
54
60
  }
55
61
  return true
56
62
  }
@@ -92,42 +98,85 @@ function readSourceFile(processors, database, config) {
92
98
  */
93
99
  async function process(plugin, processor, config) {
94
100
  const { read, filter, syntax } = processor
95
- if (filter.extensions.includes(fileInfo.ext) && read && read.text) {
101
+ if (filter && filter.extensions.includes(fileInfo.ext) && read && (read.text || read.path)) {
96
102
 
97
103
  // Check modified time
98
104
  const stat = await fs.stat(filePath)
99
105
  const source = database.getSource(filePath)
100
106
 
101
107
 
102
- if (source && source.lastModified === Number(stat.mtimeMs.toFixed())) return { jobs: [] }
108
+ if (source && source.lastModified === Number(stat.mtimeMs.toFixed())) return null
103
109
 
104
110
  database.deleteSettings(filePath)
105
111
 
106
112
  // Get destination route
107
- const destinationPath = route(filePath, plugin)
113
+ const destinationPath = route(filePath, plugin, config)
108
114
 
109
115
  // Set URL if exists
110
- const buffer = await fs.readFile(path.format(fileInfo))
111
- const data = decodeBuffer(buffer)
112
- const { jobs, abstract, metadata } = read.text(data, filePath, database, config)
113
- jobs && jobs.forEach(job => job.plugin = plugin.name)
116
+ const allJobs = []
117
+ const destination = {}
118
+
119
+ // TODO: Rename "path" to "asset"
120
+ if (read.path) {
121
+ const {metadata, abstract, jobs} = await read.path(filePath, database, config)
122
+
123
+ database.createOrUpdateDestination({
124
+ metadata,
125
+ abstract,
126
+ path: destinationPath,
127
+ syntax
128
+ })
129
+
130
+ if(Array.isArray(jobs)) {
131
+ allJobs.push(...jobs)
132
+ }
133
+ }
114
134
 
115
- const timeStamp = stat.mtimeMs.toFixed()
135
+ if (read.text) {
136
+ const buffer = await fs.readFile(path.format(fileInfo))
137
+ const data = decodeBuffer(buffer)
116
138
 
117
- if(source) {
118
- database.updateSource(filePath, Number(timeStamp))
119
- } else {
120
- database.createSource(filePath, destinationPath, Number(timeStamp))
139
+ const { jobs, abstract, metadata } = read.text(data, filePath, destinationPath, database, config)
140
+
141
+ if(Array.isArray(jobs)) {
142
+ allJobs.push(...jobs)
143
+ }
144
+
145
+ database.createOrUpdateDestination({
146
+ abstract,
147
+ path: destinationPath,
148
+ metadata,
149
+ syntax
150
+ })
151
+
152
+ // TODO: DRY up
153
+ allJobs && allJobs.forEach(job => job.syntax = processor.syntax)
154
+ updateSource()
155
+ return { abstract, destinationPath, syntax, jobs: allJobs }
121
156
  }
122
157
 
123
- database.createOrUpdateDestination({
124
- metadata,
125
- path: destinationPath,
126
- abstract: abstract,
127
- syntax: syntax
128
- })
158
+ // TODO: DRY up
159
+ allJobs && allJobs.forEach(job => job.syntax = processor.syntax)
160
+ updateSource()
161
+
162
+ return {
163
+ jobs: allJobs,
164
+ abstract: null,
165
+ destinationPath: null,
166
+ metadata: null,
167
+ syntax
168
+ }
169
+
170
+ function updateSource() {
171
+ const timeStamp = stat.mtimeMs.toFixed()
172
+
173
+ if (source) {
174
+ database.updateSource(filePath, Number(timeStamp))
175
+ } else {
176
+ database.createSource(filePath, destinationPath, Number(timeStamp))
177
+ }
178
+ }
129
179
 
130
- return { abstract, destinationPath, syntax, jobs }
131
180
  }
132
181
  }
133
182
 
@@ -138,10 +187,22 @@ function readSourceFile(processors, database, config) {
138
187
  /**
139
188
  * @param {string} filePath
140
189
  * @param {VotivePlugin} plugin
190
+ * @param {VotiveConfig} config
141
191
  */
142
- function route(filePath, plugin) {
143
- if (!plugin.router) return ""
144
- return plugin.router(filePath) || ""
192
+ function route(filePath, plugin, config) {
193
+ const { dir, ...parsedPath } = path.parse(filePath)
194
+ const rooty = !path.relative(config.sourceFolder, dir)
195
+ const pathInfo = {
196
+ inRootDir: rooty,
197
+ dir: dir.split(path.sep),
198
+ ...parsedPath
199
+ }
200
+ if (!plugin.router) return "0"
201
+ const routedPath = plugin.router(pathInfo)
202
+ if (routedPath && typeof routedPath.dir !== "string") {
203
+ routedPath.dir = path.join(...routedPath.dir)
204
+ }
205
+ return routedPath ? path.format(routedPath) : "0"
145
206
  }
146
207
 
147
208
  export default readSources
package/lib/runJobs.js CHANGED
@@ -1,8 +1,5 @@
1
- import workerpool from "workerpool"
2
-
3
1
  /** @import {Job, Jobs, Database, VotiveConfig} from "./bundle.js" */
4
2
 
5
- const pool = workerpool.pool()
6
3
 
7
4
  /**
8
5
  * @param {Jobs} jobs
@@ -10,14 +7,37 @@ const pool = workerpool.pool()
10
7
  * @param {Database} database
11
8
  */
12
9
  async function runJobs(jobs, config, database) {
13
- const running = jobs.map(async job => {
10
+ const processors = config.plugins.flatMap(plugin => plugin.processors.flatMap(processor => processor.read?.url && ({ fetcher: processor.read.url, syntax: processor.syntax }))).filter(a => a)
11
+ const running = jobs.flatMap(async job => {
14
12
  if (!job) return
15
- const plugin = config.plugins.find(plugin => plugin.name === job.plugin)
16
- return pool.exec(plugin.runners[job.runner], [job.data])
13
+ const cachedURL = database.getURL(job.data)
14
+ if (cachedURL) return
15
+ const processing = processors.map(async processor => {
16
+ if (processor.syntax === job.syntax) {
17
+ try {
18
+ const response = await fetch(job.data)
19
+ // TODO: Change name of "job" to "url"
20
+ // TODO: Change name of "runner" to "format"
21
+ // TODO: Probably get rid of syntax filter
22
+ if (response.status >= 200 && response.status < 300) {
23
+ const data = await response[job.runner]()
24
+ const processed = processor.fetcher(data)
25
+ database.createURL(job.data, processed, job.destination)
26
+ return
27
+ } else {
28
+ console.warn(`Error fetching URL: ${job.data}`)
29
+ }
30
+ } catch (e) {
31
+ return
32
+ }
33
+ }
34
+ })
35
+
36
+ return Promise.allSettled(processing)
17
37
  })
18
38
 
19
- const ran = await Promise.allSettled(running)
20
- pool.terminate()
39
+ const settled = await Promise.allSettled(running)
40
+ return settled
21
41
  }
22
42
 
23
43
  export default runJobs
package/lib/sqlite.js ADDED
@@ -0,0 +1,76 @@
1
+ // TODO: and, or, not, between, not in, order
2
+
3
+ const operators = {
4
+ glob: "GLOB",
5
+ like: "LIKE",
6
+ in: "IN",
7
+ gt: ">",
8
+ lt: "<",
9
+ gte: ">=",
10
+ lte: "<=",
11
+ equal: "=",
12
+ notEqual: "!-"
13
+ }
14
+
15
+ const select = `SELECT * FROM destinations d`
16
+ const join = `LEFT JOIN metadata m ON d.path = m.destination`
17
+
18
+ /**
19
+ * @typedef {Condition[]} Filter
20
+ */
21
+
22
+ /**
23
+ * @typedef {object} Condition
24
+ * @property {string} property
25
+ * @property {keyof operators} operator
26
+ * @property {string | number | array} value
27
+ */
28
+
29
+ /**
30
+ * @typedef {object} Query
31
+ * @property {number} [query.limit]
32
+ * @property {number} [query.offset]
33
+ * @property {string} [query.orderBy]
34
+ * @property {Filter} [query.filter]
35
+ */
36
+
37
+ /**
38
+ * @param {Query} query
39
+ */
40
+ export default function createStatement(query) {
41
+ const segments = [select, join]
42
+ if(query.limit) segments.push(`LIMIT ${query.limit}`)
43
+ if(query.offset) segments.push(`OFFSET ${query.offset}`)
44
+ if(query.orderBy) segments.push(`ORDER BY ${query.orderBy}`)
45
+ if(query.filter) {
46
+ const conditions = query.filter.map(condition => {
47
+ const { property, operator, value } = condition
48
+
49
+ if(["dir", "abstract", "path"].includes(property)) {
50
+
51
+ return `d.${property} ${operators[operator]} ${formatValue(value)}`
52
+ }
53
+ return `m.label = '${property}' AND m.value ${operators[operator]} ${formatValue(value)}`
54
+ })
55
+
56
+ segments.push(`WHERE ${conditions.join(" AND ")}`)
57
+ }
58
+
59
+ return segments.join(`\n`)
60
+ }
61
+
62
+ function formatProperty(property) {
63
+ if(property === "abstract") return `d.${property}`
64
+ else return `m.${property}`
65
+ }
66
+
67
+ /** @param {any} value */
68
+ function formatValue(value) {
69
+ if(Array.isArray(value)) {
70
+ return `(${value.map(x => `'${x}'`).join(', ')})`
71
+ }
72
+
73
+ if(typeof value === "number") return value
74
+
75
+ return `'${value}'`
76
+ }
@@ -1,5 +1,6 @@
1
1
  import { styleText } from "node:util"
2
2
  import { statSync } from "node:fs"
3
+ import path from "path"
3
4
 
4
5
  /** @param {string} label */
5
6
  export function stopwatch(label) {
@@ -18,8 +19,11 @@ export function stopwatch(label) {
18
19
  * @param {string} urlPath
19
20
  */
20
21
  export function splitURL(urlPath) {
21
- const [urlFileName, ...urlDirSegmentsReversed] = urlPath.split("/").reverse()
22
- return [urlDirSegmentsReversed.reverse().join("/") || "/", urlFileName]
22
+ const [urlFileName, ...urlDirSegmentsReversed] = urlPath.split("/").filter(a => a).reverse()
23
+ const segments = urlDirSegmentsReversed.reverse()
24
+ segments.push('')
25
+ const folder = segments.join(path.sep)
26
+ return folder
23
27
  }
24
28
 
25
29
  /** @param {string} dbPath */
@@ -1,5 +1,6 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises"
2
2
  import path from "node:path"
3
+ import { checkFile } from "./utils/index.js"
3
4
 
4
5
  /** @import {Abstract, Abstracts, VotiveConfig} from "./bundle.js" */
5
6
  /** @import {Database} from "./createDatabase.js" */
@@ -20,15 +21,29 @@ async function writeDestinations(config, database) {
20
21
  if (!destinations) return
21
22
 
22
23
  const writing = destinations.flatMap(destination => {
24
+ if(!destination.path) database.freshenDependency(destination.path)
23
25
  const destinationPath = path.join(config.destinationFolder, String(destination.path))
24
26
  const { dir } = path.parse(destinationPath)
25
- return writeProcessors.map(processor => {
27
+ return writeProcessors.map(async processor => {
26
28
  if (processor.syntax === destination.syntax) {
27
- const { data, encoding = 'utf-8' } = processor.write(destination, database, config)
29
+ const writeInfo = await processor.write(destination, database, config)
30
+ if(!writeInfo) return
31
+ const { data, buffer, encoding = 'utf-8' } = writeInfo
28
32
 
29
33
  async function write() {
30
- await mkdir(dir, { recursive: true })
31
- await writeFile(destinationPath, data, encoding)
34
+ const destinationExists = checkFile(dir)
35
+
36
+ // TODO: Avoid collisions
37
+ if(!destinationExists) {
38
+ await mkdir(dir, { recursive: true })
39
+ }
40
+
41
+ if(buffer) {
42
+ await writeFile(destinationPath, buffer)
43
+ } else if(data) {
44
+ await writeFile(destinationPath, data, encoding)
45
+ }
46
+
32
47
  database.freshenDependency(destination.path)
33
48
  }
34
49
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "votive",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "A file processor.",
5
5
  "homepage": "https://github.com/samlfair/votive#readme",
6
6
  "bugs": {
package/tests/.votive.db CHANGED
Binary file
package/tests/index.js CHANGED
@@ -56,7 +56,7 @@ test("internals", async (t) => {
56
56
 
57
57
 
58
58
  /** @type {ReadText} */
59
- function exampleTextReader(text, filePath, database, config) {
59
+ function exampleTextReader(text, filePath, destinationPath, database, config) {
60
60
  const matches = text.match(/\b\w+\b/)
61
61
  const title = matches ? matches[0] : "Untitled"
62
62
 
@@ -86,7 +86,7 @@ test("internals", async (t) => {
86
86
  jobs: [createExampleJob()],
87
87
  destinations: [
88
88
  {
89
- path: "/index.html",
89
+ path: "index.html",
90
90
  abstract: { content: "" },
91
91
  metadata: {
92
92
  title: "home"
@@ -116,9 +116,8 @@ test("internals", async (t) => {
116
116
  }
117
117
 
118
118
  /** @param {string} sourcePath */
119
- function router(sourcePath) {
120
- const { base, ...parsed }= path.parse(sourcePath)
121
- return path.format({ ...parsed, ext: ".html" })
119
+ function router({ base, ...parsed }) {
120
+ return { ...parsed, ext: ".html" }
122
121
  }
123
122
 
124
123
  /** @type {VotivePlugin} */
@@ -207,7 +206,9 @@ test("internals", async (t) => {
207
206
  })
208
207
 
209
208
  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")
209
+ const destination = database.getDestinationDependently("abc.html", "abc/def.html")
210
+
211
+ console.info(`'abc/def.html' requests the property 'a' from 'abc.html', creating a dependency: ${destination.metadata.a}`)
211
212
 
212
213
  const dependencies = database.getDependencies()
213
214
 
@@ -293,7 +294,9 @@ test("internals", async (t) => {
293
294
  runJobs(jobs, config, database)
294
295
 
295
296
  const destinations = database.getAllDestinations()
296
- database.getDestinationDependently("markdown/prunee.html", ["title"], "abc/def.html")
297
+ const prunee = database.getDestinationDependently("markdown/prunee.html", "abc/def.html")
298
+
299
+ console.info(`'abc/def.html' requests the property 'title' from 'markdown/prunee.html', creating a dependency: ${prunee.metadata.title}`)
297
300
  const newDependencies = database.getDependencies()
298
301
 
299
302
  fs.rmSync("markdown/prunee.md")
@@ -310,6 +313,20 @@ test("internals", async (t) => {
310
313
  assert(!prunedMetadata)
311
314
  })
312
315
 
316
+ const finalDestinations = database.getDestinations({
317
+ filter: [
318
+ {
319
+ property: "a",
320
+ operator: "gt",
321
+ value: 3
322
+ }
323
+ ]
324
+ }, "markdown/prunee.html")
325
+
326
+ const finalDependencies = database.getDependencies()
327
+
328
+ // TODO: Write a test for get destinations
329
+
313
330
  await database.saveDB()
314
331
 
315
332
  temp.remove()