h1v3 0.3.0 → 0.4.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.
@@ -41,7 +41,6 @@ export function eventStore(firebase, path) {
41
41
  }
42
42
 
43
43
  return {
44
-
45
44
  async record(eventType, payload) {
46
45
 
47
46
  const eid = newEventId(eventType);
@@ -49,14 +48,12 @@ export function eventStore(firebase, path) {
49
48
  await set(ref(db, `${eventsPath}/${eid}`), data);
50
49
 
51
50
  },
52
-
53
51
  async getProjection(name) {
54
52
 
55
53
  const snap = await get(ref(db, `${projectionsPath}/${name}`));
56
54
  return snap.val();
57
55
 
58
56
  },
59
-
60
57
  onProjectionValue(name, callback) {
61
58
 
62
59
  const query = ref(db, `${projectionsPath}/${name}`);
@@ -64,19 +61,21 @@ export function eventStore(firebase, path) {
64
61
  subscriptions[name] = onValue(query, callback);
65
62
 
66
63
  },
67
-
68
64
  async offProjection(name) {
69
65
 
70
66
  await unsubscribe(name);
71
67
 
72
68
  },
73
-
74
69
  async dispose() {
75
70
 
76
71
  await unsubscribeAll();
77
72
 
78
- }
73
+ },
74
+ get path() {
79
75
 
76
+ return path;
77
+
78
+ }
80
79
  };
81
80
 
82
81
  }
