primate 0.2.0 → 0.5.0
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 +11 -5
- package/package.json +9 -3
- package/source/client/Action.js +20 -22
- package/source/client/Client.js +13 -17
- package/source/client/Context.js +10 -16
- package/source/client/Element.js +21 -15
- package/source/client/Session.js +27 -0
- package/source/client/View.js +1 -2
- package/source/client/document.js +1 -1
- package/source/preset/primate.json +2 -6
- package/source/server/Action.js +56 -75
- package/source/server/App.js +27 -8
- package/source/server/Bundler.js +13 -16
- package/source/server/Context.js +63 -56
- package/source/server/{promises/Eager.js → EagerPromise.js} +4 -2
- package/source/server/File.js +6 -2
- package/source/server/Projector.js +86 -0
- package/source/server/Router.js +15 -24
- package/source/server/Session.js +9 -33
- package/source/server/attributes.js +3 -0
- package/source/server/conf.js +20 -26
- package/source/server/{Crypto.js → crypto.js} +0 -0
- package/source/server/domain/Domain.js +60 -48
- package/source/server/domain/Field.js +34 -36
- package/source/server/domain/Predicate.js +24 -0
- package/source/server/domain/domains.js +16 -0
- package/source/server/errors/InternalServer.js +1 -1
- package/source/server/errors/Predicate.js +1 -1
- package/source/server/errors.js +0 -1
- package/source/server/exports.js +10 -9
- package/source/server/{utils/extend_object.js → extend_object.js} +0 -0
- package/source/server/invariants.js +23 -10
- package/source/server/sanitize.js +10 -0
- package/source/server/servers/Dynamic.js +19 -13
- package/source/server/servers/Server.js +1 -1
- package/source/server/servers/Static.js +25 -20
- package/source/server/servers/content-security-policy.json +0 -1
- package/source/server/store/Store.js +13 -0
- package/source/server/types/Array.js +2 -2
- package/source/server/types/Boolean.js +2 -2
- package/source/server/types/Date.js +2 -2
- package/source/server/types/Domain.js +1 -6
- package/source/server/types/Instance.js +1 -1
- package/source/server/types/Number.js +2 -2
- package/source/server/types/Object.js +2 -2
- package/source/server/types/Primitive.js +1 -1
- package/source/server/types/Storeable.js +12 -19
- package/source/server/types/String.js +2 -2
- package/source/server/view/TreeNode.js +2 -0
- package/source/server/view/View.js +6 -1
- package/source/client/Base.js +0 -5
- package/source/server/Base.js +0 -35
- package/source/server/errors/Fallback.js +0 -1
- package/source/server/fallback.js +0 -11
- package/source/server/promises/Meta.js +0 -42
- package/source/server/promises.js +0 -2
package/README.md
CHANGED
|
@@ -18,8 +18,8 @@ yarn add primate
|
|
|
18
18
|
* **Minimal** just one dependency ([ws][])
|
|
19
19
|
* **Simple** only native web technologies (JavaScript, HTML)
|
|
20
20
|
* **Full-stack** server and client, both in JavaScript
|
|
21
|
-
* **Layered** separation of data (domains), logic (actions) and
|
|
22
|
-
|
|
21
|
+
* **Layered** separation of data (domains), logic (actions) and presentation
|
|
22
|
+
(views)
|
|
23
23
|
* **Correct** data verification using field definitions in domains
|
|
24
24
|
* **Secure** serving hash-verified scripts on secure HTTP with a strong CSP
|
|
25
25
|
* **Roles** access control with contexts
|
|
@@ -27,15 +27,18 @@ presentation (views)
|
|
|
27
27
|
* **Sane defaults** minimally opinionated with good, overrideable defaults
|
|
28
28
|
for most features
|
|
29
29
|
* **Data linking** modeling `1:1`, `1:n` and `n:m` relationships on domains via
|
|
30
|
-
ad-hoc getters
|
|
30
|
+
cacheable ad-hoc getters
|
|
31
31
|
|
|
32
32
|
## Getting started
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
See the [getting started][getting-started] guide.
|
|
35
35
|
|
|
36
36
|
## Resources
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
* [Source code][source-code]
|
|
39
|
+
* [Issues][issues]
|
|
40
|
+
|
|
41
|
+
A full guide is coming soon.
|
|
39
42
|
|
|
40
43
|
## Versioning
|
|
41
44
|
|
|
@@ -48,3 +51,6 @@ will move over to semantic versioning.
|
|
|
48
51
|
BSD-3-Clause
|
|
49
52
|
|
|
50
53
|
[ws]: https://github.com/websockets/ws
|
|
54
|
+
[getting-started]: https://primatejs.com/getting-started
|
|
55
|
+
[source-code]: https://adaptivecloud.dev/primate/primate
|
|
56
|
+
[issues]: https://adaptivecloud.dev/primate/primate/issues
|
package/package.json
CHANGED
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primate",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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.
|
|
11
|
+
"ws": "^8.5.0"
|
|
10
12
|
},
|
|
11
13
|
"scripts": {
|
|
12
|
-
"
|
|
14
|
+
"copy": "rm -rf output && mkdir output && cp source/* output -a",
|
|
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"
|
|
13
19
|
},
|
|
14
20
|
"type": "module",
|
|
15
21
|
"exports": "./source/server/exports.js",
|
package/source/client/Action.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import Element from "../Element.js";
|
|
2
2
|
import View from "./View.js";
|
|
3
|
-
import
|
|
4
|
-
import {origin_base} from "./document.js";
|
|
3
|
+
import {origin_base, origin} from "./document.js";
|
|
5
4
|
|
|
6
|
-
export default class Action
|
|
7
|
-
constructor(name,
|
|
8
|
-
super();
|
|
5
|
+
export default class Action {
|
|
6
|
+
constructor(name, session) {
|
|
9
7
|
this.name = name;
|
|
10
|
-
this.
|
|
8
|
+
this.session = session;
|
|
11
9
|
this.listeners = [];
|
|
12
10
|
}
|
|
13
11
|
|
|
14
12
|
before() {}
|
|
15
13
|
|
|
16
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
74
|
+
return data.type === "update"
|
|
77
75
|
? this.view.update(data.payload)
|
|
78
|
-
: this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
151
|
+
return this.session.read(pathname);
|
|
154
152
|
}
|
|
155
153
|
|
|
156
154
|
write(pathname, payload) {
|
|
157
|
-
return this.
|
|
155
|
+
return this.session.write(pathname, payload);
|
|
158
156
|
}
|
|
159
157
|
}
|
package/source/client/Client.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
|
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({
|
|
56
|
+
this.connection.send(JSON.stringify({
|
|
57
|
+
type, pathname, search, payload, "url": _url,
|
|
58
|
+
}));
|
|
63
59
|
});
|
|
64
60
|
}
|
|
65
61
|
}
|
package/source/client/Context.js
CHANGED
|
@@ -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
|
|
6
|
-
constructor(name,
|
|
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.
|
|
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
|
|
18
|
-
this.actions[route] = new
|
|
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
|
|
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
|
}
|
package/source/client/Element.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
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) {
|
|
@@ -143,8 +141,10 @@ export default class Element {
|
|
|
143
141
|
return this;
|
|
144
142
|
}
|
|
145
143
|
|
|
146
|
-
class(
|
|
147
|
-
|
|
144
|
+
class(classes, set) {
|
|
145
|
+
classes.split(" ").forEach(name => {
|
|
146
|
+
this.element.classList.toggle(name, set !== false);
|
|
147
|
+
});
|
|
148
148
|
return this;
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -181,7 +181,8 @@ export default class Element {
|
|
|
181
181
|
|
|
182
182
|
options(value) {
|
|
183
183
|
value instanceof Array && value.forEach(option => {
|
|
184
|
-
|
|
184
|
+
const children = [...this.element.children];
|
|
185
|
+
if (!children.find(child => child.value === option._id)) {
|
|
185
186
|
const newElement = document.createElement("option");
|
|
186
187
|
newElement.setAttribute("value", option._id);
|
|
187
188
|
newElement.innerHTML = option.name;
|
|
@@ -201,11 +202,13 @@ export default class Element {
|
|
|
201
202
|
|
|
202
203
|
select_value(value) {
|
|
203
204
|
this.element.value = value === undefined ? "" : value;
|
|
205
|
+
return this;
|
|
204
206
|
}
|
|
205
207
|
|
|
206
208
|
input_value(value) {
|
|
207
209
|
const type = Element.value(this.element, "type");
|
|
208
210
|
this[`input_${type}_value`]?.(value) ?? this.input_input_value(value);
|
|
211
|
+
return this;
|
|
209
212
|
}
|
|
210
213
|
|
|
211
214
|
textarea_value(value) {
|
|
@@ -216,21 +219,24 @@ export default class Element {
|
|
|
216
219
|
if (value === true) {
|
|
217
220
|
this.element.checked = true;
|
|
218
221
|
}
|
|
222
|
+
return this;
|
|
219
223
|
}
|
|
220
224
|
|
|
221
225
|
input_file_value(value) {
|
|
222
|
-
const element = this
|
|
226
|
+
const {element} = this;
|
|
223
227
|
if (value !== undefined) {
|
|
224
228
|
element.setAttribute("base64", value);
|
|
225
229
|
}
|
|
226
230
|
reader.onload = ({target}) => element.setAttribute("base64", target.result);
|
|
227
231
|
element.addEventListener("change", file_listener);
|
|
232
|
+
return this;
|
|
228
233
|
}
|
|
229
234
|
|
|
230
235
|
input_input_value(value) {
|
|
231
236
|
if (value !== undefined) {
|
|
232
237
|
this.element.value = value;
|
|
233
238
|
}
|
|
239
|
+
return this;
|
|
234
240
|
}
|
|
235
241
|
|
|
236
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
|
+
}
|
package/source/client/View.js
CHANGED
|
@@ -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
|
}
|
|
@@ -2,10 +2,7 @@
|
|
|
2
2
|
"base": "/",
|
|
3
3
|
"debug": false,
|
|
4
4
|
"defaults": {
|
|
5
|
-
"
|
|
6
|
-
"context": "guest",
|
|
7
|
-
"namespace": "default",
|
|
8
|
-
"store": "default.js"
|
|
5
|
+
"context": "guest"
|
|
9
6
|
},
|
|
10
7
|
"files": {
|
|
11
8
|
"index": "index.html"
|
|
@@ -21,8 +18,7 @@
|
|
|
21
18
|
"paths": {
|
|
22
19
|
"client": "client",
|
|
23
20
|
"data": {
|
|
24
|
-
"domains": "domains"
|
|
25
|
-
"stores": "stores"
|
|
21
|
+
"domains": "domains"
|
|
26
22
|
},
|
|
27
23
|
"public": "public",
|
|
28
24
|
"server": "server",
|
package/source/server/Action.js
CHANGED
|
@@ -1,118 +1,99 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import {resolve} from "path";
|
|
2
|
+
import {default as EagerPromise, eager} from "./EagerPromise.js";
|
|
3
|
+
import View from "./view/View.js";
|
|
4
|
+
import Projector from "./Projector.js";
|
|
5
|
+
import InternalServerError from "./errors/InternalServer.js";
|
|
6
|
+
import {assert, defined} from "./invariants.js";
|
|
7
|
+
import sanitize from "./sanitize.js";
|
|
8
|
+
|
|
9
|
+
export default class Action {
|
|
10
|
+
static action = "index";
|
|
11
|
+
static namespace = "default";
|
|
12
|
+
static layout = "default";
|
|
13
|
+
|
|
14
|
+
constructor(request, session, context) {
|
|
15
|
+
this.request = request;
|
|
10
16
|
this.session = session;
|
|
17
|
+
this.context = EagerPromise.resolve(context);
|
|
11
18
|
}
|
|
12
19
|
|
|
13
|
-
static
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
let module;
|
|
19
|
-
const server = this.conf.paths.server;
|
|
20
|
-
const {namespace, action} = data.path;
|
|
20
|
+
static async new(request, session, context) {
|
|
21
|
+
const {namespace = this.namespace, action = this.action} = request.path;
|
|
22
|
+
assert(!namespace.includes("."), () => {
|
|
23
|
+
throw new InternalServerError("namespace may not include a dot");
|
|
24
|
+
});
|
|
21
25
|
const route = `${namespace}/${action}`;
|
|
26
|
+
const path = resolve(`${context.directory}/${route}.js`);
|
|
22
27
|
try {
|
|
23
|
-
|
|
28
|
+
return new (await import(path)).default(request, session, context);
|
|
24
29
|
} catch (error) {
|
|
25
|
-
throw new Error(`route
|
|
30
|
+
throw new Error(`route \`${route}\` missing`);
|
|
26
31
|
}
|
|
27
|
-
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get Class() {
|
|
35
|
+
return this.constructor;
|
|
28
36
|
}
|
|
29
37
|
|
|
30
38
|
get layout() {
|
|
31
|
-
return this.Class
|
|
39
|
+
return this.Class.layout;
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
get path() {
|
|
35
|
-
|
|
43
|
+
const {path} = this.request;
|
|
44
|
+
const namespace = path.namespace ?? this.Class.namespace;
|
|
45
|
+
const action = path.action ?? this.Class.action;
|
|
46
|
+
const {_id} = path;
|
|
47
|
+
return {action, namespace, _id};
|
|
36
48
|
}
|
|
37
49
|
|
|
38
50
|
get params() {
|
|
39
|
-
return this.
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
async _view(path) {
|
|
43
|
-
const {namespace, action} = this.path;
|
|
44
|
-
const view_path = path !== undefined ? path : `${namespace}/${action}`;
|
|
45
|
-
return (await this.context).view(view_path);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
static sanitize(payload = {}) {
|
|
49
|
-
//assert(typeof payload === "object");
|
|
50
|
-
return Object.keys(payload)
|
|
51
|
-
.map(key => ({key, "value": payload[key].toString().trim()}))
|
|
52
|
-
.map(({key, value}) => ({key, "value":
|
|
53
|
-
value === "" ? undefined : value}))
|
|
54
|
-
.reduce((o, {key, value}) => {o[key] = value; return o;}, {});
|
|
51
|
+
return this.request.params;
|
|
55
52
|
}
|
|
56
53
|
|
|
57
54
|
async run(type) {
|
|
55
|
+
defined(this[type]);
|
|
58
56
|
this.before && await this.before();
|
|
59
|
-
await this[type](
|
|
57
|
+
await this[type](sanitize(this.request.payload));
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
|
|
63
|
-
return
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return(data = {}) {
|
|
67
|
-
this.respond("return", () => ({"payload": data}));
|
|
60
|
+
return(payload = {}) {
|
|
61
|
+
this.respond("return", () => ({payload}));
|
|
68
62
|
}
|
|
69
63
|
|
|
70
64
|
view(data = {}, layout = true) {
|
|
71
65
|
this.respond("view", async () => {
|
|
72
|
-
const _view = await this.
|
|
73
|
-
const payload = await this.prepare(data, _view);
|
|
66
|
+
const {_view, payload} = await this.prepare(data);
|
|
74
67
|
const view = layout ? _view.file(this.layout) : _view.content;
|
|
75
68
|
return {view, payload};
|
|
76
69
|
});
|
|
77
70
|
}
|
|
78
71
|
|
|
79
72
|
update(data) {
|
|
80
|
-
this.respond("update", async () =>
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return {payload};
|
|
84
|
-
});
|
|
73
|
+
this.respond("update", async () =>
|
|
74
|
+
({"payload": (await this.prepare(data)).payload})
|
|
75
|
+
);
|
|
85
76
|
}
|
|
86
77
|
|
|
87
78
|
redirect(location) {
|
|
88
79
|
this.respond("redirect", () => ({location}));
|
|
89
80
|
}
|
|
90
81
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
async prepare(data = {}, view) {
|
|
100
|
-
(await this.context).Class().prepare(this, data);
|
|
101
|
-
return MetaPromise(data, await view.elements(this.layout));
|
|
82
|
+
async prepare(data = {}) {
|
|
83
|
+
const route = `${this.path.namespace}/${this.path.action}`;
|
|
84
|
+
const path = await eager`${this.context.directory}/${route}`;
|
|
85
|
+
const _view = await View.new(path, await this.context.layouts);
|
|
86
|
+
await this.context.Class.prepare(this, data);
|
|
87
|
+
const payload = await new Projector(data,
|
|
88
|
+
await _view.elements(this.layout)).project(route);
|
|
89
|
+
return {payload, _view};
|
|
102
90
|
}
|
|
103
91
|
|
|
104
|
-
respond(type,
|
|
105
|
-
this
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
data.action = this.path.action;
|
|
110
|
-
if (data.pathname === undefined) {
|
|
111
|
-
data.pathname = this._data.pathname.replace(this.conf.base, "")
|
|
112
|
-
+ (this._data.search ?? "");
|
|
113
|
-
}
|
|
114
|
-
data.type = type;
|
|
115
|
-
return data;
|
|
92
|
+
respond(type, how) {
|
|
93
|
+
this.response = async () => {
|
|
94
|
+
const context = await this.context.name;
|
|
95
|
+
const {url} = this.request;
|
|
96
|
+
return {...await how(), type, context, ...this.path, url};
|
|
116
97
|
};
|
|
117
98
|
}
|
|
118
99
|
}
|