h1v3 0.12.0 → 0.14.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/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # h1v3
2
2
  A firebase web platform
3
3
 
4
-
5
4
  # Install
6
5
 
7
6
  ```
@@ -87,7 +86,8 @@ e.g. firebase.json
87
86
  {
88
87
  "source": "/store_meta",
89
88
  "function": {
90
- "functionId": "store_meta"
89
+ "functionId": "store_meta",
90
+ "region": "europe-west1"
91
91
  }
92
92
  }
93
93
  ]
@@ -101,6 +101,17 @@ This allows you to, in the browser, do:
101
101
  const meta = import("/store_meta") with { type: "json" }
102
102
  ```
103
103
 
104
+ > [!CAUTION]
105
+ > At time of writing, the firebase emulators don't properly support the use of RTDB and triggers outside of the us-central1 region.
106
+ > This is a problem if you are deploying to production based on a `firebase.json`.
107
+
108
+ > For that reason, you many need to use us-central1 as your "default" firebase.json entry, and replace it at deploy time.
109
+ > For example, using:
110
+ >
111
+ > `cat firebase.json | jq "(.hosting.rewrites.[] | select(.source == \"/store_meta\") | .function) += { region: \"europe-west1\" }" > firebase.json`.
112
+ >
113
+ >IKR
114
+
104
115
  ## Client-side libraries
105
116
 
106
117
  You can vendor the (free) dependencies and client files for the library using npx
