primate 0.8.1 → 0.9.1

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 (68) hide show
  1. package/LICENSE +17 -23
  2. package/README.md +199 -218
  3. package/README.template.md +170 -0
  4. package/bin/primate.js +5 -0
  5. package/exports.js +3 -29
  6. package/html.js +13 -0
  7. package/module.json +10 -0
  8. package/package.json +14 -16
  9. package/{source → src}/Bundler.js +2 -2
  10. package/{source → src}/cache.js +0 -0
  11. package/src/conf.js +25 -0
  12. package/{source → src}/errors/InternalServer.js +0 -0
  13. package/{source → src}/errors/Predicate.js +0 -0
  14. package/src/errors/Route.js +1 -0
  15. package/{source → src}/errors.js +0 -0
  16. package/{source/extend_object.js → src/extend.js} +3 -3
  17. package/src/extend.spec.js +111 -0
  18. package/src/handlers/http.js +1 -0
  19. package/src/handlers/http404.js +7 -0
  20. package/src/handlers/json.js +7 -0
  21. package/src/handlers/stream.js +7 -0
  22. package/src/handlers/text.js +14 -0
  23. package/{source → src}/http-codes.json +0 -0
  24. package/src/log.js +22 -0
  25. package/{source → src}/mimes.json +0 -0
  26. package/{source → src}/preset/primate.json +0 -0
  27. package/{source → src}/preset/stores/default.js +0 -0
  28. package/src/route.js +61 -0
  29. package/src/run.js +26 -0
  30. package/src/serve.js +90 -0
  31. package/source/App.js +0 -35
  32. package/source/EagerPromise.js +0 -49
  33. package/source/Router.js +0 -31
  34. package/source/Server.js +0 -93
  35. package/source/Session.js +0 -26
  36. package/source/attributes.js +0 -14
  37. package/source/conf.js +0 -26
  38. package/source/domain/Domain.js +0 -285
  39. package/source/domain/Field.js +0 -113
  40. package/source/domain/Predicate.js +0 -24
  41. package/source/handlers/DOM/Node.js +0 -179
  42. package/source/handlers/DOM/Parser.js +0 -135
  43. package/source/handlers/html.js +0 -28
  44. package/source/handlers/http.js +0 -8
  45. package/source/handlers/json.js +0 -6
  46. package/source/handlers/redirect.js +0 -14
  47. package/source/invariants.js +0 -36
  48. package/source/map_entries.js +0 -6
  49. package/source/sanitize.js +0 -8
  50. package/source/store/Memory.js +0 -60
  51. package/source/store/Store.js +0 -30
  52. package/source/types/Array.js +0 -33
  53. package/source/types/Boolean.js +0 -29
  54. package/source/types/Date.js +0 -20
  55. package/source/types/Domain.js +0 -11
  56. package/source/types/Instance.js +0 -8
  57. package/source/types/Number.js +0 -45
  58. package/source/types/Object.js +0 -12
  59. package/source/types/Primitive.js +0 -7
  60. package/source/types/Storeable.js +0 -44
  61. package/source/types/String.js +0 -49
  62. package/source/types/errors/Array.json +0 -7
  63. package/source/types/errors/Boolean.json +0 -5
  64. package/source/types/errors/Date.json +0 -3
  65. package/source/types/errors/Number.json +0 -9
  66. package/source/types/errors/Object.json +0 -3
  67. package/source/types/errors/String.json +0 -11
  68. package/source/types.js +0 -6
package/LICENSE CHANGED
@@ -1,27 +1,21 @@
1
- Copyright (c) 2022, Primate core team <core@primatejs.com>. All rights
2
- reserved.
1
+ Primate JavaScript Framework
3
2
 
4
- Redistribution and use in source and binary forms, with or without
5
- modification, are permitted provided that the following conditions are met:
3
+ Copyright (c) Terrablue <terrablue@proton.me> and contributors.
6
4
 
7
- 1. Redistributions of source code must retain the above copyright notice, this
8
- list of conditions and the following disclaimer.
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
9
11
 
10
- 2. Redistributions in binary form must reproduce the above copyright notice,
11
- this list of conditions and the following disclaimer in the documentation
12
- and/or other materials provided with the distribution.
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
13
14
 
14
- 3. Neither the name of the copyright holder nor the names of its contributors
15
- may be used to endorse or promote products derived from this software without
16
- specific prior written permission.
17
-
18
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
- ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22
- FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24
- SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25
- CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26
- OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27
- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18
+ THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
package/README.md CHANGED
@@ -1,328 +1,309 @@
1
1
  # Primate
