diff --git a/Makefile b/Makefile index 28b2eab3..c387c8c9 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,7 @@ fmt: ## Format all go files @go fmt ./... check: ## Run static analysis on all go files - staticcheck ./... + staticcheck -f stylish ./... test: check ## Run all tests go test ./... diff --git a/internal/api/dbmodels/board.sql.go b/internal/api/dbmodels/board.sql.go index 1ce78a0d..217ce3d2 100644 --- a/internal/api/dbmodels/board.sql.go +++ b/internal/api/dbmodels/board.sql.go @@ -56,21 +56,34 @@ INSERT INTO position ( oid, semester, - tier + tier, + full_name, + title, + team ) VALUES -(?, ?, ?) +(?, ?, ?, ?, ?, ?) RETURNING oid, semester, tier, full_name, title, team ` type CreatePositionParams struct { - Oid string `json:"oid"` - Semester string `json:"semester"` - Tier int64 `json:"tier"` + Oid string `json:"oid"` + Semester string `json:"semester"` + Tier int64 `json:"tier"` + FullName string `json:"full_name"` + Title sql.NullString `json:"title"` + Team sql.NullString `json:"team"` } func (q *Queries) CreatePosition(ctx context.Context, arg CreatePositionParams) (Position, error) { - row := q.db.QueryRowContext(ctx, createPosition, arg.Oid, arg.Semester, arg.Tier) + row := q.db.QueryRowContext(ctx, createPosition, + arg.Oid, + arg.Semester, + arg.Tier, + arg.FullName, + arg.Title, + arg.Team, + ) var i Position err := row.Scan( &i.Oid, diff --git a/internal/api/docs/docs.go b/internal/api/docs/docs.go index 808210d4..7e410d65 100644 --- a/internal/api/docs/docs.go +++ b/internal/api/docs/docs.go @@ -94,7 +94,7 @@ const docTemplate = `{ "200": { "description": "Announcement details", "schema": { - "$ref": "#/definitions/dbmodels.Announcement" + "$ref": "#/definitions/dto.Announcement" } }, "404": { @@ -1294,26 +1294,6 @@ const docTemplate = `{ } }, "definitions": { - "dbmodels.Announcement": { - "type": "object", - "properties": { - "announce_at": { - "type": "integer" - }, - "discord_channel_id": { - "$ref": "#/definitions/sql.NullString" - }, - "discord_message_id": { - "$ref": "#/definitions/sql.NullString" - }, - "uuid": { - "type": "string" - }, - "visibility": { - "type": "string" - } - } - }, "dbmodels.CreateAnnouncementParams": { "type": "object", "properties": { @@ -1380,14 +1360,23 @@ const docTemplate = `{ "dbmodels.CreatePositionParams": { "type": "object", "properties": { + "full_name": { + "type": "string" + }, "oid": { "type": "string" }, "semester": { "type": "string" }, + "team": { + "$ref": "#/definitions/sql.NullString" + }, "tier": { "type": "integer" + }, + "title": { + "$ref": "#/definitions/sql.NullString" } } }, @@ -1608,6 +1597,26 @@ const docTemplate = `{ } } }, + "dto.Announcement": { + "type": "object", + "properties": { + "announce_at": { + "type": "integer" + }, + "discord_channel_id": { + "type": "string" + }, + "discord_message_id": { + "type": "string" + }, + "uuid": { + "type": "string" + }, + "visibility": { + "type": "string" + } + } + }, "sql.NullBool": { "type": "object", "properties": { diff --git a/internal/api/docs/swagger.json b/internal/api/docs/swagger.json index 08f86682..b26c6f34 100644 --- a/internal/api/docs/swagger.json +++ b/internal/api/docs/swagger.json @@ -83,7 +83,7 @@ "200": { "description": "Announcement details", "schema": { - "$ref": "#/definitions/dbmodels.Announcement" + "$ref": "#/definitions/dto.Announcement" } }, "404": { @@ -1283,26 +1283,6 @@ } }, "definitions": { - "dbmodels.Announcement": { - "type": "object", - "properties": { - "announce_at": { - "type": "integer" - }, - "discord_channel_id": { - "$ref": "#/definitions/sql.NullString" - }, - "discord_message_id": { - "$ref": "#/definitions/sql.NullString" - }, - "uuid": { - "type": "string" - }, - "visibility": { - "type": "string" - } - } - }, "dbmodels.CreateAnnouncementParams": { "type": "object", "properties": { @@ -1369,14 +1349,23 @@ "dbmodels.CreatePositionParams": { "type": "object", "properties": { + "full_name": { + "type": "string" + }, "oid": { "type": "string" }, "semester": { "type": "string" }, + "team": { + "$ref": "#/definitions/sql.NullString" + }, "tier": { "type": "integer" + }, + "title": { + "$ref": "#/definitions/sql.NullString" } } }, @@ -1597,6 +1586,26 @@ } } }, + "dto.Announcement": { + "type": "object", + "properties": { + "announce_at": { + "type": "integer" + }, + "discord_channel_id": { + "type": "string" + }, + "discord_message_id": { + "type": "string" + }, + "uuid": { + "type": "string" + }, + "visibility": { + "type": "string" + } + } + }, "sql.NullBool": { "type": "object", "properties": { diff --git a/internal/api/docs/swagger.yaml b/internal/api/docs/swagger.yaml index c7679583..8d0bf9b0 100644 --- a/internal/api/docs/swagger.yaml +++ b/internal/api/docs/swagger.yaml @@ -1,17 +1,4 @@ definitions: - dbmodels.Announcement: - properties: - announce_at: - type: integer - discord_channel_id: - $ref: '#/definitions/sql.NullString' - discord_message_id: - $ref: '#/definitions/sql.NullString' - uuid: - type: string - visibility: - type: string - type: object dbmodels.CreateAnnouncementParams: properties: announce_at: @@ -55,12 +42,18 @@ definitions: type: object dbmodels.CreatePositionParams: properties: + full_name: + type: string oid: type: string semester: type: string + team: + $ref: '#/definitions/sql.NullString' tier: type: integer + title: + $ref: '#/definitions/sql.NullString' type: object dbmodels.CreateTierParams: properties: @@ -203,6 +196,19 @@ definitions: title: $ref: '#/definitions/sql.NullString' type: object + dto.Announcement: + properties: + announce_at: + type: integer + discord_channel_id: + type: string + discord_message_id: + type: string + uuid: + type: string + visibility: + type: string + type: object sql.NullBool: properties: bool: @@ -317,7 +323,7 @@ paths: "200": description: Announcement details schema: - $ref: '#/definitions/dbmodels.Announcement' + $ref: '#/definitions/dto.Announcement' "404": description: Not Found schema: diff --git a/internal/api/handlers/announcement.go b/internal/api/handlers/announcement.go index 2fc804eb..98caf443 100644 --- a/internal/api/handlers/announcement.go +++ b/internal/api/handlers/announcement.go @@ -3,10 +3,15 @@ package handlers import ( + "database/sql" + "errors" + "fmt" + "log" "net/http" "github.com/acmcsufoss/api.acmcsuf.com/internal/api/dbmodels" "github.com/acmcsufoss/api.acmcsuf.com/internal/api/services" + "github.com/acmcsufoss/api.acmcsuf.com/internal/dto" "github.com/gin-gonic/gin" ) @@ -26,7 +31,7 @@ func NewAnnouncementHandler(announcementService services.AnnouncementServicer) * // @Accept json // @Produce json // @Param id path string true "Announcement ID" -// @Success 200 {object} dbmodels.Announcement "Announcement details" +// @Success 200 {object} dto.Announcement "Announcement details" // @Failure 404 {object} map[string]string // @Failure 500 {object} map[string]string // @Router /v1/announcements/{id} [get] @@ -35,9 +40,8 @@ func (h *AnnouncementHandler) GetAnnouncement(c *gin.Context) { id := c.Param("id") announcement, err := h.announcementService.Get(ctx, id) - if err != nil { - if err.Error() == "sql: no rows in result set" { + if errors.Is(err, sql.ErrNoRows) { c.JSON(http.StatusNotFound, gin.H{ "error": "Announcement not found", }) @@ -46,9 +50,28 @@ func (h *AnnouncementHandler) GetAnnouncement(c *gin.Context) { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to retrieve announcement", }) + log.Println("Failed to retrieve announcement:", err) + return + } + + // NOTE: We won't have to do this once implement domain models + var discordChannelID *string + if announcement.DiscordChannelID.Valid { + discordChannelID = &announcement.DiscordChannelID.String + } + var discordMessageID *string + if announcement.DiscordMessageID.Valid { + discordMessageID = &announcement.DiscordMessageID.String + } + dto := dto.Announcement{ + Uuid: announcement.Uuid, + Visibility: announcement.Visibility, + AnnounceAt: announcement.AnnounceAt, + DiscordChannelID: discordChannelID, + DiscordMessageID: discordMessageID, } - c.JSON(http.StatusOK, announcement) + c.JSON(http.StatusOK, dto) } func (h *AnnouncementHandler) GetAnnouncements(c *gin.Context) { @@ -80,7 +103,7 @@ func (h *AnnouncementHandler) GetAnnouncements(c *gin.Context) { // @Router /v1/announcements [post] func (h *AnnouncementHandler) CreateAnnouncement(c *gin.Context) { ctx := c.Request.Context() - var params dbmodels.CreateAnnouncementParams + var params dto.Announcement if err := c.ShouldBindJSON(¶ms); err != nil { c.JSON(http.StatusBadRequest, gin.H{ @@ -89,8 +112,26 @@ func (h *AnnouncementHandler) CreateAnnouncement(c *gin.Context) { return } + var chanID sql.NullString + if params.DiscordChannelID != nil { + chanID = sql.NullString{String: *params.DiscordChannelID, Valid: true} + } + + var msgID sql.NullString + if params.DiscordMessageID != nil { + msgID = sql.NullString{String: *params.DiscordMessageID, Valid: true} + } + dbParams := dbmodels.CreateAnnouncementParams{ + Uuid: params.Uuid, + Visibility: params.Visibility, + AnnounceAt: params.AnnounceAt, + DiscordChannelID: chanID, + DiscordMessageID: msgID, + } + + fmt.Println("DTO ->", params, "\nDBMODEL->", dbParams) // TODO: error out if required fields aren't provided - if err := h.announcementService.Create(ctx, params); err != nil { + if err := h.announcementService.Create(ctx, dbParams); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to create announcement", }) @@ -119,7 +160,7 @@ func (h *AnnouncementHandler) CreateAnnouncement(c *gin.Context) { // @Router /v1/announcements/{id} [put] func (h *AnnouncementHandler) UpdateAnnouncement(c *gin.Context) { ctx := c.Request.Context() - var params dbmodels.UpdateAnnouncementParams + var params dto.UpdateAnnouncement id := c.Param("id") if err := c.ShouldBindJSON(¶ms); err != nil { @@ -128,7 +169,34 @@ func (h *AnnouncementHandler) UpdateAnnouncement(c *gin.Context) { }) } - if err := h.announcementService.Update(ctx, id, params); err != nil { + var chanID sql.NullString + if params.DiscordChannelID != nil { + chanID = sql.NullString{String: *params.DiscordChannelID, Valid: true} + } + + var msgID sql.NullString + if params.DiscordMessageID != nil { + msgID = sql.NullString{String: *params.DiscordMessageID, Valid: true} + } + + var vis sql.NullString + if params.Visibility != nil { + vis = sql.NullString{String: *params.Visibility, Valid: true} + } + + var announceAt sql.NullInt64 + if params.AnnounceAt != nil { + announceAt = sql.NullInt64{Int64: *params.AnnounceAt, Valid: true} + } + dbParams := dbmodels.UpdateAnnouncementParams{ + Uuid: params.Uuid, + Visibility: vis, + AnnounceAt: announceAt, + DiscordChannelID: chanID, + DiscordMessageID: msgID, + } + + if err := h.announcementService.Update(ctx, id, dbParams); err != nil { c.JSON(http.StatusInternalServerError, gin.H{ "error": "Failed to update announcement", }) diff --git a/internal/cli/announcements/post.go b/internal/cli/announcements/post.go index fbb2330a..a1fa018f 100644 --- a/internal/cli/announcements/post.go +++ b/internal/cli/announcements/post.go @@ -10,10 +10,10 @@ import ( "github.com/charmbracelet/huh" "github.com/spf13/cobra" - "github.com/acmcsufoss/api.acmcsuf.com/internal/api/dbmodels" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/config" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/forms" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/oauth" + "github.com/acmcsufoss/api.acmcsuf.com/internal/dto" "github.com/acmcsufoss/api.acmcsuf.com/utils" ) @@ -67,9 +67,8 @@ func postAnnouncement(cfg *config.Config) { utils.PrettyPrintJSON(body) } -// TODO: Use DTO models instaad of dbmodels -func postForm() (*dbmodels.CreateAnnouncementParams, error) { - var payload dbmodels.CreateAnnouncementParams +func postForm() (*dto.Announcement, error) { + var payload dto.Announcement var err error var ( announceAtStr string @@ -105,13 +104,12 @@ func postForm() (*dbmodels.CreateAnnouncementParams, error) { return nil, err } - // HACK: These conversions won't be necessary once we start using DTO models here payload.AnnounceAt, err = utils.ByteSlicetoUnix([]byte(announceAtStr)) if err != nil { return nil, err } - payload.DiscordChannelID = utils.StringtoNullString(channelIDStr) - payload.DiscordMessageID = utils.StringtoNullString(messageIDStr) + payload.DiscordChannelID = &channelIDStr + payload.DiscordMessageID = &messageIDStr return &payload, err } diff --git a/internal/cli/announcements/put.go b/internal/cli/announcements/put.go index e4ab8047..14651e28 100644 --- a/internal/cli/announcements/put.go +++ b/internal/cli/announcements/put.go @@ -10,9 +10,9 @@ import ( "github.com/charmbracelet/huh" "github.com/spf13/cobra" - "github.com/acmcsufoss/api.acmcsuf.com/internal/api/dbmodels" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/config" "github.com/acmcsufoss/api.acmcsuf.com/internal/cli/oauth" + "github.com/acmcsufoss/api.acmcsuf.com/internal/dto" "github.com/acmcsufoss/api.acmcsuf.com/utils" ) @@ -58,7 +58,7 @@ func putAnnouncements(id string, cfg *config.Config) { } // ----- Update found announceement ----- - var oldPayload dbmodels.CreateAnnouncementParams + var oldPayload dto.UpdateAnnouncement err = json.Unmarshal(body, &oldPayload) if err != nil { fmt.Println("Error: failed to unmarshal response body:", err) @@ -99,8 +99,8 @@ func putAnnouncements(id string, cfg *config.Config) { } // TODO: Use DTO models instaad of dbmodels -func putForm(uuid string) (*dbmodels.UpdateAnnouncementParams, error) { - var payload dbmodels.UpdateAnnouncementParams +func putForm(uuid string) (*dto.UpdateAnnouncement, error) { + var payload dto.UpdateAnnouncement var err error var ( visibilityStr string @@ -133,21 +133,17 @@ func putForm(uuid string) (*dbmodels.UpdateAnnouncementParams, error) { payload.Uuid = uuid // HACK: These conversions won't be necessary once we start using DTO models here - if visibilityStr != "" { - payload.Visibility = utils.StringtoNullString(visibilityStr) - } + payload.Visibility = &visibilityStr if announceAtStr != "" { timestamp, err := utils.ByteSlicetoUnix([]byte(announceAtStr)) if err != nil { return nil, err } - payload.AnnounceAt = utils.Int64toNullInt64(timestamp) - } - if channelIDStr != "" { - payload.DiscordChannelID = utils.StringtoNullString(channelIDStr) - } - if messageIDStr != "" { - payload.DiscordMessageID = utils.StringtoNullString(messageIDStr) + payload.AnnounceAt = ×tamp } + + payload.DiscordChannelID = &channelIDStr + payload.DiscordMessageID = &messageIDStr + return &payload, nil } diff --git a/internal/dto/announcement.go b/internal/dto/announcement.go new file mode 100644 index 00000000..26124232 --- /dev/null +++ b/internal/dto/announcement.go @@ -0,0 +1,17 @@ +package dto + +type Announcement struct { + Uuid string `json:"uuid"` + Visibility string `json:"visibility"` + AnnounceAt int64 `json:"announce_at"` + DiscordChannelID *string `json:"discord_channel_id"` + DiscordMessageID *string `json:"discord_message_id"` +} + +type UpdateAnnouncement struct { + Uuid string `json:"uuid"` + Visibility *string `json:"visibility"` + AnnounceAt *int64 `json:"announce_at"` + DiscordChannelID *string `json:"discord_channel_id"` + DiscordMessageID *string `json:"discord_message_id"` +} diff --git a/internal/dto/event.go b/internal/dto/event.go new file mode 100644 index 00000000..30ad285e --- /dev/null +++ b/internal/dto/event.go @@ -0,0 +1,19 @@ +package dto + +type Event struct { + Uuid string `json:"uuid"` + Location string `json:"location"` + StartAt int64 `json:"start_at"` + EndAt int64 `json:"end_at"` + IsAllDay bool `json:"is_all_day"` + Host string `json:"host"` +} + +type UpdateEvent struct { + Uuid string `json:"uuid"` + Location *string `json:"location"` + StartAt *int64 `json:"start_at"` + EndAt *int64 `json:"end_at"` + IsAllDay *bool `json:"is_all_day"` + Host *string `json:"host"` +} diff --git a/internal/dto/officer.go b/internal/dto/officer.go new file mode 100644 index 00000000..c22a6f5e --- /dev/null +++ b/internal/dto/officer.go @@ -0,0 +1,17 @@ +package dto + +type Officer struct { + Uuid string `json:"uuid"` + FullName string `json:"full_name"` + Picture *string `json:"picture"` + Github *string `json:"github"` + Discord *string `json:"discord"` +} + +type UpdateOfficer struct { + Uuid string `json:"uuid"` + FullName *string `json:"full_name"` + Picture *string `json:"picture"` + Github *string `json:"github"` + Discord *string `json:"discord"` +} diff --git a/internal/dto/position.go b/internal/dto/position.go new file mode 100644 index 00000000..52181991 --- /dev/null +++ b/internal/dto/position.go @@ -0,0 +1,19 @@ +package dto + +type Position struct { + Oid string `json:"oid"` + Semester string `json:"semester"` + Tier int `json:"tier"` + FullName string `json:"full_name"` + Title *string `json:"title"` + Team *string `json:"team"` +} + +type UpdatePosition struct { + Oid string `json:"oid"` + Semester string `json:"semester"` + Tier int `json:"tier"` + FullName string `json:"full_name"` + Title *string `json:"title"` + Team *string `json:"team"` +} diff --git a/internal/dto/tier.go b/internal/dto/tier.go new file mode 100644 index 00000000..41b2cb89 --- /dev/null +++ b/internal/dto/tier.go @@ -0,0 +1,15 @@ +package dto + +type Tier struct { + Tier int `json:"tier"` + Title *string `json:"title"` + Tindex *int `json:"t_index"` + Team *string `json:"team"` +} + +type UpdateTier struct { + Tier int `json:"tier"` + Title *string `json:"title"` + Tindex *int `json:"t_index"` + Team *string `json:"team"` +}