primate 0.8.0 → 0.9.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 (68) hide show
  1. package/LICENSE +17 -23
  2. package/README.md +202 -210
  3. package/README.template.md +181 -0
  4. package/bin/primate.js +5 -0
  5. package/exports.js +3 -26
  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 -34
  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 -29
  44. package/source/handlers/http.js +0 -6
  45. package/source/handlers/json.js +0 -6
  46. package/source/handlers/redirect.js +0 -10
  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,324 +1,314 @@
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
86
68
 
87
- Create an import map file (`import-map.json`)
69
+ ```js
70
+ import {File} from "runtime-compat/filesystem";
88
71
 
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
- ```
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
+ ]);
97
78
 
98
- Run app
79
+ // load from a file and serve as JSON
80
+ router.get("/users-from-file", () => File.json("users.json"));
81
+ };
99
82
 
100
- ```
101
- deno run --import-map=import-map.json app.js
102
83
  ```
103
84
 
104
- You will typically need the `allow-read`, `allow-write` and `allow-net`
105
- permissions.
85
+ ### Streams
106
86
 
107
- ## Table of Contents
87
+ ```js
88
+ import {File} from "runtime-compat/filesystem";
108
89
 
109
- * [Routes](#routes)
110
- * [Handlers](#handlers)
111
- * [Components](#components)
90
+ export default router => {
91
+ // `File` implements `readable`, which is a ReadableStream
92
+ router.get("/users", () => new File("users.json"));
93
+ };
94
+
95
+ ```
112
96
 
113
- ## Routes
97
+ ### HTML
114
98
 
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.
99
+ Create an HTML component in `components/user-index.html`
118
100
 
119
- ### `router[get|post](pathname, request => ...)`
101
+ ```html
102
+ <div for="${users}">
103
+ User ${name}.
104
+ Email ${email}.
105
+ </div>
120
106
 
121
- Routes are tied to a pathname and execute their callback when the pathname is
122
- encountered.
107
+ ```
108
+
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. All 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";
139
+
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
+ };
143
145
 
144
- router.get("/site/login", request => json`${{"path": request.path}}`);
145
- // accessing /site/login -> {"path":["site","login"]}
146
146
  ```
147
147
 
148
- The HTTP request's body is available under `request.payload`.
148
+ ### Working with the request path
149
+
150
+ ```js
151
+ export default router => {
152
+ // accessing /site/login will serve `["site", "login"]` as JSON
153
+ router.get("/site/login", request => request.path);
154
+ };
149
155
 
150
- ### Regular expressions in routes
156
+ ```
151
157
 
152
- All routes are treated as regular expressions.
158
+ ### Regular expressions
153
159
 
154
160
  ```js
155
- 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
+ };
156
166
 
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
167
  ```
161
168
 
162
- ### `router.alias(from, to)`
169
+ ### Named groups
170
+
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
+ };
177
+
178
+ ```
163
179
 
164
- To reuse certain parts of a pathname, you can define aliases which will be
165
- applied before matching.
180
+ ### Aliasing
166
181
 
167
182
  ```js
168
- import {router, json} from "primate";
183
+ export default router => {
184
+ // will replace `"_id"` in any path with `"([0-9])+"`
185
+ router.alias("_id", "([0-9])+");
169
186
 
170
- 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]);
171
190
 
172
- router.get("/user/view/_id", request => json`${{"path": request.path}}`);
191
+ // can be combined with named groups
192
+ router.alias("_name", "(<name>[a-z])+");
173
193
 
174
- router.get("/user/edit/_id", request => ...);
175
- ```
194
+ // will return name if matched, 404 otherwise
195
+ router.get("/user/view/_name", request => request.named.name);
196
+ };
176
197
 
177
- ### `router.map(pathname, request => ...)`
198
+ ```
178
199
 
179
- You can reuse functionality across the same path but different HTTP verbs. This
180
- function has the same signature as `router[get|post]`.
200
+ ### Sharing logic across HTTP verbs
181
201
 
182
202
  ```js
183
- import {router, html, redirect} from "primate";
203
+ import html from "@primate/html";
204
+ import redirect from "@primate/redirect";
184
205
 
