h1v3 0.1.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 (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +2 -0
  3. package/dist/browser/event-store/modular.js +82 -0
  4. package/dist/browser/web/events.js +7 -0
  5. package/dist/browser/web/login.js +48 -0
  6. package/dist/browser/web/system.js +67 -0
  7. package/dist/browser/web-ui/components/login.html.js +44 -0
  8. package/dist/browser/web-ui/components/login.js +74 -0
  9. package/dist/browser/web-ui/components/notification.html.js +12 -0
  10. package/dist/browser/web-ui/components/notification.js +25 -0
  11. package/dist/browser/web-ui/components/partials/wa-utils.js +17 -0
  12. package/dist/browser/web-ui/errors.js +23 -0
  13. package/dist/browser/web-ui/system.js +20 -0
  14. package/package.json +22 -0
  15. package/scripts/dist-client.js +31 -0
  16. package/src/client/index.js +2 -0
  17. package/src/client/modular.js +82 -0
  18. package/src/client/node.js +29 -0
  19. package/src/client/web/events.js +7 -0
  20. package/src/client/web/login.js +48 -0
  21. package/src/client/web/system.js +67 -0
  22. package/src/client/web-ui/components/login.html.js +44 -0
  23. package/src/client/web-ui/components/login.js +74 -0
  24. package/src/client/web-ui/components/notification.html.js +12 -0
  25. package/src/client/web-ui/components/notification.js +25 -0
  26. package/src/client/web-ui/components/partials/wa-utils.js +17 -0
  27. package/src/client/web-ui/errors.js +23 -0
  28. package/src/client/web-ui/system.js +20 -0
  29. package/src/commands/generate-rules.js +80 -0
  30. package/src/commands/list-event-stores.js +24 -0
  31. package/src/commands/vendor.js +54 -0
  32. package/src/event-store/initialise.js +28 -0
  33. package/src/event-store/projections.js +38 -0
  34. package/src/exec-eventstore.js +65 -0
  35. package/src/index.js +18 -0
  36. package/src/load-configuration.js +21 -0
  37. package/src/schema.js +66 -0
  38. package/src/system/json.js +23 -0
  39. package/src/system/main.js +76 -0
@@ -0,0 +1,67 @@
1
+ // vendor: firebase
2
+ import { initializeApp } from "https://www.gstatic.com/firebasejs/12.3.0/firebase-app.js";
3
+ import {
4
+ connectAuthEmulator,
5
+ getAuth,
6
+ onAuthStateChanged,
7
+ signInWithPopup,
8
+ signInWithEmailAndPassword,
9
+ signOut,
10
+ GoogleAuthProvider
11
+ } from "https://www.gstatic.com/firebasejs/12.3.0/firebase-auth.js";
12
+ import {
13
+ child,
14
+ connectDatabaseEmulator,
15
+ get,
16
+ getDatabase,
17
+ off,
18
+ onValue,
19
+ ref,
20
+ set,
21
+ update,
22
+ } from "https://www.gstatic.com/firebasejs/12.3.0/firebase-database.js";
23
+
24
+ // init firebase
25
+ import firebaseConfig from "/firebase.config.js";
26
+ const app = initializeApp(firebaseConfig);
27
+ const auth = getAuth(app);
28
+ const db = getDatabase(app);
29
+
30
+ // firebase emulator support
31
+ const useEmulator = location?.hostname === "localhost" || location?.hostname === "127.0.0.1";
32
+ if (useEmulator) {
33
+
34
+ const resp = await fetch("http://127.0.0.1:4400/emulators");
35
+ const emulators = await resp.json();
36
+ connectAuthEmulator(auth, `http://${emulators.auth.host}:${emulators.auth.port}`);
37
+ connectDatabaseEmulator(db, emulators.database.host, emulators.database.port);
38
+
39
+ }
40
+
41
+ // controlled export of firebase
42
+ export const firebase = {
43
+ authentication: {
44
+ auth,
45
+ onAuthStateChanged,
46
+ signInWithEmailAndPassword,
47
+ signInWithPopup,
48
+ signOut,
49
+ GoogleAuthProvider
50
+ },
51
+ database: {
52
+ child,
53
+ db,
54
+ ref,
55
+ get,
56
+ set,
57
+ update,
58
+ onValue,
59
+ off
60
+ }
61
+ }
62
+
63
+ export const bus = document;
64
+
65
+ // h1v3
66
+ import { registerLoginHandlers } from "./login.js";
67
+ registerLoginHandlers(bus, firebase);
@@ -0,0 +1,44 @@
1
+ import { html } from "../system.js";
2
+ import { dialog } from "./partials/wa-utils.js";
3
+
4
+ export const loginButton = () => html`<wa-button class="open-login">Login</wa-button>`;
5
+
6
+ export const loginDialog = () => dialog("Login", html`
7
+ <div class="wa-stack">
8
+
9
+ <wa-input label="Email" type="email"></wa-input>
10
+ <wa-input label="Password" type="password"></wa-input>
11
+ <a href="#">Having trouble signing in?</a>
12
+ <wa-button class="login-with-email">Sign in</wa-button>
13
+ <wa-divider></wa-divider>
14
+ <p>Or sign in with:</p>
15
+ <div class="wa-grid" style="--min-column-size: 12ch;">
16
+
17
+ <wa-button appearance="outlined" class="login-with-google">
18
+
19
+ <wa-icon slot="start" name="google" family="brands"></wa-icon>
20
+ Google
21
+
22
+ </wa-button>
23
+ <wa-button appearance="outlined" disabled>
24
+
25
+ <wa-icon slot="start" name="apple" family="brands"></wa-icon>
26
+ Apple ID
27
+
28
+ </wa-button>
29
+ <wa-button appearance="outlined" disabled>
30
+
31
+ <wa-icon slot="start" name="facebook" family="brands"></wa-icon>
32
+ Facebook
33
+
34
+ </wa-button>
35
+
36
+ </div>
37
+ <p>Don't have an account? <a href="#">Create one</a></p>
38
+
39
+ </div>
40
+ `);
41
+
42
+ export const signOutButton = () => html`<wa-button class="sign-out">Sign out</wa-button>`;
43
+
44
+ export const hello = ({ displayName, email }) => html`<strong>${displayName}</strong> (${email}) `;
@@ -0,0 +1,74 @@
1
+ import { LitElement, html, bus } from "../system.js";
2
+ import { styled } from "./partials/wa-utils.js";
3
+ import { hello, loginButton, loginDialog, signOutButton } from './login.html.js';
4
+ import { EVENT_EMAIL_AUTH_REQUESTED, EVENT_GOOGLE_AUTH_REQUESTED, EVENT_SIGN_OUT_REQUESTED } from "../../web/events.js";
5
+
6
+ class Login extends LitElement {
7
+
8
+ static get properties() {
9
+
10
+ return {
11
+
12
+ currentUser: { type: Object }
13
+
14
+ };
15
+
16
+ }
17
+
18
+ createRenderRoot() {
19
+
20
+ const root = super.createRenderRoot();
21
+ root.addEventListener("click", e => this.handleClick(e));
22
+ return root;
23
+
24
+ }
25
+
26
+ handleClick(e) {
27
+
28
+ switch(e.target.className) {
29
+
30
+ case "open-login":
31
+ this.shadowRoot.querySelector("wa-dialog").open = true;
32
+ break;
33
+ case "login-with-google":
34
+ bus.dispatchEvent(new CustomEvent(EVENT_GOOGLE_AUTH_REQUESTED));
35
+ break;
36
+ case "login-with-email":
37
+ const inputLogin = this.shadowRoot.querySelector("wa-input[type=email]");
38
+ const inputPassword = this.shadowRoot.querySelector("wa-input[type=password]");
39
+ bus.dispatchEvent(new CustomEvent(EVENT_EMAIL_AUTH_REQUESTED, { detail: { email: inputLogin.value, password: inputPassword.value }}));
40
+ break;
41
+ case "sign-out":
42
+ bus.dispatchEvent(new CustomEvent(EVENT_SIGN_OUT_REQUESTED));
43
+ break;
44
+ }
45
+
46
+ }
47
+
48
+ render() {
49
+
50
+ if (this.currentUser === undefined) {
51
+
52
+ return html`Initialising...`;
53
+
54
+ } else if(this.currentUser === null) {
55
+
56
+ return styled(
57
+ loginDialog(),
58
+ loginButton()
59
+ );
60
+
61
+ } else {
62
+
63
+ return styled(
64
+ hello(this.currentUser),
65
+ signOutButton()
66
+ );
67
+
68
+ }
69
+
70
+ }
71
+
72
+ }
73
+ customElements.define('h1v3-login', Login);
74
+
@@ -0,0 +1,12 @@
1
+ import { html } from "../system.js";
2
+
3
+ export const errorNotification = ({ message }) => html`
4
+
5
+ <wa-callout variant="danger" style="opacity: 1; margin: 0.5rem; zoom: 0.8;" role="alert">
6
+
7
+ <wa-icon slot="icon" name="circle-exclamation"></wa-icon>
8
+ <strong>${message}</strong>
9
+
10
+ </wa-callout>
11
+
12
+ `;
@@ -0,0 +1,25 @@
1
+ import { LitElement } from "../system.js";
2
+ import { errorNotification } from "./notification.html.js";
3
+ import { styled } from "./partials/wa-utils.js";
4
+
5
+ class Notification extends LitElement {
6
+
7
+ static get properties() {
8
+
9
+ return {
10
+
11
+ err: { type: Object }
12
+
13
+ };
14
+
15
+ }
16
+
17
+ render() {
18
+
19
+ if (this.err)
20
+ return styled(errorNotification(this.err));
21
+
22
+ }
23
+
24
+ }
25
+ customElements.define("h1v3-notification", Notification);
@@ -0,0 +1,17 @@
1
+ import { html, waDist } from "../../system.js";
2
+
3
+ export const styled = (...children) => html`
4
+ <link rel="stylesheet" href="${waDist}/styles/webawesome.css" />
5
+ <link rel="stylesheet" href="${waDist}/styles/themes/premium.css" />
6
+ <div class="wa-cloak">
7
+ ${children}
8
+ </div>
9
+ `;
10
+
11
+ export const dialog = (title, ...children) => html`
12
+ <wa-dialog label="${title}" light-dismiss class="dialog-light-dismiss">
13
+
14
+ ${children}
15
+
16
+ </wa-dialog>
17
+ `;
@@ -0,0 +1,23 @@
1
+ import { EVENT_ERROR_OCCURRED } from "../web/events.js";
2
+
3
+ const container = document.createElement("ASIDE");
4
+ container.style.position = "fixed";
5
+ container.style.top = "0px";
6
+ container.style.right = "0px";
7
+ container.style.maxWidth = "40rem";
8
+ container.style.zIndex = 9999;
9
+
10
+ document.body.appendChild(container);
11
+
12
+ document.addEventListener(EVENT_ERROR_OCCURRED, ({ detail: { err } }) => {
13
+
14
+ if (err) {
15
+
16
+ const notification = document.createElement("h1v3-notification");
17
+ container.appendChild(notification);
18
+ notification.err = err;
19
+ setTimeout(() => notification.remove(), 5000);
20
+
21
+ }
22
+
23
+ });
@@ -0,0 +1,20 @@
1
+ export * from "../web/system.js";
2
+
3
+ export const waDist = "/vendor/@shoelace-style/webawesome-pro@3.0.0-beta.6/dist";
4
+
5
+ // vendor: webawesome
6
+ import "/vendor/@shoelace-style/webawesome-pro@3.0.0-beta.6/dist/components/button/button.js";
7
+ import "/vendor/@shoelace-style/webawesome-pro@3.0.0-beta.6/dist/components/input/input.js";
8
+ import "/vendor/@shoelace-style/webawesome-pro@3.0.0-beta.6/dist/components/card/card.js";
9
+ import "/vendor/@shoelace-style/webawesome-pro@3.0.0-beta.6/dist/components/divider/divider.js";
10
+ import "/vendor/@shoelace-style/webawesome-pro@3.0.0-beta.6/dist/components/icon/icon.js";
11
+ import "/vendor/@shoelace-style/webawesome-pro@3.0.0-beta.6/dist/components/dialog/dialog.js";
12
+ import "/vendor/@shoelace-style/webawesome-pro@3.0.0-beta.6/dist/components/callout/callout.js";
13
+
14
+ // vendor: lit
15
+ export { html, LitElement } from "/vendor/lit@3.3.1/dist/lit-core.min.js";
16
+
17
+ // h1v3
18
+ import "./errors.js";
19
+ import "./components/notification.js";
20
+ import "./components/login.js";
@@ -0,0 +1,80 @@
1
+ function eventWriteConditions(config) {
2
+
3
+ let expr = "!data.exists()"
4
+ if (config?.write)
5
+ expr += " && (" + config.write + ")"
6
+ return expr;
7
+
8
+ }
9
+
10
+ function projectionReadConditions(config) {
11
+
12
+ return config.read || true;
13
+
14
+ }
15
+
16
+ const eventStoreRules = config => ({
17
+ ".read": false,
18
+ ".write": false,
19
+ events: {
20
+ "$eid": {
21
+ ".write": eventWriteConditions(config),
22
+ ".validate": "newData.child('type').isString()"
23
+ }
24
+ },
25
+ projections: {
26
+ "$pid": {
27
+ ".read": projectionReadConditions(config)
28
+ }
29
+ }
30
+ });
31
+
32
+ const splitPathIntoSegments = path =>
33
+ path
34
+ .split("/")
35
+ .filter(x => x);
36
+
37
+ const addRulesForPath = (existingRules, [
38
+ [
39
+ nextSegment,
40
+ ...theRestOfThePath
41
+ ],
42
+ config
43
+ ]) =>
44
+ nextSegment
45
+ // still walking to the right place - build up nested objects as needed
46
+ ? {
47
+ ...existingRules,
48
+ [nextSegment]: addRulesForPath(
49
+ // create the object if necessary
50
+ existingRules[nextSegment] || {},
51
+ // pass the rest of the path and the config recursively
52
+ [theRestOfThePath, config]
53
+ )
54
+ }
55
+ // stick the rules here
56
+ : eventStoreRules(config);
57
+
58
+ const parseConfig = ([_name, config]) => [
59
+ // 0: the path
60
+ splitPathIntoSegments(config.ref),
61
+ // 1: the config
62
+ config
63
+ ];
64
+
65
+ export function generateRules(argv, stores) {
66
+
67
+ const json = JSON.stringify(
68
+ stores
69
+ // parse the path
70
+ .map(parseConfig)
71
+ // build the map
72
+ .reduce(addRulesForPath, {}),
73
+ null, 4);
74
+ if(argv.snippet)
75
+ console.log(json.slice(1,-1));
76
+ else
77
+ console.log(json);
78
+
79
+ }
80
+
@@ -0,0 +1,24 @@
1
+
2
+ export async function listEventStores(_argv, stores) {
3
+
4
+ console.log("List event stores");
5
+ if (!stores.length) {
6
+
7
+ console.log("No event stores configured");
8
+
9
+ } else {
10
+
11
+ for(const [name, { ref, triggerPath, projections }] of stores) {
12
+
13
+ console.log();
14
+ console.log(name);
15
+ console.log(" Database location:", ref);
16
+ console.log(" Trigger path:", triggerPath);
17
+ console.log(" Projections:", projections.join(", "));
18
+
19
+ }
20
+ }
21
+
22
+ }
23
+
24
+
@@ -0,0 +1,54 @@
1
+ import fs from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+
7
+ async function readPackageJSON() {
8
+
9
+ return JSON.parse(await fs.readFile(`${__dirname}/../../package.json`));
10
+
11
+ }
12
+
13
+ async function vendor(subpath) {
14
+
15
+ const { version, name } = await readPackageJSON();
16
+ const src = join(__dirname, "..", "..", "dist", "browser", subpath);;
17
+
18
+
19
+ // Destination: include semver
20
+ const versionedName = `${name}@${version}`;
21
+ const dest =join(".", "public", "vendor", versionedName, subpath);
22
+
23
+ // Recursive copy
24
+ await fs.cp(src, dest, { recursive: true });
25
+ console.log(`Vendored ${versionedName}/${subpath} -> ${dest}`);
26
+
27
+ }
28
+
29
+ export async function eventStore(_argv) {
30
+
31
+ await vendor("event-store");
32
+
33
+ }
34
+
35
+ export async function webPlatform(_argv) {
36
+
37
+ await vendor("web");
38
+
39
+ }
40
+
41
+ export async function webPlatformUI(_argv) {
42
+
43
+ await webPlatform(_argv);
44
+ await vendor("web-ui");
45
+ const resp = await fetch("https://cdn.jsdelivr.net/gh/lit/dist@3.3.1/core/lit-core.min.js")
46
+ const lit = await resp.text();
47
+ const dest = join(".", "public", "vendor", "lit@3.3.1", "dist");
48
+ await fs.mkdir(dest, { recursive: true });
49
+ await fs.writeFile(
50
+ join(".", "public", "vendor", "lit@3.3.1", "dist", "lit-core.min.js"),
51
+ lit
52
+ );
53
+
54
+ }
@@ -0,0 +1,28 @@
1
+ import { updateProjections } from "./projections.js";
2
+
3
+ export const EVENT_STORE_META = Symbol("Event store configuration metadata");
4
+
5
+ export function configure({ ref, projections, ...rest }, onValueWritten, logger) {
6
+
7
+ // considered the "home" of the event store, under which events and projections will be stored
8
+ const eventStorePath = ref.replaceAll(/\/\$([^/]*)/g, "/{$1}");
9
+
10
+ // the path which will trigger event store projections
11
+ const writeRefPath = `${eventStorePath}/events/{eid}`;
12
+
13
+ // handle an incoming write event
14
+ const handler = e => updateProjections(e.data.after, projections, logger);
15
+
16
+ const metadata = {
17
+ ...rest,
18
+ ref,
19
+ triggerPath: writeRefPath,
20
+ projections: Object.keys(projections)
21
+ };
22
+ // tag the trigger with metadata
23
+ return Object.assign(
24
+ onValueWritten(writeRefPath, handler),
25
+ { [EVENT_STORE_META]: metadata }
26
+ );
27
+
28
+ }
@@ -0,0 +1,38 @@
1
+ export async function updateProjections(incomingEventSnap, projectionTransformationPerEvent, logger) {
2
+
3
+ // order the events
4
+ const eventsSnap = await incomingEventSnap.ref.parent.get();
5
+ const events = eventsSnap.val() || {};
6
+ const sortedEvents = Object.entries(events).sort((a, b) => a[0] > b[0] ? 1 : a[0] === b[0] ? 0 : -1);
7
+
8
+ const projections = Object.entries(projectionTransformationPerEvent);
9
+ for(const [key, transformations] of projections) {
10
+
11
+ const fallbackTransformation = transformations["?"];
12
+ const view = sortedEvents.reduce(
13
+ (agg, e) => {
14
+
15
+ const transform = transformations[e[1]?.type] || fallbackTransformation || missingTransform;
16
+ return transform(agg, e[1], e[0]);
17
+
18
+ }, {});
19
+ await writeProjection(incomingEventSnap, key, view);
20
+
21
+ }
22
+
23
+ function missingTransform(agg, e) {
24
+
25
+ logger.warn("Missing transformation for event", { event: e });
26
+ return agg;
27
+
28
+ }
29
+
30
+ async function writeProjection(incomingEventSnap, key, view) {
31
+
32
+ const projectionRef = incomingEventSnap.ref.parent.parent.child("projections").child(key);
33
+ logger.info("Writing projection after event", { path: projectionRef, incomingEvent: incomingEventSnap.val() });
34
+ await projectionRef.set(view);
35
+
36
+ }
37
+
38
+ }
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { listEventStores } from "./commands/list-event-stores.js";
4
+ import { generateRules } from "./commands/generate-rules.js";
5
+ import { main } from "./system/main.js";
6
+ import { eventStore, webPlatform, webPlatformUI } from "./commands/vendor.js";
7
+
8
+ const configParameter = {
9
+
10
+ description: "path to the configuration script",
11
+ defaultValue: "h1v3.config.js",
12
+ examples: ["./myconfig.js"]
13
+
14
+ };
15
+
16
+ const sharedParameters = {
17
+
18
+ config: configParameter
19
+
20
+ };
21
+
22
+ main({
23
+
24
+ "list": {
25
+
26
+ description: "List the configured event stores",
27
+ parameters: sharedParameters,
28
+ strategy: listEventStores
29
+
30
+ },
31
+ "rules": {
32
+
33
+ description: "Generate expected database rules JSON",
34
+ parameters: sharedParameters,
35
+ strategy: generateRules
36
+
37
+ },
38
+ "vendor-eventstore": {
39
+
40
+ description: "Copy event store client into your browser assets folder",
41
+ loadConfig: false,
42
+ parameters: {},
43
+ strategy: eventStore
44
+
45
+ },
46
+ "vendor-web": {
47
+
48
+ description: "Copy web logic platform into your browser assets folder",
49
+ loadConfig: false,
50
+ parameters: {},
51
+ strategy: webPlatform
52
+
53
+ },
54
+ "vendor-webui": {
55
+
56
+ description: "Copy web UI platform into your browser assets folder (requires web)",
57
+ loadConfig: false,
58
+ parameters: {},
59
+ strategy: webPlatformUI
60
+
61
+ }
62
+
63
+ });
64
+
65
+
package/src/index.js ADDED
@@ -0,0 +1,18 @@
1
+ export * from "./event-store/initialise.js";
2
+
3
+ export const passThroughView = {
4
+ "?": (view, e, key) => {
5
+
6
+ view.events = view.events || {};
7
+ view.events[key] = e;
8
+ return view;
9
+
10
+ }
11
+ };
12
+
13
+ export function ifUserIdExists(path) {
14
+
15
+ const expr = `root.child('${path}/' + auth.uid).exists()`;
16
+ return expr.replaceAll(/(\$[^/]*)/g, "' + $1 + '")
17
+
18
+ }
@@ -0,0 +1,21 @@
1
+ import { resolve } from "path";
2
+ import { pathToFileURL } from "url";
3
+ import { EVENT_STORE_META } from "./event-store/initialise.js";
4
+
5
+ export async function loadConfiguration(argv) {
6
+
7
+ const configPath = argv.config || resolve(process.cwd(), "./h1v3.config.js");
8
+ if (!argv.snippet)
9
+ console.log("Loading configuration:", configPath);
10
+ const module = await import(pathToFileURL(configPath));
11
+ return readStoresMetadata(module);
12
+
13
+ }
14
+
15
+ export function readStoresMetadata(config) {
16
+
17
+ return Object.entries(config)
18
+ .map(([k, v]) => [k, v?.[EVENT_STORE_META]])
19
+ .filter(x => x[1]);
20
+
21
+ }