primate 0.0.1 → 0.1.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 (74) hide show
  1. package/LICENSE +27 -0
  2. package/README.md +48 -0
  3. package/package.json +10 -1
  4. package/source/client/Action.js +161 -0
  5. package/source/client/App.js +19 -0
  6. package/source/client/Base.js +5 -0
  7. package/source/client/Client.js +71 -0
  8. package/source/client/Context.js +53 -0
  9. package/source/client/Element.js +243 -0
  10. package/source/client/Node.js +13 -0
  11. package/source/client/View.js +90 -0
  12. package/source/client/exports.js +15 -0
  13. package/source/preset/client/Element.js +2 -0
  14. package/source/preset/client/app.js +2 -0
  15. package/source/preset/data/stores/default.js +2 -0
  16. package/source/preset/primate.json +31 -0
  17. package/source/preset/static/index.html +10 -0
  18. package/source/server/Action.js +118 -0
  19. package/source/server/App.js +42 -0
  20. package/source/server/Base.js +35 -0
  21. package/source/server/Bundler.js +180 -0
  22. package/source/server/Context.js +90 -0
  23. package/source/server/Crypto.js +8 -0
  24. package/source/server/Directory.js +35 -0
  25. package/source/server/File.js +113 -0
  26. package/source/server/Router.js +61 -0
  27. package/source/server/Session.js +69 -0
  28. package/source/server/attributes.js +11 -0
  29. package/source/server/cache.js +17 -0
  30. package/source/server/conf.js +33 -0
  31. package/source/server/constructible.js +8 -0
  32. package/source/server/domain/Domain.js +282 -0
  33. package/source/server/domain/Field.js +115 -0
  34. package/source/server/domain/domains.js +15 -0
  35. package/source/server/errors/Fallback.js +1 -0
  36. package/source/server/errors/InternalServer.js +1 -0
  37. package/source/server/errors/Predicate.js +1 -0
  38. package/source/server/errors.js +3 -0
  39. package/source/server/exports.js +27 -0
  40. package/source/server/fallback.js +11 -0
  41. package/source/server/invariants.js +19 -0
  42. package/source/server/log.js +22 -0
  43. package/source/server/promises/Eager.js +49 -0
  44. package/source/server/promises/Meta.js +42 -0
  45. package/source/server/promises.js +2 -0
  46. package/source/server/servers/Dynamic.js +51 -0
  47. package/source/server/servers/Server.js +5 -0
  48. package/source/server/servers/Static.js +113 -0
  49. package/source/server/servers/content-security-policy.json +9 -0
  50. package/source/server/servers/http-codes.json +5 -0
  51. package/source/server/servers/mimes.json +12 -0
  52. package/source/server/store/Memory.js +60 -0
  53. package/source/server/store/Store.js +17 -0
  54. package/source/server/types/Array.js +33 -0
  55. package/source/server/types/Boolean.js +29 -0
  56. package/source/server/types/Date.js +20 -0
  57. package/source/server/types/Domain.js +16 -0
  58. package/source/server/types/Instance.js +8 -0
  59. package/source/server/types/Number.js +45 -0
  60. package/source/server/types/Object.js +12 -0
  61. package/source/server/types/Primitive.js +7 -0
  62. package/source/server/types/Storeable.js +51 -0
  63. package/source/server/types/String.js +49 -0
  64. package/source/server/types/errors/Array.json +7 -0
  65. package/source/server/types/errors/Boolean.json +5 -0
  66. package/source/server/types/errors/Date.json +3 -0
  67. package/source/server/types/errors/Number.json +9 -0
  68. package/source/server/types/errors/Object.json +3 -0
  69. package/source/server/types/errors/String.json +11 -0
  70. package/source/server/types.js +6 -0
  71. package/source/server/utils/extend_object.js +10 -0
  72. package/source/server/view/Parser.js +122 -0
  73. package/source/server/view/TreeNode.js +195 -0
  74. package/source/server/view/View.js +30 -0
