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.
- package/LICENSE +17 -23
- package/README.md +199 -218
- package/README.template.md +170 -0
- package/bin/primate.js +5 -0
- package/exports.js +3 -29
- package/html.js +13 -0
- package/module.json +10 -0
- package/package.json +14 -16
- package/{source → src}/Bundler.js +2 -2
- package/{source → src}/cache.js +0 -0
- package/src/conf.js +25 -0
- package/{source → src}/errors/InternalServer.js +0 -0
- package/{source → src}/errors/Predicate.js +0 -0
- package/src/errors/Route.js +1 -0
- package/{source → src}/errors.js +0 -0
- package/{source/extend_object.js → src/extend.js} +3 -3
- package/src/extend.spec.js +111 -0
- package/src/handlers/http.js +1 -0
- package/src/handlers/http404.js +7 -0
- package/src/handlers/json.js +7 -0
- package/src/handlers/stream.js +7 -0
- package/src/handlers/text.js +14 -0
- package/{source → src}/http-codes.json +0 -0
- package/src/log.js +22 -0
- package/{source → src}/mimes.json +0 -0
- package/{source → src}/preset/primate.json +0 -0
- package/{source → src}/preset/stores/default.js +0 -0
- package/src/route.js +61 -0
- package/src/run.js +26 -0
- package/src/serve.js +90 -0
- package/source/App.js +0 -35
- package/source/EagerPromise.js +0 -49
- package/source/Router.js +0 -31
- package/source/Server.js +0 -93
- package/source/Session.js +0 -26
- package/source/attributes.js +0 -14
- package/source/conf.js +0 -26
- package/source/domain/Domain.js +0 -285
- package/source/domain/Field.js +0 -113
- package/source/domain/Predicate.js +0 -24
- package/source/handlers/DOM/Node.js +0 -179
- package/source/handlers/DOM/Parser.js +0 -135
- package/source/handlers/html.js +0 -28
- package/source/handlers/http.js +0 -8
- package/source/handlers/json.js +0 -6
- package/source/handlers/redirect.js +0 -14
- package/source/invariants.js +0 -36
- package/source/map_entries.js +0 -6
- package/source/sanitize.js +0 -8
- package/source/store/Memory.js +0 -60
- package/source/store/Store.js +0 -30
- package/source/types/Array.js +0 -33
- package/source/types/Boolean.js +0 -29
- package/source/types/Date.js +0 -20
- package/source/types/Domain.js +0 -11
- package/source/types/Instance.js +0 -8
- package/source/types/Number.js +0 -45
- package/source/types/Object.js +0 -12
- package/source/types/Primitive.js +0 -7
- package/source/types/Storeable.js +0 -44
- package/source/types/String.js +0 -49
- package/source/types/errors/Array.json +0 -7
- package/source/types/errors/Boolean.json +0 -5
- package/source/types/errors/Date.json +0 -3
- package/source/types/errors/Number.json +0 -9
- package/source/types/errors/Object.json +0 -3
- package/source/types/errors/String.json +0 -11
- package/source/types.js +0 -6
package/LICENSE
CHANGED
|
@@ -1,27 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
reserved.
|
|
1
|
+
Primate JavaScript Framework
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
modification, are permitted provided that the following conditions are met:
|
|
3
|
+
Copyright (c) Terrablue <terrablue@proton.me> and contributors.
|
|
6
4
|
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
11
|
-
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
Lay out your app
|
|
7
|
+
Lay out app
|
|
24
8
|
|
|
25
9
|
```sh
|
|
26
|
-
|
|
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
|
-
|
|
17
|
+
import html from "@primate/html";
|
|
33
18
|
|
|
34
|
-
|
|
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
|
|
25
|
+
Create a component in `components/site-index.html`
|
|
40
26
|
|
|
41
27
|
```html
|
|
42
|
-
Today's date is
|
|
28
|
+
Today's date is ${date}.
|
|
29
|
+
|
|
43
30
|
```
|
|
44
31
|
|
|
45
|
-
Generate SSL
|
|
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
|
-
|
|
37
|
+
```
|
|
52
38
|
|
|
53
|
-
|
|
54
|
-
// app.js
|
|
39
|
+
Run
|
|
55
40
|
|
|
56
|
-
|
|
57
|
-
|
|
41
|
+
```sh
|
|
42
|
+
npx primate
|
|
58
43
|
```
|
|
59
44
|
|
|
60
|
-
|
|
45
|
+
## Table of contents
|
|
61
46
|
|
|
62
|
-
|
|
47
|
+
* [Serving content](#serving-content)
|
|
48
|
+
* [Routing](#routing)
|
|
49
|
+
* [Domains](#domains)
|
|
50
|
+
* [Stores](#stores)
|
|
51
|
+
* [Components](#components)
|
|
63
52
|
|
|
64
|
-
|
|
65
|
-
{
|
|
66
|
-
"scripts": {
|
|
67
|
-
"start": "node --experimental-json-modules app.js"
|
|
68
|
-
},
|
|
69
|
-
"type": "module"
|
|
70
|
-
}
|
|
71
|
-
```
|
|
53
|
+
## Serving content
|
|
72
54
|
|
|
73
|
-
|
|
55
|
+
Create a file in `routes` that exports a default function
|
|
74
56
|
|
|
75
|
-
|
|
76
|
-
$ npm install primate
|
|
77
|
-
```
|
|
57
|
+
### Plain text
|
|
78
58
|
|
|
79
|
-
|
|
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
|
-
###
|
|
67
|
+
### JSON
|
|
68
|
+
|
|
69
|
+
```js
|
|
70
|
+
import {File} from "runtime-compat/filesystem";
|
|
86
71
|
|
|
87
|
-
|
|
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
|
-
|
|
85
|
+
### Streams
|
|
99
86
|
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
```
|
|
87
|
+
```js
|
|
88
|
+
import {File} from "runtime-compat/filesystem";
|
|
103
89
|
|
|
104
|
-
|
|
105
|
-
|
|
90
|
+
export default router => {
|
|
91
|
+
// `File` implements `readable`, which is a ReadableStream
|
|
92
|
+
router.get("/users", () => new File("users.json"));
|
|
93
|
+
};
|
|
106
94
|
|
|
107
|
-
|
|
95
|
+
```
|
|
108
96
|
|
|
109
|
-
|
|
110
|
-
* [Handlers](#handlers)
|
|
111
|
-
* [Components](#components)
|
|
97
|
+
### HTML
|
|
112
98
|
|
|
113
|
-
|
|
99
|
+
Create an HTML component in `components/user-index.html`
|
|
114
100
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
101
|
+
```html
|
|
102
|
+
<div for="${users}">
|
|
103
|
+
User ${name}.
|
|
104
|
+
Email ${email}.
|
|
105
|
+
</div>
|
|
118
106
|
|
|
119
|
-
|
|
107
|
+
```
|
|
120
108
|
|
|
121
|
-
|
|
122
|
-
encountered.
|
|
109
|
+
Serve the component in your route
|
|
123
110
|
|
|
124
111
|
```js
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
133
|
-
[section on handlers for common handlers](#handlers).
|
|
128
|
+
## Routing
|
|
134
129
|
|
|
135
|
-
|
|
130
|
+
Routes map requests to responses. Routes are loaded from `routes`.
|
|
136
131
|
|
|
137
|
-
|
|
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
|
-
|
|
135
|
+
### Basic GET route
|
|
140
136
|
|
|
141
137
|
```js
|
|
142
|
-
import
|
|
138
|
+
import html from "@primate/html";
|
|
143
139
|
|
|
144
|
-
router
|
|
145
|
-
// accessing /site/login
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
146
|
+
```
|
|
151
147
|
|
|
152
|
-
|
|
148
|
+
### Working with the request path
|
|
153
149
|
|
|
154
150
|
```js
|
|
155
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
167
|
+
```
|
|
171
168
|
|
|
172
|
-
|
|
169
|
+
### Named groups
|
|
173
170
|
|
|
174
|
-
|
|
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
|
-
|
|
178
|
+
```
|
|
178
179
|
|
|
179
|
-
|
|
180
|
-
function has the same signature as `router[get|post]`.
|
|
180
|
+
### Aliasing
|
|
181
181
|
|
|
182
182
|
```js
|
|
183
|
-
|
|
183
|
+
export default router => {
|
|
184
|
+
// will replace `"_id"` in any path with `"([0-9])+"`
|
|
185
|
+
router.alias("_id", "([0-9])+");
|
|
184
186
|
|
|
185
|
-
router.
|
|
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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
200
|
+
### Sharing logic across HTTP verbs
|
|
208
201
|
|
|
209
|
-
|
|
202
|
+
```js
|
|
203
|
+
import html from "@primate/html";
|
|
204
|
+
import redirect from "@primate/redirect";
|
|
210
205
|
|
|
211
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
213
|
+
// show user edit form
|
|
214
|
+
router.get("edit-user", user => html`<user-edit user="${user}" />`);
|
|
218
215
|
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
222
|
+
```
|
|
223
223
|
|
|
224
|
-
|
|
224
|
+
## Domains
|
|
225
225
|
|
|
226
|
-
|
|
226
|
+
Domains represent a collection in a store, primarily with the class `fields`
|
|
227
|
+
property.
|
|
227
228
|
|
|
228
|
-
|
|
229
|
-
passed data within your component.
|
|
229
|
+
### Fields
|
|
230
230
|
|
|
231
|
-
|
|
232
|
-
// in routes/user.js
|
|
233
|
-
import {router, html, redirect} from "primate";
|
|
231
|
+
Field types delimit acceptable values for a field.
|
|
234
232
|
|
|
235
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
###
|
|
272
|
+
### Predicates
|
|
291
273
|
|
|
292
|
-
|
|
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
|
-
|
|
296
|
-
import
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
##
|
|
295
|
+
## Stores
|
|
316
296
|
|
|
317
|
-
|
|
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
|
-
|
|
300
|
+
Stores are loaded from `stores`.
|
|
320
301
|
|
|
321
|
-
|
|
302
|
+
### Resources
|
|
303
|
+
|
|
304
|
+
* Website: https://primatejs.com
|
|
305
|
+
* IRC: Join the `#primate` channel on `irc.libera.chat`.
|
|
306
|
+
|
|
307
|
+
## License
|
|
322
308
|
|
|
323
|
-
|
|
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
|