primate 0.3.1 → 0.5.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 (57) hide show
  1. package/README.md +10 -7
  2. package/debris.json +5 -0
  3. package/jsconfig.json +8 -0
  4. package/package.json +16 -4
  5. package/source/client/Action.js +20 -22
  6. package/source/client/Client.js +13 -17
  7. package/source/client/Context.js +10 -16
  8. package/source/client/Element.js +18 -14
  9. package/source/client/Session.js +27 -0
  10. package/source/client/View.js +1 -2
  11. package/source/client/document.js +1 -1
  12. package/source/preset/primate.json +2 -8
  13. package/source/server/Action.js +57 -75
  14. package/source/server/App.js +27 -8
  15. package/source/server/Bundler.js +13 -16
  16. package/source/server/Context.js +63 -56
  17. package/source/server/{promises/Eager.js → EagerPromise.js} +4 -2
  18. package/source/server/File.js +5 -1
  19. package/source/server/Projector.js +86 -0
  20. package/source/server/Router.js +15 -24
  21. package/source/server/Session.js +9 -33
  22. package/source/server/attributes.js +3 -0
  23. package/source/server/conf.js +20 -26
  24. package/source/server/{Crypto.js → crypto.js} +0 -0
  25. package/source/server/domain/Domain.js +56 -46
  26. package/source/server/domain/Field.js +34 -36
  27. package/source/server/domain/Predicate.js +24 -0
  28. package/source/server/domain/domains.js +17 -1
  29. package/source/server/errors/InternalServer.js +1 -1
  30. package/source/server/errors/Predicate.js +1 -1
  31. package/source/server/errors.js +0 -1
  32. package/source/server/exports.js +10 -9
  33. package/source/server/{utils/extend_object.js → extend_object.js} +0 -0
  34. package/source/server/invariants.js +23 -10
  35. package/source/server/sanitize.js +10 -0
  36. package/source/server/servers/Dynamic.js +19 -13
  37. package/source/server/servers/Server.js +1 -1
  38. package/source/server/servers/Static.js +25 -20
  39. package/source/server/store/Store.js +13 -0
  40. package/source/server/types/Array.js +2 -2
  41. package/source/server/types/Boolean.js +2 -2
  42. package/source/server/types/Date.js +2 -2
  43. package/source/server/types/Domain.js +1 -6
  44. package/source/server/types/Instance.js +1 -1
  45. package/source/server/types/Number.js +2 -2
  46. package/source/server/types/Object.js +2 -2
  47. package/source/server/types/Primitive.js +1 -1
  48. package/source/server/types/Storeable.js +12 -19
  49. package/source/server/types/String.js +2 -2
  50. package/source/server/view/TreeNode.js +2 -0
  51. package/source/server/view/View.js +6 -1
  52. package/source/client/Base.js +0 -5
  53. package/source/server/Base.js +0 -35
  54. package/source/server/errors/Fallback.js +0 -1
  55. package/source/server/fallback.js +0 -11
  56. package/source/server/promises/Meta.js +0 -42
  57. package/source/server/promises.js +0 -2
package/README.md CHANGED
@@ -1,8 +1,5 @@
1
1
  # Primate: a JavaScript framework
2
2
 
3
- ⚠️ **This software is currently its initial development phase. It is not
4
- considered production-ready. Expect breakage.** ⚠️
5
-
6
3
  Primate is a server-client JavaScript framework for web applications aimed at
7
4
  relieving you of dealing with repetitive, error-prone tasks and letting you
8
5
  concentrate on writing effective, expressive code.
@@ -10,7 +7,7 @@ concentrate on writing effective, expressive code.
10
7
  ## Installing
11
8
 
12
9
  ```
13
- yarn add primate
10
+ npm install primate
14
11
  ```
15
12
 
16
13
  ## Concepts
@@ -23,11 +20,12 @@ yarn add primate
23
20
  * **Correct** data verification using field definitions in domains
24
21
  * **Secure** serving hash-verified scripts on secure HTTP with a strong CSP
25
22
  * **Roles** access control with contexts
26
- * **Sessions** using cookies that are `HttpOnly`, `Secure` and `Path`-limited
23
+ * **Sessions** using cookies that are `Secure`, `SameSite=Strict`, `HttpOnly`
24
+ and `Path`-limited
27
25
  * **Sane defaults** minimally opinionated with good, overrideable defaults
28
26
  for most features
29
27
  * **Data linking** modeling `1:1`, `1:n` and `n:m` relationships on domains via