2
2
 
3
- A full-stack Javascript framework with data verification and server-side HTML
4
- rendering.
5
-
6
- ## Highlights
7
-
8
- * Flexible HTTP routing, returning HTML, JSON or a custom handler
9
- * Secure by default with HTTPS, hash-verified scripts and a strong CSP
10
- * Built-in support for sessions with secure cookies
11
- * Input verification using data domains
12
- * Many different data store modules: In-Memory (built-in),
13
- [File][primate-file-store], [JSON][primate-json-store],
14
- [MongoDB][primate-mongodb-store]
15
- * Easy modelling of`1:1`, `1:n` and `n:m` relationships
16
- * Minimally opinionated with sane, overrideable defaults
17
- * Supports both Node.js and Deno
3
+ Primal JavaScript framework.
18
4
 
19
5
  ## Getting started
20
6
 
21
- ### Prepare
22
-
23
- Lay out your app
7
+ Lay out app
24
8
 
25
9
  ```sh
26
- $ mkdir -p primate-app/{routes,components,ssl} && cd primate-app
10
+ mkdir -p app/{routes,components,ssl} && cd app
11
+
27
12
  ```
28
13
 
29
- Create a route for `/`
14
+ Create a route for `/` in `routes/site.js`
30
15
 
31
16
  ```js
32
- // routes/site.js
17
+ import html from "@primate/html";
33
18
 
34
- import {router, html} from "primate";
19
+ export default router => {
20
+ router.get("/", () => html`<site-index date="${new Date()}" />`);
21
+ };
35
22
 
36
- router.get("/", () => html`<site-index date="${new Date()}" />`);
37
23
  ```
38
24
 
39
- Create a component for your route (in `components/site-index.html`)
25
+ Create a component in `components/site-index.html`
40
26
 
41
27
  ```html
42
- Today's date is <span value="${date}"></span>.
28
+ Today's date is ${date}.
29
+
43
30
  ```
44
31
 
45
- Generate SSL key/certificate
32
+ Generate SSL files
46
33
 
47
34
  ```sh
48
35
  openssl req -x509 -out ssl/default.crt -keyout ssl/default.key -newkey rsa:2048 -nodes -sha256 -batch
49
- ```
50
36
 
51
- Add an entry file
37
+ ```
52
38
 
53
- ```js
54
- // app.js
39
+ Run
55
40
 
56
- import {app} from "primate";
57
- app.run();
41
+ ```sh
42
+ npx primate
58
43
  ```
59
44
 
60
- ### Run on Node.js
45
+ ## Table of contents
61
46
 
