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.
Files changed (63) hide show
  1. package/README.md +261 -0
  2. package/client/README.md +69 -0
  3. package/client/browser.js +17 -0
  4. package/client/connectSocket.js +260 -0
  5. package/dist/ape.js +454 -0
  6. package/example/ExpressJs/README.md +97 -0
  7. package/example/ExpressJs/api/message.js +11 -0
  8. package/example/ExpressJs/backend.js +37 -0
  9. package/example/ExpressJs/index.html +88 -0
  10. package/example/ExpressJs/package-lock.json +834 -0
  11. package/example/ExpressJs/package.json +10 -0
  12. package/example/ExpressJs/styles.css +128 -0
  13. package/example/NextJs/.dockerignore +29 -0
  14. package/example/NextJs/Dockerfile +52 -0
  15. package/example/NextJs/Dockerfile.dev +27 -0
  16. package/example/NextJs/README.md +113 -0
  17. package/example/NextJs/ape/client.js +66 -0
  18. package/example/NextJs/ape/embed.js +12 -0
  19. package/example/NextJs/ape/index.js +23 -0
  20. package/example/NextJs/ape/logic/chat.js +62 -0
  21. package/example/NextJs/ape/onConnect.js +69 -0
  22. package/example/NextJs/ape/onDisconnect.js +13 -0
  23. package/example/NextJs/ape/onError.js +9 -0
  24. package/example/NextJs/ape/onReceive.js +15 -0
  25. package/example/NextJs/ape/onSend.js +15 -0
  26. package/example/NextJs/api/message.js +44 -0
  27. package/example/NextJs/docker-compose.yml +22 -0
  28. package/example/NextJs/next-env.d.ts +5 -0
  29. package/example/NextJs/next.config.js +8 -0
  30. package/example/NextJs/package-lock.json +5107 -0
  31. package/example/NextJs/package.json +25 -0
  32. package/example/NextJs/pages/_app.tsx +6 -0
  33. package/example/NextJs/pages/index.tsx +182 -0
  34. package/example/NextJs/public/favicon.ico +0 -0
  35. package/example/NextJs/public/vercel.svg +4 -0
  36. package/example/NextJs/server.js +40 -0
  37. package/example/NextJs/styles/Chat.module.css +194 -0
  38. package/example/NextJs/styles/Home.module.css +129 -0
  39. package/example/NextJs/styles/globals.css +26 -0
  40. package/example/NextJs/tsconfig.json +20 -0
  41. package/example/README.md +66 -0
  42. package/index.d.ts +179 -0
  43. package/index.js +11 -0
  44. package/package.json +11 -4
  45. package/server/README.md +93 -0
  46. package/server/index.js +6 -0
  47. package/server/lib/broadcast.js +63 -0
  48. package/server/lib/loader.js +10 -0
  49. package/server/lib/main.js +23 -0
  50. package/server/lib/wiring.js +94 -0
  51. package/server/security/extractRootDomain.js +21 -0
  52. package/server/security/origin.js +13 -0
  53. package/server/security/reply.js +21 -0
  54. package/server/socket/open.js +10 -0
  55. package/server/socket/receive.js +66 -0
  56. package/server/socket/send.js +55 -0
  57. package/server/utils/deepRequire.js +45 -0
  58. package/server/utils/genId.js +24 -0
  59. package/todo.md +85 -0
  60. package/utils/jss.js +273 -0
  61. package/utils/jss.test.js +261 -0
  62. package/utils/messageHash.js +43 -0
  63. 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 };