h1v3 0.1.0 → 0.2.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/README.md +27 -0
- package/event-stores.md +238 -0
- package/package.json +2 -2
- package/web.md +0 -0
package/README.md
CHANGED
|
@@ -1,2 +1,29 @@
|
|
|
1
1
|
# h1v3
|
|
2
2
|
A firebase web platform
|
|
3
|
+
|
|
4
|
+
# Install
|
|
5
|
+
|
|
6
|
+
```
|
|
7
|
+
npm install h1v3
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
# Event stores
|
|
11
|
+
Built on Firebase Real-time Database allowing simple projections updated when events are written. See [event-stores.md](event-stores.md).
|
|
12
|
+
|
|
13
|
+
# Web platform
|
|
14
|
+
The web platform covers the following (headless) functionality:
|
|
15
|
+
- message bus
|
|
16
|
+
- login
|
|
17
|
+
- firebase initialisation
|
|
18
|
+
|
|
19
|
+
You can install the web platform using `npx h1v3 vendor-web`
|
|
20
|
+
|
|
21
|
+
**Note** if using the Web UI platform, the web platform will be installed and used by default
|
|
22
|
+
|
|
23
|
+
# Web UI platform
|
|
24
|
+
The following components build on the headless web platform:
|
|
25
|
+
- login
|
|
26
|
+
- error notifications
|
|
27
|
+
- Styling and registration of Web Awesome components
|
|
28
|
+
|
|
29
|
+
You can install the web platform UI (and headlesss) platform using `npx h1v3 vendor-webui`
|
package/event-stores.md
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
[Home](README.md)
|
|
2
|
+
|
|
3
|
+
# Event store architecture
|
|
4
|
+
Events are written to `/events` and must contain a `type` property (string)
|
|
5
|
+
Projections are read from `/projections/the_projection_nmae`
|
|
6
|
+
|
|
7
|
+
When an event is written, a trigger is fired which updates all the defined projections. This means that an updated projection may take some time to synchronise with the written events. Typically this is a few ms, but code patterns need to follow an asynchronous pattern. Although it is possible to fetch a snapshot of a projection using the `getProjection` method of the event store client, it is preferred to subscribe using the `onProjectionValue` callback.
|
|
8
|
+
|
|
9
|
+
# Identify event sourced collections using conviguration
|
|
10
|
+
|
|
11
|
+
e.g. Below we configure three event stores:
|
|
12
|
+
|
|
13
|
+
`user_profile`: A store of events at /h1v3_dev/users/$uid/profile with a single projection "current". Read and Write are both only if $uid == auth.uid.
|
|
14
|
+
|
|
15
|
+
`teams_data_docs`: at /h1v3_dev/teams/$tid/data/$did - storing events pertaining to a particular document for a particular team. Both read and write are determined by if my user id is present in the "members" projection of the teams_membership event store.
|
|
16
|
+
|
|
17
|
+
`teams_membership`: at /h1v3_dev/teams/$tid/membership - stores events pertaining to team membership. There are two projections: "members" (a flat list of user ids) and "details" (which holds details of the role within a team - admin, owner etc.)
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
import { onValueWritten } from "firebase-functions/database";
|
|
21
|
+
import { logger } from "firebase-functions";
|
|
22
|
+
import { configure, ifUserIdExists, passThroughView } from "h1v3";
|
|
23
|
+
|
|
24
|
+
import { membershipDetails, members } from "./storage-dev-sample-projections/team-membership.js";
|
|
25
|
+
import { current } from "./storage-dev-sample-projections/user-profile.js";
|
|
26
|
+
|
|
27
|
+
const ifMyTeam = ifUserIdExists("/h1v3_dev/teams/$tid/membership/projections/members");
|
|
28
|
+
const ifAdminInThisTeam = ifUserIdExists("/h1v3_dev/teams/$tid/membership/projections/details/admins");
|
|
29
|
+
const ifMe = "$uid == auth.uid";
|
|
30
|
+
|
|
31
|
+
const stores = {
|
|
32
|
+
|
|
33
|
+
user_profile: {
|
|
34
|
+
|
|
35
|
+
ref: "/h1v3_dev/users/$uid/profile",
|
|
36
|
+
projections: {
|
|
37
|
+
current
|
|
38
|
+
},
|
|
39
|
+
write: ifMe,
|
|
40
|
+
read: ifMe
|
|
41
|
+
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
teams_membership: {
|
|
45
|
+
ref: "/h1v3_dev/teams/$tid/membership",
|
|
46
|
+
projections: {
|
|
47
|
+
"details": membershipDetails,
|
|
48
|
+
members
|
|
49
|
+
},
|
|
50
|
+
write: ifAdminInThisTeam,
|
|
51
|
+
read: ifAdminInThisTeam
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
teams_data_docs: {
|
|
55
|
+
|
|
56
|
+
ref: "/h1v3_dev/teams/$tid/data/$did",
|
|
57
|
+
projections: {
|
|
58
|
+
"all": passThroughView
|
|
59
|
+
},
|
|
60
|
+
write: ifMyTeam,
|
|
61
|
+
read: ifMyTeam
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const user_profile = configure(stores.user_profile, onValueWritten, logger);
|
|
68
|
+
export const teams_membership = configure(stores.teams_membership, onValueWritten, logger);
|
|
69
|
+
export const teams_data_docs = configure(stores.teams_data_docs, onValueWritten, logger);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
# Generate database rules
|
|
73
|
+
|
|
74
|
+
Once you have configured your event stores, you can generate Firebase database rules like so:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
npx h1v3 rules --snippet
|
|
78
|
+
|
|
79
|
+
"h1v3_dev": {
|
|
80
|
+
"teams": {
|
|
81
|
+
"$tid": {
|
|
82
|
+
"data": {
|
|
83
|
+
"$did": {
|
|
84
|
+
".read": false,
|
|
85
|
+
".write": false,
|
|
86
|
+
"events": {
|
|
87
|
+
"$eid": {
|
|
88
|
+
".write": "!data.exists() && (root.child('/h1v3_dev/teams/' + $tid + '/membership/projections/members/' + auth.uid).exists())",
|
|
89
|
+
".validate": "newData.child('type').isString()"
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"projections": {
|
|
93
|
+
"$pid": {
|
|
94
|
+
".read": "root.child('/h1v3_dev/teams/' + $tid + '/membership/projections/members/' + auth.uid).exists()"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"membership": {
|
|
100
|
+
".read": false,
|
|
101
|
+
".write": false,
|
|
102
|
+
"events": {
|
|
103
|
+
"$eid": {
|
|
104
|
+
".write": "!data.exists() && (root.child('/h1v3_dev/teams/' + $tid + '/membership/projections/details/admins/' + auth.uid).exists())",
|
|
105
|
+
".validate": "newData.child('type').isString()"
|
|
106
|
+
}
|
|
107
|
+
},
|
|
108
|
+
"projections": {
|
|
109
|
+
"$pid": {
|
|
110
|
+
".read": "root.child('/h1v3_dev/teams/' + $tid + '/membership/projections/details/admins/' + auth.uid).exists()"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"users": {
|
|
117
|
+
"$uid": {
|
|
118
|
+
"profile": {
|
|
119
|
+
".read": false,
|
|
120
|
+
".write": false,
|
|
121
|
+
"events": {
|
|
122
|
+
"$eid": {
|
|
123
|
+
".write": "!data.exists() && ($uid == auth.uid)",
|
|
124
|
+
".validate": "newData.child('type').isString()"
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
"projections": {
|
|
128
|
+
"$pid": {
|
|
129
|
+
".read": "$uid == auth.uid"
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
# Server-side event store access
|
|
139
|
+
|
|
140
|
+
h1v3 provides a server-side client library compatible with Firebase's admin-sdk for node.js. Below is an example where we record an event "DETAILS_UPDATED" against the user profile event store for the `testAlice` user:
|
|
141
|
+
|
|
142
|
+
```javascript
|
|
143
|
+
import { initializeApp } from "firebase-admin/app";
|
|
144
|
+
import { getAuth } from "firebase-admin/auth";
|
|
145
|
+
import { getDatabase } from "firebase-admin/database";
|
|
146
|
+
|
|
147
|
+
import { node } from "h1v3/client";
|
|
148
|
+
|
|
149
|
+
const app = initializeApp();
|
|
150
|
+
|
|
151
|
+
const auth = getAuth(app);
|
|
152
|
+
const testAlice = await auth.createUser({
|
|
153
|
+
uid: "test-alice"
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const db = getDatabase();
|
|
157
|
+
const store = node.eventStore(db, `/h1v3_dev/users/${testAlice.uid}/profile`);
|
|
158
|
+
await store.record(
|
|
159
|
+
"DETAILS_UPDATED",
|
|
160
|
+
{
|
|
161
|
+
name: "Alice",
|
|
162
|
+
favourite: {
|
|
163
|
+
color: "pink"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// note - may be stale, consider using `onProjectionValue` instead
|
|
169
|
+
const currentProfile = await store.getProjection("current");
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
# Client-side event store access
|
|
173
|
+
|
|
174
|
+
Similarly, the client-side event store provides the same access but using h1v3's exposed Firebase instance. You can install the client-side javascript components using `npx h1v3 vendor-eventstore` and `npx h1v3 vendor-web`.
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npx h1v3 vendor-eventstore # ./public/vendor/h1v3@1.2.3/event-store
|
|
178
|
+
npx h1v3 vendor-web # ./public/vendor/h1v3@1.2.3/web
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
<script type="module">
|
|
183
|
+
|
|
184
|
+
import { eventStore } from "/vendor/h1v3@1.2.3/event-store/modular.js";
|
|
185
|
+
import { firebase } from "/vendor/h1v3@1.2.3/web/system.js";
|
|
186
|
+
|
|
187
|
+
const store = eventStore(firebase.database, eventStorePathValue);
|
|
188
|
+
await store.record(
|
|
189
|
+
"DETAILS_UPDATED",
|
|
190
|
+
{
|
|
191
|
+
name: "Alice",
|
|
192
|
+
favourite: {
|
|
193
|
+
color: "pink"
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// note - may be stale, consider using `onProjectionValue` instead
|
|
199
|
+
const currentProfile = await store.getProjection("current");
|
|
200
|
+
</script>
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
# Creating projections
|
|
204
|
+
Projections need a method which can apply an event to a view in event written order. For example, the user profile projection "current" could be defined as follows:
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
```javascript
|
|
208
|
+
import { deleteByPath, filterBySchema, schemaAllowsPath } from "h1v3/schema";
|
|
209
|
+
|
|
210
|
+
const schema = {
|
|
211
|
+
"name": String,
|
|
212
|
+
"age": Number,
|
|
213
|
+
"favourite": {
|
|
214
|
+
"color": String
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
export const current = {
|
|
219
|
+
|
|
220
|
+
"DETAILS_UPDATED": (view, { payload }) => {
|
|
221
|
+
|
|
222
|
+
if (!payload || typeof payload !== "object") return view;
|
|
223
|
+
view = Object.assign(view, filterBySchema(schema, payload));
|
|
224
|
+
if(payload?.unset)
|
|
225
|
+
payload.unset.forEach(path => {
|
|
226
|
+
|
|
227
|
+
if (schemaAllowsPath(schema, path))
|
|
228
|
+
deleteByPath(view, path);
|
|
229
|
+
else
|
|
230
|
+
console.warn("Unset path not allowed by schema", path);
|
|
231
|
+
|
|
232
|
+
});
|
|
233
|
+
return view;
|
|
234
|
+
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
};
|
|
238
|
+
```
|
package/package.json
CHANGED
package/web.md
ADDED
|
File without changes
|