primate 0.4.0 → 0.5.2

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 (46) hide show
  1. package/README.md +10 -7
  2. package/debris.json +5 -0
  3. package/jsconfig.json +8 -0
  4. package/package.json +12 -6
  5. package/source/client/Action.js +8 -7
  6. package/source/client/Client.js +3 -3
  7. package/source/client/Context.js +2 -2
  8. package/source/client/Element.js +10 -11
  9. package/source/client/View.js +1 -2
  10. package/source/preset/primate.json +10 -4
  11. package/source/preset/{data/stores → stores}/default.js +0 -0
  12. package/source/server/Action.js +10 -6
  13. package/source/server/App.js +3 -2
  14. package/source/server/Bundler.js +2 -2
  15. package/source/server/Context.js +1 -2
  16. package/source/server/EagerPromise.js +1 -1
  17. package/source/server/File.js +4 -0
  18. package/source/server/Projector.js +12 -12
  19. package/source/server/Router.js +12 -19
  20. package/source/server/Session.js +1 -1
  21. package/source/server/attributes.js +1 -0
  22. package/source/server/domain/Domain.js +1 -1
  23. package/source/server/domain/Field.js +21 -26
  24. package/source/server/domain/Predicate.js +24 -0
  25. package/source/server/domain/domains.js +1 -1
  26. package/source/server/errors/InternalServer.js +1 -1
  27. package/source/server/errors/Predicate.js +1 -1
  28. package/source/server/exports.js +1 -1
  29. package/source/server/invariants.js +23 -8
  30. package/source/server/sanitize.js +8 -3
  31. package/source/server/servers/Dynamic.js +4 -4
  32. package/source/server/servers/Server.js +1 -1
  33. package/source/server/servers/Static.js +7 -8
  34. package/source/server/store/Store.js +1 -1
  35. package/source/server/types/Array.js +2 -2
  36. package/source/server/types/Boolean.js +2 -2
  37. package/source/server/types/Date.js +2 -2
  38. package/source/server/types/Domain.js +1 -1
  39. package/source/server/types/Instance.js +1 -1
  40. package/source/server/types/Number.js +2 -2
  41. package/source/server/types/Object.js +2 -2
  42. package/source/server/types/Primitive.js +1 -1
  43. package/source/server/types/Storeable.js +9 -15
  44. package/source/server/types/String.js +2 -2
  45. package/source/server/view/View.js +1 -1
  46. package/source/server/servers/content-security-policy.json +0 -7
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,6 +1,6 @@
1
1
  {
2
2
  "name": "primate",
3
- "version": "0.4.0",
3
+ "version": "0.5.2",
4
4
  "author": "Primate core team <core@primatejs.com>",
5
5
  "homepage": "https://primatejs.com",
6
6
  "bugs": "https://adaptivecloud.dev/primate/primate/issues",
@@ -8,18 +8,24 @@
8
8
  "description": "Server-client framework",
9
9
  "license": "BSD-3-Clause",
10
10
  "dependencies": {
11
- "ws": "^8.4.2"
11
+ "ws": "^8.5.0"
12
12
  },
13
13
  "scripts": {
14
14
  "copy": "rm -rf output && mkdir output && cp source/* output -a",
15
15
  "instrument": "nyc instrument source ./output",
16
- "stick": "node --experimental-json-modules node_modules/stick stick.json",
17
- "coverage": "yarn instrument && nyc yarn run stick",
18
- "test": "yarn copy && yarn run stick"
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"
19
25
  },
20
26
  "type": "module",
21
27
  "exports": "./source/server/exports.js",
22
28
  "engines": {
23
- "node": ">=17.3.0"
29
+ "node": ">=17.7.2"
24
30
  }
25
31
  }
