Skip to content

Commit 28e7627

Browse files
Merge branch 'develop' into develop
2 parents d34f75f + d967451 commit 28e7627

File tree

24 files changed

+1814
-337
lines changed

24 files changed

+1814
-337
lines changed

.github/workflows/go.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ jobs:
2525
- name: Set up Go
2626
uses: actions/setup-go@v4
2727
with:
28-
go-version: '1.23'
28+
go-version: '1.24'
2929
cache-dependency-path: "**/go.sum"
3030

3131
- name: Setup
@@ -50,7 +50,7 @@ jobs:
5050
- name: Set up Go
5151
uses: actions/setup-go@v4
5252
with:
53-
go-version: '1.23'
53+
go-version: '1.24'
5454
cache-dependency-path: "**\\go.sum"
5555

5656
- name: Check & Build

README.md

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,72 @@
1+
12
# Nebula API
23

34
_A database for some really useful UTD data collected by our [tools](https://github.com/UTDNebula/api-tools)._
45

56
Project maintained by [Nebula Labs](https://about.utdnebula.com).
67

7-
### Contributing
8-
9-
Please visit our [Discord](https://discord.utdnebula.com) and talk to us if you'd like to contribute!
10-
11-
### Prerequisites
12-
13-
- Golang 1.23 (or higher)
14-
15-
### Documentation
8+
## Documentation
169

1710
Documentation for the current production API can be found [here.](https://api.utdnebula.com/swagger/index.html)
1811

19-
### How to use
12+
## How to use
2013

2114
- Visit our [Discord](https://discord.utdnebula.com) and ask to be provisioned an API key (please provide details on your use case)
2215
- Read the documentation listed above (and authenticate with your key for interactive demos)
2316
- Make requests to `https://api.utdnebula.com` with your provisioned api key set as the `x-api-key` request header
2417
- **Build cool stuff!**
18+
19+
## Contributing
20+
Contributions are welcome!
21+
22+
This project uses the MIT License.
23+
24+
Please visit our [Discord](https://discord.utdnebula.com) and talk to us if you'd like to contribute!
25+
### How to Contribute
26+
27+
Create your own fork by [forking this repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo#forking-a-repository)
28+
29+
[Clone](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo#cloning-your-forked-repository) your forked repository. (Don't forget to install Git if you haven't already)
30+
31+
Submit proposed changes via a [Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request)
32+
33+
## Building
34+
### Requirements
35+
- [Golang 1.23 or Higher](https://go.dev/dl/)
36+
37+
### Building for Windows
38+
cd into `nebula-api\api`
39+
40+
Setup Go Dependencies with
41+
`.\build.bat setup`
42+
43+
Build with
44+
`.\build.bat build`
45+
46+
Run with
47+
`.\go-api.exe`
48+
> Note: some have experienced issues with Windows Defender or other antivirus blocking `go-api.exe` from reading files, editing files, or causing slowed performance. Consider adding a exception to your `nebula-api` folder.
49+
50+
### Building for macOs, Linux, and WSL
51+
cd into `nebula-api/api`
52+
53+
Setup Go dependencies with
54+
`make setup`
55+
56+
Build with
57+
`make build`
58+
59+
Run with
60+
`./go-api`
61+
62+
## Running to API locally
63+
Copy `.env.template` to `.env` with
64+
`cp .env.template .env`
65+
66+
Enter Nebula MongoDB URI in `.env`
67+
68+
Run `go-api`
69+
70+
Check command output to see the route serving traffic. It's likely port 8080
71+
72+
Visit `http://localhost:8080` to access nebula-api locally

api/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# syntax=docker/dockerfile:1
22

3-
FROM docker.io/golang:1.23 AS builder
3+
FROM docker.io/golang:1.24 AS builder
44
WORKDIR /build
55

66
COPY ./configs ./configs

api/controllers/astra.go

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package controllers
22

33
import (
44
"context"
5+
"errors"
56
"net/http"
67
"time"
78

@@ -19,6 +20,7 @@ var astraCollection *mongo.Collection = configs.GetCollection("astra")
1920

2021
// @Id AstraEvents
2122
// @Router /astra/{date} [get]
23+
// @Tags Events
2224
// @Description "Returns AstraEvent based on the input date"
2325
// @Produce json
2426
// @Param date path string true "date (ISO format) to retrieve astra events"
@@ -41,3 +43,114 @@ func AstraEvents(c *gin.Context) {
4143

4244
respond(c, http.StatusOK, "success", astra_events)
4345
}
46+
47+
// @Id AstraEventsByBuilding
48+
// @Router /astra/{date}/{building} [get]
49+
// @Tags Events
50+
// @Description "Returns AstraEvent based on the input date and building name"
51+
// @Produce json
52+
// @Param date path string true "date (ISO format) to retrieve astra events"
53+
// @Param building path string true "building abbreviation of event locations"
54+
// @Success 200 {object} schema.APIResponse[schema.SingleBuildingEvents[schema.AstraEvent]] "All sections with meetings on the specified date in the specified building"
55+
// @Failure 500 {object} schema.APIResponse[string] "A string describing the error"
56+
// @Failure 404 {object} schema.APIResponse[string] "A string describing the error"
57+
func AstraEventsByBuilding(c *gin.Context) {
58+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
59+
defer cancel()
60+
61+
date := c.Param("date")
62+
building := c.Param("building")
63+
64+
var astra_events schema.MultiBuildingEvents[schema.AstraEvent]
65+
var astra_eventsByBuilding schema.SingleBuildingEvents[schema.AstraEvent]
66+
67+
// Find astra event given date
68+
err := astraCollection.FindOne(ctx, bson.M{"date": date}).Decode(&astra_events)
69+
if err != nil {
70+
if errors.Is(err, mongo.ErrNoDocuments) {
71+
astra_events.Date = date
72+
astra_events.Buildings = []schema.SingleBuildingEvents[schema.AstraEvent]{}
73+
} else {
74+
respondWithInternalError(c, err)
75+
return
76+
}
77+
}
78+
79+
//parse response for requested building
80+
for _, b := range astra_events.Buildings {
81+
if b.Building == building {
82+
astra_eventsByBuilding = b
83+
break
84+
}
85+
}
86+
87+
if astra_eventsByBuilding.Building == "" {
88+
c.JSON(http.StatusNotFound, schema.APIResponse[string]{
89+
Status: http.StatusNotFound,
90+
Message: "error",
91+
Data: "No events found for the specified building",
92+
})
93+
return
94+
}
95+
96+
respond(c, http.StatusOK, "success", astra_eventsByBuilding)
97+
}
98+
99+
// @Id AstraEventsByBuildingandRoom
100+
// @Router /astra/{date}/{building}/{room} [get]
101+
// @Tags Events
102+
// @Description "Returns AstraEvent based on the input date building name and room number"
103+
// @Produce json
104+
// @Param date path string true "date (ISO format) to retrieve astra events"
105+
// @Param building path string true "building abbreviation of event locations"
106+
// @Param room path string true "room number for event"
107+
// @Success 200 {object} schema.APIResponse[schema.SingleBuildingEvents[schema.AstraEvent]] "All sections with meetings on the specified date in the specified building"
108+
// @Failure 500 {object} schema.APIResponse[string] "A string describing the error"
109+
// @Failure 404 {object} schema.APIResponse[string] "A string describing the error"
110+
func AstraEventsByBuildingAndRoom(c *gin.Context) {
111+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
112+
defer cancel()
113+
114+
date := c.Param("date")
115+
building := c.Param("building")
116+
room := c.Param("room")
117+
118+
var astra_events schema.MultiBuildingEvents[schema.AstraEvent]
119+
var roomEvents schema.RoomEvents[schema.AstraEvent]
120+
121+
// Find astra event given date
122+
err := astraCollection.FindOne(ctx, bson.M{"date": date}).Decode(&astra_events)
123+
if err != nil {
124+
if errors.Is(err, mongo.ErrNoDocuments) {
125+
astra_events.Date = date
126+
astra_events.Buildings = []schema.SingleBuildingEvents[schema.AstraEvent]{}
127+
} else {
128+
respondWithInternalError(c, err)
129+
return
130+
}
131+
}
132+
133+
//parse response for requested building and room
134+
for _, b := range astra_events.Buildings {
135+
if b.Building == building {
136+
for _, r := range b.Rooms {
137+
if r.Room == room {
138+
roomEvents = r
139+
break
140+
}
141+
}
142+
break
143+
}
144+
}
145+
146+
if roomEvents.Room == "" {
147+
c.JSON(http.StatusNotFound, schema.APIResponse[string]{
148+
Status: http.StatusNotFound,
149+
Message: "error",
150+
Data: "No rooms found for the specified building or event",
151+
})
152+
return
153+
}
154+
155+
respond(c, http.StatusOK, "success", roomEvents)
156+
}

api/controllers/autocomplete.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ var DAGCollection *mongo.Collection = configs.GetCollection("DAG")
1919

2020
// @Id autocompleteDAG
2121
// @Router /autocomplete/dag [get]
22+
// @Tags Other
2223
// @Description "Returns an aggregation of courses for use in generating autocomplete DAGs"
2324
// @Produce json
2425
// @Success 200 {object} schema.APIResponse[[]schema.Autocomplete] "An aggregation of courses for use in generating autocomplete DAGs"

api/controllers/controller_utils.go

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,52 @@ import (
99
"github.com/getsentry/sentry-go"
1010
sentrygin "github.com/getsentry/sentry-go/gin"
1111
"github.com/gin-gonic/gin"
12+
"go.mongodb.org/mongo-driver/bson"
1213
"go.mongodb.org/mongo-driver/bson/primitive"
1314
)
1415

1516
// Sets the API's response to a request, producing valid JSON given a status code and data.
1617
func respond[T any](c *gin.Context, status int, message string, data T) {
17-
c.JSON(status, schema.APIResponse[T]{Status: status, Message: message, Data: data})
18+
c.JSON(
19+
status,
20+
schema.APIResponse[T]{
21+
Status: status,
22+
Message: message,
23+
Data: data,
24+
},
25+
)
26+
}
27+
28+
// Builds a MongoDB filter for type T based on the given flag search or byid
29+
func getQuery[T any](flag string, c *gin.Context) (bson.M, error) {
30+
switch flag {
31+
case "Search":
32+
query, err := schema.FilterQuery[T](c)
33+
if err != nil {
34+
respond(c, http.StatusBadRequest, "Invalid query parameters", err.Error())
35+
return nil, err
36+
}
37+
return query, nil
38+
39+
case "ById":
40+
objId, err := objectIDFromParam(c, "id")
41+
if err != nil {
42+
// objectIDFromParam already responds with 400 if conversion fails
43+
return nil, err
44+
}
45+
return bson.M{"_id": objId}, nil
46+
47+
default:
48+
err := fmt.Errorf("invalid flag for getQuery: %s", flag)
49+
respondWithInternalError(c, err)
50+
return nil, err
51+
}
1852
}
1953

2054
// Helper function for logging and responding to a generic internal server error.
2155
func respondWithInternalError(c *gin.Context, err error) {
22-
// Note that we use log.Output here to be able to set the stack depth to the frame above this one (2), which allows us to log the location this function was called from
56+
// Note that we use log.Output here to be able to set the stack depth to the frame above this one (2),
57+
// which allows us to log the location this function was called from
2358
log.Output(2, fmt.Sprintf("INTERNAL SERVER ERROR: %s", err.Error()))
2459
// Capture error with Sentry
2560
if hub := sentrygin.GetHubFromContext(c); hub != nil {
@@ -30,14 +65,19 @@ func respondWithInternalError(c *gin.Context, err error) {
3065
respond(c, http.StatusInternalServerError, "error", err.Error())
3166
}
3267

33-
// Attempts to convert the given parameter to an ObjectID for use with MongoDB. Automatically responds with http.StatusBadRequest if conversion fails.
68+
// Attempts to convert the given parameter to an ObjectID for use with MongoDB.
69+
// Automatically responds with http.StatusBadRequest if conversion fails.
3470
func objectIDFromParam(c *gin.Context, paramName string) (*primitive.ObjectID, error) {
3571
idHex := c.Param(paramName)
3672
objectId, convertIdErr := primitive.ObjectIDFromHex(idHex)
3773
if convertIdErr != nil {
3874
// Respond with an error if we can't covert successfully
3975
log.Println(convertIdErr)
40-
respond(c, http.StatusBadRequest, fmt.Sprintf("Parameter \"%s\" is not a valid ObjectID.", paramName), convertIdErr.Error())
76+
respond(c,
77+
http.StatusBadRequest,
78+
fmt.Sprintf("Parameter \"%s\" is not a valid ObjectID.", paramName),
79+
convertIdErr.Error(),
80+
)
4181
return nil, convertIdErr
4282
}
4383
return &objectId, nil

0 commit comments

Comments
 (0)