@muze-nl/simplystore 0.1.2 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.parcel-cache/07206d1728063d8c.txt +1604 -0
- package/.parcel-cache/423b2927e907f0d0 +0 -0
- package/.parcel-cache/ce1c7c93e1d158dd +0 -0
- package/.parcel-cache/d43c1a783b32ded4 +0 -0
- package/.parcel-cache/d9c636a9a4b84c2a +0 -0
- package/.parcel-cache/data.mdb +0 -0
- package/.parcel-cache/lock.mdb +0 -0
- package/README.md +86 -34
- package/curriculum-contexts.txt +11 -0
- package/curriculum-examenprogramma.jsontag +89012 -0
- package/default.jsontag +18 -0
- package/package.json +1 -1
- package/package.json~ +2 -2
- package/simplystore-talk.md +45 -0
- package/src/DEADJOE +3 -0
- package/src/editor.mjs~ +7 -0
- package/src/main.mjs~ +301 -0
- package/src/server.js +278 -0
- package/src/server.mjs +238 -274
- package/src/test.jsontag +8 -0
- package/src/test.mjs +20 -0
- package/src/testrefs.mjs +59 -0
- package/test.jsontag +301 -0
- package/test2.jsontag +17 -0
- package/tojsontag.mjs +55 -0
- package/www/codemirror/keymap/vim.js +3 -3
- package/www/editor.bundle.js +87694 -0
- package/www/index.html~ +136 -0
- package/www/test.html +30 -0
- package/.gitignore~ +0 -1
package/default.jsontag
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"persons": [
|
|
3
|
+
<object id="john" class="Person">{
|
|
4
|
+
"name": "John",
|
|
5
|
+
"dob": <date>"1972-09-20",
|
|
6
|
+
"foaf": [
|
|
7
|
+
<link>"jane"
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
<object id="jane" class="Person">{
|
|
11
|
+
"name": "Jane",
|
|
12
|
+
"dob": <date>"1986-01-01",
|
|
13
|
+
"foaf": [
|
|
14
|
+
<link>"john"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
17
|
+
]
|
|
18
|
+
}
|
package/package.json
CHANGED
package/package.json~
CHANGED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
simplystore
|
|
2
|
+
- logo
|
|
3
|
+
- website
|
|
4
|
+
- talk
|
|
5
|
+
focus op max 3 problemen/oplossingen
|
|
6
|
+
linked data complexiteit - tussenstap met jsontag
|
|
7
|
+
minder moving parts - code naar data brengen ipv andersom
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
SimplyStore
|
|
12
|
+
|
|
13
|
+
All remote API's suck, can we make it better?
|
|
14
|
+
|
|
15
|
+
Why do we need remote API's?
|
|
16
|
+
|
|
17
|
+
Large datasets
|
|
18
|
+
Collaboration
|
|
19
|
+
Control of centralized resources
|
|
20
|
+
|
|
21
|
+
What are the problems with remote API's
|
|
22
|
+
|
|
23
|
+
Local resources and data can be managed with normal code flow
|
|
24
|
+
Remote API's require breaking out, with usually RPC calls
|
|
25
|
+
That means - asynchronous calls, converting data from/to a stringified format, limited to whatever the API provides
|
|
26
|
+
|
|
27
|
+
RPC means network, network access is slow
|
|
28
|
+
API's provide limited semantics, usually HTTP endpoints with JSON, if you are lucky GraphQL
|
|
29
|
+
So you usually end up fetching too much data, to filter it down locally. Or you do many batches of smaller fetches. Both are always too slow.
|
|
30
|
+
|
|
31
|
+
Finally, API's change. So all clients must change. Each API is unique, so all client code is unique. Any change in a single API blossoms into requiring changes in many clients. Schema changes are the worst. Versioning helps, but keep old versions running!
|
|
32
|
+
|
|
33
|
+
Sometimes changes are so large that older versions cannog be kept. This is a shame.
|
|
34
|
+
|
|
35
|
+
OpenAPI (Swagger) is not a solution. The semantics of the data and schema changes aren't tackled with OpenAPI specifications.
|
|
36
|
+
|
|
37
|
+
What technology exists that promises a solution?
|
|
38
|
+
|
|
39
|
+
AI. done.
|
|
40
|
+
|
|
41
|
+
Ok, so not AI. How about Linked Data?
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
package/src/DEADJOE
ADDED
package/src/editor.mjs~
ADDED
package/src/main.mjs~
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import pointer from 'json-pointer'
|
|
4
|
+
import JSONTag from '@muze-nl/jsontag'
|
|
5
|
+
import {JSONPath} from 'jsonpath-plus'
|
|
6
|
+
import jsonExt from '@discoveryjs/json-ext'
|
|
7
|
+
//import TripleStore from './triplestore.mjs'
|
|
8
|
+
import {VM} from 'vm2'
|
|
9
|
+
|
|
10
|
+
const server = express()
|
|
11
|
+
const port = process.env.NODE_PORT || 3000;
|
|
12
|
+
|
|
13
|
+
const datafile = process.env.DATAFILE || 'data.jsontag'
|
|
14
|
+
|
|
15
|
+
server.use(express.static(process.cwd()+'/www'))
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
|
|
19
|
+
const originalJSON = JSON
|
|
20
|
+
JSON = JSONTag // monkeypatching
|
|
21
|
+
|
|
22
|
+
console.log('loading data...')
|
|
23
|
+
// let file = fs.readFileSync(datafile);
|
|
24
|
+
// let dataspace = JSONTag.parse(file.toString());
|
|
25
|
+
let reader = fs.createReadStream(datafile);
|
|
26
|
+
let dataspace = await JSONTag.parseStream(reader);
|
|
27
|
+
|
|
28
|
+
console.log('indexing data...')
|
|
29
|
+
// let tripleStore = new TripleStore(dataspace)
|
|
30
|
+
|
|
31
|
+
let used = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
|
|
32
|
+
console.log(`data loaded (${used} MB)`);
|
|
33
|
+
|
|
34
|
+
// allow access to raw body, used to parse a query send as post body
|
|
35
|
+
server.use(express.raw({
|
|
36
|
+
type: (req) => true // parse body on all requests
|
|
37
|
+
}))
|
|
38
|
+
|
|
39
|
+
server.get('/query/*', (req, res, next) =>
|
|
40
|
+
{
|
|
41
|
+
let start = Date.now()
|
|
42
|
+
let accept = req.accepts(['application/jsontag','application/json','text/html','text/javascript','image/*'])
|
|
43
|
+
if (!accept) {
|
|
44
|
+
res.status(406)
|
|
45
|
+
res.send("<h1>406 Unacceptable</h1>\n")
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
switch(accept) {
|
|
49
|
+
case 'text/html':
|
|
50
|
+
case 'image/*':
|
|
51
|
+
case 'text/javascript':
|
|
52
|
+
handleWebRequest(req,res);
|
|
53
|
+
return
|
|
54
|
+
break
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let path = req.path.substr(6); // cut '/query'
|
|
58
|
+
if (!path) {
|
|
59
|
+
path = '';
|
|
60
|
+
}
|
|
61
|
+
if (path.substring(path.length-1)==='/') {
|
|
62
|
+
//jsonpointer doesn't allow a trailing '/'
|
|
63
|
+
path = path.substring(0, path.length-1)
|
|
64
|
+
}
|
|
65
|
+
let result
|
|
66
|
+
if (path) {
|
|
67
|
+
//jsonpointer doesn't allow an empty pointer
|
|
68
|
+
try {
|
|
69
|
+
if (pointer.has(dataspace, path)) {
|
|
70
|
+
result = pointer.get(dataspace, path)
|
|
71
|
+
} else {
|
|
72
|
+
result = JSONTag.parse('<object class="Error">{"message":"Not found", "code":404}')
|
|
73
|
+
}
|
|
74
|
+
} catch(err) {
|
|
75
|
+
result = JSONTag.parse('<object class="Error">{"message":'+originalJSON.stringify(err.message)+', "code":500}')
|
|
76
|
+
}
|
|
77
|
+
} else {
|
|
78
|
+
result = dataspace
|
|
79
|
+
}
|
|
80
|
+
if (JSONTag.getAttribute(result, 'class')==='Error') {
|
|
81
|
+
res.status(result.code)
|
|
82
|
+
}
|
|
83
|
+
result = linkReplacer(result, path+'/')
|
|
84
|
+
if (req.accepts('application/jsontag')) {
|
|
85
|
+
res.setHeader('content-type','application/jsontag+json')
|
|
86
|
+
res.send(JSONTag.stringify(result, null, 4)+"\n")
|
|
87
|
+
} else {
|
|
88
|
+
res.setHeader('content-type','application/json')
|
|
89
|
+
res.send(originalJSON.stringify(result, null, 4)+"\n")
|
|
90
|
+
}
|
|
91
|
+
let end = Date.now()
|
|
92
|
+
console.log(path, (end-start), process.memoryUsage())
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* handle queries, query is the post body
|
|
97
|
+
*/
|
|
98
|
+
server.post('/query/*', (req, res) => {
|
|
99
|
+
let start = Date.now()
|
|
100
|
+
function parseParams(paramsString) {
|
|
101
|
+
let result = {}
|
|
102
|
+
let lineRe = /^(.*)$/m
|
|
103
|
+
let parseRe = /^\s*([a-z].*?)?\s*(\{)?\s*$/i
|
|
104
|
+
let line = lineRe.exec(paramsString).pop()
|
|
105
|
+
let full, prop, recurse
|
|
106
|
+
do {
|
|
107
|
+
paramsString = paramsString.substring(line.length+1)
|
|
108
|
+
let parsed = parseRe.exec(line)
|
|
109
|
+
if (parsed) {
|
|
110
|
+
[full,prop,recurse] = parsed
|
|
111
|
+
}
|
|
112
|
+
if (recurse) {
|
|
113
|
+
[ result[prop], paramsString ] = parseParams(paramsString)
|
|
114
|
+
} else if (prop) {
|
|
115
|
+
result[prop] = ''
|
|
116
|
+
} else if (/\}/.exec(line)) {
|
|
117
|
+
return [ result, paramsString ]
|
|
118
|
+
}
|
|
119
|
+
line = lineRe.exec(paramsString).pop()
|
|
120
|
+
} while(line)
|
|
121
|
+
return [ result, '' ]
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function filterProperties(paramsOb) {
|
|
125
|
+
return function(object) {
|
|
126
|
+
let result = {}
|
|
127
|
+
let obType = JSONTag.getType(object)
|
|
128
|
+
JSONTag.setType(result, obType)
|
|
129
|
+
JSONTag.setAttributes(result, JSONTag.getAttributes(object))
|
|
130
|
+
Object.entries(paramsOb).forEach(([key,value]) =>{
|
|
131
|
+
let alias, queryResult
|
|
132
|
+
[alias,key] = key.split(':',2).map(p => p.trim())
|
|
133
|
+
if (!key) {
|
|
134
|
+
key = alias
|
|
135
|
+
}
|
|
136
|
+
if (key[0]==='$') {
|
|
137
|
+
queryResult = JSONPath({path:key, json:object, flatten: true})
|
|
138
|
+
} else {
|
|
139
|
+
queryResult = object[key]
|
|
140
|
+
}
|
|
141
|
+
if (!value) {
|
|
142
|
+
// @TODO: just set the entire value for now, should run linkReplacer here
|
|
143
|
+
// this is better solved after forcing all objects to have an id attribute with a unique id
|
|
144
|
+
result[alias] = queryResult
|
|
145
|
+
} else {
|
|
146
|
+
result[alias] = filterProperties(value)(queryResult)
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
let accept = req.accepts(['application/jsontag','application/json','text/html','text/javascript','image/*'])
|
|
154
|
+
if (!accept) {
|
|
155
|
+
res.status(406)
|
|
156
|
+
res.send("<h1>406 Unacceptable</h1>\n")
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
let path = req.path.substring(6); // cut '/query'
|
|
160
|
+
if (!path) {
|
|
161
|
+
path = '';
|
|
162
|
+
}
|
|
163
|
+
if (path.substring(path.length-1)==='/') {
|
|
164
|
+
//jsonpointer doesn't allow a trailing '/'
|
|
165
|
+
path = path.substring(0, path.length-1)
|
|
166
|
+
}
|
|
167
|
+
let error,result
|
|
168
|
+
if (path) {
|
|
169
|
+
//jsonpointer doesn't allow an empty pointer
|
|
170
|
+
try {
|
|
171
|
+
if (pointer.has(dataspace, path)) {
|
|
172
|
+
result = pointer.get(dataspace, path)
|
|
173
|
+
} else {
|
|
174
|
+
error = JSONTag.parse('<object class="Error">{"message":"Not found", "code":404}')
|
|
175
|
+
}
|
|
176
|
+
} catch(err) {
|
|
177
|
+
error = JSONTag.parse('<object class="Error">{"message":'+originalJSON.stringify(err.message)+', "code":500}')
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
result = dataspace
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (result) {
|
|
184
|
+
// do the query here
|
|
185
|
+
let query = req.body.toString() // raw body through express.raw()
|
|
186
|
+
console.log(query)
|
|
187
|
+
// @todo add text search: https://github.com/nextapps-de/flexsearch
|
|
188
|
+
// @todo add tree walk map/reduce/find/filter style functions
|
|
189
|
+
// @todo add arc tree dive function?
|
|
190
|
+
const vm = new VM({
|
|
191
|
+
// timeout: 1000,
|
|
192
|
+
allowAsync: false,
|
|
193
|
+
sandbox: {
|
|
194
|
+
// query: function(params) {
|
|
195
|
+
// return tripleStore.query(params)
|
|
196
|
+
// }
|
|
197
|
+
},
|
|
198
|
+
wasm: false
|
|
199
|
+
})
|
|
200
|
+
vm.freeze(result, 'data') // adds immutable result dataspace to sandbox as data
|
|
201
|
+
try {
|
|
202
|
+
result = vm.run(query)
|
|
203
|
+
let used = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
|
|
204
|
+
console.log(`(${used} MB)`);
|
|
205
|
+
} catch(err) {
|
|
206
|
+
console.log(err)
|
|
207
|
+
error = JSONTag.parse('<object class="Error">{"message":'+originalJSON.stringify(''+err)+',"code":422}')
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (error) {
|
|
212
|
+
res.status(error.code)
|
|
213
|
+
result = error
|
|
214
|
+
}
|
|
215
|
+
if (req.accepts('application/jsontag')) {
|
|
216
|
+
res.setHeader('content-type','application/jsontag+json')
|
|
217
|
+
res.send(JSONTag.stringify(result, null, 4)+"\n")
|
|
218
|
+
} else {
|
|
219
|
+
res.setHeader('content-type','application/json')
|
|
220
|
+
res.send(originalJSON.stringify(result, null, 4)+"\n")
|
|
221
|
+
}
|
|
222
|
+
let end = Date.now()
|
|
223
|
+
console.log(path, (end-start), process.memoryUsage())
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
server.get('/', (req,res) => {
|
|
227
|
+
res.send('<h1>JSONTag REST+ server</h1>')
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
function handleWebRequest(req,res)
|
|
231
|
+
{
|
|
232
|
+
let path = req.path;
|
|
233
|
+
path = path.replace(/[^a-z0-9_\.\-\/]*/gi, '') // whitelist acceptable file paths
|
|
234
|
+
path = path.replace(/\.+/g, '.') // blacklist '..'
|
|
235
|
+
if (!path) {
|
|
236
|
+
path = '/'
|
|
237
|
+
}
|
|
238
|
+
if (path.substring(path.length-1)==='/') {
|
|
239
|
+
path += 'index.html'
|
|
240
|
+
}
|
|
241
|
+
const options = {
|
|
242
|
+
root: process.cwd()+'/www'
|
|
243
|
+
}
|
|
244
|
+
if (fs.existsSync(options.root+path)) {
|
|
245
|
+
res.sendFile(path, options)
|
|
246
|
+
} else {
|
|
247
|
+
res.sendFile('/index.html', options)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function linkReplacer(data, baseURL) {
|
|
252
|
+
let type = JSONTag.getType(data)
|
|
253
|
+
let attributes = JSONTag.getAttributes(data)
|
|
254
|
+
if (Array.isArray(data)) {
|
|
255
|
+
data = data.map((entry,index) => {
|
|
256
|
+
return linkReplacer(data[index], baseURL+index+'/')
|
|
257
|
+
})
|
|
258
|
+
} else if (type === 'link') {
|
|
259
|
+
// do nothing
|
|
260
|
+
} else if (data && typeof data === 'object') {
|
|
261
|
+
data = JSONTag.clone(data)
|
|
262
|
+
Object.keys(data).forEach(key => {
|
|
263
|
+
if (Array.isArray(data[key])) {
|
|
264
|
+
data[key] = data[key].map((e,i) => {
|
|
265
|
+
if (e && typeof e === 'object') {
|
|
266
|
+
let id=JSONTag.getAttribute(e, 'id')
|
|
267
|
+
if (!id) {
|
|
268
|
+
id = baseURL+key+'/'+i+'/'
|
|
269
|
+
} else {
|
|
270
|
+
id = '#'+id
|
|
271
|
+
}
|
|
272
|
+
return new JSONTag.Link(id)
|
|
273
|
+
}
|
|
274
|
+
return e
|
|
275
|
+
})
|
|
276
|
+
} else if (typeof data[key] === 'object') {
|
|
277
|
+
let id=JSONTag.getAttribute(data[key], 'id')
|
|
278
|
+
if (!id) {
|
|
279
|
+
id = baseURL+key+'/'
|
|
280
|
+
} else {
|
|
281
|
+
id = '#'+id
|
|
282
|
+
}
|
|
283
|
+
data[key] = new JSONTag.Link(id)
|
|
284
|
+
}
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
return data
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function isString(s)
|
|
291
|
+
{
|
|
292
|
+
return typeof s === 'string' || s instanceof String
|
|
293
|
+
}
|
|
294
|
+
server.listen(port, () =>
|
|
295
|
+
{
|
|
296
|
+
console.log('JSONTag REST server listening on port '+port)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
main()
|
package/src/server.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import express from 'express'
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import pointer from 'json-pointer'
|
|
4
|
+
import JSONTag from '@muze-nl/jsontag'
|
|
5
|
+
import {JSONPath} from 'jsonpath-plus'
|
|
6
|
+
import {VM} from 'vm2'
|
|
7
|
+
import _ from 'array-where-select'
|
|
8
|
+
|
|
9
|
+
const server = express()
|
|
10
|
+
|
|
11
|
+
server.use(express.static(process.cwd()+'/www'))
|
|
12
|
+
|
|
13
|
+
function deepFreeze(obj) {
|
|
14
|
+
Object.freeze(obj)
|
|
15
|
+
Object.keys(obj).forEach(prop => {
|
|
16
|
+
if (typeof obj[prop] === 'object' && !Object.isFrozen(obj[prop])) {
|
|
17
|
+
deepFreeze(obj[prop])
|
|
18
|
+
}
|
|
19
|
+
})
|
|
20
|
+
return obj
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function joinArgs(args) {
|
|
24
|
+
return args = args.map(arg => {
|
|
25
|
+
if (isString(arg)) {
|
|
26
|
+
return arg
|
|
27
|
+
} else {
|
|
28
|
+
return JSONTag.stringify(arg)
|
|
29
|
+
}
|
|
30
|
+
}).join(' ')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function connectConsole(res) {
|
|
34
|
+
return {
|
|
35
|
+
log: function(...args) {
|
|
36
|
+
res.append('X-Console-Log', joinArgs(args))
|
|
37
|
+
},
|
|
38
|
+
warning: function(...args) {
|
|
39
|
+
res.append('X-Console-Warning', joinArgs(args))
|
|
40
|
+
},
|
|
41
|
+
error: function(...args) {
|
|
42
|
+
res.append('X-Console-Error', joinArgs(args))
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function main(options) {
|
|
48
|
+
|
|
49
|
+
const port = options.port || 3000;
|
|
50
|
+
const datafile = options.datafile || 'data.jsontag'
|
|
51
|
+
|
|
52
|
+
const originalJSON = JSON
|
|
53
|
+
JSON = JSONTag // monkeypatching
|
|
54
|
+
|
|
55
|
+
console.log('loading data...')
|
|
56
|
+
let file = fs.readFileSync(datafile)
|
|
57
|
+
let dataspace;
|
|
58
|
+
let meta = {}
|
|
59
|
+
try {
|
|
60
|
+
dataspace = JSONTag.parse(file.toString(), null, meta)
|
|
61
|
+
if (typeof options.index == 'function') {
|
|
62
|
+
options.index(dataspace, meta)
|
|
63
|
+
}
|
|
64
|
+
deepFreeze(dataspace)
|
|
65
|
+
} catch(e) {
|
|
66
|
+
console.error(e)
|
|
67
|
+
process.exit()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
let used = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
|
|
71
|
+
console.log(`data loaded (${used} MB)`);
|
|
72
|
+
|
|
73
|
+
// allow access to raw body, used to parse a query send as post body
|
|
74
|
+
server.use(express.raw({
|
|
75
|
+
type: (req) => true // parse body on all requests
|
|
76
|
+
}))
|
|
77
|
+
|
|
78
|
+
function accept(req, res, mimetypes, handler) {
|
|
79
|
+
let accept = req.accepts(mimetypes)
|
|
80
|
+
if (!accept) {
|
|
81
|
+
res.status(406)
|
|
82
|
+
res.send("<h1>406 Unacceptable</h1>\n")
|
|
83
|
+
return false
|
|
84
|
+
}
|
|
85
|
+
if (typeof handler === 'function') {
|
|
86
|
+
return handler(req, res, accept)
|
|
87
|
+
}
|
|
88
|
+
return true
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getDataSpace(req, res, dataspace) {
|
|
92
|
+
let path = req.path.substr(6); // cut '/query'
|
|
93
|
+
if (!path) {
|
|
94
|
+
path = '';
|
|
95
|
+
}
|
|
96
|
+
if (path.substring(path.length-1)==='/') {
|
|
97
|
+
//jsonpointer doesn't allow a trailing '/'
|
|
98
|
+
path = path.substring(0, path.length-1)
|
|
99
|
+
}
|
|
100
|
+
let result
|
|
101
|
+
if (path) {
|
|
102
|
+
//jsonpointer doesn't allow an empty pointer
|
|
103
|
+
try {
|
|
104
|
+
if (pointer.has(dataspace, path)) {
|
|
105
|
+
result = pointer.get(dataspace, path)
|
|
106
|
+
} else {
|
|
107
|
+
result = JSONTag.parse('<object class="Error">{"message":"Not found", "code":404}')
|
|
108
|
+
}
|
|
109
|
+
} catch(err) {
|
|
110
|
+
result = JSONTag.parse('<object class="Error">{"message":'+originalJSON.stringify(err.message)+', "code":500}')
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
result = dataspace
|
|
114
|
+
}
|
|
115
|
+
return result
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
server.get('/query/*', (req, res, next) =>
|
|
119
|
+
{
|
|
120
|
+
let start = Date.now()
|
|
121
|
+
|
|
122
|
+
if ( !accept(req,res,
|
|
123
|
+
['application/jsontag','application/json','text/html','text/javascript','image/*'],
|
|
124
|
+
function(req, res, accept) {
|
|
125
|
+
switch(accept) {
|
|
126
|
+
case 'text/html':
|
|
127
|
+
case 'image/*':
|
|
128
|
+
case 'text/javascript':
|
|
129
|
+
handleWebRequest(req,res,options);
|
|
130
|
+
return false
|
|
131
|
+
break
|
|
132
|
+
}
|
|
133
|
+
return true
|
|
134
|
+
}
|
|
135
|
+
)) {
|
|
136
|
+
// done
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
result = getDataSpace(req, res, dataspace)
|
|
141
|
+
if (JSONTag.getAttribute(result, 'class')==='Error') {
|
|
142
|
+
res.status(result.code)
|
|
143
|
+
}
|
|
144
|
+
result = linkReplacer(result, path+'/')
|
|
145
|
+
if (req.accepts('application/jsontag')) {
|
|
146
|
+
res.setHeader('content-type','application/jsontag+json')
|
|
147
|
+
res.send(JSONTag.stringify(result, null, 4)+"\n")
|
|
148
|
+
} else {
|
|
149
|
+
res.setHeader('content-type','application/json')
|
|
150
|
+
res.send(originalJSON.stringify(result, null, 4)+"\n")
|
|
151
|
+
}
|
|
152
|
+
let end = Date.now()
|
|
153
|
+
console.log(path, (end-start), process.memoryUsage())
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* handle queries, query is the post body
|
|
158
|
+
*/
|
|
159
|
+
server.post('/query/*', (req, res) => {
|
|
160
|
+
let start = Date.now()
|
|
161
|
+
if ( !accept(req,res,
|
|
162
|
+
['application/jsontag','application/json']) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
result = getDataSpace(req, res, dataspace)
|
|
167
|
+
if (result) {
|
|
168
|
+
// do the query here
|
|
169
|
+
let query = req.body.toString() // raw body through express.raw()
|
|
170
|
+
// @todo add text search: https://github.com/nextapps-de/flexsearch
|
|
171
|
+
// @todo add tree walk map/reduce/find/filter style functions
|
|
172
|
+
// @todo add arc tree dive function?
|
|
173
|
+
const vm = new VM({
|
|
174
|
+
// timeout: 1000,
|
|
175
|
+
allowAsync: false,
|
|
176
|
+
sandbox: {
|
|
177
|
+
root: dataspace,
|
|
178
|
+
data: result,
|
|
179
|
+
meta: meta,
|
|
180
|
+
_: _,
|
|
181
|
+
console: connectConsole(res),
|
|
182
|
+
JSONTag: JSONTag,
|
|
183
|
+
request: req
|
|
184
|
+
},
|
|
185
|
+
wasm: false
|
|
186
|
+
})
|
|
187
|
+
try {
|
|
188
|
+
result = vm.run(query)
|
|
189
|
+
let used = Math.round(process.memoryUsage().heapUsed / 1024 / 1024);
|
|
190
|
+
console.log(`(${used} MB)`);
|
|
191
|
+
} catch(err) {
|
|
192
|
+
console.log(err)
|
|
193
|
+
error = JSONTag.parse('<object class="Error">{"message":'+originalJSON.stringify(''+err)+',"code":422}')
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (error) {
|
|
198
|
+
res.status(error.code)
|
|
199
|
+
result = error
|
|
200
|
+
}
|
|
201
|
+
if (req.accepts('application/jsontag')) {
|
|
202
|
+
res.setHeader('content-type','application/jsontag+json')
|
|
203
|
+
res.send(JSONTag.stringify(result, null, 4)+"\n")
|
|
204
|
+
} else {
|
|
205
|
+
res.setHeader('content-type','application/json')
|
|
206
|
+
res.send(originalJSON.stringify(result, null, 4)+"\n")
|
|
207
|
+
}
|
|
208
|
+
let end = Date.now()
|
|
209
|
+
console.log(path, (end-start), process.memoryUsage())
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
server.get('/', (req,res) => {
|
|
213
|
+
res.send('<h1>JSONTag REST+ server</h1>') //TODO: implement something nice
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
function handleWebRequest(req,res,options)
|
|
217
|
+
{
|
|
218
|
+
let path = req.path;
|
|
219
|
+
path = path.replace(/[^a-z0-9_\.\-\/]*/gi, '') // whitelist acceptable file paths
|
|
220
|
+
path = path.replace(/\.+/g, '.') // blacklist '..'
|
|
221
|
+
if (!path) {
|
|
222
|
+
path = '/'
|
|
223
|
+
}
|
|
224
|
+
if (path.substring(path.length-1)==='/') {
|
|
225
|
+
path += 'index.html'
|
|
226
|
+
}
|
|
227
|
+
const fileOptions = {
|
|
228
|
+
root: options.root || process.cwd()+'/www'
|
|
229
|
+
}
|
|
230
|
+
if (fs.existsSync(fileOptions.root+path)) {
|
|
231
|
+
res.sendFile(path, fileOptions)
|
|
232
|
+
} else {
|
|
233
|
+
res.sendFile('/index.html', fileOptions)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function linkReplacer(data, baseURL) {
|
|
238
|
+
let type = JSONTag.getType(data)
|
|
239
|
+
let attributes = JSONTag.getAttributes(data)
|
|
240
|
+
if (Array.isArray(data)) {
|
|
241
|
+
data = data.map((entry,index) => {
|
|
242
|
+
return linkReplacer(data[index], baseURL+index+'/')
|
|
243
|
+
})
|
|
244
|
+
} else if (type === 'link') {
|
|
245
|
+
// do nothing
|
|
246
|
+
} else if (data && typeof data === 'object') {
|
|
247
|
+
data = JSONTag.clone(data)
|
|
248
|
+
Object.keys(data).forEach(key => {
|
|
249
|
+
if (Array.isArray(data[key])) {
|
|
250
|
+
data[key] = new JSONTag.Link(baseURL+key+'/')
|
|
251
|
+
} else if (typeof data[key] === 'object') {
|
|
252
|
+
if (JSONTag.getType(data[key])!=='link') {
|
|
253
|
+
let id=JSONTag.getAttribute(data[key], 'id')
|
|
254
|
+
if (!id) {
|
|
255
|
+
id = baseURL+key+'/'
|
|
256
|
+
}
|
|
257
|
+
data[key] = new JSONTag.Link(id)
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
})
|
|
261
|
+
}
|
|
262
|
+
return data
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function isString(s)
|
|
266
|
+
{
|
|
267
|
+
return typeof s === 'string' || s instanceof String
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
server.listen(port, () =>
|
|
271
|
+
{
|
|
272
|
+
console.log('JSONTag REST server listening on port '+port)
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
server.run = main
|
|
278
|
+
export default server
|