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.
- package/LICENSE +21 -0
- package/README.md +2 -0
- package/dist/browser/event-store/modular.js +82 -0
- package/dist/browser/web/events.js +7 -0
- package/dist/browser/web/login.js +48 -0
- package/dist/browser/web/system.js +67 -0
- package/dist/browser/web-ui/components/login.html.js +44 -0
- package/dist/browser/web-ui/components/login.js +74 -0
- package/dist/browser/web-ui/components/notification.html.js +12 -0
- package/dist/browser/web-ui/components/notification.js +25 -0
- package/dist/browser/web-ui/components/partials/wa-utils.js +17 -0
- package/dist/browser/web-ui/errors.js +23 -0
- package/dist/browser/web-ui/system.js +20 -0
- package/package.json +22 -0
- package/scripts/dist-client.js +31 -0
- package/src/client/index.js +2 -0
- package/src/client/modular.js +82 -0
- package/src/client/node.js +29 -0
- package/src/client/web/events.js +7 -0
- package/src/client/web/login.js +48 -0
- package/src/client/web/system.js +67 -0
- package/src/client/web-ui/components/login.html.js +44 -0
- package/src/client/web-ui/components/login.js +74 -0
- package/src/client/web-ui/components/notification.html.js +12 -0
- package/src/client/web-ui/components/notification.js +25 -0
- package/src/client/web-ui/components/partials/wa-utils.js +17 -0
- package/src/client/web-ui/errors.js +23 -0
- package/src/client/web-ui/system.js +20 -0
- package/src/commands/generate-rules.js +80 -0
- package/src/commands/list-event-stores.js +24 -0
- package/src/commands/vendor.js +54 -0
- package/src/event-store/initialise.js +28 -0
- package/src/event-store/projections.js +38 -0
- package/src/exec-eventstore.js +65 -0
- package/src/index.js +18 -0
- package/src/load-configuration.js +21 -0
- package/src/schema.js +66 -0
- package/src/system/json.js +23 -0
- 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
|
+
}
|