this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

wip: continue

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