@@ -11,7 +11,7 @@ export default class Action {
11
11
 
12
12
  before() {}
13
13
 
14
- async enter(data) {
14
+ enter(data) {
15
15
  if (data.location) {
16
16
  location.href = data.location;
17
17
  } else {
@@ -28,13 +28,13 @@ export default class Action {
28
28
  this.view = new View(data, this, root);
29
29
  }
30
30
 
31
- create_and_return_view(data, root, save_history) {
31
+ create_and_return_view(data, root) {
32
32
  return new View(data, this, root);
33
33
  }
34
34
 
35
35
  get_form_data(target) {
36
36
  const payload = {};
37
- const elements = target.elements;
37
+ const {elements} = target;
38
38
  for (let i = 0; i < elements.length; i++) {
39
39
  const element = elements[i];
40
40
  const type = Element.value(element, "type");
@@ -64,7 +64,7 @@ export default class Action {
64
64
  }
65
65
 
66
66
  async onsubmit(event) {
67
- const target = event.target;
67
+ const {target} = event;
68
68
  const stop = this.fire("submit", event);
69
69
  if (stop) {
70
70
  return event.preventDefault();
@@ -108,13 +108,13 @@ export default class Action {
108
108
  }
109
109
 
110
110
  oninput() {
111
- const stop = this.fire("input", event);
111
+ this.fire("input", event);
112
112
  }
113
113
 
114
114
  fire(type, event) {
115
115
  const listeners = this.listeners[type];
116
116
  if (listeners !== undefined) {
117
- const target = event.target;
117
+ const {target} = event;
118
118
  for (const listener of listeners) {
119
119
  if (event.target.closest(listener.selector)) {
120
120
  listener.listener(event, target);
@@ -131,7 +131,8 @@ export default class Action {
131
131
  const index = this.listeners[event]
132
132
  .findIndex(listener => listener.selector === selector);
133
133
  const object = {selector, listener};
134
- if (index === -1) {
134
+ const not_found = -1;
135
+ if (index === not_found) {
135
136
  this.listeners[event].push(object);
136
137
  } else {
137
138
  this.listeners[event].splice(index, 1, object);
@@ -31,11 +31,11 @@ export default class Client {
31
31
  }
32
32
 
33
33
  async receive(data) {
34
- if (back !== undefined) {
34
+ if (back === undefined) {
35
+ await this.session.execute(data);
36
+ } else {
35
37
  back(data);
36
38
  back = undefined;
37
- } else {
38
- await this.session.execute(data);
39
39
  }
40
40
  }
41
41
 
@@ -16,8 +16,8 @@ export default class Context {
16
16
  async run(data) {
17
17
  const {namespace, action} = data;
18
18
  const route = `${namespace}/${action}`;
19
- const module = routes?.[namespace]?.[action] ?? Action;
20
- this.actions[route] = new module(action, this.session);
19
+ const Module = routes?.[namespace]?.[action] ?? Action;
20
+ this.actions[route] = new Module(action, this.session);
21
21
  const nextaction = this.actions[route];
22
22
  nextaction.enter(data);
23
23
  await this.Class.before(nextaction);
@@ -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
 
@@ -145,7 +144,7 @@ export default class Element {
145
144
  class(classes, set) {
146
145
  classes.split(" ").forEach(name => {
147
146
  this.element.classList.toggle(name, set !== false);
148
- })
147
+ });
149
148
  return this;
150
149
  }
151
150
 
@@ -224,7 +223,7 @@ export default class Element {
224
223
  }
225
224
 
226
225
  input_file_value(value) {
227
- const element = this.element;
226
+ const {element} = this;
228
227
  if (value !== undefined) {
229
228
  element.setAttribute("base64", value);
230
229
  }
@@ -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
  }
@@ -13,13 +13,19 @@
13
13
  "ssl": {
14
14
  "key": "ssl/default.key",
15
15
  "cert": "ssl/default.crt"
16
- }
16
+ },
17
+ "csp": {
18
+ "default-src": "'self'",
19
+ "object-src": "'none'",
20
+ "frame-ancestors": "'none'",
21
+ "form-action": "'self'",
22
+ "base-uri": "'self'"
23
+ },
24
+ "same-site": "Strict"
17
25
  },
18
26
  "paths": {
19
27
  "client": "client",
20
- "data": {
21
- "domains": "domains"
22
- },
28
+ "domains": "domains",
23
29
  "public": "public",
24
30
  "server": "server",
25
31
  "static": "static"
File without changes
@@ -2,8 +2,8 @@ import {resolve} from "path";
2
2
  import {default as EagerPromise, eager} from "./EagerPromise.js";
3
3
  import View from "./view/View.js";
4
4
  import Projector from "./Projector.js";
5
- import InternalServerError from "./errors/InternalServer.js";
6
- import {assert} from "./invariants.js";
5
+ import {InternalServerError} from "./errors.js";
6
+ import {assert, defined} from "./invariants.js";
7
7
  import sanitize from "./sanitize.js";
8
8
 
9
9
  export default class Action {
@@ -20,11 +20,13 @@ export default class Action {
20
20
  static async new(request, session, context) {
21
21
  const {namespace = this.namespace, action = this.action} = request.path;
22
22
  assert(!namespace.includes("."), () => {
23
- throw new InternalServerError("namespace may not include a dot"); });
23
+ throw new InternalServerError("namespace may not include a dot");
24
+ });
24
25
  const route = `${namespace}/${action}`;
25
26
  const path = resolve(`${context.directory}/${route}.js`);
26
27
  try {
27
- return new (await import(path)).default(request, session, context);
28
+ const {"default": UserAction} = await import(path);
29
+ return new UserAction(request, session, context);
28
30
  } catch (error) {
29
31
  throw new Error(`route \`${route}\` missing`);
30
32
  }
@@ -42,7 +44,7 @@ export default class Action {
42
44
  const {path} = this.request;
43
45
  const namespace = path.namespace ?? this.Class.namespace;
44
46
  const action = path.action ?? this.Class.action;
45
- const _id = path._id;
47
+ const {_id} = path;
46
48
  return {action, namespace, _id};
47
49
  }
48
50
 
@@ -51,6 +53,7 @@ export default class Action {
51
53
  }
52
54
 
53
55
  async run(type) {
56
+ defined(this[type]);
54
57
  this.before && await this.before();
55
58
  await this[type](sanitize(this.request.payload));
56
59
  }
@@ -82,7 +85,8 @@ export default class Action {
82
85
  const path = await eager`${this.context.directory}/${route}`;
83
86
  const _view = await View.new(path, await this.context.layouts);
84
87
  await this.context.Class.prepare(this, data);
85
- const payload = await new Projector(data, await _view.elements(this.layout)).project(route);
88
+ const payload = await new Projector(data,
89
+ await _view.elements(this.layout)).project(route);
86
90
  return {payload, _view};
87
91
  }
88
92
 
@@ -6,7 +6,7 @@ import DynamicServer from "./servers/Dynamic.js";
6
6
  import StaticServer from "./servers/Static.js";
7
7
  import cache from "./cache.js";
8
8
  import log from "./log.js";
9
- import package_json from "../../package.json" assert {"type": "json" };
9
+ import package_json from "../../package.json" assert {"type": "json"};
10
10
 
11
11
  export default class App {
12
12
  constructor(conf) {
@@ -31,11 +31,12 @@ export default class App {
31
31
 
32
32
  this.router = new Router(await this.routes, this.conf);
33
33
  const {index, hashes} = await new this.Bundler(this.conf).bundle();
34
- const router = this.router;
34
+ const {router} = this;
35
35
 
36
36
  const conf = {index, hashes, router,
37
37
  "serve_from": this.conf.paths.public,
38
38
  "http": {
39
+ ...this.conf.http,
39
40
  "key": File.read_sync(resolve(this.conf.http.ssl.key)),
40
41
  "cert": File.read_sync(resolve(this.conf.http.ssl.cert)),
41
42
  },
@@ -94,7 +94,7 @@ export default class Bundler {
94
94
  const index_html = await File.read(`${paths.public}/${this.index}`);
95
95
  await File.remove(`${paths.public}/${this.index}`);
96
96
 
97
- const body = "return `"+index_html+"`";
97
+ const body = `return \`${index_html}\``;
98
98
  const index = new Function("conf", "client", body)(this.conf, this.client);
99
99
 
100
100
  return {index, "hashes": this.hashes};
@@ -138,7 +138,7 @@ export default class Bundler {
138
138
  client += `<script type="module" integrity="${integrity}" src="${src}">`
139
139
  + "</script>\n";
140
140
  }
141
- return client + "<first-view />";
141
+ return `${client}<first-view />`;
142
142
  }
143
143
 
144
144
  async write_exports() {
@@ -2,7 +2,7 @@ import {resolve} from "path";
2
2
  import File from "./File.js";
3
3
  import Action from "./Action.js";
4
4
  import {InternalServerError} from "./errors.js";
5
- import {assert, defined} from "./invariants.js";
5
+ import {assert} from "./invariants.js";
6
6
  import cache from "./cache.js";
7
7
  import log from "./log.js";
8
8
 
@@ -87,7 +87,6 @@ export default class Context {
87
87
  const {type = "read"} = request;
88
88
  try {
89
89
  const action = await Action.new(request, session, this);
90
- defined(action[type]);
91
90
  await this.Class.before(action);
92
91
  await action.run(type);
93
92
  return action.response();
@@ -3,7 +3,7 @@ import {inconstructible_function} from "./attributes.js";
3
3
  const $promise = Symbol("#promise");
4
4
 
5
5
  const handler = {
6
- "get": function(target, property) {
6
+ "get": (target, property) => {
7
7
  const promise = target[$promise];
8
8
 
9
9
  if (["then", "catch"].includes(property)) {
@@ -31,6 +31,10 @@ export default class File {
31
31
  return this.exists && !this.stats.isDirectory();
32
32
  }
33
33
 
34
+ get stream() {
35
+ return this.read_stream;
36
+ }
37
+
34
38
  get read_stream() {
35
39
  return fs.createReadStream(this.path, {"flags": "r"});
36
40
  }
@@ -15,7 +15,7 @@ export default class Projector {
15
15
  }
16
16
  this.documents = documents;
17
17
  this.projection = projection.reduce((sofar, domain) => {
18
- const key = Object.keys(domain)[0];
18
+ const [key] = Object.keys(domain);
19
19
  sofar[key] = domain[key];
20
20
  return sofar;
21
21
  }, {});
@@ -39,9 +39,9 @@ export default class Projector {
39
39
  }
40
40
 
41
41
  object(document, projection, key, fields) {
42
- return fields?.[key] !== undefined
43
- ? this.by_cache(fields[key], key, document, projection)
44
- : this.by_store(key, document, projection);
42
+ return fields?.[key] === undefined
43
+ ? this.by_store(key, document, projection)
44
+ : this.by_cache(fields[key], key, document, projection);
45
45
  }
46
46
 
47
47
  async one(document, projection) {
@@ -57,7 +57,7 @@ export default class Projector {
57
57
  if (typeof property === "string") {
58
58
  resolved[property] = await scalar(document, property);
59
59
  } else if (typeof property === "object") {
60
- const key = Object.keys(property)[0];
60
+ const [key] = Object.keys(property);
61
61
  resolved[key] = await this.object(document, property, key, fields);
62
62
  }
63
63
  }
@@ -68,17 +68,17 @@ export default class Projector {
68
68
  return this[Array.isArray(document) ? "many" : "one"](document, projection);
69
69
  }
70
70
 
71
- async project(route) {
71
+ async project() {
72
72
  return (await Promise.all(Object.keys(this.documents).map(async key => {
73
- const resolved = this.projection[key] !== undefined
74
- ? await this.resolve(this.documents[key], this.projection[key])
75
- : undefined;
73
+ const resolved = this.projection[key] === undefined
74
+ ? undefined
75
+ : await this.resolve(await this.documents[key], this.projection[key]);
76
76
  return {key, resolved};
77
77
  }))).reduce((projected, {key, resolved}) => {
78
- if (resolved !== undefined) {
79
- projected[key] = resolved;
80
- } else {
78
+ if (resolved === undefined) {
81
79
  log.yellow(` \`${key}\` not projected`).nl();
80
+ } else {
81
+ projected[key] = resolved;
82
82
  }
83
83
  return projected;
84
84
  }, {});
@@ -4,7 +4,7 @@ export default class Router {
4
4
  this.routes = {};
5
5
  routes.forEach(({from, to, contexts}) => contexts.forEach(context => {
6
6
  this.routes[context] = this.routes[context] ?? [];
7
- this.routes[context].push({"from": new RegExp("^"+from+"$", "u"), to});
7
+ this.routes[context].push({"from": new RegExp(`^${from}$`, "u"), to});
8
8
  }));
9
9
  }
10
10
 
@@ -24,36 +24,29 @@ export default class Router {
24
24
  const [path, search = ""] = pathname.split("?");
25
25
 
26
26
  const route = this.route_by_context(context, path);
27
- if (route !== undefined) {
28
- const replace = path.replace(route.from, route.to);
29
- return `${replace}${replace.includes("?") ? "" : "?"}&${search}`;
30
- } else {
27
+ if (route === undefined) {
31
28
  return pathname;
32
29
  }
30
+
31
+ const replace = path.replace(route.from, route.to);
32
+ return `${replace}${replace.includes("?") ? "" : "?"}&${search}`;
33
33
  }
34
34
 
35
35
  resolve(pathname, context) {
36
- return this.context(context) !== undefined
37
- ? this.resolve_by_context(pathname, context)
38
- : pathname;
36
+ return this.context(context) === undefined
37
+ ? pathname
38
+ : this.resolve_by_context(pathname, context);
39
39
  }
40
40
 
41
41
  async route(pathname, context) {
42
42
  const resolved = await this.resolve(this.debase(pathname), context);
43
43
  const url = new URL(`https://primatejs.com/${resolved}`);
44
44
  const parts = url.pathname.split("/").filter(part => part !== "");
45
+ const [namespace, action, _id] = parts;
45
46
  return {
46
- "pathname": url.pathname,
47
- "resolved": resolved,
48
- "parts": parts,
49
- "path": {
50
- "namespace": parts[0],
51
- "action": parts[1],
52
- "_id": parts[2],
53
- },
54
- "params": {
55
- ...Object.fromEntries(url.searchParams),
56
- },
47
+ "pathname": url.pathname, resolved, parts,
48
+ "path": {namespace, action, _id},
49
+ "params": {...Object.fromEntries(url.searchParams)},
57
50
  };
58
51
  }
59
52
  }
@@ -31,7 +31,7 @@ export default class Session extends Domain {
31
31
  }
32
32
 
33
33
  get cookie() {
34
- return `session_id=${this._id}; Secure; SameSite=Strict; HttpOnly; Path=/`;
34
+ return `session_id=${this._id}; Path=/; Secure; HttpOnly`;
35
35
  }
36
36
 
37
37
  async switch_context(context, data = {}) {
@@ -11,3 +11,4 @@ export const inconstructible_function = value =>
11
11
  typeof value === "function" && !constructible(value);
12
12
  export const numeric = value => !isNaN(parseFloat(value)) && isFinite(value);
13
13
  export const boolish = value => value === "true" || value === "false";
14
+ export const nullish = value => value === undefined || value === null;
@@ -10,7 +10,7 @@ import {random} from "../crypto.js";
10
10
  const length = 12;
11
11
 
12
12
  export default class Domain {
13
- static stores_directory = "data/stores";
13
+ static stores_directory = "stores";
14
14
  static store_file = "default.js";
15
15
 
16
16
  static {
@@ -1,28 +1,30 @@
1
- import * as types from "../types.js";
2
1
  import DomainType from "../types/Domain.js";
3
- import Storeable from "../types/Storeable.js";
2
+ import Predicate from "./Predicate.js";
4
3
  import {PredicateError} from "../errors.js";
5
- import {defined, is_array, instances, is_constructible} from "../invariants.js";
4
+ import Storeable from "../types/Storeable.js";
5
+ import * as types from "../types.js";
6
+ import cache from "../cache.js";
6
7
  import {constructible} from "../attributes.js";
8
+ import {defined, is, maybe} from "../invariants.js";
7
9
 
8
10
  const builtins = Object.values(types).reduce((aggregate, Type) => {
9
11
  aggregate[Type.instance] = Type;
10
12
  return aggregate;
11
13
  }, {});
12
14
 
13
- const parse = field => constructible(field)
14
- ? {"type": field}
15
- : as_non_constructible(field);
15
+ const as_array = field => ({"type": field[0], "predicates": field.slice(1)});
16
16
 
17
- const as_non_constructible =
18
- field => typeof field === "function" ? as_function(field) : as_object(field);
17
+ const as_object = field => field instanceof Array ? as_array(field) : field;
19
18
 
20
19
  const as_function = field => ({"in": field,
21
20
  "type": field(undefined, {}).constructor});
22
21
 
23
- const as_object = field => field instanceof Array ? as_array(field) : field;
22
+ const as_non_constructible =
23
+ field => typeof field === "function" ? as_function(field) : as_object(field);
24
24
 
25
- const as_array = field => ({"type": field[0], "predicates": field.slice(1)});
25
+ const parse = field => constructible(field)
26
+ ? {"type": field}
27
+ : as_non_constructible(field);
26
28
 
27
29
  export default class Field {
28
30
  constructor(property, definition, options) {
@@ -30,9 +32,9 @@ export default class Field {
30
32
  this.property = property;
31
33
  this.definition = parse(definition);
32
34
  this.options = options ?? {"transient": false, "optional": false};
33
- is_constructible(this.Type);
34
- instances(this.type.prototype, Storeable, "type must extend Storeable");
35
- is_array(this.predicates);
35
+ is.constructible(this.Type);
36
+ is.subclass(this.type, Storeable);
37
+ maybe.array(this.definition.predicates);
36
38
  }
37
39
 
38
40
  static resolve(name) {
@@ -57,12 +59,15 @@ export default class Field {
57
59
  return this.Type.prototype instanceof DomainType.instance;
58
60
  }
59
61
 
60
- get Type(){
62
+ get Type() {
61
63
  return this.definition.type;
62
64
  }
63
65
 
64
66
  get predicates() {
65
- return this.definition.predicates ?? [];
67
+ return cache(this, "predicates", () => {
68
+ const predicates = this.definition.predicates ?? [];
69
+ return predicates.map(name => new Predicate(name));
70
+ });
66
71
  }
67
72
 
68
73
  by_id(id) {
@@ -75,23 +80,13 @@ export default class Field {
75
80
  return in_function !== undefined ? in_function(value, document) : value;
76
81
  }
77
82
 
78
- override_predicates(document) {
79
- return this.predicates.map(predicate => {
80
- const [name, ...params] = predicate.split(":");
81
- return document[name] !== undefined
82
- ? {"function": document[name].bind(document), params}
83
- : predicate;
84
- });
85
- }
86
-
87
83
  verify_undefined() {
88
84
  return this.options.optional ? true : "Must not be empty";
89
85
  }
90
86
 
91
87
  async verify_defined(property, document) {
92
88
  try {
93
- const predicates = this.override_predicates(document);
94
- await this.type.verify(property, document, predicates, this.Type);
89
+ await this.type.verify(property, document, this.predicates, this.Type);
95
90
  return true;
96
91
  } catch (error) {
97
92
  if (error instanceof PredicateError) {
@@ -0,0 +1,24 @@
1
+ import Domain from "./Domain.js";
2
+ import Storeable from "../types/Storeable.js";
3
+ import {is} from "../invariants.js";
4
+
5
+ export default class Predicate {
6
+ constructor(definition) {
7
+ is.string(definition);
8
+ const [name, ...params] = definition.split(":");
9
+ this.name = name;
10
+ this.params = params;
11
+ }
12
+
13
+ async check(property, document, Type) {
14
+ is.string(property);
15
+ is.instance(document, Domain);
16
+ const {name, params} = this;
17
+ if (document[name] === undefined) {
18
+ is.subclass(Type, Storeable);
19
+ await Type.has(name, document[property], params);
20
+ } else {
21
+ await document[name](property, ...params);
22
+ }
23
+ }
24
+ }
@@ -4,7 +4,7 @@ import Field from "./Field.js";
4
4
  import Domain from "./Domain.js";
5
5
 
6
6
  const domains = {};
7
- const base = conf().paths.data.domains;
7
+ const base = conf().paths.domains;
8
8
 
9
9
  for (const domain of await new File(base).list(".js")) {
10
10
  const name = domain.slice(0, -3);
@@ -1 +1 @@
1
- export default class extends Error {}
1
+ export default class InternalServerError extends Error {}
@@ -1 +1 @@
1
- export default class extends Error {}
1
+ export default class PredicateError extends Error {}
@@ -14,11 +14,11 @@ export {default as domains} from "./domain/domains.js";
14
14
  export {default as Storeable} from "./types/Storeable.js";
15
15
 
16
16
  export * from "./errors.js";
17
+ export * from "./invariants.js";
17
18
 
18
19
  export {default as MemoryStore} from "./store/Memory.js";
19
20
  export {default as Store} from "./store/Store.js";
20
21
 
21
- export {assert, defined} from "./invariants.js";
22
22
  export {default as log} from "./log.js";
23
23
  export {default as extend_object} from "./extend_object.js";
24
24
  export {default as sanitize} from "./sanitize.js";
@@ -1,17 +1,32 @@
1
- import {constructible} from "./attributes.js";
1
+ import {constructible, nullish} from "./attributes.js";
2
2
 
3
3
  const errored = error => {
4
- if (typeof error === "function") { // fallback
4
+ if (typeof error === "function") {
5
+ // fallback
5
6
  error();
6
- } else { // error
7
+ } else {
8
+ // error
7
9
  throw new Error(error);
8
10
  }
9
11
  };
10
12
 
11
13
  const assert = (predicate, error) => Boolean(predicate) || errored(error);
12
- const defined = (value, error) => assert(value !== undefined, error);
13
- const is_array = value => assert(Array.isArray(value), "must be array");
14
- const instances = (sub, parent, error) => assert(sub instanceof parent, error);
15
- const is_constructible = (value, error) => assert(constructible(value), error);
14
+ const is = {
15
+ "array": value => assert(Array.isArray(value), "must be array"),
16
+ "string": value => assert(typeof value === "string", "must be string"),
17
+ "defined": (value, error) => assert(value !== undefined, error),
18
+ "undefined": value => assert(value === undefined, "must be undefined"),
19
+ "constructible": (value, error) => assert(constructible(value), error),
20
+ "instance": (object, Class) => assert(object instanceof Class,
21
+ `must instance ${Class.name}`),
22
+ "subclass": (object, Class) => assert(object?.prototype instanceof Class,
23
+ `must subclass ${Class.name}`),
24
+ };
25
+ const {defined} = is;
26
+
27
+ const maybe = Object.keys(is).reduce((aggregator, property) => {
28
+ aggregator[property] = value => nullish(value) || is[property](value);
29
+ return aggregator;
30
+ }, {});
16
31
 
17
- export {assert, defined, is_array, instances, is_constructible};
32
+ export {assert, defined, is, maybe};
@@ -1,5 +1,10 @@
1
1
  export default (payload = {}) => Object.keys(payload)
2
2
  .map(key => ({key, "value": payload[key].toString().trim()}))
3
- .map(datum => { datum.value = datum.value !== "" ? datum.value : undefined;
4
- return datum; })
5
- .reduce((data, {key, value}) => { data[key] = value; return data; }, {});
3
+ .map(datum => {
4
+ datum.value = datum.value === "" ? undefined : datum.value;
5
+ return datum;
6
+ })
7
+ .reduce((data, {key, value}) => {
8
+ data[key] = value;
9
+ return data;
10
+ }, {});
@@ -23,24 +23,24 @@ export default class DynamicServer extends Server {
23
23
  const {session} = socket;
24
24
  const request = JSON.parse(event);
25
25
  try {
26
- await this.onmessage(request, socket);
26
+ await this.serve(request, socket);
27
27
  await session.log("green", `${request.type} ${request.url}`);
28
28
  } catch(error) {
29
29
  await session.log("red", error.message);
30
30
  }
31
31
  }
32
32
 
33
- async onmessage(request, socket) {
33
+ async serve(request, socket) {
34
34
  const {router} = this.conf;
35
35
  const {session} = socket;
36
36
  const {path, params} = await session.route(router, request.url);
37
37
  const response = await session.run({...request, path, params});
38
38
  const {type} = response;
39
39
  return this[type]?.(socket, response)
40
- ?? DynamicServer.send(socket, response);
40
+ ?? DynamicServer.serve(socket, response);
41
41
  }
42
42
 
43
- static send(socket, response) {
43
+ static serve(socket, response) {
44
44
  socket.send(JSON.stringify(response));
45
45
  }
46
46
 
@@ -1,4 +1,4 @@
1
- export default class StaticServer {
1
+ export default class Server {
2
2
  constructor(conf) {
3
3
  this.conf = conf;
4
4
  }
@@ -7,18 +7,13 @@ import Server from "./Server.js";
7
7
  import Session from "../Session.js";
8
8
  import File from "../File.js";
9
9
  import {algorithm, hash} from "../crypto.js";
10
- import {assert} from "../invariants.js";
11
10
  import log from "../log.js";
12
11
  import codes from "./http-codes.json" assert {"type": "json"};
13
12
  import mimes from "./mimes.json" assert {"type": "json"};
14
- import policy from "./content-security-policy.json" assert {"type": "json"};
15
13
 
16
14
  const regex = /\.([a-z1-9]*)$/u;
17
15
  const mime = filename => mimes[filename.match(regex)[1]] ?? mimes.binary;
18
16
 
19
- const csp = Object.keys(policy)
20
- .reduce((policy_string, key) => policy_string + `${key} ${policy[key]};`, "");
21
-
22
17
  const stream = (from, response) => {
23
18
  response.setHeader("Content-Encoding", "br");
24
19
  response.writeHead(codes.OK);
@@ -30,11 +25,15 @@ const stream = (from, response) => {
30
25
  export default class StaticServer extends Server {
31
26
  async run() {
32
27
  const {http, context} = this.conf;
28
+ const {csp, "same-site": same_site = "Strict"} = http;
29
+ this.csp = Object.keys(csp).reduce((policy_string, key) =>
30
+ policy_string + `${key} ${csp[key]};`, "");
33
31
 
34
32
  this.server = await createServer(http, async (request, response) => {
35
33
  const session = await Session.get(request.headers.cookie, context);
36
34
  if (!session.has_cookie) {
37
- response.setHeader("Set-Cookie", session.cookie);
35
+ const {cookie} = session;
36
+ response.setHeader("Set-Cookie", `${cookie}; SameSite=${same_site}`);
38
37
  }
39
38
  response.session = session;
40
39
  request.on("end", () =>
@@ -84,13 +83,13 @@ export default class StaticServer extends Server {
84
83
  const integrity = `${algorithm}-${hash(src)}`;
85
84
  const view = `<script type="module" integrity="${integrity}">${src}`
86
85
  + "</script>";
87
- const file = index.replace("<first-view />", view);
86
+ const file = index.replace("<first-view />", () => view);
88
87
  const script_src = Array.from(hashes)
89
88
  .concat([integrity])
90
89
  .reduce((hash_string, next_hash) => hash_string + ` '${next_hash}'`, "");
91
90
 
92
91
  response.setHeader("Content-Security-Policy",
93
- csp + `script-src 'self'${script_src};`);
92
+ this.csp + `script-src 'self'${script_src};`);
94
93
  response.setHeader("Content-Type", "text/html");
95
94
  response.setHeader("Referrer-Policy", "same-origin");
96
95
  return stream(Readable.from([file]), response);
@@ -1,5 +1,5 @@
1
1
  import {resolve} from "path";
2
- const preset = "../../preset/data/stores";
2
+ const preset = "../../preset/stores";
3
3
 
4
4
  export default class Store {
5
5
  constructor(conf = {}) {
@@ -1,7 +1,7 @@
1
- import Instance from "./Instance.js";
1
+ import InstanceType from "./Instance.js";
2
2
  import errors from "./errors/Array.json" assert {"type": "json"};
3
3
 
4
- export default class extends Instance {
4
+ export default class ArrayType extends InstanceType {
5
5
  static get instance() {
6
6
  return Array;
7
7
  }
@@ -1,8 +1,8 @@
1
- import Primitive from "./Primitive.js";
1
+ import PrimitiveType from "./Primitive.js";
2
2
  import {boolish} from "../attributes.js";
3
3
  import errors from "./errors/Boolean.json" assert {"type": "json"};
4
4
 
5
- export default class extends Primitive {
5
+ export default class BooleanType extends PrimitiveType {
6
6
  static get type() {
7
7
  return "boolean";
8
8
  }
@@ -1,7 +1,7 @@
1
- import Instance from "./Instance.js";
1
+ import InstanceType from "./Instance.js";
2
2
  import errors from "./errors/Date.json" assert {"type": "json"};
3
3
 
4
- export default class extends Instance {
4
+ export default class DateType extends InstanceType {
5
5
  static get instance() {
6
6
  return Date;
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import Storeable from "./Storeable.js";
2
2
 
3
- export default class extends Storeable {
3
+ export default class DomainType extends Storeable {
4
4
  static type_error({name}) {
5
5
  return `Must be a ${name}`;
6
6
  }
@@ -1,6 +1,6 @@
1
1
  import Storeable from "./Storeable.js";
2
2
 
3
- export default class extends Storeable {
3
+ export default class InstanceType extends Storeable {
4
4
  static is(value) {
5
5
  // no subclassing allowed, as [] instanceof Object === true et al.
6
6
  return value?.constructor === this.instance;
@@ -1,8 +1,8 @@
1
- import Primitive from "./Primitive.js";
1
+ import PrimitiveType from "./Primitive.js";
2
2
  import {numeric} from "../attributes.js";
3
3
  import errors from "./errors/Number.json" assert {"type": "json"};
4
4
 
5
- export default class extends Primitive {
5
+ export default class NumberType extends PrimitiveType {
6
6
  static get type() {
7
7
  return "number";
8
8
  }
@@ -1,7 +1,7 @@
1
- import Instance from "./Instance.js";
1
+ import InstanceType from "./Instance.js";
2
2
  import errors from "./errors/Object.json" assert {"type": "json"};
3
3
 
4
- export default class extends Instance {
4
+ export default class ObjectType extends InstanceType {
5
5
  static get instance() {
6
6
  return Object;
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import Storeable from "./Storeable.js";
2
2
 
3
- export default class extends Storeable {
3
+ export default class PrimitiveType extends Storeable {
4
4
  static is(value) {
5
5
  return typeof value === this.type;
6
6
  }
@@ -1,12 +1,13 @@
1
1
  import {PredicateError} from "../errors.js";
2
2
 
3
- export default class {
3
+ export default class Storeable {
4
4
  static async verify(property, document, predicates, type) {
5
5
  document[property] = this.coerce(document[property]);
6
6
  if (!await this.is(document[property], type)) {
7
7
  throw new PredicateError(this.type_error(type));
8
8
  }
9
- await this.has(property, document, predicates);
9
+ await Promise.all(predicates.map(predicate =>
10
+ predicate.check(property, document, this)));
10
11
  }
11
12
 
12
13
  static type_error() {
@@ -17,20 +18,13 @@ export default class {
17
18
  throw new Error("must be implemented");
18
19
  }
19
20
 
20
- static async has(property, document, predicates) {
21
- for (const predicate of predicates) {
22
- if (typeof predicate === "object") {
23
- await predicate.function(property, ...predicate.params);
24
- } else {
25
- const [name, ...params] = predicate.split(":");
26
- if (!this[name](document[property], ...params)) {
27
- let error = this.errors[name];
28
- for (let i = 0; i < params.length; i++) {
29
- error = error.replace(`$${i+1}`, params[i]);
30
- }
31
- throw new PredicateError(error);
32
- }
21
+ static async has(name, value, params) {
22
+ if (!this[name](value, ...params)) {
23
+ let error = this.errors[name];
24
+ for (let i = 0; i < params.length; i++) {
25
+ error = error.replace(`$${i+1}`, () => params[i]);
33
26
  }
27
+ throw new PredicateError(error);
34
28
  }
35
29
  }
36
30
 
@@ -1,7 +1,7 @@
1
- import Primitive from "./Primitive.js";
1
+ import PrimitiveType from "./Primitive.js";
2
2
  import errors from "./errors/String.json" assert {"type": "json"};
3
3
 
4
- export default class extends Primitive {
4
+ export default class StringType extends PrimitiveType {
5
5
  static get type() {
6
6
  return "string";
7
7
  }
@@ -30,6 +30,6 @@ export default class View {
30
30
  }
31
31
 
32
32
  file(layout = "default") {
33
- return this.layout(layout).replace($content, this.content);
33
+ return this.layout(layout).replace($content, () => this.content);
34
34
  }
35
35
  }
@@ -1,7 +0,0 @@
1
- {
2
- "default-src": "'self'",
3
- "object-src": "'none'",
4
- "frame-ancestors": "'none'",
5
- "form-action": "'self'",
6
- "base-uri": "'self'"
7
- }