@@ -0,0 +1,113 @@
1
+ import fs from "fs";
2
+ import {join} from "path";
3
+ import Directory from "./Directory.js";
4
+ import EagerPromise from "./promises/Eager.js";
5
+
6
+ const array = maybe => Array.isArray(maybe) ? maybe : [maybe];
7
+
8
+ const filter_files = (files, filter) =>
9
+ files.filter(file => array(filter).some(ending => file.endsWith(ending)));
10
+
11
+ export default class File {
12
+ constructor(...args) {
13
+ this.path = join(...args);
14
+ return EagerPromise.resolve(new Promise(resolve => {
15
+ fs.lstat(this.path, (error, stats) => {
16
+ this.stats = stats;
17
+ resolve(this);
18
+ });
19
+ }));
20
+ }
21
+
22
+ get modified() {
23
+ return Math.round(this.stats.mtimeMs);
24
+ }
25
+
26
+ get exists() {
27
+ return this.stats !== undefined;
28
+ }
29
+
30
+ get is_file() {
31
+ return this.exists && !this.stats.isDirectory();
32
+ }
33
+
34
+ get stream() {
35
+ return fs.createReadStream(this.path, {"flags": "r"});
36
+ }
37
+
38
+ remove() {
39
+ return new Promise((resolve, reject) => fs.rm(this.path,
40
+ {"recursive": true, "force": true},
41
+ error => error === null ? resolve(this) : reject(error)
42
+ ));
43
+ }
44
+
45
+ create() {
46
+ return new Promise((resolve, reject) => fs.mkdir(this.path, error =>
47
+ error === null ? resolve(this) : reject(error)
48
+ ));
49
+ }
50
+
51
+ async copy(to, recreate = true) {
52
+ if (this.stats.isDirectory()) {
53
+ if (recreate) {
54
+ await new File(`${to}`).recreate();
55
+ }
56
+ // copy all files
57
+ return Promise.all((await this.list()).map(file =>
58
+ new File(`${this.path}/${file}`).copy(`${to}/${file}`)
59
+ ));
60
+ } else {
61
+ return new Promise((resolve, reject) => fs.copyFile(this.path, to,
62
+ error => error === null ? resolve(this) : reject(error)));
63
+ }
64
+ }
65
+
66
+ async list(filter) {
67
+ if (!this.exists) {
68
+ return [];
69
+ }
70
+ const files = await Directory.list(this.path);
71
+ return filter !== undefined ? filter_files(files, filter) : files;
72
+ }
73
+
74
+ async recreate() {
75
+ return (await this.remove()).create();
76
+ }
77
+
78
+ read(options = {"encoding": "utf8"}) {
79
+ return new Promise((resolve, reject) =>
80
+ fs.readFile(this.path, options, (error, nonerror) =>
81
+ error === null ? resolve(nonerror) : reject(error)));
82
+ }
83
+
84
+ write(data, options = {"encoding": "utf8"}) {
85
+ return new Promise((resolve, reject) => fs.writeFile(this.path, data,
86
+ options,
87
+ error => error === null ? resolve(this) : reject(error)));
88
+ }
89
+
90
+ static read_sync(path, options = {"encoding": "utf8"}) {
91
+ return fs.readFileSync(path, options);
92
+ }
93
+
94
+ static exists(...args) {
95
+ return new File(...args).exists;
96
+ }
97
+
98
+ static read(...args) {
99
+ return new File(...args).read();
100
+ }
101
+
102
+ static write(path, data, options) {
103
+ return new File(path).write(data, options);
104
+ }
105
+
106
+ static remove(...args) {
107
+ return new File(...args).remove();
108
+ }
109
+
110
+ static copy(from, to, recreate) {
111
+ return new File(from).copy(to, recreate);
112
+ }
113
+ }
@@ -0,0 +1,61 @@
1
+ export default class Router {
2
+ constructor(routes = [], conf) {
3
+ this.conf = conf;
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
+ }));
10
+ }
11
+
12
+ context(context) {
13
+ return context ?? this.conf.defaults.context;
14
+ }
15
+
16
+ debase(pathname) {
17
+ return pathname.replace(this.conf.base, "/");
18
+ }
19
+
20
+ route_by_context(context, path) {
21
+ return this.routes[context]?.find(({from}) => from.test(path)) ?? [];
22
+ }
23
+
24
+ resolve_by_context(pathname, context) {
25
+ const [path, search = ""] = pathname.split("?");
26
+
27
+ const route = this.route_by_context(context, path);
28
+ if (route !== undefined) {
29
+ const replace = path.replace(route.from, route.to);
30
+ return `${replace}${replace.includes("?") ? "&" : "?&"}${search}`;
31
+ } else {
32
+ return pathname;
33
+ }
34
+ }
35
+
36
+ resolve(pathname, context) {
37
+ return this.context(context) !== undefined
38
+ ? this.resolve_by_context(pathname, context)
39
+ : pathname;
40
+ }
41
+
42
+ async route(pathname, context) {
43
+ const resolved = await this.resolve(this.debase(pathname), context);
44
+ const url = new URL(`https://primatejs.com/${resolved}`);
45
+ const parts = url.pathname.split("/").filter(part => part !== "");
46
+ const {namespace, action} = this.conf.defaults;
47
+ return {
48
+ "pathname": url.pathname,
49
+ "resolved": resolved,
50
+ "parts": parts,
51
+ "path": {
52
+ "namespace": parts[0] ?? namespace,
53
+ "action": parts[1] ?? action,
54
+ "_id": parts[2],
55
+ },
56
+ "params": {
57
+ ...Object.fromEntries(url.searchParams),
58
+ },
59
+ };
60
+ }
61
+ }
@@ -0,0 +1,69 @@
1
+ import Context from "./Context.js";
2
+ import Domain from "./domain/Domain.js";
3
+ import {FallbackError, InternalServerError} from "./errors.js";
4
+
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];
11
+
12
+ export default class Session extends Domain {
13
+ static get fields() {
14
+ const default_context = this.conf.defaults.context;
15
+ return {
16
+ "?data": Object,
17
+ "context" : value => value ?? default_context,
18
+ "created": value => value ?? new Date(),
19
+ };
20
+ }
21
+
22
+ async log(color, message) {
23
+ (await this.actual_context).log(color, message);
24
+ }
25
+
26
+ route(router, url) {
27
+ return router.route(url, this.context);
28
+ }
29
+
30
+ get actual_context() {
31
+ return Context.get(this.context);
32
+ }
33
+
34
+ static async get(cookie_header) {
35
+ const session = await Session.touch({"_id": extract_id(cookie_header)});
36
+ if (session.new) {
37
+ await session.save();
38
+ session.has_cookie = false;
39
+ }
40
+ return session;
41
+ }
42
+
43
+ get cookie() {
44
+ return `session_id=${this._id}; Secure; HttpOnly; Path=/`;
45
+ }
46
+
47
+ async switch_context(context, data = {}) {
48
+ await this.save({context, data});
49
+ return this;
50
+ }
51
+
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;
68
+ }
69
+ }
@@ -0,0 +1,11 @@
1
+ export const constructible = value => {
2
+ try {
3
+ Reflect.construct(String, [], value);
4
+ return true;
5
+ } catch (error) {
6
+ return false;
7
+ }
8
+ };
9
+
10
+ export const numeric = value => !isNaN(parseFloat(value)) && isFinite(value);
11
+ export const boolish = value => value === "true" || value === "false";
@@ -0,0 +1,17 @@
1
+ const Cache = class Cache {
2
+ static #caches = [];
3
+
4
+ static get(object, property) {
5
+ return Cache.#caches.find(entry =>
6
+ entry.object === object && entry.property === property)?.value;
7
+ }
8
+
9
+ static put(object, property, cacher) {
10
+ const value = cacher();
11
+ Cache.#caches.push({object, property, value});
12
+ return value;
13
+ }
14
+ };
15
+
16
+ export default (object, property, cacher) =>
17
+ Cache.get(object, property) ?? Cache.put(object, property, cacher);
@@ -0,0 +1,33 @@
1
+ import {join, resolve} from "path";
2
+ import log from "./log.js";
3
+ import cache from "./cache.js";
4
+ import File from "./File.js";
5
+ import extend_object from "./utils/extend_object.js";
6
+ import primate_json from "../preset/primate.json" assert {"type": "json" };
7
+
8
+ const qualify = (root, paths) => {
9
+ const object = {};
10
+ for (const key in paths) {
11
+ 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
+ }
20
+
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
+ });
@@ -0,0 +1,8 @@
1
+ export default value => {
2
+ try {
3
+ Reflect.construct(String, [], value);
4
+ return true;
5
+ } catch (error) {
6
+ return false;
7
+ }
8
+ };
@@ -0,0 +1,282 @@
1
+ import Base from "../Base.js";
2
+ import Field from "./Field.js";
3
+ import Store from "../store/Store.js";
4
+ import {PredicateError, InternalServerError} from "../errors.js";
5
+ import {EagerPromise} from "../promises.js";
6
+ import {assert} from "../invariants.js";
7
+ import cache from "../cache.js";
8
+ import {random} from "../Crypto.js";
9
+ import fallback from "../fallback.js";
10
+
11
+ const length = 12;
12
+ const preset = "../../preset/data/stores";
13
+
14
+ const get = (target, property, receiver) =>
15
+ Reflect.get(target, property, receiver) ?? receiver.as_foreign(property);
16
+
17
+ export default class Domain extends Base {
18
+ constructor(document) {
19
+ super();
20
+
21
+ const errors = {};
22
+ return new Proxy(this, {get}).set({...document, errors}).define("_id", {
23
+ "type": String,
24
+ "predicates": ["unique"],
25
+ "in": value => value ?? random(length).toString("hex"),
26
+ });
27
+ }
28
+
29
+ get Class() {
30
+ return this.constructor;
31
+ }
32
+
33
+ static get fields() {
34
+ return {};
35
+ }
36
+
37
+ static get _fields() {
38
+ // initialize programmatic defines
39
+ cache(this, "initialized", () => {new this();});
40
+ Object.keys(this.fields).map(name => this.define(name, this.fields[name]));
41
+ return cache(this, "fields", () => ({}));
42
+ }
43
+
44
+ static get store() {
45
+ return cache(this, "store", async () => {
46
+ const logic = path => import(`${path}/${this.store_file}`);
47
+ const store = await fallback(this.conf.paths.data.stores, preset, logic);
48
+
49
+ const instance = store.default;
50
+
51
+ const message = `${instance.constructor.name} must instance Store`;
52
+ const error = () => { throw new InternalServerError(message); };
53
+ assert(instance instanceof Store, error);
54
+
55
+ return instance.open();
56
+ });
57
+ }
58
+
59
+ static get store_file() {
60
+ return this.conf.defaults.store;
61
+ }
62
+
63
+ static get collection() {
64
+ return this.name.toLowerCase();
65
+ }
66
+
67
+ static get properties() {
68
+ const properties = [];
69
+ const fields = this._fields;
70
+ for (const key in fields) {
71
+ const field = fields[key];
72
+ if (!field.options.transient) {
73
+ properties.push(key);
74
+ }
75
+ }
76
+ return properties;
77
+ }
78
+
79
+ static define(name, definition) {
80
+ const fields = cache(this, "fields", () => ({}));
81
+ const {property, options} = Field.resolve(name);
82
+ if (fields[property] === undefined) {
83
+ fields[property] = new Field(property, definition, options);
84
+ }
85
+ }
86
+
87
+ define(name, definition) {
88
+ this.Class.define(name, definition);
89
+ return this;
90
+ }
91
+
92
+ get() {
93
+ return this.properties
94
+ .filter(property => property !== "_id")
95
+ .reduce((document, property) => {
96
+ document[property] = this[property];
97
+ return document;
98
+ }, {});
99
+ }
100
+
101
+ set(document = {}) {
102
+ return Object.assign(this, document);
103
+ }
104
+
105
+ // #as_foreign
106
+ as_foreign(name) {
107
+ const field = this.fields[`${name}_id`];
108
+ return field?.is_domain ? field.by_id(this[`${name}_id`]) : undefined;
109
+ }
110
+
111
+ // #serialize
112
+ // Serializing is done from the instance's point of view.
113
+ serialize() {
114
+ const {properties, fields} = this;
115
+ return properties.map(property =>
116
+ ({property, "value": fields[property].serialize(this[property])}))
117
+ .filter(({value}) => value !== undefined)
118
+ .reduce((document, {property, value}) => {
119
+ document[property] = value;
120
+ return document;
121
+ }, {});
122
+ }
123
+
124
+ // #deserialize
125
+ // Deserializing is done from the class's point of view.
126
+ static deserialize(serialized) {
127
+ const fields = this._fields;
128
+ return new this(Object.keys(serialized)
129
+ //.filter(property => fields[property] !== undefined)
130
+ .map(property =>
131
+ ({property,
132
+ "value": fields[property].deserialize(serialized[property])}))
133
+ .reduce((document, {property, value}) => {
134
+ document[property] = value;
135
+ return document;
136
+ }, {}));
137
+ }
138
+
139
+ get fields() {
140
+ return this.Class._fields;
141
+ }
142
+
143
+ get collection() {
144
+ return this.Class.collection;
145
+ }
146
+ get properties() {
147
+ return this.Class.properties;
148
+ }
149
+
150
+ get store() {
151
+ return this.Class.store;
152
+ }
153
+
154
+ get new() {
155
+ return this._id === undefined;
156
+ }
157
+
158
+ async verify(delta) {
159
+ this.set(delta);
160
+ const fields = this.fields;
161
+ this.errors = (await Promise.all(Object.keys(fields).map(async property =>
162
+ ({property, "value": await fields[property].verify(property, this)}))))
163
+ .filter(result => typeof result.value === "string")
164
+ .reduce((errors, result) => {
165
+ errors[result.property] = result.value;
166
+ return errors;
167
+ }, {});
168
+
169
+ return Object.keys(this.errors).length === 0;
170
+ }
171
+
172
+ save(delta) {
173
+ return this.new ? this.insert(delta) : this.update(delta);
174
+ }
175
+
176
+ async savewith(delta, after = () => undefined) {
177
+ const verified = await this.verify(delta);
178
+ if (verified) {
179
+ const store = await this.store;
180
+ const document = this.serialize();
181
+ await store.save(this.collection, {"_id": document._id}, document);
182
+ await after();
183
+ }
184
+ return verified;
185
+ }
186
+
187
+ insert(delta) {
188
+ delete this._id;
189
+ return this.savewith(delta, () => this.inserted());
190
+ }
191
+
192
+ inserted() {
193
+ return this;
194
+ }
195
+
196
+ async update(delta) {
197
+ const old = await this.Class.by_id(this._id);
198
+ return this.savewith(delta, () => this.updated(old));
199
+ }
200
+
201
+ updated() {
202
+ return this;
203
+ }
204
+
205
+ async delete() {
206
+ const store = await this.store;
207
+ return store.delete(this.collection, {"_id": this._id});
208
+ }
209
+
210
+ static async delete(criteria) {
211
+ const store = await this.store;
212
+ return store.delete(this.collection, criteria);
213
+ }
214
+
215
+ static by_id(_id) {
216
+ return new EagerPromise(async resolve => {
217
+ const result = await (await this.store).one(this.collection, _id);
218
+ resolve(result === undefined ? undefined : this.deserialize(result));
219
+ });
220
+ }
221
+
222
+ static first(criteria) {
223
+ return new EagerPromise(async resolve => {
224
+ const result = await (await this.store).one(this.collection, criteria);
225
+ resolve(result === undefined ? undefined : this.deserialize(result));
226
+ });
227
+ }
228
+
229
+ static one(criteria) {
230
+ return this[typeof criteria === "object" ? "first" : "by_id"](criteria);
231
+ }
232
+
233
+ static async touch(criteria) {
234
+ return await this.one(criteria) ?? new this();
235
+ }
236
+
237
+ static async find(criteria, options) {
238
+ const store = await this.store;
239
+ const results = await store.find(this.collection, criteria, options);
240
+ return results.map(result => this.deserialize(result));
241
+ }
242
+
243
+ static async count(criteria) {
244
+ const store = await this.store;
245
+ return store.count(this.collection, criteria);
246
+ }
247
+
248
+ static async exists(criteria) {
249
+ return await this.count(criteria) > 0;
250
+ }
251
+
252
+ async exists(property) {
253
+ if (!await this.Class.exists({[property]: this[property]})) {
254
+ throw new PredicateError(`No document exists with this ${property}`);
255
+ }
256
+ }
257
+
258
+ static async unique(criteria, _id) {
259
+ const one = await this.count({...criteria, _id});
260
+ const count = await this.count(criteria);
261
+ return count-one === 0;
262
+ }
263
+
264
+ async unique(property) {
265
+ if (!await this.Class.unique({[property]: this[property]}, this._id)) {
266
+ throw new PredicateError("Must be unique");
267
+ }
268
+ }
269
+
270
+ async unique_by(property, by_property) {
271
+ if (!await this.Class.unique({
272
+ [property]: await this[property],
273
+ [by_property]: await this[by_property],
274
+ }, this._id)) {
275
+ throw new PredicateError(`Must be unique in respect to ${by_property}`);
276
+ }
277
+ }
278
+
279
+ static insert(document) {
280
+ return new this(document).save();
281
+ }
282
+ }