api-ape 0.0.0 → 1.0.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/README.md +261 -0
- package/client/README.md +69 -0
- package/client/browser.js +17 -0
- package/client/connectSocket.js +260 -0
- package/dist/ape.js +454 -0
- package/example/ExpressJs/README.md +97 -0
- package/example/ExpressJs/api/message.js +11 -0
- package/example/ExpressJs/backend.js +37 -0
- package/example/ExpressJs/index.html +88 -0
- package/example/ExpressJs/package-lock.json +834 -0
- package/example/ExpressJs/package.json +10 -0
- package/example/ExpressJs/styles.css +128 -0
- package/example/NextJs/.dockerignore +29 -0
- package/example/NextJs/Dockerfile +52 -0
- package/example/NextJs/Dockerfile.dev +27 -0
- package/example/NextJs/README.md +113 -0
- package/example/NextJs/ape/client.js +66 -0
- package/example/NextJs/ape/embed.js +12 -0
- package/example/NextJs/ape/index.js +23 -0
- package/example/NextJs/ape/logic/chat.js +62 -0
- package/example/NextJs/ape/onConnect.js +69 -0
- package/example/NextJs/ape/onDisconnect.js +13 -0
- package/example/NextJs/ape/onError.js +9 -0
- package/example/NextJs/ape/onReceive.js +15 -0
- package/example/NextJs/ape/onSend.js +15 -0
- package/example/NextJs/api/message.js +44 -0
- package/example/NextJs/docker-compose.yml +22 -0
- package/example/NextJs/next-env.d.ts +5 -0
- package/example/NextJs/next.config.js +8 -0
- package/example/NextJs/package-lock.json +5107 -0
- package/example/NextJs/package.json +25 -0
- package/example/NextJs/pages/_app.tsx +6 -0
- package/example/NextJs/pages/index.tsx +182 -0
- package/example/NextJs/public/favicon.ico +0 -0
- package/example/NextJs/public/vercel.svg +4 -0
- package/example/NextJs/server.js +40 -0
- package/example/NextJs/styles/Chat.module.css +194 -0
- package/example/NextJs/styles/Home.module.css +129 -0
- package/example/NextJs/styles/globals.css +26 -0
- package/example/NextJs/tsconfig.json +20 -0
- package/example/README.md +66 -0
- package/index.d.ts +179 -0
- package/index.js +11 -0
- package/package.json +11 -4
- package/server/README.md +93 -0
- package/server/index.js +6 -0
- package/server/lib/broadcast.js +63 -0
- package/server/lib/loader.js +10 -0
- package/server/lib/main.js +23 -0
- package/server/lib/wiring.js +94 -0
- package/server/security/extractRootDomain.js +21 -0
- package/server/security/origin.js +13 -0
- package/server/security/reply.js +21 -0
- package/server/socket/open.js +10 -0
- package/server/socket/receive.js +66 -0
- package/server/socket/send.js +55 -0
- package/server/utils/deepRequire.js +45 -0
- package/server/utils/genId.js +24 -0
- package/todo.md +85 -0
- package/utils/jss.js +273 -0
- package/utils/jss.test.js +261 -0
- package/utils/messageHash.js +43 -0
- package/utils/messageHash.test.js +56 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const jss = require('../../utils/jss')
|
|
2
|
+
|
|
3
|
+
function checkSocketState(socket) {
|
|
4
|
+
if (socket.readyState !== socket.OPEN) {
|
|
5
|
+
switch (socket.readyState) {
|
|
6
|
+
case socket.CONNECTING:
|
|
7
|
+
throw "The connection is not yet open"
|
|
8
|
+
break;
|
|
9
|
+
case socket.CLOSING:
|
|
10
|
+
throw "The connection is in theprocess of closing."
|
|
11
|
+
break;
|
|
12
|
+
case socket.CLOSED:
|
|
13
|
+
throw "The connection is closed or couldn't be opened."
|
|
14
|
+
break;
|
|
15
|
+
} // END switch
|
|
16
|
+
//TODO: remove this socket if closed
|
|
17
|
+
} // END if
|
|
18
|
+
} // END checkSocketState
|
|
19
|
+
|
|
20
|
+
module.exports = function sendHandler({ socket, events, hostId }) {
|
|
21
|
+
|
|
22
|
+
return function send(queryId, type, data, err) {
|
|
23
|
+
if (!type && !queryId) {
|
|
24
|
+
throw new Error("You must pass a type OR a queryId in-order to send messages")
|
|
25
|
+
}
|
|
26
|
+
if (!data && !err) {
|
|
27
|
+
throw new Error("You must pass a data payload OR an error message in-order to send messages")
|
|
28
|
+
}
|
|
29
|
+
let onFinish = false
|
|
30
|
+
if (!queryId) { // dont call onSend as this will be past of the onReceive Flow
|
|
31
|
+
onFinish = events.onSend(data, type)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
checkSocketState(socket)
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (onFinish) {
|
|
38
|
+
onFinish(err, false)
|
|
39
|
+
} else if (queryId) {
|
|
40
|
+
throw err
|
|
41
|
+
} else {
|
|
42
|
+
console.error(err)
|
|
43
|
+
}
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (err) {
|
|
47
|
+
socket.send(jss.stringify({ err: err.message || err, type, queryId }))
|
|
48
|
+
if (typeof onFinish === 'function') onFinish(err, true)
|
|
49
|
+
} else {
|
|
50
|
+
socket.send(jss.stringify({ data, type, queryId }))
|
|
51
|
+
if (typeof onFinish === 'function') onFinish(false, data)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
} // END send
|
|
55
|
+
} //sendHandler
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
if(!global.process){//(!!process && typeof process !== 'object'){
|
|
2
|
+
throw new Error("deepRequire need to be run on Node server")
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
var fs = require('fs');
|
|
6
|
+
var path = require('path');
|
|
7
|
+
|
|
8
|
+
// Return a list of files of the specified fileTypes in the provided dir,
|
|
9
|
+
// with the file path relative to the given dir
|
|
10
|
+
// dir: path of the directory you want to search the files for
|
|
11
|
+
// fileTypes: array of file types you are search files, ex: ['.txt', '.jpg']
|
|
12
|
+
function getFilesFromDir(dir, fileTypes) {
|
|
13
|
+
var filesToReturn = [];
|
|
14
|
+
function walkDir(currentPath) {
|
|
15
|
+
var files = fs.readdirSync(currentPath);
|
|
16
|
+
for (var i in files) {
|
|
17
|
+
var curFile = path.join(currentPath, files[i]);
|
|
18
|
+
if (fs.statSync(curFile).isFile() && fileTypes.indexOf(path.extname(curFile)) != -1) {
|
|
19
|
+
filesToReturn.push(curFile.replace(dir, ''));
|
|
20
|
+
} else if (fs.statSync(curFile).isDirectory()) {
|
|
21
|
+
walkDir(curFile);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
walkDir(dir);
|
|
26
|
+
return filesToReturn;
|
|
27
|
+
}
|
|
28
|
+
const re = /(?:\.([^.]+))?$/;
|
|
29
|
+
|
|
30
|
+
module.exports = function(dirname,selector){
|
|
31
|
+
selector = selector || ["js"]
|
|
32
|
+
return getFilesFromDir(dirname, selector.map(ext=>`.${ext}`)).reduce((packages,file) =>{
|
|
33
|
+
|
|
34
|
+
if(file === "/index.js") return packages
|
|
35
|
+
//if(file[0] !== "/") file = "/"+file;
|
|
36
|
+
|
|
37
|
+
const pathParts = file.replace(re.exec(file)[0],"").split("/").slice(1)
|
|
38
|
+
if(pathParts[pathParts.length-1] === "index")
|
|
39
|
+
pathParts.pop()
|
|
40
|
+
|
|
41
|
+
packages[pathParts.join("/").toLowerCase()] = require(dirname+`/${file}`)
|
|
42
|
+
return packages;
|
|
43
|
+
},{});
|
|
44
|
+
|
|
45
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
function genId(size, range) {
|
|
2
|
+
|
|
3
|
+
size = size || 10
|
|
4
|
+
range = range|| "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
|
|
5
|
+
|
|
6
|
+
if ('number' !== typeof size ) {
|
|
7
|
+
throw new Error("size must be a number")
|
|
8
|
+
} else if (1 > size) {
|
|
9
|
+
throw new Error("positive size needed")
|
|
10
|
+
} else if ('string' !== typeof range ) {
|
|
11
|
+
throw new Error("range must be a string")
|
|
12
|
+
} else if (1 > range.length) {
|
|
13
|
+
throw new Error("range to small")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
var id = ""
|
|
17
|
+
|
|
18
|
+
for (var i = 0; i < size; i++) {
|
|
19
|
+
id += range[~~(Math.random() * range.length)]
|
|
20
|
+
}
|
|
21
|
+
return id
|
|
22
|
+
} // END genId
|
|
23
|
+
|
|
24
|
+
module.exports = genId
|
package/todo.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Query System - TODO
|
|
2
|
+
|
|
3
|
+
A fluent, chainable query API for api-ape with filtering, field selection, and real-time subscriptions.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Client API Design
|
|
8
|
+
```js
|
|
9
|
+
const petsReq = ape.pets.list(data, (item) => shouldSubscribe)
|
|
10
|
+
petsReq.filter`name ! ${undefined} AND bio.checkin > ${10} OR bio.type = ${"admin"}`
|
|
11
|
+
petsReq.fields("*", {bio: ["email"]})
|
|
12
|
+
petsReq.then(pets => ...).catch(err => ...)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Phase 1: Filter Parser
|
|
18
|
+
- [ ] Create `client/utils/filter.js` - tagged template parser
|
|
19
|
+
- [ ] Parse operators: `=`, `!` (not equal), `?` (exists), `>`, `<`
|
|
20
|
+
- [ ] Parse `AND` / `OR` with correct precedence
|
|
21
|
+
- [ ] Support nested paths (e.g., `bio.checkin`, `owner.type`)
|
|
22
|
+
- [ ] Return serializable filter object to send over wire
|
|
23
|
+
|
|
24
|
+
## Phase 2: Fields Selection
|
|
25
|
+
- [ ] Parse `fields("*", {relation: ["field1", "field2"]})`
|
|
26
|
+
- [ ] `"*"` = all root-level fields
|
|
27
|
+
- [ ] Object syntax for nested/related field selection
|
|
28
|
+
- [ ] Max depth limit (≤ 4)
|
|
29
|
+
|
|
30
|
+
## Phase 3: Query Builder (Client)
|
|
31
|
+
- [ ] Refactor `client/index.js` → proper query builder
|
|
32
|
+
- [ ] Chainable `.filter()` and `.fields()` methods
|
|
33
|
+
- [ ] Integrate with existing `connectSocket.js` sender
|
|
34
|
+
- [ ] Send query payload: `{ type, data, filter, fields, subscribe }`
|
|
35
|
+
|
|
36
|
+
## Phase 4: Subscription Callback
|
|
37
|
+
- [ ] Second arg: `(item) => boolean` subscription predicate
|
|
38
|
+
- [ ] Register listener via `setOnReciver` for matching type
|
|
39
|
+
- [ ] Filter incoming broadcasts client-side with predicate
|
|
40
|
+
|
|
41
|
+
## Phase 5: Server Query Execution
|
|
42
|
+
- [ ] Parse incoming `filter` object
|
|
43
|
+
- [ ] Apply filter to controller response data
|
|
44
|
+
- [ ] Respect `fields` selection (project/limit returned data)
|
|
45
|
+
- [ ] Track subscribed queryIds for broadcast targeting
|
|
46
|
+
|
|
47
|
+
## Phase 6: Skip/Have Optimization (Future)
|
|
48
|
+
- [ ] `.dont([id1, id2])` - skip items client already has
|
|
49
|
+
- [ ] Server tracks live refs per client
|
|
50
|
+
- [ ] Only send missing/changed items
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Open Questions
|
|
55
|
+
|
|
56
|
+
### Query Method Semantics
|
|
57
|
+
- `ape.pets.list(null, (pet) => { pet.owner == me })`:
|
|
58
|
+
- [ ] What does the first argument (`null`) represent? Initial filter data? Query options?
|
|
59
|
+
- [ ] What does the callback do? Live subscription filter? Server-side predicate?
|
|
60
|
+
|
|
61
|
+
### Filter Syntax
|
|
62
|
+
- `.filter\`name ! ${undefined} AND owner.checkin > ${10} OR owner.type = ${"nice"}\``:
|
|
63
|
+
- [ ] Confirm operators: `!` = not equal, `?` = exists, `>` `<` `=` = comparison
|
|
64
|
+
- [ ] Need additional operators? `>=`, `<=`, `LIKE`, `IN`, `BETWEEN`?
|
|
65
|
+
- [ ] Should `AND`/`OR` respect standard boolean precedence?
|
|
66
|
+
- [ ] Should `${10}DaysAgo` be a special relative time syntax?
|
|
67
|
+
|
|
68
|
+
### Fields Selection
|
|
69
|
+
- `.fields("*", {toys: ["type"]})`:
|
|
70
|
+
- [ ] `"*"` = all fields at root level?
|
|
71
|
+
- [ ] Object syntax `{relation: [...]}` for nested/related fields?
|
|
72
|
+
- [ ] Max depth limit? (code comment says ≤ 4)
|
|
73
|
+
- [ ] Support exclusion? (e.g., `"-password"`)
|
|
74
|
+
|
|
75
|
+
### Real-time Subscriptions
|
|
76
|
+
- [ ] How should live updates work? Push on any change to matching items?
|
|
77
|
+
- [ ] Can the subscription filter differ from the initial query filter?
|
|
78
|
+
- [ ] What events trigger updates? Create, Update, Delete?
|
|
79
|
+
- [ ] Broadcast filtering - server-side or client-side predicate eval?
|
|
80
|
+
|
|
81
|
+
### Additional Features (In Scope?)
|
|
82
|
+
- [ ] Pagination: `.limit()`, `.offset()`, `.cursor()`
|
|
83
|
+
- [ ] Sorting: `.orderBy()`
|
|
84
|
+
- [ ] Aggregations: `.count()`, `.sum()`
|
|
85
|
+
- [ ] Skip/Have optimization: `.dont([ids])` for delta updates
|
package/utils/jss.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
//JsonSuperSet
|
|
2
|
+
|
|
3
|
+
// TODO: add tests
|
|
4
|
+
// check for any repeated ref not just cyclical references
|
|
5
|
+
// support nasted array a<![,,[D]]>:["a","b",[Date]]
|
|
6
|
+
// support array for the same type a<![*D]>:[Date,Date,Date]
|
|
7
|
+
|
|
8
|
+
function encode(obj) {
|
|
9
|
+
const tagLookup = {
|
|
10
|
+
'[object RegExp]': 'R',
|
|
11
|
+
'[object Date]': 'D',
|
|
12
|
+
'[object Error]': 'E',
|
|
13
|
+
"[object Undefined]": 'U',
|
|
14
|
+
"[object Map]": 'M',
|
|
15
|
+
"[object Set]": 'S',
|
|
16
|
+
};
|
|
17
|
+
const visited = new WeakMap();
|
|
18
|
+
|
|
19
|
+
function encodeValue(value, path = '') {
|
|
20
|
+
const type = typeof value;
|
|
21
|
+
const tag = tagLookup[Object.prototype.toString.call(value)];
|
|
22
|
+
// console.log({tag,value,path})
|
|
23
|
+
if (tag !== undefined) {
|
|
24
|
+
if ('D' === tag) return [tag, value.valueOf()];
|
|
25
|
+
if ('E' === tag) return [tag, [value.name, value.message, value.stack]];
|
|
26
|
+
if ('R' === tag) return [tag, value.toString()];
|
|
27
|
+
if ('U' === tag) return [tag, null];
|
|
28
|
+
if ('S' === tag) return [tag, Array.from(value)];
|
|
29
|
+
if ('M' === tag) return [tag, Object.fromEntries(value)];
|
|
30
|
+
|
|
31
|
+
return [tag, JSON.stringify(value)];
|
|
32
|
+
} else if (type === 'object' && value !== null) {
|
|
33
|
+
/*if (value.$ID) {
|
|
34
|
+
return ['', value.$ID];
|
|
35
|
+
}*/
|
|
36
|
+
if (visitedEncode.has(value)) {
|
|
37
|
+
return ['P', visitedEncode.get(value)];
|
|
38
|
+
}
|
|
39
|
+
visitedEncode.set(value, path);
|
|
40
|
+
const isArray = Array.isArray(value);
|
|
41
|
+
// keep index with undefined in Array
|
|
42
|
+
const keys = isArray ? Array.from(Array(value.length).keys()) : Object.keys(value);
|
|
43
|
+
const result = isArray ? [] : {};
|
|
44
|
+
const typesFound = [];
|
|
45
|
+
for (let i = 0; i < keys.length; i++) {
|
|
46
|
+
const key = keys[i];
|
|
47
|
+
const [t, v] = encodeValue(value[key], key);
|
|
48
|
+
// console.log([t, v])
|
|
49
|
+
if (isArray) {
|
|
50
|
+
typesFound.push(t);
|
|
51
|
+
result.push(v);
|
|
52
|
+
// remove key with undefined from Objects
|
|
53
|
+
} else if (value[key] !== undefined) {
|
|
54
|
+
result[key + (t ? `<!${t}>` : '')] = v;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
visited.delete(value);
|
|
59
|
+
if (isArray && typesFound.find((t) => !!t)) {
|
|
60
|
+
return [`[${typesFound.join()}]`, result];
|
|
61
|
+
}
|
|
62
|
+
return ['', result];
|
|
63
|
+
} else {
|
|
64
|
+
return ['', value];
|
|
65
|
+
}
|
|
66
|
+
} // END encodeValue
|
|
67
|
+
|
|
68
|
+
let keys = [];
|
|
69
|
+
// console.log(obj)
|
|
70
|
+
if (Array.isArray(obj)) {
|
|
71
|
+
keys = Array.from(Array(obj.length).keys())
|
|
72
|
+
} else {
|
|
73
|
+
keys = Object.keys(obj);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Track root object to handle self-references
|
|
77
|
+
const visitedEncode = new WeakMap();
|
|
78
|
+
visitedEncode.set(obj, []); // Root path is empty array
|
|
79
|
+
|
|
80
|
+
function encodeValueWithVisited(value, path = []) {
|
|
81
|
+
const type = typeof value;
|
|
82
|
+
const tag = tagLookup[Object.prototype.toString.call(value)];
|
|
83
|
+
if (tag !== undefined) {
|
|
84
|
+
if ('D' === tag) return [tag, value.valueOf()];
|
|
85
|
+
if ('E' === tag) return [tag, [value.name, value.message, value.stack]];
|
|
86
|
+
if ('R' === tag) return [tag, value.toString()];
|
|
87
|
+
if ('U' === tag) return [tag, null];
|
|
88
|
+
if ('S' === tag) return [tag, Array.from(value)];
|
|
89
|
+
if ('M' === tag) return [tag, Object.fromEntries(value)];
|
|
90
|
+
return [tag, JSON.stringify(value)];
|
|
91
|
+
} else if (type === 'object' && value !== null) {
|
|
92
|
+
if (visitedEncode.has(value)) {
|
|
93
|
+
return ['P', visitedEncode.get(value)]; // Return array path
|
|
94
|
+
}
|
|
95
|
+
visitedEncode.set(value, path);
|
|
96
|
+
const isArray = Array.isArray(value);
|
|
97
|
+
const objKeys = isArray ? Array.from(Array(value.length).keys()) : Object.keys(value);
|
|
98
|
+
const result = isArray ? [] : {};
|
|
99
|
+
const typesFound = [];
|
|
100
|
+
for (let i = 0; i < objKeys.length; i++) {
|
|
101
|
+
const key = objKeys[i];
|
|
102
|
+
const [t, v] = encodeValueWithVisited(value[key], [...path, key]); // Append key to path array
|
|
103
|
+
if (isArray) {
|
|
104
|
+
typesFound.push(t);
|
|
105
|
+
result.push(v);
|
|
106
|
+
} else if (value[key] !== undefined) {
|
|
107
|
+
result[key + (t ? `<!${t}>` : '')] = v;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (isArray && typesFound.find((t) => !!t)) {
|
|
111
|
+
return [`[${typesFound.join()}]`, result];
|
|
112
|
+
}
|
|
113
|
+
return ['', result];
|
|
114
|
+
} else {
|
|
115
|
+
return ['', value];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const result = {};
|
|
120
|
+
for (let i = 0; i < keys.length; i++) {
|
|
121
|
+
const key = keys[i];
|
|
122
|
+
// remove key with undefined from Objects
|
|
123
|
+
if (obj[key] !== undefined) {
|
|
124
|
+
const [t, v] = encodeValueWithVisited(obj[key], [key]); // Start path with single key
|
|
125
|
+
result[key + (t ? `<!${t}>` : '')] = v;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
} // END encode
|
|
130
|
+
|
|
131
|
+
function stringify(obj) {
|
|
132
|
+
return JSON.stringify(encode(obj))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
function parse(encoded) {
|
|
137
|
+
return decode(JSON.parse(encoded))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function decode(data) {
|
|
141
|
+
const result = {};
|
|
142
|
+
const pointers2Res = [];
|
|
143
|
+
const tagLookup = {
|
|
144
|
+
R: (s) => new RegExp(s),
|
|
145
|
+
D: (n) => new Date(n),
|
|
146
|
+
P: function (sourceToPointAt, replaceAtThisPlace) {
|
|
147
|
+
// Both paths are now arrays
|
|
148
|
+
pointers2Res.push([sourceToPointAt, replaceAtThisPlace]);
|
|
149
|
+
return null; // Placeholder, will be replaced by changeAttributeReference
|
|
150
|
+
},
|
|
151
|
+
E: ([name, message, stack]) => {
|
|
152
|
+
let err;
|
|
153
|
+
try {
|
|
154
|
+
err = new global[name](message);
|
|
155
|
+
if (err instanceof Error) err.stack = stack;
|
|
156
|
+
else throw {};
|
|
157
|
+
} catch (e) {
|
|
158
|
+
err = new Error(message);
|
|
159
|
+
err.name = name;
|
|
160
|
+
err.stack = stack;
|
|
161
|
+
}
|
|
162
|
+
return err;
|
|
163
|
+
},
|
|
164
|
+
U: () => undefined,
|
|
165
|
+
S: (a) => new Set(a),
|
|
166
|
+
M: (o) => new Map(Object.entries(o))
|
|
167
|
+
};
|
|
168
|
+
const visited = new Map();
|
|
169
|
+
|
|
170
|
+
function decodeValue(name, tag, val) {
|
|
171
|
+
// this is now an array path
|
|
172
|
+
const currentPath = Array.isArray(this) ? this : [];
|
|
173
|
+
|
|
174
|
+
if (tag in tagLookup) {
|
|
175
|
+
return tagLookup[tag](val, currentPath);
|
|
176
|
+
} else if (Array.isArray(val)) {
|
|
177
|
+
if (tag && tag.startsWith('[')) {
|
|
178
|
+
const typeTags = tag.slice(1, -1).split(',');
|
|
179
|
+
const res = [];
|
|
180
|
+
for (let i = 0; i < val.length; i++) {
|
|
181
|
+
// Pass path with array index appended
|
|
182
|
+
const itemPath = [...currentPath, i];
|
|
183
|
+
const decodedValue = decodeValue.call(
|
|
184
|
+
itemPath,
|
|
185
|
+
i.toString(),
|
|
186
|
+
typeTags[i],
|
|
187
|
+
val[i]
|
|
188
|
+
);
|
|
189
|
+
res.push(decodedValue);
|
|
190
|
+
}
|
|
191
|
+
return res;
|
|
192
|
+
} else {
|
|
193
|
+
const res = [];
|
|
194
|
+
for (let i = 0; i < val.length; i++) {
|
|
195
|
+
const decodedValue = decodeValue.call([...currentPath, i], '', '', val[i]);
|
|
196
|
+
res.push(decodedValue);
|
|
197
|
+
}
|
|
198
|
+
return res;
|
|
199
|
+
}
|
|
200
|
+
} else if ('object' === typeof val && val !== null) {
|
|
201
|
+
if (visited.has(val)) {
|
|
202
|
+
return visited.get(val);
|
|
203
|
+
}
|
|
204
|
+
visited.set(val, {});
|
|
205
|
+
const res = {};
|
|
206
|
+
for (const key in val) {
|
|
207
|
+
const [nam, t] = parseKeyWithTags(key);
|
|
208
|
+
const decodedValue = decodeValue.call(
|
|
209
|
+
[...currentPath, nam],
|
|
210
|
+
nam,
|
|
211
|
+
t,
|
|
212
|
+
val[key]
|
|
213
|
+
);
|
|
214
|
+
res[nam] = decodedValue;
|
|
215
|
+
}
|
|
216
|
+
visited.set(val, res);
|
|
217
|
+
return res;
|
|
218
|
+
} else {
|
|
219
|
+
return val;
|
|
220
|
+
}
|
|
221
|
+
} // END decodeValue
|
|
222
|
+
|
|
223
|
+
function parseKeyWithTags(key) {
|
|
224
|
+
const match = key.match(/(.+)(<!(.)>)/);
|
|
225
|
+
if (match) {
|
|
226
|
+
return [match[1], match[3]];
|
|
227
|
+
}
|
|
228
|
+
// Try multi-character tags like array types [,D,]
|
|
229
|
+
const multiMatch = key.match(/(.+)(<!!(.+)>)/);
|
|
230
|
+
if (multiMatch) {
|
|
231
|
+
return [multiMatch[1], multiMatch[3]];
|
|
232
|
+
}
|
|
233
|
+
// Also handle array type tags that start with [
|
|
234
|
+
const arrayMatch = key.match(/(.+)(<!\[(.*)>)/);
|
|
235
|
+
if (arrayMatch) {
|
|
236
|
+
return [arrayMatch[1], '[' + arrayMatch[3]];
|
|
237
|
+
}
|
|
238
|
+
return [key, undefined];
|
|
239
|
+
} // END parseKeyWithTags
|
|
240
|
+
|
|
241
|
+
for (const key in data) {
|
|
242
|
+
const [name, tag] = parseKeyWithTags(key);
|
|
243
|
+
// Start with path containing just the key name
|
|
244
|
+
result[name] = decodeValue.call([name], name, tag, data[key]);
|
|
245
|
+
}
|
|
246
|
+
pointers2Res.forEach(changeAttributeReference.bind(null, result));
|
|
247
|
+
return result;
|
|
248
|
+
} // END decode
|
|
249
|
+
|
|
250
|
+
function changeAttributeReference(obj, [refPath, attrPath]) {
|
|
251
|
+
// refPath and attrPath are now arrays, no splitting needed
|
|
252
|
+
const refKeys = refPath || [];
|
|
253
|
+
const attrKeys = attrPath || [];
|
|
254
|
+
|
|
255
|
+
// Get the reference target by traversing refPath
|
|
256
|
+
let ref = obj;
|
|
257
|
+
for (let i = 0; i < refKeys.length; i++) {
|
|
258
|
+
ref = ref[refKeys[i]];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Get the parent of the attribute to set
|
|
262
|
+
let attr = obj;
|
|
263
|
+
for (let i = 0; i < attrKeys.length - 1; i++) {
|
|
264
|
+
attr = attr[attrKeys[i]];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Set the attribute to point to the reference
|
|
268
|
+
attr[attrKeys[attrKeys.length - 1]] = ref;
|
|
269
|
+
return obj;
|
|
270
|
+
} // END changeAttributeReference
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
module.exports = { parse, stringify, encode, decode };
|