@muze-nl/simplystore 0.9.2 → 0.10.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muze-nl/simplystore",
3
- "version": "0.9.2",
3
+ "version": "0.10.1",
4
4
  "main": "src/server.mjs",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -16,14 +16,13 @@
16
16
  "bugs": "https://github.com/simplyedit/simplystore/issues",
17
17
  "homepage": "https://github.com/simplyedit/simplystore#readme",
18
18
  "dependencies": {
19
- "@muze-nl/jaqt": "^0.10.3",
19
+ "@muze-nl/jaqt": "^0.10.6",
20
20
  "@muze-nl/jsontag": "^0.10.4",
21
- "@muze-nl/od-jsontag": "^0.4.4",
21
+ "@muze-nl/od-jsontag": "^0.4.6",
22
22
  "codemirror": "^6.0.1",
23
- "express": "^4.18.1",
24
- "json-pointer": "^0.6.2",
25
- "vm2": "^3.9.13",
26
- "write-file-atomic": "^5.0.1"
23
+ "express": "^5.2.1",
24
+ "vm2": "^3.10.5",
25
+ "write-file-atomic": "^7.0.1"
27
26
  },
28
27
  "devDependencies": {
29
28
  "@eslint/js": "^9.36.0",
@@ -1,25 +1,57 @@
1
1
  import JSONTag from '@muze-nl/jsontag'
2
2
  import serialize, { stringify } from '@muze-nl/od-jsontag/src/serialize.mjs'
3
+ import Parser from '@muze-nl/od-jsontag'
3
4
  import fs from 'node:fs'
5
+ import path from 'node:path'
6
+
7
+ const __dirname = import.meta.dirname;
4
8
 
5
9
  if (process.argv.length<=3) {
6
- console.log('usage: node ./convert.mjs {inputfile} {outputfile}')
10
+ console.log('usage: node ./convert.mjs {inputfile} {outputfile} {indexlib?}')
7
11
  process.exit()
8
12
  }
9
13
 
10
14
  // parse command line
11
15
  let inputFile = process.argv[2]
12
16
  let outputFile = process.argv[3]
17
+ let indexFile = process.argv[4] || __dirname+'/../src/index.mjs'
18
+
19
+ async function main() {
20
+ // load file
21
+ let input = fs.readFileSync(inputFile, 'utf-8')
22
+
23
+ // parse jsontag
24
+ let data = JSONTag.parse(input)
13
25
 
14
- // load file
15
- let input = fs.readFileSync(inputFile, 'utf-8')
26
+ // write resultset to output
27
+ let strData = stringify(serialize(data))
28
+ fs.writeFileSync(outputFile, strData)
29
+ console.log('Converted data written to ',outputFile)
16
30
 
17
- // parse jsontag
18
- let data = JSONTag.parse(input)
31
+ // now create indexes
32
+ console.log('Using index library:', indexFile)
33
+ const index = await import(indexFile).then(mod => {
34
+ return mod.default
35
+ })
19
36
 
20
- // write resultset to output
21
- let strData = stringify(serialize(data))
37
+ // indexes need the position data which is only available after
38
+ // parsing the od-jsontag data
39
+ const parser = new Parser()
40
+ const odData = parser.parse(strData)
22
41
 
23
- fs.writeFileSync(outputFile, strData)
42
+ let meta = {
43
+ index: {
44
+ id: new Map()
45
+ },
46
+ resultArray: parser.meta.resultArray,
47
+ data: path.dirname(outputFile)
48
+ }
49
+ for (const ob of meta.resultArray) {
50
+ meta.index.id.set(JSONTag.getAttribute(ob, 'id'), ob)
51
+ }
52
+
53
+ index.create(odData, meta)
54
+ console.log('Indexes created')
55
+ }
24
56
 
25
- console.log('Converted data written to ',outputFile)
57
+ main()
@@ -0,0 +1 @@
1
+ {}
@@ -0,0 +1 @@
1
+ {}
@@ -1,10 +1,11 @@
1
1
  import JSONTag from '@muze-nl/jsontag'
2
- import {getIndex, resultSet} from '@muze-nl/od-jsontag/src/symbols.mjs'
2
+ import {getIndex, resultSet, isChanged} from '@muze-nl/od-jsontag/src/symbols.mjs'
3
3
  import Parser from '@muze-nl/od-jsontag/src/parse.mjs'
4
4
  import serialize from '@muze-nl/od-jsontag/src/serialize.mjs'
5
5
  import writeFileAtomic from 'write-file-atomic'
6
6
 
7
7
  let commands = {}
8
+ let index = {}
8
9
  let resultArr = []
9
10
  let dataspace
10
11
  let datafile, basefile, extension
@@ -79,6 +80,9 @@ export async function initialize(task) {
79
80
  commands = await import(task.commandsFile).then(mod => {
80
81
  return mod.default
81
82
  })
83
+ index = await import(task.indexFile).then(mod => {
84
+ return mod.default
85
+ })
82
86
  }
83
87
 
84
88
  export default async function runCommand(commandStr, request) {
@@ -94,6 +98,12 @@ export default async function runCommand(commandStr, request) {
94
98
  commands[task.name](dataspace, task, request, metaProxy)
95
99
  //TODO: if command/task makes no changes, skip updating data.jsontag and writing it, skip response.data
96
100
 
101
+ const changes = meta.resultArray.filter(e => e[isChanged])
102
+ //FIXME: new entities should also report isChanged = true
103
+ if (changes.length) {
104
+ changes.uuid = task.id
105
+ index.update(dataspace, meta, changes)
106
+ }
97
107
  const uint8sab = serialize(dataspace, {meta, changes: true}) // serialize only changes
98
108
  response.data = uint8sab
99
109
  response.meta = {
@@ -0,0 +1,45 @@
1
+ import fs from 'fs'
2
+ import JSONTag from '@muze-nl/jsontag'
3
+ import { getIndex } from '@muze-nl/od-jsontag/src/symbols.mjs'
4
+
5
+ export default {
6
+ create(data, meta) {
7
+ console.log('creating '+meta.data+'/index.id.json')
8
+ // jsontag parse automatically fills meta.index.id, so no need to create anything
9
+ // just store meta.index.id in index.id.json
10
+ const index = {}
11
+ for (const key of meta.index.id.keys()) {
12
+ if (!key) {
13
+ continue
14
+ }
15
+ const entity = meta.index.id.get(key)
16
+ if (!entity) {
17
+ continue
18
+ }
19
+ index[key] = entity[getIndex]
20
+ }
21
+ fs.writeFileSync(meta.data+'/index.id.json', JSON.stringify(index))
22
+ },
23
+ update(data, meta, changes) {
24
+ if (!changes.length) {
25
+ return
26
+ }
27
+ const index = {}
28
+ for (const entry of changes) {
29
+ const id = JSONTag.getAttribute(entry, 'id')
30
+ if (id) {
31
+ index[id] = entry[getIndex]
32
+ }
33
+ }
34
+ fs.writeFileSync(meta.data+'/index.id.'+changes.uuid+'.json', JSON.stringify(index))
35
+ },
36
+ load(uuid=null) {
37
+ let filename
38
+ if (!uuid) {
39
+ filename = 'index.id.json'
40
+ } else {
41
+ filename = 'index.id.'+filename+'.json'
42
+ }
43
+ return JSON.parse(fs.readFileSync(meta.data+filename))
44
+ }
45
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,19 @@
1
+ import idIndex from './index.id.mjs'
2
+ import offsetIndex from './index.offset.mjs'
3
+
4
+ export default {
5
+ create(data, meta) {
6
+ idIndex.create(data, meta)
7
+ offsetIndex.create(data, meta)
8
+ },
9
+ update(data, meta, changes) {
10
+ idIndex.update(data, meta, changes)
11
+ offsetIndex.update(data, meta, changes)
12
+ },
13
+ load(meta, uuid=null) {
14
+ return {
15
+ id: idIndex.load(uuid),
16
+ offset: offsetIndex.load(uuid)
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,39 @@
1
+ import fs from 'fs'
2
+ import JSONTag from '@muze-nl/jsontag'
3
+ import { getIndex, position } from '@muze-nl/od-jsontag/src/symbols.mjs'
4
+
5
+ export default {
6
+ create(data, meta) {
7
+ console.log('creating '+meta.data+'/index.offset.json')
8
+ // jsontag parse automatically fills meta.index.offset, so no need to create anything
9
+ const index = {}
10
+ const max = meta.resultArray.length
11
+ for (let i=0;i<max;i++) {
12
+ const entity = meta.resultArray[i]
13
+ index[i] = [ entity[position].start, entity[position].end ]
14
+ }
15
+ fs.writeFileSync(meta.data+'/index.offset.json', JSON.stringify(index))
16
+ },
17
+ update(data, meta, changes) {
18
+ if (!changes.length) {
19
+ return
20
+ }
21
+ const index = {}
22
+ for (const entry of changes) {
23
+ let pos = entry[position]
24
+ if (pos) {
25
+ index[entry[getIndex]] = [ pos.start, pos.end ]
26
+ }
27
+ }
28
+ fs.writeFileSync(meta.data+'/index.offset.'+changes.uuid+'.json', JSON.stringify(index))
29
+ },
30
+ load(uuid=null) {
31
+ let filename
32
+ if (!uuid) {
33
+ filename = 'index.offset.json'
34
+ } else {
35
+ filename = 'index.offset.'+filename+'.json'
36
+ }
37
+ return JSON.parse(fs.readFileSync(meta.data+filename))
38
+ }
39
+ }
@@ -2,25 +2,39 @@ import { parentPort } from 'node:worker_threads'
2
2
  import JSONTag from '@muze-nl/jsontag'
3
3
  import Parser from '@muze-nl/od-jsontag/src/parse.mjs'
4
4
  import fs from 'fs'
5
+ import path from 'path'
5
6
  import serialize from '@muze-nl/od-jsontag/src/serialize.mjs'
6
7
 
7
8
  const parser = new Parser()
8
- parentPort.on('message', (files) => {
9
+ let index = {}
10
+
11
+ parentPort.on('message', async (files) => {
9
12
  let meta = {
10
13
  index: {
11
14
  id: new Map()
12
15
  }
13
16
  }
14
17
 
18
+ index = await import(files.indexFile).then(mod => {
19
+ return mod.default
20
+ })
15
21
  const extension = files.dataFile.split('.').pop()
16
22
  const basefile = files.dataFile.substring(0, files.dataFile.length - (extension.length + 1)) //+1 for . character
17
-
23
+ meta.data = path.dirname(basefile)
18
24
  let count = 0
19
25
  let data
20
26
  let jsontag
21
27
  let datafile = files.dataFile
22
28
  let commands = files.commands
23
29
  commands.push('done')
30
+ // TODO
31
+ // - only load index files
32
+ // - for each command id
33
+ // - load files as raw bytes
34
+ // - index.id.*.jsontag and index.offset.*.jsontag to create proxies that will get the correct offset on access
35
+ // - do the same for resultSet[0] - the dataspace root entity
36
+ // don't parse entire files with od-jsontag
37
+ // add version info in proxies with a symbol to get that information
24
38
  do {
25
39
  if (fs.existsSync(datafile)) {
26
40
  jsontag = fs.readFileSync(datafile)
@@ -1,10 +1,9 @@
1
- import pointer from 'json-pointer'
2
1
  import {VM} from 'vm2'
3
2
  import { memoryUsage } from 'node:process'
4
3
  import JSONTag from '@muze-nl/jsontag'
5
4
  import {source} from '@muze-nl/od-jsontag/src/symbols.mjs'
6
5
  import Parser from '@muze-nl/od-jsontag/src/parse.mjs'
7
- import {_,from,not,anyOf,allOf,asc,desc,sum,count,avg,max,min,many,one,distinct} from '@muze-nl/jaqt'
6
+ import {_,from,not,anyOf,allOf,asc,desc,sum,count,avg,max,min,many,one,first,distinct} from '@muze-nl/jaqt'
8
7
  import process from 'node:process'
9
8
 
10
9
  let dataspace
@@ -56,7 +55,7 @@ const tasks = {
56
55
  return true
57
56
  },
58
57
  query: async (task) => {
59
- return runQuery(task.req.path, task.req, task.req.body)
58
+ return runQuery(task.req.path, task.req, task.req.body, task.timeout)
60
59
  },
61
60
  memoryUsage: async () => {
62
61
  let result = memoryUsage()
@@ -67,7 +66,7 @@ const tasks = {
67
66
 
68
67
  export default tasks
69
68
 
70
- export function runQuery(pointer, request, query) {
69
+ export function runQuery(pointer, request, query, timeout=1000) {
71
70
  if (!pointer) { throw new Error('missing pointer parameter')}
72
71
  if (!request) { throw new Error('missing request parameter')}
73
72
  let response = {
@@ -79,7 +78,7 @@ export function runQuery(pointer, request, query) {
79
78
  // @todo add text search: https://github.com/nextapps-de/flexsearch
80
79
  // @todo replace VM with V8 isolate
81
80
  const vm = new VM({
82
- timeout: 1000,
81
+ timeout: timeout,
83
82
  allowAsync: false,
84
83
  sandbox: {
85
84
  root: dataspace, //@TODO: if we don't pass the root, we can later shard
@@ -99,6 +98,7 @@ export function runQuery(pointer, request, query) {
99
98
  min,
100
99
  many,
101
100
  one,
101
+ first,
102
102
  distinct,
103
103
  // console: connectConsole(res),
104
104
  JSONTag,
@@ -171,24 +171,18 @@ function parseAllObjects(o, reset=true) {
171
171
  }
172
172
 
173
173
  export function getDataSpace(path, dataspace) {
174
- if (path.substring(path.length-1)==='/') {
175
- //jsonpointer doesn't allow a trailing '/'
174
+ if (path.substring(path.length-1)=='/') {
176
175
  path = path.substring(0, path.length-1)
177
- }
178
- let result
179
- if (path) {
180
- //jsonpointer doesn't allow an empty pointer
181
- try {
182
- if (pointer.has(dataspace, path)) {
183
- result = pointer.get(dataspace, path)
184
- } else {
185
- result = JSONTag.parse('<object class="Error">{"message":"Not found", "code":404}')
186
- }
187
- } catch(err) {
188
- result = JSONTag.parse('<object class="Error">{"message":'+JSON.stringify(err.message)+', "code":500}')
176
+ }
177
+ const pointer = path.split('/')
178
+ let result = dataspace
179
+ for (const part of pointer) {
180
+ if (part && result) {
181
+ result = result[part]
189
182
  }
190
- } else {
191
- result = dataspace
183
+ }
184
+ if (result===undefined) {
185
+ result = JSONTag.parse(`<object class="Error">{"message":"Path Not found", "code":404, "path":"${path}"}`)
192
186
  }
193
187
  return [result,path]
194
188
  }
package/src/server.mjs CHANGED
@@ -27,11 +27,14 @@ async function main(options) {
27
27
  const loadWorker = options.loadWorker || __dirname+'/src/load-worker.mjs'
28
28
  const commandWorker = options.commandWorker || __dirname+'/src/command-worker.mjs'
29
29
  const commandsFile = options.commandsFile || __dirname+'/src/commands.mjs'
30
+ const indexFile = options.indexFile || __dirname+'/src/index.mjs'
30
31
  const commandLog = options.commandLog || './command-log.jsontag'
31
32
  const commandStatus = options.commandStatus || './command-status.jsontag'
32
33
  const access = options.access || null
34
+ const timeout = options.timeout || 1000
35
+ const slowTimeout = options.slowTimeout || 10000
33
36
 
34
- server.use(express.static(wwwroot))
37
+ server.get('/', serveHomepage)
35
38
 
36
39
  // allow access to raw body, used to parse a query send as post body
37
40
  server.use(express.raw({
@@ -59,17 +62,33 @@ async function main(options) {
59
62
  body: jsontagBuffers,
60
63
  meta,
61
64
  access
62
- }
65
+ },
66
+ timeout
63
67
  }
64
68
  }
65
69
 
70
+ const slowQueryWorkerInitTask = () => {
71
+ let result = queryWorkerInitTask()
72
+ result.timeout = slowTimeout
73
+ return result
74
+ }
75
+
66
76
  let queryWorkerPool = new WorkerPool(maxWorkers, queryWorker, queryWorkerInitTask())
77
+ let slowQueryWorkerPool = new WorkerPool(1, queryWorker, slowQueryWorkerInitTask())
67
78
  let commandWorkerInstance
68
79
 
69
- server.get('/query/*', handleGetQuery)
70
- server.post('/query/*', handlePostQuery)
71
- server.post('/command', handlePostCommand)
72
- server.get('/command/:id', handleGetCommand)
80
+ server.get('/query/', (req,next) => handleGetQuery(req,next))
81
+ server.get('/query/*remainder', (req,next) => handleGetQuery(req,next))
82
+ server.get('/slowquery/', (req,next) => handleSlowGetQuery(req,next))
83
+ server.get('/slowquery/*remainder', (req,next) => handleSlowGetQuery(req,next))
84
+ server.post('/query/', (req,next) => handlePostQuery(req,next))
85
+ server.post('/query/*remainder', (req,next) => handlePostQuery(req,next))
86
+ server.post('/slowquery/', (req,next) => handleSlowPostQuery(req,next))
87
+ server.post('/slowquery/*remainder', (req,next) => handleSlowPostQuery(req,next))
88
+ server.post('/command', (req,next) => handlePostCommand(req,next))
89
+ server.get('/command/:id', (req,next) => handleGetCommand(req,next))
90
+
91
+ server.use(express.static(wwwroot))
73
92
 
74
93
  try {
75
94
  await fetch(`http://localhost:${port}`, {
@@ -95,6 +114,11 @@ async function main(options) {
95
114
 
96
115
  /* ------ */
97
116
 
117
+ function serveHomepage(req, res) {
118
+ res.setHeader('content-type', 'text/html');
119
+ res.send(fs.readFileSync(wwwroot+'/home.html'))
120
+ }
121
+
98
122
  function loadCommandStatus(commandStatusFile) {
99
123
  let status = new Map()
100
124
  if (fs.existsSync(commandStatusFile)) {
@@ -134,6 +158,7 @@ async function main(options) {
134
158
  meta,
135
159
  data:jsontagBuffers,
136
160
  commandsFile,
161
+ indexFile,
137
162
  datafile
138
163
  })
139
164
  break;
@@ -158,12 +183,17 @@ async function main(options) {
158
183
  reject(error)
159
184
  worker.terminate()
160
185
  })
161
- worker.postMessage({dataFile:datafile,schemaFile,commands})
186
+ worker.postMessage({dataFile:datafile,indexFile,schemaFile,commands})
162
187
  })
163
188
  }
164
189
 
165
- async function handleGetQuery(req, res) {
190
+ async function handleGetQuery(req, res, pool=null) {
166
191
  let start = Date.now()
192
+ if (!pool) {
193
+ pool = queryWorkerPool
194
+ } else {
195
+ console.log(pool)
196
+ }
167
197
  if ( !accept(req,res,
168
198
  ['application/jsontag','application/json','text/html','text/javascript','image/*'],
169
199
  function(req, res, accept) {
@@ -182,7 +212,7 @@ async function main(options) {
182
212
  // done
183
213
  return
184
214
  }
185
- let path = req.path.substr(6) // cut '/query'
215
+ let path = req.params.remainder?.join('/') || '/'
186
216
  console.log('query',path)
187
217
  let request = {
188
218
  method: req.method,
@@ -194,7 +224,7 @@ async function main(options) {
194
224
  request.jsontag = true
195
225
  }
196
226
  try {
197
- let result = await queryWorkerPool.run('query', request)
227
+ let result = await pool.run('query', request)
198
228
  sendResponse(result, res)
199
229
  } catch(error) {
200
230
  sendError(error, res)
@@ -203,7 +233,14 @@ async function main(options) {
203
233
  console.log(path, (end-start), process.memoryUsage())
204
234
  }
205
235
 
206
- async function handlePostQuery(req,res) {
236
+ async function handleSlowGetQuery(req, res) {
237
+ return handleGetQuery(req, res, slowQueryWorkerPool)
238
+ }
239
+
240
+ async function handlePostQuery(req,res,pool=null) {
241
+ if (!pool) {
242
+ pool = queryWorkerPool
243
+ }
207
244
  let start = Date.now()
208
245
  if ( !accept(req,res,
209
246
  ['application/jsontag','application/json'])
@@ -211,7 +248,7 @@ async function main(options) {
211
248
  sendError({code:406, message:'Not Acceptable',accept:['application/json','application/jsontag']},res)
212
249
  return
213
250
  }
214
- let path = req.path.substr(6) // cut '/query'
251
+ let path = req.params.remainder?.join('/') || '/'
215
252
  let request = {
216
253
  method: req.method,
217
254
  url: req.originalUrl,
@@ -223,7 +260,7 @@ async function main(options) {
223
260
  request.jsontag = true
224
261
  }
225
262
  try {
226
- let result = await queryWorkerPool.run('query', request)
263
+ let result = await pool.run('query', request)
227
264
  sendResponse(result, res)
228
265
  } catch(error) {
229
266
  sendError(error, res)
@@ -233,6 +270,10 @@ async function main(options) {
233
270
  // queryWorkerPool.memoryUsage()
234
271
  }
235
272
 
273
+ async function handleSlowPostQuery(req, res) {
274
+ return handlePostQuery(req, res, slowQueryWorkerPool)
275
+ }
276
+
236
277
  async function handlePostCommand(req, res) {
237
278
  let commandId = checkCommand(req, res)
238
279
  if (!commandId) {
@@ -253,6 +294,7 @@ async function main(options) {
253
294
  meta,
254
295
  data:jsontagBuffers,
255
296
  commandsFile,
297
+ indexFile,
256
298
  datafile
257
299
  })
258
300
  let result
package/www/home.html ADDED
@@ -0,0 +1,21 @@
1
+ <!doctype html>
2
+ <meta charset="utf-8">
3
+ <link rel="stylesheet" href="/assets/css/style.css">
4
+ <title>SimplyStore</title>
5
+ <body>
6
+ <h1>SimplyStore</h1>
7
+ <h2>In memory database and API</h2>
8
+ <p>This is a placeholder page. To run queries go to <a href="query/">the <code>/query</code> endpoint</a></p>
9
+ <p>You can override this placeholder page by adding this to your SimplyStore server code:</p>
10
+ <pre>
11
+ import SimplyStore from '@muze-nl/simplystore'
12
+
13
+ SimplyStore.get('/', function(req, res) {
14
+ res.setHeader('Content-Type', 'text/html')
15
+ res.send('Your homepage content')
16
+ })
17
+
18
+ SimplyStore.run()
19
+ </pre>
20
+ </body>
21
+