185
- router.alias("_id", "([0-9])+");
206
+ export default router => {
207
+ // declare `"edit-user"` as alias of `"/user/edit/([0-9])+"`
208
+ router.alias("edit-user", "/user/edit/([0-9])+");
186
209
 
187
- router.map("/user/edit/_id", request => {
188
- const user = {"name": "Donald"};
189
- // return original request and user
190
- return {...request, user};
191
- });
210
+ // pass user instead of request to all verbs with this route
211
+ router.map("edit-user", () => ({name: "Donald"}));
192
212
 
193
- router.get("/user/edit/_id", request => {
194
213
  // show user edit form
195
- return html`<user-edit user="${request.user}" />`
196
- });
214
+ router.get("edit-user", user => html`<user-edit user="${user}" />`);
197
215
 
198
- router.post("/user/edit/_id", request => {
199
- const {user} = request;
200
- // verify form and save / show errors
201
- return await user.save()
216
+ // verify form and save, or show errors
217
+ router.post("edit-user", async user => await user.save()
202
218
  ? redirect`/users`
203
- : html`<user-edit user="${user}" />`
204
- });
219
+ : html`<user-edit user="${user}" />`);
220
+ };
221
+
205
222
  ```
206
223
 
207
- ## Handlers
224
+ ## Domains
208
225
 
209
- Handlers are tagged template functions usually associated with data.
226
+ Domains represent a collection in a store. All domains are loaded from
227
+ `domains`.
210
228
 
211
- ### ``html`<component-name attribute="${value}" />` ``
229
+ A collection is primarily described using the class `fields` property.
212
230
 
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.
231
+ ### Fields
216
232
 
217
- ### ``json`${{data}}` ``
233
+ Field types delimit acceptable values for a field.
218
234
 
219
- Serves JSON `data`. Returns an HTTP 200 response with the `application/json`
220
- content type.
235
+ ```js
236
+ import {Domain} from "@primate/domains";
237
+
238
+ // A basic domain that contains two string properies
239
+ export default class User extends Domain {
240
+ static fields = {
241
+ // a user's name must be a string
242
+ name: String,
243
+ // a user's age must be a number
244
+ age: Number,
245
+ };
246
+ }
221
247
 
222
- ### ``redirect`${url}` ``
223
248
 
224
- Redirects to `url`. Returns an HTTP 302 response.
249
+ ```
225
250
 
226
- ## Components
251
+ ### Short notation
227
252
 
228
- Create HTML components in the `components` directory. Use attributes to expose
229
- passed data within your component.
253
+ Field types may be any constructible JavaScript object, including other
254
+ domains. When using other domains as types, data integrity (on saving) is
255
+ ensured.
230
256
 
231
257
  ```js
232
- // in routes/user.js
233
- import {router, html, redirect} from "primate";
234
-
235
- router.alias("_id", "([0-9])+");
236
-
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
- });
258
+ import {Domain} from "@primate/domains";
259
+ import House from "./House.js";
260
+
261
+ export default class User extends Domain {
262
+ static fields = {
263
+ // a user's name must be a string
264
+ name: String,
265
+ // a user's age must be a number
266
+ age: Number,
267
+ // a user's house must have the foreign id of a house record
268
+ house_id: House,
269
+ };
270
+ }
242
271
 
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
272
  ```
257
273
 
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
- ```
274
+ ### Predicates
271
275
 
272
- ### Grouping objects with `for`
276
+ Field types may also be specified as an array, to specify additional predicates
277
+ aside from the type.
273
278
 
274
- You can use the special attribute `for` to group objects.
279
+ ```js
280
+ import {Domain} from "@primate/domains";
281
+ import House from "./House.js";
282
+
283
+ export default class User extends Domain {
284
+ static fields = {
285
+ // a user's name must be a string and unique across the user collection
286
+ name: [String, "unique"],
287
+ // a user's age must be a positive integer
288
+ age: [Number, "integer", "positive"],
289
+ // a user's house must have the foreign id of a house record and no two
290
+ // users may have the same house
291
+ house_id: [House, "unique"],
292
+ };
293
+ }
275
294
 
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
295
  ```
289
296
 
290
- ### Expanding arrays
297
+ ## Stores
291
298
 
292
- `for` can also be used to expand arrays.
299
+ Stores interface data. Primate comes with volatile in-memory store used as a
300
+ default. Other stores can be imported as modules.
293
301
 
294
- ```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
- ```
306
-
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
- ```
302
+ All stores are loaded from `stores`.
314
303
 
315
- ## Resources
304
+ ### Resources
316
305
 
317
- * [Getting started guide][getting-started]
306
+ * Website: https://primatejs.com
307
+ * IRC: Join the `#primate` channel on `irc.libera.chat`.
318
308
 
319
309
  ## License
320
310
 
321
- BSD-3-Clause
311
+ MIT
322
312
 
323
313
  [getting-started]: https://primatejs.com/getting-started
324
314
  [source-code]: https://github.com/primatejs/primate
@@ -326,3 +316,5 @@ BSD-3-Clause
326
316
  [primate-file-store]: https://npmjs.com/primate-file-store
327
317
  [primate-json-store]: https://npmjs.com/primate-json-store
328
318
  [primate-mongodb-store]: https://npmjs.com/primate-mongodb-store
319
+ [primate-react]: https://github.com/primatejs/primate-react
320
+ [primate-vue]: https://github.com/primatejs/primate-vue