h1v3 0.4.1 → 0.6.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/.vscode/settings.json +3 -0
- package/dist/browser/client/auth.js +128 -0
- package/dist/browser/client/bus.js +1 -0
- package/{src/client/web-ui → dist/browser/client}/components/login.js +11 -2
- package/dist/browser/client/components/notification.html.js +25 -0
- package/dist/browser/client/components/notification.js +27 -0
- package/{src/client/web-ui → dist/browser/client}/components/partials/wa-utils.js +1 -0
- package/dist/browser/client/context.js +98 -0
- package/dist/browser/{event-store/modular.js → client/event-store.js} +22 -14
- package/dist/browser/{web → client}/events.js +5 -1
- package/{src/client/web/system.js → dist/browser/client/firebase.js} +1 -7
- package/dist/browser/client/id.js +37 -0
- package/dist/browser/client/logger.js +60 -0
- package/dist/browser/client/notifications.js +50 -0
- package/dist/browser/{web-ui → client}/system.js +10 -4
- package/dist/browser/client/team/invites.js +119 -0
- package/dist/browser/client/team/team.js +143 -0
- package/dist/browser/client/team/user.js +62 -0
- package/package.json +2 -7
- package/src/client/context.js +58 -0
- package/src/client/index.js +2 -4
- package/src/client/node.js +2 -2
- package/src/commands/generate-rules.js +56 -39
- package/src/commands/vendor.js +86 -23
- package/src/configuration.js +116 -0
- package/src/event-store/configuration.js +44 -0
- package/src/event-store/projections.js +40 -11
- package/src/exec-eventstore.js +48 -14
- package/src/index.js +20 -4
- package/src/load-configuration.js +17 -3
- package/src/membership/index.js +5 -3
- package/src/membership/team-details/events.js +5 -0
- package/src/membership/team-details/projections/current.js +9 -0
- package/src/membership/team-details/store.js +30 -0
- package/src/membership/team-details/verify-store-paths.js +8 -0
- package/src/membership/team-membership/events.js +17 -0
- package/src/membership/team-membership/projections/_shared.js +55 -0
- package/src/membership/team-membership/projections/details.js +105 -0
- package/src/membership/team-membership/projections/inviteToEmail.js +25 -0
- package/src/membership/team-membership/projections/members.js +68 -0
- package/src/membership/team-membership/store.js +56 -0
- package/src/membership/user-profile/events.js +6 -0
- package/src/membership/user-profile/projections/current.js +9 -0
- package/src/membership/user-profile/store.js +28 -0
- package/src/membership/user-teams/events.js +5 -0
- package/src/membership/{user-teams-store.js → user-teams/projections/current.js} +2 -24
- package/src/membership/user-teams/store.js +27 -0
- package/src/membership/userInvites/events.js +12 -0
- package/src/membership/userInvites/projections/pending.js +45 -0
- package/src/membership/userInvites/store.js +46 -0
- package/src/paths.js +5 -0
- package/src/rules.js +153 -0
- package/src/schema.js +91 -1
- package/src/system/main.js +1 -1
- package/dist/browser/team/team.js +0 -25
- package/dist/browser/web/login.js +0 -48
- package/dist/browser/web/system.js +0 -67
- package/dist/browser/web-ui/components/login.js +0 -74
- package/dist/browser/web-ui/components/notification.html.js +0 -12
- package/dist/browser/web-ui/components/notification.js +0 -25
- package/dist/browser/web-ui/components/partials/wa-utils.js +0 -17
- package/dist/browser/web-ui/errors.js +0 -23
- package/scripts/dist-client.js +0 -32
- package/src/client/modular.js +0 -81
- package/src/client/team-node.js +0 -28
- package/src/client/team.js +0 -38
- package/src/client/web/events.js +0 -7
- package/src/client/web/login.js +0 -48
- package/src/client/web-ui/components/login.html.js +0 -44
- package/src/client/web-ui/components/notification.html.js +0 -12
- package/src/client/web-ui/components/notification.js +0 -25
- package/src/client/web-ui/errors.js +0 -23
- package/src/client/web-ui/system.js +0 -20
- package/src/event-store/initialise.js +0 -28
- package/src/membership/membership-store.js +0 -131
- package/src/membership/user-profile-store.js +0 -42
- /package/dist/browser/{web-ui → client}/components/login.html.js +0 -0
|
@@ -0,0 +1,105 @@
|
|
|
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";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
|
|
6
|
+
[BECAME_MEMBER]: (view, { meta, payload }) => {
|
|
7
|
+
|
|
8
|
+
if (!payload.uid) return view;
|
|
9
|
+
return ({
|
|
10
|
+
...view,
|
|
11
|
+
members: { ...view.members, [payload.uid]: resolveUser(view, payload, null, meta) }
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
,
|
|
15
|
+
|
|
16
|
+
[BECAME_ADMIN]: (view, { meta, payload }) => {
|
|
17
|
+
|
|
18
|
+
if (!payload.uid) return view;
|
|
19
|
+
return ({
|
|
20
|
+
...view,
|
|
21
|
+
admins: { ...view.admins, [payload.uid]: resolveUser(view, payload, null, meta) }
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
[INVITE_ACCEPTED]: (view, { meta, payload }, context) => {
|
|
27
|
+
|
|
28
|
+
if (!payload?.id) return view;
|
|
29
|
+
if (!payload.uid) return view;
|
|
30
|
+
if (!context.invites) return view;
|
|
31
|
+
const record = context.invites[payload.id];
|
|
32
|
+
if (!record) return view;
|
|
33
|
+
switch (record.type) {
|
|
34
|
+
|
|
35
|
+
case MEMBER_INVITED:
|
|
36
|
+
view.members = { ...view.members, [payload.uid]: resolveUser(view, payload, record, meta) };
|
|
37
|
+
break;
|
|
38
|
+
|
|
39
|
+
case ADMIN_INVITED:
|
|
40
|
+
view.admins = { ...view.admins, [payload.uid]: resolveUser(view, payload, record, meta) };
|
|
41
|
+
break;
|
|
42
|
+
|
|
43
|
+
default:
|
|
44
|
+
return view;
|
|
45
|
+
|
|
46
|
+
}
|
|
47
|
+
if (view.invitedMembers) {
|
|
48
|
+
|
|
49
|
+
view.invitedMembers = Object.fromEntries(Object.entries(view.invitedMembers).filter(([key, invite]) => invite.id !== payload.id));
|
|
50
|
+
|
|
51
|
+
}
|
|
52
|
+
if (view.invitedAdmins) {
|
|
53
|
+
|
|
54
|
+
view.invitedAdmins = Object.fromEntries(Object.entries(view.invitedAdmins).filter(([key, invite]) => invite.id !== payload.id));
|
|
55
|
+
|
|
56
|
+
}
|
|
57
|
+
delete context.invites[payload.id];
|
|
58
|
+
return view;
|
|
59
|
+
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
[LEFT_TEAM]: (view, { payload }) => {
|
|
63
|
+
|
|
64
|
+
purgeUser(view, payload);
|
|
65
|
+
return view;
|
|
66
|
+
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
[MEMBER_INVITED]: (view, { meta, payload }, context) => {
|
|
70
|
+
|
|
71
|
+
if (!payload?.email) return view;
|
|
72
|
+
if (!payload?.createdBy) return view;
|
|
73
|
+
if (!payload.id) return view;
|
|
74
|
+
const key = btoa(payload.email.toLowerCase());
|
|
75
|
+
purgeInvited(view, key);
|
|
76
|
+
view.invitedMembers = { ...view.invitedMembers, [key]: resolveInvitationView(payload, meta) };
|
|
77
|
+
recordInviteInContext(context, payload.id, { type: MEMBER_INVITED, payload });
|
|
78
|
+
return view;
|
|
79
|
+
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
[ADMIN_INVITED]: (view, { meta, payload }, context) => {
|
|
83
|
+
|
|
84
|
+
if (!payload?.email) return view;
|
|
85
|
+
if (!payload.createdBy) return view;
|
|
86
|
+
if (!payload.id) return view;
|
|
87
|
+
const key = btoa(payload.email.toLowerCase());
|
|
88
|
+
purgeInvited(view, key);
|
|
89
|
+
view.invitedAdmins = { ...view.invitedAdmins, [key]: resolveInvitationView(payload, meta) };
|
|
90
|
+
recordInviteInContext(context, payload.id, { type: ADMIN_INVITED, payload });
|
|
91
|
+
return view;
|
|
92
|
+
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
function resolveUser(view, { uid, name, email, createdBy }, maybeInvite, meta) {
|
|
98
|
+
|
|
99
|
+
const found = purgeUser(view, { uid });
|
|
100
|
+
name = name || maybeInvite?.payload?.name;
|
|
101
|
+
return resolveUserView({ name, email, createdBy }, found, meta);
|
|
102
|
+
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ADMIN_INVITED, INVITE_ACCEPTED, INVITE_REJECTED, MEMBER_INVITED } from "../events.js";
|
|
2
|
+
import { passThrough } from "./_shared.js";
|
|
3
|
+
|
|
4
|
+
function addToMap(view, { payload }) {
|
|
5
|
+
|
|
6
|
+
if (!(payload?.id && payload.email)) return view;
|
|
7
|
+
return Object.assign({}, view, { [payload.id]: payload.email });
|
|
8
|
+
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function removeFromMap(view, { payload }) {
|
|
12
|
+
|
|
13
|
+
if (!payload.id) return view;
|
|
14
|
+
delete view[payload.id];
|
|
15
|
+
return view;
|
|
16
|
+
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default {
|
|
20
|
+
[MEMBER_INVITED]: addToMap,
|
|
21
|
+
[ADMIN_INVITED]: addToMap,
|
|
22
|
+
[INVITE_ACCEPTED]: removeFromMap,
|
|
23
|
+
[INVITE_REJECTED]: removeFromMap,
|
|
24
|
+
["?"]: passThrough
|
|
25
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { BECAME_MEMBER, BECAME_ADMIN, LEFT_TEAM, MEMBER_INVITED, ADMIN_INVITED, INVITE_ACCEPTED } from "../events.js";
|
|
2
|
+
import { recordInviteInContext } from "./_shared.js";
|
|
3
|
+
|
|
4
|
+
export default {
|
|
5
|
+
|
|
6
|
+
[BECAME_MEMBER]: (view, { payload }) => {
|
|
7
|
+
|
|
8
|
+
if (!payload?.uid) return view;
|
|
9
|
+
view[payload.uid] = true;
|
|
10
|
+
return view;
|
|
11
|
+
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
[BECAME_ADMIN]: (view, { payload }) => {
|
|
15
|
+
|
|
16
|
+
if (!payload?.uid) return view;
|
|
17
|
+
view[payload.uid] = true;
|
|
18
|
+
return view;
|
|
19
|
+
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
[INVITE_ACCEPTED]: (view, { payload }, context) => {
|
|
23
|
+
|
|
24
|
+
if (!payload?.id) return view;
|
|
25
|
+
if (!payload.uid) return view;
|
|
26
|
+
if (!context.invites) return view;
|
|
27
|
+
const record = context.invites[payload.id];
|
|
28
|
+
if (!record) return view;
|
|
29
|
+
switch (record.type) {
|
|
30
|
+
|
|
31
|
+
case MEMBER_INVITED:
|
|
32
|
+
view[payload.uid] = true;
|
|
33
|
+
delete context.invites[payload.id];
|
|
34
|
+
break;
|
|
35
|
+
|
|
36
|
+
case ADMIN_INVITED:
|
|
37
|
+
view[payload.uid] = true;
|
|
38
|
+
delete context.invites[payload.id];
|
|
39
|
+
break;
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
return view;
|
|
43
|
+
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
[LEFT_TEAM]: (view, { payload }) => {
|
|
47
|
+
|
|
48
|
+
if (!payload?.uid) return view;
|
|
49
|
+
delete view[payload.uid];
|
|
50
|
+
return view;
|
|
51
|
+
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
[MEMBER_INVITED]: (view, { payload }, context) => {
|
|
55
|
+
|
|
56
|
+
recordInviteInContext(context, payload.id, { type: MEMBER_INVITED, payload });
|
|
57
|
+
return view;
|
|
58
|
+
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
[ADMIN_INVITED]: (view, { payload }, context) => {
|
|
62
|
+
|
|
63
|
+
recordInviteInContext(context, payload.id, { type: ADMIN_INVITED, payload });
|
|
64
|
+
return view;
|
|
65
|
+
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import details from "./projections/details.js";
|
|
2
|
+
import members from "./projections/members.js";
|
|
3
|
+
import inviteToEmail from "./projections/inviteToEmail.js";
|
|
4
|
+
|
|
5
|
+
import { eventTypes, INVITE_ACCEPTED, INVITE_REJECTED } from "./events.js";
|
|
6
|
+
import {
|
|
7
|
+
newDataPayloadId,
|
|
8
|
+
assertPayloadUidMyUserId,
|
|
9
|
+
assertMyRoleIsAdmin,
|
|
10
|
+
assertIsMyTeam,
|
|
11
|
+
assertMyEmailVerified,
|
|
12
|
+
teamInviteToEmailMapping,
|
|
13
|
+
any,
|
|
14
|
+
all,
|
|
15
|
+
assertEventIsOfType
|
|
16
|
+
} from "../../rules.js";
|
|
17
|
+
import { verifyStorePaths } from "../team-details/verify-store-paths.js";
|
|
18
|
+
|
|
19
|
+
export function store(paths) {
|
|
20
|
+
|
|
21
|
+
verifyStorePaths(paths, {
|
|
22
|
+
team: {
|
|
23
|
+
membership: { path: String }
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
return {
|
|
27
|
+
ref: paths.team.membership.path,
|
|
28
|
+
projections: {
|
|
29
|
+
details,
|
|
30
|
+
members,
|
|
31
|
+
inviteToEmail
|
|
32
|
+
},
|
|
33
|
+
write: any(
|
|
34
|
+
|
|
35
|
+
assertMyRoleIsAdmin(paths),
|
|
36
|
+
all(
|
|
37
|
+
|
|
38
|
+
assertEventIsOfType(INVITE_ACCEPTED, INVITE_REJECTED),
|
|
39
|
+
all(
|
|
40
|
+
|
|
41
|
+
`${teamInviteToEmailMapping(paths)}.child(${newDataPayloadId}).val() === auth.email`,
|
|
42
|
+
assertMyEmailVerified,
|
|
43
|
+
assertPayloadUidMyUserId
|
|
44
|
+
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
),
|
|
50
|
+
read: assertIsMyTeam(paths),
|
|
51
|
+
eventTypes
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export * from "./events.js";
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { assertUserIsMe } from "../../rules.js";
|
|
2
|
+
import { verifyStorePaths } from "../team-details/verify-store-paths.js";
|
|
3
|
+
import { eventTypes } from "./events.js";
|
|
4
|
+
import current from "./projections/current.js";
|
|
5
|
+
|
|
6
|
+
export * from "./events.js";
|
|
7
|
+
|
|
8
|
+
export function store(paths, schema) {
|
|
9
|
+
|
|
10
|
+
verifyStorePaths(paths, {
|
|
11
|
+
user: {
|
|
12
|
+
profile: { path: String }
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
if (!schema) throw new Error("Missing schema");
|
|
16
|
+
return {
|
|
17
|
+
|
|
18
|
+
ref: paths.user.profile.path,
|
|
19
|
+
projections: {
|
|
20
|
+
current: current(schema)
|
|
21
|
+
},
|
|
22
|
+
write: assertUserIsMe,
|
|
23
|
+
read: assertUserIsMe,
|
|
24
|
+
eventTypes
|
|
25
|
+
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
}
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
export const LEFT_TEAM = "LEFT_TEAM";
|
|
4
|
-
|
|
5
|
-
const current = {
|
|
1
|
+
import { ADDED_TO_TEAM, LEFT_TEAM } from "../events.js";
|
|
6
2
|
|
|
3
|
+
export default {
|
|
7
4
|
[ADDED_TO_TEAM]: (view, { payload }) => {
|
|
8
5
|
|
|
9
6
|
if (!payload || typeof payload !== "object") return view;
|
|
@@ -21,23 +18,4 @@ const current = {
|
|
|
21
18
|
return view;
|
|
22
19
|
|
|
23
20
|
}
|
|
24
|
-
|
|
25
21
|
};
|
|
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
|
-
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { assertUserIsMe } from "../../rules.js";
|
|
2
|
+
import { verifyStorePaths } from "../team-details/verify-store-paths.js";
|
|
3
|
+
import { eventTypes } from "./events.js";
|
|
4
|
+
import current from "./projections/current.js";
|
|
5
|
+
|
|
6
|
+
export function store(paths) {
|
|
7
|
+
|
|
8
|
+
verifyStorePaths(paths, {
|
|
9
|
+
user: {
|
|
10
|
+
teams: { path: String }
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
|
|
15
|
+
ref: paths.user.teams.path,
|
|
16
|
+
projections: {
|
|
17
|
+
current
|
|
18
|
+
},
|
|
19
|
+
eventTypes: eventTypes,
|
|
20
|
+
read: assertUserIsMe,
|
|
21
|
+
write: assertUserIsMe
|
|
22
|
+
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export * from "./events.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const INVITED_TO_TEAM = "INVITED_TO_TEAM";
|
|
2
|
+
|
|
3
|
+
export const ACCEPTED_INVITATION = "ACCEPTED_INVITATION";
|
|
4
|
+
|
|
5
|
+
export const REJECTED_INVITATION = "REJECTED_INVITATION";
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
export const eventTypes = [
|
|
9
|
+
INVITED_TO_TEAM,
|
|
10
|
+
ACCEPTED_INVITATION,
|
|
11
|
+
REJECTED_INVITATION
|
|
12
|
+
];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ACCEPTED_INVITATION, INVITED_TO_TEAM, REJECTED_INVITATION } from "../events.js";
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
|
|
5
|
+
[INVITED_TO_TEAM]: (view, { meta, payload }) => {
|
|
6
|
+
|
|
7
|
+
if (!payload?.tid) return view;
|
|
8
|
+
if (!payload.id) return view;
|
|
9
|
+
if (!payload.email) return view;
|
|
10
|
+
|
|
11
|
+
view.email = payload.email || view.email;
|
|
12
|
+
view.invites = Object.fromEntries(Object
|
|
13
|
+
.entries(view.invites || {})
|
|
14
|
+
.filter(([_, invite]) => invite?.tid !== payload.tid) // filter out existing team invites
|
|
15
|
+
.concat([[
|
|
16
|
+
payload.id,
|
|
17
|
+
{
|
|
18
|
+
...payload,
|
|
19
|
+
when: meta.when || new Date().toISOString()
|
|
20
|
+
}
|
|
21
|
+
]]) // add the incoming one
|
|
22
|
+
);
|
|
23
|
+
return view;
|
|
24
|
+
|
|
25
|
+
},
|
|
26
|
+
[ACCEPTED_INVITATION]: (view, { payload }) => {
|
|
27
|
+
|
|
28
|
+
if (!payload?.id) return view;
|
|
29
|
+
if (!view.invites) return view;
|
|
30
|
+
if (!(payload.id in view.invites)) return view;
|
|
31
|
+
delete view.invites[payload.id];
|
|
32
|
+
return view;
|
|
33
|
+
|
|
34
|
+
},
|
|
35
|
+
[REJECTED_INVITATION]: (view, { payload }) => {
|
|
36
|
+
|
|
37
|
+
if (!payload?.id) return view;
|
|
38
|
+
if (!view.invites) return view;
|
|
39
|
+
if (!(payload.id in view.invites)) return view;
|
|
40
|
+
delete view.invites[payload.id];
|
|
41
|
+
return view;
|
|
42
|
+
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { eventTypes, ACCEPTED_INVITATION, REJECTED_INVITATION } from "./events.js";
|
|
2
|
+
import pending from "./projections/pending.js";
|
|
3
|
+
import { PROJECTIONS } from "../../index.js";
|
|
4
|
+
import { verifyStorePaths } from "../team-details/verify-store-paths.js";
|
|
5
|
+
import { all, any, assertEventIsOfType, assertDataFieldIsMyVerifiedEmail, assertMyVerifiedEmail, not, assertNewDataHas } from "../../rules.js";
|
|
6
|
+
|
|
7
|
+
const assertAcceptedOrRejectedEvent = assertEventIsOfType(
|
|
8
|
+
ACCEPTED_INVITATION,
|
|
9
|
+
REJECTED_INVITATION
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
const assertAcceptedOrRejectedEvent_and_storeIsForMyVerifiedEmail = ({ userInvites: { path } }) =>
|
|
13
|
+
all(
|
|
14
|
+
assertAcceptedOrRejectedEvent,
|
|
15
|
+
assertMyVerifiedEmail(`${path}/${PROJECTIONS}/pending`)
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export function store(paths) {
|
|
19
|
+
|
|
20
|
+
verifyStorePaths(paths, {
|
|
21
|
+
userInvites: { path: String }
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
|
|
26
|
+
ref: paths.userInvites.path,
|
|
27
|
+
projections: {
|
|
28
|
+
pending
|
|
29
|
+
},
|
|
30
|
+
eventTypes: eventTypes,
|
|
31
|
+
read: assertDataFieldIsMyVerifiedEmail("email"),
|
|
32
|
+
write: any(
|
|
33
|
+
assertAcceptedOrRejectedEvent_and_storeIsForMyVerifiedEmail(paths),
|
|
34
|
+
all(
|
|
35
|
+
not(assertAcceptedOrRejectedEvent),
|
|
36
|
+
assertNewDataHas("payload/email")
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export * from "./events.js";
|
|
45
|
+
|
|
46
|
+
|
package/src/paths.js
ADDED
package/src/rules.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { PROJECTIONS } from "../dist/browser/client/event-store.js";
|
|
2
|
+
|
|
3
|
+
export const any = (...args) =>
|
|
4
|
+
|
|
5
|
+
args.map(x => `(${x})`).join(" || ");
|
|
6
|
+
|
|
7
|
+
export const all = (...args) =>
|
|
8
|
+
|
|
9
|
+
args.map(x => `(${x})`).join(" && ");
|
|
10
|
+
|
|
11
|
+
export const not = x =>
|
|
12
|
+
|
|
13
|
+
`!(${x})`;
|
|
14
|
+
|
|
15
|
+
const newDataField = fieldName =>
|
|
16
|
+
|
|
17
|
+
`newData.child('${fieldName}')`;
|
|
18
|
+
|
|
19
|
+
export const newDataPayloadId =
|
|
20
|
+
|
|
21
|
+
`${newDataField("payload/id")}.val()`;
|
|
22
|
+
|
|
23
|
+
export const newDataType =
|
|
24
|
+
|
|
25
|
+
`${newDataField("type")}.val()`;
|
|
26
|
+
|
|
27
|
+
export const teamInviteToEmailMapping = ({ team: { membership: { path } } }) =>
|
|
28
|
+
|
|
29
|
+
`root.child('${path}/${PROJECTIONS}/inviteToEmail')`;
|
|
30
|
+
|
|
31
|
+
export const assertDoesNotAlreadyExist =
|
|
32
|
+
|
|
33
|
+
"!data.exists()";
|
|
34
|
+
|
|
35
|
+
export const assertIAmAuthenticated =
|
|
36
|
+
|
|
37
|
+
"auth.uid !== null";
|
|
38
|
+
|
|
39
|
+
export const assertPayloadUidMyUserId =
|
|
40
|
+
|
|
41
|
+
`${newDataField("payload/uid")}.val() === auth.uid`;
|
|
42
|
+
|
|
43
|
+
export const assertMyEmailVerified =
|
|
44
|
+
|
|
45
|
+
"auth.email_verified === true";
|
|
46
|
+
|
|
47
|
+
export const assertDataFieldIsMyVerifiedEmail = fieldName =>
|
|
48
|
+
|
|
49
|
+
all(
|
|
50
|
+
`data.child('${fieldName}').val() == auth.email`,
|
|
51
|
+
assertMyEmailVerified
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
export const assertMyVerifiedEmail = path =>
|
|
55
|
+
|
|
56
|
+
all(
|
|
57
|
+
`root.child('${path}/email').val() == auth.email`,
|
|
58
|
+
assertMyEmailVerified
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
export const assertUserIsMe =
|
|
62
|
+
|
|
63
|
+
"$uid == auth.uid";
|
|
64
|
+
|
|
65
|
+
export const assertMyUserIdIn = path =>
|
|
66
|
+
|
|
67
|
+
`root.child('${path}').child(auth.uid).exists()`;
|
|
68
|
+
|
|
69
|
+
export const assertEventIsOfType = (...types) =>
|
|
70
|
+
|
|
71
|
+
`${newDataType}.matches(/^(${types.join("|")})$/)`;
|
|
72
|
+
|
|
73
|
+
export const assertIsMyTeam = ({ team: { membership: { path } } }) =>
|
|
74
|
+
|
|
75
|
+
assertMyUserIdIn(`${path}/${PROJECTIONS}/members`);
|
|
76
|
+
|
|
77
|
+
export const assertMyRoleIsAdmin = ({ team: { membership: { path } } }) =>
|
|
78
|
+
|
|
79
|
+
assertMyUserIdIn(`${path}/${PROJECTIONS}/details/admins`);
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
export const assertNewDataHas = fieldName =>
|
|
83
|
+
|
|
84
|
+
`${newDataField(fieldName)}.exists()`;
|
|
85
|
+
|
|
86
|
+
export const assertNewDataDoesNotHave = fieldName =>
|
|
87
|
+
|
|
88
|
+
`!${assertNewDataHas(fieldName)}`;
|
|
89
|
+
|
|
90
|
+
const assertNewDataHasUncheckedString = fieldName =>
|
|
91
|
+
|
|
92
|
+
`${newDataField(fieldName)}.isString()`;
|
|
93
|
+
|
|
94
|
+
const truthies = (...xs) => xs.filter(x => x);
|
|
95
|
+
|
|
96
|
+
export const assertNewDataHasString = (fieldName, maxLength = 1000, minLength = 0) =>
|
|
97
|
+
|
|
98
|
+
all(
|
|
99
|
+
...truthies(
|
|
100
|
+
assertNewDataHasUncheckedString(fieldName),
|
|
101
|
+
`${newDataField(fieldName)}.val().length <= ${maxLength}`,
|
|
102
|
+
minLength && `${newDataField(fieldName)}.val().length >= ${minLength}`
|
|
103
|
+
)
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
export const assertNewDataHasNumber = fieldName =>
|
|
108
|
+
|
|
109
|
+
`${newDataField(fieldName)}.isNumber()`;
|
|
110
|
+
|
|
111
|
+
const assertNewDataFieldValueMatches = (fieldName, pattern) =>
|
|
112
|
+
|
|
113
|
+
`${newDataField(fieldName)}.val().matches(/^${pattern}$/)`;
|
|
114
|
+
|
|
115
|
+
export const assertNewDataHasFieldMatching = (fieldName, pattern) =>
|
|
116
|
+
|
|
117
|
+
all(
|
|
118
|
+
assertNewDataHasUncheckedString(fieldName),
|
|
119
|
+
assertNewDataFieldValueMatches(fieldName, pattern)
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const hexCharacters = "[0-9a-fA-F]";
|
|
123
|
+
|
|
124
|
+
export const assertNewDataHasHexCharacters = (fieldName, characterCount) =>
|
|
125
|
+
|
|
126
|
+
assertNewDataHasFieldMatching(fieldName, `${hexCharacters}{${characterCount}}`);
|
|
127
|
+
|
|
128
|
+
const lowerHexCharacters = "[0-9a-f]";
|
|
129
|
+
|
|
130
|
+
export const assertNewDataHasLowercaseHexCharacters = (fieldName, characterCount) =>
|
|
131
|
+
|
|
132
|
+
assertNewDataHasFieldMatching(fieldName, `${lowerHexCharacters}{${characterCount}}`);
|
|
133
|
+
|
|
134
|
+
const uuidPattern = [
|
|
135
|
+
`${hexCharacters}{8}`,
|
|
136
|
+
`${hexCharacters}{4}`,
|
|
137
|
+
`${hexCharacters}{4}`,
|
|
138
|
+
`${hexCharacters}{4}`,
|
|
139
|
+
`${hexCharacters}{12}`
|
|
140
|
+
].join("-");
|
|
141
|
+
|
|
142
|
+
export const assertNewDataHasUUID = fieldName =>
|
|
143
|
+
|
|
144
|
+
assertNewDataHasFieldMatching(fieldName, uuidPattern);
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
export const assertNewDataHasOneOf = (fieldName, ...values) =>
|
|
148
|
+
|
|
149
|
+
assertNewDataHasFieldMatching(fieldName, `(${values.join("|")})`);
|
|
150
|
+
|
|
151
|
+
export const assertNewDataFieldDoesNotMatch = (fieldName, pattern) =>
|
|
152
|
+
|
|
153
|
+
`!(${assertNewDataFieldValueMatches(fieldName, pattern)})`;
|