30
- ad-hoc getters
28
+ cacheable ad-hoc getters
31
29
 
32
30
  ## Getting started
33
31
 
@@ -35,7 +33,10 @@ See the [getting started][getting-started] guide.
35
33
 
36
34
  ## Resources
37
35
 
38
- Coming soon.
36
+ * [Source code][source-code]
37
+ * [Issues][issues]
38
+
39
+ A full guide is coming soon.
39
40
 
40
41
  ## Versioning
41
42
 
@@ -49,3 +50,5 @@ BSD-3-Clause
49
50
 
50
51
  [ws]: https://github.com/websockets/ws
51
52
  [getting-started]: https://primatejs.com/getting-started
53
+ [source-code]: https://adaptivecloud.dev/primate/primate
54
+ [issues]: https://adaptivecloud.dev/primate/primate/issues
package/debris.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "suites": "test/suites",
3
+ "fixtures": "test/fixtures",
4
+ "explicit": false
5
+ }
package/jsconfig.json ADDED
@@ -0,0 +1,8 @@
1
+ {
2
+ "compilerOptions": {
3
+ "baseUrl": ".",
4
+ "target": "esnext",
5
+ "module": "esnext"
6
+ },
7
+ "exclude": ["node_modules"]
8
+ }
package/package.json CHANGED
@@ -1,19 +1,31 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.3.1",
3
+ "version": "0.5.1",
4
4
  "author": "Primate core team <core@primatejs.com>",
5
5
  "homepage": "https://primatejs.com",
6
+ "bugs": "https://adaptivecloud.dev/primate/primate/issues",
7
+ "repository": "https://adaptivecloud.dev/primate/primate",
6
8
  "description": "Server-client framework",
7
9
  "license": "BSD-3-Clause",
8
10
  "dependencies": {
9
- "ws": "^8.4.0"
11
+ "ws": "^8.5.0"
10
12
  },
11
13
  "scripts": {
12
- "test": "node --experimental-json-modules node_modules/stick stick.json"
14
+ "copy": "rm -rf output && mkdir output && cp source/* output -a",
15
+ "instrument": "nyc instrument source ./output",
16
+ "debris": "node --experimental-json-modules node_modules/debris debris.json",
17
+ "coverage": "npm run instrument && nyc npm run debris",
18
+ "test": "npm run copy && npm run debris"
19
+ },
20
+ "devDependencies": {
21
+ "debris": "^0.2.0",
22
+ "eslint": "^8.11.0",
23
+ "eslint-plugin-json": "^3.1.0",
24
+ "nyc": "^15.1.0"
13
25
  },
14
26
  "type": "module",
15
27
  "exports": "./source/server/exports.js",
16
28
  "engines": {
17
- "node": ">=17.3.0"
29
+ "node": ">=17.7.2"
18
30
  }
19
31
  }
@@ -1,19 +1,17 @@
1
1
  import Element from "../Element.js";
2
2
  import View from "./View.js";
3
- import Base from "./Base.js";
4
- import {origin_base} from "./document.js";
3
+ import {origin_base, origin} from "./document.js";
5
4
 
