primate 0.3.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 (41) hide show
  1. package/package.json +9 -3
  2. package/source/client/Action.js +12 -15
  3. package/source/client/Client.js +11 -15
  4. package/source/client/Context.js +9 -15
  5. package/source/client/Element.js +8 -3
  6. package/source/client/Session.js +27 -0
  7. package/source/client/document.js +1 -1
  8. package/source/preset/primate.json +2 -6
  9. package/source/server/Action.js +53 -75
  10. package/source/server/App.js +26 -7
  11. package/source/server/Bundler.js +11 -14
  12. package/source/server/Context.js +64 -56
  13. package/source/server/{promises/Eager.js → EagerPromise.js} +3 -1
  14. package/source/server/File.js +1 -1
  15. package/source/server/Projector.js +86 -0
  16. package/source/server/Router.js +7 -9
  17. package/source/server/Session.js +9 -33
  18. package/source/server/attributes.js +2 -0
  19. package/source/server/conf.js +20 -26
  20. package/source/server/{Crypto.js → crypto.js} +0 -0
  21. package/source/server/domain/Domain.js +56 -46
  22. package/source/server/domain/Field.js +16 -13
  23. package/source/server/domain/domains.js +16 -0
  24. package/source/server/errors.js +0 -1
  25. package/source/server/exports.js +9 -8
  26. package/source/server/{utils/extend_object.js → extend_object.js} +0 -0
  27. package/source/server/invariants.js +0 -2
  28. package/source/server/sanitize.js +5 -0
  29. package/source/server/servers/Dynamic.js +19 -13
  30. package/source/server/servers/Static.js +24 -19
  31. package/source/server/store/Store.js +13 -0
  32. package/source/server/types/Domain.js +0 -5
  33. package/source/server/types/Storeable.js +6 -7
  34. package/source/server/view/TreeNode.js +2 -0
  35. package/source/server/view/View.js +5 -0
  36. package/source/client/Base.js +0 -5
  37. package/source/server/Base.js +0 -35
  38. package/source/server/errors/Fallback.js +0 -1
  39. package/source/server/fallback.js +0 -11
  40. package/source/server/promises/Meta.js +0 -42
  41. package/source/server/promises.js +0 -2
@@ -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
 
@@ -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
@@ -1,26 +1,34 @@
1
- import Base from "../Base.js";
1
+ import {resolve as path_resolve} from "path";
2
2
  import Field from "./Field.js";
3
3
  import {PredicateError} from "../errors.js";
4
- import {EagerPromise} from "../promises.js";
4
+ import EagerPromise from "../EagerPromise.js";
5
+ import Store from "../store/Store.js";
5
6
  import cache from "../cache.js";
6
- import {random} from "../Crypto.js";
7
+ import DomainType from "../types/Domain.js";
8
+ import {random} from "../crypto.js";
7
9
 
8
10
  const length = 12;
9
- const preset = "../../preset/data/stores";
10
11
 
