implemented data sources with prisma in go

This commit is contained in:
2025-01-02 13:19:55 +00:00
parent cfb0bdf9cf
commit 615e749a12
55 changed files with 1399 additions and 788 deletions
+7
View File
@@ -0,0 +1,7 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="postgresql://user:secret@host.docker.internal:5432/time_tracking_db?schema=public"
+1 -1
View File
@@ -1,7 +1,7 @@
package main
import (
"actatempus_backend/internal/infrastructure/persistence/config"
"actatempus_backend/internal/infrastructure/config"
"actatempus_backend/internal/interfaces/http"
"fmt"
"log"
+17 -11
View File
@@ -2,26 +2,32 @@ module actatempus_backend
go 1.23.2
require github.com/spf13/viper v1.19.0
require (
github.com/joho/godotenv v1.5.1
github.com/shopspring/decimal v1.4.0
github.com/spf13/viper v1.19.0
github.com/steebchen/prisma-client-go v0.45.0
)
require (
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/IBM/fp-go v1.0.151
github.com/fsnotify/fsnotify v1.8.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/magiconair/properties v1.8.9 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/sagikazarmark/locafero v0.6.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
go.mongodb.org/mongo-driver/v2 v2.0.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+32 -35
View File
@@ -1,71 +1,68 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/IBM/fp-go v1.0.151 h1:PXje3KRwrfIq4lEP/1KUDj4/4xSQxj9M0fWZYWv0LYA=
github.com/IBM/fp-go v1.0.151/go.mod h1:QOwgpyL4RsTBcwRHOk/QfrMExEGxkMPraXkunCyrUeU=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
github.com/sagikazarmark/locafero v0.6.0 h1:ON7AQg37yzcRPU69mt7gwhFEBwxI6P9T4Qu3N51bwOk=
github.com/sagikazarmark/locafero v0.6.0/go.mod h1:77OmuIc6VTraTXKXIs/uvUxKGUXjE1GbemJYHqdNjX0=
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/steebchen/prisma-client-go v0.45.0 h1:sXqCi96/1HIh3CScfd/27OlcR/YDXDtgbrpkDSk+3TY=
github.com/steebchen/prisma-client-go v0.45.0/go.mod h1:4sNeijtPnYSX04/CIXRptUrczEsmPDpWSYR7ahY+H+c=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
go.mongodb.org/mongo-driver/v2 v2.0.0 h1:Jfd7XpdZa9yk3eY774bO7SWVb30noLSirL9nKTpavhI=
go.mongodb.org/mongo-driver/v2 v2.0.0/go.mod h1:nSjmNq4JUstE8IRZKTktLgMHM4F1fccL6HGX1yh+8RA=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 h1:1UoZQm6f0P/ZO0w1Ri+f+ifG/gXhegadRdwBIXEFWDo=
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
@@ -0,0 +1,28 @@
package dto
// ProjectDTO represents the data structure for transferring project data.
type ProjectDTO struct {
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
ClientID *string `json:"clientId"`
UserID string `json:"userId"`
CreatedAt string `json:"createdAt"` // Use ISO8601 format
UpdatedAt string `json:"updatedAt"` // Use ISO8601 format
}
// ProjectCreateDTO is used for creating a new project.
type ProjectCreateDTO struct {
Name string `json:"name"`
Description *string `json:"description"`
ClientID *string `json:"clientId"`
UserID string `json:"userId"`
}
// ProjectUpdateDTO is used for updating an existing project.
type ProjectUpdateDTO struct {
Name *string `json:"name"`
Description *string `json:"description"`
ClientID *string `json:"clientId"`
UserID *string `json:"userId"`
}
@@ -0,0 +1,25 @@
package dto
// ProjectTaskDTO represents the data structure for transferring project task data.
type ProjectTaskDTO struct {
ID string `json:"id"`
Name string `json:"name"`
Description *string `json:"description"`
ProjectID string `json:"projectId"`
CreatedAt string `json:"createdAt"` // Use ISO8601 format
UpdatedAt string `json:"updatedAt"` // Use ISO8601 format
}
// ProjectTaskCreateDTO is used for creating a new project task.
type ProjectTaskCreateDTO struct {
Name string `json:"name"`
Description *string `json:"description"`
ProjectID string `json:"projectId"`
}
// ProjectTaskUpdateDTO is used for updating an existing project task.
type ProjectTaskUpdateDTO struct {
Name *string `json:"name"`
Description *string `json:"description"`
ProjectID *string `json:"projectId"`
}
@@ -0,0 +1,31 @@
package dto
// TimeEntryDTO represents the data structure for transferring time entry data.
type TimeEntryDTO struct {
ID string `json:"id"`
StartTime string `json:"startTime"` // Use ISO8601 format
EndTime *string `json:"endTime"` // Use ISO8601 format
Description *string `json:"description"`
UserID string `json:"userId"`
ProjectID string `json:"projectId"`
CreatedAt string `json:"createdAt"` // Use ISO8601 format
UpdatedAt string `json:"updatedAt"` // Use ISO8601 format
}
// TimeEntryCreateDTO is used for creating a new time entry.
type TimeEntryCreateDTO struct {
StartTime string `json:"startTime"` // Use ISO8601 format
EndTime *string `json:"endTime"` // Use ISO8601 format
Description *string `json:"description"`
UserID string `json:"userId"`
ProjectID string `json:"projectId"`
}
// TimeEntryUpdateDTO is used for updating an existing time entry.
type TimeEntryUpdateDTO struct {
StartTime *string `json:"startTime"` // Use ISO8601 format
EndTime *string `json:"endTime"` // Use ISO8601 format
Description *string `json:"description"`
UserID *string `json:"userId"`
ProjectID *string `json:"projectId"`
}
@@ -0,0 +1,25 @@
package dto
// UserDTO represents the data structure for transferring user data.
type UserDTO struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Password *string `json:"password,omitempty"` // Optional for cases where password shouldn't be exposed
CreatedAt string `json:"createdAt"` // Use ISO8601 format
UpdatedAt string `json:"updatedAt"` // Use ISO8601 format
}
// UserCreateDTO is used for creating a new user.
type UserCreateDTO struct {
Name string `json:"name"`
Email string `json:"email"`
Password string `json:"password"`
}
// UserUpdateDTO is used for updating an existing user.
type UserUpdateDTO struct {
Name *string `json:"name"`
Email *string `json:"email"`
Password *string `json:"password"`
}
@@ -0,0 +1,14 @@
package mappers
import (
"time"
)
// Parses ISO8601 timestamps
func parseISOTime(value string) time.Time {
parsed, err := time.Parse(time.RFC3339, value)
if err != nil {
panic("Invalid ISO8601 format") // Or handle error gracefully
}
return parsed
}
@@ -0,0 +1,41 @@
package mappers
import (
"actatempus_backend/internal/application/services/dto"
"actatempus_backend/internal/domain/entities"
"time"
)
// MapProjectToDTO converts a Project domain object to a ProjectDTO.
func MapProjectToDTO(project entities.Project) dto.ProjectDTO {
return dto.ProjectDTO{
ID: project.ID,
Name: project.Name,
Description: project.Description,
ClientID: project.ClientID,
UserID: project.UserID,
CreatedAt: project.CreatedAt.Format(time.RFC3339),
UpdatedAt: project.UpdatedAt.Format(time.RFC3339),
}
}
// MapCreateDTOToProject converts a ProjectCreateDTO to a Project domain object.
func MapCreateDTOToProject(dto dto.ProjectCreateDTO) entities.ProjectCreate {
return entities.ProjectCreate{
Name: dto.Name,
Description: dto.Description,
ClientID: dto.ClientID,
UserID: dto.UserID,
}
}
// MapUpdateDTOToProject converts a ProjectUpdateDTO to a partial Project domain object.
func MapUpdateDTOToProject(dto dto.ProjectUpdateDTO, ID string) entities.ProjectUpdate {
return entities.ProjectUpdate{
ID: ID,
Name: dto.Name,
Description: dto.Description,
ClientID: dto.ClientID,
UserID: dto.UserID,
}
}
@@ -0,0 +1,38 @@
package mappers
import (
"actatempus_backend/internal/application/services/dto"
"actatempus_backend/internal/domain/entities"
"time"
)
// MapProjectTaskToDTO converts a ProjectTask domain object to a ProjectTaskDTO.
func MapProjectTaskToDTO(task entities.ProjectTask) dto.ProjectTaskDTO {
return dto.ProjectTaskDTO{
ID: task.ID,
Name: task.Name,
Description: task.Description,
ProjectID: task.ProjectID,
CreatedAt: task.CreatedAt.Format(time.RFC3339),
UpdatedAt: task.UpdatedAt.Format(time.RFC3339),
}
}
// MapCreateDTOToProjectTask converts a ProjectTaskCreateDTO to a ProjectTask domain object.
func MapCreateDTOToProjectTask(dto dto.ProjectTaskCreateDTO) entities.ProjectTaskCreate {
return entities.ProjectTaskCreate{
Name: dto.Name,
Description: dto.Description,
ProjectID: dto.ProjectID,
}
}
// MapUpdateDTOToProjectTask converts a ProjectTaskUpdateDTO to a partial ProjectTask domain object.
func MapUpdateDTOToProjectTask(dto dto.ProjectTaskUpdateDTO, ID string) entities.ProjectTaskUpdate {
return entities.ProjectTaskUpdate{
ID: ID,
Name: dto.Name,
Description: dto.Description,
ProjectID: dto.ProjectID,
}
}
@@ -0,0 +1,46 @@
package mappers
import (
"actatempus_backend/internal/application/services/dto"
"actatempus_backend/internal/domain/entities"
"actatempus_backend/internal/utils"
"time"
)
// MapTimeEntryToDTO converts a TimeEntry domain object to a TimeEntryDTO.
func MapTimeEntryToDTO(entry entities.TimeEntry) dto.TimeEntryDTO {
return dto.TimeEntryDTO{
ID: entry.ID,
StartTime: entry.StartTime.Format(time.RFC3339),
EndTime: utils.Let(entry.EndTime, func (t time.Time) string { return t.Format(time.RFC3339);}),
Description: entry.Description,
UserID: entry.UserID,
ProjectID: entry.ProjectID,
CreatedAt: entry.CreatedAt.Format(time.RFC3339),
UpdatedAt: entry.UpdatedAt.Format(time.RFC3339),
}
}
// MapCreateDTOToTimeEntry converts a TimeEntryCreateDTO to a TimeEntry domain object.
func MapCreateDTOToTimeEntry(dto dto.TimeEntryCreateDTO) entities.TimeEntryCreate {
return entities.TimeEntryCreate{
StartTime: parseISOTime(dto.StartTime),
EndTime: utils.Let(dto.EndTime,parseISOTime),
Description: dto.Description,
UserID: dto.UserID,
ProjectID: dto.ProjectID,
}
}
// MapUpdateDTOToTimeEntry converts a TimeEntryUpdateDTO to a partial TimeEntry domain object.
func MapUpdateDTOToTimeEntry(dto dto.TimeEntryUpdateDTO, ID string) entities.TimeEntryUpdate {
return entities.TimeEntryUpdate{
ID: ID,
StartTime: utils.Let(dto.StartTime,parseISOTime),
EndTime: utils.Let(dto.EndTime,parseISOTime),
Description: dto.Description,
UserID: dto.UserID,
ProjectID: dto.ProjectID,
}
}
@@ -0,0 +1,37 @@
package mappers
import (
"actatempus_backend/internal/application/services/dto"
"actatempus_backend/internal/domain/entities"
"time"
)
// MapUserToDTO converts a User domain object to a UserDTO.
func MapUserToDTO(user entities.User) dto.UserDTO {
return dto.UserDTO{
ID: user.ID,
Name: user.Name,
Email: user.Email,
CreatedAt: user.CreatedAt.Format(time.RFC3339),
UpdatedAt: user.UpdatedAt.Format(time.RFC3339),
}
}
// MapCreateDTOToUser converts a UserCreateDTO to a User domain object.
func MapCreateDTOToUser(dto dto.UserCreateDTO) entities.UserCreate {
return entities.UserCreate{
Name: dto.Name,
Email: dto.Email,
Password: dto.Password,
}
}
// MapUpdateDTOToUser converts a UserUpdateDTO to a partial User domain object.
func MapUpdateDTOToUser(dto dto.UserUpdateDTO, ID string) entities.UserUpdate {
return entities.UserUpdate{
ID: ID,
Name: dto.Name,
Email: dto.Email,
Password: dto.Password,
}
}
@@ -1,23 +0,0 @@
package usecases
import (
"actatempus_backend/internal/domain/entities"
"actatempus_backend/internal/domain/repositories"
)
type RegisterUserUseCase struct {
userRepository repositories.UserRepository
}
func NewRegisterUserUseCase(userRepo repositories.UserRepository) *RegisterUserUseCase {
return &RegisterUserUseCase{userRepository: userRepo}
}
func (uc *RegisterUserUseCase) Execute(name, email, password string) error {
user := &entities.User{
Name: name,
Email: email,
Password: password, // In der Realität: Passwörter hashen
}
return uc.userRepository.Create(user)
}
@@ -0,0 +1,40 @@
package app_error
import (
"fmt"
)
// AppError is the standard error type used throughout the application.
type AppError struct {
Code ErrorCode `json:"code"`
Message string `json:"message"`
Status int `json:"status"` // HTTP status code
Err error `json:"-"` // Original error, if any
}
// Error satisfies the error interface.
func (e *AppError) Error() string {
if e.Err != nil {
return fmt.Sprintf("Code: %s, Message: %s, Original Error: %s", e.Code, e.Message, e.Err.Error())
}
return fmt.Sprintf("Code: %s, Message: %s", e.Code, e.Message)
}
// Wrap wraps an existing error into an AppError.
func Wrap(err error, code ErrorCode, message string, status int) *AppError {
return &AppError{
Code: code,
Message: message,
Status: status,
Err: err,
}
}
// New creates a new AppError.
func New(code ErrorCode, message string, status int) *AppError {
return &AppError{
Code: code,
Message: message,
Status: status,
}
}
@@ -0,0 +1,20 @@
package app_error
// ErrorCode is a custom type for application error codes.
type ErrorCode string
const (
// General errors
InternalError ErrorCode = "internal_error"
ValidationError ErrorCode = "validation_error"
NotFoundError ErrorCode = "not_found"
UnauthorizedError ErrorCode = "unauthorized"
ForbiddenError ErrorCode = "forbidden"
ConflictError ErrorCode = "conflict"
DatabaseError ErrorCode = "database_error"
ExternalServiceError ErrorCode = "external_service_error"
// Specific errors
UserNotFoundError ErrorCode = "user_not_found"
ProjectNotFoundError ErrorCode = "project_not_found"
)
@@ -0,0 +1,25 @@
package app_error
import "net/http"
// Helpers for creating common errors
// NewValidationError creates a validation error.
func NewValidationError(message string) *AppError {
return New(ValidationError, message, http.StatusBadRequest)
}
// NewNotFoundError creates a not found error.
func NewNotFoundError(message string) *AppError {
return New(NotFoundError, message, http.StatusNotFound)
}
// NewUnauthorizedError creates an unauthorized error.
func NewUnauthorizedError(message string) *AppError {
return New(UnauthorizedError, message, http.StatusUnauthorized)
}
// NewInternalError creates an internal server error.
func NewInternalError(err error) *AppError {
return Wrap(err, InternalError, "An internal server error occurred", http.StatusInternalServerError)
}
@@ -0,0 +1,18 @@
package data
import (
"actatempus_backend/internal/domain/entities"
"context"
E "github.com/IBM/fp-go/either"
)
// ProjectDataSource defines the operations for interacting with project data.
type ProjectDataSource interface {
Create(ctx context.Context, project entities.ProjectCreate) E.Either[error,entities.Project]
FindByID(ctx context.Context, id string) E.Either[error,entities.Project]
FindByUserID(ctx context.Context, userID string) E.Either[error,[]entities.Project]
Update(ctx context.Context, project entities.ProjectUpdate) E.Either[error,entities.Project]
Delete(ctx context.Context, id string) E.Either[error,entities.Project]
FindAll(ctx context.Context) E.Either[error,[]entities.Project]
}
@@ -0,0 +1,18 @@
package data
import (
"actatempus_backend/internal/domain/entities"
"context"
E "github.com/IBM/fp-go/either"
)
// ProjectTaskDataSource defines the operations for interacting with project task data.
type ProjectTaskDataSource interface {
Create(ctx context.Context, task entities.ProjectTaskCreate) E.Either[error,entities.ProjectTask]
FindByID(ctx context.Context, id string) E.Either[error,entities.ProjectTask]
FindByProjectID(ctx context.Context, projectID string) E.Either[error,[]entities.ProjectTask]
Update(ctx context.Context, task entities.ProjectTaskUpdate) E.Either[error,entities.ProjectTask]
Delete(ctx context.Context, id string) E.Either[error,entities.ProjectTask]
FindAll(ctx context.Context) E.Either[error,[]entities.ProjectTask]
}
@@ -0,0 +1,19 @@
package data
import (
"actatempus_backend/internal/domain/entities"
"context"
E "github.com/IBM/fp-go/either"
)
// TimeEntryDataSource defines the operations for interacting with time entry data.
type TimeEntryDataSource interface {
Create(ctx context.Context, entry entities.TimeEntryCreate) E.Either[error,entities.TimeEntry]
FindByID(ctx context.Context, id string) E.Either[error,entities.TimeEntry]
FindByUserID(ctx context.Context, userID string) E.Either[error,[]entities.TimeEntry]
FindByProjectID(ctx context.Context, projectID string) E.Either[error,[]entities.TimeEntry]
Update(ctx context.Context, entry entities.TimeEntryUpdate) E.Either[error,entities.TimeEntry]
Delete(ctx context.Context, id string) E.Either[error,entities.TimeEntry]
FindAll(ctx context.Context) E.Either[error,[]entities.TimeEntry]
}
@@ -0,0 +1,17 @@
package data
import (
"actatempus_backend/internal/domain/entities"
"context"
E "github.com/IBM/fp-go/either"
)
// UserDataSource defines the operations for interacting with user data.
type UserDataSource interface {
Create(ctx context.Context, user entities.UserCreate) E.Either[error,entities.User]
FindByID(ctx context.Context, id string) E.Either[error,entities.User]
FindByEmail(ctx context.Context, email string) E.Either[error,entities.User]
Update(ctx context.Context, user entities.UserUpdate) E.Either[error,entities.User]
Delete(ctx context.Context, id string) E.Either[error,entities.User]
FindAll(ctx context.Context) E.Either[error,[]entities.User]
}
@@ -0,0 +1,31 @@
package entities
import "time"
// Project Domain
type Project struct {
ID string
Name string
Description *string
ClientID *string
UserID string
CreatedAt time.Time
UpdatedAt time.Time
}
// ProjectCreate
type ProjectCreate struct {
Name string
Description *string
ClientID *string
UserID string
}
// ProjectUpdate
type ProjectUpdate struct {
ID string
Name *string
Description *string
ClientID *string
UserID *string
}
@@ -0,0 +1,28 @@
package entities
import "time"
// ProjectTask Domain
type ProjectTask struct {
ID string
Name string
Description *string
ProjectID string
CreatedAt time.Time
UpdatedAt time.Time
}
// ProjectTaskCreate
type ProjectTaskCreate struct {
Name string
Description *string
ProjectID string
}
// ProjectTaskUpdate
type ProjectTaskUpdate struct {
ID string
Name *string
Description *string
ProjectID *string
}
@@ -0,0 +1,34 @@
package entities
import "time"
// TimeEntry Domain
type TimeEntry struct {
ID string
StartTime time.Time
EndTime *time.Time
Description *string
UserID string
ProjectID string
CreatedAt time.Time
UpdatedAt time.Time
}
// TimeEntryCreate
type TimeEntryCreate struct {
StartTime time.Time
EndTime *time.Time
Description *string
UserID string
ProjectID string
}
// TimeEntryUpdate
type TimeEntryUpdate struct {
ID string
StartTime *time.Time
EndTime *time.Time
Description *string
UserID *string
ProjectID *string
}
+23 -1
View File
@@ -1,8 +1,30 @@
package entities
import "time"
// In user.go
// User Domain
type User struct {
ID string
ID string
Name string
Email string
Password string
CreatedAt time.Time
UpdatedAt time.Time
}
// UserCreate DTO
type UserCreate struct {
Name string
Email string
Password string
}
// UserUpdate DTO
type UserUpdate struct {
ID string
Name *string
Email *string
Password *string
}
@@ -1,9 +0,0 @@
package repositories
import "actatempus_backend/internal/domain/entities"
type UserRepository interface {
Create(user *entities.User) error
FindByEmail(email string) (*entities.User, error)
FindByID(id string) (*entities.User, error)
}
@@ -0,0 +1,16 @@
package repository
import (
"actatempus_backend/internal/domain/entities"
"context"
)
// ProjectRepository defines the operations for interacting with project data.
type ProjectRepository interface {
Create(ctx context.Context, project entities.Project) (entities.Project, error)
FindByID(ctx context.Context, id string) (entities.Project, error)
FindByUserID(ctx context.Context, userID string) ([]entities.Project, error)
Update(ctx context.Context, project entities.Project) (entities.Project, error)
Delete(ctx context.Context, id string) error
FindAll(ctx context.Context) ([]entities.Project, error)
}
@@ -0,0 +1,16 @@
package repository
import (
"actatempus_backend/internal/domain/entities"
"context"
)
// ProjectTaskRepository defines the operations for interacting with project task data.
type ProjectTaskRepository interface {
Create(ctx context.Context, task entities.ProjectTask) (entities.ProjectTask, error)
FindByID(ctx context.Context, id string) (entities.ProjectTask, error)
FindByProjectID(ctx context.Context, projectID string) ([]entities.ProjectTask, error)
Update(ctx context.Context, task entities.ProjectTask) (entities.ProjectTask, error)
Delete(ctx context.Context, id string) error
FindAll(ctx context.Context) ([]entities.ProjectTask, error)
}
@@ -0,0 +1,17 @@
package repository
import (
"actatempus_backend/internal/domain/entities"
"context"
)
// TimeEntryRepository defines the operations for interacting with time entry data.
type TimeEntryRepository interface {
Create(ctx context.Context, entry entities.TimeEntry) (entities.TimeEntry, error)
FindByID(ctx context.Context, id string) (entities.TimeEntry, error)
FindByUserID(ctx context.Context, userID string) ([]entities.TimeEntry, error)
FindByProjectID(ctx context.Context, projectID string) ([]entities.TimeEntry, error)
Update(ctx context.Context, entry entities.TimeEntry) (entities.TimeEntry, error)
Delete(ctx context.Context, id string) error
FindAll(ctx context.Context) ([]entities.TimeEntry, error)
}
+16
View File
@@ -0,0 +1,16 @@
package repository
import (
"actatempus_backend/internal/domain/entities"
"context"
)
// UserRepository defines the operations for interacting with user data.
type UserRepository interface {
Create(ctx context.Context, user entities.UserCreate) (entities.User, error)
FindByID(ctx context.Context, id string) (entities.User, error)
FindByEmail(ctx context.Context, email string) (entities.User, error)
Update(ctx context.Context, user entities.UserUpdate) (entities.User, error)
Delete(ctx context.Context, id string) error
FindAll(ctx context.Context) ([]entities.User, error)
}
@@ -1,25 +1,25 @@
package config
import (
"github.com/spf13/viper"
)
type Config struct {
DatabaseURL string
Port string
}
func LoadConfig(path string) (*Config, error) {
viper.AddConfigPath(path)
viper.SetConfigName("config")
viper.SetConfigType("yaml")
if err := viper.ReadInConfig(); err != nil {
return nil, err
}
return &Config{
DatabaseURL: viper.GetString("database.url"),
Port: viper.GetString("server.port"),
}, nil
}
package config
import (
"github.com/spf13/viper"
)
type Config struct {
DatabaseURL string
Port string
}
func LoadConfig(path string) (*Config, error) {
viper.AddConfigPath(path)
viper.SetConfigName("config")
viper.SetConfigType("yaml")
if err := viper.ReadInConfig(); err != nil {
return nil, err
}
return &Config{
DatabaseURL: viper.GetString("database.url"),
Port: viper.GetString("server.port"),
}, nil
}
@@ -0,0 +1,22 @@
package data
import (
"actatempus_backend/internal/domain/app_error"
"actatempus_backend/internal/infrastructure/data/db"
"errors"
)
func handleDBError(err error, notFoundMessage string) error {
if errors.Is(err, db.ErrNotFound) {
return app_error.NewNotFoundError(notFoundMessage)
}
return app_error.NewInternalError(err)
}
func NullableField[T any](getter func() (T, bool)) *T {
if value, ok := getter(); ok {
return &value
}
return nil
}
@@ -0,0 +1,88 @@
package data
import (
"actatempus_backend/internal/domain/entities"
"actatempus_backend/internal/infrastructure/data/db"
)
// Maps a Prisma User model to the domain User model.
func mapPrismaUserToDomain(user db.UserDboModel) entities.User {
return entities.User{
ID: user.ID,
Name: user.Name,
Email: user.Email,
Password: user.Password,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
}
// Maps a slice of Prisma User models to domain User models.
func mapPrismaUsersToDomain(users []db.UserDboModel) []entities.User {
domainUsers := make([]entities.User, len(users))
for i, user := range users {
domainUsers[i] = mapPrismaUserToDomain(user)
}
return domainUsers
}
func mapPrismaProjectToDomain(project db.ProjectDboModel) entities.Project {
return entities.Project{
ID: project.ID,
Name: project.Name,
UserID: project.UserID,
Description: NullableField(project.Description),
ClientID: NullableField(project.ClientID),
CreatedAt: project.CreatedAt,
UpdatedAt: project.UpdatedAt,
}
}
func mapPrismaProjectsToDomain(projects []db.ProjectDboModel) []entities.Project {
domainProjects := make([]entities.Project, len(projects))
for i, project := range projects {
domainProjects[i] = mapPrismaProjectToDomain(project)
}
return domainProjects
}
func mapPrismaTimeEntryToDomain(timeEntry db.TimeEntryDboModel) entities.TimeEntry {
return entities.TimeEntry{
ID: timeEntry.ID,
ProjectID: timeEntry.ProjectID,
UserID: timeEntry.UserID,
StartTime: timeEntry.StartTime,
EndTime: NullableField(timeEntry.EndTime),
Description: NullableField(timeEntry.Description),
CreatedAt: timeEntry.CreatedAt,
UpdatedAt: timeEntry.UpdatedAt,
}
}
func mapPrismaTimeEntriesToDomain(timeEntries []db.TimeEntryDboModel) []entities.TimeEntry {
domainTimeEntries := make([]entities.TimeEntry, len(timeEntries))
for i, timeEntry := range timeEntries {
domainTimeEntries[i] = mapPrismaTimeEntryToDomain(timeEntry)
}
return domainTimeEntries
}
func mapPrismaProjectTaskToDomain(projectTask db.ProjectTaskDboModel) entities.ProjectTask {
return entities.ProjectTask{
ID: projectTask.ID,
ProjectID: projectTask.ProjectID,
Name: projectTask.Name,
Description: NullableField(projectTask.Description),
CreatedAt: projectTask.CreatedAt,
UpdatedAt: projectTask.UpdatedAt,
}
}
func mapPrismaProjectTasksToDomain(projectTasks []db.ProjectTaskDboModel) []entities.ProjectTask {
domainProjectTasks := make([]entities.ProjectTask, len(projectTasks))
for i, projectTask := range projectTasks {
domainProjectTasks[i] = mapPrismaProjectTaskToDomain(projectTask)
}
return domainProjectTasks
}
@@ -0,0 +1,66 @@
package data
import (
"actatempus_backend/internal/domain/data"
"actatempus_backend/internal/infrastructure/data/db"
"context"
"log"
)
// PrismaDatabase provides access to various data sources and manages the Prisma client.
type PrismaDatabase struct {
client *db.PrismaClient
users data.UserDataSource
timeEntries data.TimeEntryDataSource
projectTasks data.ProjectTaskDataSource
projects data.ProjectDataSource
}
// NewPrismaDatabase initializes the Prisma client and the data sources.
func NewPrismaDatabase() (*PrismaDatabase, error) {
// Create a new Prisma client
client := db.NewClient()
// Test the connection to ensure the client is working
if err := client.Connect(); err != nil {
return nil, err
}
// Initialize the database and data sources
db := &PrismaDatabase{
client: client,
users: NewPrismaUserDataSource(client),
timeEntries: NewPrismaTimeEntryDataSource(client),
projectTasks: NewPrismaProjectTaskDataSource(client),
projects: NewPrismaProjectDataSource(client),
}
log.Println("Database initialized")
return db, nil
}
// Users returns the User data source.
func (db *PrismaDatabase) Users() data.UserDataSource {
return db.users
}
// TimeEntries returns the TimeEntry data source.
func (db *PrismaDatabase) TimeEntries() data.TimeEntryDataSource {
return db.timeEntries
}
// ProjectTasks returns the ProjectTask data source.
func (db *PrismaDatabase) ProjectTasks() data.ProjectTaskDataSource {
return db.projectTasks
}
// Projects returns the Project data source.
func (db *PrismaDatabase) Projects() data.ProjectDataSource {
return db.projects
}
// Close releases the Prisma client connection.
func (db *PrismaDatabase) Close(ctx context.Context) error {
log.Println("Closing database connection")
return db.client.Disconnect();
}
@@ -0,0 +1,123 @@
package data
import (
"actatempus_backend/internal/domain/app_error"
"actatempus_backend/internal/domain/entities"
"actatempus_backend/internal/infrastructure/data/db"
"context"
"fmt"
E "github.com/IBM/fp-go/either"
)
type PrismaProjectDataSource struct {
client *db.PrismaClient
}
func NewPrismaProjectDataSource(client *db.PrismaClient) *PrismaProjectDataSource {
return &PrismaProjectDataSource{client: client}
}
// Create a new project
func (ds *PrismaProjectDataSource) Create(ctx context.Context, project entities.ProjectCreate) E.Either[error, entities.Project] {
createdProject, err := ds.client.ProjectDbo.CreateOne(
db.ProjectDbo.Name.Set(project.Name),
db.ProjectDbo.User.Link(
db.UserDbo.ID.Equals(project.UserID),
),
db.ProjectDbo.UserID.Set(project.UserID),
db.ProjectDbo.Description.SetIfPresent(project.Description),
db.ProjectDbo.ClientID.SetIfPresent(project.ClientID),
).Exec(ctx)
if err != nil {
return E.Left[entities.Project,error](app_error.NewInternalError(err))
}
if createdProject == nil {
return E.Left[entities.Project,error](app_error.NewInternalError(fmt.Errorf("Could not create project")))
}
return E.Right[error](mapPrismaProjectToDomain(*createdProject))
}
// FindByID retrieves a project by ID
func (ds *PrismaProjectDataSource) FindByID(ctx context.Context, id string) E.Either[error, entities.Project] {
project, err := ds.client.ProjectDbo.FindUnique(
db.ProjectDbo.ID.Equals(id),
).Exec(ctx)
if err != nil {
return E.Left[entities.Project](handleDBError(err, fmt.Sprintf("Project with ID %s not found", id)))
}
if project == nil {
return E.Left[entities.Project,error](app_error.NewNotFoundError(fmt.Sprintf("Project with ID %s not found", id)))
}
return E.Right[error](mapPrismaProjectToDomain(*project))
}
// Update updates a project
func (ds *PrismaProjectDataSource) Update(ctx context.Context, project entities.ProjectUpdate) E.Either[error, entities.Project] {
updatedProject, err := ds.client.ProjectDbo.FindUnique(
db.ProjectDbo.ID.Equals(project.ID),
).Update(
db.ProjectDbo.Name.SetIfPresent(project.Name),
db.ProjectDbo.Description.SetIfPresent(project.Description),
db.ProjectDbo.ClientID.SetIfPresent(project.ClientID),
db.ProjectDbo.User.Link(
db.UserDbo.ID.EqualsIfPresent(project.UserID),
),
).Exec(ctx)
if err != nil {
return E.Left[entities.Project](handleDBError(err, fmt.Sprintf("Could not update project with ID %s", project.ID)))
}
if updatedProject == nil {
return E.Left[entities.Project,error](app_error.NewNotFoundError(fmt.Sprintf("Project with ID %s not found", project.ID)))
}
return E.Right[error](mapPrismaProjectToDomain(*updatedProject))
}
// Delete removes a project
func (ds *PrismaProjectDataSource) Delete(ctx context.Context, id string) E.Either[error, entities.Project] {
deleted, err := ds.client.ProjectDbo.FindUnique(
db.ProjectDbo.ID.Equals(id),
).Delete().Exec(ctx)
if err != nil {
return E.Left[entities.Project](handleDBError(err, fmt.Sprintf("Could not delete project with ID %s", id)))
}
if deleted == nil {
return E.Left[entities.Project,error](app_error.NewNotFoundError(fmt.Sprintf("Project with ID %s not found", id)))
}
return E.Right[error](mapPrismaProjectToDomain(*deleted))
}
// FindAll retrieves all projects
func (ds *PrismaProjectDataSource) FindAll(ctx context.Context) E.Either[error, []entities.Project] {
projects, err := ds.client.ProjectDbo.FindMany().Exec(ctx)
if err != nil {
return E.Left[[]entities.Project](handleDBError(err, "Could not retrieve projects"))
}
return E.Right[error](mapPrismaProjectsToDomain(projects))
}
// FindByUserID retrieves all projects for a user
func (ds *PrismaProjectDataSource) FindByUserID(ctx context.Context, userID string) E.Either[error, []entities.Project] {
projects, err := ds.client.ProjectDbo.FindMany(
db.ProjectDbo.UserID.Equals(userID),
).Exec(ctx)
if err != nil {
return E.Left[[]entities.Project](handleDBError(err, fmt.Sprintf("Could not retrieve projects for user with ID %s", userID)))
}
return E.Right[error](mapPrismaProjectsToDomain(projects))
}
@@ -0,0 +1,119 @@
package data
import (
"actatempus_backend/internal/domain/app_error"
"actatempus_backend/internal/domain/entities"
"actatempus_backend/internal/infrastructure/data/db"
"context"
"fmt"
E "github.com/IBM/fp-go/either"
)
type PrismaProjectTaskDataSource struct {
client *db.PrismaClient
}
func NewPrismaProjectTaskDataSource(client *db.PrismaClient) *PrismaProjectTaskDataSource {
return &PrismaProjectTaskDataSource{client: client}
}
// Create a new ProjectTask
func (ds *PrismaProjectTaskDataSource) Create(ctx context.Context, task entities.ProjectTaskCreate) E.Either[error, entities.ProjectTask] {
createdTask, err := ds.client.ProjectTaskDbo.CreateOne(
db.ProjectTaskDbo.Name.Set(task.Name),
db.ProjectTaskDbo.Project.Link(
db.ProjectDbo.ID.Equals(task.ProjectID),
),
db.ProjectTaskDbo.Description.SetIfPresent(task.Description),
).Exec(ctx)
if err != nil {
return E.Left[entities.ProjectTask,error](handleDBError(err, fmt.Sprintf("Could not create project task")))
}
if createdTask == nil {
return E.Left[entities.ProjectTask,error](app_error.NewInternalError(fmt.Errorf("Could not create project task")))
}
return E.Right[error](mapPrismaProjectTaskToDomain(*createdTask))
}
// Find ProjectTask by ID
func (ds *PrismaProjectTaskDataSource) FindByID(ctx context.Context, id string) E.Either[error, entities.ProjectTask] {
task, err := ds.client.ProjectTaskDbo.FindUnique(
db.ProjectTaskDbo.ID.Equals(id),
).Exec(ctx)
if err != nil {
return E.Left[entities.ProjectTask](handleDBError(err, fmt.Sprintf("ProjectTask with ID %s not found", id)))
}
if task == nil {
return E.Left[entities.ProjectTask,error](app_error.NewNotFoundError(fmt.Sprintf("ProjectTask with ID %s not found", id)))
}
return E.Right[error](mapPrismaProjectTaskToDomain(*task))
}
// Update an existing ProjectTask
func (ds *PrismaProjectTaskDataSource) Update(ctx context.Context, task entities.ProjectTaskUpdate) E.Either[error, entities.ProjectTask] {
updatedTask, err := ds.client.ProjectTaskDbo.FindUnique(
db.ProjectTaskDbo.ID.Equals(task.ID),
).Update(
db.ProjectTaskDbo.Name.SetIfPresent(task.Name),
db.ProjectTaskDbo.Description.SetIfPresent(task.Description),
db.ProjectTaskDbo.Project.Link(
db.ProjectDbo.ID.EqualsIfPresent(task.ProjectID),
),
).Exec(ctx)
if err != nil {
return E.Left[entities.ProjectTask](handleDBError(err, fmt.Sprintf("Could not update project task with ID %s", task.ID)))
}
if updatedTask == nil {
return E.Left[entities.ProjectTask,error](app_error.NewNotFoundError(fmt.Sprintf("ProjectTask with ID %s not found", task.ID)))
}
return E.Right[error](mapPrismaProjectTaskToDomain(*updatedTask))
}
// Delete a ProjectTask
func (ds *PrismaProjectTaskDataSource) Delete(ctx context.Context, id string) E.Either[error, entities.ProjectTask] {
deletedTask, err := ds.client.ProjectTaskDbo.FindUnique(
db.ProjectTaskDbo.ID.Equals(id),
).Delete().Exec(ctx)
if err != nil {
return E.Left[entities.ProjectTask](handleDBError(err, fmt.Sprintf("Could not delete project task with ID %s", id)))
}
if deletedTask == nil {
return E.Left[entities.ProjectTask,error](app_error.NewNotFoundError(fmt.Sprintf("ProjectTask with ID %s not found", id)))
}
return E.Right[error](mapPrismaProjectTaskToDomain(*deletedTask))
}
// FindAll retrieves all ProjectTasks
func (ds *PrismaProjectTaskDataSource) FindAll(ctx context.Context) E.Either[error, []entities.ProjectTask] {
tasks, err := ds.client.ProjectTaskDbo.FindMany().Exec(ctx)
if err != nil {
return E.Left[[]entities.ProjectTask](handleDBError(err, "Could not retrieve project tasks"))
}
return E.Right[error](mapPrismaProjectTasksToDomain(tasks))
}
// FindByProjectID retrieves all ProjectTasks for a given Project
func (ds *PrismaProjectTaskDataSource) FindByProjectID(ctx context.Context, projectID string) E.Either[error, []entities.ProjectTask] {
tasks, err := ds.client.ProjectTaskDbo.FindMany(
db.ProjectTaskDbo.ProjectID.Equals(projectID),
).Exec(ctx)
if err != nil {
return E.Left[[]entities.ProjectTask](handleDBError(err, fmt.Sprintf("Could not retrieve project tasks for project with ID %s", projectID)))
}
return E.Right[error](mapPrismaProjectTasksToDomain(tasks))
}
@@ -0,0 +1,140 @@
package data
import (
"actatempus_backend/internal/domain/app_error"
"actatempus_backend/internal/domain/entities"
"actatempus_backend/internal/infrastructure/data/db"
"context"
"fmt"
E "github.com/IBM/fp-go/either"
)
type PrismaTimeEntryDataSource struct {
client *db.PrismaClient
}
func NewPrismaTimeEntryDataSource(client *db.PrismaClient) *PrismaTimeEntryDataSource {
return &PrismaTimeEntryDataSource{client: client}
}
// Create a new TimeEntry
func (ds *PrismaTimeEntryDataSource) Create(ctx context.Context, entry entities.TimeEntryCreate) E.Either[error, entities.TimeEntry] {
createdEntry, err := ds.client.TimeEntryDbo.CreateOne(
db.TimeEntryDbo.StartTime.Set(entry.StartTime),
db.TimeEntryDbo.User.Link(
db.UserDbo.ID.Equals(entry.UserID),
), db.TimeEntryDbo.Project.Link(
db.ProjectDbo.ID.Equals(entry.ProjectID),
),
db.TimeEntryDbo.EndTime.SetIfPresent(entry.EndTime),
db.TimeEntryDbo.Description.SetIfPresent(entry.Description),
).Exec(ctx)
if err != nil {
return E.Left[entities.TimeEntry,error](app_error.NewInternalError(err))
}
if createdEntry == nil {
return E.Left[entities.TimeEntry,error](app_error.NewInternalError(fmt.Errorf("Could not create time entry")))
}
return E.Right[error](mapPrismaTimeEntryToDomain(*createdEntry))
}
// Find TimeEntry by ID
func (ds *PrismaTimeEntryDataSource) FindByID(ctx context.Context, id string) E.Either[error, entities.TimeEntry] {
entry, err := ds.client.TimeEntryDbo.FindUnique(
db.TimeEntryDbo.ID.Equals(id),
).Exec(ctx)
if err != nil {
return E.Left[entities.TimeEntry](handleDBError(err, fmt.Sprintf("TimeEntry with ID %s not found", id)))
}
if entry == nil {
return E.Left[entities.TimeEntry,error](app_error.NewNotFoundError(fmt.Sprintf("TimeEntry with ID %s not found", id)))
}
return E.Right[error](mapPrismaTimeEntryToDomain(*entry))
}
// Update an existing TimeEntry
func (ds *PrismaTimeEntryDataSource) Update(ctx context.Context, entry entities.TimeEntryUpdate) E.Either[error, entities.TimeEntry] {
updatedEntry, err := ds.client.TimeEntryDbo.FindUnique(
db.TimeEntryDbo.ID.Equals(entry.ID),
).Update(
db.TimeEntryDbo.StartTime.SetIfPresent(entry.StartTime),
db.TimeEntryDbo.EndTime.SetIfPresent(entry.EndTime),
db.TimeEntryDbo.Description.SetIfPresent(entry.Description),
db.TimeEntryDbo.User.Link(
db.UserDbo.ID.EqualsIfPresent(entry.UserID),
),
db.TimeEntryDbo.Project.Link(
db.ProjectDbo.ID.EqualsIfPresent(entry.ProjectID),
),
).Exec(ctx)
if err != nil {
return E.Left[entities.TimeEntry,error](handleDBError(err, fmt.Sprintf("Could not update time entry with ID %s", entry.ID)))
}
if updatedEntry == nil {
return E.Left[entities.TimeEntry,error](app_error.NewNotFoundError(fmt.Sprintf("TimeEntry with ID %s not found", entry.ID)))
}
return E.Right[error](mapPrismaTimeEntryToDomain(*updatedEntry))
}
// Delete a TimeEntry
func (ds *PrismaTimeEntryDataSource) Delete(ctx context.Context, id string) E.Either[error, entities.TimeEntry] {
deletedEntry, err := ds.client.TimeEntryDbo.FindUnique(
db.TimeEntryDbo.ID.Equals(id),
).Delete().Exec(ctx)
if err != nil {
return E.Left[entities.TimeEntry](handleDBError(err, fmt.Sprintf("Could not delete time entry with ID %s", id)))
}
if deletedEntry == nil {
return E.Left[entities.TimeEntry,error](app_error.NewNotFoundError(fmt.Sprintf("TimeEntry with ID %s not found", id)))
}
return E.Right[error](mapPrismaTimeEntryToDomain(*deletedEntry))
}
// FindAll retrieves all TimeEntries
func (ds *PrismaTimeEntryDataSource) FindAll(ctx context.Context) E.Either[error, []entities.TimeEntry] {
entries, err := ds.client.TimeEntryDbo.FindMany().Exec(ctx)
if err != nil {
return E.Left[[]entities.TimeEntry](handleDBError(err, "Could not retrieve time entries"))
}
return E.Right[error](mapPrismaTimeEntriesToDomain(entries))
}
// FindByUserID retrieves all TimeEntries by UserID
func (ds *PrismaTimeEntryDataSource) FindByUserID(ctx context.Context, userID string) E.Either[error, []entities.TimeEntry] {
entries, err := ds.client.TimeEntryDbo.FindMany(
db.TimeEntryDbo.UserID.Equals(userID),
).Exec(ctx)
if err != nil {
return E.Left[[]entities.TimeEntry](handleDBError(err, fmt.Sprintf("Could not retrieve time entries for user with ID %s", userID)))
}
return E.Right[error](mapPrismaTimeEntriesToDomain(entries))
}
// FindByProjectID retrieves all TimeEntries by ProjectID
func (ds *PrismaTimeEntryDataSource) FindByProjectID(ctx context.Context, projectID string) E.Either[error, []entities.TimeEntry] {
entries, err := ds.client.TimeEntryDbo.FindMany(
db.TimeEntryDbo.ProjectID.Equals(projectID),
).Exec(ctx)
if err != nil {
return E.Left[[]entities.TimeEntry](handleDBError(err, fmt.Sprintf("Could not retrieve time entries for project with ID %s", projectID)))
}
return E.Right[error](mapPrismaTimeEntriesToDomain(entries))
}
@@ -0,0 +1,115 @@
package data
import (
"actatempus_backend/internal/domain/app_error"
"actatempus_backend/internal/domain/entities"
"actatempus_backend/internal/infrastructure/data/db"
"context"
"fmt"
E "github.com/IBM/fp-go/either"
)
type PrismaUserDataSource struct {
client *db.PrismaClient
}
func NewPrismaUserDataSource(client *db.PrismaClient) *PrismaUserDataSource {
return &PrismaUserDataSource{client: client}
}
func (ds *PrismaUserDataSource) Create(ctx context.Context, user entities.UserCreate) E.Either[error,entities.User] {
createdUser, err := ds.client.UserDbo.CreateOne(
db.UserDbo.Name.Set(user.Name),
db.UserDbo.Email.Set(user.Email),
db.UserDbo.Password.Set(user.Password),
).Exec(ctx)
if err != nil {
return E.Left[entities.User,error](app_error.NewInternalError(err))
}
if createdUser == nil {
return E.Left[entities.User,error](app_error.NewInternalError(fmt.Errorf("Could not create user")))
}
return E.Right[error](mapPrismaUserToDomain(*createdUser))
}
func (ds *PrismaUserDataSource) FindByID(ctx context.Context, id string) E.Either[error,entities.User] {
user, err := ds.client.UserDbo.FindUnique(
db.UserDbo.ID.Equals(id),
).Exec(ctx)
if err != nil {
return E.Left[entities.User,error](handleDBError(err, fmt.Sprintf("Query for user with ID %s failed", id)))
}
if user == nil {
return E.Left[entities.User,error](app_error.NewNotFoundError(fmt.Sprintf("User with ID %s not found", id)))
}
return E.Right[error](mapPrismaUserToDomain(*user))
}
func (ds *PrismaUserDataSource) FindByEmail(ctx context.Context, email string) E.Either[error,entities.User]{
user, err := ds.client.UserDbo.FindUnique(
db.UserDbo.Email.Equals(email),
).Exec(ctx)
if err != nil {
return E.Left[entities.User,error](handleDBError(err, fmt.Sprintf("Query for user with email %s failed", email)))
}
if user == nil {
return E.Left[entities.User,error](app_error.NewNotFoundError(fmt.Sprintf("User with email %s not found", email)))
}
return E.Right[error](mapPrismaUserToDomain(*user))
}
func (ds *PrismaUserDataSource) Update(ctx context.Context, user entities.UserUpdate) E.Either[error,entities.User] {
updatedUser, err := ds.client.UserDbo.FindUnique(
db.UserDbo.ID.Equals(user.ID),
).Update(
db.UserDbo.Name.SetIfPresent(user.Name),
db.UserDbo.Email.SetIfPresent(user.Email),
db.UserDbo.Password.SetIfPresent(user.Password),
).Exec(ctx)
if err != nil {
return E.Left[entities.User,error](handleDBError(err, fmt.Sprintf("Could not update user with ID %s", user.ID)))
}
if updatedUser == nil {
return E.Left[entities.User,error](app_error.NewNotFoundError(fmt.Sprintf("User with ID %s not found", user.ID)))
}
return E.Right[error](mapPrismaUserToDomain(*updatedUser))
}
func (ds *PrismaUserDataSource) Delete(ctx context.Context, id string) E.Either[error,entities.User] {
deleted, err := ds.client.UserDbo.FindUnique(
db.UserDbo.ID.Equals(id),
).Delete().Exec(ctx)
if err != nil {
return E.Left[entities.User,error](handleDBError(err, fmt.Sprintf("Could not delete user with ID %s", id)))
}
if deleted == nil {
return E.Left[entities.User,error](app_error.NewNotFoundError(fmt.Sprintf("User with ID %s not found", id)))
}
return E.Right[error](mapPrismaUserToDomain(*deleted))
}
func (ds *PrismaUserDataSource) FindAll(ctx context.Context) E.Either[error,[]entities.User] {
users, err := ds.client.UserDbo.FindMany().Exec(ctx)
if err != nil {
return E.Left[[]entities.User,error](handleDBError(err, "Could not retrieve users"))
}
return E.Right[error](mapPrismaUsersToDomain(users))
}
@@ -1,7 +1,7 @@
package http
import (
"actatempus_backend/internal/infrastructure/persistence/config"
"actatempus_backend/internal/infrastructure/config"
"fmt"
"net/http"
)
+11
View File
@@ -0,0 +1,11 @@
package utils
// Let applies a function `f` to a pointer value `ptr` if it's not nil.
// Returns nil if the pointer is nil.
func Let[T any, R any](ptr *T, f func(T) R) *R {
if ptr == nil {
return nil
}
result := f(*ptr)
return &result
}
Binary file not shown.
+60
View File
@@ -0,0 +1,60 @@
generator goClient {
provider = "go run github.com/steebchen/prisma-client-go"
output = "../internal/infrastructure/data/db"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
// User Model
model UserDbo {
id String @id @default(uuid())
name String
email String @unique
password String
projects ProjectDbo[] // Beziehung zu Projekten
timeEntries TimeEntryDbo[] // Beziehung zu Zeiteinträgen
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Project Model
model ProjectDbo {
id String @id @default(uuid())
name String
description String?
clientId String?
tasks ProjectTaskDbo[] // Beziehung zu Aufgaben
timeEntries TimeEntryDbo[] // Beziehung zu Zeiteinträgen
user UserDbo @relation(fields: [userId], references: [id])
userId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// TimeEntry Model
model TimeEntryDbo {
id String @id @default(uuid())
startTime DateTime
endTime DateTime?
description String?
user UserDbo @relation(fields: [userId], references: [id])
userId String
project ProjectDbo @relation(fields: [projectId], references: [id])
projectId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// Task Model (optional)
model ProjectTaskDbo {
id String @id @default(uuid())
name String
description String?
project ProjectDbo @relation(fields: [projectId], references: [id])
projectId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}