Enable GraphQL! #1

Merged
evan merged 8 commits from graphql into master 2021-02-09 00:44:20 +00:00
12 changed files with 727 additions and 604 deletions
Showing only changes of commit af237110f9 - Show all commits

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,61 @@
package graph package graph
import ( import (
"context"
"errors"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/dsoprea/go-exif/v3" "github.com/dsoprea/go-exif/v3"
exifcommon "github.com/dsoprea/go-exif/v3/common" exifcommon "github.com/dsoprea/go-exif/v3/common"
"github.com/google/uuid"
"reichard.io/imagini/graph/model" "reichard.io/imagini/graph/model"
) )
func getContextHTTP(ctx context.Context) (*http.ResponseWriter, *http.Request, error) {
authContext := ctx.Value("auth").(*model.AuthContext)
resp := authContext.AuthResponse
if resp == nil {
return nil, nil, errors.New("Context Error")
}
req := authContext.AuthRequest
if resp == nil {
return nil, nil, errors.New("Context Error")
}
return resp, req, nil
}
func getContextIDs(ctx context.Context) (string, string, error) {
authContext := ctx.Value("auth").(*model.AuthContext)
accessToken := *authContext.AccessToken
uid, ok := accessToken.Get("sub")
if !ok {
return "", "", errors.New("Context Error")
}
did, ok := accessToken.Get("sub")
if !ok {
return "", "", errors.New("Context Error")
}
userID, err := uuid.Parse(uid.(string))
if err != nil {
return "", "", errors.New("Context Error")
}
deviceID, err := uuid.Parse(did.(string))
if err != nil {
return "", "", errors.New("Context Error")
}
return userID.String(), deviceID.String(), nil
}
func deriveDeviceType(r *http.Request) model.DeviceType { func deriveDeviceType(r *http.Request) model.DeviceType {
userAgent := strings.ToLower(r.Header.Get("User-Agent")) userAgent := strings.ToLower(r.Header.Get("User-Agent"))
if strings.Contains(userAgent, "ios-imagini") { if strings.Contains(userAgent, "ios-imagini") {

View File

@ -27,8 +27,8 @@ type AlbumFilter struct {
} }
type AlbumResponse struct { type AlbumResponse struct {
Data []*Album `json:"data" ` Data []*Album `json:"data" `
PageInfo *PageInfo `json:"pageInfo" ` Page *PageResponse `json:"page" `
} }
type AuthResponse struct { type AuthResponse struct {
@ -66,8 +66,8 @@ type DeviceFilter struct {
} }
type DeviceResponse struct { type DeviceResponse struct {
Data []*Device `json:"data" ` Data []*Device `json:"data" `
PageInfo *PageInfo `json:"pageInfo" ` Page *PageResponse `json:"page" `
} }
type DeviceTypeFilter struct { type DeviceTypeFilter struct {
@ -127,8 +127,8 @@ type MediaItemFilter struct {
} }
type MediaItemResponse struct { type MediaItemResponse struct {
Data []*MediaItem `json:"data" ` Data []*MediaItem `json:"data" `
PageInfo *PageInfo `json:"pageInfo" ` Page *PageResponse `json:"page" `
} }
type NewAlbum struct { type NewAlbum struct {
@ -155,8 +155,18 @@ type NewUser struct {
Password *string `json:"password" ` Password *string `json:"password" `
} }
type PageInfo struct { type Order struct {
Count int `json:"count" ` By *string `json:"by" `
Direction *OrderDirection `json:"direction" `
}
type Page struct {
Size *int `json:"size" `
Page *int `json:"page" `
}
type PageResponse struct {
Size int `json:"size" `
Page int `json:"page" ` Page int `json:"page" `
Total int `json:"total" ` Total int `json:"total" `
} }
@ -193,8 +203,8 @@ type TagFilter struct {
} }
type TagResponse struct { type TagResponse struct {
Data []*Tag `json:"data" ` Data []*Tag `json:"data" `
PageInfo *PageInfo `json:"pageInfo" ` Page *PageResponse `json:"page" `
} }
type TimeFilter struct { type TimeFilter struct {
@ -233,8 +243,8 @@ type UserFilter struct {
} }
type UserResponse struct { type UserResponse struct {
Data []*User `json:"data" ` Data []*User `json:"data" `
PageInfo *PageInfo `json:"pageInfo" ` Page *PageResponse `json:"page" `
} }
type AuthResult string type AuthResult string
@ -372,6 +382,47 @@ func (e DeviceType) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String())) fmt.Fprint(w, strconv.Quote(e.String()))
} }
type OrderDirection string
const (
OrderDirectionAsc OrderDirection = "ASC"
OrderDirectionDesc OrderDirection = "DESC"
)
var AllOrderDirection = []OrderDirection{
OrderDirectionAsc,
OrderDirectionDesc,
}
func (e OrderDirection) IsValid() bool {
switch e {
case OrderDirectionAsc, OrderDirectionDesc:
return true
}
return false
}
func (e OrderDirection) String() string {
return string(e)
}
func (e *OrderDirection) UnmarshalGQL(v interface{}) error {
str, ok := v.(string)
if !ok {
return fmt.Errorf("enums must be strings")
}
*e = OrderDirection(str)
if !e.IsValid() {
return fmt.Errorf("%s is not a valid OrderDirection", str)
}
return nil
}
func (e OrderDirection) MarshalGQL(w io.Writer) {
fmt.Fprint(w, strconv.Quote(e.String()))
}
type Role string type Role string
const ( const (

View File

@ -32,6 +32,11 @@ enum AuthType {
LDAP LDAP
} }
enum OrderDirection {
ASC
DESC
}
# ------------------------------------------------------------ # ------------------------------------------------------------
# ---------------------- Authentication ---------------------- # ---------------------- Authentication ----------------------
# ------------------------------------------------------------ # ------------------------------------------------------------
@ -267,39 +272,49 @@ input NewAlbum {
name: String! name: String!
} }
input Page {
size: Int
page: Int
}
input Order {
by: String
direction: OrderDirection
}
# ------------------------------------------------------------ # ------------------------------------------------------------
# ------------------------ Responses ------------------------- # ------------------------ Responses -------------------------
# ------------------------------------------------------------ # ------------------------------------------------------------
type PageInfo { type PageResponse {
count: Int! size: Int!
page: Int! page: Int!
total: Int! total: Int!
} }
type MediaItemResponse { type MediaItemResponse {
data: [MediaItem] data: [MediaItem]
pageInfo: PageInfo! page: PageResponse!
} }
type UserResponse { type UserResponse {
data: [User] data: [User]
pageInfo: PageInfo! page: PageResponse!
} }
type DeviceResponse { type DeviceResponse {
data: [Device] data: [Device]
pageInfo: PageInfo! page: PageResponse!
} }
type TagResponse { type TagResponse {
data: [Tag] data: [Tag]
pageInfo: PageInfo! page: PageResponse!
} }
type AlbumResponse { type AlbumResponse {
data: [Album] data: [Album]
pageInfo: PageInfo! page: PageResponse!
} }
# ------------------------------------------------------------ # ------------------------------------------------------------
@ -313,62 +328,52 @@ type Query {
password: String! password: String!
deviceID: ID deviceID: ID
): AuthResponse! ): AuthResponse!
logout: AuthResponse! @hasMinRole(role: User) logout: AuthResponse! @hasMinRole(role: User)
# Single Item # Single Item
mediaItem( mediaItem(
id: ID! id: ID!
delete: Boolean ): MediaItem! @hasMinRole(role: User)
): MediaItem! @hasMinRole(role: User)
device( device(
id: ID! id: ID!
delete: Boolean ): Device! @hasMinRole(role: User)
): Device! @hasMinRole(role: User)
album( album(
id: ID! id: ID!
delete: Boolean ): Album! @hasMinRole(role: User)
): Album! @hasMinRole(role: User)
user( user(
id: ID! id: ID!
delete: Boolean ): User! @hasMinRole(role: Admin)
): User! @hasMinRole(role: Admin) # TODO: Delete All User Content
tag( tag(
id: ID! id: ID!
delete: Boolean ): Tag! @hasMinRole(role: User)
): Tag! @hasMinRole(role: User) me: User! @hasMinRole(role: User)
me(delete: Boolean): User! @hasMinRole(role: User)
# All # All
mediaItems( mediaItems(
delete: Boolean
filter: MediaItemFilter filter: MediaItemFilter
count: Int page: Page
page: Int order: Order
): MediaItemResponse! @hasMinRole(role: User) ): MediaItemResponse! @hasMinRole(role: User)
devices( devices(
delete: Boolean
filter: DeviceFilter filter: DeviceFilter
count: Int page: Page
page: Int order: Order
): DeviceResponse! @hasMinRole(role: User) ): DeviceResponse! @hasMinRole(role: User)
albums( albums(
delete: Boolean
filter: AlbumFilter filter: AlbumFilter
count: Int page: Page
page: Int order: Order
): AlbumResponse! @hasMinRole(role: User) ): AlbumResponse! @hasMinRole(role: User)
tags( tags(
delete: Boolean
filter: TagFilter filter: TagFilter
count: Int page: Page
page: Int order: Order
): TagResponse! @hasMinRole(role: User) ): TagResponse! @hasMinRole(role: User)
users( users(
delete: Boolean
filter: UserFilter filter: UserFilter
count: Int page: Page
page: Int order: Order
): UserResponse! @hasMinRole(role: Admin) # TODO: Delete All User Content ): UserResponse! @hasMinRole(role: Admin)
} }
type Mutation { type Mutation {

View File

@ -21,12 +21,10 @@ import (
) )
func (r *mutationResolver) CreateMediaItem(ctx context.Context, input model.NewMediaItem) (*model.MediaItem, error) { func (r *mutationResolver) CreateMediaItem(ctx context.Context, input model.NewMediaItem) (*model.MediaItem, error) {
// Get Context // Acquire Context
authContext := ctx.Value("auth").(*model.AuthContext) userID, _, err := getContextIDs(ctx)
accessToken := *authContext.AccessToken if err != nil {
userID, ok := accessToken.Get("sub") return nil, err
if !ok {
return nil, errors.New("Upload Failed")
} }
// File header placeholder // File header placeholder
@ -52,7 +50,7 @@ func (r *mutationResolver) CreateMediaItem(ctx context.Context, input model.NewM
// Derive Folder & File Path // Derive Folder & File Path
mediaItemID := uuid.New().String() mediaItemID := uuid.New().String()
fileName := mediaItemID + fileMime.Extension() fileName := mediaItemID + fileMime.Extension()
folderPath := path.Join("/" + r.Config.DataPath + "/media/" + userID.(string)) folderPath := path.Join("/" + r.Config.DataPath + "/media/" + userID)
os.MkdirAll(folderPath, 0700) os.MkdirAll(folderPath, 0700)
filePath := path.Join(folderPath + "/" + fileName) filePath := path.Join(folderPath + "/" + fileName)
@ -83,7 +81,7 @@ func (r *mutationResolver) CreateMediaItem(ctx context.Context, input model.NewM
// Add Additional MediaItem Fields // Add Additional MediaItem Fields
mediaItem.ID = mediaItemID mediaItem.ID = mediaItemID
mediaItem.UserID = userID.(string) mediaItem.UserID = userID
mediaItem.IsVideo = isVideo mediaItem.IsVideo = isVideo
mediaItem.FileName = fileName mediaItem.FileName = fileName
mediaItem.OrigName = input.File.Filename mediaItem.OrigName = input.File.Filename
@ -99,20 +97,18 @@ func (r *mutationResolver) CreateMediaItem(ctx context.Context, input model.NewM
} }
func (r *mutationResolver) CreateAlbum(ctx context.Context, input model.NewAlbum) (*model.Album, error) { func (r *mutationResolver) CreateAlbum(ctx context.Context, input model.NewAlbum) (*model.Album, error) {
// Get Context // Acquire Context
authContext := ctx.Value("auth").(*model.AuthContext) userID, _, err := getContextIDs(ctx)
accessToken := *authContext.AccessToken if err != nil {
userID, ok := accessToken.Get("sub") return nil, err
if !ok {
return nil, errors.New("Upload Failed")
} }
album := &model.Album{ album := &model.Album{
Name: input.Name, Name: input.Name,
UserID: userID.(string), UserID: userID,
} }
err := r.DB.CreateAlbum(album) err = r.DB.CreateAlbum(album)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -121,20 +117,18 @@ func (r *mutationResolver) CreateAlbum(ctx context.Context, input model.NewAlbum
} }
func (r *mutationResolver) CreateTag(ctx context.Context, input model.NewTag) (*model.Tag, error) { func (r *mutationResolver) CreateTag(ctx context.Context, input model.NewTag) (*model.Tag, error) {
// Get Context // Acquire Context
authContext := ctx.Value("auth").(*model.AuthContext) userID, _, err := getContextIDs(ctx)
accessToken := *authContext.AccessToken if err != nil {
userID, ok := accessToken.Get("sub") return nil, err
if !ok {
return nil, errors.New("Upload Failed")
} }
tag := &model.Tag{ tag := &model.Tag{
Name: input.Name, Name: input.Name,
UserID: userID.(string), UserID: userID,
} }
err := r.DB.CreateTag(tag) err = r.DB.CreateTag(tag)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -162,10 +156,11 @@ func (r *mutationResolver) CreateUser(ctx context.Context, input model.NewUser)
} }
func (r *queryResolver) Login(ctx context.Context, user string, password string, deviceID *string) (*model.AuthResponse, error) { func (r *queryResolver) Login(ctx context.Context, user string, password string, deviceID *string) (*model.AuthResponse, error) {
// Get Context // Acquire Context
authContext := ctx.Value("auth").(*model.AuthContext) resp, req, err := getContextHTTP(ctx)
resp := authContext.AuthResponse if err != nil {
req := authContext.AuthRequest return nil, err
}
// Clear All Cookies By Default // Clear All Cookies By Default
accessCookie := http.Cookie{Name: "AccessToken", Path: "/", HttpOnly: true, MaxAge: -1, Expires: time.Now().Add(-100 * time.Hour)} accessCookie := http.Cookie{Name: "AccessToken", Path: "/", HttpOnly: true, MaxAge: -1, Expires: time.Now().Add(-100 * time.Hour)}
@ -219,9 +214,11 @@ func (r *queryResolver) Login(ctx context.Context, user string, password string,
} }
func (r *queryResolver) Logout(ctx context.Context) (*model.AuthResponse, error) { func (r *queryResolver) Logout(ctx context.Context) (*model.AuthResponse, error) {
// Set Cookie From Context // Acquire Context
authContext := ctx.Value("auth").(*model.AuthContext) resp, _, err := getContextHTTP(ctx)
resp := authContext.AuthResponse if err != nil {
return nil, err
}
// Clear All Cookies // Clear All Cookies
accessCookie := http.Cookie{Name: "AccessToken", Path: "/", HttpOnly: true, MaxAge: -1, Expires: time.Now().Add(-100 * time.Hour)} accessCookie := http.Cookie{Name: "AccessToken", Path: "/", HttpOnly: true, MaxAge: -1, Expires: time.Now().Add(-100 * time.Hour)}
@ -232,13 +229,11 @@ func (r *queryResolver) Logout(ctx context.Context) (*model.AuthResponse, error)
return &model.AuthResponse{Result: model.AuthResultSuccess}, nil return &model.AuthResponse{Result: model.AuthResultSuccess}, nil
} }
func (r *queryResolver) MediaItem(ctx context.Context, id string, delete *bool) (*model.MediaItem, error) { func (r *queryResolver) MediaItem(ctx context.Context, id string) (*model.MediaItem, error) {
// Get Context // Acquire Context
authContext := ctx.Value("auth").(*model.AuthContext) userID, _, err := getContextIDs(ctx)
accessToken := *authContext.AccessToken if err != nil {
userID, ok := accessToken.Get("sub") return nil, err
if !ok {
return nil, errors.New("Context Error")
} }
mediaItemID, err := uuid.Parse(id) mediaItemID, err := uuid.Parse(id)
@ -246,7 +241,7 @@ func (r *queryResolver) MediaItem(ctx context.Context, id string, delete *bool)
return nil, errors.New("Invalid ID Format") return nil, errors.New("Invalid ID Format")
} }
foundMediaItem := &model.MediaItem{ID: mediaItemID.String(), UserID: userID.(string)} foundMediaItem := &model.MediaItem{ID: mediaItemID.String(), UserID: userID}
count, err := r.DB.MediaItem(foundMediaItem) count, err := r.DB.MediaItem(foundMediaItem)
if err != nil { if err != nil {
return nil, errors.New("DB Error") return nil, errors.New("DB Error")
@ -256,13 +251,11 @@ func (r *queryResolver) MediaItem(ctx context.Context, id string, delete *bool)
return foundMediaItem, nil return foundMediaItem, nil
} }
func (r *queryResolver) Device(ctx context.Context, id string, delete *bool) (*model.Device, error) { func (r *queryResolver) Device(ctx context.Context, id string) (*model.Device, error) {
// Get Context // Acquire Context
authContext := ctx.Value("auth").(*model.AuthContext) userID, _, err := getContextIDs(ctx)
accessToken := *authContext.AccessToken if err != nil {
userID, ok := accessToken.Get("sub") return nil, err
if !ok {
return nil, errors.New("Context Error")
} }
deviceID, err := uuid.Parse(id) deviceID, err := uuid.Parse(id)
@ -270,7 +263,7 @@ func (r *queryResolver) Device(ctx context.Context, id string, delete *bool) (*m
return nil, errors.New("Invalid ID Format") return nil, errors.New("Invalid ID Format")
} }
foundDevice := &model.Device{ID: deviceID.String(), UserID: userID.(string)} foundDevice := &model.Device{ID: deviceID.String(), UserID: userID}
count, err := r.DB.Device(foundDevice) count, err := r.DB.Device(foundDevice)
if err != nil { if err != nil {
return nil, errors.New("DB Error") return nil, errors.New("DB Error")
@ -280,13 +273,11 @@ func (r *queryResolver) Device(ctx context.Context, id string, delete *bool) (*m
return foundDevice, nil return foundDevice, nil
} }
func (r *queryResolver) Album(ctx context.Context, id string, delete *bool) (*model.Album, error) { func (r *queryResolver) Album(ctx context.Context, id string) (*model.Album, error) {
// Get Context // Acquire Context
authContext := ctx.Value("auth").(*model.AuthContext) userID, _, err := getContextIDs(ctx)
accessToken := *authContext.AccessToken if err != nil {
userID, ok := accessToken.Get("sub") return nil, err
if !ok {
return nil, errors.New("Context Error")
} }
albumID, err := uuid.Parse(id) albumID, err := uuid.Parse(id)
@ -294,7 +285,7 @@ func (r *queryResolver) Album(ctx context.Context, id string, delete *bool) (*mo
return nil, errors.New("Invalid ID Format") return nil, errors.New("Invalid ID Format")
} }
foundAlbum := &model.Album{ID: albumID.String(), UserID: userID.(string)} foundAlbum := &model.Album{ID: albumID.String(), UserID: userID}
count, err := r.DB.Album(foundAlbum) count, err := r.DB.Album(foundAlbum)
if err != nil { if err != nil {
return nil, errors.New("DB Error") return nil, errors.New("DB Error")
@ -304,7 +295,7 @@ func (r *queryResolver) Album(ctx context.Context, id string, delete *bool) (*mo
return foundAlbum, nil return foundAlbum, nil
} }
func (r *queryResolver) User(ctx context.Context, id string, delete *bool) (*model.User, error) { func (r *queryResolver) User(ctx context.Context, id string) (*model.User, error) {
userID, err := uuid.Parse(id) userID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, errors.New("Invalid ID Format") return nil, errors.New("Invalid ID Format")
@ -320,13 +311,19 @@ func (r *queryResolver) User(ctx context.Context, id string, delete *bool) (*mod
return foundUser, nil return foundUser, nil
} }
func (r *queryResolver) Tag(ctx context.Context, id string, delete *bool) (*model.Tag, error) { func (r *queryResolver) Tag(ctx context.Context, id string) (*model.Tag, error) {
// Acquire Context
userID, _, err := getContextIDs(ctx)
if err != nil {
return nil, err
}
tagID, err := uuid.Parse(id) tagID, err := uuid.Parse(id)
if err != nil { if err != nil {
return nil, errors.New("Invalid ID Format") return nil, errors.New("Invalid ID Format")
} }
foundTag := &model.Tag{ID: tagID.String()} foundTag := &model.Tag{ID: tagID.String(), UserID: userID}
count, err := r.DB.Tag(foundTag) count, err := r.DB.Tag(foundTag)
if err != nil { if err != nil {
return nil, errors.New("DB Error") return nil, errors.New("DB Error")
@ -336,16 +333,14 @@ func (r *queryResolver) Tag(ctx context.Context, id string, delete *bool) (*mode
return foundTag, nil return foundTag, nil
} }
func (r *queryResolver) Me(ctx context.Context, delete *bool) (*model.User, error) { func (r *queryResolver) Me(ctx context.Context) (*model.User, error) {
// Get Context // Acquire Context
authContext := ctx.Value("auth").(*model.AuthContext) userID, _, err := getContextIDs(ctx)
accessToken := *authContext.AccessToken if err != nil {
userID, ok := accessToken.Get("sub") return nil, err
if !ok {
return nil, errors.New("Context Error")
} }
foundUser := &model.User{ID: userID.(string)} foundUser := &model.User{ID: userID}
count, err := r.DB.User(foundUser) count, err := r.DB.User(foundUser)
if err != nil || count != 1 { if err != nil || count != 1 {
return nil, errors.New("DB Error") return nil, errors.New("DB Error")
@ -353,88 +348,82 @@ func (r *queryResolver) Me(ctx context.Context, delete *bool) (*model.User, erro
return foundUser, nil return foundUser, nil
} }
func (r *queryResolver) MediaItems(ctx context.Context, delete *bool, filter *model.MediaItemFilter, count *int, page *int) (*model.MediaItemResponse, error) { func (r *queryResolver) MediaItems(ctx context.Context, filter *model.MediaItemFilter, page *model.Page, order *model.Order) (*model.MediaItemResponse, error) {
resp, totalCount, err := r.DB.MediaItems(filter) // Acquire Context
userID, _, err := getContextIDs(ctx)
if err != nil { if err != nil {
return nil, errors.New("Context Error") return nil, err
}
resp, pageResponse, err := r.DB.MediaItems(userID, filter, page, order)
if err != nil {
return nil, errors.New("DB Error")
} }
return &model.MediaItemResponse{ return &model.MediaItemResponse{
Data: resp, Data: resp,
PageInfo: &model.PageInfo{ Page: &pageResponse,
Count: int(totalCount),
Page: 0,
Total: int(totalCount),
},
}, nil }, nil
} }
func (r *queryResolver) Devices(ctx context.Context, delete *bool, filter *model.DeviceFilter, count *int, page *int) (*model.DeviceResponse, error) { func (r *queryResolver) Devices(ctx context.Context, filter *model.DeviceFilter, page *model.Page, order *model.Order) (*model.DeviceResponse, error) {
// Get Context // Acquire Context
authContext := ctx.Value("auth").(*model.AuthContext) userID, _, err := getContextIDs(ctx)
accessToken := *authContext.AccessToken if err != nil {
userID, ok := accessToken.Get("sub") return nil, err
if !ok {
return nil, errors.New("Context Error")
} }
_ = userID
resp, totalCount, err := r.DB.Devices() resp, pageResponse, err := r.DB.Devices(userID, filter, page, order)
if err != nil { if err != nil {
return nil, errors.New("DB Error") return nil, errors.New("DB Error")
} }
return &model.DeviceResponse{ return &model.DeviceResponse{
Data: resp, Data: resp,
PageInfo: &model.PageInfo{ Page: &pageResponse,
Count: int(totalCount),
Page: 0,
Total: int(totalCount),
},
}, nil }, nil
} }
func (r *queryResolver) Albums(ctx context.Context, delete *bool, filter *model.AlbumFilter, count *int, page *int) (*model.AlbumResponse, error) { func (r *queryResolver) Albums(ctx context.Context, filter *model.AlbumFilter, page *model.Page, order *model.Order) (*model.AlbumResponse, error) {
// TODO: User Specific // Acquire Context
resp, totalCount, err := r.DB.Albums() userID, _, err := getContextIDs(ctx)
if err != nil {
return nil, err
}
resp, pageResponse, err := r.DB.Albums(userID, filter, page, order)
if err != nil { if err != nil {
return nil, errors.New("Context Error") return nil, errors.New("Context Error")
} }
return &model.AlbumResponse{ return &model.AlbumResponse{
Data: resp, Data: resp,
PageInfo: &model.PageInfo{ Page: &pageResponse,
Count: int(totalCount),
Page: 0,
Total: int(totalCount),
},
}, nil }, nil
} }
func (r *queryResolver) Tags(ctx context.Context, delete *bool, filter *model.TagFilter, count *int, page *int) (*model.TagResponse, error) { func (r *queryResolver) Tags(ctx context.Context, filter *model.TagFilter, page *model.Page, order *model.Order) (*model.TagResponse, error) {
resp, totalCount, err := r.DB.Tags() // Acquire Context
userID, _, err := getContextIDs(ctx)
if err != nil {
return nil, err
}
resp, pageResponse, err := r.DB.Tags(userID, filter, page, order)
if err != nil { if err != nil {
return nil, errors.New("Context Error") return nil, errors.New("Context Error")
} }
return &model.TagResponse{ return &model.TagResponse{
Data: resp, Data: resp,
PageInfo: &model.PageInfo{ Page: &pageResponse,
Count: int(totalCount),
Page: 0,
Total: int(totalCount),
},
}, nil }, nil
} }
func (r *queryResolver) Users(ctx context.Context, delete *bool, filter *model.UserFilter, count *int, page *int) (*model.UserResponse, error) { func (r *queryResolver) Users(ctx context.Context, filter *model.UserFilter, page *model.Page, order *model.Order) (*model.UserResponse, error) {
resp, totalCount, err := r.DB.Users() resp, pageResponse, err := r.DB.Users(filter, page, order)
if err != nil { if err != nil {
return nil, errors.New("Context Error") return nil, errors.New("Context Error")
} }
return &model.UserResponse{ return &model.UserResponse{
Data: resp, Data: resp,
PageInfo: &model.PageInfo{ Page: &pageResponse,
Count: int(totalCount),
Page: 0,
Total: int(totalCount),
},
}, nil }, nil
} }

View File

@ -2,6 +2,7 @@ package db
import ( import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gorm.io/gorm"
"reichard.io/imagini/graph/model" "reichard.io/imagini/graph/model"
) )
@ -18,11 +19,17 @@ func (dbm *DBManager) Album(album *model.Album) (int64, error) {
return count, err return count, err
} }
func (dbm *DBManager) Albums() ([]*model.Album, int64, error) { func (dbm *DBManager) Albums(userID string, filters *model.AlbumFilter, page *model.Page, order *model.Order) ([]*model.Album, model.PageResponse, error) {
// Initial User Filter
tx := dbm.db.Session(&gorm.Session{}).Model(&model.Album{}).Where("user_id == ?", userID)
// Dynamically Generate Base Query
tx, pageResponse := dbm.generateBaseQuery(tx, filters, page, order)
// Acquire Results
var foundAlbums []*model.Album var foundAlbums []*model.Album
var count int64 err := tx.Find(&foundAlbums).Error
err := dbm.db.Find(&foundAlbums).Count(&count).Error return foundAlbums, pageResponse, err
return foundAlbums, count, err
} }
func (dbm *DBManager) DeleteAlbum(album *model.Album) error { func (dbm *DBManager) DeleteAlbum(album *model.Album) error {

View File

@ -1,11 +1,13 @@
package db package db
import ( import (
"errors"
"fmt" "fmt"
"path" "path"
"reflect"
"github.com/iancoleman/strcase"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
// "gorm.io/gorm/logger" // "gorm.io/gorm/logger"
"gorm.io/driver/sqlite" "gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
@ -30,6 +32,7 @@ func NewMgr(c *config.Config) *DBManager {
if c.DBType == "SQLite" { if c.DBType == "SQLite" {
dbLocation := path.Join(c.ConfigPath, "imagini.db") dbLocation := path.Join(c.ConfigPath, "imagini.db")
dbm.db, _ = gorm.Open(sqlite.Open(dbLocation), gormConfig) dbm.db, _ = gorm.Open(sqlite.Open(dbLocation), gormConfig)
dbm.db = dbm.db.Debug()
} else { } else {
log.Fatal("Unsupported Database") log.Fatal("Unsupported Database")
} }
@ -48,28 +51,9 @@ func NewMgr(c *config.Config) *DBManager {
dbm.bootstrapDatabase() dbm.bootstrapDatabase()
} }
dbm.testFeatures()
return dbm return dbm
} }
func (dbm *DBManager) testFeatures() {
// Get Devices By UserID
// var myDevices []model.Device
// dbm.db.Debug().Where(&model.Device{UserID: "97589354-cd42-40e2-bc5e-7ba6badf89fa"}).Find(&myDevices)
// fmt.Printf("Devices: %+v\n", myDevices)
// Get User by DeviceID
// var myUser []model.User
// dbm.db.Debug().Model(&model.User{}).
// Select("users.*").
// Joins("left join devices on users.id = devices.user_id").
// Where("devices.id = ?", "4e9aa851-f25b-4330-91dc-c0f975e56fa1").
// Find(&myUser)
// fmt.Printf("User: %+v\n", myUser)
}
func (dbm *DBManager) bootstrapDatabase() { func (dbm *DBManager) bootstrapDatabase() {
log.Info("[query] Bootstrapping database.") log.Info("[query] Bootstrapping database.")
@ -88,46 +72,84 @@ func (dbm *DBManager) bootstrapDatabase() {
} }
} }
// TODO func (dbm *DBManager) generateBaseQuery(tx *gorm.DB, filter interface{}, page *model.Page, order *model.Order) (*gorm.DB, model.PageResponse) {
func (dbm *DBManager) QueryBuilder(dest interface{}, params []byte) (int64, error) { tx = dbm.generateFilter(tx, filter)
// TODO: tx = dbm.generateOrder(tx, order, filter)
// - Where Filters tx, pageResponse := dbm.generatePage(tx, page)
// - Sort Filters return tx, pageResponse
// - Paging Filters }
objType := fmt.Sprintf("%T", dest) func (dbm *DBManager) generateOrder(tx *gorm.DB, order *model.Order, filter interface{}) *gorm.DB {
if objType == "*[]model.MediaItem" { // Set Defaults
// TODO: Validate MediaItem Type orderBy := "created_at"
} else { orderDirection := model.OrderDirectionDesc
// Return Error
return 0, errors.New("Invalid type") if order == nil {
order = &model.Order{
By: &orderBy,
Direction: &orderDirection,
}
} }
var count int64 if order.By == nil {
err := dbm.db.Find(dest).Count(&count).Error order.By = &orderBy
return count, err }
// Paging: if order.Direction == nil {
// - Regular Pagination: order.Direction = &orderDirection
// - /api/v1/MediaItems?page[limit]=50&page=2 }
// - Meta Count Only
// - /api/v1/MediaItems?page[limit]=0
// Sorting: // Get Possible Values
// - Ascending Sort: ptr := reflect.New(reflect.TypeOf(filter).Elem())
// - /api/v1/MediaItems?sort=created_at v := reflect.Indirect(ptr)
// - Descending Sort:
// - /api/v1/MediaItems?sort=-created_at
// Filters: isValid := false
// - Greater Than / Less Than (created_at, updated_at, exif_date) for i := 0; i < v.NumField(); i++ {
// - /api/v1/MediaItems?filter[created_at]>=2020-01-01&filter[created_at]<=2021-01-01 fieldName := v.Type().Field(i).Name
// - Long / Lat Range (latitude, longitude) if strcase.ToSnake(*order.By) == strcase.ToSnake(fieldName) {
// - /api/v1/MediaItems?filter[latitude]>=71.1827&filter[latitude]<=72.0000&filter[longitude]>=100.000&filter[longitude]<=101.0000 isValid = true
// - Image / Video (media_type) break
// - /api/v1/MediaItems?filter[media_type]=Image }
// - Tags (tags) }
// - /api/v1/MediaItems?filter[tags]=id1,id2,id3
// - Albums (albums) if isValid {
// - /api/v1/MediaItems?filter[albums]=id1 tx = tx.Order(fmt.Sprintf("%s %s", strcase.ToSnake(*order.By), order.Direction.String()))
}
return tx
}
func (dbm *DBManager) generatePage(tx *gorm.DB, page *model.Page) (*gorm.DB, model.PageResponse) {
// Set Defaults
var count int64
pageSize := 50
pageNum := 1
if page == nil {
page = &model.Page{
Size: &pageSize,
Page: &pageNum,
}
}
if page.Size == nil {
page.Size = &pageSize
}
if page.Page == nil {
page.Page = &pageNum
}
// Acquire Counts Before Pagination
tx.Count(&count)
// Calculate Offset
calculatedOffset := (*page.Page - 1) * *page.Size
tx = tx.Limit(*page.Size).Offset(calculatedOffset)
return tx, model.PageResponse{
Page: *page.Page,
Size: *page.Size,
Total: int(count),
}
} }

View File

@ -3,6 +3,7 @@ package db
import ( import (
"github.com/google/uuid" "github.com/google/uuid"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gorm.io/gorm"
"reichard.io/imagini/graph/model" "reichard.io/imagini/graph/model"
) )
@ -21,11 +22,17 @@ func (dbm *DBManager) Device(device *model.Device) (int64, error) {
return count, err return count, err
} }
func (dbm *DBManager) Devices() ([]*model.Device, int64, error) { func (dbm *DBManager) Devices(userID string, filters *model.DeviceFilter, page *model.Page, order *model.Order) ([]*model.Device, model.PageResponse, error) {
// Initial User Filter
tx := dbm.db.Session(&gorm.Session{}).Model(&model.Device{}).Where("user_id == ?", userID)
// Dynamically Generate Base Query
tx, pageResponse := dbm.generateBaseQuery(tx, filters, page, order)
// Acquire Results
var foundDevices []*model.Device var foundDevices []*model.Device
var count int64 err := tx.Find(&foundDevices).Error
err := dbm.db.Find(&foundDevices).Count(&count).Error return foundDevices, pageResponse, err
return foundDevices, count, err
} }
func (dbm *DBManager) DeleteDevice(user *model.Device) error { func (dbm *DBManager) DeleteDevice(user *model.Device) error {

View File

@ -10,10 +10,13 @@ import (
) )
// Generic function used to generate filters for the DB // Generic function used to generate filters for the DB
func (dbm *DBManager) generateFilters(filter interface{}) (*gorm.DB, error) { func (dbm *DBManager) generateFilter(tx *gorm.DB, filter interface{}) *gorm.DB {
tx := dbm.db.Session(&gorm.Session{}).Debug() ptr := reflect.ValueOf(filter)
v := reflect.Indirect(ptr)
v := reflect.ValueOf(filter) if v == reflect.ValueOf(nil) {
return tx
}
for i := 0; i < v.NumField(); i++ { for i := 0; i < v.NumField(); i++ {
fieldName := strcase.ToSnake(v.Type().Field(i).Name) fieldName := strcase.ToSnake(v.Type().Field(i).Name)
@ -45,7 +48,7 @@ func (dbm *DBManager) generateFilters(filter interface{}) (*gorm.DB, error) {
} }
} }
return tx, nil return tx
} }
func generateStringFilter(tx *gorm.DB, fieldName string, filter *model.StringFilter) *gorm.DB { func generateStringFilter(tx *gorm.DB, fieldName string, filter *model.StringFilter) *gorm.DB {

View File

@ -2,6 +2,7 @@ package db
import ( import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gorm.io/gorm"
"reichard.io/imagini/graph/model" "reichard.io/imagini/graph/model"
) )
@ -18,14 +19,16 @@ func (dbm *DBManager) MediaItem(mediaItem *model.MediaItem) (int64, error) {
return count, err return count, err
} }
func (dbm *DBManager) MediaItems(filters *model.MediaItemFilter) ([]*model.MediaItem, int64, error) { // UserID, Filters, Sort, Page, Delete
// Perform Filters func (dbm *DBManager) MediaItems(userID string, filters *model.MediaItemFilter, page *model.Page, order *model.Order) ([]*model.MediaItem, model.PageResponse, error) {
tx, err := dbm.generateFilters(*filters) // Initial User Filter
if err != nil { tx := dbm.db.Session(&gorm.Session{}).Model(&model.MediaItem{}).Where("user_id == ?", userID)
return nil, 0, err
} // Dynamically Generate Base Query
tx, pageResponse := dbm.generateBaseQuery(tx, filters, page, order)
// Acquire Results
var mediaItems []*model.MediaItem var mediaItems []*model.MediaItem
var count int64 err := tx.Find(&mediaItems).Error
err = tx.Find(&mediaItems).Count(&count).Error return mediaItems, pageResponse, err
return mediaItems, count, err
} }

View File

@ -2,6 +2,7 @@ package db
import ( import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"gorm.io/gorm"
"reichard.io/imagini/graph/model" "reichard.io/imagini/graph/model"
) )
@ -18,11 +19,17 @@ func (dbm *DBManager) Tag(tag *model.Tag) (int64, error) {
return count, err return count, err
} }
func (dbm *DBManager) Tags() ([]*model.Tag, int64, error) { func (dbm *DBManager) Tags(userID string, filters *model.TagFilter, page *model.Page, order *model.Order) ([]*model.Tag, model.PageResponse, error) {
// Initial User Filter
tx := dbm.db.Session(&gorm.Session{}).Model(&model.Tag{}).Where("user_id == ?", userID)
// Dynamically Generate Base Query
tx, pageResponse := dbm.generateBaseQuery(tx, filters, page, order)
// Acquire Results
var foundTags []*model.Tag var foundTags []*model.Tag
var count int64 err := tx.Find(&foundTags).Error
err := dbm.db.Find(&foundTags).Count(&count).Error return foundTags, pageResponse, err
return foundTags, count, err
} }
func (dbm *DBManager) DeleteTag(tag *model.Tag) error { func (dbm *DBManager) DeleteTag(tag *model.Tag) error {

View File

@ -3,6 +3,7 @@ package db
import ( import (
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
"reichard.io/imagini/graph/model" "reichard.io/imagini/graph/model"
) )
@ -25,11 +26,17 @@ func (dbm *DBManager) User(user *model.User) (int64, error) {
return count, err return count, err
} }
func (dbm *DBManager) Users() ([]*model.User, int64, error) { func (dbm *DBManager) Users(filters *model.UserFilter, page *model.Page, order *model.Order) ([]*model.User, model.PageResponse, error) {
// Initial User Filter
tx := dbm.db.Session(&gorm.Session{}).Model(&model.Tag{})
// Dynamically Generate Base Query
tx, pageResponse := dbm.generateBaseQuery(tx, filters, page, order)
// Acquire Results
var foundUsers []*model.User var foundUsers []*model.User
var count int64 err := tx.Find(&foundUsers).Error
err := dbm.db.Find(&foundUsers).Count(&count).Error return foundUsers, pageResponse, err
return foundUsers, count, err
} }
func (dbm *DBManager) DeleteUser(user model.User) error { func (dbm *DBManager) DeleteUser(user model.User) error {