Backend support for gallery custom fields (#6592)

This commit is contained in:
WithoutPants 2026-02-23 07:39:28 +11:00 committed by GitHub
parent 076032ff8b
commit 47dcdd439c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 528 additions and 50 deletions

View file

@ -596,6 +596,8 @@ input GalleryFilterType {
files_filter: FileFilterType
"Filter by related folders that meet this criteria"
folders_filter: FolderFilterType
custom_fields: [CustomFieldCriterionInput!]
}
input TagFilterType {

View file

@ -32,6 +32,7 @@ type Gallery {
cover: Image
paths: GalleryPathsType! # Resolver
custom_fields: Map!
image(index: Int!): Image!
}
@ -50,6 +51,8 @@ input GalleryCreateInput {
studio_id: ID
tag_ids: [ID!]
performer_ids: [ID!]
custom_fields: Map
}
input GalleryUpdateInput {
@ -71,6 +74,8 @@ input GalleryUpdateInput {
performer_ids: [ID!]
primary_file_id: ID
custom_fields: CustomFieldsInput
}
input BulkGalleryUpdateInput {
@ -89,6 +94,8 @@ input BulkGalleryUpdateInput {
studio_id: ID
tag_ids: BulkUpdateIds
performer_ids: BulkUpdateIds
custom_fields: CustomFieldsInput
}
input GalleryDestroyInput {

View file

@ -54,8 +54,9 @@ type Loaders struct {
ImageFiles *ImageFileIDsLoader
GalleryFiles *GalleryFileIDsLoader
GalleryByID *GalleryLoader
ImageByID *ImageLoader
GalleryByID *GalleryLoader
GalleryCustomFields *CustomFieldsLoader
ImageByID *ImageLoader
PerformerByID *PerformerLoader
PerformerCustomFields *CustomFieldsLoader
@ -88,6 +89,11 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
maxBatch: maxBatch,
fetch: m.fetchGalleries(ctx),
},
GalleryCustomFields: &CustomFieldsLoader{
wait: wait,
maxBatch: maxBatch,
fetch: m.fetchGalleryCustomFields(ctx),
},
ImageByID: &ImageLoader{
wait: wait,
maxBatch: maxBatch,
@ -319,6 +325,18 @@ func (m Middleware) fetchTagCustomFields(ctx context.Context) func(keys []int) (
}
}
func (m Middleware) fetchGalleryCustomFields(ctx context.Context) func(keys []int) ([]models.CustomFieldMap, []error) {
return func(keys []int) (ret []models.CustomFieldMap, errs []error) {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {
var err error
ret, err = m.Repository.Gallery.GetCustomFieldsBulk(ctx, keys)
return err
})
return ret, toErrorSlice(err)
}
}
func (m Middleware) fetchGroups(ctx context.Context) func(keys []int) ([]*models.Group, []error) {
return func(keys []int) (ret []*models.Group, errs []error) {
err := m.Repository.WithDB(ctx, func(ctx context.Context) error {

View file

@ -216,3 +216,16 @@ func (r *galleryResolver) Image(ctx context.Context, obj *models.Gallery, index
return
}
func (r *galleryResolver) CustomFields(ctx context.Context, obj *models.Gallery) (map[string]interface{}, error) {
m, err := loaders.From(ctx).GalleryCustomFields.Load(obj.ID)
if err != nil {
return nil, err
}
if m == nil {
return make(map[string]interface{}), nil
}
return m, nil
}

View file

@ -42,7 +42,10 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
}
// Populate a new gallery from the input
newGallery := models.NewGallery()
newGallery := models.CreateGalleryInput{
Gallery: &models.Gallery{},
}
*newGallery.Gallery = models.NewGallery()
newGallery.Title = strings.TrimSpace(input.Title)
newGallery.Code = translator.string(input.Code)
@ -81,10 +84,12 @@ func (r *mutationResolver) GalleryCreate(ctx context.Context, input GalleryCreat
newGallery.URLs = models.NewRelatedStrings([]string{strings.TrimSpace(*input.URL)})
}
newGallery.CustomFields = convertMapJSONNumbers(input.CustomFields)
// Start the transaction and save the gallery
if err := r.withTxn(ctx, func(ctx context.Context) error {
qb := r.repository.Gallery
if err := qb.Create(ctx, &newGallery, nil); err != nil {
if err := qb.Create(ctx, &newGallery); err != nil {
return err
}
@ -241,6 +246,10 @@ func (r *mutationResolver) galleryUpdate(ctx context.Context, input models.Galle
return nil, fmt.Errorf("converting scene ids: %w", err)
}
if input.CustomFields != nil {
updatedGallery.CustomFields = handleUpdateCustomFields(*input.CustomFields)
}
// gallery scene is set from the scene only
gallery, err := qb.UpdatePartial(ctx, galleryID, updatedGallery)
@ -293,6 +302,10 @@ func (r *mutationResolver) BulkGalleryUpdate(ctx context.Context, input BulkGall
return nil, fmt.Errorf("converting scene ids: %w", err)
}
if input.CustomFields != nil {
updatedGallery.CustomFields = handleUpdateCustomFields(*input.CustomFields)
}
ret := []*models.Gallery{}
// Start the transaction and save the galleries

View file

@ -468,7 +468,10 @@ func makeGallery(expectedResult bool) *models.Gallery {
}
func createGallery(ctx context.Context, w models.GalleryWriter, o *models.Gallery, f *models.BaseFile) error {
err := w.Create(ctx, o, []models.FileID{f.ID})
err := w.Create(ctx, &models.CreateGalleryInput{
Gallery: o,
FileIDs: []models.FileID{f.ID},
})
if err != nil {
return fmt.Errorf("Failed to create gallery with path '%s': %s", f.Path, err.Error())
}

View file

@ -779,6 +779,7 @@ func (t *ExportTask) exportGallery(ctx context.Context, wg *sync.WaitGroup, jobC
studioReader := r.Studio
performerReader := r.Performer
tagReader := r.Tag
galleryReader := r.Gallery
galleryChapterReader := r.GalleryChapter
for g := range jobChan {
@ -847,6 +848,12 @@ func (t *ExportTask) exportGallery(ctx context.Context, wg *sync.WaitGroup, jobC
newGalleryJSON.Tags = tag.GetNames(tags)
newGalleryJSON.CustomFields, err = galleryReader.GetCustomFields(ctx, g.ID)
if err != nil {
logger.Errorf("[galleries] <%s> error getting gallery custom fields: %v", g.DisplayName(), err)
continue
}
if t.includeDependencies {
if g.StudioID != nil {
t.studios.IDs = sliceutil.AppendUnique(t.studios.IDs, *g.StudioID)

View file

@ -28,8 +28,9 @@ type Importer struct {
Input jsonschema.Gallery
MissingRefBehaviour models.ImportMissingRefEnum
ID int
gallery models.Gallery
ID int
gallery models.Gallery
customFields map[string]interface{}
}
func (i *Importer) PreImport(ctx context.Context) error {
@ -51,6 +52,8 @@ func (i *Importer) PreImport(ctx context.Context) error {
return err
}
i.customFields = i.Input.CustomFields
return nil
}
@ -356,7 +359,11 @@ func (i *Importer) Create(ctx context.Context) (*int, error) {
for _, f := range i.gallery.Files.List() {
fileIDs = append(fileIDs, f.Base().ID)
}
err := i.ReaderWriter.Create(ctx, &i.gallery, fileIDs)
err := i.ReaderWriter.Create(ctx, &models.CreateGalleryInput{
Gallery: &i.gallery,
FileIDs: fileIDs,
CustomFields: i.customFields,
})
if err != nil {
return nil, fmt.Errorf("error creating gallery: %v", err)
}
@ -368,7 +375,12 @@ func (i *Importer) Create(ctx context.Context) (*int, error) {
func (i *Importer) Update(ctx context.Context, id int) error {
gallery := i.gallery
gallery.ID = id
err := i.ReaderWriter.Update(ctx, &gallery)
err := i.ReaderWriter.Update(ctx, &models.UpdateGalleryInput{
Gallery: &gallery,
CustomFields: models.CustomFieldsInput{
Full: i.customFields,
},
})
if err != nil {
return fmt.Errorf("error updating existing gallery: %v", err)
}

View file

@ -17,7 +17,7 @@ type ScanCreatorUpdater interface {
FindByFingerprints(ctx context.Context, fp []models.Fingerprint) ([]*models.Gallery, error)
GetFiles(ctx context.Context, relatedID int) ([]models.File, error)
Create(ctx context.Context, newGallery *models.Gallery, fileIDs []models.FileID) error
models.GalleryCreator
UpdatePartial(ctx context.Context, id int, updatedGallery models.GalleryPartial) (*models.Gallery, error)
AddFileID(ctx context.Context, id int, fileID models.FileID) error
}
@ -80,7 +80,10 @@ func (h *ScanHandler) Handle(ctx context.Context, f models.File, oldFile models.
logger.Infof("%s doesn't exist. Creating new gallery...", f.Base().Path)
if err := h.CreatorUpdater.Create(ctx, &newGallery, []models.FileID{baseFile.ID}); err != nil {
if err := h.CreatorUpdater.Create(ctx, &models.CreateGalleryInput{
Gallery: &newGallery,
FileIDs: []models.FileID{baseFile.ID},
}); err != nil {
return fmt.Errorf("creating new gallery: %w", err)
}

View file

@ -35,7 +35,7 @@ type ScanCreatorUpdater interface {
type GalleryFinderCreator interface {
FindByFileID(ctx context.Context, fileID models.FileID) ([]*models.Gallery, error)
FindByFolderID(ctx context.Context, folderID models.FolderID) ([]*models.Gallery, error)
Create(ctx context.Context, newObject *models.Gallery, fileIDs []models.FileID) error
models.GalleryCreator
UpdatePartial(ctx context.Context, id int, updatedGallery models.GalleryPartial) (*models.Gallery, error)
}
@ -252,9 +252,13 @@ func (h *ScanHandler) getOrCreateFolderBasedGallery(ctx context.Context, f model
newGallery := models.NewGallery()
newGallery.FolderID = &folderID
input := models.CreateGalleryInput{
Gallery: &newGallery,
}
logger.Infof("Creating folder-based gallery for %s", filepath.Dir(f.Base().Path))
if err := h.GalleryFinder.Create(ctx, &newGallery, nil); err != nil {
if err := h.GalleryFinder.Create(ctx, &input); err != nil {
return nil, fmt.Errorf("creating folder based gallery: %w", err)
}
@ -308,7 +312,12 @@ func (h *ScanHandler) getOrCreateZipBasedGallery(ctx context.Context, zipFile mo
logger.Infof("%s doesn't exist. Creating new gallery...", zipFile.Base().Path)
if err := h.GalleryFinder.Create(ctx, &newGallery, []models.FileID{zipFile.Base().ID}); err != nil {
input := models.CreateGalleryInput{
Gallery: &newGallery,
FileIDs: []models.FileID{zipFile.Base().ID},
}
if err := h.GalleryFinder.Create(ctx, &input); err != nil {
return nil, fmt.Errorf("creating zip-based gallery: %w", err)
}

View file

@ -67,6 +67,9 @@ type GalleryFilterType struct {
CreatedAt *TimestampCriterionInput `json:"created_at"`
// Filter by updated at
UpdatedAt *TimestampCriterionInput `json:"updated_at"`
// Filter by custom fields
CustomFields []CustomFieldCriterionInput `json:"custom_fields"`
}
type GalleryUpdateInput struct {
@ -86,6 +89,8 @@ type GalleryUpdateInput struct {
PerformerIds []string `json:"performer_ids"`
PrimaryFileID *string `json:"primary_file_id"`
CustomFields *CustomFieldsInput `json:"custom_fields"`
// deprecated
URL *string `json:"url"`
}

View file

@ -18,22 +18,23 @@ type GalleryChapter struct {
}
type Gallery struct {
ZipFiles []string `json:"zip_files,omitempty"`
FolderPath string `json:"folder_path,omitempty"`
Title string `json:"title,omitempty"`
Code string `json:"code,omitempty"`
URLs []string `json:"urls,omitempty"`
Date string `json:"date,omitempty"`
Details string `json:"details,omitempty"`
Photographer string `json:"photographer,omitempty"`
Rating int `json:"rating,omitempty"`
Organized bool `json:"organized,omitempty"`
Chapters []GalleryChapter `json:"chapters,omitempty"`
Studio string `json:"studio,omitempty"`
Performers []string `json:"performers,omitempty"`
Tags []string `json:"tags,omitempty"`
CreatedAt json.JSONTime `json:"created_at,omitempty"`
UpdatedAt json.JSONTime `json:"updated_at,omitempty"`
ZipFiles []string `json:"zip_files,omitempty"`
FolderPath string `json:"folder_path,omitempty"`
Title string `json:"title,omitempty"`
Code string `json:"code,omitempty"`
URLs []string `json:"urls,omitempty"`
Date string `json:"date,omitempty"`
Details string `json:"details,omitempty"`
Photographer string `json:"photographer,omitempty"`
Rating int `json:"rating,omitempty"`
Organized bool `json:"organized,omitempty"`
Chapters []GalleryChapter `json:"chapters,omitempty"`
Studio string `json:"studio,omitempty"`
Performers []string `json:"performers,omitempty"`
Tags []string `json:"tags,omitempty"`
CreatedAt json.JSONTime `json:"created_at,omitempty"`
UpdatedAt json.JSONTime `json:"updated_at,omitempty"`
CustomFields map[string]interface{} `json:"custom_fields,omitempty"`
// deprecated - for import only
URL string `json:"url,omitempty"`

View file

@ -114,13 +114,13 @@ func (_m *GalleryReaderWriter) CountByFileID(ctx context.Context, fileID models.
return r0, r1
}
// Create provides a mock function with given fields: ctx, newGallery, fileIDs
func (_m *GalleryReaderWriter) Create(ctx context.Context, newGallery *models.Gallery, fileIDs []models.FileID) error {
ret := _m.Called(ctx, newGallery, fileIDs)
// Create provides a mock function with given fields: ctx, newGallery
func (_m *GalleryReaderWriter) Create(ctx context.Context, newGallery *models.CreateGalleryInput) error {
ret := _m.Called(ctx, newGallery)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *models.Gallery, []models.FileID) error); ok {
r0 = rf(ctx, newGallery, fileIDs)
if rf, ok := ret.Get(0).(func(context.Context, *models.CreateGalleryInput) error); ok {
r0 = rf(ctx, newGallery)
} else {
r0 = ret.Error(0)
}
@ -395,6 +395,52 @@ func (_m *GalleryReaderWriter) FindUserGalleryByTitle(ctx context.Context, title
return r0, r1
}
// GetCustomFields provides a mock function with given fields: ctx, id
func (_m *GalleryReaderWriter) GetCustomFields(ctx context.Context, id int) (map[string]interface{}, error) {
ret := _m.Called(ctx, id)
var r0 map[string]interface{}
if rf, ok := ret.Get(0).(func(context.Context, int) map[string]interface{}); ok {
r0 = rf(ctx, id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(map[string]interface{})
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, int) error); ok {
r1 = rf(ctx, id)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetCustomFieldsBulk provides a mock function with given fields: ctx, ids
func (_m *GalleryReaderWriter) GetCustomFieldsBulk(ctx context.Context, ids []int) ([]models.CustomFieldMap, error) {
ret := _m.Called(ctx, ids)
var r0 []models.CustomFieldMap
if rf, ok := ret.Get(0).(func(context.Context, []int) []models.CustomFieldMap); ok {
r0 = rf(ctx, ids)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]models.CustomFieldMap)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, []int) error); ok {
r1 = rf(ctx, ids)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// GetFiles provides a mock function with given fields: ctx, relatedID
func (_m *GalleryReaderWriter) GetFiles(ctx context.Context, relatedID int) ([]models.File, error) {
ret := _m.Called(ctx, relatedID)
@ -656,12 +702,26 @@ func (_m *GalleryReaderWriter) SetCover(ctx context.Context, galleryID int, cove
return r0
}
// SetCustomFields provides a mock function with given fields: ctx, id, fields
func (_m *GalleryReaderWriter) SetCustomFields(ctx context.Context, id int, fields models.CustomFieldsInput) error {
ret := _m.Called(ctx, id, fields)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, int, models.CustomFieldsInput) error); ok {
r0 = rf(ctx, id, fields)
} else {
r0 = ret.Error(0)
}
return r0
}
// Update provides a mock function with given fields: ctx, updatedGallery
func (_m *GalleryReaderWriter) Update(ctx context.Context, updatedGallery *models.Gallery) error {
func (_m *GalleryReaderWriter) Update(ctx context.Context, updatedGallery *models.UpdateGalleryInput) error {
ret := _m.Called(ctx, updatedGallery)
var r0 error
if rf, ok := ret.Get(0).(func(context.Context, *models.Gallery) error); ok {
if rf, ok := ret.Get(0).(func(context.Context, *models.UpdateGalleryInput) error); ok {
r0 = rf(ctx, updatedGallery)
} else {
r0 = ret.Error(0)

View file

@ -46,6 +46,20 @@ func NewGallery() Gallery {
}
}
type CreateGalleryInput struct {
*Gallery
FileIDs []FileID
CustomFields map[string]interface{} `json:"custom_fields"`
}
type UpdateGalleryInput struct {
*Gallery
FileIDs []FileID
CustomFields CustomFieldsInput `json:"custom_fields"`
}
// GalleryPartial represents part of a Gallery object. It is used to update
// the database entry. Only non-nil fields will be updated.
type GalleryPartial struct {
@ -70,6 +84,8 @@ type GalleryPartial struct {
TagIDs *UpdateIDs
PerformerIDs *UpdateIDs
PrimaryFileID *FileID
CustomFields CustomFieldsInput
}
func NewGalleryPartial() GalleryPartial {

View file

@ -37,12 +37,12 @@ type GalleryCounter interface {
// GalleryCreator provides methods to create galleries.
type GalleryCreator interface {
Create(ctx context.Context, newGallery *Gallery, fileIDs []FileID) error
Create(ctx context.Context, newGallery *CreateGalleryInput) error
}
// GalleryUpdater provides methods to update galleries.
type GalleryUpdater interface {
Update(ctx context.Context, updatedGallery *Gallery) error
Update(ctx context.Context, updatedGallery *UpdateGalleryInput) error
UpdatePartial(ctx context.Context, id int, updatedGallery GalleryPartial) (*Gallery, error)
UpdateImages(ctx context.Context, galleryID int, imageIDs []int) error
}
@ -70,6 +70,7 @@ type GalleryReader interface {
PerformerIDLoader
TagIDLoader
FileLoader
CustomFieldsReader
All(ctx context.Context) ([]*Gallery, error)
}
@ -80,6 +81,8 @@ type GalleryWriter interface {
GalleryUpdater
GalleryDestroyer
CustomFieldsWriter
AddFileID(ctx context.Context, id int, fileID FileID) error
AddImages(ctx context.Context, galleryID int, imageIDs ...int) error
RemoveImages(ctx context.Context, galleryID int, imageIDs ...int) error

View file

@ -522,6 +522,10 @@ func (db *Anonymiser) anonymiseGalleries(ctx context.Context) error {
return err
}
if err := db.anonymiseCustomFields(ctx, goqu.T(galleriesCustomFieldsTable.GetTable()), "gallery_id"); err != nil {
return err
}
return nil
}

View file

@ -240,3 +240,9 @@ func TestSceneSetCustomFields(t *testing.T) {
testSetCustomFields(t, "Scene", db.Scene, sceneIDs[sceneIdx], getSceneCustomFields(sceneIdx))
}
func TestGallerySetCustomFields(t *testing.T) {
galleryIdx := galleryIdxWithScene
testSetCustomFields(t, "Gallery", db.Gallery, galleryIDs[galleryIdx], getGalleryCustomFields(galleryIdx))
}

View file

@ -34,7 +34,7 @@ const (
cacheSizeEnv = "STASH_SQLITE_CACHE_SIZE"
)
var appSchemaVersion uint = 80
var appSchemaVersion uint = 81
//go:embed migrations/*.sql
var migrationsBox embed.FS

View file

@ -183,6 +183,8 @@ var (
)
type GalleryStore struct {
customFieldsStore
tableMgr *table
fileStore *FileStore
@ -191,6 +193,10 @@ type GalleryStore struct {
func NewGalleryStore(fileStore *FileStore, folderStore *FolderStore) *GalleryStore {
return &GalleryStore{
customFieldsStore: customFieldsStore{
table: galleriesCustomFieldsTable,
fk: galleriesCustomFieldsTable.Col(galleryIDColumn),
},
tableMgr: galleryTableMgr,
fileStore: fileStore,
folderStore: folderStore,
@ -231,18 +237,18 @@ func (qb *GalleryStore) selectDataset() *goqu.SelectDataset {
)
}
func (qb *GalleryStore) Create(ctx context.Context, newObject *models.Gallery, fileIDs []models.FileID) error {
func (qb *GalleryStore) Create(ctx context.Context, newObject *models.CreateGalleryInput) error {
var r galleryRow
r.fromGallery(*newObject)
r.fromGallery(*newObject.Gallery)
id, err := qb.tableMgr.insertID(ctx, r)
if err != nil {
return err
}
if len(fileIDs) > 0 {
if len(newObject.FileIDs) > 0 {
const firstPrimary = true
if err := galleriesFilesTableMgr.insertJoins(ctx, id, firstPrimary, fileIDs); err != nil {
if err := galleriesFilesTableMgr.insertJoins(ctx, id, firstPrimary, newObject.FileIDs); err != nil {
return err
}
}
@ -269,19 +275,24 @@ func (qb *GalleryStore) Create(ctx context.Context, newObject *models.Gallery, f
}
}
const partial = false
if err := qb.setCustomFields(ctx, id, newObject.CustomFields, partial); err != nil {
return err
}
updated, err := qb.find(ctx, id)
if err != nil {
return fmt.Errorf("finding after create: %w", err)
}
*newObject = *updated
*newObject.Gallery = *updated
return nil
}
func (qb *GalleryStore) Update(ctx context.Context, updatedObject *models.Gallery) error {
func (qb *GalleryStore) Update(ctx context.Context, updatedObject *models.UpdateGalleryInput) error {
var r galleryRow
r.fromGallery(*updatedObject)
r.fromGallery(*updatedObject.Gallery)
if err := qb.tableMgr.updateByID(ctx, updatedObject.ID, r); err != nil {
return err
@ -319,6 +330,10 @@ func (qb *GalleryStore) Update(ctx context.Context, updatedObject *models.Galler
}
}
if err := qb.SetCustomFields(ctx, updatedObject.ID, updatedObject.CustomFields); err != nil {
return err
}
return nil
}
@ -364,6 +379,10 @@ func (qb *GalleryStore) UpdatePartial(ctx context.Context, id int, partial model
}
}
if err := qb.SetCustomFields(ctx, id, partial.CustomFields); err != nil {
return nil, err
}
return qb.find(ctx, id)
}

View file

@ -105,6 +105,13 @@ func (qb *galleryFilterHandler) criterionHandler() criterionHandler {
&timestampCriterionHandler{filter.CreatedAt, "galleries.created_at", nil},
&timestampCriterionHandler{filter.UpdatedAt, "galleries.updated_at", nil},
&customFieldsFilterHandler{
table: galleriesCustomFieldsTable.GetTable(),
fkCol: galleryIDColumn,
c: filter.CustomFields,
idCol: "galleries.id",
},
&relatedFilterHandler{
relatedIDCol: "scenes_galleries.scene_id",
relatedRepo: sceneRepository.repository,

View file

@ -160,7 +160,10 @@ func Test_galleryQueryBuilder_Create(t *testing.T) {
fileIDs = []models.FileID{s.Files.List()[0].Base().ID}
}
if err := qb.Create(ctx, &s, fileIDs); (err != nil) != tt.wantErr {
if err := qb.Create(ctx, &models.CreateGalleryInput{
Gallery: &s,
FileIDs: fileIDs,
}); (err != nil) != tt.wantErr {
t.Errorf("galleryQueryBuilder.Create() error = %v, wantErr = %v", err, tt.wantErr)
}
@ -360,7 +363,9 @@ func Test_galleryQueryBuilder_Update(t *testing.T) {
copy := *tt.updatedObject
if err := qb.Update(ctx, tt.updatedObject); (err != nil) != tt.wantErr {
if err := qb.Update(ctx, &models.UpdateGalleryInput{
Gallery: tt.updatedObject,
}); (err != nil) != tt.wantErr {
t.Errorf("galleryQueryBuilder.Update() error = %v, wantErr %v", err, tt.wantErr)
}
@ -3001,6 +3006,245 @@ func TestGallerySetAndResetCover(t *testing.T) {
})
}
func TestGalleryQueryCustomFields(t *testing.T) {
tests := []struct {
name string
filter *models.GalleryFilterType
includeIdxs []int
excludeIdxs []int
wantErr bool
}{
{
"equals",
&models.GalleryFilterType{
CustomFields: []models.CustomFieldCriterionInput{
{
Field: "string",
Modifier: models.CriterionModifierEquals,
Value: []any{getGalleryStringValue(galleryIdxWithImage, "custom")},
},
},
},
[]int{galleryIdxWithImage},
nil,
false,
},
{
"not equals",
&models.GalleryFilterType{
Title: &models.StringCriterionInput{
Value: getGalleryStringValue(galleryIdxWithImage, titleField),
Modifier: models.CriterionModifierEquals,
},
CustomFields: []models.CustomFieldCriterionInput{
{
Field: "string",
Modifier: models.CriterionModifierNotEquals,
Value: []any{getGalleryStringValue(galleryIdxWithImage, "custom")},
},
},
},
nil,
[]int{galleryIdxWithImage},
false,
},
{
"includes",
&models.GalleryFilterType{
CustomFields: []models.CustomFieldCriterionInput{
{
Field: "string",
Modifier: models.CriterionModifierIncludes,
Value: []any{getGalleryStringValue(galleryIdxWithImage, "custom")[9:]},
},
},
},
[]int{galleryIdxWithImage},
nil,
false,
},
{
"excludes",
&models.GalleryFilterType{
Title: &models.StringCriterionInput{
Value: getGalleryStringValue(galleryIdxWithImage, titleField),
Modifier: models.CriterionModifierEquals,
},
CustomFields: []models.CustomFieldCriterionInput{
{
Field: "string",
Modifier: models.CriterionModifierExcludes,
Value: []any{getGalleryStringValue(galleryIdxWithImage, "custom")[9:]},
},
},
},
nil,
[]int{galleryIdxWithImage},
false,
},
{
"regex",
&models.GalleryFilterType{
CustomFields: []models.CustomFieldCriterionInput{
{
Field: "string",
Modifier: models.CriterionModifierMatchesRegex,
Value: []any{".*17_custom"},
},
},
},
[]int{galleryIdxWithPerformerTag},
nil,
false,
},
{
"invalid regex",
&models.GalleryFilterType{
CustomFields: []models.CustomFieldCriterionInput{
{
Field: "string",
Modifier: models.CriterionModifierMatchesRegex,
Value: []any{"["},
},
},
},
nil,
nil,
true,
},
{
"not matches regex",
&models.GalleryFilterType{
Title: &models.StringCriterionInput{
Value: getGalleryStringValue(galleryIdxWithPerformerTag, titleField),
Modifier: models.CriterionModifierEquals,
},
CustomFields: []models.CustomFieldCriterionInput{
{
Field: "string",
Modifier: models.CriterionModifierNotMatchesRegex,
Value: []any{".*17_custom"},
},
},
},
nil,
[]int{galleryIdxWithPerformerTag},
false,
},
{
"invalid not matches regex",
&models.GalleryFilterType{
CustomFields: []models.CustomFieldCriterionInput{
{
Field: "string",
Modifier: models.CriterionModifierNotMatchesRegex,
Value: []any{"["},
},
},
},
nil,
nil,
true,
},
{
"null",
&models.GalleryFilterType{
Title: &models.StringCriterionInput{
Value: getGalleryStringValue(galleryIdxWithImage, titleField),
Modifier: models.CriterionModifierEquals,
},
CustomFields: []models.CustomFieldCriterionInput{
{
Field: "not existing",
Modifier: models.CriterionModifierIsNull,
},
},
},
[]int{galleryIdxWithImage},
nil,
false,
},
{
"not null",
&models.GalleryFilterType{
Title: &models.StringCriterionInput{
Value: getGalleryStringValue(galleryIdxWithImage, titleField),
Modifier: models.CriterionModifierEquals,
},
CustomFields: []models.CustomFieldCriterionInput{
{
Field: "string",
Modifier: models.CriterionModifierNotNull,
},
},
},
[]int{galleryIdxWithImage},
nil,
false,
},
{
"between",
&models.GalleryFilterType{
CustomFields: []models.CustomFieldCriterionInput{
{
Field: "real",
Modifier: models.CriterionModifierBetween,
Value: []any{0.15, 0.25},
},
},
},
[]int{galleryIdxWithImage},
nil,
false,
},
{
"not between",
&models.GalleryFilterType{
Title: &models.StringCriterionInput{
Value: getGalleryStringValue(galleryIdxWithImage, titleField),
Modifier: models.CriterionModifierEquals,
},
CustomFields: []models.CustomFieldCriterionInput{
{
Field: "real",
Modifier: models.CriterionModifierNotBetween,
Value: []any{0.15, 0.25},
},
},
},
nil,
[]int{galleryIdxWithImage},
false,
},
}
for _, tt := range tests {
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
assert := assert.New(t)
galleries, _, err := db.Gallery.Query(ctx, tt.filter, nil)
if (err != nil) != tt.wantErr {
t.Errorf("GalleryStore.Query() error = %v, wantErr %v", err, tt.wantErr)
}
if err != nil {
return
}
ids := galleriesToIDs(galleries)
include := indexesToIDs(galleryIDs, tt.includeIdxs)
exclude := indexesToIDs(galleryIDs, tt.excludeIdxs)
for _, i := range include {
assert.Contains(ids, i)
}
for _, e := range exclude {
assert.NotContains(ids, e)
}
})
}
}
// TODO Count
// TODO All
// TODO Query

View file

@ -0,0 +1,9 @@
CREATE TABLE `gallery_custom_fields` (
`gallery_id` integer NOT NULL,
`field` varchar(64) NOT NULL,
`value` BLOB NOT NULL,
PRIMARY KEY (`gallery_id`, `field`),
foreign key(`gallery_id`) references `galleries`(`id`) on delete CASCADE
);
CREATE INDEX `index_gallery_custom_fields_field_value` ON `gallery_custom_fields` (`field`, `value`);

View file

@ -1389,6 +1389,18 @@ func makeGallery(i int, includeScenes bool) *models.Gallery {
return ret
}
func getGalleryCustomFields(index int) map[string]interface{} {
if index%5 == 0 {
return nil
}
return map[string]interface{}{
"string": getGalleryStringValue(index, "custom"),
"int": int64(index % 5),
"real": float64(index) / 10,
}
}
func createGalleries(ctx context.Context, n int) error {
gqb := db.Gallery
fqb := db.File
@ -1410,7 +1422,11 @@ func createGalleries(ctx context.Context, n int) error {
const includeScenes = false
gallery := makeGallery(i, includeScenes)
err := gqb.Create(ctx, gallery, fileIDs)
err := gqb.Create(ctx, &models.CreateGalleryInput{
Gallery: gallery,
FileIDs: fileIDs,
CustomFields: getGalleryCustomFields(i),
})
if err != nil {
return fmt.Errorf("Error creating gallery %v+: %s", gallery, err.Error())

View file

@ -20,6 +20,7 @@ var (
performersGalleriesJoinTable = goqu.T(performersGalleriesTable)
galleriesScenesJoinTable = goqu.T(galleriesScenesTable)
galleriesURLsJoinTable = goqu.T(galleriesURLsTable)
galleriesCustomFieldsTable = goqu.T("gallery_custom_fields")
scenesFilesJoinTable = goqu.T(scenesFilesTable)
scenesTagsJoinTable = goqu.T(scenesTagsTable)