@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@muze-nl/simplystore",
3
- "version": "0.1.2",
3
+ "version": "0.1.5",
4
4
  "main": "src/server.mjs",
5
5
  "type": "module",
6
6
  "scripts": {
package/package.json~ CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "name": "@muze-nl/SimplyStore",
3
- "version": "0.1.2",
2
+ "name": "@muze-nl/simplystore",
3
+ "version": "0.1.4",
4
4
  "main": "src/server.mjs",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -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
@@ -0,0 +1,3 @@
1
+
2
+ *** These modified files were found in JOE when it aborted on Thu Mar 23 15:39:53 2023
3
+ *** JOE was aborted because the terminal closed
@@ -0,0 +1,7 @@
1
+ import {EditorView, basicSetup} from "codemirror"
2
+ import {javascript} from "@codemirror/lang-javascript"
3
+
4
+ let codeEditor = new EditorView({
5
+ extensions: [basicSetup, javascript()],
6
+ parent: document.body
7
+ })
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