11
- const get = (target, property, receiver) =>
12
- Reflect.get(target, property, receiver) ?? receiver.as_foreign(property);
12
+ export default class Domain {
13
+ static stores_directory = "data/stores";
14
+ static store_file = "default.js";
13
15
 
14
- export default class Domain extends Base {
15
- constructor(document) {
16
- super();
16
+ static {
17
+ // avoid transitive cyclic dependency between Domain and Field
18
+ DomainType.instance = Domain;
19
+ this.cache = {};
20
+ }
17
21
 
22
+ constructor(document) {
18
23
  const errors = {};
19
- return new Proxy(this, {get}).set({...document, errors}).define("_id", {
24
+ this.define("_id", {
20
25
  "type": String,
21
26
  "predicates": ["unique"],
22
27
  "in": value => value ?? random(length).toString("hex"),
23
28
  });
29
+ return new Proxy(this, {"get": (target, property, receiver) =>
30
+ Reflect.get(target, property, receiver) ?? target.#proxy(property)
31
+ }).set({...document, errors})
24
32
  }
25
33
 
26
34
  get Class() {
@@ -39,20 +47,9 @@ export default class Domain extends Base {
39
47
  }
40
48
 
41
49
  static get store() {
42
- return cache(this, "store", async () => {
43
- const create_path = path => `${path}/${this.store_file}`;
44
- let store;
45
- try {
46
- store = await import(create_path(this.conf.paths.data.stores));
47
- } catch(error) {
48
- store = await import(create_path(preset));
49
- }
50
- return store.default.open();
51
- });
52
- }
53
-
54
- static get store_file() {
55
- return this.conf.defaults.store;
50
+ return EagerPromise.resolve(cache(this, "store", async () =>
51
+ Store.get(this.stores_directory, this.store_file)
52
+ ));
56
53
  }
57
54
 
58
55
  static get collection() {
@@ -97,13 +94,27 @@ export default class Domain extends Base {
97
94
  return Object.assign(this, document);
98
95
  }
99
96
 
100
- // #as_foreign
101
- as_foreign(name) {
97
+ #proxy(property) {
98
+ return typeof property === "string" ? this.#link(property) : this[property];
99
+ }
100
+
101
+ #link(name) {
102
102
  const field = this.fields[`${name}_id`];
103
- return field?.is_domain ? field.by_id(this[`${name}_id`]) : undefined;
103
+ if (field?.is_domain) {
104
+ const collection = field.Type.collection;
105
+ const cache = this.Class.cache;
106
+ if (cache[collection] === undefined) {
107
+ cache[collection] = {};
108
+ }
109
+ if (cache[collection][this[`${name}_id`]] === undefined) {
110
+ cache[collection][this[`${name}_id`]] = field.by_id(this[`${name}_id`]);
111
+ }
112
+ return cache[collection][this[`${name}_id`]];
113
+ } else {
114
+ return undefined
115
+ }
104
116
  }
105
117
 
106
- // #serialize
107
118
  // Serializing is done from the instance's point of view.
108
119
  async serialize() {
109
120
  const {properties, fields} = this;
@@ -116,7 +127,6 @@ export default class Domain extends Base {
116
127
  }, {});
117
128
  }
118
129
 
119
- // #deserialize
120
130
  // Deserializing is done from the class's point of view.
121
131
  static deserialize(serialized) {
122
132
  const fields = this._fields;
@@ -138,6 +148,7 @@ export default class Domain extends Base {
138
148
  get collection() {
139
149
  return this.Class.collection;
140
150
  }
151
+
141
152
  get properties() {
142
153
  return this.Class.properties;
143
154
  }
@@ -171,9 +182,12 @@ export default class Domain extends Base {
171
182
  async savewith(delta, after = () => undefined) {
172
183
  const verified = await this.verify(delta);
173
184
  if (verified) {
174
- const store = await this.store;
175
185
  const document = await this.serialize();
176
- await store.save(this.collection, {"_id": document._id}, document);
186
+ await this.store.save(this.collection, {"_id": document._id}, document);
187
+ const cache = this.Class.cache;
188
+ if (cache[this.collection]?.[document._id] !== undefined) {
189
+ delete cache[this.collection][document._id];
190
+ }
177
191
  await after();
178
192
  }
179
193
  return verified;
@@ -197,26 +211,24 @@ export default class Domain extends Base {
197
211
  return this;
198
212
  }
199
213
 
200
- async delete() {
201
- const store = await this.store;
202
- return store.delete(this.collection, {"_id": this._id});
214
+ delete() {
215
+ return this.store.delete(this.collection, {"_id": this._id});
203
216
  }
204
217
 
205
- static async delete(criteria) {
206
- const store = await this.store;
207
- return store.delete(this.collection, criteria);
218
+ static delete(criteria) {
219
+ return this.store.delete(this.collection, criteria);
208
220
  }
209
221
 
210
222
  static by_id(_id) {
211
223
  return new EagerPromise(async resolve => {
212
- const result = await (await this.store).one(this.collection, _id);
213
- resolve(result === undefined ? undefined : this.deserialize(result));
224
+ const result = await this.store.find(this.collection, {"_id": await _id});
225
+ resolve(result.length > 0 ? this.deserialize(result[0]) : undefined);
214
226
  });
215
227
  }
216
228
 
217
- static first(criteria) {
229
+ static first(criteria, options) {
218
230
  return new EagerPromise(async resolve => {
219
- const result = await (await this.store).one(this.collection, criteria);
231
+ const result = await this.store.one(this.collection, criteria, options);
220
232
  resolve(result === undefined ? undefined : this.deserialize(result));
221
233
  });
222
234
  }
@@ -230,14 +242,12 @@ export default class Domain extends Base {
230
242
  }
231
243
 
232
244
  static async find(criteria, options) {
233
- const store = await this.store;
234
- const results = await store.find(this.collection, criteria, options);
245
+ const results = await this.store.find(this.collection, criteria, options);
235
246
  return results.map(result => this.deserialize(result));
236
247
  }
237
248
 
238
- static async count(criteria) {
239
- const store = await this.store;
240
- return store.count(this.collection, criteria);
249
+ static count(criteria) {
250
+ return this.store.count(this.collection, criteria);
241
251
  }
242
252
 
243
253
  static async exists(criteria) {