diff --git a/apps/api/Makefile b/apps/api/Makefile index 820dad83..84d5b63d 100644 --- a/apps/api/Makefile +++ b/apps/api/Makefile @@ -1,10 +1,10 @@ include .env.dev migrate-up: - @goose -dir ./internal/db/migrations postgres ${DATABASE_URL_MIGRATIONS} up + @goose -dir ./internal/db/migrations postgres ${DATABASE_URL_MIGRATION} up migrate-down: - @goose -dir ./internal/db/migrations postgres ${DATABASE_URL_MIGRATIONS} down + @goose -dir ./internal/db/migrations postgres ${DATABASE_URL_MIGRATION} down generate: @sqlc generate diff --git a/apps/api/internal/db/migrations/20250619161938_event_schema.sql b/apps/api/internal/db/migrations/20250619161938_event_schema.sql index ddb8929a..997e7ea9 100644 --- a/apps/api/internal/db/migrations/20250619161938_event_schema.sql +++ b/apps/api/internal/db/migrations/20250619161938_event_schema.sql @@ -13,7 +13,6 @@ CREATE TABLE events ( application_close TIMESTAMPTZ NOT NULL, rsvp_deadline TIMESTAMPTZ, decision_release TIMESTAMPTZ, - -- Event phase start_time TIMESTAMPTZ NOT NULL, @@ -22,6 +21,7 @@ CREATE TABLE events ( -- Metadata website_url TEXT, is_published BOOLEAN DEFAULT FALSE, + saved_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() diff --git a/apps/api/internal/db/queries/applications.sql b/apps/api/internal/db/queries/applications.sql new file mode 100644 index 00000000..2e553de7 --- /dev/null +++ b/apps/api/internal/db/queries/applications.sql @@ -0,0 +1,24 @@ +-- name: CreateApplication :one +INSERT INTO applications ( + user_id, event_id +) VALUES ( + $1, $2 +) +RETURNING *; + +-- name: GetApplicationByUserAndEventID :one +SELECT * FROM applications +WHERE user_id = $1 AND event_id = $2; + +-- name: UpdateApplication :exec +UPDATE applications +SET + status = CASE WHEN @status_do_update::boolean THEN @status::application_status ELSE status END, + application = CASE WHEN @application_do_update::boolean THEN @application::JSONB ELSE application END, + resume_url = CASE WHEN @resume_url_do_update::boolean THEN @resume_url ELSE resume_url END +WHERE + user_id = @user_id AND event_id = @event_id; + +-- name: DeleteApplication :exec +DELETE FROM applications +WHERE user_id = $1 AND event_id = $2; diff --git a/apps/api/internal/db/queries/events.sql b/apps/api/internal/db/queries/events.sql new file mode 100644 index 00000000..55c57897 --- /dev/null +++ b/apps/api/internal/db/queries/events.sql @@ -0,0 +1,39 @@ +-- name: CreateEvent :one +INSERT INTO events ( + name, + application_open, application_close, + start_time, end_time +) VALUES ( + $1, + $2, $3, + $4, $5 +) +RETURNING *; + +-- name: GetEventByID :one +SELECT * FROM events +WHERE id = $1; + +-- name: UpdateEventById :exec +UPDATE events +SET + name = coalesce(sqlc.narg('name'), name), + description = coalesce(sqlc.narg('description'), description), + location = coalesce(sqlc.narg('location'), location), + location_url = coalesce(sqlc.narg('location_url'), location_url), + max_attendees = coalesce(sqlc.narg('max_attendees'), max_attendees), + application_open = coalesce(sqlc.narg('application_open'), application_open), + application_close = coalesce(sqlc.narg('application_close'), application_close), + rsvp_deadline = coalesce(sqlc.narg('rsvp_deadline'), rsvp_deadline), + decision_release = coalesce(sqlc.narg('decision_release'), decision_release), + start_time = coalesce(sqlc.narg('start_time'), start_time), + end_time = coalesce(sqlc.narg('end_time'), end_time), + website_url = coalesce(sqlc.narg('website_url'), website_url), + is_published = coalesce(sqlc.narg('is_published'), is_published), + saved_at = coalesce(sqlc.narg('saved_at'), is_published) +WHERE + id = @id::uuid; + +-- name: DeleteEvent :exec +DELETE FROM events +WHERE id = $1; diff --git a/apps/api/internal/db/sqlc/applications.sql.go b/apps/api/internal/db/sqlc/applications.sql.go new file mode 100644 index 00000000..f8d9a23b --- /dev/null +++ b/apps/api/internal/db/sqlc/applications.sql.go @@ -0,0 +1,118 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: applications.sql + +package sqlc + +import ( + "context" + + "github.com/google/uuid" +) + +const createApplication = `-- name: CreateApplication :one +INSERT INTO applications ( + user_id, event_id +) VALUES ( + $1, $2 +) +RETURNING user_id, event_id, status, application, resume_url, created_at, saved_at, updated_at +` + +type CreateApplicationParams struct { + UserID uuid.UUID `json:"user_id"` + EventID uuid.UUID `json:"event_id"` +} + +func (q *Queries) CreateApplication(ctx context.Context, arg CreateApplicationParams) (Application, error) { + row := q.db.QueryRow(ctx, createApplication, arg.UserID, arg.EventID) + var i Application + err := row.Scan( + &i.UserID, + &i.EventID, + &i.Status, + &i.Application, + &i.ResumeUrl, + &i.CreatedAt, + &i.SavedAt, + &i.UpdatedAt, + ) + return i, err +} + +const deleteApplication = `-- name: DeleteApplication :exec +DELETE FROM applications +WHERE user_id = $1 AND event_id = $2 +` + +type DeleteApplicationParams struct { + UserID uuid.UUID `json:"user_id"` + EventID uuid.UUID `json:"event_id"` +} + +func (q *Queries) DeleteApplication(ctx context.Context, arg DeleteApplicationParams) error { + _, err := q.db.Exec(ctx, deleteApplication, arg.UserID, arg.EventID) + return err +} + +const getApplicationByUserAndEventID = `-- name: GetApplicationByUserAndEventID :one +SELECT user_id, event_id, status, application, resume_url, created_at, saved_at, updated_at FROM applications +WHERE user_id = $1 AND event_id = $2 +` + +type GetApplicationByUserAndEventIDParams struct { + UserID uuid.UUID `json:"user_id"` + EventID uuid.UUID `json:"event_id"` +} + +func (q *Queries) GetApplicationByUserAndEventID(ctx context.Context, arg GetApplicationByUserAndEventIDParams) (Application, error) { + row := q.db.QueryRow(ctx, getApplicationByUserAndEventID, arg.UserID, arg.EventID) + var i Application + err := row.Scan( + &i.UserID, + &i.EventID, + &i.Status, + &i.Application, + &i.ResumeUrl, + &i.CreatedAt, + &i.SavedAt, + &i.UpdatedAt, + ) + return i, err +} + +const updateApplication = `-- name: UpdateApplication :exec +UPDATE applications +SET + status = CASE WHEN $1::boolean THEN $2::application_status ELSE status END, + application = CASE WHEN $3::boolean THEN $4::JSONB ELSE application END, + resume_url = CASE WHEN $5::boolean THEN $6 ELSE resume_url END +WHERE + user_id = $7 AND event_id = $8 +` + +type UpdateApplicationParams struct { + StatusDoUpdate bool `json:"status_do_update"` + Status ApplicationStatus `json:"status"` + ApplicationDoUpdate bool `json:"application_do_update"` + Application []byte `json:"application"` + ResumeUrlDoUpdate bool `json:"resume_url_do_update"` + ResumeUrl *string `json:"resume_url"` + UserID uuid.UUID `json:"user_id"` + EventID uuid.UUID `json:"event_id"` +} + +func (q *Queries) UpdateApplication(ctx context.Context, arg UpdateApplicationParams) error { + _, err := q.db.Exec(ctx, updateApplication, + arg.StatusDoUpdate, + arg.Status, + arg.ApplicationDoUpdate, + arg.Application, + arg.ResumeUrlDoUpdate, + arg.ResumeUrl, + arg.UserID, + arg.EventID, + ) + return err +} diff --git a/apps/api/internal/db/sqlc/events.sql.go b/apps/api/internal/db/sqlc/events.sql.go new file mode 100644 index 00000000..25b8de7a --- /dev/null +++ b/apps/api/internal/db/sqlc/events.sql.go @@ -0,0 +1,165 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.29.0 +// source: events.sql + +package sqlc + +import ( + "context" + "time" + + "github.com/google/uuid" +) + +const createEvent = `-- name: CreateEvent :one +INSERT INTO events ( + name, + application_open, application_close, + start_time, end_time +) VALUES ( + $1, + $2, $3, + $4, $5 +) +RETURNING id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url, is_published, saved_at, created_at, updated_at +` + +type CreateEventParams struct { + Name string `json:"name"` + ApplicationOpen time.Time `json:"application_open"` + ApplicationClose time.Time `json:"application_close"` + StartTime time.Time `json:"start_time"` + EndTime time.Time `json:"end_time"` +} + +func (q *Queries) CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) { + row := q.db.QueryRow(ctx, createEvent, + arg.Name, + arg.ApplicationOpen, + arg.ApplicationClose, + arg.StartTime, + arg.EndTime, + ) + var i Event + err := row.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.Location, + &i.LocationUrl, + &i.MaxAttendees, + &i.ApplicationOpen, + &i.ApplicationClose, + &i.RsvpDeadline, + &i.DecisionRelease, + &i.StartTime, + &i.EndTime, + &i.WebsiteUrl, + &i.IsPublished, + &i.SavedAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const deleteEvent = `-- name: DeleteEvent :exec +DELETE FROM events +WHERE id = $1 +` + +func (q *Queries) DeleteEvent(ctx context.Context, id uuid.UUID) error { + _, err := q.db.Exec(ctx, deleteEvent, id) + return err +} + +const getEventByID = `-- name: GetEventByID :one +SELECT id, name, description, location, location_url, max_attendees, application_open, application_close, rsvp_deadline, decision_release, start_time, end_time, website_url, is_published, saved_at, created_at, updated_at FROM events +WHERE id = $1 +` + +func (q *Queries) GetEventByID(ctx context.Context, id uuid.UUID) (Event, error) { + row := q.db.QueryRow(ctx, getEventByID, id) + var i Event + err := row.Scan( + &i.ID, + &i.Name, + &i.Description, + &i.Location, + &i.LocationUrl, + &i.MaxAttendees, + &i.ApplicationOpen, + &i.ApplicationClose, + &i.RsvpDeadline, + &i.DecisionRelease, + &i.StartTime, + &i.EndTime, + &i.WebsiteUrl, + &i.IsPublished, + &i.SavedAt, + &i.CreatedAt, + &i.UpdatedAt, + ) + return i, err +} + +const updateEventById = `-- name: UpdateEventById :exec +UPDATE events +SET + name = coalesce($1, name), + description = coalesce($2, description), + location = coalesce($3, location), + location_url = coalesce($4, location_url), + max_attendees = coalesce($5, max_attendees), + application_open = coalesce($6, application_open), + application_close = coalesce($7, application_close), + rsvp_deadline = coalesce($8, rsvp_deadline), + decision_release = coalesce($9, decision_release), + start_time = coalesce($10, start_time), + end_time = coalesce($11, end_time), + website_url = coalesce($12, website_url), + is_published = coalesce($13, is_published), + saved_at = coalesce($14, is_published) +WHERE + id = $15::uuid +` + +type UpdateEventByIdParams struct { + Name *string `json:"name"` + Description *string `json:"description"` + Location *string `json:"location"` + LocationUrl *string `json:"location_url"` + MaxAttendees *int32 `json:"max_attendees"` + ApplicationOpen *time.Time `json:"application_open"` + ApplicationClose *time.Time `json:"application_close"` + RsvpDeadline *time.Time `json:"rsvp_deadline"` + DecisionRelease *time.Time `json:"decision_release"` + StartTime *time.Time `json:"start_time"` + EndTime *time.Time `json:"end_time"` + WebsiteUrl *string `json:"website_url"` + IsPublished *bool `json:"is_published"` + SavedAt *time.Time `json:"saved_at"` + ID uuid.UUID `json:"id"` +} + +func (q *Queries) UpdateEventById(ctx context.Context, arg UpdateEventByIdParams) error { + _, err := q.db.Exec(ctx, updateEventById, + arg.Name, + arg.Description, + arg.Location, + arg.LocationUrl, + arg.MaxAttendees, + arg.ApplicationOpen, + arg.ApplicationClose, + arg.RsvpDeadline, + arg.DecisionRelease, + arg.StartTime, + arg.EndTime, + arg.WebsiteUrl, + arg.IsPublished, + arg.SavedAt, + arg.ID, + ) + return err +} diff --git a/apps/api/internal/db/sqlc/models.go b/apps/api/internal/db/sqlc/models.go index 159ac243..2ca6d713 100644 --- a/apps/api/internal/db/sqlc/models.go +++ b/apps/api/internal/db/sqlc/models.go @@ -210,6 +210,7 @@ type Event struct { EndTime time.Time `json:"end_time"` WebsiteUrl *string `json:"website_url"` IsPublished *bool `json:"is_published"` + SavedAt *time.Time `json:"saved_at"` CreatedAt *time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at"` } diff --git a/apps/api/internal/db/sqlc/querier.go b/apps/api/internal/db/sqlc/querier.go index f07c2601..65bbba66 100644 --- a/apps/api/internal/db/sqlc/querier.go +++ b/apps/api/internal/db/sqlc/querier.go @@ -16,20 +16,28 @@ type Querier interface { // Returns the newly created email record. AddEmail(ctx context.Context, arg AddEmailParams) (EventInterestSubmission, error) CreateAccount(ctx context.Context, arg CreateAccountParams) (AuthAccount, error) + CreateApplication(ctx context.Context, arg CreateApplicationParams) (Application, error) + CreateEvent(ctx context.Context, arg CreateEventParams) (Event, error) CreateSession(ctx context.Context, arg CreateSessionParams) (AuthSession, error) CreateUser(ctx context.Context, arg CreateUserParams) (AuthUser, error) DeleteAccount(ctx context.Context, arg DeleteAccountParams) error + DeleteApplication(ctx context.Context, arg DeleteApplicationParams) error + DeleteEvent(ctx context.Context, id uuid.UUID) error DeleteExpiredSession(ctx context.Context) error DeleteUser(ctx context.Context, id uuid.UUID) error GetActiveSessionUserInfo(ctx context.Context, id uuid.UUID) (GetActiveSessionUserInfoRow, error) + GetApplicationByUserAndEventID(ctx context.Context, arg GetApplicationByUserAndEventIDParams) (Application, error) GetByProviderAndAccountID(ctx context.Context, arg GetByProviderAndAccountIDParams) (AuthAccount, error) GetByUserID(ctx context.Context, userID uuid.UUID) ([]AuthAccount, error) + GetEventByID(ctx context.Context, id uuid.UUID) (Event, error) GetSessionByID(ctx context.Context, id uuid.UUID) (AuthSession, error) GetSessionsByUserID(ctx context.Context, userID uuid.UUID) ([]AuthSession, error) GetUserByEmail(ctx context.Context, email *string) (AuthUser, error) GetUserByID(ctx context.Context, id uuid.UUID) (AuthUser, error) InvalidateSessionByID(ctx context.Context, id uuid.UUID) error TouchSession(ctx context.Context, arg TouchSessionParams) error + UpdateApplication(ctx context.Context, arg UpdateApplicationParams) error + UpdateEventById(ctx context.Context, arg UpdateEventByIdParams) error UpdateSessionExpiration(ctx context.Context, arg UpdateSessionExpirationParams) error UpdateTokens(ctx context.Context, arg UpdateTokensParams) error UpdateUser(ctx context.Context, arg UpdateUserParams) error diff --git a/apps/docs/mkdocs.yml b/apps/docs/mkdocs.yml index 21a8abd8..40af77ab 100644 --- a/apps/docs/mkdocs.yml +++ b/apps/docs/mkdocs.yml @@ -50,6 +50,7 @@ nav: - Overview: api/index.md - Installation: api/installation.md - OpenAPI: 'https://core.apidocumentation.com/guide/swamphacks-core-api' + - Database testing: 'api/db_testing.md' - Discord Bot: - Overview: discord-bot/index.md - Installation: discord-bot/installation.md diff --git a/apps/docs/src/api/db_testing.md b/apps/docs/src/api/db_testing.md new file mode 100644 index 00000000..6a0d5f19 --- /dev/null +++ b/apps/docs/src/api/db_testing.md @@ -0,0 +1,30 @@ +# Database testing + +## Manual + +1. Make sure the docker instance is currently running (i.e. you ran `$ docker compose up`) +!!! note + + You may have to run docker with sudo depending on your system configuration. + +1. In a seperate terminal (I suggest you try using tmux) list the current docker processes +``` bash +docker ps +``` + +1. Copy the process id for the docker container running postgres, and paste into the following command in order to spawn a shell with access to the container. +``` bash +sudo docker exec -it ef7XXXXXX07e sh +``` + +1. Connect to the postgres database using a database url. The url can be found inside `core/apps/api/.env.example`. +``` bash +psql postgres://postgres:postgres@postgres:5432/coredb +``` + +1. Check to see if database tables currently exist. You can by listing the currently created tables. +``` bash +\dt +``` +If there is nothing here, then go into `/core/apps/api` and run `make migrate`, which runs sql commands added to [migrations](https://en.wikipedia.org/wiki/Schema_migration) in `core/apps/api/internal/db/migrations`. +1. You can now test to see if rows, columns and tables are updated appropriately with psql commands. Use a [reference to psql](https://www.postgresql.org/docs/17/app-psql.html) if you need help finding commands. diff --git a/apps/docs/src/api/installation.md b/apps/docs/src/api/installation.md index 0283f2ea..d6840531 100644 --- a/apps/docs/src/api/installation.md +++ b/apps/docs/src/api/installation.md @@ -3,23 +3,25 @@ ### Setup with Docker Compose (main setup) 1. Navigate to `core/apps/api` -2. **Set up environment variables**: +1. **Set up environment variables**: ``` bash cp .env.dev.example .env.dev ``` -3. Open `.env.dev` +1. Open `.env.dev` 1. For `AUTH_DISCORD_CLIENT_ID` and `AUTH_DISCORD_CLIENT_SECRET`, go to the Discord developer portal and create an account. Create a new application and go to the OAuth2 tab in the left sidebar. Copy the Client ID and the Client Secret into their respective environment variables. - 2. Fill out any other required keys and tokens, if empty. + 1. While in the OAuth2 menu, copy the `AUTH_DISCORD_REDIRECT_URI` parameter from the example configuration and paste into the box under the *Redirects* header. This is the URL which discord will redirect the user to after Discord authentication has completed. -3. Continue with the [main setup instructions](../getting-started.md) + 1. Fill out any other required keys and tokens, if empty. + +1. Continue with the [main setup instructions](../getting-started.md) ### Setup without Docker Compose 1. Make sure you have [Go](https://go.dev/) installed on your system. -2. Initialize the Go project +1. Initialize the Go project ``` bash go mod tidy ``` @@ -29,7 +31,7 @@ go install github.com/sqlc-dev/sqlc/cmd/sqlc@latest go install github.com/pressly/goose/v3/cmd/goose@latest ``` -3. Run the program with +1. Run the program with ```bash air ``` diff --git a/docker-compose.yml b/docker-compose.yml index 292d7adb..d7bb5bc8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -17,6 +17,7 @@ services: dockerfile: Dockerfile.dev volumes: - ./apps/web:/app + - web_node_modules:/app/node_modules ports: - "5173:5173" environment: @@ -46,5 +47,6 @@ services: volumes: postgres_data: + web_node_modules: