···
1
1
PORT=8000
2
2
CHURROS_API_URL=http://localhost:4000/graphql
3
3
+
DATABASE_URL="postgres://postgres:dev@localhost:5432/postgres?schema=public"
···
1
1
-
# Created by https://www.toptal.com/developers/gitignore/api/go
2
2
-
# Edit at https://www.toptal.com/developers/gitignore?templates=go
1
1
+
# Created by https://www.toptal.com/developers/gitignore/api/go,node
2
2
+
# Edit at https://www.toptal.com/developers/gitignore?templates=go,node
3
3
4
4
### Go ###
5
5
# If you prefer the allow list template instead of the deny list, see community template:
···
24
24
# Go workspace file
25
25
go.work
26
26
27
27
-
# End of https://www.toptal.com/developers/gitignore/api/go
27
27
+
### Node ###
28
28
+
# Logs
29
29
+
logs
30
30
+
*.log
31
31
+
npm-debug.log*
32
32
+
yarn-debug.log*
33
33
+
yarn-error.log*
34
34
+
lerna-debug.log*
35
35
+
.pnpm-debug.log*
36
36
+
37
37
+
# Diagnostic reports (https://nodejs.org/api/report.html)
38
38
+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
39
39
+
40
40
+
# Runtime data
41
41
+
pids
42
42
+
*.pid
43
43
+
*.seed
44
44
+
*.pid.lock
45
45
+
46
46
+
# Directory for instrumented libs generated by jscoverage/JSCover
47
47
+
lib-cov
48
48
+
49
49
+
# Coverage directory used by tools like istanbul
50
50
+
coverage
51
51
+
*.lcov
52
52
+
53
53
+
# nyc test coverage
54
54
+
.nyc_output
55
55
+
56
56
+
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
57
57
+
.grunt
58
58
+
59
59
+
# Bower dependency directory (https://bower.io/)
60
60
+
bower_components
61
61
+
62
62
+
# node-waf configuration
63
63
+
.lock-wscript
64
64
+
65
65
+
# Compiled binary addons (https://nodejs.org/api/addons.html)
66
66
+
build/Release
67
67
+
68
68
+
# Dependency directories
69
69
+
node_modules/
70
70
+
jspm_packages/
71
71
+
72
72
+
# Snowpack dependency directory (https://snowpack.dev/)
73
73
+
web_modules/
74
74
+
75
75
+
# TypeScript cache
76
76
+
*.tsbuildinfo
77
77
+
78
78
+
# Optional npm cache directory
79
79
+
.npm
80
80
+
81
81
+
# Optional eslint cache
82
82
+
.eslintcache
83
83
+
84
84
+
# Optional stylelint cache
85
85
+
.stylelintcache
86
86
+
87
87
+
# Microbundle cache
88
88
+
.rpt2_cache/
89
89
+
.rts2_cache_cjs/
90
90
+
.rts2_cache_es/
91
91
+
.rts2_cache_umd/
92
92
+
93
93
+
# Optional REPL history
94
94
+
.node_repl_history
95
95
+
96
96
+
# Output of 'npm pack'
97
97
+
*.tgz
98
98
+
99
99
+
# Yarn Integrity file
100
100
+
.yarn-integrity
101
101
+
102
102
+
# dotenv environment variable files
28
103
.env
104
104
+
.env.development.local
105
105
+
.env.test.local
106
106
+
.env.production.local
107
107
+
.env.local
108
108
+
109
109
+
# parcel-bundler cache (https://parceljs.org/)
110
110
+
.cache
111
111
+
.parcel-cache
112
112
+
113
113
+
# Next.js build output
114
114
+
.next
115
115
+
out
116
116
+
117
117
+
# Nuxt.js build / generate output
118
118
+
.nuxt
119
119
+
dist
120
120
+
121
121
+
# Gatsby files
122
122
+
.cache/
123
123
+
# Comment in the public line in if your project uses Gatsby and not Next.js
124
124
+
# https://nextjs.org/blog/next-9-1#public-directory-support
125
125
+
# public
126
126
+
127
127
+
# vuepress build output
128
128
+
.vuepress/dist
129
129
+
130
130
+
# vuepress v2.x temp and cache directory
131
131
+
.temp
132
132
+
133
133
+
# Docusaurus cache and generated files
134
134
+
.docusaurus
135
135
+
136
136
+
# Serverless directories
137
137
+
.serverless/
138
138
+
139
139
+
# FuseBox cache
140
140
+
.fusebox/
141
141
+
142
142
+
# DynamoDB Local files
143
143
+
.dynamodb/
144
144
+
145
145
+
# TernJS port file
146
146
+
.tern-port
147
147
+
148
148
+
# Stores VSCode versions used for testing VSCode extensions
149
149
+
.vscode-test
150
150
+
151
151
+
# yarn v2
152
152
+
.yarn/cache
153
153
+
.yarn/unplugged
154
154
+
.yarn/build-state.yml
155
155
+
.yarn/install-state.gz
156
156
+
.pnp.*
157
157
+
158
158
+
### Node Patch ###
159
159
+
# Serverless Webpack directories
160
160
+
.webpack/
161
161
+
162
162
+
# Optional stylelint cache
163
163
+
164
164
+
# SvelteKit build / generate output
165
165
+
.svelte-kit
166
166
+
167
167
+
# End of https://www.toptal.com/developers/gitignore/api/go,node
168
168
+
bin
···
1
1
dev:
2
2
-
go run main.go
2
2
+
go run server/main.go
3
3
4
4
+
build:
5
5
+
go build -o bin/server server/main.go
4
6
7
7
+
updateschema:
8
8
+
curl -fsSL https://git.inpt.fr/churros/churros/-/raw/main/packages/db/prisma/schema.prisma -o schema.prisma
···
1
1
package notella
2
2
3
3
import (
4
4
+
"context"
4
5
"fmt"
5
5
-
"github.com/SherClockHolmes/webpush-go"
6
6
"strings"
7
7
+
8
8
+
"git.inpt.fr/churros/notella/db"
9
9
+
"github.com/SherClockHolmes/webpush-go"
7
10
)
8
11
12
12
+
var prisma = db.NewClient()
13
13
+
9
14
type ChurrosId struct {
10
15
Type string
11
16
LocalID string
17
17
+
}
18
18
+
19
19
+
func (id ChurrosId) String() string {
20
20
+
return fmt.Sprintf("%s:%s", id.Type, id.LocalID)
12
21
}
13
22
14
23
func ParseChurrosId(churrosId string) (ChurrosId, error) {
···
24
33
}
25
34
26
35
// UnmarshalText implements the encoding.TextUnmarshaler interface for ChurrosId.
27
27
-
func (id ChurrosId) UnmarshalText(text []byte) (err error) {
36
36
+
func (id *ChurrosId) UnmarshalText(text []byte) error {
28
37
s := string(text)
29
38
30
30
-
id, err = ParseChurrosId(s)
39
39
+
parsed, err := ParseChurrosId(s)
31
40
if err != nil {
32
41
return err
33
42
}
34
43
44
44
+
id.Type = parsed.Type
45
45
+
id.LocalID = parsed.LocalID
46
46
+
35
47
return nil
36
48
}
37
49
38
38
-
func notificationSubscriptionsOf(userUid string) []webpush.Subscription {
39
39
-
return []webpush.Subscription{}
50
50
+
func notificationSubscriptionsOf(userUid string) (subscriptions []webpush.Subscription, err error) {
51
51
+
if err := prisma.Prisma.Connect(); err != nil {
52
52
+
return nil, fmt.Errorf("could not connect to prisma: %w", err)
53
53
+
}
54
54
+
subs, err := prisma.NotificationSubscription.FindMany(
55
55
+
db.NotificationSubscription.Owner.Where(db.User.UID.Equals(userUid)),
56
56
+
).Exec(context.Background())
57
57
+
58
58
+
if err != nil {
59
59
+
return subscriptions, fmt.Errorf("while getting notification subscriptions from database: %w", err)
60
60
+
}
61
61
+
62
62
+
for _, sub := range subs {
63
63
+
subscriptions = append(subscriptions, webpush.Subscription{
64
64
+
Endpoint: sub.Endpoint,
65
65
+
Keys: webpush.Keys{
66
66
+
Auth: sub.AuthKey,
67
67
+
P256dh: sub.P256DhKey,
68
68
+
},
69
69
+
})
70
70
+
}
71
71
+
72
72
+
return subscriptions, nil
40
73
}
···
1
1
+
# gitignore generated by Prisma Client Go. DO NOT EDIT.
2
2
+
*_gen.go
···
1
1
+
package notella
2
2
+
3
3
+
type Event = string
4
4
+
5
5
+
var (
6
6
+
EventNewTicket Event = "new_ticket"
7
7
+
EventNewPost Event = "new_post"
8
8
+
EventGodchildRequest Event = "godchild_request"
9
9
+
EventNewComment Event = "new_comment"
10
10
+
EventCommentReply Event = "comment_reply"
11
11
+
)
···
5
5
require (
6
6
github.com/caarlos0/env/v11 v11.2.2
7
7
github.com/ewen-lbh/label-logger-go v0.0.0-20241011201023-2c63f6a50d58
8
8
+
github.com/google/uuid v1.6.0
9
9
+
github.com/joho/godotenv v1.5.1
10
10
+
github.com/segmentio/encoding v0.4.0
11
11
+
github.com/shopspring/decimal v1.4.0
12
12
+
github.com/steebchen/prisma-client-go v0.42.0
8
13
)
9
14
10
15
require (
11
16
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
17
17
+
github.com/segmentio/asm v1.1.3 // indirect
12
18
golang.org/x/crypto v0.9.0 // indirect
13
19
)
14
20
···
4
4
github.com/caarlos0/env/v11 v11.2.2/go.mod h1:JBfcdeQiBoI3Zh1QRAWfe+tpiNTmDtcCj/hHHHMx0vc=
5
5
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ=
6
6
github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w=
7
7
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
8
8
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7
9
github.com/ewen-lbh/label-logger-go v0.0.0-20241011201023-2c63f6a50d58 h1:oS+iVtATEd6oi6vA0Zn8tWhL7K1KEyE5jS2u6K3I0cU=
8
10
github.com/ewen-lbh/label-logger-go v0.0.0-20241011201023-2c63f6a50d58/go.mod h1:ORVakjovWm+MfrGXmHBZAJvxNqYwAxdG3Sev8CXXChM=
9
11
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
10
12
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
13
13
+
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
14
14
+
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
11
15
github.com/gosuri/uilive v0.0.4 h1:hUEBpQDj8D8jXgtCdBu7sWsy5sbW/5GhuO8KBwJ2jyY=
12
16
github.com/gosuri/uilive v0.0.4/go.mod h1:V/epo5LjjlDE5RJUcqx8dbw+zc93y5Ya3yg8tfZ74VI=
13
17
github.com/gosuri/uiprogress v0.0.1 h1:0kpv/XY/qTmFWl/SkaJykZXrBBzwwadmW8fRb7RJSxw=
14
18
github.com/gosuri/uiprogress v0.0.1/go.mod h1:C1RTYn4Sc7iEyf6j8ft5dyoZ4212h8G1ol9QQluh5+0=
19
19
+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
20
20
+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
15
21
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
16
22
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
17
23
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
18
24
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
25
25
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
26
26
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
27
27
+
github.com/segmentio/asm v1.1.3 h1:WM03sfUOENvvKexOLp+pCqgb/WDjsi7EK8gIsICtzhc=
28
28
+
github.com/segmentio/asm v1.1.3/go.mod h1:Ld3L4ZXGNcSLRg4JBsZ3//1+f/TjYl0Mzen/DQy1EJg=
29
29
+
github.com/segmentio/encoding v0.4.0 h1:MEBYvRqiUB2nfR2criEXWqwdY6HJOUrCn5hboVOVmy8=
30
30
+
github.com/segmentio/encoding v0.4.0/go.mod h1:/d03Cd8PoaDeceuhUUUQWjU0KhWjrmYrWPgtJHYZSnI=
31
31
+
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
32
32
+
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
33
33
+
github.com/steebchen/prisma-client-go v0.42.0 h1:83keN+4jGvoTccCKCk74UU5JQj6pOwPcg3/zkoqxKJE=
34
34
+
github.com/steebchen/prisma-client-go v0.42.0/go.mod h1:wp2xU9HO5WIefc65vcl1HOiFUzaHKyOhHw5atrzs8hc=
35
35
+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
36
36
+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
19
37
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
20
38
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
21
39
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
···
54
72
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
55
73
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
56
74
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
75
75
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
76
76
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
···
1
1
package notella
2
2
3
3
-
import "time"
3
3
+
import (
4
4
+
"fmt"
5
5
+
"time"
6
6
+
)
4
7
5
8
type ScheduledJob struct {
6
6
-
ID string
7
7
-
When time.Time
8
8
-
ChurrosObjectId ChurrosId
9
9
+
ID string `json:"id"`
10
10
+
When time.Time `json:"when"`
11
11
+
Object ChurrosId `json:"object"`
12
12
+
Event Event `json:"event"`
9
13
}
10
14
11
15
func (job ScheduledJob) ShouldRun() bool {
12
16
return time.Now().After(job.When)
13
17
}
14
18
15
15
-
func (job ScheduledJob) Run() {
19
19
+
func (job ScheduledJob) Run() error {
20
20
+
subscriptions, err := notificationSubscriptionsOf("versairea")
21
21
+
if err != nil {
22
22
+
return fmt.Errorf("while getting notification subscriptions for %s: %w", "versairea", err)
23
23
+
}
16
24
25
25
+
fmt.Printf("%+v\n", subscriptions)
26
26
+
27
27
+
switch job.Event {
28
28
+
// TODO
29
29
+
}
30
30
+
31
31
+
return nil
17
32
}
···
1
1
+
{
2
2
+
"name": "notella",
3
3
+
"lockfileVersion": 3,
4
4
+
"requires": true,
5
5
+
"packages": {
6
6
+
"": {
7
7
+
"dependencies": {
8
8
+
"@prisma/client": "^5.20.0"
9
9
+
},
10
10
+
"devDependencies": {
11
11
+
"@pothos/core": "^4.3.0",
12
12
+
"@pothos/plugin-prisma": "^4.2.1",
13
13
+
"prisma": "^5.20.0"
14
14
+
}
15
15
+
},
16
16
+
"node_modules/@pothos/core": {
17
17
+
"version": "4.3.0",
18
18
+
"resolved": "https://registry.npmjs.org/@pothos/core/-/core-4.3.0.tgz",
19
19
+
"integrity": "sha512-i4nyI8wSCjxdHUomqQ2K3FH0Glklomfv4tJNifQVU9lPtQKoKaS69VxGv+fru0HAZzb1eC1XPEPN3XPCYcvZ7Q==",
20
20
+
"dev": true,
21
21
+
"peerDependencies": {
22
22
+
"graphql": ">=16.6.0"
23
23
+
}
24
24
+
},
25
25
+
"node_modules/@pothos/plugin-prisma": {
26
26
+
"version": "4.2.1",
27
27
+
"resolved": "https://registry.npmjs.org/@pothos/plugin-prisma/-/plugin-prisma-4.2.1.tgz",
28
28
+
"integrity": "sha512-leFWc+akd/EbrsAGEL3Th0fOEl3MnNTGimoLpLa+89rNppiymJz6QmpSq88jTa6yFwPiKU0EsICJ6xv0ioCzig==",
29
29
+
"dev": true,
30
30
+
"dependencies": {
31
31
+
"@prisma/generator-helper": "^5.17.0"
32
32
+
},
33
33
+
"bin": {
34
34
+
"prisma-pothos-types": "bin/generator.js"
35
35
+
},
36
36
+
"peerDependencies": {
37
37
+
"@pothos/core": "*",
38
38
+
"@prisma/client": "*",
39
39
+
"graphql": ">=16.6.0",
40
40
+
"typescript": ">=4.7.2"
41
41
+
}
42
42
+
},
43
43
+
"node_modules/@prisma/client": {
44
44
+
"version": "5.20.0",
45
45
+
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-5.20.0.tgz",
46
46
+
"integrity": "sha512-CLv55ZuMuUawMsxoqxGtLT3bEZoa2W8L3Qnp6rDIFWy+ZBrUcOFKdoeGPSnbBqxc3SkdxJrF+D1veN/WNynZYA==",
47
47
+
"hasInstallScript": true,
48
48
+
"engines": {
49
49
+
"node": ">=16.13"
50
50
+
},
51
51
+
"peerDependencies": {
52
52
+
"prisma": "*"
53
53
+
},
54
54
+
"peerDependenciesMeta": {
55
55
+
"prisma": {
56
56
+
"optional": true
57
57
+
}
58
58
+
}
59
59
+
},
60
60
+
"node_modules/@prisma/debug": {
61
61
+
"version": "5.20.0",
62
62
+
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-5.20.0.tgz",
63
63
+
"integrity": "sha512-oCx79MJ4HSujokA8S1g0xgZUGybD4SyIOydoHMngFYiwEwYDQ5tBQkK5XoEHuwOYDKUOKRn/J0MEymckc4IgsQ==",
64
64
+
"devOptional": true
65
65
+
},
66
66
+
"node_modules/@prisma/engines": {
67
67
+
"version": "5.20.0",
68
68
+
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-5.20.0.tgz",
69
69
+
"integrity": "sha512-DtqkP+hcZvPEbj8t8dK5df2b7d3B8GNauKqaddRRqQBBlgkbdhJkxhoJTrOowlS3vaRt2iMCkU0+CSNn0KhqAQ==",
70
70
+
"devOptional": true,
71
71
+
"hasInstallScript": true,
72
72
+
"dependencies": {
73
73
+
"@prisma/debug": "5.20.0",
74
74
+
"@prisma/engines-version": "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284",
75
75
+
"@prisma/fetch-engine": "5.20.0",
76
76
+
"@prisma/get-platform": "5.20.0"
77
77
+
}
78
78
+
},
79
79
+
"node_modules/@prisma/engines-version": {
80
80
+
"version": "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284",
81
81
+
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284.tgz",
82
82
+
"integrity": "sha512-Lg8AS5lpi0auZe2Mn4gjuCg081UZf88k3cn0RCwHgR+6cyHHpttPZBElJTHf83ZGsRNAmVCZCfUGA57WB4u4JA==",
83
83
+
"devOptional": true
84
84
+
},
85
85
+
"node_modules/@prisma/fetch-engine": {
86
86
+
"version": "5.20.0",
87
87
+
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-5.20.0.tgz",
88
88
+
"integrity": "sha512-JVcaPXC940wOGpCOwuqQRTz6I9SaBK0c1BAyC1pcz9xBi+dzFgUu3G/p9GV1FhFs9OKpfSpIhQfUJE9y00zhqw==",
89
89
+
"devOptional": true,
90
90
+
"dependencies": {
91
91
+
"@prisma/debug": "5.20.0",
92
92
+
"@prisma/engines-version": "5.20.0-12.06fc58a368dc7be9fbbbe894adf8d445d208c284",
93
93
+
"@prisma/get-platform": "5.20.0"
94
94
+
}
95
95
+
},
96
96
+
"node_modules/@prisma/generator-helper": {
97
97
+
"version": "5.20.0",
98
98
+
"resolved": "https://registry.npmjs.org/@prisma/generator-helper/-/generator-helper-5.20.0.tgz",
99
99
+
"integrity": "sha512-37Aibw0wVRQgQVtCdNAIN71YFnSQfvetok7vd95KKkYkQRbEx94gsvPDpyN9Mw7p3IwA3nFgPfLc3jBRztUkKw==",
100
100
+
"dev": true,
101
101
+
"dependencies": {
102
102
+
"@prisma/debug": "5.20.0"
103
103
+
}
104
104
+
},
105
105
+
"node_modules/@prisma/get-platform": {
106
106
+
"version": "5.20.0",
107
107
+
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-5.20.0.tgz",
108
108
+
"integrity": "sha512-8/+CehTZZNzJlvuryRgc77hZCWrUDYd/PmlZ7p2yNXtmf2Una4BWnTbak3us6WVdqoz5wmptk6IhsXdG2v5fmA==",
109
109
+
"devOptional": true,
110
110
+
"dependencies": {
111
111
+
"@prisma/debug": "5.20.0"
112
112
+
}
113
113
+
},
114
114
+
"node_modules/fsevents": {
115
115
+
"version": "2.3.3",
116
116
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
117
117
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
118
118
+
"hasInstallScript": true,
119
119
+
"optional": true,
120
120
+
"os": [
121
121
+
"darwin"
122
122
+
],
123
123
+
"engines": {
124
124
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
125
125
+
}
126
126
+
},
127
127
+
"node_modules/graphql": {
128
128
+
"version": "16.9.0",
129
129
+
"resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz",
130
130
+
"integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==",
131
131
+
"dev": true,
132
132
+
"peer": true,
133
133
+
"engines": {
134
134
+
"node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
135
135
+
}
136
136
+
},
137
137
+
"node_modules/prisma": {
138
138
+
"version": "5.20.0",
139
139
+
"resolved": "https://registry.npmjs.org/prisma/-/prisma-5.20.0.tgz",
140
140
+
"integrity": "sha512-6obb3ucKgAnsGS9x9gLOe8qa51XxvJ3vLQtmyf52CTey1Qcez3A6W6ROH5HIz5Q5bW+0VpmZb8WBohieMFGpig==",
141
141
+
"devOptional": true,
142
142
+
"hasInstallScript": true,
143
143
+
"dependencies": {
144
144
+
"@prisma/engines": "5.20.0"
145
145
+
},
146
146
+
"bin": {
147
147
+
"prisma": "build/index.js"
148
148
+
},
149
149
+
"engines": {
150
150
+
"node": ">=16.13"
151
151
+
},
152
152
+
"optionalDependencies": {
153
153
+
"fsevents": "2.3.3"
154
154
+
}
155
155
+
},
156
156
+
"node_modules/typescript": {
157
157
+
"version": "5.6.3",
158
158
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
159
159
+
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
160
160
+
"dev": true,
161
161
+
"peer": true,
162
162
+
"bin": {
163
163
+
"tsc": "bin/tsc",
164
164
+
"tsserver": "bin/tsserver"
165
165
+
},
166
166
+
"engines": {
167
167
+
"node": ">=14.17"
168
168
+
}
169
169
+
}
170
170
+
}
171
171
+
}
···
1
1
+
{
2
2
+
"devDependencies": {
3
3
+
"prisma": "^5.20.0"
4
4
+
},
5
5
+
"dependencies": {
6
6
+
"@prisma/client": "^5.20.0"
7
7
+
}
8
8
+
}
···
1
1
package notella
2
2
3
3
+
import (
4
4
+
ll "github.com/ewen-lbh/label-logger-go"
5
5
+
)
6
6
+
3
7
var schedules = make(map[string]ScheduledJob)
4
8
5
9
func (job ScheduledJob) Unschedule() {
···
20
24
for {
21
25
for _, job := range schedules {
22
26
if job.ShouldRun() {
23
23
-
job.Run()
27
27
+
ll.Log("Running", "cyan", "job for %s on %s", job.Event, job.Object)
28
28
+
job.Unschedule()
29
29
+
go job.Run()
24
30
}
25
31
}
26
32
}
···
1
1
+
// generator client {
2
2
+
// provider = "prisma-client-js"
3
3
+
// previewFeatures = ["fullTextSearch", "postgresqlExtensions"]
4
4
+
// output = "../src/client"
5
5
+
// }
6
6
+
7
7
+
// See https://pothos-graphql.dev/docs/plugins/prisma#setup
8
8
+
// generator pothos {
9
9
+
// provider = "prisma-pothos-types"
10
10
+
// clientOutput = "@churros/db/prisma"
11
11
+
// output = "../src/pothos/index.d.ts"
12
12
+
// prismaUtils = true
13
13
+
// }
14
14
+
15
15
+
generator goprisma {
16
16
+
provider = "go run github.com/steebchen/prisma-client-go"
17
17
+
previewFeatures = ["fullTextSearch", "postgresqlExtensions"]
18
18
+
}
19
19
+
20
20
+
datasource db {
21
21
+
provider = "postgresql"
22
22
+
url = env("DATABASE_URL")
23
23
+
extensions = [fuzzystrmatch, pgcrypto, unaccent, pg_trgm]
24
24
+
}
25
25
+
26
26
+
/// Users are the people who use the app
27
27
+
model User {
28
28
+
id String @id @default(dbgenerated("nanoid('u:')"))
29
29
+
uid String @unique @db.VarChar(255)
30
30
+
createdAt DateTime @default(now())
31
31
+
32
32
+
// School details
33
33
+
schoolServer String? @db.VarChar(255)
34
34
+
schoolUid String? @db.VarChar(255)
35
35
+
// email of the user from the school's LDAP
36
36
+
schoolEmail String? @db.VarChar(255)
37
37
+
38
38
+
email String @unique @db.VarChar(255)
39
39
+
otherEmails String[] @db.VarChar(255)
40
40
+
firstName String @db.VarChar(255)
41
41
+
lastName String @db.VarChar(255)
42
42
+
majorId String?
43
43
+
major Major? @relation(fields: [majorId], references: [id], onUpdate: Cascade, onDelete: Restrict)
44
44
+
minor Minor? @relation(fields: [minorId], references: [id])
45
45
+
minorId String?
46
46
+
graduationYear Int
47
47
+
apprentice Boolean @default(false)
48
48
+
address String @default("") @db.VarChar(255)
49
49
+
birthday DateTime?
50
50
+
description String @default("") @db.Text
51
51
+
nickname String @default("") @db.VarChar(255)
52
52
+
phone String @default("") @db.VarChar(255)
53
53
+
pictureFile String @default("") @db.VarChar(255)
54
54
+
links Link[]
55
55
+
godparentId String?
56
56
+
cededImageRightsToTVn7 Boolean @default(false)
57
57
+
enabledNotificationChannels NotificationChannel[] @default([Articles, Shotguns, Permissions, GroupBoard, GodparentRequests, Comments, Other])
58
58
+
latestVersionSeenInChangelog String @default("0.0.0")
59
59
+
bot Boolean @default(false)
60
60
+
// Not shown on profile, used to autofill Lydia payment request forms
61
61
+
lydiaPhone String @default("") @db.VarChar(255)
62
62
+
// Prevent these themes from being auto-deployed to the user
63
63
+
blockedThemes Theme[]
64
64
+
65
65
+
// Permissions
66
66
+
admin Boolean @default(false)
67
67
+
canEditGroups StudentAssociation[] @relation("studentAssociationGroupsEditor")
68
68
+
canAccessDocuments Boolean @default(false)
69
69
+
70
70
+
// Relationships
71
71
+
articles Article[]
72
72
+
groups GroupMember[]
73
73
+
credentials Credential[]
74
74
+
Reservation Registration[] @relation("author")
75
75
+
managedEvents EventManager[]
76
76
+
logs LogEntry[]
77
77
+
events Event[]
78
78
+
notificationSubscriptions NotificationSubscription[]
79
79
+
notifications Notification[]
80
80
+
godparent User? @relation("mentorship", fields: [godparentId], references: [id]) // Le parrain ou la marraine
81
81
+
godchildren User[] @relation("mentorship") // Les filleul(e)s
82
82
+
incomingGodparentRequests GodparentRequest[] @relation("godparent")
83
83
+
outgoingGodparentRequests GodparentRequest[] @relation("godchild")
84
84
+
passwordResets PasswordReset[]
85
85
+
emailChanges EmailChange[]
86
86
+
Announcement Announcement[]
87
87
+
contributions Contribution[]
88
88
+
verifications Registration[] @relation("verifiedBy")
89
89
+
oppositions Registration[] @relation("opposedBy")
90
90
+
cancellations Registration[] @relation("cancelledBy")
91
91
+
receivedBookings Registration[] @relation("beneficiaryOf")
92
92
+
documents Document[]
93
93
+
comments Comment[]
94
94
+
bannedFromEvents Event[] @relation("bannedFromEvents")
95
95
+
reactions Reaction[]
96
96
+
formAnswers Answer[]
97
97
+
createdForms Form[]
98
98
+
completedForms Form[] @relation("completedForms")
99
99
+
partiallyCompletedForms Form[] @relation("partiallyCompletedForms")
100
100
+
adminOfStudentAssociations StudentAssociation[] @relation("studentAssociationAdmins")
101
101
+
bookmarks Bookmark[]
102
102
+
sharedPosts Article[] @relation("shares")
103
103
+
sharedEvents Event[] @relation("shares")
104
104
+
seenBookings Registration[] @relation("seenBy")
105
105
+
106
106
+
// For full-text search
107
107
+
search Unsupported("tsvector") @default(dbgenerated("''::tsvector"))
108
108
+
claimedPromotions PromotionCode[]
109
109
+
formsWithMarkedCheckboxes Form[] @relation("formsWithMarkedCheckbox")
110
110
+
createdPages Page[]
111
111
+
112
112
+
@@unique([schoolServer, schoolUid])
113
113
+
@@index([search], type: Gin)
114
114
+
}
115
115
+
116
116
+
/// A bookmarked page, used to make personal quick access links
117
117
+
model Bookmark {
118
118
+
id String @id @default(dbgenerated("nanoid('bookmark:')"))
119
119
+
createdAt DateTime @default(now())
120
120
+
updatedAt DateTime @updatedAt
121
121
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
122
122
+
userId String
123
123
+
// The path to the bookmarked page. Up to the frontend's interpretation
124
124
+
path String
125
125
+
126
126
+
@@unique([userId, path])
127
127
+
}
128
128
+
129
129
+
/// Requests to become someone's godchild. Gets deleted once the request has been accepted (or denied). godchild is the requester, godparent is the requested.
130
130
+
model GodparentRequest {
131
131
+
id String @id @unique @default(dbgenerated("nanoid('godparentreq:')"))
132
132
+
createdAt DateTime @default(now())
133
133
+
updatedAt DateTime @updatedAt
134
134
+
135
135
+
godchild User @relation("godchild", fields: [godchildId], references: [id])
136
136
+
godchildId String
137
137
+
godparent User @relation("godparent", fields: [godparentId], references: [id])
138
138
+
godparentId String
139
139
+
140
140
+
@@unique([godchildId, godparentId])
141
141
+
}
142
142
+
143
143
+
/// UserCandidates are users in the registration process
144
144
+
model UserCandidate {
145
145
+
id String @id @default(dbgenerated("nanoid('candidate:')"))
146
146
+
token String @unique
147
147
+
createdAt DateTime @default(now())
148
148
+
149
149
+
email String @unique @db.VarChar(255)
150
150
+
emailValidated Boolean @default(false)
151
151
+
uid String @default("") @db.VarChar(255)
152
152
+
firstName String @default("") @db.VarChar(255)
153
153
+
lastName String @default("") @db.VarChar(255)
154
154
+
155
155
+
churrosPassword String @db.VarChar(255)
156
156
+
ldapPassword String @db.VarChar(255)
157
157
+
158
158
+
majorId String?
159
159
+
major Major? @relation(fields: [majorId], references: [id], onUpdate: Cascade, onDelete: Restrict)
160
160
+
graduationYear Int?
161
161
+
apprentice Boolean @default(false)
162
162
+
163
163
+
birthday DateTime?
164
164
+
cededImageRightsToTVn7 Boolean @default(false)
165
165
+
166
166
+
usingQuickSignup QuickSignup? @relation(fields: [quickSignupId], references: [id])
167
167
+
quickSignupId String?
168
168
+
}
169
169
+
170
170
+
/// A password reset token
171
171
+
model PasswordReset {
172
172
+
id String @id @default(dbgenerated("nanoid('passreset:')"))
173
173
+
createdAt DateTime @default(now())
174
174
+
updatedAt DateTime @updatedAt
175
175
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
176
176
+
userId String
177
177
+
expiresAt DateTime?
178
178
+
}
179
179
+
180
180
+
/// A email validation request
181
181
+
model EmailChange {
182
182
+
id String @id @default(dbgenerated("nanoid('emailchange:')"))
183
183
+
createdAt DateTime @default(now())
184
184
+
updatedAt DateTime @updatedAt
185
185
+
email String
186
186
+
expiresAt DateTime?
187
187
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
188
188
+
userId String
189
189
+
pending Boolean @default(true)
190
190
+
// token is not exposed to the API, whereas id has to be
191
191
+
token String @unique @default(dbgenerated("nanoid('', 30)"))
192
192
+
}
193
193
+
194
194
+
model QuickSignup {
195
195
+
id String @id @default(dbgenerated("nanoid('quicksignup:', 6)"))
196
196
+
createdAt DateTime @default(now())
197
197
+
updatedAt DateTime @updatedAt
198
198
+
validUntil DateTime
199
199
+
school School @relation(fields: [schoolId], references: [id], onDelete: Cascade, onUpdate: Cascade)
200
200
+
schoolId String
201
201
+
candidates UserCandidate[]
202
202
+
}
203
203
+
204
204
+
// Enum for the different kinds of logos
205
205
+
enum LogoSourceType {
206
206
+
InternalLink
207
207
+
ExternalLink
208
208
+
GroupLogo
209
209
+
Icon
210
210
+
}
211
211
+
212
212
+
/// A service
213
213
+
model Service {
214
214
+
id String @id @default(dbgenerated("nanoid('service:')"))
215
215
+
name String @db.VarChar(255)
216
216
+
url String @default("") @db.VarChar(255)
217
217
+
description String @default("") @db.VarChar(255)
218
218
+
logo String @db.VarChar(255)
219
219
+
logoSourceType LogoSourceType
220
220
+
schoolId String?
221
221
+
school School? @relation(fields: [schoolId], references: [id], onUpdate: Cascade, onDelete: SetNull)
222
222
+
studentAssociationId String?
223
223
+
studentAssociation StudentAssociation? @relation(fields: [studentAssociationId], references: [id], onUpdate: Cascade, onDelete: SetNull)
224
224
+
groupId String?
225
225
+
group Group? @relation(fields: [groupId], references: [id], onUpdate: Cascade, onDelete: SetNull)
226
226
+
importance Int @default(0)
227
227
+
228
228
+
@@unique([schoolId, studentAssociationId, groupId, url])
229
229
+
@@unique([schoolId, studentAssociationId, groupId, name])
230
230
+
}
231
231
+
232
232
+
/// A single external link
233
233
+
model Link {
234
234
+
id String @id @default(dbgenerated("nanoid('link:')"))
235
235
+
name String @db.VarChar(255)
236
236
+
value String @db.VarChar(255)
237
237
+
createdAt DateTime @default(now())
238
238
+
239
239
+
// All resources that can have links
240
240
+
User User? @relation(fields: [userId], references: [id])
241
241
+
userId String?
242
242
+
StudentAssociation StudentAssociation? @relation(fields: [studentAssociationId], references: [id])
243
243
+
studentAssociationId String?
244
244
+
Group Group? @relation(fields: [groupId], references: [id])
245
245
+
groupId String?
246
246
+
Article Article? @relation(fields: [articleId], references: [id])
247
247
+
articleId String?
248
248
+
Event Event? @relation(fields: [eventId], references: [id])
249
249
+
eventId String?
250
250
+
Ticket Ticket? @relation(fields: [ticketId], references: [id])
251
251
+
ticketId String?
252
252
+
Notification Notification? @relation(fields: [notificationId], references: [id])
253
253
+
notificationId String?
254
254
+
Subject Subject? @relation(fields: [subjectId], references: [id])
255
255
+
subjectId String?
256
256
+
257
257
+
@@unique([name, userId, studentAssociationId, groupId, articleId, eventId, ticketId, notificationId, subjectId])
258
258
+
@@unique([userId, value])
259
259
+
@@unique([studentAssociationId, value])
260
260
+
@@unique([groupId, value])
261
261
+
@@unique([articleId, value])
262
262
+
@@unique([eventId, value])
263
263
+
@@unique([ticketId, value])
264
264
+
@@unique([notificationId, value])
265
265
+
@@unique([subjectId, value])
266
266
+
}
267
267
+
268
268
+
/// A school syllabus
269
269
+
model Major {
270
270
+
id String @id @default(dbgenerated("nanoid('major:')"))
271
271
+
uid String @unique @db.VarChar(255)
272
272
+
name String @db.VarChar(255)
273
273
+
shortName String @default("") @db.VarChar(255)
274
274
+
ldapSchoolUid String? @db.VarChar(255)
275
275
+
pictureFile String @default("") @db.VarChar(255)
276
276
+
createdAt DateTime @default(now())
277
277
+
updatedAt DateTime @default(now()) @updatedAt
278
278
+
discontinued Boolean @default(false)
279
279
+
280
280
+
schools School[] @relation("MajorToSchool")
281
281
+
ldapSchool School? @relation("ldapSchool", fields: [ldapSchoolUid], references: [uid], onUpdate: Cascade, onDelete: SetNull)
282
282
+
students User[]
283
283
+
userCandidates UserCandidate[]
284
284
+
accessibleTickets Ticket[]
285
285
+
subjects Subject[]
286
286
+
minors Minor[]
287
287
+
}
288
288
+
289
289
+
model Minor {
290
290
+
id String @id @default(dbgenerated("nanoid('minor:')"))
291
291
+
name String
292
292
+
shortName String @default("")
293
293
+
slug String
294
294
+
majors Major[]
295
295
+
yearTier Int
296
296
+
subjects Subject[]
297
297
+
users User[]
298
298
+
299
299
+
@@unique([slug, yearTier])
300
300
+
}
301
301
+
302
302
+
model School {
303
303
+
id String @id @default(dbgenerated("nanoid('school:')"))
304
304
+
uid String @unique @db.VarChar(255)
305
305
+
name String @db.VarChar(255)
306
306
+
color String @db.VarChar(7)
307
307
+
studentMailDomain String @default("") @db.VarChar(255)
308
308
+
aliasMailDomains String[] @default([]) @db.VarChar(255)
309
309
+
description String @default("") @db.Text
310
310
+
address String @default("") @db.VarChar(255)
311
311
+
pictureFile String @default("") @db.VarChar(255)
312
312
+
313
313
+
majors Major[] @relation("MajorToSchool")
314
314
+
majorsLdapSchool Major[] @relation("ldapSchool")
315
315
+
studentAssociations StudentAssociation[]
316
316
+
accessibleTickets Ticket[]
317
317
+
contributionOptions ContributionOption[]
318
318
+
services Service[]
319
319
+
quickSignups QuickSignup[]
320
320
+
}
321
321
+
322
322
+
enum CredentialType {
323
323
+
Password
324
324
+
Token
325
325
+
GroupAccessToken
326
326
+
Google // External token, used for google sheets integration (and possibly more in the future)
327
327
+
}
328
328
+
329
329
+
/// A credential is a way to authenticate a user
330
330
+
model Credential {
331
331
+
id String @id @default(dbgenerated("nanoid('credential:')"))
332
332
+
userId String?
333
333
+
groupId String?
334
334
+
name String @default("") @db.VarChar(255)
335
335
+
type CredentialType
336
336
+
value String @db.VarChar(255)
337
337
+
userAgent String @default("") @db.VarChar(255)
338
338
+
createdAt DateTime @default(now())
339
339
+
expiresAt DateTime?
340
340
+
refresh String? @db.VarChar(255)
341
341
+
342
342
+
user User? @relation(fields: [userId], references: [id], onUpdate: Cascade, onDelete: Cascade)
343
343
+
group Group? @relation(fields: [groupId], references: [id], onUpdate: Cascade, onDelete: Cascade)
344
344
+
}
345
345
+
346
346
+
/// There is one student association per school
347
347
+
model StudentAssociation {
348
348
+
id String @id @default(dbgenerated("nanoid('ae:')"))
349
349
+
uid String @unique @db.VarChar(255)
350
350
+
description String @default("") @db.Text
351
351
+
schoolId String
352
352
+
name String @unique @db.VarChar(255)
353
353
+
links Link[]
354
354
+
pictureFile String @default("") @db.VarChar(255)
355
355
+
heroBackgroundFile String @default("") @db.VarChar(255) /// Used for the student association's homepage
356
356
+
allPrezMailingList String @default("") @db.VarChar(255)
357
357
+
allTrezMailingList String @default("") @db.VarChar(255)
358
358
+
allBoardMailingList String @default("") @db.VarChar(255)
359
359
+
internalMailDomain String @default("") @db.VarChar(255)
360
360
+
361
361
+
createdAt DateTime @default(now())
362
362
+
updatedAt DateTime @updatedAt
363
363
+
364
364
+
school School @relation(fields: [schoolId], references: [id], onUpdate: Cascade, onDelete: Restrict)
365
365
+
groups Group[]
366
366
+
board Group? @relation("studentAssociationBoard", fields: [boardId], references: [id], onDelete: SetNull, onUpdate: Cascade)
367
367
+
boardId String? @unique
368
368
+
lydiaAccounts LydiaAccount[]
369
369
+
contributionOptions ContributionOption[]
370
370
+
services Service[]
371
371
+
admins User[] @relation("studentAssociationAdmins")
372
372
+
groupsEditors User[] @relation("studentAssociationGroupsEditor")
373
373
+
pages Page[]
374
374
+
}
375
375
+
376
376
+
model Contribution {
377
377
+
id String @id @default(dbgenerated("nanoid('contribution:')"))
378
378
+
option ContributionOption @relation(fields: [optionId], references: [id], onDelete: Cascade, onUpdate: Cascade)
379
379
+
optionId String
380
380
+
transaction LydiaTransaction?
381
381
+
transactionId String?
382
382
+
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)
383
383
+
paid Boolean @default(false)
384
384
+
userId String
385
385
+
386
386
+
@@unique([optionId, userId])
387
387
+
}
388
388
+
389
389
+
model ContributionOption {
390
390
+
id String @id @default(dbgenerated("nanoid('contributionoption:')"))
391
391
+
offeredIn School @relation(fields: [offeredInId], references: [id], onDelete: Cascade, onUpdate: Cascade)
392
392
+
offeredInId String
393
393
+
paysFor StudentAssociation[]
394
394
+
beneficiary LydiaAccount? @relation(fields: [lydiaAccountId], references: [id], onDelete: SetNull, onUpdate: Cascade)
395
395
+
lydiaAccountId String?
396
396
+
name String
397
397
+
price Float
398
398
+
description String @default("") @db.Text
399
399
+
contributions Contribution[]
400
400
+
}
401
401
+
402
402
+
/// The different kinds of groups
403
403
+
enum GroupType {
404
404
+
StudentAssociationSection
405
405
+
Association
406
406
+
Club
407
407
+
List
408
408
+
Integration
409
409
+
Group
410
410
+
}
411
411
+
412
412
+
/// A group is a collection of users
413
413
+
model Group {
414
414
+
id String @id @default(dbgenerated("nanoid('g:')"))
415
415
+
uid String @unique @db.VarChar(255)
416
416
+
parentId String?
417
417
+
/// Helper field to get a whole tree without processing all groups
418
418
+
/// To be set to the group's id itself for root groups.
419
419
+
familyId String?
420
420
+
studentAssociationId String
421
421
+
pictureFile String @default("") @db.VarChar(255)
422
422
+
pictureFileDark String @default("") @db.VarChar(255)
423
423
+
name String @db.VarChar(255)
424
424
+
type GroupType
425
425
+
color String @db.VarChar(7)
426
426
+
selfJoinable Boolean @default(false)
427
427
+
roomIsOpen Boolean @default(false)
428
428
+
createdAt DateTime @default(now())
429
429
+
updatedAt DateTime @default(now()) @updatedAt
430
430
+
unlisted Boolean @default(false)
431
431
+
432
432
+
address String @default("") @db.VarChar(255)
433
433
+
description String @default("") @db.VarChar(255)
434
434
+
email String @default("") @db.VarChar(255)
435
435
+
mailingList String @default("") @db.VarChar(255)
436
436
+
longDescription String @default("")
437
437
+
website String @default("") @db.VarChar(255)
438
438
+
ldapUid String @default("") @db.VarChar(255)
439
439
+
links Link[]
440
440
+
pages Page[]
441
441
+
442
442
+
/// Parent group, from which this group inherits its permissions
443
443
+
parent Group? @relation("parent", fields: [parentId], references: [id], onUpdate: Cascade, onDelete: Restrict)
444
444
+
children Group[] @relation("parent")
445
445
+
446
446
+
/// Family root, only created for performance reasons
447
447
+
familyRoot Group? @relation("root", fields: [familyId], references: [id], onUpdate: Cascade, onDelete: Restrict)
448
448
+
familyChildren Group[] @relation("root")
449
449
+
450
450
+
/// Related clubs
451
451
+
related Group[] @relation("related")
452
452
+
relatedTo Group[] @relation("related")
453
453
+
454
454
+
articles Article[]
455
455
+
members GroupMember[]
456
456
+
studentAssociation StudentAssociation @relation(fields: [studentAssociationId], references: [id], onUpdate: Cascade, onDelete: Restrict)
457
457
+
events Event[]
458
458
+
coOrganizedEvents Event[] @relation("coOrganizer")
459
459
+
lyiaAccounts LydiaAccount[]
460
460
+
tickets Ticket[] @relation("openTo")
461
461
+
services Service[]
462
462
+
restrictedFormSections FormSection[] @relation("restrictedTo")
463
463
+
464
464
+
notifications Notification[]
465
465
+
groupId String?
466
466
+
ticketGroupId String?
467
467
+
joinedByBookingTickets Ticket[] @relation("autojoin")
468
468
+
themes Theme[]
469
469
+
470
470
+
// For ldap
471
471
+
ldapGidNumber Int @unique @default(autoincrement())
472
472
+
473
473
+
// For full-text search
474
474
+
search Unsupported("tsvector") @default(dbgenerated("''::tsvector"))
475
475
+
forms Form[]
476
476
+
boardOf StudentAssociation? @relation("studentAssociationBoard")
477
477
+
boardOfId String?
478
478
+
tokens Credential[]
479
479
+
480
480
+
@@index([search], type: Gin)
481
481
+
}
482
482
+
483
483
+
/// The intermediate model between users and groups
484
484
+
model GroupMember {
485
485
+
groupId String
486
486
+
memberId String
487
487
+
title String @default("") @db.VarChar(255)
488
488
+
president Boolean @default(false)
489
489
+
treasurer Boolean @default(false)
490
490
+
vicePresident Boolean @default(false)
491
491
+
secretary Boolean @default(false)
492
492
+
canEditMembers Boolean @default(false)
493
493
+
canEditArticles Boolean @default(false)
494
494
+
canScanEvents Boolean @default(false)
495
495
+
isDeveloper Boolean @default(false)
496
496
+
createdAt DateTime @default(now())
497
497
+
498
498
+
group Group @relation(fields: [groupId], references: [id], onUpdate: Cascade, onDelete: Cascade)
499
499
+
member User @relation(fields: [memberId], references: [id], onUpdate: Cascade, onDelete: Cascade)
500
500
+
501
501
+
@@id([groupId, memberId])
502
502
+
}
503
503
+
504
504
+
/// An article is a post in a group
505
505
+
model Article {
506
506
+
id String @id @default(dbgenerated("nanoid('a:')"))
507
507
+
authorId String?
508
508
+
groupId String
509
509
+
slug String @db.VarChar(255) // TODO remove
510
510
+
title String @db.VarChar(255)
511
511
+
body String @db.Text
512
512
+
published Boolean @default(false)
513
513
+
visibility Visibility @default(Private)
514
514
+
createdAt DateTime @default(now())
515
515
+
publishedAt DateTime @default(now())
516
516
+
notifiedAt DateTime? @default(now()) // to prevent old notifications before this was added. Another migration will remove this default value.
517
517
+
pictureFile String @default("") @db.VarChar(255)
518
518
+
links Link[]
519
519
+
comments Comment[]
520
520
+
521
521
+
author User? @relation(fields: [authorId], references: [id], onUpdate: Cascade, onDelete: SetNull)
522
522
+
group Group @relation(fields: [groupId], references: [id], onUpdate: Cascade, onDelete: Cascade)
523
523
+
event Event? @relation(fields: [eventId], references: [id])
524
524
+
eventId String?
525
525
+
reactions Reaction[]
526
526
+
sharedBy User[] @relation("shares")
527
527
+
528
528
+
// For full-text search
529
529
+
search Unsupported("tsvector") @default(dbgenerated("''::tsvector"))
530
530
+
531
531
+
@@index([search], type: Gin)
532
532
+
}
533
533
+
534
534
+
enum EventFrequency {
535
535
+
Once
536
536
+
Weekly
537
537
+
Monthly
538
538
+
Biweekly
539
539
+
}
540
540
+
541
541
+
/// An event is a date, time and place, as well as an optional ticket
542
542
+
model Event {
543
543
+
id String @id @default(dbgenerated("nanoid('e:')"))
544
544
+
createdAt DateTime @default(now())
545
545
+
updatedAt DateTime @default(now()) @updatedAt
546
546
+
authorId String?
547
547
+
groupId String
548
548
+
contactMail String
549
549
+
beneficiary LydiaAccount? @relation(fields: [lydiaAccountId], references: [id])
550
550
+
description String @db.Text
551
551
+
slug String @db.VarChar(255)
552
552
+
title String @db.VarChar(255)
553
553
+
// events without dates are useful when they're "drafts", but they' can't appear in the calendar - thus they're only visible by link
554
554
+
startsAt DateTime?
555
555
+
endsAt DateTime?
556
556
+
globalCapacity Int?
557
557
+
notifiedAt DateTime? @default(now()) // to prevent old notifications before this was added. Another migration will remove this default value.
558
558
+
location String @default("") @db.VarChar(255)
559
559
+
visibility Visibility
560
560
+
frequency EventFrequency @default(Once)
561
561
+
recurringUntil DateTime?
562
562
+
pictureFile String @default("") @db.VarChar(255)
563
563
+
showPlacesLeft Boolean @default(true)
564
564
+
managers EventManager[]
565
565
+
author User? @relation(fields: [authorId], references: [id], onUpdate: Cascade, onDelete: SetNull)
566
566
+
includeInKiosk Boolean @default(false)
567
567
+
group Group @relation(fields: [groupId], references: [id])
568
568
+
coOrganizers Group[] @relation("coOrganizer")
569
569
+
tickets Ticket[]
570
570
+
ticketGroups TicketGroup[]
571
571
+
articles Article[]
572
572
+
lydiaAccountId String?
573
573
+
links Link[]
574
574
+
bannedUsers User[] @relation("bannedFromEvents")
575
575
+
reactions Reaction[]
576
576
+
forms Form[]
577
577
+
sharedBy User[] @relation("shares")
578
578
+
applicableOffers Promotion[]
579
579
+
580
580
+
// For full-text search
581
581
+
search Unsupported("tsvector") @default(dbgenerated("''::tsvector"))
582
582
+
583
583
+
@@index([search], type: Gin)
584
584
+
}
585
585
+
586
586
+
enum Visibility {
587
587
+
Private
588
588
+
Unlisted
589
589
+
GroupRestricted
590
590
+
SchoolRestricted
591
591
+
Public
592
592
+
}
593
593
+
594
594
+
/// An event manager is a user that can scan tickets, and may be able to manage the event
595
595
+
model EventManager {
596
596
+
id String @id @default(dbgenerated("nanoid('em:')"))
597
597
+
eventId String
598
598
+
userId String
599
599
+
canVerifyRegistrations Boolean @default(true) // Can scan tickets
600
600
+
canEdit Boolean @default(false)
601
601
+
canEditPermissions Boolean @default(false)
602
602
+
603
603
+
event Event @relation(fields: [eventId], references: [id], onUpdate: Cascade, onDelete: Cascade)
604
604
+
user User @relation(fields: [userId], references: [id], onUpdate: Cascade, onDelete: Cascade)
605
605
+
606
606
+
@@unique([eventId, userId])
607
607
+
}
608
608
+
609
609
+
/// A ticket group allows for conditions on multiple tickets, such as an upper limit on the sum of registrations in the sub-tickets
610
610
+
model TicketGroup {
611
611
+
id String @id @default(dbgenerated("nanoid('tg:')"))
612
612
+
eventId String
613
613
+
name String @db.VarChar(255)
614
614
+
615
615
+
capacity Int @default(0) // 0 means unlimited, capacity is on the sum of sub-tickets registrations
616
616
+
tickets Ticket[]
617
617
+
event Event @relation(fields: [eventId], references: [id])
618
618
+
619
619
+
@@unique([eventId, name])
620
620
+
}
621
621
+
622
622
+
/// Decide when to count a ticket as counting towards the capacity
623
623
+
enum TicketCountingPolicy {
624
624
+
OnBooked // Count when the ticket is booked
625
625
+
OnPaid // Count when the ticket is paid
626
626
+
}
627
627
+
628
628
+
/// A ticket is a way to register for an event. May include a price and conditions.
629
629
+
model Ticket {
630
630
+
id String @id @default(dbgenerated("nanoid('t:')"))
631
631
+
slug String
632
632
+
eventId String
633
633
+
ticketGroupId String?
634
634
+
name String @db.VarChar(255)
635
635
+
description String @db.VarChar(255)
636
636
+
opensAt DateTime?
637
637
+
closesAt DateTime?
638
638
+
minimumPrice Float
639
639
+
maximumPrice Float
640
640
+
capacity Int?
641
641
+
registrations Registration[]
642
642
+
links Link[]
643
643
+
allowedPaymentMethods PaymentMethod[] @default([]) // empty means all
644
644
+
countingPolicy TicketCountingPolicy @default(OnBooked)
645
645
+
646
646
+
// Conditions for that ticket.
647
647
+
openToPromotions Int[] @default([])
648
648
+
openToAlumni Boolean? @default(false) // false means only non-alumni, true means only alumni, null means both
649
649
+
openToExternal Boolean? @default(false) // same thing
650
650
+
openToContributors Boolean? @default(false) // same thing
651
651
+
openToApprentices Boolean? // same thing
652
652
+
openToSchools School[]
653
653
+
openToMajors Major[]
654
654
+
openToGroups Group[] @relation("openTo")
655
655
+
godsonLimit Int @default(0) // 0 means unlimited
656
656
+
autojoinGroups Group[] @relation("autojoin")
657
657
+
658
658
+
onlyManagersCanProvide Boolean @default(false)
659
659
+
660
660
+
event Event @relation(fields: [eventId], references: [id], onUpdate: Cascade, onDelete: Cascade)
661
661
+
group TicketGroup? @relation(fields: [ticketGroupId], references: [id], onDelete: SetNull, onUpdate: Cascade)
662
662
+
Promotion Promotion? @relation(fields: [promotionId], references: [id])
663
663
+
promotionId String?
664
664
+
}
665
665
+
666
666
+
/// A reservation is a user's registration for a ticket
667
667
+
model Registration {
668
668
+
id String @id @default(dbgenerated("nanoid('r:')"))
669
669
+
ticketId String
670
670
+
externalBeneficiary String?
671
671
+
internalBeneficiary User? @relation(fields: [internalBeneficiaryId], references: [id], onUpdate: Cascade, onDelete: SetNull, name: "beneficiaryOf")
672
672
+
internalBeneficiaryId String?
673
673
+
authorId String?
674
674
+
authorEmail String @default("")
675
675
+
verifiedById String?
676
676
+
opposedById String?
677
677
+
cancelledById String?
678
678
+
createdAt DateTime @default(now())
679
679
+
updatedAt DateTime @updatedAt
680
680
+
paymentMethod PaymentMethod?
681
681
+
paid Boolean @default(false)
682
682
+
wantsToPay Float?
683
683
+
684
684
+
lydiaTransaction LydiaTransaction?
685
685
+
paypalTransaction PaypalTransaction?
686
686
+
ticket Ticket @relation(fields: [ticketId], references: [id], onUpdate: Cascade, onDelete: Cascade)
687
687
+
author User? @relation(name: "author", fields: [authorId], references: [id], onUpdate: Cascade, onDelete: Cascade)
688
688
+
verifiedBy User? @relation(name: "verifiedBy", fields: [verifiedById], references: [id], onUpdate: Cascade, onDelete: SetNull)
689
689
+
verifiedAt DateTime?
690
690
+
opposedBy User? @relation(name: "opposedBy", fields: [opposedById], references: [id], onUpdate: Cascade, onDelete: SetNull)
691
691
+
opposedAt DateTime?
692
692
+
cancelledBy User? @relation(name: "cancelledBy", fields: [cancelledById], references: [id], onDelete: SetNull, onUpdate: Cascade)
693
693
+
cancelledAt DateTime?
694
694
+
seenBy User[] @relation(name: "seenBy")
695
695
+
formAnswer Answer?
696
696
+
697
697
+
search Unsupported("tsvector") @default(dbgenerated("''::tsvector"))
698
698
+
699
699
+
@@index([search], type: Gin)
700
700
+
}
701
701
+
702
702
+
enum PaymentMethod {
703
703
+
Lydia
704
704
+
PayPal
705
705
+
Card
706
706
+
Transfer
707
707
+
Check
708
708
+
Cash
709
709
+
External
710
710
+
Other
711
711
+
}
712
712
+
713
713
+
/// A log entry is a log of an action that happened on the website
714
714
+
model LogEntry {
715
715
+
id String @id @default(dbgenerated("nanoid('log:')"))
716
716
+
happenedAt DateTime @default(now())
717
717
+
userId String?
718
718
+
area String @db.VarChar(255) // billeterie, gestion clubs, etc. à typer, mais pas dans la DB (pour être plus flexible)
719
719
+
action String @db.VarChar(255)
720
720
+
target String? @db.VarChar(255)
721
721
+
message String @db.Text
722
722
+
723
723
+
user User? @relation(fields: [userId], references: [id], onUpdate: Cascade, onDelete: SetNull)
724
724
+
}
725
725
+
726
726
+
/// A Lydia account
727
727
+
model LydiaAccount {
728
728
+
id String @id @default(dbgenerated("nanoid('lydia:')"))
729
729
+
groupId String?
730
730
+
group Group? @relation(fields: [groupId], references: [id], onUpdate: Cascade, onDelete: Cascade)
731
731
+
name String @default("") @db.VarChar(255)
732
732
+
privateToken String @default("") @db.VarChar(255)
733
733
+
vendorToken String @default("") @db.VarChar(255)
734
734
+
studentAssociationId String?
735
735
+
events Event[]
736
736
+
studentAssociation StudentAssociation? @relation(fields: [studentAssociationId], references: [id])
737
737
+
ContributionOption ContributionOption[]
738
738
+
739
739
+
@@unique([privateToken, vendorToken, groupId])
740
740
+
}
741
741
+
742
742
+
// Lydia payment
743
743
+
model LydiaTransaction {
744
744
+
id String @id @default(dbgenerated("nanoid('lydiapayment:')"))
745
745
+
phoneNumber String @default("") @db.VarChar(255)
746
746
+
registrationId String? @unique
747
747
+
registration Registration? @relation(fields: [registrationId], references: [id], onUpdate: Cascade, onDelete: Cascade)
748
748
+
requestId String?
749
749
+
requestUuid String?
750
750
+
transactionId String?
751
751
+
createdAt DateTime @default(now())
752
752
+
updatedAt DateTime @updatedAt
753
753
+
contribution Contribution? @relation(fields: [studentAssociationContributionId], references: [id], onDelete: Cascade, onUpdate: Cascade)
754
754
+
studentAssociationContributionId String? @unique
755
755
+
paidCallback String?
756
756
+
}
757
757
+
758
758
+
enum PayPalTransactionStatus {
759
759
+
Created
760
760
+
Saved
761
761
+
Approved
762
762
+
Voided
763
763
+
Completed
764
764
+
PayerActionRequired
765
765
+
}
766
766
+
767
767
+
/// Paypal payment
768
768
+
model PaypalTransaction {
769
769
+
id String @id @default(dbgenerated("nanoid('paypalpayment:')"))
770
770
+
emailAddress String @default("") @db.VarChar(255)
771
771
+
registrationId String? @unique
772
772
+
registration Registration? @relation(fields: [registrationId], references: [id], onUpdate: Cascade, onDelete: Cascade)
773
773
+
status PayPalTransactionStatus?
774
774
+
775
775
+
/// PayPal's order ID.
776
776
+
orderId String?
777
777
+
createdAt DateTime @default(now())
778
778
+
updatedAt DateTime @updatedAt
779
779
+
}
780
780
+
781
781
+
/// A NotificationSubscription stores a user's subscription to push notifications on a user agent
782
782
+
model NotificationSubscription {
783
783
+
id String @id @default(dbgenerated("nanoid('notifsub:')"))
784
784
+
name String @default("") @db.VarChar(255)
785
785
+
createdAt DateTime @default(now())
786
786
+
updatedAt DateTime @updatedAt
787
787
+
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade, onUpdate: Cascade)
788
788
+
ownerId String
789
789
+
endpoint String @unique
790
790
+
expiresAt DateTime?
791
791
+
authKey String
792
792
+
p256dhKey String
793
793
+
notifications Notification[]
794
794
+
}
795
795
+
796
796
+
/// A notification is a push notification that was sent to a user
797
797
+
model Notification {
798
798
+
id String @id @default(dbgenerated("nanoid('notif:')"))
799
799
+
createdAt DateTime @default(now())
800
800
+
updatedAt DateTime @updatedAt
801
801
+
timestamp DateTime?
802
802
+
subscription NotificationSubscription @relation(fields: [subscriptionId], references: [id], onDelete: Cascade, onUpdate: Cascade)
803
803
+
subscriptionId String
804
804
+
image String @default("")
805
805
+
actions Link[]
806
806
+
title String @db.VarChar(255)
807
807
+
imageFile String @default("")
808
808
+
body String @db.Text
809
809
+
vibrate Int[] @default([])
810
810
+
group Group? @relation(fields: [groupId], references: [id], onDelete: SetNull, onUpdate: Cascade)
811
811
+
groupId String?
812
812
+
channel NotificationChannel @default(Other)
813
813
+
user User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade)
814
814
+
userId String?
815
815
+
goto String @default("")
816
816
+
}
817
817
+
818
818
+
/// A NotificationChannel represents the different kinds of reasons why you might receive a notification: a shotgun just opened, etc.
819
819
+
enum NotificationChannel {
820
820
+
Articles
821
821
+
Shotguns
822
822
+
Permissions
823
823
+
GroupBoard
824
824
+
GodparentRequests
825
825
+
Comments
826
826
+
Mandatory
827
827
+
Other // should't be used too much
828
828
+
}
829
829
+
830
830
+
/// Announcement is a way to get a message accross the entire site, such as for maintenance announcements.
831
831
+
model Announcement {
832
832
+
id String @id @default(dbgenerated("nanoid('ann:')"))
833
833
+
by User? @relation(fields: [userId], references: [id], onDelete: SetNull, onUpdate: Cascade)
834
834
+
userId String?
835
835
+
title String @db.VarChar(255)
836
836
+
body String @db.Text
837
837
+
warning Boolean
838
838
+
createdAt DateTime @default(now())
839
839
+
updatedAt DateTime @updatedAt
840
840
+
startsAt DateTime
841
841
+
endsAt DateTime
842
842
+
}
843
843
+
844
844
+
model TeachingUnit {
845
845
+
id String @id @default(dbgenerated("nanoid('ue:')"))
846
846
+
name String @db.VarChar(255)
847
847
+
shortName String @default("") @db.VarChar(255)
848
848
+
apogeeCode String? @db.VarChar(255)
849
849
+
subjects Subject[]
850
850
+
}
851
851
+
852
852
+
model Subject {
853
853
+
id String @id @default(dbgenerated("nanoid('subj:')"))
854
854
+
name String
855
855
+
slug String
856
856
+
shortName String @default("") @db.VarChar(255)
857
857
+
yearTier Int?
858
858
+
forApprentices Boolean @default(false)
859
859
+
links Link[]
860
860
+
apogeeCode String? @db.VarChar(255)
861
861
+
unitId String?
862
862
+
unit TeachingUnit? @relation(fields: [unitId], references: [id], onDelete: SetNull, onUpdate: Cascade)
863
863
+
nextExamAt DateTime?
864
864
+
majors Major[]
865
865
+
minors Minor[]
866
866
+
documents Document[]
867
867
+
// 1 for the first semester, 2 for the second one. Null means both
868
868
+
semester Int?
869
869
+
emoji String @default("") @db.VarChar(4)
870
870
+
871
871
+
@@unique([slug, yearTier, forApprentices])
872
872
+
}
873
873
+
874
874
+
enum DocumentType {
875
875
+
// Graded
876
876
+
Exam // Partiel
877
877
+
PracticalExam // BE
878
878
+
GradedExercises // DM
879
879
+
880
880
+
// Ungraded
881
881
+
Exercises // TD
882
882
+
Practical // TP
883
883
+
CourseNotes // CM
884
884
+
CourseSlides // Diapos
885
885
+
Summary // Fiche de rev, Fiche
886
886
+
887
887
+
Miscellaneous // Divers
888
888
+
}
889
889
+
890
890
+
model Document {
891
891
+
id String @id @default(dbgenerated("nanoid('doc:')"))
892
892
+
slug String
893
893
+
createdAt DateTime @default(now())
894
894
+
updatedAt DateTime @updatedAt
895
895
+
schoolYear Int // stored using the school year start's year
896
896
+
897
897
+
title String
898
898
+
description String @db.Text
899
899
+
// Null subject means the document needs to be sorted
900
900
+
subject Subject? @relation(fields: [subjectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
901
901
+
subjectId String?
902
902
+
type DocumentType
903
903
+
paperPaths String[] // le sujet
904
904
+
solutionPaths String[] // la correction
905
905
+
906
906
+
uploader User? @relation(fields: [uploaderId], references: [id], onDelete: Cascade, onUpdate: Cascade)
907
907
+
uploaderId String?
908
908
+
909
909
+
comments Comment[]
910
910
+
reactions Reaction[]
911
911
+
912
912
+
search Unsupported("tsvector") @default(dbgenerated("''::tsvector"))
913
913
+
914
914
+
@@unique([subjectId, slug])
915
915
+
@@index([search], type: Gin)
916
916
+
}
917
917
+
918
918
+
model Comment {
919
919
+
id String @id @default(dbgenerated("nanoid('comment:')"))
920
920
+
authorId String?
921
921
+
body String @db.Text
922
922
+
createdAt DateTime @default(now())
923
923
+
updatedAt DateTime @updatedAt
924
924
+
author User? @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: Cascade)
925
925
+
926
926
+
document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
927
927
+
documentId String?
928
928
+
article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade, onUpdate: Cascade)
929
929
+
articleId String?
930
930
+
931
931
+
inReplyTo Comment? @relation(name: "reply", fields: [inReplyToId], references: [id], onDelete: SetNull, onUpdate: Cascade)
932
932
+
inReplyToId String?
933
933
+
replies Comment[] @relation(name: "reply")
934
934
+
reactions Reaction[]
935
935
+
}
936
936
+
937
937
+
model Reaction {
938
938
+
id String @id @default(dbgenerated("nanoid('reac:')"))
939
939
+
emoji String
940
940
+
941
941
+
createdAt DateTime @default(now())
942
942
+
updatedAt DateTime @updatedAt
943
943
+
author User? @relation(fields: [authorId], references: [id], onDelete: Cascade, onUpdate: Cascade)
944
944
+
authorId String?
945
945
+
946
946
+
document Document? @relation(fields: [documentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
947
947
+
documentId String?
948
948
+
article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade, onUpdate: Cascade)
949
949
+
articleId String?
950
950
+
event Event? @relation(fields: [eventId], references: [id], onDelete: Cascade, onUpdate: Cascade)
951
951
+
eventId String?
952
952
+
comment Comment? @relation(fields: [commentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
953
953
+
commentId String?
954
954
+
955
955
+
@@unique([emoji, authorId, documentId])
956
956
+
@@unique([emoji, authorId, articleId])
957
957
+
@@unique([emoji, authorId, eventId])
958
958
+
@@unique([emoji, authorId, commentId])
959
959
+
}
960
960
+
961
961
+
enum PromotionType {
962
962
+
SIMPPS
963
963
+
// More to come...
964
964
+
}
965
965
+
966
966
+
model PromotionCode {
967
967
+
id String @id @default(dbgenerated("nanoid('promocode:')"))
968
968
+
createdAt DateTime @default(now())
969
969
+
updatedAt DateTime @updatedAt
970
970
+
claimedAt DateTime?
971
971
+
code String @unique
972
972
+
claimedBy User? @relation(fields: [claimedById], references: [id])
973
973
+
claimedById String?
974
974
+
promotion Promotion @relation(fields: [promotionId], references: [id])
975
975
+
promotionId String
976
976
+
}
977
977
+
978
978
+
model Promotion {
979
979
+
id String @id @default(dbgenerated("nanoid('promo:')"))
980
980
+
createdAt DateTime @default(now())
981
981
+
updatedAt DateTime @updatedAt
982
982
+
validUntil DateTime?
983
983
+
type PromotionType
984
984
+
codes PromotionCode[]
985
985
+
validOn Ticket[]
986
986
+
987
987
+
priceOverride Int
988
988
+
events Event[]
989
989
+
}
990
990
+
991
991
+
model Picture {
992
992
+
id String @id @default(dbgenerated("nanoid('picfile:')"))
993
993
+
path String @db.VarChar(255)
994
994
+
position Int @default(0)
995
995
+
}
996
996
+
997
997
+
model Form {
998
998
+
id String @id @default(dbgenerated("nanoid('form:')"))
999
999
+
createdAt DateTime @default(now())
1000
1000
+
updatedAt DateTime @updatedAt
1001
1001
+
createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull, onUpdate: Cascade)
1002
1002
+
createdById String?
1003
1003
+
visibility Visibility
1004
1004
+
groupId String?
1005
1005
+
group Group? @relation(fields: [groupId], references: [id], onDelete: Cascade, onUpdate: Cascade)
1006
1006
+
1007
1007
+
event Event? @relation(fields: [eventId], references: [id], onDelete: SetNull, onUpdate: Cascade)
1008
1008
+
eventId String?
1009
1009
+
1010
1010
+
opensAt DateTime?
1011
1011
+
closesAt DateTime?
1012
1012
+
title String @db.VarChar(255)
1013
1013
+
description String @db.Text
1014
1014
+
sections FormSection[]
1015
1015
+
answeredBy User[] @relation("completedForms")
1016
1016
+
partiallyAnsweredBy User[] @relation("partiallyCompletedForms")
1017
1017
+
allowEditingAnswers Boolean @default(true)
1018
1018
+
// Pretty specific use-case: manually marking or un-marking answers from the answer list for each user. Used for votes that can be both online and offline.
1019
1019
+
enableAnswersCompletionCheckbox Boolean @default(false)
1020
1020
+
markedCheckboxes User[] @relation("formsWithMarkedCheckbox")
1021
1021
+
restrictToPromotions Int[]
1022
1022
+
contributorsOnly Boolean @default(false)
1023
1023
+
1024
1024
+
// For full-text search
1025
1025
+
search Unsupported("tsvector") @default(dbgenerated("''::tsvector"))
1026
1026
+
linkedGoogleSheetId String?
1027
1027
+
1028
1028
+
@@index([search], type: Gin)
1029
1029
+
}
1030
1030
+
1031
1031
+
model FormSection {
1032
1032
+
id String @id @default(dbgenerated("nanoid('formsection:')"))
1033
1033
+
order Int
1034
1034
+
form Form @relation(fields: [formId], references: [id], onDelete: Cascade)
1035
1035
+
formId String
1036
1036
+
title String @db.VarChar(255)
1037
1037
+
description String @db.Text
1038
1038
+
questions Question[]
1039
1039
+
1040
1040
+
/// Conditions
1041
1041
+
1042
1042
+
restrictedToGroups Group[] @relation("restrictedTo")
1043
1043
+
jumps FormJump[]
1044
1044
+
1045
1045
+
@@unique([formId, order])
1046
1046
+
}
1047
1047
+
1048
1048
+
/// Represent a jump condition: answering a certain value of a certain question changes what section of the form is shown next
1049
1049
+
model FormJump {
1050
1050
+
id String @id @default(dbgenerated("nanoid('formjump:')"))
1051
1051
+
question Question @relation(fields: [questionId], references: [id], onDelete: Cascade)
1052
1052
+
questionId String
1053
1053
+
value String
1054
1054
+
target FormSection @relation(fields: [targetId], references: [id], onDelete: Cascade, onUpdate: Cascade)
1055
1055
+
targetId String
1056
1056
+
}
1057
1057
+
1058
1058
+
model Question {
1059
1059
+
id String @id @default(dbgenerated("nanoid('question:')"))
1060
1060
+
section FormSection @relation(fields: [sectionId], references: [id], onDelete: Cascade, onUpdate: Cascade)
1061
1061
+
sectionId String
1062
1062
+
order Int
1063
1063
+
1064
1064
+
title String @db.VarChar(255)
1065
1065
+
description String @db.Text
1066
1066
+
type QuestionKind
1067
1067
+
mandatory Boolean @default(false)
1068
1068
+
anonymous Boolean @default(false)
1069
1069
+
1070
1070
+
options String[] @default([])
1071
1071
+
scaleStart Int?
1072
1072
+
scaleEnd Int?
1073
1073
+
allowOptionOther Boolean @default(false)
1074
1074
+
allowedFiletypes String[] @default([]) // empty means all
1075
1075
+
1076
1076
+
// See Answer
1077
1077
+
defaultAnswer String[]
1078
1078
+
1079
1079
+
answers Answer[]
1080
1080
+
jumps FormJump[]
1081
1081
+
1082
1082
+
@@unique([sectionId, order])
1083
1083
+
}
1084
1084
+
1085
1085
+
enum QuestionKind {
1086
1086
+
Text
1087
1087
+
LongText
1088
1088
+
SelectOne
1089
1089
+
SelectMultiple
1090
1090
+
FileUpload
1091
1091
+
// In scale, options has two values, the labels of the start and end of the scale
1092
1092
+
Scale
1093
1093
+
Number
1094
1094
+
Date
1095
1095
+
Time
1096
1096
+
}
1097
1097
+
1098
1098
+
model Answer {
1099
1099
+
id String @id @default(dbgenerated("nanoid('answer:')"))
1100
1100
+
question Question @relation(fields: [questionId], references: [id], onDelete: Cascade)
1101
1101
+
questionId String
1102
1102
+
1103
1103
+
createdAt DateTime @default(now())
1104
1104
+
updatedAt DateTime @updatedAt
1105
1105
+
1106
1106
+
createdBy User? @relation(fields: [createdById], references: [id], onDelete: SetNull, onUpdate: Cascade)
1107
1107
+
createdById String?
1108
1108
+
1109
1109
+
/// Only the first value is considered for all types except SelectMultiple.
1110
1110
+
/// Can be empty if the user didn't answer the question (when it wasn't mandatory, for example)
1111
1111
+
answer String[]
1112
1112
+
1113
1113
+
/// Data to link back to an event booking
1114
1114
+
booking Registration? @relation(fields: [bookingId], references: [id], onDelete: SetNull, onUpdate: Cascade)
1115
1115
+
bookingId String? @unique
1116
1116
+
1117
1117
+
// For full-text search
1118
1118
+
search Unsupported("tsvector") @default(dbgenerated("''::tsvector"))
1119
1119
+
1120
1120
+
// For now, users can answer only once to every question
1121
1121
+
@@unique([questionId, createdById])
1122
1122
+
@@index([search], type: Gin)
1123
1123
+
}
1124
1124
+
1125
1125
+
/// A user-defined markdown page, useful for custom pages, only liked to student associations for now.
1126
1126
+
model Page {
1127
1127
+
id String @id @default(dbgenerated("nanoid('page:')"))
1128
1128
+
1129
1129
+
// Paths are unique per linked resource, final URL of the page should therefore be namespaced
1130
1130
+
path String @db.VarChar(255)
1131
1131
+
title String @db.VarChar(255)
1132
1132
+
body String @db.Text
1133
1133
+
createdAt DateTime @default(now())
1134
1134
+
updatedAt DateTime @updatedAt
1135
1135
+
lastAuthor User? @relation(fields: [lastAuthorId], references: [id], onDelete: SetNull, onUpdate: Cascade)
1136
1136
+
lastAuthorId String?
1137
1137
+
studentAssociation StudentAssociation? @relation(fields: [studentAssociationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
1138
1138
+
studentAssociationId String?
1139
1139
+
group Group? @relation(fields: [groupId], references: [id], onDelete: SetNull, onUpdate: Cascade)
1140
1140
+
groupId String?
1141
1141
+
/// Paths to files that are included on that page (relative to the storage root as always)
1142
1142
+
files String[]
1143
1143
+
// For full-text search
1144
1144
+
search Unsupported("tsvector") @default(dbgenerated("''::tsvector"))
1145
1145
+
1146
1146
+
@@unique([studentAssociationId, path])
1147
1147
+
@@unique([groupId, path])
1148
1148
+
@@index([search], type: Gin)
1149
1149
+
}
1150
1150
+
1151
1151
+
// UIDs that should never be considered free
1152
1152
+
model BlockedUid {
1153
1153
+
uid String @id
1154
1154
+
}
1155
1155
+
1156
1156
+
model Theme {
1157
1157
+
id String @id @default(dbgenerated("nanoid('theme:')"))
1158
1158
+
name String @db.VarChar(255)
1159
1159
+
values ThemeValue[]
1160
1160
+
createdAt DateTime @default(now())
1161
1161
+
updatedAt DateTime @updatedAt
1162
1162
+
visibility Visibility @default(Private)
1163
1163
+
startsAt DateTime? @default(now())
1164
1164
+
endsAt DateTime?
1165
1165
+
author Group? @relation(fields: [authorId], references: [id], onDelete: SetNull, onUpdate: Cascade)
1166
1166
+
authorId String?
1167
1167
+
1168
1168
+
// Auto-change people's theme to this one when it's visible to them
1169
1169
+
autodeploy Boolean @default(false)
1170
1170
+
blockedBy User[]
1171
1171
+
}
1172
1172
+
1173
1173
+
enum ThemeVariant {
1174
1174
+
Light
1175
1175
+
Dark
1176
1176
+
// Might add more stuff, like "High contrast light, high contrast dark", etc
1177
1177
+
}
1178
1178
+
1179
1179
+
enum ThemeVariable {
1180
1180
+
ColorBackground
1181
1181
+
ColorBackground2
1182
1182
+
ColorBackground3
1183
1183
+
ColorBackground4
1184
1184
+
ColorShy
1185
1185
+
ColorMuted
1186
1186
+
ColorForeground
1187
1187
+
ColorPrimary
1188
1188
+
ColorSuccess
1189
1189
+
ColorDanger
1190
1190
+
ColorWarning
1191
1191
+
ColorPrimaryBackground
1192
1192
+
ColorSuccessBackground
1193
1193
+
ColorDangerBackground
1194
1194
+
ColorWarningBackground
1195
1195
+
ImageLogoNavbarTop
1196
1196
+
ImageLogoNavbarSide
1197
1197
+
ImageBackgroundNavbarBottom
1198
1198
+
ImageBackgroundNavbarTop
1199
1199
+
PatternBackground
1200
1200
+
}
1201
1201
+
1202
1202
+
model ThemeValue {
1203
1203
+
id String @id @default(dbgenerated("nanoid('themeval:')"))
1204
1204
+
variable ThemeVariable
1205
1205
+
value String @db.VarChar(500)
1206
1206
+
theme Theme @relation(fields: [themeId], references: [id], onDelete: Cascade, onUpdate: Cascade)
1207
1207
+
themeId String
1208
1208
+
variant ThemeVariant @default(Light)
1209
1209
+
1210
1210
+
@@unique([themeId, variant, variable])
1211
1211
+
}
···
1
1
+
//go:generate go run github.com/steebchen/prisma-client-go generate
2
2
+
1
3
package main
2
4
3
5
import (
···
9
11
"github.com/caarlos0/env/v11"
10
12
"github.com/common-nighthawk/go-figure"
11
13
ll "github.com/ewen-lbh/label-logger-go"
14
14
+
"github.com/google/uuid"
15
15
+
"github.com/segmentio/encoding/json"
12
16
)
13
17
14
18
type Configuration struct {
···
20
24
type PostScheduleRequest struct {
21
25
When time.Time `json:"when"`
22
26
Ressource notella.ChurrosId `json:"ressource"`
27
27
+
Event notella.Event `json:"event"`
23
28
}
24
29
25
30
func main() {
···
38
43
ll.Log("", "reset", "Poll interval: [bold]%d[reset] ms", config.PollInterval)
39
44
fmt.Println()
40
45
41
41
-
http.HandleFunc("POST /schedule/{when}", func(w http.ResponseWriter, r *http.Request) {
46
46
+
http.HandleFunc("POST /schedule", func(w http.ResponseWriter, r *http.Request) {
47
47
+
var req PostScheduleRequest
48
48
+
err := json.NewDecoder(r.Body).Decode(&req)
49
49
+
if err != nil {
50
50
+
ll.ErrorDisplay("could not decode json", err)
51
51
+
http.Error(w, "could not decode json", http.StatusBadRequest)
52
52
+
return
53
53
+
}
54
54
+
55
55
+
job := notella.ScheduledJob{
56
56
+
ID: uuid.New().String(),
57
57
+
When: req.When,
58
58
+
Object: req.Ressource,
59
59
+
Event: req.Event,
60
60
+
}
42
61
62
62
+
job.Schedule()
63
63
+
w.WriteHeader(http.StatusCreated)
43
64
})
44
65
45
66
ll.Info("starting scheduler")