h1v3 0.1.1 → 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 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`
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "h1v3",
3
- "version": "0.1.1",
3
+ "version": "0.2.1",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "author": "",
package/web.md ADDED
File without changes