primate 0.1.1 → 0.4.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.
Files changed (46) hide show
  1. package/README.md +4 -3
  2. package/package.json +9 -3
  3. package/source/client/Action.js +14 -19
  4. package/source/client/App.js +1 -4
  5. package/source/client/Client.js +14 -24
  6. package/source/client/Context.js +9 -15
  7. package/source/client/Element.js +12 -5
  8. package/source/client/Session.js +27 -0
  9. package/source/client/document.js +6 -0
  10. package/source/preset/primate.json +2 -6
  11. package/source/server/Action.js +53 -75
  12. package/source/server/App.js +26 -7
  13. package/source/server/Bundler.js +13 -16
  14. package/source/server/Context.js +64 -56
  15. package/source/server/{promises/Eager.js → EagerPromise.js} +3 -1
  16. package/source/server/File.js +6 -2
  17. package/source/server/Projector.js +86 -0
  18. package/source/server/Router.js +7 -9
  19. package/source/server/Session.js +9 -33
  20. package/source/server/attributes.js +2 -0
  21. package/source/server/conf.js +20 -26
  22. package/source/server/{Crypto.js → crypto.js} +0 -0
  23. package/source/server/domain/Domain.js +61 -56
  24. package/source/server/domain/Field.js +16 -13
  25. package/source/server/domain/domains.js +16 -0
  26. package/source/server/errors.js +0 -1
  27. package/source/server/exports.js +9 -8
  28. package/source/server/{utils/extend_object.js → extend_object.js} +0 -0
  29. package/source/server/invariants.js +0 -2
  30. package/source/server/sanitize.js +5 -0
  31. package/source/server/servers/Dynamic.js +19 -13
  32. package/source/server/servers/Static.js +25 -20
  33. package/source/server/servers/content-security-policy.json +0 -2
  34. package/source/server/store/Store.js +13 -0
  35. package/source/server/types/Date.js +2 -2
  36. package/source/server/types/Domain.js +0 -5
  37. package/source/server/types/Storeable.js +6 -7
  38. package/source/server/view/TreeNode.js +2 -0
  39. package/source/server/view/View.js +5 -0
  40. package/source/client/Base.js +0 -5
  41. package/source/server/Base.js +0 -35
  42. package/source/server/constructible.js +0 -8
  43. package/source/server/errors/Fallback.js +0 -1
  44. package/source/server/fallback.js +0 -11
  45. package/source/server/promises/Meta.js +0 -42
  46. package/source/server/promises.js +0 -2
@@ -2,7 +2,7 @@ import {dirname} from "path";
2
2
  import {fileURLToPath} from "url";
3
3
  import Directory from "./Directory.js";
4
4
  import File from "./File.js";
5
- import {algorithm, hash} from "./Crypto.js";
5
+ import {algorithm, hash} from "./crypto.js";
6
6
 
7
7
  const meta_url = fileURLToPath(import.meta.url);
8
8
  const directory = dirname(meta_url);
@@ -25,17 +25,18 @@ const stringify_actions = ({context, domain, actions = []}, i, j) =>
25
25
  actions.reduce((s, action, k) =>
26
26
  s + stringify_action(context, domain, action, i, j, k), "");
27
27
 
28
- const stringify_domain = (domain, i, j) =>
29
- `routes["${domain.context}"]["${domain.domain}"] = {};\n`
28
+ const stringify_domain = (context, domain, i, j) =>
29
+ `routes["${context}"]["${domain.domain}"] = {};\n`
30
30
  + stringify_actions(domain, i, j);
31
31
 
32
- const stringify_domains = (domains, i) =>
33
- domains.reduce((s, domain, j) => s + stringify_domain(domain, i, j), "");
32
+ const stringify_domains = ({context, domains}, i) =>
33
+ domains.reduce((s, domain, j) =>
34
+ s + stringify_domain(context, domain, i, j), "");
34
35
 
35
36
  const stringify_context = (context, i) =>
36
- `import context${i} from "./${context[0].context}/context.js";\n`
37
- + `contexts["${context[0].context}"] = context${i};\n`
38
- + `routes["${context[0].context}"] = {};\n`
37
+ `import context${i} from "./${context.context}/context.js";\n`
38
+ + `contexts["${context.context}"] = context${i};\n`
39
+ + `routes["${context.context}"] = {};\n`
39
40
  + stringify_domains(context, i) + "\n";
40
41
 
41
42
  const s_exports = () =>
