@muze-nl/simplystore 0.1.2 → 0.1.4
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 +1 -1
- package/package.json~ +2 -2
- package/src/server.mjs +276 -274
package/package.json
CHANGED
package/package.json~
CHANGED
package/src/server.mjs
CHANGED
|
@@ -8,314 +8,316 @@ import _ from 'array-where-select'
|
|
|
8
8
|
import { fileURLToPath } from 'url'
|
|
9
9
|
import path from 'path'
|
|
10
10
|
|
|
11
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
11
|
+
const __dirname = path.dirname(path.dirname(fileURLToPath(import.meta.url)))
|
|
12
|
+
|
|
12
13
|
const server = express()
|
|
13
14
|
|
|
14
15
|
function deepFreeze(obj) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
Object.freeze(obj)
|
|
17
|
+
Object.keys(obj).forEach(prop => {
|
|
18
|
+
if (typeof obj[prop] === 'object' && !Object.isFrozen(obj[prop])) {
|
|
19
|
+
deepFreeze(obj[prop])
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
return obj
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
function isString(s)
|
|
25
26
|
{
|
|
26
|
-
|
|
27
|
+
return typeof s === 'string' || s instanceof String
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
function joinArgs(args) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
return args = args.map(arg => {
|
|
32
|
+
if (isString(arg)) {
|
|
33
|
+
return arg
|
|
34
|
+
} else {
|
|
35
|
+
return JSONTag.stringify(arg)
|
|
36
|
+
}
|
|
37
|
+
}).join(' ')
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
function connectConsole(res) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
return {
|
|
42
|
+
log: function(...args) {
|
|
43
|
+
res.append('X-Console-Log', joinArgs(args))
|
|
44
|
+
},
|
|
45
|
+
warning: function(...args) {
|
|
46
|
+
res.append('X-Console-Warning', joinArgs(args))
|
|
47
|
+
},
|
|
48
|
+
error: function(...args) {
|
|
49
|
+
res.append('X-Console-Error', joinArgs(args))
|
|
50
|
+
}
|
|
51
|
+
}
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
async function main(options) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
if (!options) {
|
|
56
|
+
options = {}
|
|
57
|
+
}
|
|
58
|
+
const port = options.port || 3000
|
|
59
|
+
const datafile = options.datafile || 'data.jsontag'
|
|
60
|
+
const wwwroot = options.wwwroot || __dirname+'/www'
|
|
61
|
+
let meta = options.meta || {}
|
|
62
|
+
let dataspace = options.dataspace || null
|
|
60
63
|
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
const originalJSON = JSON
|
|
65
|
+
JSON = JSONTag // monkeypatching
|
|
63
66
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
67
|
+
if (!dataspace) {
|
|
68
|
+
console.log('loading data...')
|
|
69
|
+
let file = fs.readFileSync(datafile)
|
|
70
|
+
try {
|
|
71
|
+
dataspace = JSONTag.parse(file.toString(), null, meta)
|
|
72
|
+
} catch(e) {
|
|
73
|
+
console.error(e)
|
|
74
|
+
process.exit()
|
|
75
|
+
}
|
|
72
76
|
}
|
|
73
|
-
|
|
74
|
-
} catch(e) {
|
|
75
|
-
console.error(e)
|
|
76
|
-
process.exit()
|
|
77
|
-
}
|
|
77
|
+
deepFreeze(dataspace)
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
let used = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
|
|
80
|
+
console.log(`data loaded (${used} MB)`);
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
server.get('/', (req,res) => {
|
|
83
|
+
res.send('<h1>SimplyStore</h1>') //TODO: implement something nice
|
|
84
|
+
})
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
server.use(express.static(wwwroot))
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
88
|
+
// allow access to raw body, used to parse a query send as post body
|
|
89
|
+
server.use(express.raw({
|
|
90
|
+
type: (req) => true // parse body on all requests
|
|
91
|
+
}))
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
93
|
+
function accept(req, res, mimetypes, handler) {
|
|
94
|
+
let accept = req.accepts(mimetypes)
|
|
95
|
+
if (!accept) {
|
|
96
|
+
res.status(406)
|
|
97
|
+
res.send("<h1>406 Unacceptable</h1>\n")
|
|
98
|
+
return false
|
|
99
|
+
}
|
|
100
|
+
if (typeof handler === 'function') {
|
|
101
|
+
return handler(req, res, accept)
|
|
102
|
+
}
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
106
|
+
function getDataSpace(req, res, dataspace) {
|
|
107
|
+
let path = req.path.substr(6); // cut '/query'
|
|
108
|
+
if (!path) {
|
|
109
|
+
path = '';
|
|
110
|
+
}
|
|
111
|
+
if (path.substring(path.length-1)==='/') {
|
|
112
|
+
//jsonpointer doesn't allow a trailing '/'
|
|
113
|
+
path = path.substring(0, path.length-1)
|
|
114
|
+
}
|
|
115
|
+
let result
|
|
116
|
+
if (path) {
|
|
117
|
+
//jsonpointer doesn't allow an empty pointer
|
|
118
|
+
try {
|
|
119
|
+
if (pointer.has(dataspace, path)) {
|
|
120
|
+
result = pointer.get(dataspace, path)
|
|
121
|
+
} else {
|
|
122
|
+
result = JSONTag.parse('<object class="Error">{"message":"Not found", "code":404}')
|
|
123
|
+
}
|
|
124
|
+
} catch(err) {
|
|
125
|
+
result = JSONTag.parse('<object class="Error">{"message":'+originalJSON.stringify(err.message)+', "code":500}')
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
result = dataspace
|
|
129
|
+
}
|
|
130
|
+
return [result,path]
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
let seen = new Map();
|
|
134
|
+
function countObjects(obj) {
|
|
135
|
+
if (seen.has(obj)) {
|
|
136
|
+
return 0
|
|
137
|
+
}
|
|
138
|
+
seen.set(obj, true)
|
|
139
|
+
let count = 0
|
|
140
|
+
let values = []
|
|
141
|
+
if (Array.isArray(obj)) {
|
|
142
|
+
values = obj
|
|
143
|
+
count++
|
|
144
|
+
} else if (typeof obj === 'object') {
|
|
145
|
+
if (obj instanceof String || obj instanceof Number || obj instanceof Boolean) {
|
|
146
|
+
// console.log('skipped', obj, typeof obj)
|
|
147
|
+
} else {
|
|
148
|
+
values = Object.values(obj)
|
|
149
|
+
count++
|
|
150
|
+
}
|
|
151
|
+
} else {
|
|
152
|
+
// console.log('skipped', obj, typeof obj)
|
|
153
|
+
}
|
|
154
|
+
return values
|
|
155
|
+
.filter((o) => typeof o === 'object')
|
|
156
|
+
.reduce((count, o) => count + countObjects(o), count)
|
|
157
|
+
}
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
159
|
+
server.get('/status/', (req, res, next) =>
|
|
160
|
+
{
|
|
161
|
+
seen = new Map()
|
|
162
|
+
let result = {
|
|
163
|
+
memory: Math.round(process.memoryUsage().heapUsed / 1024 / 1024)+'MB',
|
|
164
|
+
datasets: Object.keys(dataspace),
|
|
165
|
+
objects: countObjects(dataspace)
|
|
166
|
+
}
|
|
167
|
+
res.setHeader('content-type','application/json')
|
|
168
|
+
res.send(originalJSON.stringify(result, null, 4)+"\n")
|
|
169
|
+
})
|
|
170
170
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
171
|
+
server.get('/query/*', (req, res, next) =>
|
|
172
|
+
{
|
|
173
|
+
let start = Date.now()
|
|
174
174
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
175
|
+
if ( !accept(req,res,
|
|
176
|
+
['application/jsontag','application/json','text/html','text/javascript','image/*'],
|
|
177
|
+
function(req, res, accept) {
|
|
178
|
+
switch(accept) {
|
|
179
|
+
case 'text/html':
|
|
180
|
+
case 'image/*':
|
|
181
|
+
case 'text/javascript':
|
|
182
|
+
handleWebRequest(req,res,options);
|
|
183
|
+
return false
|
|
184
|
+
break
|
|
185
|
+
}
|
|
186
|
+
return true
|
|
187
|
+
}
|
|
188
|
+
)) {
|
|
189
|
+
// done
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
192
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
193
|
+
let [result,path] = getDataSpace(req, res, dataspace)
|
|
194
|
+
if (JSONTag.getAttribute(result, 'class')==='Error') {
|
|
195
|
+
res.status(result.code)
|
|
196
|
+
}
|
|
197
|
+
result = linkReplacer(result, path+'/')
|
|
198
|
+
if (req.accepts('application/jsontag')) {
|
|
199
|
+
res.setHeader('content-type','application/jsontag+json')
|
|
200
|
+
res.send(JSONTag.stringify(result, null, 4)+"\n")
|
|
201
|
+
} else {
|
|
202
|
+
res.setHeader('content-type','application/json')
|
|
203
|
+
res.send(originalJSON.stringify(result, null, 4)+"\n")
|
|
204
|
+
}
|
|
205
|
+
let end = Date.now()
|
|
206
|
+
console.log(path, (end-start), process.memoryUsage())
|
|
207
|
+
})
|
|
208
208
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
209
|
+
/**
|
|
210
|
+
* handle queries, query is the post body
|
|
211
|
+
*/
|
|
212
|
+
server.post('/query/*', (req, res) => {
|
|
213
|
+
let start = Date.now()
|
|
214
|
+
if ( !accept(req,res,
|
|
215
|
+
['application/jsontag','application/json'])
|
|
216
|
+
) {
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
let error
|
|
220
|
+
let [result,path] = getDataSpace(req, res, dataspace)
|
|
221
|
+
if (result) {
|
|
222
|
+
// do the query here
|
|
223
|
+
let query = req.body.toString() // raw body through express.raw()
|
|
224
|
+
// @todo add text search: https://github.com/nextapps-de/flexsearch
|
|
225
|
+
// @todo add tree walk map/reduce/find/filter style functions
|
|
226
|
+
// @todo add arc tree dive function?
|
|
227
|
+
const vm = new VM({
|
|
228
|
+
// timeout: 1000,
|
|
229
|
+
allowAsync: false,
|
|
230
|
+
sandbox: {
|
|
231
|
+
root: dataspace,
|
|
232
|
+
data: result,
|
|
233
|
+
meta: meta,
|
|
234
|
+
_: _,
|
|
235
|
+
console: connectConsole(res),
|
|
236
|
+
JSONTag: JSONTag,
|
|
237
|
+
request: req,
|
|
238
|
+
Array: Array
|
|
239
|
+
},
|
|
240
|
+
wasm: false
|
|
241
|
+
})
|
|
242
|
+
try {
|
|
243
|
+
result = vm.run(query)
|
|
244
|
+
let used = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
|
|
245
|
+
console.log(`(${used} MB)`);
|
|
246
|
+
} catch(err) {
|
|
247
|
+
console.log(err)
|
|
248
|
+
error = JSONTag.parse('<object class="Error">{"message":'+originalJSON.stringify(''+err)+',"code":422}')
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
251
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
252
|
+
if (error) {
|
|
253
|
+
res.status(error.code)
|
|
254
|
+
result = error
|
|
255
|
+
}
|
|
256
|
+
if (req.accepts('application/jsontag')) {
|
|
257
|
+
res.setHeader('content-type','application/jsontag+json')
|
|
258
|
+
res.send(JSONTag.stringify(result, null, 4)+"\n")
|
|
259
|
+
} else {
|
|
260
|
+
res.setHeader('content-type','application/json')
|
|
261
|
+
res.send(originalJSON.stringify(result, null, 4)+"\n")
|
|
262
|
+
}
|
|
263
|
+
let end = Date.now()
|
|
264
|
+
console.log(path, (end-start), process.memoryUsage())
|
|
265
|
+
})
|
|
266
266
|
|
|
267
|
-
function handleWebRequest(req,res,options)
|
|
268
|
-
{
|
|
269
|
-
let path = req.path;
|
|
270
|
-
path = path.replace(/[^a-z0-9_\.\-\/]*/gi, '') // whitelist acceptable file paths
|
|
271
|
-
path = path.replace(/\.+/g, '.') // blacklist '..'
|
|
272
|
-
if (!path) {
|
|
273
|
-
path = '/'
|
|
274
|
-
}
|
|
275
|
-
if (path.substring(path.length-1)==='/') {
|
|
276
|
-
path += 'index.html'
|
|
277
|
-
}
|
|
278
|
-
const fileOptions = {
|
|
279
|
-
root: options.root || process.cwd()+'/www'
|
|
280
|
-
}
|
|
281
|
-
if (fs.existsSync(fileOptions.root+path)) {
|
|
282
|
-
res.sendFile(path, fileOptions)
|
|
283
|
-
} else {
|
|
284
|
-
res.sendFile('/index.html', fileOptions)
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
267
|
|
|
288
|
-
function linkReplacer(data, baseURL) {
|
|
289
|
-
let type = JSONTag.getType(data)
|
|
290
|
-
let attributes = JSONTag.getAttributes(data)
|
|
291
|
-
if (Array.isArray(data)) {
|
|
292
|
-
data = data.map((entry,index) => {
|
|
293
|
-
return linkReplacer(data[index], baseURL+index+'/')
|
|
294
|
-
})
|
|
295
|
-
} else if (type === 'link') {
|
|
296
|
-
// do nothing
|
|
297
|
-
} else if (data && typeof data === 'object') {
|
|
298
|
-
data = JSONTag.clone(data)
|
|
299
|
-
Object.keys(data).forEach(key => {
|
|
300
|
-
if (Array.isArray(data[key])) {
|
|
301
|
-
data[key] = new JSONTag.Link(baseURL+key+'/')
|
|
302
|
-
} else if (typeof data[key] === 'object') {
|
|
303
|
-
if (JSONTag.getType(data[key])!=='link') {
|
|
304
|
-
let id=JSONTag.getAttribute(data[key], 'id')
|
|
305
|
-
if (!id) {
|
|
306
|
-
id = baseURL+key+'/'
|
|
307
|
-
}
|
|
308
|
-
data[key] = new JSONTag.Link(id)
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
})
|
|
312
|
-
}
|
|
313
|
-
return data
|
|
314
|
-
}
|
|
315
268
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
269
|
+
function handleWebRequest(req,res,options)
|
|
270
|
+
{
|
|
271
|
+
let path = req.path;
|
|
272
|
+
path = path.replace(/[^a-z0-9_\.\-\/]*/gi, '') // whitelist acceptable file paths
|
|
273
|
+
path = path.replace(/\.+/g, '.') // blacklist '..'
|
|
274
|
+
if (!path) {
|
|
275
|
+
path = '/'
|
|
276
|
+
}
|
|
277
|
+
if (path.substring(path.length-1)==='/') {
|
|
278
|
+
path += 'index.html'
|
|
279
|
+
}
|
|
280
|
+
const fileOptions = {
|
|
281
|
+
root: options.root || wwwroot
|
|
282
|
+
}
|
|
283
|
+
if (fs.existsSync(fileOptions.root+path)) {
|
|
284
|
+
res.sendFile(path, fileOptions)
|
|
285
|
+
} else {
|
|
286
|
+
res.sendFile('/index.html', fileOptions)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function linkReplacer(data, baseURL) {
|
|
291
|
+
let type = JSONTag.getType(data)
|
|
292
|
+
let attributes = JSONTag.getAttributes(data)
|
|
293
|
+
if (Array.isArray(data)) {
|
|
294
|
+
data = data.map((entry,index) => {
|
|
295
|
+
return linkReplacer(data[index], baseURL+index+'/')
|
|
296
|
+
})
|
|
297
|
+
} else if (type === 'link') {
|
|
298
|
+
// do nothing
|
|
299
|
+
} else if (data && typeof data === 'object') {
|
|
300
|
+
data = JSONTag.clone(data)
|
|
301
|
+
Object.keys(data).forEach(key => {
|
|
302
|
+
if (Array.isArray(data[key])) {
|
|
303
|
+
data[key] = new JSONTag.Link(baseURL+key+'/')
|
|
304
|
+
} else if (typeof data[key] === 'object') {
|
|
305
|
+
if (JSONTag.getType(data[key])!=='link') {
|
|
306
|
+
let id=JSONTag.getAttribute(data[key], 'id')
|
|
307
|
+
if (!id) {
|
|
308
|
+
id = baseURL+key+'/'
|
|
309
|
+
}
|
|
310
|
+
data[key] = new JSONTag.Link(id)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
return data
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
server.listen(port, () => {
|
|
319
|
+
console.log('SimplyStore listening on port '+port)
|
|
320
|
+
})
|
|
319
321
|
|
|
320
322
|
}
|
|
321
323
|
|