@muze-nl/simplystore 0.9.3 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muze-nl/simplystore",
3
- "version": "0.9.3",
3
+ "version": "0.10.2",
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.5",
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,15 +1,42 @@
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]
18
+ if (indexFile && indexFile[0]!='/') {
19
+ indexFile = process.cwd()+'/'+indexFile
20
+ } else if (!indexFile) {
21
+ indexFile = __dirname+'/../src/index.mjs'
22
+ }
23
+ let schemaFile = process.argv[5]
24
+ if (schemaFile && schemaFile[0]!='/') {
25
+ schemaFile = process.cwd()+'/'+schemaFile
26
+ }
27
+
28
+ const index = await import(indexFile).then(mod => {
29
+ return mod.default
30
+ })
31
+
32
+ // now create indexes
33
+ console.log('Using index library:', indexFile)
34
+
35
+
36
+ let schema = {}
37
+ if (schemaFile) {
38
+ schema = JSONTag.parse(fs.readFileSync(schemaFile, 'utf-8'))
39
+ }
13
40
 
14
41
  // load file
15
42
  let input = fs.readFileSync(inputFile, 'utf-8')
@@ -17,9 +44,43 @@ let input = fs.readFileSync(inputFile, 'utf-8')
17
44
  // parse jsontag
18
45
  let data = JSONTag.parse(input)
19
46
 
47
+ console.log('input data parsed')
20
48
  // write resultset to output
21
49
  let strData = stringify(serialize(data))
22
50
 
23
- fs.writeFileSync(outputFile, strData)
51
+ console.log('od-jsontag created')
24
52
 
53
+ // indexes need the position data which is only available after
54
+ // parsing the od-jsontag data
55
+ const parser = new Parser('https://localhost/',false) // allow mutations
56
+ const odData = parser.parse(strData)
57
+
58
+ console.log('offsets parsed')
59
+
60
+ let meta = {
61
+ index: {
62
+ id: new Map()
63
+ },
64
+ schema,
65
+ resultArray: parser.meta.resultArray,
66
+ data: path.dirname(outputFile)
67
+ }
68
+
69
+ for (let ob of meta.resultArray) {
70
+ let id = JSONTag.getAttribute(ob, 'id')
71
+ if (id) {
72
+ meta.index.id.set(id, ob)
73
+ }
74
+ }
75
+
76
+ console.log('Indexing...')
77
+ //FIXME: offset index can only be calculated by parsing serialized odData
78
+ //but index.create updates that data, so previous information is no longer correct
79
+ //TODO: split index creation in internal (updates odData) and external (creates index files)
80
+ index.create(odData, meta)
81
+ console.log('Indexes created')
82
+
83
+ strData = stringify(serialize(odData))
84
+
85
+ fs.writeFileSync(outputFile, strData)
25
86
  console.log('Converted data written to ',outputFile)
@@ -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,15 @@ 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
+ }
167
195
  if ( !accept(req,res,
168
196
  ['application/jsontag','application/json','text/html','text/javascript','image/*'],
169
197
  function(req, res, accept) {
@@ -182,7 +210,7 @@ async function main(options) {
182
210
  // done
183
211
  return
184
212
  }
185
- let path = req.path.substr(6) // cut '/query'
213
+ let path = req.params.remainder?.join('/') || '/'
186
214
  console.log('query',path)
187
215
  let request = {
188
216
  method: req.method,
@@ -194,7 +222,7 @@ async function main(options) {
194
222
  request.jsontag = true
195
223
  }
196
224
  try {
197
- let result = await queryWorkerPool.run('query', request)
225
+ let result = await pool.run('query', request, { timeout })
198
226
  sendResponse(result, res)
199
227
  } catch(error) {
200
228
  sendError(error, res)
@@ -203,7 +231,17 @@ async function main(options) {
203
231
  console.log(path, (end-start), process.memoryUsage())
204
232
  }
205
233
 
206
- async function handlePostQuery(req,res) {
234
+ async function handleSlowGetQuery(req, res) {
235
+ return handleGetQuery(req, res, slowQueryWorkerPool)
236
+ }
237
+
238
+ async function handlePostQuery(req,res,pool=null, slowTimeout=null) {
239
+ if (!pool) {
240
+ pool = queryWorkerPool
241
+ }
242
+ if (!slowTimeout) {
243
+ slowTimeout = timeout
244
+ }
207
245
  let start = Date.now()
208
246
  if ( !accept(req,res,
209
247
  ['application/jsontag','application/json'])
@@ -211,7 +249,7 @@ async function main(options) {
211
249
  sendError({code:406, message:'Not Acceptable',accept:['application/json','application/jsontag']},res)
212
250
  return
213
251
  }
214
- let path = req.path.substr(6) // cut '/query'
252
+ let path = req.params.remainder?.join('/') || '/'
215
253
  let request = {
216
254
  method: req.method,
217
255
  url: req.originalUrl,
@@ -223,7 +261,7 @@ async function main(options) {
223
261
  request.jsontag = true
224
262
  }
225
263
  try {
226
- let result = await queryWorkerPool.run('query', request)
264
+ let result = await pool.run('query', request, {slowTimeout})
227
265
  sendResponse(result, res)
228
266
  } catch(error) {
229
267
  sendError(error, res)
@@ -233,6 +271,10 @@ async function main(options) {
233
271
  // queryWorkerPool.memoryUsage()
234
272
  }
235
273
 
274
+ async function handleSlowPostQuery(req, res) {
275
+ return handlePostQuery(req, res, slowQueryWorkerPool, slowTimeout)
276
+ }
277
+
236
278
  async function handlePostCommand(req, res) {
237
279
  let commandId = checkCommand(req, res)
238
280
  if (!commandId) {
@@ -253,6 +295,7 @@ async function main(options) {
253
295
  meta,
254
296
  data:jsontagBuffers,
255
297
  commandsFile,
298
+ indexFile,
256
299
  datafile
257
300
  })
258
301
  let result
@@ -326,13 +369,15 @@ async function main(options) {
326
369
  if (data.data) { // data has changed, commands may do other things instead of changing data
327
370
  jsontagBuffers.push(data.data) // push changeset to jsontagBuffers so that new query workers get all changes from scratch
328
371
  Object.assign(meta, data.meta)
329
- queryWorkerPool.update({
372
+ const updateTask = {
330
373
  name: 'update',
331
374
  req: {
332
375
  body: jsontagBuffers[jsontagBuffers.length-1], // only add the last change, update tasks for earlier changes have already been sent
333
376
  meta
334
377
  }
335
- })
378
+ }
379
+ queryWorkerPool.update(updateTask)
380
+ slowQueryWorkerPool.update(updateTask)
336
381
  }
337
382
  appendFile(commandStatus, JSONTag.stringify(Object.assign({command:command.id}, s)))
338
383
  mainResolve(s)
@@ -68,9 +68,9 @@ export default class WorkerPool extends EventEmitter {
68
68
  worker.postMessage(this.initTask);
69
69
  }
70
70
 
71
- async run(name, req) {
71
+ async run(name, req, options={}) {
72
72
  return new Promise((resolve, reject) => {
73
- this.runTask({name,req}, (error, result) => {
73
+ this.runTask({name,req,...options}, (error, result) => {
74
74
  if (error) {
75
75
  return reject(error)
76
76
  }
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
+