@@ -100,11 +101,9 @@ export default class Bundler {
100
101
  }
101
102
 
102
103
  async register_scripts() {
103
- const {paths} = this.conf;
104
-
105
104
  const scripts = await this.collect("client");
106
105
  Promise.all(scripts.map(async script =>
107
- this.register(script, await File.read(paths.public, script))
106
+ this.register(script, await File.read(this.conf.paths.public, script))
108
107
  ));
109
108
  }
110
109
 
@@ -125,7 +124,7 @@ export default class Bundler {
125
124
 
126
125
  register(src, source) {
127
126
  const integrity = `${algorithm}-${hash(source)}`;
128
- this.scripts.push({src, integrity});
127
+ this.scripts.push({"src": `${this.conf.base}${src}`, integrity});
129
128
  this.hashes.add(integrity);
130
129
  }
131
130
 
@@ -139,7 +138,7 @@ export default class Bundler {
139
138
  client += `<script type="module" integrity="${integrity}" src="${src}">`
140
139
  + "</script>\n";
141
140
  }
142
- return client + "${view}";
141
+ return client + "<first-view />";
143
142
  }
144
143
 
145
144
  async write_exports() {
@@ -173,8 +172,6 @@ export default class Bundler {
173
172
  .map(action => action.slice(0, -ending)),
174
173
  })))));
175
174
 
176
- return actions.length === 1 && actions[0].length === 0
177
- ? [context_domains]
178
- : actions;
175
+ return context_domains;
179
176
  }
180
177
  }
@@ -1,40 +1,18 @@
1
- import Base from "./Base.js";
1
+ import {resolve} from "path";
2
2
  import File from "./File.js";
3
- import View from "./view/View.js";
4
3
  import Action from "./Action.js";
5
4
  import {InternalServerError} from "./errors.js";
5
+ import {assert, defined} from "./invariants.js";
6
6
  import cache from "./cache.js";
7
- import {defined} from "./invariants.js";
8
- import fallback from "./fallback.js";
9
7
  import log from "./log.js";
10
8
 