6
- export default class Action extends Base {
7
- constructor(name, context) {
8
- super();
5
+ export default class Action {
6
+ constructor(name, session) {
9
7
  this.name = name;
10
- this.context = context;
8
+ this.session = session;
11
9
  this.listeners = [];
12
10
  }
13
11
 
14
12
  before() {}
15
13
 
16
- async enter(data) {
14
+ enter(data) {
17
15
  if (data.location) {
18
16
  location.href = data.location;
19
17
  } else {
@@ -23,20 +21,20 @@ export default class Action extends Base {
23
21
 
24
22
  create_view(data, root, save_history = true) {
25
23
  if (save_history) {
26
- const pathname = data.pathname === "" ? "/" : data.pathname;
27
- window.location.pathname !== pathname
28
- && history.pushState({}, "", pathname);
24
+ if (data.url !== document.location.href.replace(origin, "")) {
25
+ history.pushState({}, "", data.url);
26
+ }
29
27
  }
30
28
  this.view = new View(data, this, root);
31
29
  }
32
30
 
33
- create_and_return_view(data, root, save_history) {
31
+ create_and_return_view(data, root) {
34
32
  return new View(data, this, root);
35
33
  }
36
34
 
37
35
  get_form_data(target) {
38
36
  const payload = {};
39
- const elements = target.elements;
37
+ const {elements} = target;
40
38
  for (let i = 0; i < elements.length; i++) {
41
39
  const element = elements[i];
42
40
  const type = Element.value(element, "type");
@@ -66,16 +64,16 @@ export default class Action extends Base {
66
64
  }
67
65
 
68
66
  async onsubmit(event) {
69
- const target = event.target;
67
+ const {target} = event;
70
68
  const stop = this.fire("submit", event);
71
69
  if (stop) {
72
70
  return event.preventDefault();
73
71
  }
74
72
  event.preventDefault();
75
73
  const data = await this.write(target.action, this.get_form_data(target));
76
- return data.pathname === document.location.href.replace(origin_base, "")
74
+ return data.type === "update"
77
75
  ? this.view.update(data.payload)
78
- : this.context.run(data);
76
+ : this.session.run(data);
79
77
  }
80
78
 
81
79
  async onclick(event) {
@@ -96,8 +94,7 @@ export default class Action extends Base {
96
94
  if (event.button === 0 && url.href.startsWith(origin_base)) {
97
95
  event.preventDefault();
98
96
  if (href !== "") {
99
- const data = await this.read(href);
100
- await this.context.run(data);
97
+ await this.session.run(await this.read(href));
101
98
  }
102
99
  }
103
100
  }
@@ -111,13 +108,13 @@ export default class Action extends Base {
111
108
  }
112
109
 
113
110
  oninput() {
114
- const stop = this.fire("input", event);
111
+ this.fire("input", event);
115
112
  }
116
113
 
117
114
  fire(type, event) {
118
115
  const listeners = this.listeners[type];
119
116
  if (listeners !== undefined) {
120
- const target = event.target;
117
+ const {target} = event;
121
118
  for (const listener of listeners) {
122
119
  if (event.target.closest(listener.selector)) {
123
120
  listener.listener(event, target);
@@ -134,7 +131,8 @@ export default class Action extends Base {
134
131
  const index = this.listeners[event]
135
132
  .findIndex(listener => listener.selector === selector);
136
133
  const object = {selector, listener};
137
- if (index === -1) {
134
+ const not_found = -1;
135
+ if (index === not_found) {
138
136
  this.listeners[event].push(object);
139
137
  } else {
140
138
  this.listeners[event].splice(index, 1, object);
@@ -150,10 +148,10 @@ export default class Action extends Base {
150
148
  }
151
149
 
152
150
  read(pathname) {
153
- return this.context.read(pathname);
151
+ return this.session.read(pathname);
154
152
  }
155
153
 
156
154
  write(pathname, payload) {
157
- return this.context.write(pathname, payload);
155
+ return this.session.write(pathname, payload);
158
156
  }
159
157
  }
@@ -1,5 +1,4 @@
1
- import Context from "./Context.js";
2
- import {contexts} from "./exports.js";
1
+ import Session from "./Session.js";
3
2
  import {base, host, origin_base} from "./document.js";
4
3
 
5
4
  const events = ["submit", "click", "change", "input"];
@@ -9,43 +8,37 @@ const location = `wss://${host}${base}`;
9
8
  export default class Client {
10
9
  constructor(conf) {
11
10
  this.conf = conf;
11
+ this.session = new Session(this);
12
12
 
13
13
  window.onpopstate = async event => {
14
14
  const {pathname, search} = event.target.location;
15
- return this.execute(await this.read(pathname + search));
15
+ return this.session.run(await this.read(pathname + search));
16
16
  };
17
17
 
18
18
  for (const key of events) {
19
- window.addEventListener(key, event => this.context.on(key, event));
19
+ window.addEventListener(key, event => this.session.on(key, event));
20
20
  }
21
21
  }
22
22
 
23
23
  open() {
24
24
  return this.connection?.readyState === 1 ? this
25
25
  : new Promise(resolve => {
26
- const connection = new WebSocket(location);
27
- connection.addEventListener("message", ({data}) => data === "open"
26
+ this.connection = new WebSocket(location);
27
+ this.connection.addEventListener("message", ({data}) => data === "open"
28
28
  ? resolve(this)
29
29
  : this.receive(JSON.parse(data)));
30
- this.connection = connection;
31
30
  });
32
31
  }
33
32
 
34
33
  async receive(data) {
35
- if (back !== undefined) {
34
+ if (back === undefined) {
35
+ await this.session.execute(data);
36
+ } else {
36
37
  back(data);
37
38
  back = undefined;
38
- } else {
39
- await this.execute(data);
40
39
  }
41
40
  }
42
41
 
43
- async execute(data) {
44
- const {context} = data;
45
- this.context = new (contexts[context] ?? Context)(context, this);
46
- await this.context.run(data);
47
- }
48
-
49
42
  read(url) {
50
43
  return this.send("read", url);
51
44
  }
@@ -57,9 +50,12 @@ export default class Client {
57
50
  async send(type, url, payload = {}) {
58
51
  await this.open();
59
52
  const {pathname, search} = new URL(url, origin_base);
53
+ const _url = pathname + search;
60
54
  return new Promise(resolve => {
61
55
  back = data => resolve(data);
62
- this.connection.send(JSON.stringify({type, pathname, search, payload}));
56
+ this.connection.send(JSON.stringify({
57
+ type, pathname, search, payload, "url": _url,
58
+ }));
63
59
  });
64
60
  }
65
61
  }
@@ -1,24 +1,26 @@
1
- import Base from "./Base.js";
2
1
  import Action from "./Action.js";
3
2
  import {routes} from "./exports.js";
4
3
 
5
- export default class Context extends Base {
6
- constructor(name, client) {
7
- super();
4
+ export default class Context {
5
+ constructor(name, session) {
8
6
  this.name = name;
9
7
  this.actions = {};
10
8
  this.used_actions = [];
11
- this.client = client;
9
+ this.session = session;
10
+ }
11
+
12
+ get Class() {
13
+ return this.constructor;
12
14
  }
13
15
 
14
16
  async run(data) {
15
17
  const {namespace, action} = data;
16
18
  const route = `${namespace}/${action}`;
17
- const module = routes?.[namespace]?.[action] ?? Action;
18
- this.actions[route] = new module(action, this);
19
+ const Module = routes?.[namespace]?.[action] ?? Action;
20
+ this.actions[route] = new Module(action, this.session);
19
21
  const nextaction = this.actions[route];
20
22
  nextaction.enter(data);
21
- await this.Class().before(nextaction);
23
+ await this.Class.before(nextaction);
22
24
  nextaction.before();
23
25
  this.action = nextaction;
24
26
  }
@@ -31,14 +33,6 @@ export default class Context extends Base {
31
33
  return data;
32
34
  }
33
35
 
34
- read(pathname) {
35
- return this.client.read(pathname);
36
- }
37
-
38
- write(pathname, payload) {
39
- return this.client.write(pathname, payload);
40
- }
41
-
42
36
  on(key, event) {
43
37
  this.action[`on${key}`](event);
44
38
  }
@@ -1,5 +1,5 @@
1
- const file_listener = event => reader.readAsDataURL(event.target.files[0]);
2
1
  const reader = new FileReader();
2
+ const file_listener = event => reader.readAsDataURL(event.target.files[0]);
3
3
 
4
4
  const replace = (attribute, source) => {
5
5
  if (attribute.includes(".")) {
@@ -20,11 +20,11 @@ const fulfill = (attribute, source) => {
20
20
  if (source === undefined) {
21
21
  return undefined;
22
22
  }
23
- let value = attribute.value;
23
+ let {value} = attribute;
24
24
  const matches = [...value.matchAll(data_regex)];
25
25
  if (matches.length > 0) {
26
26
  for (const match of matches) {
27
- const key = match[0];
27
+ const [key] = match;
28
28
  const new_value = replace(match[1], source);
29
29
  value = value.replace(key, new_value);
30
30
  }
@@ -34,7 +34,6 @@ const fulfill = (attribute, source) => {
34
34
  return value;
35
35
  };
36
36
 
37
-
38
37
  const $selector = Symbol("$selector");
39
38
  const $element = Symbol("$element");
40
39
  const $action = Symbol("$action");
@@ -47,13 +46,13 @@ const proxy_handler = {
47
46
  if (property === "on" || property === "insert") {
48
47
  return target[property].bind(target);
49
48
  }
50
- if (target.element !== null) {
51
- return (...args) => target[property] !== undefined
52
- ? target[property](...args)
53
- : receiver;
54
- } else {
49
+ if (target.element === null) {
55
50
  return () => receiver;
56
51
  }
52
+
53
+ return (...args) => target[property] === undefined
54
+ ? receiver
55
+ : target[property](...args);
57
56
  },
58
57
  };
59
58
 
@@ -77,8 +76,7 @@ export default class Element {
77
76
  }
78
77
 
79
78
  static value(element, name) {
80
- const attribute = element.attributes.getNamedItem(name);
81
- return attribute === null ? null : attribute.value;
79
+ return element?.attributes.getNamedItem(name)?.value;
82
80
  }
83
81
 
84
82
  static evaluate(attribute, source, data) {
@@ -146,7 +144,7 @@ export default class Element {
146
144
  class(classes, set) {
147
145
  classes.split(" ").forEach(name => {
148
146
  this.element.classList.toggle(name, set !== false);
149
- })
147
+ });
150
148
  return this;
151
149
  }
152
150
 
@@ -183,7 +181,8 @@ export default class Element {
183
181
 
184
182
  options(value) {
185
183
  value instanceof Array && value.forEach(option => {
186
- if (this.element.children[option._id] === undefined) {
184
+ const children = [...this.element.children];
185
+ if (!children.find(child => child.value === option._id)) {
187
186
  const newElement = document.createElement("option");
188
187
  newElement.setAttribute("value", option._id);
189
188
  newElement.innerHTML = option.name;
@@ -203,11 +202,13 @@ export default class Element {
203
202
 
204
203
  select_value(value) {
205
204
  this.element.value = value === undefined ? "" : value;
205
+ return this;
206
206
  }
207
207
 
208
208
  input_value(value) {
209
209
  const type = Element.value(this.element, "type");
210
210
  this[`input_${type}_value`]?.(value) ?? this.input_input_value(value);
211
+ return this;
211
212
  }
212
213
 
213
214
  textarea_value(value) {
@@ -218,21 +219,24 @@ export default class Element {
218
219
  if (value === true) {
219
220
  this.element.checked = true;
220
221
  }
222
+ return this;
221
223
  }
222
224
 
223
225
  input_file_value(value) {
224
- const element = this.element;
226
+ const {element} = this;
225
227
  if (value !== undefined) {
226
228
  element.setAttribute("base64", value);
227
229
  }
228
230
  reader.onload = ({target}) => element.setAttribute("base64", target.result);
229
231
  element.addEventListener("change", file_listener);
232
+ return this;
230
233
  }
231
234
 
232
235
  input_input_value(value) {
233
236
  if (value !== undefined) {
234
237
  this.element.value = value;
235
238
  }
239
+ return this;
236
240
  }
237
241
 
238
242
  default_value(value) {
@@ -0,0 +1,27 @@
1
+ import Context from "./Context.js";
2
+ import {contexts} from "./exports.js";
3
+
4
+ export default class Session {
5
+ constructor(client) {
6
+ this.client = client;
7
+ }
8
+
9
+ async run(data) {
10
+ const {context} = data;
11
+ this.context = new (contexts[context] ?? Context)(context, this);
12
+
13
+ await this.context.run(data);
14
+ }
15
+
16
+ on(key, event) {
17
+ this.context.on(key, event);
18
+ }
19
+
20
+ read(pathname) {
21
+ return this.client.read(pathname);
22
+ }
23
+
24
+ write(pathname, payload) {
25
+ return this.client.write(pathname, payload);
26
+ }
27
+ }
@@ -3,8 +3,6 @@
3
3
  import Element from "../Element.js";
4
4
  import Node from "./Node.js";
5
5
 
6
- const data_regex = /\${([^}]*)}/g;
7
-
8
6
  export default class View {
9
7
  constructor(data, action, root) {
10
8
  this.sources = {};
@@ -87,4 +85,5 @@ export default class View {
87
85
  write(pathname, payload) {
88
86
  return this.action.write(pathname, payload);
89
87
  }
88
+
90
89
  }
@@ -3,4 +3,4 @@ const {host, href, origin} = document.location;
3
3
  const base = baseURI === href ? "/" : baseURI.replace(origin, "");
4
4
  const origin_base = `${origin}${base}`;
5
5
 
6
- export {base, origin_base, host};
6
+ export {base, origin, origin_base, host};
@@ -2,10 +2,7 @@
2
2
  "base": "/",
3
3
  "debug": false,
4
4
  "defaults": {
5
- "action": "index",
6
- "context": "guest",
7
- "namespace": "default",
8
- "store": "default.js"
5
+ "context": "guest"
9
6
  },
10
7
  "files": {
11
8
  "index": "index.html"
@@ -20,10 +17,7 @@
20
17
  },
21
18
  "paths": {
22
19
  "client": "client",
23
- "data": {
24
- "domains": "domains",
25
- "stores": "stores"
26
- },
20
+ "domains": "domains",
27
21
  "public": "public",
28
22
  "server": "server",
29
23
  "static": "static"