h1v3 0.3.0 → 0.6.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 (73) hide show
  1. package/.vscode/settings.json +3 -0
  2. package/dist/browser/client/auth.js +128 -0
  3. package/dist/browser/client/bus.js +1 -0
  4. package/dist/browser/{web-ui → client}/components/login.js +11 -2
  5. package/dist/browser/client/components/notification.html.js +25 -0
  6. package/dist/browser/client/components/notification.js +27 -0
  7. package/{src/client/web-ui → dist/browser/client}/components/partials/wa-utils.js +1 -0
  8. package/dist/browser/client/context.js +98 -0
  9. package/dist/browser/{event-store/modular.js → client/event-store.js} +21 -14
  10. package/{src/client/web → dist/browser/client}/events.js +5 -1
  11. package/dist/browser/{web/system.js → client/firebase.js} +1 -7
  12. package/dist/browser/client/id.js +37 -0
  13. package/dist/browser/client/logger.js +60 -0
  14. package/dist/browser/client/notifications.js +50 -0
  15. package/{src/client/web-ui → dist/browser/client}/system.js +10 -4
  16. package/dist/browser/client/team/invites.js +119 -0
  17. package/dist/browser/client/team/team.js +143 -0
  18. package/dist/browser/client/team/user.js +62 -0
  19. package/package.json +2 -7
  20. package/src/client/context.js +58 -0
  21. package/src/client/index.js +2 -2
  22. package/src/client/node.js +29 -1
  23. package/src/commands/generate-rules.js +56 -26
  24. package/src/commands/vendor.js +90 -21
  25. package/src/configuration.js +106 -0
  26. package/src/event-store/{initialise.js → configuration.js} +15 -4
  27. package/src/event-store/projections.js +40 -11
  28. package/src/exec-eventstore.js +52 -10
  29. package/src/index.js +20 -4
  30. package/src/load-configuration.js +17 -3
  31. package/src/membership/index.js +5 -2
  32. package/src/membership/team-details/events.js +5 -0
  33. package/src/membership/team-details/projections/current.js +9 -0
  34. package/src/membership/team-details/store.js +30 -0
  35. package/src/membership/team-details/verify-store-paths.js +8 -0
  36. package/src/membership/team-membership/events.js +17 -0
  37. package/src/membership/team-membership/projections/_shared.js +55 -0
  38. package/src/membership/team-membership/projections/details.js +105 -0
  39. package/src/membership/team-membership/projections/inviteToEmail.js +25 -0
  40. package/src/membership/team-membership/projections/members.js +68 -0
  41. package/src/membership/team-membership/store.js +56 -0
  42. package/src/membership/user-profile/events.js +6 -0
  43. package/src/membership/user-profile/projections/current.js +9 -0
  44. package/src/membership/user-profile/store.js +28 -0
  45. package/src/membership/user-teams/events.js +5 -0
  46. package/src/membership/user-teams/projections/current.js +21 -0
  47. package/src/membership/user-teams/store.js +27 -0
  48. package/src/membership/userInvites/events.js +12 -0
  49. package/src/membership/userInvites/projections/pending.js +45 -0
  50. package/src/membership/userInvites/store.js +46 -0
  51. package/src/paths.js +5 -0
  52. package/src/rules.js +153 -0
  53. package/src/schema.js +91 -1
  54. package/src/system/main.js +1 -1
  55. package/dist/browser/web/events.js +0 -7
  56. package/dist/browser/web/login.js +0 -48
  57. package/dist/browser/web-ui/components/notification.html.js +0 -12
  58. package/dist/browser/web-ui/components/notification.js +0 -25
  59. package/dist/browser/web-ui/components/partials/wa-utils.js +0 -17
  60. package/dist/browser/web-ui/errors.js +0 -23
  61. package/dist/browser/web-ui/system.js +0 -20
  62. package/scripts/dist-client.js +0 -31
  63. package/src/client/modular.js +0 -82
  64. package/src/client/web/login.js +0 -48
  65. package/src/client/web/system.js +0 -67
  66. package/src/client/web-ui/components/login.html.js +0 -44
  67. package/src/client/web-ui/components/login.js +0 -74
  68. package/src/client/web-ui/components/notification.html.js +0 -12
  69. package/src/client/web-ui/components/notification.js +0 -25
  70. package/src/client/web-ui/errors.js +0 -23
  71. package/src/membership/membership-store.js +0 -88
  72. package/src/membership/user-profile-store.js +0 -39
  73. /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,6 @@
1
+
2
+ export const DETAILS_UPDATED = "DETAILS_UPDATED";
3
+
4
+ export const eventTypes = [
5
+ DETAILS_UPDATED
6
+ ];
@@ -0,0 +1,9 @@
1
+ import { DETAILS_UPDATED } from "../events.js";
2
+ import { patchViewWithSchema } from "../../../schema.js";
3
+
4
+ export default schema => ({
5
+
6
+ [DETAILS_UPDATED]: (view, { payload }) =>
7
+ patchViewWithSchema({ view, payload, schema })
8
+
9
+ });
@@ -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
+ }
@@ -0,0 +1,5 @@
1
+ export const ADDED_TO_TEAM = "ADDED_TO_TEAM";
2
+
3
+ export const LEFT_TEAM = "LEFT_TEAM";
4
+
5
+ export const eventTypes = [ADDED_TO_TEAM, LEFT_TEAM];
@@ -0,0 +1,21 @@
1
+ import { ADDED_TO_TEAM, LEFT_TEAM } from "../events.js";
2
+
3
+ export default {
4
+ [ADDED_TO_TEAM]: (view, { payload }) => {
5
+
6
+ if (!payload || typeof payload !== "object") return view;
7
+ if (!payload.tid) return view;
8
+ view.teams = Object.assign({}, view.teams, { [payload.tid]: Date.now() });
9
+ return view;
10
+
11
+ },
12
+ [LEFT_TEAM]: (view, { payload }) => {
13
+
14
+ if (!payload || typeof payload !== "object") return view;
15
+ if (!payload.tid) return view;
16
+ if (!view.teams) return view;
17
+ delete view.teams[payload.tid];
18
+ return view;
19
+
20
+ }
21
+ };
@@ -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
@@ -0,0 +1,5 @@
1
+ export function refPathToTriggerPath(ref) {
2
+
3
+ return ref.replaceAll(/\/\$([^/]*)/g, "/{$1}");
4
+
5
+ }
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)})`;