@@ -0,0 +1,25 @@
1
+ export const BECAME_MEMBER = "BECAME_MEMBER";
2
+
3
+ export const ADDED_TO_TEAM = "ADDED_TO_TEAM";
4
+
5
+ export function team(teamMembershipStore, userTeamsStoreFactory) {
6
+
7
+ const teamPath = /\/teams\/([^\/]*)/g.exec(teamMembershipStore.path);
8
+ if (!teamPath?.[1]) throw new Error(`Invalid team path: ${teamMembershipStore.path}`);
9
+ const tid = teamPath[1];
10
+
11
+ return {
12
+
13
+ async addAdmin({ uid, name, ...rest }) {
14
+
15
+ if (!uid) throw new Error("Missing uid");
16
+ if (!name) throw new Error("Missing name");
17
+ await teamMembershipStore.record(BECAME_MEMBER, { uid, name, ...rest });
18
+ const userTeamsStore = userTeamsStoreFactory({ uid });
19
+ await userTeamsStore.record(ADDED_TO_TEAM, { tid });
20
+
21
+ }
22
+
23
+ };
24
+
25
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "h1v3",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -23,7 +23,8 @@ async function distribute(subPath, path) {
23
23
  const publishList = [
24
24
  ["event-store", "modular.js"],
25
25
  ["", "web/"],
26
- ["", "web-ui/"]
26
+ ["", "web-ui/"],
27
+ ["team", "team.js"]
27
28
  ];
28
29
 
29
30
  await Promise.all(publishList.map(kv => distribute(...kv)));
@@ -1,2 +1,4 @@
1
1
  export * as node from "./node.js";
2
- export * as modular from "./modular.js";
2
+ export * as modular from "./modular.js";
3
+ export * as teamNode from "./team-node.js";
4
+ export * as team from "./team.js";
@@ -41,7 +41,6 @@ export function eventStore(firebase, path) {
41
41
  }
42
42
 
43
43
  return {
44
-
45
44
  async record(eventType, payload) {
46
45
 
47
46
  const eid = newEventId(eventType);
@@ -49,14 +48,12 @@ export function eventStore(firebase, path) {
49
48
  await set(ref(db, `${eventsPath}/${eid}`), data);
50
49
 
51
50
  },
52
-
53
51
  async getProjection(name) {
54
52
 
55
53
  const snap = await get(ref(db, `${projectionsPath}/${name}`));
56
54
  return snap.val();
57
55
 
58
56
  },
59
-
60
57
  onProjectionValue(name, callback) {
61
58
 
62
59
  const query = ref(db, `${projectionsPath}/${name}`);
@@ -64,19 +61,21 @@ export function eventStore(firebase, path) {
64
61
  subscriptions[name] = onValue(query, callback);
65
62
 
66
63
  },
67
-
68
64
  async offProjection(name) {
69
65
 
70
66
  await unsubscribe(name);
71
67
 
72
68
  },
73
-
74
69
  async dispose() {
75
70
 
76
71
  await unsubscribeAll();
77
72
 
78
- }
73
+ },
74
+ get path() {
79
75
 
76
+ return path;
77
+
78
+ }
80
79
  };
81
80
 
82
81
  }
@@ -1,3 +1,4 @@
1
+ import { EVENT_STORE_META } from "../event-store/initialise.js";
1
2
  import { eventStore as eventStoreModular } from "./modular.js";
2
3
 
3
4
  function adapter(db) {
@@ -5,22 +6,49 @@ function adapter(db) {
5
6
  return {
6
7
  db,
7
8
  ref(_db, path) {
9
+
8
10
  return db.ref(path);
11
+
9
12
  },
10
13
  async onValue(query, callback) {
14
+
11
15
  query.on("value", callback);
12
16
  return () => query.off("value", callback);
17
+
13
18
  },
14
19
  async get(query) {
20
+
15
21
  return await query.get();
22
+
16
23
  },
17
24
  async set(ref, data) {
25
+
18
26
  return await ref.set(data);
27
+
28
+ },
29
+ get path() {
30
+
31
+ return path;
32
+
19
33
  }
20
34
  };
21
35
 
22
36
  }
23
37
 
38
+ function resolvePath(pathTemplate, pathValues) {
39
+
40
+ return Object.entries(pathValues)
41
+ .reduce((working, [key, value]) => working.replaceAll(`$${key}`, value), pathTemplate);
42
+
43
+ }
44
+
45
+ export function eventStoreFromConfig(db, config, pathValues = {}) {
46
+
47
+ const meta = config[EVENT_STORE_META];
48
+ return eventStore(db, resolvePath(meta.ref, pathValues));
49
+
50
+ }
51
+
24
52
  export function eventStore(db, path) {
25
53
 
26
54
  const firebase = adapter(db);
@@ -0,0 +1,28 @@
1
+ import { eventStoreFromConfig as defaultEventStoreFromConfig } from "./node.js";
2
+ import { team } from "./team.js";
3
+
4
+ export function teamsContext({ db, teamMembershipConfig, userTeamsConfig, eventStoreFromConfig = defaultEventStoreFromConfig }) {
5
+
6
+ if (!teamMembershipConfig) throw new Error("Missing: teamMembershipConfig");
7
+ if (!userTeamsConfig) throw new Error("Missing: userTeamsConfig");
8
+ if (!db) throw new Error("Missing: db");
9
+
10
+ return {
11
+
12
+ team({ tid }) {
13
+
14
+ const membershipEventStore = eventStoreFromConfig(db, teamMembershipConfig, { tid });
15
+ return team(
16
+ membershipEventStore,
17
+ function({ uid }) {
18
+
19
+ return eventStoreFromConfig(db, userTeamsConfig, { uid });
20
+
21
+ }
22
+ );
23
+
24
+ }
25
+
26
+ };
27
+
28
+ }
@@ -0,0 +1,38 @@
1
+ export const BECAME_MEMBER = "BECAME_MEMBER";
2
+ export const BECAME_ADMIN = "BECAME_ADMIN";
3
+ export const ADDED_TO_TEAM = "ADDED_TO_TEAM";
4
+
5
+ export function team(teamMembershipStore, userTeamsStoreFactory) {
6
+
7
+ const teamPath = /\/teams\/([^\/]*)/g.exec(teamMembershipStore.path);
8
+ if (!teamPath?.[1]) throw new Error(`Invalid team path: ${teamMembershipStore.path}`);
9
+ const tid = teamPath[1];
10
+
11
+ return {
12
+
13
+ async addAdmin({ uid, name, ...rest }) {
14
+
15
+ await record({ uid, name, eventType: BECAME_ADMIN, rest });
16
+
17
+ },
18
+
19
+ async addMember({ uid, name, ...rest }) {
20
+
21
+ await record({ uid, name, eventType: BECAME_MEMBER, rest });
22
+
23
+ }
24
+
25
+ };
26
+
27
+
28
+ async function record({ uid, name, eventType, rest }) {
29
+
30
+ if (!uid) throw new Error("Missing uid");
31
+ if (!name) throw new Error("Missing name");
32
+ await teamMembershipStore.record(eventType, { uid, name, ...rest });
33
+ const userTeamsStore = userTeamsStoreFactory({ uid });
34
+ await userTeamsStore.record(ADDED_TO_TEAM, { tid });
35
+
36
+ }
37
+
38
+ }
@@ -1,5 +1,7 @@
1
1
  function eventWriteConditions(config) {
2
2
 
3
+ if (config?.write === false)
4
+ return false;
3
5
  let expr = "!data.exists()"
4
6
  if (config?.write)
5
7
  expr += " && (" + config.write + ")"
@@ -19,7 +21,7 @@ const eventStoreRules = config => ({
19
21
  events: {
20
22
  "$eid": {
21
23
  ".write": eventWriteConditions(config),
22
- ".validate": "newData.child('type').isString()"
24
+ ".validate": eventValidation(config)
23
25
  }
24
26
  },
25
27
  projections: {
@@ -62,6 +64,17 @@ const parseConfig = ([_name, config]) => [
62
64
  config
63
65
  ];
64
66
 
67
+ function eventValidation(config) {
68
+
69
+ const requireType = "newData.child('type').isString()";
70
+ if (!Array.isArray(config?.eventTypes)) return requireType;
71
+ return [
72
+ requireType,
73
+ `newData.child('type').val().matches(/^(${config.eventTypes.join("|")})$/)`
74
+ ].join(" && ");
75
+
76
+ }
77
+
65
78
  export function generateRules(argv, stores) {
66
79
 
67
80
  const json = JSON.stringify(
@@ -51,4 +51,10 @@ export async function webPlatformUI(_argv) {
51
51
  lit
52
52
  );
53
53
 
54
+ }
55
+
56
+ export async function membership(_argv) {
57
+
58
+ await eventStore(_argv);
59
+ await vendor("team");
54
60
  }
@@ -1,9 +1,9 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { listEventStores } from "./commands/list-event-stores.js";
4
4
  import { generateRules } from "./commands/generate-rules.js";
5
5
  import { main } from "./system/main.js";
6
- import { eventStore, webPlatform, webPlatformUI } from "./commands/vendor.js";
6
+ import * as vendor from "./commands/vendor.js";
7
7
 
8
8
  const configParameter = {
9
9
 
@@ -40,7 +40,7 @@ main({
40
40
  description: "Copy event store client into your browser assets folder",
41
41
  loadConfig: false,
42
42
  parameters: {},
43
- strategy: eventStore
43
+ strategy: vendor.eventStore
44
44
 
45
45
  },
46
46
  "vendor-web": {
@@ -48,7 +48,7 @@ main({
48
48
  description: "Copy web logic platform into your browser assets folder",
49
49
  loadConfig: false,
50
50
  parameters: {},
51
- strategy: webPlatform
51
+ strategy: vendor.webPlatform
52
52
 
53
53
  },
54
54
  "vendor-webui": {
@@ -56,7 +56,15 @@ main({
56
56
  description: "Copy web UI platform into your browser assets folder (requires web)",
57
57
  loadConfig: false,
58
58
  parameters: {},
59
- strategy: webPlatformUI
59
+ strategy: vendor.webPlatformUI
60
+
61
+ },
62
+ "vendor-membership": {
63
+
64
+ description: "Copy team management logic into your browser assets folder",
65
+ loadConfig: false,
66
+ parameters: {},
67
+ strategy: vendor.membership
60
68
 
61
69
  }
62
70
 
@@ -1,2 +1,3 @@
1
- export { userProfileStore } from "./user-profile-store.js";
2
- export { ifMyTeam, membershipStore } from "./membership-store.js";
1
+ export * as userProfile from "./user-profile-store.js";
2
+ export * as membership from "./membership-store.js";
3
+ export * as userTeams from "./user-teams-store.js";
@@ -8,49 +8,76 @@ export const ifMyTeam = rootPath =>
8
8
  export const ifAdminInThisTeam = rootPath =>
9
9
  ifUserIdExists(`${membershipStorePath(rootPath)}/projections/details/admins`);
10
10
 
11
+ export const BECAME_MEMBER = "BECAME_MEMBER";
12
+ export const BECAME_ADMIN = "BECAME_ADMIN";
13
+ export const LEFT_TEAM = "LEFT_TEAM";
14
+ export const INVITED_MEMBER = "INVITED_MEMBER";
15
+ export const INVITED_ADMIN = "INVITED_ADMIN";
16
+
17
+ const eventTypes = [
18
+ BECAME_MEMBER,
19
+ BECAME_ADMIN,
20
+ LEFT_TEAM,
21
+ INVITED_MEMBER,
22
+ INVITED_ADMIN
23
+ ];
11
24
 
12
25
  const membershipDetails = {
13
26
 
14
- "BECAME_MEMBER": (view, { payload }) => {
27
+ [BECAME_MEMBER]: (view, { payload }) => {
15
28
 
16
29
  const found = purgeUser(view, payload);
17
- view.members = { ...view.members, [payload.uid]: { name: payload.name || found?.name || "" }};
30
+ view.members = { ...view.members, [payload.uid]: resolveUserView(payload, found)};
18
31
  return view;
19
32
 
20
33
  },
21
- "BECAME_ADMIN": (view, { payload }) => {
34
+ [BECAME_ADMIN]: (view, { payload }) => {
22
35
 
23
36
  const found = purgeUser(view, payload);
24
- view.admins = { ...view.admins, [payload.uid]: { name: payload.name || found?.name || "" }};
37
+ view.admins = { ...view.admins, [payload.uid]: resolveUserView(payload, found)};
25
38
  return view;
26
39
 
27
40
  },
28
- "LEFT_TEAM": (view, { payload }) => {
41
+ [LEFT_TEAM]: (view, { payload }) => {
29
42
 
30
43
  purgeUser(view, payload);
31
44
  return view;
32
45
 
46
+ },
47
+ [INVITED_MEMBER]: (view, { payload }) => {
48
+
49
+ const found = purgeInvited(view, payload);
50
+ view.invitedMembers = { ...view.invited, [payload.uid]: resolveUserView(payload, found)};
51
+ return view;
52
+
53
+ },
54
+ [INVITED_ADMIN]: (view, { payload }) => {
55
+
56
+ const found = purgeInvited(view, payload);
57
+ view.invitedAdmins = { ...view.invited, [payload.uid]: resolveUserView(payload, found)};
58
+ return view;
59
+
33
60
  }
34
61
 
35
62
  };
36
63
 
37
64
  const members = {
38
65
 
39
- "BECAME_MEMBER": (view, { payload }) => {
66
+ [BECAME_MEMBER]: (view, { payload }) => {
40
67
 
41
68
  if (!payload?.uid) return view;
42
69
  view[payload.uid] = true;
43
70
  return view;
44
71
 
45
72
  },
46
- "BECAME_ADMIN": (view, { payload }) => {
73
+ [BECAME_ADMIN]: (view, { payload }) => {
47
74
 
48
75
  if (!payload?.uid) return view;
49
76
  view[payload.uid] = true;
50
77
  return view;
51
78
 
52
79
  },
53
- "LEFT_TEAM": (view, { payload }) => {
80
+ [LEFT_TEAM]: (view, { payload }) => {
54
81
 
55
82
  if (!payload?.uid) return view;
56
83
  delete view[payload.uid];
@@ -60,15 +87,30 @@ const members = {
60
87
 
61
88
  }
62
89
 
90
+ function resolveUserView(payload, found) {
91
+
92
+ return { name: payload.name || found?.name || "" };
93
+
94
+ }
95
+
96
+ function purgeInvited(view, { uid }) {
97
+
98
+ const found = view.invitedMembers?.[uid] || view.invitedAdmins?.[uid];
99
+ if (view.invitedMembers && (uid in view.invitedMembers))
100
+ delete view.invitedMembers[uid];
101
+ if (view.invitedAdmins && (uid in view.invitedAdmins))
102
+ delete view.invitedAdmins[uid];
103
+ return found;
104
+
105
+ }
106
+
63
107
  function purgeUser(view, { uid }) {
64
108
 
65
- let found = view.members?.[uid] || view.admins?.[uid] || view.owners?.[uid];
109
+ const found = view.members?.[uid] || view.admins?.[uid] || view.owners?.[uid];
66
110
  if (view.members && (uid in view.members))
67
111
  delete view.members[uid];
68
112
  if (view.admins && (uid in view.admins))
69
113
  delete view.admins[uid];
70
- if (view.owners && (uid in view.owners))
71
- delete view.owners[uid];
72
114
  return found;
73
115
 
74
116
  }
@@ -79,10 +121,11 @@ export function membershipStore(rootPath) {
79
121
  ref: membershipStorePath(rootPath),
80
122
  projections: {
81
123
  "details": membershipDetails,
82
- members
124
+ members,
83
125
  },
84
- write: ifAdminInThisTeam,
85
- read: ifAdminInThisTeam
126
+ write: ifAdminInThisTeam(rootPath),
127
+ read: ifMyTeam(rootPath),
128
+ eventTypes
86
129
  };
87
130
 
88
131
  }
@@ -1,8 +1,10 @@
1
1
  import { deleteByPath, filterBySchema, schemaAllowsPath } from "h1v3/schema";
2
2
 
3
+ export const DETAILS_UPDATED = "DETAILS_UPDATED";
4
+
3
5
  const current = schema => ({
4
6
 
5
- "DETAILS_UPDATED": (view, { payload }) => {
7
+ [DETAILS_UPDATED]: (view, { payload }) => {
6
8
 
7
9
  if (!payload || typeof payload !== "object") return view;
8
10
  view = Object.assign(view, filterBySchema(schema, payload));
@@ -32,7 +34,8 @@ export function userProfileStore(rootPath, schema) {
32
34
  current: current(schema)
33
35
  },
34
36
  write: ifMe,
35
- read: ifMe
37
+ read: ifMe,
38
+ eventTypes: [DETAILS_UPDATED]
36
39
 
37
40
  };
38
41
 
@@ -0,0 +1,43 @@
1
+ export const ADDED_TO_TEAM = "ADDED_TO_TEAM";
2
+
3
+ export const LEFT_TEAM = "LEFT_TEAM";
4
+
5
+ const current = {
6
+
7
+ [ADDED_TO_TEAM]: (view, { payload }) => {
8
+
9
+ if (!payload || typeof payload !== "object") return view;
10
+ if (!payload.tid) return view;
11
+ view.teams = Object.assign({}, view.teams, { [payload.tid]: Date.now() });
12
+ return view;
13
+
14
+ },
15
+ [LEFT_TEAM]: (view, { payload }) => {
16
+
17
+ if (!payload || typeof payload !== "object") return view;
18
+ if (!payload.tid) return view;
19
+ if (!view.teams) return view;
20
+ delete view.teams[payload.tid];
21
+ return view;
22
+
23
+ }
24
+
25
+ };
26
+
27
+ const ifMe = "$uid == auth.uid";
28
+
29
+ export function userTeamsStore(rootPath) {
30
+
31
+ return {
32
+
33
+ ref: `${rootPath}/users/$uid/teams`,
34
+ projections: {
35
+ current
36
+ },
37
+ eventTypes: [ADDED_TO_TEAM, LEFT_TEAM],
38
+ read: ifMe,
39
+ write: false
40
+
41
+ };
42
+
43
+ }