11
- export default class Context extends Base {
9
+ export default class Context {
10
+ static directory = "server";
11
+
12
12
  constructor(name) {
13
- super();
14
13
  this.name = name;
15
14
  }
16
15
 
17
- log(color, message) {
18
- log[color](this.name).reset(message).nl();
19
- }
20
-
21
- async errored(data, error, session) {
22
- if (this.Class().error !== undefined) {
23
- const action = new Action(data, session);
24
- this.Class().error(action);
25
- const response = await action.response;
26
- if (data.pathname === response.location) {
27
- throw new InternalServerError("redirect loop detected");
28
- }
29
- this.log("red", "handling error");
30
- return response;
31
- } else {
32
- const {namespace, action} = data.path;
33
- const route = `${namespace}/${action}`;
34
- throw new InternalServerError(`${data.type} /${route} missing`);
35
- }
36
- }
37
-
38
16
  static before(action) {
39
17
  return action;
40
18
  }
@@ -45,46 +23,76 @@ export default class Context extends Base {
45
23
 
46
24
  static get(name) {
47
25
  return cache(this, name, async () => {
48
- const exists = await File.exists(`${this.conf.paths.server}/${name}`);
49
- if (!exists) {
50
- throw new InternalServerError("missing context directory");
26
+ const directory = resolve(`${this.directory}/${name}`);
27
+ assert(await File.exists(directory), () => {
28
+ throw new InternalServerError(`missing context directory \`${name}\``);
29
+ });
30
+ try {
31
+ return new (await import(`${directory}/context.js`)).default(name);
32
+ } catch (error) {
33
+ return new this(name);
51
34
  }
52
- const path = `${this.conf.paths.server}/${name}/context.js`;
53
- const context = await fallback(
54
- async () => (await import(path)).default,
55
- () => Context);
56
- return new context(name);
57
35
  });
58
36
  }
59
37
 
60
- get base_path() {
61
- return `${this.conf.paths.server}/${this.name}`;
38
+ get Class() {
39
+ return this.constructor;
62
40
  }
63
41
 
64
- async view(path) {
65
- const file = await new File(`${this.base_path}/${path}.html`).read();
66
- return new View(path, file, await this.layouts());
42
+ get directory() {
43
+ return `${this.Class.directory}/${this.name}`;
67
44
  }
68
45
 
69
- layouts() {
70
- return cache(this, "layouts", async () => {
71
- const path = `${this.base_path}/layouts`;
46
+ get layouts() {
47
+ return cache(this, "layouts", () => {
48
+ const ending = -5;
49
+ const path = `${this.directory}/layouts`;
72
50
 
73
- const layouts = {};
74
- for (const file of await new File(path).list()) {
75
- layouts[file.slice(0, -5)] = await File.read(`${path}/${file}`);
76
- }
77
-
78
- return layouts;
51
+ return new File(path).list().reduce(async (sofar, file) => {
52
+ const layouts = await sofar;
53
+ layouts[file.slice(0, ending)] = await File.read(`${path}/${file}`);
54
+ return layouts;
55
+ }, {});
79
56
  });
80
57
  }
81
58
 
82
- async run(data, session) {
83
- const {type = "read"} = data;
84
- const action = await Action.load(data, session, this.name);
85
- defined(action[type]);
86
- await this.Class().before(action);
87
- await action.run(type);
88
- return action.response;
59
+ log(color, message) {
60
+ log[color](this.name).reset(message).nl();
61
+ }
62
+
63
+ async handle(error, action) {
64
+ if (this.Class.error !== undefined) {
65
+ this.Class.error(action);
66
+ const response = await action.response();
67
+ assert(action.request.pathname !== response.location, () => {
68
+ throw new InternalServerError("redirect loop detected");
69
+ });
70
+ this.log("red", "handling error with context error handler");
71
+ return response;
72
+ } else {
73
+ throw new InternalServerError(error.message);
74
+ }
75
+ }
76
+
77
+ catch(error, action) {
78
+ if (error instanceof InternalServerError) {
79
+ // cannot proceed, throw up
80
+ throw error;
81
+ } else {
82
+ return this.handle(error, action);
83
+ }
84
+ }
85
+
86
+ async run(request, session) {
87
+ const {type = "read"} = request;
88
+ try {
89
+ const action = await Action.new(request, session, this);
90
+ defined(action[type]);
91
+ await this.Class.before(action);
92
+ await action.run(type);
93
+ return action.response();
94
+ } catch(error) {
95
+ return this.catch(error, new Action(request, session, this));
96
+ }
89
97
  }
90
98
  }
@@ -1,3 +1,5 @@
1
+ import {inconstructible_function} from "./attributes.js";
2
+
1
3
  const $promise = Symbol("#promise");
2
4
 
3
5
  const handler = {
@@ -12,7 +14,7 @@ const handler = {
12
14
  if (property === "bind") {
13
15
  return result;
14
16
  }
15
- return typeof result[property] === "function"
17
+ return inconstructible_function(result[property])
16
18
  ? result[property].bind(result)
17
19
  : result[property];
18
20
  }));
@@ -1,7 +1,7 @@
1
1
  import fs from "fs";
2
2
  import {join} from "path";
3
3
  import Directory from "./Directory.js";
4
- import EagerPromise from "./promises/Eager.js";
4
+ import EagerPromise from "./EagerPromise.js";
5
5
 
6
6
  const array = maybe => Array.isArray(maybe) ? maybe : [maybe];
7
7
 
@@ -31,10 +31,14 @@ export default class File {
31
31
  return this.exists && !this.stats.isDirectory();
32
32
  }
33
33
 
34
- get stream() {
34
+ get read_stream() {
35
35
  return fs.createReadStream(this.path, {"flags": "r"});
36
36
  }
37
37
 
38
+ get write_stream() {
39
+ return fs.createWriteStream(this.path);
40
+ }
41
+
38
42
  remove() {
39
43
  return new Promise((resolve, reject) => fs.rm(this.path,
40
44
  {"recursive": true, "force": true},
@@ -0,0 +1,86 @@
1
+ import Domain from "./domain/Domain.js";
2
+ import {actuals} from "./domain/domains.js";
3
+ import log from "./log.js";
4
+
5
+ const scalar = async (document, property) => {
6
+ const value = await document[property];
7
+ return value instanceof Domain ? {} : value;
8
+ };
9
+
10
+ export default class Projector {
11
+ constructor(documents, projection = []) {
12
+ this.cache = {};
13
+ for (const actual in actuals) {
14
+ this.cache[actual] = {};
15
+ }
16
+ this.documents = documents;
17
+ this.projection = projection.reduce((sofar, domain) => {
18
+ const key = Object.keys(domain)[0];
19
+ sofar[key] = domain[key];
20
+ return sofar;
21
+ }, {});
22
+ }
23
+
24
+ many(documents, projection) {
25
+ return Promise.all(documents.map(d => this.resolve(d, projection)));
26
+ }
27
+
28
+ async by_store(key, document, projection) {
29
+ return this.resolve(await document[key], projection[key]);
30
+ }
31
+
32
+ async by_cache(space, key, document, projection) {
33
+ const cache = this.cache[space];
34
+ const key_id = document[`${key}_id`];
35
+ if (cache[key_id] === undefined) {
36
+ cache[key_id] = await this.by_store(key, document, projection);
37
+ }
38
+ return cache[key_id];
39
+ }
40
+
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);
45
+ }
46
+
47
+ async one(document, projection) {
48
+ if (document === undefined) {
49
+ return undefined;
50
+ }
51
+ const resolved = {};
52
+ let fields = undefined;
53
+ if (document instanceof Domain && document.Class.name !== "default") {
54
+ fields = actuals[document.Class.name];
55
+ }
56
+ for (const property of projection) {
57
+ if (typeof property === "string") {
58
+ resolved[property] = await scalar(document, property);
59
+ } else if (typeof property === "object") {
60
+ const key = Object.keys(property)[0];
61
+ resolved[key] = await this.object(document, property, key, fields);
62
+ }
63
+ }
64
+ return resolved;
65
+ }
66
+
67
+ resolve(document, projection) {
68
+ return this[Array.isArray(document) ? "many" : "one"](document, projection);
69
+ }
70
+
71
+ async project(route) {
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;
76
+ return {key, resolved};
77
+ }))).reduce((projected, {key, resolved}) => {
78
+ if (resolved !== undefined) {
79
+ projected[key] = resolved;
80
+ } else {
81
+ log.yellow(` \`${key}\` not projected`).nl();
82
+ }
83
+ return projected;
84
+ }, {});
85
+ }
86
+ }
@@ -2,11 +2,10 @@ export default class Router {
2
2
  constructor(routes = [], conf) {
3
3
  this.conf = conf;
4
4
  this.routes = {};
5
- routes.forEach(({from, to, contexts}) =>
6
- contexts.forEach(context => {
7
- this.routes[context] = this.routes[context] ?? [];
8
- this.routes[context].push({"from": new RegExp("^"+from+"$", "u"), to});
9
- }));
5
+ routes.forEach(({from, to, contexts}) => contexts.forEach(context => {
6
+ this.routes[context] = this.routes[context] ?? [];
7
+ this.routes[context].push({"from": new RegExp("^"+from+"$", "u"), to});
8
+ }));
10
9
  }
11
10
 
12
11
  context(context) {
@@ -27,7 +26,7 @@ export default class Router {
27
26
  const route = this.route_by_context(context, path);
28
27
  if (route !== undefined) {
29
28
  const replace = path.replace(route.from, route.to);
30
- return `${replace}${replace.includes("?") ? "&" : "?&"}${search}`;
29
+ return `${replace}${replace.includes("?") ? "" : "?"}&${search}`;
31
30
  } else {
32
31
  return pathname;
33
32
  }
@@ -43,14 +42,13 @@ export default class Router {
43
42
  const resolved = await this.resolve(this.debase(pathname), context);
44
43
  const url = new URL(`https://primatejs.com/${resolved}`);
45
44
  const parts = url.pathname.split("/").filter(part => part !== "");
46
- const {namespace, action} = this.conf.defaults;
47
45
  return {
48
46
  "pathname": url.pathname,
49
47
  "resolved": resolved,
50
48
  "parts": parts,
51
49
  "path": {
52
- "namespace": parts[0] ?? namespace,
53
- "action": parts[1] ?? action,
50
+ "namespace": parts[0],
51
+ "action": parts[1],
54
52
  "_id": parts[2],
55
53
  },
56
54
  "params": {
@@ -1,47 +1,37 @@
1
1
  import Context from "./Context.js";
2
2
  import Domain from "./domain/Domain.js";
3
- import {FallbackError, InternalServerError} from "./errors.js";
4
3
 
5
- const extract_id = cookie_header => cookie_header === undefined
6
- ? undefined
7
- : cookie_header
8
- .split(";")
9
- .filter(cookie => cookie.includes("session_id="))[0]
10
- ?.split("=")[1];
4
+ const extract_id = cookie_header => cookie_header
5
+ ?.split(";").filter(text => text.includes("session_id="))[0]?.split("=")[1];
11
6
 
12
7
  export default class Session extends Domain {
13
8
  static get fields() {
14
- const default_context = this.conf.defaults.context;
15
9
  return {
16
10
  "?data": Object,
17
- "context" : value => value ?? default_context,
11
+ "context" : String,
18
12
  "created": value => value ?? new Date(),
19
13
  };
20
14
  }
21
15
 
22
16
  async log(color, message) {
23
- (await this.actual_context).log(color, message);
17
+ (await Context.get(this.context)).log(color, message);
24
18
  }
25
19
 
26
20
  route(router, url) {
27
21
  return router.route(url, this.context);
28
22
  }
29
23
 
30
- get actual_context() {
31
- return Context.get(this.context);
32
- }
33
-
34
- static async get(cookie_header) {
24
+ static async get(cookie_header, default_context) {
35
25
  const session = await Session.touch({"_id": extract_id(cookie_header)});
36
26
  if (session.new) {
37
- await session.save();
27
+ await session.save({"context": default_context});
38
28
  session.has_cookie = false;
39
29
  }
40
30
  return session;
41
31
  }
42
32
 
43
33
  get cookie() {
44
- return `session_id=${this._id}; Secure; HttpOnly; Path=/`;
34
+ return `session_id=${this._id}; Secure; SameSite=Strict; HttpOnly; Path=/`;
45
35
  }
46
36
 
47
37
  async switch_context(context, data = {}) {
@@ -49,21 +39,7 @@ export default class Session extends Domain {
49
39
  return this;
50
40
  }
51
41
 
52
- async run(data) {
53
- let response, context;
54
- try {
55
- context = await this.actual_context;
56
- response = await context.run(data, this);
57
- } catch (error) {
58
- if (error instanceof FallbackError) {
59
- // do nothing
60
- } else if (error instanceof InternalServerError) {
61
- // cannot proceed, throw up
62
- throw error;
63
- } else {
64
- return context.errored(data, error, this);
65
- }
66
- }
67
- return response;
42
+ async run(request) {
43
+ return (await Context.get(this.context)).run(request, this);
68
44
  }
69
45
  }
@@ -7,5 +7,7 @@ export const constructible = value => {
7
7
  }
8
8
  };
9
9
 
10
+ export const inconstructible_function = value =>
11
+ typeof value === "function" && !constructible(value);
10
12
  export const numeric = value => !isNaN(parseFloat(value)) && isFinite(value);
11
13
  export const boolish = value => value === "true" || value === "false";
@@ -1,33 +1,27 @@
1
1
  import {join, resolve} from "path";
2
- import log from "./log.js";
3
2
  import cache from "./cache.js";
4
3
  import File from "./File.js";
5
- import extend_object from "./utils/extend_object.js";
4
+ import extend_object from "./extend_object.js";
6
5
  import primate_json from "../preset/primate.json" assert {"type": "json" };
7
6
 
8
- const qualify = (root, paths) => {
9
- const object = {};
10
- for (const key in paths) {
7
+ const qualify = (root, paths) =>
8
+ Object.keys(paths).reduce((sofar, key) => {
11
9
  const value = paths[key];
12
- if (typeof value === "string") {
13
- object[key] = join(root, value);
14
- } else {
15
- object[key] = qualify(`${root}/${key}`, value);
16
- }
17
- }
18
- return object;
19
- }
10
+ sofar[key] = typeof value === "string"
11
+ ? join(root, value)
12
+ : qualify(`${root}/${key}`, value);
13
+ return sofar;
14
+ }, {});
20
15
 
21
- export default (file = "primate.json") =>
22
- cache("conf", file, () => {
23
- const root = resolve();
24
- let conf = primate_json;
25
- try {
26
- conf = extend_object(conf, JSON.parse(File.read_sync(join(root, file))));
27
- } catch (error) {
28
- // local primate.json not required
29
- }
30
- conf.paths = qualify(root, conf.paths);
31
- conf.root = root;
32
- return conf
33
- });
16
+ export default (file = "primate.json") => cache("conf", file, () => {
17
+ let conf = primate_json;
18
+ const root = resolve();
19
+ try {
20
+ conf = extend_object(conf, JSON.parse(File.read_sync(join(root, file))));
21
+ } catch (error) {
22
+ // local primate.json not required
23
+ }
24
+ conf.paths = qualify(root, conf.paths);
25
+ conf.root = root;
26
+ return conf;
27
+ });
File without changes