62
- Create a start script and enable ES modules (in `package.json`)
47
+ * [Serving content](#serving-content)
48
+ * [Routing](#routing)
49
+ * [Domains](#domains)
50
+ * [Stores](#stores)
51
+ * [Components](#components)
63
52
 
64
- ```json
65
- {
66
- "scripts": {
67
- "start": "node --experimental-json-modules app.js"
68
- },
69
- "type": "module"
70
- }
71
- ```
53
+ ## Serving content
72
54
 
73
- Install Primate
55
+ Create a file in `routes` that exports a default function
74
56
 
75
- ```sh
76
- $ npm install primate
77
- ```
57
+ ### Plain text
78
58
 
79
- Run app
59
+ ```js
60
+ export default router => {
61
+ // strings will be served as plain text
62
+ router.get("/user", () => "Donald");
63
+ };
80
64
 
81
- ```sh
82
- $ npm start
83
65
  ```
84
66
 
85
- ### Run on Deno
67
+ ### JSON
68
+
69
+ ```js
70
+ import {File} from "runtime-compat/filesystem";
86
71
 
87
- Create an import map file (`import-map.json`)
72
+ export default router => {
73
+ // any proper JavaScript object will be served as JSON
74
+ router.get("/users", () => [
75
+ {name: "Donald"},
76
+ {name: "Ryan"},
77
+ ]);
78
+
79
+ // load from a file and serve as JSON
80
+ router.get("/users-from-file", () => File.json("users.json"));
81
+ };
88
82
 
89
- ```json
90
- {
91
- "imports": {
92
- "runtime-compat": "https://deno.land/x/runtime_compat/exports.js",
93
- "primate": "https:/deno.land/x/primate/exports.js"
94
- }
95
- }
96
83
  ```
97
84
 
98
- Run app
85
+ ### Streams
99
86
 
100
- ```
101
- deno run --import-map=import-map.json app.js
102
- ```
87
+ ```js
88
+ import {File} from "runtime-compat/filesystem";
103
89
 
104
- You will typically need the `allow-read`, `allow-write` and `allow-net`
105
- permissions.
90
+ export default router => {
91
+ // `File` implements `readable`, which is a ReadableStream
92
+ router.get("/users", () => new File("users.json"));
93
+ };
106
94
 
107
- ## Table of Contents
95
+ ```
108
96
 
109
- * [Routes](#routes)
110
- * [Handlers](#handlers)
111
- * [Components](#components)
97
+ ### HTML
112
98
 
113
- ## Routes
99
+ Create an HTML component in `components/user-index.html`
114
100
 
115
- Create routes in the `routes` directory by importing and using the `router`
116
- singleton. You can group your routes across several files or keep them
117
- in one file.
101
+ ```html
102
+ <div for="${users}">
103
+ User ${name}.
104
+ Email ${email}.
105
+ </div>
118
106
 
119
- ### `router[get|post](pathname, request => ...)`
107
+ ```
120
108
 
121
- Routes are tied to a pathname and execute their callback when the pathname is
122
- encountered.
109
+ Serve the component in your route
123
110
 
124
111
  ```js
125
- // in routes/some-file.js
126
- import {router, json} from "primate";
112
+ import html from "@primate/html";
113
+
114
+ export default router => {
115
+ // the HTML tagged template handler loads a component from the `components`
116
+ // directory and serves it as HTML, passing any given data as attributes
117
+ router.get("/users", () => {
118
+ const users = [
119
+ {name: "Donald", email: "donald@the.duck"},
120
+ {name: "Joe", email: "joe@was.absent"},
121
+ ];
122
+ return html`<user-index users="${users}" />`;
123
+ });
124
+ };
127
125
 
128
- // on matching the exact pathname /, returns {"foo": "bar"} as JSON
129
- router.get("/", () => json`${{"foo": "bar"}}`);
130
126
  ```
131
127
 
132
- All routes must return a template function handler. See the
133
- [section on handlers for common handlers](#handlers).
128
+ ## Routing
134
129
 
135
- The callback has one parameter, the request data.
130
+ Routes map requests to responses. Routes are loaded from `routes`.
136
131
 
137
- ### The `request` object
132
+ The order in which routes are declared is irrelevant. Redeclaring a route
133
+ (same pathname and same HTTP verb) throws a `RouteError`.
138
134
 
139
- The request contains the `path`, a `/` separated array of the pathname.
135
+ ### Basic GET route
140
136
 
141
137
  ```js
142
- import {router, html} from "primate";
138
+ import html from "@primate/html";
143
139
 
144
- router.get("/site/login", request => json`${{"path": request.path}}`);
145
- // accessing /site/login -> {"path":["site","login"]}
146
- ```
147
-
148
- The HTTP request's body is available under `request.payload`.
140
+ export default router => {
141
+ // accessing /site/login will serve the contents of
142
+ // `components/site-login.html` as HTML
143
+ router.get("/site/login", () => html`<site-login />`);
144
+ };
149
145
 
150
- ### Regular expressions in routes
146
+ ```
151
147
 
152
- All routes are treated as regular expressions.
148
+ ### Working with the request path
153
149
 
154
150
  ```js
155
- import {router, json} from "primate";
151
+ export default router => {
152
+ // accessing /site/login will serve `["site", "login"]` as JSON
153
+ router.get("/site/login", request => request.path);
154
+ };
156
155
 
157
- router.get("/user/view/([0-9])+", request => json`${{"path": request.path}}`);
158
- // accessing /user/view/1234 -> {"path":["site","login","1234"]}
159
- // accessing /user/view/abcd -> error 404
160
156
  ```
161
157
 
162
- ### `router.alias(from, to)`
163
-
164
- To reuse certain parts of a pathname, you can define aliases which will be
165
- applied before matching.
158
+ ### Regular expressions
166
159
 
167
160
  ```js
168
- import {router, json} from "primate";
161
+ export default router => {
162
+ // accessing /user/view/1234 will serve `1234` as plain text
163
+ // accessing /user/view/abcd will show a 404 error
164
+ router.get("/user/view/([0-9])+", request => request[2]);
165
+ };
169
166
 
170
- router.alias("_id", "([0-9])+");
167
+ ```
171
168
 
172
- router.get("/user/view/_id", request => json`${{"path": request.path}}`);
169
+ ### Named groups
173
170
 
174
- router.get("/user/edit/_id", request => ...);
175
- ```
171
+ ```js
172
+ export default router => {
173
+ // named groups are mapped to properties of `request.named`
174
+ // accessing /user/view/1234 will serve `1234` as plain text
175
+ router.get("/user/view/(?<_id>[0-9])+", ({named}) => named._id);
176
+ };
176
177
 
177
- ### `router.map(pathname, request => ...)`
178
+ ```
178
179
 
179
- You can reuse functionality across the same path but different HTTP verbs. This
180
- function has the same signature as `router[get|post]`.
180
+ ### Aliasing
181
181
 
182
182
  ```js
183
- import {router, html, redirect} from "primate";
183
+ export default router => {
184
+ // will replace `"_id"` in any path with `"([0-9])+"`
185
+ router.alias("_id", "([0-9])+");
184
186
 
185
- router.alias("_id", "([0-9])+");
187
+ // equivalent to `router.get("/user/view/([0-9])+", ...)`
188
+ // will return id if matched, 404 otherwise
189
+ router.get("/user/view/_id", request => request.path[2]);
186
190
 
187
- router.map("/user/edit/_id", request => {
188
- const user = {"name": "Donald"};
189
- // return original request and user
190
- return {...request, user};
191
- });
191
+ // can be combined with named groups
192
+ router.alias("_name", "(?<name>[a-z])+");
192
193
 
193
- router.get("/user/edit/_id", request => {
194
- // show user edit form
195
- return html`<user-edit user="${request.user}" />`
196
- });
194
+ // will return name if matched, 404 otherwise
195
+ router.get("/user/view/_name", request => request.named.name);
196
+ };
197
197
 
198
- router.post("/user/edit/_id", request => {
199
- const {user} = request;
200
- // verify form and save / show errors
201
- return await user.save()
202
- ? redirect`/users`
203
- : html`<user-edit user="${user}" />`
204
- });
205
198
  ```
206
199
 
207
- ## Handlers
200
+ ### Sharing logic across HTTP verbs
208
201
 
209
- Handlers are tagged template functions usually associated with data.
202
+ ```js
203
+ import html from "@primate/html";
204
+ import redirect from "@primate/redirect";
210
205
 
211
- ### ``html`<component-name attribute="${value}" />` ``
206
+ export default router => {
207
+ // declare `"edit-user"` as alias of `"/user/edit/([0-9])+"`
208
+ router.alias("edit-user", "/user/edit/([0-9])+");
212
209
 
213
- Compiles and serves a component from the `components` directory and with the
214
- specified attributes and their values. Returns an HTTP 200 response with the
215
- `text/html` content type.
210
+ // pass user instead of request to all verbs with this route
211
+ router.map("edit-user", () => ({name: "Donald"}));
216
212
 
217
- ### ``json`${{data}}` ``
213
+ // show user edit form
214
+ router.get("edit-user", user => html`<user-edit user="${user}" />`);
218
215
 
219
- Serves JSON `data`. Returns an HTTP 200 response with the `application/json`
220
- content type.
216
+ // verify form and save, or show errors
217
+ router.post("edit-user", async user => await user.save()
218
+ ? redirect`/users`
219
+ : html`<user-edit user="${user}" />`);
220
+ };
221
221
 
222
- ### ``redirect`${url}` ``
222
+ ```
223
223
 
224
- Redirects to `url`. Returns an HTTP 302 response.
224
+ ## Domains
225
225
 
226
- ## Components
226
+ Domains represent a collection in a store, primarily with the class `fields`
227
+ property.
227
228
 
228
- Create HTML components in the `components` directory. Use attributes to expose
229
- passed data within your component.
229
+ ### Fields
230
230
 
231
- ```js
232
- // in routes/user.js
233
- import {router, html, redirect} from "primate";
231
+ Field types delimit acceptable values for a field.
234
232
 
235
- router.alias("_id", "([0-9])+");
233
+ ```js
234
+ import {Domain} from "@primate/domains";
235
+
236
+ // A basic domain that contains two string properies
237
+ export default class User extends Domain {
238
+ static fields = {
239
+ // a user's name must be a string
240
+ name: String,
241
+ // a user's age must be a number
242
+ age: Number,
243
+ };
244
+ }
236
245
 
237
- router.map("/user/edit/_id", request => {
238
- const user = {"name": "Donald", "email": "donald@was.here"};
239
- // return original request and user
240
- return {...request, user};
241
- });
242
246
 
243
- router.get("/user/edit/_id", request => {
244
- // show user edit form
245
- return html`<user-edit user="${request.user}" />`
246
- });
247
-
248
- router.post("/user/edit/_id", request => {
249
- const {user, payload} = request;
250
- // verify form and save / show errors
251
- // this assumes `user` has a method `save` to verify data
252
- return await user.save(payload)
253
- ? redirect`/users`
254
- : html`<user-edit user="${user}" />`
255
- });
256
247
  ```
257
248
 
258
- ```html
259
- <!-- components/edit-user.html -->
260
- <form method="post">
261
- <h1>Edit user</h1>
262
- <p>
263
- <input name="name" value="${user.name}"></textarea>
264
- </p>
265
- <p>
266
- <input name="email" value="${user.email}"></textarea>
267
- </p>
268
- <input type="submit" value="Save user" />
269
- </form>
270
- ```
249
+ ### Short notation
271
250
 
272
- ### Grouping objects with `for`
251
+ Field types may be any constructible JavaScript object, including other
252
+ domains. When using other domains as types, data integrity (on saving) is
253
+ ensured.
273
254
 
274
- You can use the special attribute `for` to group objects.
255
+ ```js
256
+ import {Domain} from "@primate/domains";
257
+ import House from "./House.js";
258
+
259
+ export default class User extends Domain {
260
+ static fields = {
261
+ // a user's name must be a string
262
+ name: String,
263
+ // a user's age must be a number
264
+ age: Number,
265
+ // a user's house must have the foreign id of a house record
266
+ house_id: House,
267
+ };
268
+ }
275
269
 
276
- ```html
277
- <!-- components/edit-user.html -->
278
- <form for="${user}" method="post">
279
- <h1>Edit user</h1>
280
- <p>
281
- <input name="name" value="${name}" />
282
- </p>
283
- <p>
284
- <input name="email" value="${email}" />
285
- </p>
286
- <input type="submit" value="Save user" />
287
- </form>
288
270
  ```
289
271
 
290
- ### Expanding arrays
272
+ ### Predicates
291
273
 
292
- `for` can also be used to expand arrays.
274
+ Field types may also be specified as an array, to specify additional predicates
275
+ aside from the type.
293
276
 
294
277
  ```js
295
- // in routes/user.js
296
- import {router, html} from "primate";
297
-
298
- router.get("/users", request => {
299
- const users = [
300
- {"name": "Donald", "email": "donald@was.here"},
301
- {"name": "Ryan", "email": "ryan@was.here"},
302
- ];
303
- return html`<user-index users="${users}" />`;
304
- });
305
- ```
278
+ import {Domain} from "@primate/domains";
279
+ import House from "./House.js";
280
+
281
+ export default class User extends Domain {
282
+ static fields = {
283
+ // a user's name must be a string and unique across the user collection
284
+ name: [String, "unique"],
285
+ // a user's age must be a positive integer
286
+ age: [Number, "integer", "positive"],
287
+ // a user's house must have the foreign id of a house record and no two
288
+ // users may have the same house
289
+ house_id: [House, "unique"],
290
+ };
291
+ }
306
292
 
307
- ```html
308
- <!-- in components/user-index.html -->
309
- <div for="${users}">
310
- User <span value="${name}"></span>
311
- Email <span value="${email}"></span>
312
- </div>
313
293
  ```
314
294
 
315
- ## Resources
295
+ ## Stores
316
296
 
317
- * [Getting started guide][getting-started]
297
+ Stores interface data. Primate comes with volatile in-memory store used as a
298
+ default. Other stores can be imported as modules.
318
299
 
319
- ## License
300
+ Stores are loaded from `stores`.
320
301
 
321
- BSD-3-Clause
302
+ ### Resources
303
+
304
+ * Website: https://primatejs.com
305
+ * IRC: Join the `#primate` channel on `irc.libera.chat`.
306
+
307
+ ## License
322
308
 
323
- [getting-started]: https://primatejs.com/getting-started
324
- [source-code]: https://github.com/primatejs/primate
325
- [issues]: https://github.com/primatejs/primate/issues
326
- [primate-file-store]: https://npmjs.com/primate-file-store
327
- [primate-json-store]: https://npmjs.com/primate-json-store
328
- [primate-mongodb-store]: https://npmjs.com/primate-mongodb-store
309
+ MIT