@@ -42,7 +42,7 @@ export async function inject({ database, authentication, metaURL, meta, handleEr
42
42
 
43
43
  const userTeamsStore = userTeamsStoreFactory({ uid });
44
44
  const userProfileStore = userProfileStoreFactory({ uid });
45
- const client = user({ userTeamsStore, userProfileStore });
45
+ const client = user({ uid, userTeamsStore, userProfileStore, teamDetailsStoreFactory, teamMembershipStoreFactory });
46
46
  return makeDisposable(client, [userTeamsStore, userProfileStore]);
47
47
 
48
48
  },
@@ -0,0 +1,46 @@
1
+ export async function resolveOrTimeout(tryStrategy, pollingDelay = 300) {
2
+
3
+ let isPolling = true;
4
+
5
+ async function poll() {
6
+
7
+ while (true) {
8
+
9
+ try {
10
+
11
+ return await tryStrategy();
12
+
13
+ } catch (err) {
14
+
15
+ // polling failed - wait a bit before retrying
16
+ await new Promise(resolve => setTimeout(resolve, pollingDelay));
17
+ if (!isPolling) return undefined;
18
+
19
+ }
20
+
21
+ }
22
+
23
+ }
24
+
25
+ try {
26
+
27
+ return await Promise.race([
28
+ poll(),
29
+ timeout("Timed out waiting for creation")
30
+ ]);
31
+
32
+ } finally {
33
+
34
+ isPolling = false;
35
+
36
+ }
37
+
38
+
39
+
40
+ }
41
+
42
+ async function timeout(message, timeout = 5000) {
43
+
44
+ await new Promise((_resolve, reject) => setTimeout(() => reject(message), timeout));
45
+
46
+ }
@@ -1,9 +1,15 @@
1
+ import { rand62 } from "../id.js";
2
+ import { resolveOrTimeout } from "./promise.js";
3
+
1
4
  export const DETAILS_UPDATED = "DETAILS_UPDATED";
2
5
 
3
- export function user({ userTeamsStore, userProfileStore }) {
6
+ export function user({ uid, userTeamsStore, userProfileStore, teamDetailsStoreFactory, teamMembershipStoreFactory }) {
4
7
 
8
+ if (!uid) throw new Error("Missing uid");
5
9
  if (!userTeamsStore) throw new Error("Missing userTeamsStore");
6
10
  if (!userProfileStore) throw new Error("Missing userProfileStore");
11
+ if (!teamDetailsStoreFactory) throw new Error("Missing teamDetailsStoreFactory");
12
+ if (!teamMembershipStoreFactory) throw new Error("Missing teamMembershipStoreFactory");
7
13
 
8
14
  userTeamsStore.onProjectionValue("current", handleTeamsCurrentChanged);
9
15
 
@@ -35,6 +41,11 @@ export function user({ userTeamsStore, userProfileStore }) {
35
41
 
36
42
  },
37
43
 
44
+ async createTeam({ team: { id, name }, user: { displayName } = {} }, progressCallback) {
45
+
46
+ return await createTeam({ team: { id, name }, user: { displayName } }, progressCallback);
47
+
48
+ }
38
49
 
39
50
  }
40
51
 
@@ -52,12 +63,74 @@ export function user({ userTeamsStore, userProfileStore }) {
52
63
  const val = myTeamsSnap.val();
53
64
  const teams = val?.teams;
54
65
  if (!teams) return null;
55
- return Object.entries(teams).map(([key, val]) => ({
56
- id: key,
57
- name: key,
58
- since: new Date(val)
66
+ return await Promise.all(Object.entries(teams).map(async ([key, val]) => {
67
+
68
+ const { name } = await fetchTeamDetails(teamDetailsStoreFactory, key);
69
+ return {
70
+ id: key,
71
+ name,
72
+ since: new Date(val)
73
+ };
74
+
59
75
  }));
60
76
 
61
77
  }
62
78
 
79
+ async function createTeam({ team: { id, name }, user: { displayName } = {} }, progressCallback = () => {}) {
80
+
81
+ const progress = {
82
+ createTeam: false,
83
+ ensureAccess: false,
84
+ addToTeam: false,
85
+ setTeamName: false
86
+ }
87
+ const tid = id || rand62(22);
88
+ const teamMembershipStore = teamMembershipStoreFactory({ tid });
89
+ const teamDetailsStore = teamDetailsStoreFactory({ tid });
90
+ try {
91
+
92
+ const accessibleMembers = resolveOrTimeout(() =>
93
+ teamMembershipStore.getProjection("members"));
94
+ // create the access
95
+ const data = { uid };
96
+ if (displayName) data.name = displayName;
97
+ await teamMembershipStore.record("CREATED_TEAM", data);
98
+ progress.createTeam = true;
99
+ progressCallback(progress);
100
+
101
+ // wait for (admin) access
102
+ await accessibleMembers;
103
+ progress.ensureAccess = true;
104
+ progressCallback(progress);
105
+
106
+ // once we get access - record access...
107
+ await userTeamsStore.record("ADDED_TO_TEAM", { tid });
108
+ progress.addToTeam = true;
109
+ progressCallback(progress);
110
+
111
+ // ...and update team details
112
+ await teamDetailsStore.record("DETAILS_UPDATED", { name });
113
+ progress.setTeamName = true;
114
+ progressCallback(progress);
115
+
116
+ } finally {
117
+
118
+ teamMembershipStore.dispose();
119
+ teamDetailsStore.dispose();
120
+
121
+ }
122
+ return tid;
123
+
124
+ }
125
+
126
+ }
127
+
128
+ async function fetchTeamDetails(teamDetailsStoreFactory, key) {
129
+
130
+ const teamDetailsStore = teamDetailsStoreFactory({ tid: key });
131
+ const details = await teamDetailsStore.getProjection("current");
132
+ const name = details?.name || key;
133
+ return { name };
134
+
63
135
  }
136
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "h1v3",
3
- "version": "0.12.0",
3
+ "version": "0.14.0",
4
4
  "description": "",
5
5
  "license": "MIT",
6
6
  "author": "",
@@ -16,9 +16,11 @@ export async function updateProjections(incomingEventSnap, projectionTransformat
16
16
  const context = {
17
17
  projection: key
18
18
  };
19
+ let eventCount = 0;
19
20
  const view = sortedEvents.reduce(
20
21
  (agg, [eventKey, eventValue]) => {
21
22
 
23
+ context.eventCount = eventCount++;
22
24
  context.eventId = eventKey;
23
25
  if (debug) trace.push(agg, e);
24
26
  const transform = transformations[eventValue?.type] || fallbackTransformation || missingTransformFor(key);
@@ -31,7 +31,13 @@ main({
31
31
  "rules": {
32
32
 
33
33
  description: "Generate expected database rules JSON",
34
- parameters: sharedParameters,
34
+ parameters: {
35
+ ...sharedParameters,
36
+ output: {
37
+ description: "path to write output",
38
+ examples: ["./myconfig.js"]
39
+ }
40
+ },
35
41
  strategy: generateRules
36
42
 
37
43
  },
@@ -1,4 +1,4 @@
1
- import { assertIsMyTeam } from "../../configuration/rules.js";
1
+ import { assertIsMyTeam, assertMyRoleIsAdmin } from "../../configuration/rules.js";
2
2
  import { eventTypes } from "./events.js";
3
3
  import current from "./projections/current.js";
4
4
  import { verifyStorePaths } from "./verify-store-paths.js";
@@ -19,7 +19,7 @@ export function store(paths, schema) {
19
19
  projections: {
20
20
  current: current(schema)
21
21
  },
22
- write: false,
22
+ write: assertMyRoleIsAdmin(paths),
23
23
  read: assertIsMyTeam(paths),
24
24
  eventTypes
25
25
  };
@@ -5,10 +5,12 @@ export const MEMBER_INVITED = "MEMBER_INVITED";
5
5
  export const ADMIN_INVITED = "ADMIN_INVITED";
6
6
  export const INVITE_ACCEPTED = "INVITE_ACCEPTED";
7
7
  export const INVITE_REJECTED = "INVITE_REJECTED";
8
+ export const CREATED_TEAM = "CREATED_TEAM";
8
9
 
9
10
  export const eventTypes = [
10
11
  BECAME_MEMBER,
11
12
  BECAME_ADMIN,
13
+ CREATED_TEAM,
12
14
  LEFT_TEAM,
13
15
  MEMBER_INVITED,
14
16
  ADMIN_INVITED,
@@ -1,5 +1,5 @@
1
1
  import { purgeUser, resolveUserView, purgeInvited, resolveInvitationView, recordInviteInContext } from "./_shared.js";
2
- import { BECAME_MEMBER, BECAME_ADMIN, LEFT_TEAM, MEMBER_INVITED, ADMIN_INVITED, INVITE_ACCEPTED } from "../events.js";
2
+ import { BECAME_MEMBER, BECAME_ADMIN, LEFT_TEAM, MEMBER_INVITED, ADMIN_INVITED, INVITE_ACCEPTED, CREATED_TEAM } from "../events.js";
3
3
 
4
4
  export default {
5
5
 
@@ -23,6 +23,17 @@ export default {
23
23
 
24
24
  },
25
25
 
26
+ [CREATED_TEAM]: (view, { meta, payload }, context) => {
27
+
28
+ if (context?.eventCount) return view; // must be the first event in the stream
29
+ if (!payload.uid) return view;
30
+ return ({
31
+ ...view,
32
+ admins: { ...view.admins, [payload.uid]: resolveUser(view, payload, null, meta) }
33
+ });
34
+
35
+ },
36
+
26
37
  [INVITE_ACCEPTED]: (view, { meta, payload }, context) => {
27
38
 
28
39
  if (!payload?.id) return view;
@@ -1,4 +1,4 @@
1
- import { BECAME_MEMBER, BECAME_ADMIN, LEFT_TEAM, MEMBER_INVITED, ADMIN_INVITED, INVITE_ACCEPTED } from "../events.js";
1
+ import { BECAME_MEMBER, BECAME_ADMIN, LEFT_TEAM, MEMBER_INVITED, ADMIN_INVITED, INVITE_ACCEPTED, CREATED_TEAM } from "../events.js";
2
2
  import { recordInviteInContext } from "./_shared.js";
3
3
 
4
4
  export default {
@@ -19,6 +19,15 @@ export default {
19
19
 
20
20
  },
21
21
 
22
+ [CREATED_TEAM]: (view, { payload }, context) => {
23
+
24
+ if (context?.eventCount) return view; // must be the first event in the stream
25
+ if (!payload.uid) return view;
26
+ view[payload.uid] = true;
27
+ return view;
28
+
29
+ },
30
+
22
31
  [INVITE_ACCEPTED]: (view, { payload }, context) => {
23
32
 
24
33
  if (!payload?.id) return view;
@@ -2,7 +2,7 @@ import details from "./projections/details.js";
2
2
  import members from "./projections/members.js";
3
3
  import inviteToEmail from "./projections/inviteToEmail.js";
4
4
 
5
- import { BECAME_ADMIN, eventTypes, INVITE_ACCEPTED, INVITE_REJECTED } from "./events.js";
5
+ import { CREATED_TEAM, eventTypes, INVITE_ACCEPTED, INVITE_REJECTED } from "./events.js";
6
6
  import {
7
7
  newDataPayloadId,
8
8
  assertPayloadUidMyUserId,
@@ -50,7 +50,7 @@ export function store(paths) {
50
50
  all(
51
51
 
52
52
  assertTeamDoesNotExist(paths),
53
- assertEventIsOfType(BECAME_ADMIN),
53
+ assertEventIsOfType(CREATED_TEAM),
54
54
  assertPayloadUidMyUserId
55
55
  )
56
56
 
@@ -59,15 +59,15 @@ function displayHelp(commands) {
59
59
 
60
60
  const maybeExamples = examples?.length ?examples.map(x => `--${name}="${x}"`).join(", ") : "";
61
61
  console.log(`${indent}--${name}: ${description}`);
62
- indent += " ".repeat(name.length + 4);
62
+ let innerIndent = indent + " ".repeat(name.length + 4);
63
63
  if (maybeExamples) {
64
64
 
65
- console.log(`${indent}(e.g. ${maybeExamples})`);
65
+ console.log(`${innerIndent}(e.g. ${maybeExamples})`);
66
66
 
67
67
  }
68
68
  if (defaultValue) {
69
69
 
70
- console.log(`${indent}DEFAULT: ${defaultValue}`);
70
+ console.log(`${innerIndent}DEFAULT: ${defaultValue}`);
71
71
 
72
72
  }
73
73