-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathrules.bolt
More file actions
187 lines (156 loc) · 4.08 KB
/
rules.bolt
File metadata and controls
187 lines (156 loc) · 4.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
type Matrix<Type> extends Map<Index, Map<Index, Type>> {}
type Presence<Type> extends Map<Type, Boolean> {}
type CurrentUser extends UserId {
validate() { return isCurrentUser(this)}
}
isCurrentUser(userId) {
return auth != null && auth.uid == userId;
}
type Clues {
across: Matrix<String>
down: Matrix<String>
}
type Crossword {
rows: Number
symmetric: Boolean
themeEntries: Boolean[]
// Looks like a gap in Bolt where it doesn't notice that the value can be null
// so without specifying `| Null` here explicity, it will add a validation to require
// the children
clues: Clues | Null
boxes: Matrix<Box>
title: String | Null
read() {
auth.uid == root.permissions[key()].owner ||
root.permissions[key()].collaborators[auth.uid] ||
root.permissions[key()].global
}
create() {
auth.uid == root.permissions[key()].owner &&
root.users[auth.uid].crosswords[key()] != null
}
delete() {
auth.uid == root.permissions[key()].owner
}
update() {
!root.permissions[key()].readonly && (
auth.uid == root.permissions[key()].owner ||
root.permissions[key()].collaborators[auth.uid] ||
root.permissions[key()].global
)
}
}
type Index extends String {
validate() {
this.test(/^[0-9]+$/)
}
}
type Direction extends String {
validate() {
this.test(/(across|down)/)
}
}
type Box {
blocked: Boolean | Null,
circled: Boolean | Null,
shaded: Boolean | Null,
content: String | Null,
}
path /crosswords is Crossword[] {}
type CrosswordMetadata {
title: String | Null,
}
type WordlistEntry {
word: String,
usedBy: Presence<CrosswordId>,
}
type User {
crosswords: CrosswordMetadata[],
wordlist: WordlistEntry[],
write() {
key() == auth.uid
}
read() {
key() == auth.uid
}
}
path /users is User[] {}
type Permissions {
// currently this can be Null when `global` is true
// but we should make that explicit
owner: UserId,
collaborators: Presence<UserId>,
global: Boolean | Null,
readonly: Boolean | Null,
create() {
auth.uid != null &&
auth.uid == this.owner &&
root.crosswords[key()] != null
}
update() {
auth.uid == this.owner
}
delete() {
auth.uid == this.owner
}
validate() {
auth.token.isAdmin == true ||
this.owner == auth.uid &&
(
prior(this.owner) == null ||
this.owner == prior(this.owner)
) &&
this.global == prior(this.global) &&
this.readonly == prior(this.readonly)
}
}
// would like to be able to ensure this user exists but we don't own that list
type UserId extends String {}
type CrosswordId extends String {
validate() {
root.crosswords[key()] != null
}
}
path /permissions is Map<CrosswordId, Permissions> {}
/*
* Cursors
* Note: displayName, photoUrl, and color are populated by a cloud function.
* They're marked as nullable here so that the client write of the other data is not blocked.
* TODO: confirm that we there's no way to write a cloud function that lets
* us mark them as non-nullable here
*/
type Cursor {
userId: CurrentUser
row: Number | Null
column: Number | Null
displayName: String | Null
photoURL: String | Null
color: String | Null
write() {
auth.uid == this.userId || auth.uid == prior(this.userId)
}
}
// would rather specify the path this way, but then we can't access the cw id key
// from the cursor key object level
// path /cursors is Map<CrosswordId, Cursor[]> {
path /cursors/{cwid} is Cursor[] {
validate() {
root.crosswords[key()] != null
}
read() {
auth.uid == root.permissions[cwid].owner ||
root.permissions[cwid].collaborators[auth.uid] ||
root.permissions[cwid].global
}
}
type CommunalCrossword {
current: CrosswordId
archive: CrosswordId[]
write() {
return false;
}
read() {
return true;
}
}
path /communalCrossword is CommunalCrossword {}