mirror of
https://github.com/stashapp/stash.git
synced 2026-02-28 02:02:57 +01:00
Backend support for Group custom fields (#6596)
This commit is contained in:
parent
47dcdd439c
commit
ca5178f05e
27 changed files with 725 additions and 82 deletions
|
|
@ -462,6 +462,9 @@ input GroupFilterType {
|
|||
scenes_filter: SceneFilterType
|
||||
"Filter by related studios that meet this criteria"
|
||||
studios_filter: StudioFilterType
|
||||
|
||||
"Filter by custom fields"
|
||||
custom_fields: [CustomFieldCriterionInput!]
|
||||
}
|
||||
|
||||
input StudioFilterType {
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ type Group {
|
|||
sub_group_count(depth: Int): Int! # Resolver
|
||||
scenes: [Scene!]!
|
||||
o_counter: Int # Resolver
|
||||
custom_fields: Map!
|
||||
}
|
||||
|
||||
input GroupDescriptionInput {
|
||||
|
|
@ -59,6 +60,8 @@ input GroupCreateInput {
|
|||
front_image: String
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
back_image: String
|
||||
|
||||
custom_fields: Map
|
||||
}
|
||||
|
||||
input GroupUpdateInput {
|
||||
|
|
@ -82,6 +85,8 @@ input GroupUpdateInput {
|
|||
front_image: String
|
||||
"This should be a URL or a base64 encoded data URL"
|
||||
back_image: String
|
||||
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
|
||||
input BulkUpdateGroupDescriptionsInput {
|
||||
|
|
@ -101,6 +106,8 @@ input BulkGroupUpdateInput {
|
|||
|
||||
containing_groups: BulkUpdateGroupDescriptionsInput
|
||||
sub_groups: BulkUpdateGroupDescriptionsInput
|
||||
|
||||
custom_fields: CustomFieldsInput
|
||||
}
|
||||
|
||||
input GroupDestroyInput {
|
||||
|
|
|
|||
|
|
@ -64,11 +64,12 @@ type Loaders struct {
|
|||
StudioByID *StudioLoader
|
||||
StudioCustomFields *CustomFieldsLoader
|
||||
|
||||
TagByID *TagLoader
|
||||
TagCustomFields *CustomFieldsLoader
|
||||
GroupByID *GroupLoader
|
||||
FileByID *FileLoader
|
||||
FolderByID *FolderLoader
|
||||
TagByID *TagLoader
|
||||
TagCustomFields *CustomFieldsLoader
|
||||
GroupByID *GroupLoader
|
||||
GroupCustomFields *CustomFieldsLoader
|
||||
FileByID *FileLoader
|
||||
FolderByID *FolderLoader
|
||||
}
|
||||
|
||||
type Middleware struct {
|
||||
|
|
@ -139,6 +140,11 @@ func (m Middleware) Middleware(next http.Handler) http.Handler {
|
|||
maxBatch: maxBatch,
|
||||
fetch: m.fetchGroups(ctx),
|
||||
},
|
||||
GroupCustomFields: &CustomFieldsLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
fetch: m.fetchGroupCustomFields(ctx),
|
||||
},
|
||||
FileByID: &FileLoader{
|
||||
wait: wait,
|
||||
maxBatch: maxBatch,
|
||||
|
|
@ -325,6 +331,18 @@ func (m Middleware) fetchTagCustomFields(ctx context.Context) func(keys []int) (
|
|||
}
|
||||
}
|
||||
|
||||
func (m Middleware) fetchGroupCustomFields(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.Group.GetCustomFieldsBulk(ctx, keys)
|
||||
return err
|
||||
})
|
||||
|
||||
return ret, toErrorSlice(err)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -215,3 +215,16 @@ func (r *groupResolver) OCounter(ctx context.Context, obj *models.Group) (ret *i
|
|||
}
|
||||
return &count, nil
|
||||
}
|
||||
|
||||
func (r *groupResolver) CustomFields(ctx context.Context, obj *models.Group) (map[string]interface{}, error) {
|
||||
m, err := loaders.From(ctx).GroupCustomFields.Load(obj.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if m == nil {
|
||||
return make(map[string]interface{}), nil
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,13 +14,17 @@ import (
|
|||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
func groupFromGroupCreateInput(ctx context.Context, input GroupCreateInput) (*models.Group, error) {
|
||||
func groupFromGroupCreateInput(ctx context.Context, input GroupCreateInput) (*models.CreateGroupInput, error) {
|
||||
translator := changesetTranslator{
|
||||
inputMap: getUpdateInputMap(ctx),
|
||||
}
|
||||
|
||||
// Populate a new group from the input
|
||||
newGroup := models.NewGroup()
|
||||
newGroupInput := &models.CreateGroupInput{
|
||||
Group: &models.Group{},
|
||||
}
|
||||
*newGroupInput.Group = models.NewGroup()
|
||||
newGroup := newGroupInput.Group
|
||||
|
||||
newGroup.Name = strings.TrimSpace(input.Name)
|
||||
newGroup.Aliases = translator.string(input.Aliases)
|
||||
|
|
@ -59,28 +63,19 @@ func groupFromGroupCreateInput(ctx context.Context, input GroupCreateInput) (*mo
|
|||
newGroup.URLs = models.NewRelatedStrings(stringslice.TrimSpace(input.Urls))
|
||||
}
|
||||
|
||||
return &newGroup, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GroupCreate(ctx context.Context, input GroupCreateInput) (*models.Group, error) {
|
||||
newGroup, err := groupFromGroupCreateInput(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
newGroupInput.CustomFields = convertMapJSONNumbers(input.CustomFields)
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
var frontimageData []byte
|
||||
if input.FrontImage != nil {
|
||||
frontimageData, err = utils.ProcessImageInput(ctx, *input.FrontImage)
|
||||
newGroupInput.FrontImageData, err = utils.ProcessImageInput(ctx, *input.FrontImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("processing front image: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Process the base 64 encoded image string
|
||||
var backimageData []byte
|
||||
if input.BackImage != nil {
|
||||
backimageData, err = utils.ProcessImageInput(ctx, *input.BackImage)
|
||||
newGroupInput.BackImageData, err = utils.ProcessImageInput(ctx, *input.BackImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("processing back image: %w", err)
|
||||
}
|
||||
|
|
@ -88,13 +83,22 @@ func (r *mutationResolver) GroupCreate(ctx context.Context, input GroupCreateInp
|
|||
|
||||
// HACK: if back image is being set, set the front image to the default.
|
||||
// This is because we can't have a null front image with a non-null back image.
|
||||
if len(frontimageData) == 0 && len(backimageData) != 0 {
|
||||
frontimageData = static.ReadAll(static.DefaultGroupImage)
|
||||
if len(newGroupInput.FrontImageData) == 0 && len(newGroupInput.BackImageData) != 0 {
|
||||
newGroupInput.FrontImageData = static.ReadAll(static.DefaultGroupImage)
|
||||
}
|
||||
|
||||
return newGroupInput, nil
|
||||
}
|
||||
|
||||
func (r *mutationResolver) GroupCreate(ctx context.Context, input GroupCreateInput) (*models.Group, error) {
|
||||
createGroupInput, err := groupFromGroupCreateInput(ctx, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Start the transaction and save the group
|
||||
if err := r.withTxn(ctx, func(ctx context.Context) error {
|
||||
if err = r.groupService.Create(ctx, newGroup, frontimageData, backimageData); err != nil {
|
||||
if err = r.groupService.Create(ctx, createGroupInput); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -104,9 +108,9 @@ func (r *mutationResolver) GroupCreate(ctx context.Context, input GroupCreateInp
|
|||
}
|
||||
|
||||
// for backwards compatibility - run both movie and group hooks
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newGroup.ID, hook.GroupCreatePost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, newGroup.ID, hook.MovieCreatePost, input, nil)
|
||||
return r.getGroup(ctx, newGroup.ID)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, createGroupInput.Group.ID, hook.GroupCreatePost, input, nil)
|
||||
r.hookExecutor.ExecutePostHooks(ctx, createGroupInput.Group.ID, hook.MovieCreatePost, input, nil)
|
||||
return r.getGroup(ctx, createGroupInput.Group.ID)
|
||||
}
|
||||
|
||||
func groupPartialFromGroupUpdateInput(translator changesetTranslator, input GroupUpdateInput) (ret models.GroupPartial, err error) {
|
||||
|
|
@ -150,6 +154,12 @@ func groupPartialFromGroupUpdateInput(translator changesetTranslator, input Grou
|
|||
}
|
||||
|
||||
updatedGroup.URLs = translator.updateStrings(input.Urls, "urls")
|
||||
if input.CustomFields != nil {
|
||||
updatedGroup.CustomFields = *input.CustomFields
|
||||
// convert json.Numbers to int/float
|
||||
updatedGroup.CustomFields.Full = convertMapJSONNumbers(updatedGroup.CustomFields.Full)
|
||||
updatedGroup.CustomFields.Partial = convertMapJSONNumbers(updatedGroup.CustomFields.Partial)
|
||||
}
|
||||
|
||||
return updatedGroup, nil
|
||||
}
|
||||
|
|
@ -246,6 +256,13 @@ func groupPartialFromBulkGroupUpdateInput(translator changesetTranslator, input
|
|||
|
||||
updatedGroup.URLs = translator.optionalURLsBulk(input.Urls, nil)
|
||||
|
||||
if input.CustomFields != nil {
|
||||
updatedGroup.CustomFields = *input.CustomFields
|
||||
// convert json.Numbers to int/float
|
||||
updatedGroup.CustomFields.Full = convertMapJSONNumbers(updatedGroup.CustomFields.Full)
|
||||
updatedGroup.CustomFields.Partial = convertMapJSONNumbers(updatedGroup.CustomFields.Partial)
|
||||
}
|
||||
|
||||
return updatedGroup, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ type GalleryService interface {
|
|||
}
|
||||
|
||||
type GroupService interface {
|
||||
Create(ctx context.Context, group *models.Group, frontimageData []byte, backimageData []byte) error
|
||||
Create(ctx context.Context, input *models.CreateGroupInput) error
|
||||
UpdatePartial(ctx context.Context, id int, updatedGroup models.GroupPartial, frontImage group.ImageInput, backImage group.ImageInput) (*models.Group, error)
|
||||
|
||||
AddSubGroups(ctx context.Context, groupID int, subGroups []models.GroupIDDescription, insertIndex *int) error
|
||||
|
|
|
|||
|
|
@ -12,27 +12,37 @@ var (
|
|||
ErrHierarchyLoop = errors.New("a group cannot be contained by one of its subgroups")
|
||||
)
|
||||
|
||||
func (s *Service) Create(ctx context.Context, group *models.Group, frontimageData []byte, backimageData []byte) error {
|
||||
func (s *Service) Create(ctx context.Context, input *models.CreateGroupInput) error {
|
||||
r := s.Repository
|
||||
group := input.Group
|
||||
|
||||
if err := s.validateCreate(ctx, group); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := r.Create(ctx, group)
|
||||
err := r.Create(ctx, input.Group)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update image table
|
||||
if len(frontimageData) > 0 {
|
||||
if err := r.UpdateFrontImage(ctx, group.ID, frontimageData); err != nil {
|
||||
// set custom fields
|
||||
if len(input.CustomFields) > 0 {
|
||||
if err := r.SetCustomFields(ctx, group.ID, models.CustomFieldsInput{
|
||||
Full: input.CustomFields,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(backimageData) > 0 {
|
||||
if err := r.UpdateBackImage(ctx, group.ID, backimageData); err != nil {
|
||||
// update image table
|
||||
if len(input.FrontImageData) > 0 {
|
||||
if err := r.UpdateFrontImage(ctx, group.ID, input.FrontImageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(input.BackImageData) > 0 {
|
||||
if err := r.UpdateBackImage(ctx, group.ID, input.BackImageData); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,61 +11,67 @@ import (
|
|||
"github.com/stashapp/stash/pkg/utils"
|
||||
)
|
||||
|
||||
type ImageGetter interface {
|
||||
GetFrontImage(ctx context.Context, movieID int) ([]byte, error)
|
||||
GetBackImage(ctx context.Context, movieID int) ([]byte, error)
|
||||
type GroupExportReader interface {
|
||||
GetFrontImage(ctx context.Context, groupID int) ([]byte, error)
|
||||
GetBackImage(ctx context.Context, groupID int) ([]byte, error)
|
||||
GetCustomFields(ctx context.Context, groupID int) (map[string]interface{}, error)
|
||||
}
|
||||
|
||||
// ToJSON converts a Movie into its JSON equivalent.
|
||||
func ToJSON(ctx context.Context, reader ImageGetter, studioReader models.StudioGetter, movie *models.Group) (*jsonschema.Group, error) {
|
||||
newMovieJSON := jsonschema.Group{
|
||||
Name: movie.Name,
|
||||
Aliases: movie.Aliases,
|
||||
Director: movie.Director,
|
||||
Synopsis: movie.Synopsis,
|
||||
URLs: movie.URLs.List(),
|
||||
CreatedAt: json.JSONTime{Time: movie.CreatedAt},
|
||||
UpdatedAt: json.JSONTime{Time: movie.UpdatedAt},
|
||||
// ToJSON converts a Group into its JSON equivalent.
|
||||
func ToJSON(ctx context.Context, reader GroupExportReader, studioReader models.StudioGetter, group *models.Group) (*jsonschema.Group, error) {
|
||||
newGroupJSON := jsonschema.Group{
|
||||
Name: group.Name,
|
||||
Aliases: group.Aliases,
|
||||
Director: group.Director,
|
||||
Synopsis: group.Synopsis,
|
||||
URLs: group.URLs.List(),
|
||||
CreatedAt: json.JSONTime{Time: group.CreatedAt},
|
||||
UpdatedAt: json.JSONTime{Time: group.UpdatedAt},
|
||||
}
|
||||
|
||||
if movie.Date != nil {
|
||||
newMovieJSON.Date = movie.Date.String()
|
||||
if group.Date != nil {
|
||||
newGroupJSON.Date = group.Date.String()
|
||||
}
|
||||
if movie.Rating != nil {
|
||||
newMovieJSON.Rating = *movie.Rating
|
||||
if group.Rating != nil {
|
||||
newGroupJSON.Rating = *group.Rating
|
||||
}
|
||||
if movie.Duration != nil {
|
||||
newMovieJSON.Duration = *movie.Duration
|
||||
if group.Duration != nil {
|
||||
newGroupJSON.Duration = *group.Duration
|
||||
}
|
||||
|
||||
if movie.StudioID != nil {
|
||||
studio, err := studioReader.Find(ctx, *movie.StudioID)
|
||||
if group.StudioID != nil {
|
||||
studio, err := studioReader.Find(ctx, *group.StudioID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error getting movie studio: %v", err)
|
||||
}
|
||||
|
||||
if studio != nil {
|
||||
newMovieJSON.Studio = studio.Name
|
||||
newGroupJSON.Studio = studio.Name
|
||||
}
|
||||
}
|
||||
|
||||
frontImage, err := reader.GetFrontImage(ctx, movie.ID)
|
||||
frontImage, err := reader.GetFrontImage(ctx, group.ID)
|
||||
if err != nil {
|
||||
logger.Errorf("Error getting movie front image: %v", err)
|
||||
}
|
||||
|
||||
if len(frontImage) > 0 {
|
||||
newMovieJSON.FrontImage = utils.GetBase64StringFromData(frontImage)
|
||||
newGroupJSON.FrontImage = utils.GetBase64StringFromData(frontImage)
|
||||
}
|
||||
|
||||
backImage, err := reader.GetBackImage(ctx, movie.ID)
|
||||
backImage, err := reader.GetBackImage(ctx, group.ID)
|
||||
if err != nil {
|
||||
logger.Errorf("Error getting movie back image: %v", err)
|
||||
}
|
||||
|
||||
if len(backImage) > 0 {
|
||||
newMovieJSON.BackImage = utils.GetBase64StringFromData(backImage)
|
||||
newGroupJSON.BackImage = utils.GetBase64StringFromData(backImage)
|
||||
}
|
||||
|
||||
return &newMovieJSON, nil
|
||||
newGroupJSON.CustomFields, err = reader.GetCustomFields(ctx, group.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting group custom fields: %v", err)
|
||||
}
|
||||
|
||||
return &newGroupJSON, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,24 +8,26 @@ import (
|
|||
"github.com/stashapp/stash/pkg/models/jsonschema"
|
||||
"github.com/stashapp/stash/pkg/models/mocks"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
movieID = 1
|
||||
emptyID = 2
|
||||
errFrontImageID = 3
|
||||
errBackImageID = 4
|
||||
errStudioMovieID = 5
|
||||
missingStudioMovieID = 6
|
||||
movieID = iota + 1
|
||||
emptyID
|
||||
errFrontImageID
|
||||
errBackImageID
|
||||
errStudioMovieID
|
||||
missingStudioMovieID
|
||||
errCustomFieldsID
|
||||
)
|
||||
|
||||
const (
|
||||
studioID = 1
|
||||
missingStudioID = 2
|
||||
errStudioID = 3
|
||||
studioID = iota + 1
|
||||
missingStudioID
|
||||
errStudioID
|
||||
)
|
||||
|
||||
const movieName = "testMovie"
|
||||
|
|
@ -51,6 +53,11 @@ const (
|
|||
var (
|
||||
frontImageBytes = []byte("frontImageBytes")
|
||||
backImageBytes = []byte("backImageBytes")
|
||||
|
||||
emptyCustomFields = make(map[string]interface{})
|
||||
customFields = map[string]interface{}{
|
||||
"customField1": "customValue1",
|
||||
}
|
||||
)
|
||||
|
||||
var movieStudio models.Studio = models.Studio{
|
||||
|
|
@ -88,7 +95,7 @@ func createEmptyMovie(id int) models.Group {
|
|||
}
|
||||
}
|
||||
|
||||
func createFullJSONMovie(studio, frontImage, backImage string) *jsonschema.Group {
|
||||
func createFullJSONMovie(studio, frontImage, backImage string, customFields map[string]interface{}) *jsonschema.Group {
|
||||
return &jsonschema.Group{
|
||||
Name: movieName,
|
||||
Aliases: movieAliases,
|
||||
|
|
@ -107,6 +114,7 @@ func createFullJSONMovie(studio, frontImage, backImage string) *jsonschema.Group
|
|||
UpdatedAt: json.JSONTime{
|
||||
Time: updateTime,
|
||||
},
|
||||
CustomFields: customFields,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -119,13 +127,15 @@ func createEmptyJSONMovie() *jsonschema.Group {
|
|||
UpdatedAt: json.JSONTime{
|
||||
Time: updateTime,
|
||||
},
|
||||
CustomFields: emptyCustomFields,
|
||||
}
|
||||
}
|
||||
|
||||
type testScenario struct {
|
||||
movie models.Group
|
||||
expected *jsonschema.Group
|
||||
err bool
|
||||
movie models.Group
|
||||
customFields map[string]interface{}
|
||||
expected *jsonschema.Group
|
||||
err bool
|
||||
}
|
||||
|
||||
var scenarios []testScenario
|
||||
|
|
@ -134,36 +144,48 @@ func initTestTable() {
|
|||
scenarios = []testScenario{
|
||||
{
|
||||
createFullMovie(movieID, studioID),
|
||||
createFullJSONMovie(studioName, frontImage, backImage),
|
||||
customFields,
|
||||
createFullJSONMovie(studioName, frontImage, backImage, customFields),
|
||||
false,
|
||||
},
|
||||
{
|
||||
createEmptyMovie(emptyID),
|
||||
emptyCustomFields,
|
||||
createEmptyJSONMovie(),
|
||||
false,
|
||||
},
|
||||
{
|
||||
createFullMovie(errFrontImageID, studioID),
|
||||
createFullJSONMovie(studioName, "", backImage),
|
||||
emptyCustomFields,
|
||||
createFullJSONMovie(studioName, "", backImage, emptyCustomFields),
|
||||
// failure to get front image should not cause error
|
||||
false,
|
||||
},
|
||||
{
|
||||
createFullMovie(errBackImageID, studioID),
|
||||
createFullJSONMovie(studioName, frontImage, ""),
|
||||
emptyCustomFields,
|
||||
createFullJSONMovie(studioName, frontImage, "", emptyCustomFields),
|
||||
// failure to get back image should not cause error
|
||||
false,
|
||||
},
|
||||
{
|
||||
createFullMovie(errStudioMovieID, errStudioID),
|
||||
emptyCustomFields,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
createFullMovie(missingStudioMovieID, missingStudioID),
|
||||
createFullJSONMovie("", frontImage, backImage),
|
||||
emptyCustomFields,
|
||||
createFullJSONMovie("", frontImage, backImage, emptyCustomFields),
|
||||
false,
|
||||
},
|
||||
{
|
||||
createFullMovie(errCustomFieldsID, studioID),
|
||||
customFields,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -179,6 +201,7 @@ func TestToJSON(t *testing.T) {
|
|||
db.Group.On("GetFrontImage", testCtx, emptyID).Return(nil, nil).Once().Maybe()
|
||||
db.Group.On("GetFrontImage", testCtx, errFrontImageID).Return(nil, imageErr).Once()
|
||||
db.Group.On("GetFrontImage", testCtx, errBackImageID).Return(frontImageBytes, nil).Once()
|
||||
db.Group.On("GetFrontImage", testCtx, errCustomFieldsID).Return(nil, nil).Once()
|
||||
|
||||
db.Group.On("GetBackImage", testCtx, movieID).Return(backImageBytes, nil).Once()
|
||||
db.Group.On("GetBackImage", testCtx, missingStudioMovieID).Return(backImageBytes, nil).Once()
|
||||
|
|
@ -186,6 +209,11 @@ func TestToJSON(t *testing.T) {
|
|||
db.Group.On("GetBackImage", testCtx, errBackImageID).Return(nil, imageErr).Once()
|
||||
db.Group.On("GetBackImage", testCtx, errFrontImageID).Return(backImageBytes, nil).Maybe()
|
||||
db.Group.On("GetBackImage", testCtx, errStudioMovieID).Return(backImageBytes, nil).Maybe()
|
||||
db.Group.On("GetBackImage", testCtx, errCustomFieldsID).Return(nil, nil).Once()
|
||||
|
||||
db.Group.On("GetCustomFields", testCtx, movieID).Return(customFields, nil).Once()
|
||||
db.Group.On("GetCustomFields", testCtx, errCustomFieldsID).Return(nil, errors.New("error getting custom fields")).Once()
|
||||
db.Group.On("GetCustomFields", testCtx, mock.Anything).Return(emptyCustomFields, nil).Times(4)
|
||||
|
||||
studioErr := errors.New("error getting studio")
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
type ImporterReaderWriter interface {
|
||||
models.GroupCreatorUpdater
|
||||
models.CustomFieldsWriter
|
||||
FindByName(ctx context.Context, name string, nocase bool) (*models.Group, error)
|
||||
}
|
||||
|
||||
|
|
@ -233,6 +234,14 @@ func (i *Importer) PostImport(ctx context.Context, id int) error {
|
|||
}
|
||||
}
|
||||
|
||||
if len(i.Input.CustomFields) > 0 {
|
||||
if err := i.ReaderWriter.SetCustomFields(ctx, id, models.CustomFieldsInput{
|
||||
Full: i.Input.CustomFields,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("error setting custom fields: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(i.frontImageData) > 0 {
|
||||
if err := i.ReaderWriter.UpdateFrontImage(ctx, id, i.frontImageData); err != nil {
|
||||
return fmt.Errorf("error setting group front image: %v", err)
|
||||
|
|
|
|||
|
|
@ -259,17 +259,29 @@ func TestImporterPostImport(t *testing.T) {
|
|||
db := mocks.NewDatabase()
|
||||
|
||||
i := Importer{
|
||||
ReaderWriter: db.Group,
|
||||
StudioWriter: db.Studio,
|
||||
ReaderWriter: db.Group,
|
||||
StudioWriter: db.Studio,
|
||||
Input: jsonschema.Group{
|
||||
CustomFields: customFields,
|
||||
},
|
||||
frontImageData: frontImageBytes,
|
||||
backImageData: backImageBytes,
|
||||
}
|
||||
|
||||
updateMovieImageErr := errors.New("UpdateImages error")
|
||||
customFieldsErr := errors.New("SetCustomFields error")
|
||||
|
||||
customFieldsInput := models.CustomFieldsInput{
|
||||
Full: customFields,
|
||||
}
|
||||
|
||||
db.Group.On("UpdateFrontImage", testCtx, movieID, frontImageBytes).Return(nil).Once()
|
||||
db.Group.On("UpdateBackImage", testCtx, movieID, backImageBytes).Return(nil).Once()
|
||||
db.Group.On("UpdateFrontImage", testCtx, errImageID, frontImageBytes).Return(updateMovieImageErr).Once()
|
||||
db.Group.On("UpdateBackImage", testCtx, movieID, backImageBytes).Return(nil).Once()
|
||||
|
||||
db.Group.On("SetCustomFields", testCtx, movieID, customFieldsInput).Return(nil).Once()
|
||||
db.Group.On("SetCustomFields", testCtx, errImageID, customFieldsInput).Return(nil).Once()
|
||||
db.Group.On("SetCustomFields", testCtx, errCustomFieldsID, customFieldsInput).Return(customFieldsErr).Once()
|
||||
|
||||
err := i.PostImport(testCtx, movieID)
|
||||
assert.Nil(t, err)
|
||||
|
|
@ -277,6 +289,9 @@ func TestImporterPostImport(t *testing.T) {
|
|||
err = i.PostImport(testCtx, errImageID)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
err = i.PostImport(testCtx, errCustomFieldsID)
|
||||
assert.NotNil(t, err)
|
||||
|
||||
db.AssertExpectations(t)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ type CreatorUpdater interface {
|
|||
models.GroupGetter
|
||||
models.GroupCreator
|
||||
models.GroupUpdater
|
||||
models.CustomFieldsWriter
|
||||
|
||||
models.ContainingGroupLoader
|
||||
models.SubGroupLoader
|
||||
|
|
|
|||
|
|
@ -43,4 +43,6 @@ type GroupFilterType struct {
|
|||
CreatedAt *TimestampCriterionInput `json:"created_at"`
|
||||
// Filter by updated at
|
||||
UpdatedAt *TimestampCriterionInput `json:"updated_at"`
|
||||
// Filter by custom fields
|
||||
CustomFields []CustomFieldCriterionInput `json:"custom_fields"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ type Group struct {
|
|||
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"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -312,6 +312,52 @@ func (_m *GroupReaderWriter) GetContainingGroupDescriptions(ctx context.Context,
|
|||
return r0, r1
|
||||
}
|
||||
|
||||
// GetCustomFields provides a mock function with given fields: ctx, id
|
||||
func (_m *GroupReaderWriter) 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 *GroupReaderWriter) 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
|
||||
}
|
||||
|
||||
// GetFrontImage provides a mock function with given fields: ctx, groupID
|
||||
func (_m *GroupReaderWriter) GetFrontImage(ctx context.Context, groupID int) ([]byte, error) {
|
||||
ret := _m.Called(ctx, groupID)
|
||||
|
|
@ -497,6 +543,20 @@ func (_m *GroupReaderWriter) QueryCount(ctx context.Context, groupFilter *models
|
|||
return r0, r1
|
||||
}
|
||||
|
||||
// SetCustomFields provides a mock function with given fields: ctx, id, fields
|
||||
func (_m *GroupReaderWriter) 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, updatedGroup
|
||||
func (_m *GroupReaderWriter) Update(ctx context.Context, updatedGroup *models.Group) error {
|
||||
ret := _m.Called(ctx, updatedGroup)
|
||||
|
|
|
|||
|
|
@ -34,6 +34,14 @@ func NewGroup() Group {
|
|||
}
|
||||
}
|
||||
|
||||
type CreateGroupInput struct {
|
||||
*Group
|
||||
|
||||
CustomFields map[string]interface{} `json:"custom_fields"`
|
||||
FrontImageData []byte
|
||||
BackImageData []byte
|
||||
}
|
||||
|
||||
func (m *Group) LoadURLs(ctx context.Context, l URLLoader) error {
|
||||
return m.URLs.load(func() ([]string, error) {
|
||||
return l.GetURLs(ctx, m.ID)
|
||||
|
|
@ -74,6 +82,8 @@ type GroupPartial struct {
|
|||
SubGroups *UpdateGroupDescriptions
|
||||
CreatedAt OptionalTime
|
||||
UpdatedAt OptionalTime
|
||||
|
||||
CustomFields CustomFieldsInput
|
||||
}
|
||||
|
||||
func NewGroupPartial() GroupPartial {
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ type GroupReader interface {
|
|||
TagIDLoader
|
||||
ContainingGroupLoader
|
||||
SubGroupLoader
|
||||
CustomFieldsReader
|
||||
|
||||
All(ctx context.Context) ([]*Group, error)
|
||||
GetFrontImage(ctx context.Context, groupID int) ([]byte, error)
|
||||
|
|
@ -81,6 +82,7 @@ type GroupWriter interface {
|
|||
GroupCreator
|
||||
GroupUpdater
|
||||
GroupDestroyer
|
||||
CustomFieldsWriter
|
||||
}
|
||||
|
||||
// GroupReaderWriter provides all group methods.
|
||||
|
|
|
|||
|
|
@ -964,6 +964,10 @@ func (db *Anonymiser) anonymiseGroups(ctx context.Context) error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := db.anonymiseCustomFields(ctx, goqu.T(groupsCustomFieldsTable.GetTable()), "group_id"); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -242,7 +242,13 @@ func TestSceneSetCustomFields(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestGallerySetCustomFields(t *testing.T) {
|
||||
galleryIdx := galleryIdxWithScene
|
||||
galleryIdx := galleryIdxWithChapters
|
||||
|
||||
testSetCustomFields(t, "Gallery", db.Gallery, galleryIDs[galleryIdx], getGalleryCustomFields(galleryIdx))
|
||||
}
|
||||
|
||||
func TestGroupSetCustomFields(t *testing.T) {
|
||||
groupIdx := groupIdxWithScene
|
||||
|
||||
testSetCustomFields(t, "Group", db.Group, groupIDs[groupIdx], getGroupCustomFields(groupIdx))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ const (
|
|||
cacheSizeEnv = "STASH_SQLITE_CACHE_SIZE"
|
||||
)
|
||||
|
||||
var appSchemaVersion uint = 81
|
||||
var appSchemaVersion uint = 82
|
||||
|
||||
//go:embed migrations/*.sql
|
||||
var migrationsBox embed.FS
|
||||
|
|
|
|||
|
|
@ -831,6 +831,79 @@ func Test_galleryQueryBuilder_UpdatePartialRelationships(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_GalleryStore_UpdatePartialCustomFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
id int
|
||||
partial models.GalleryPartial
|
||||
expected map[string]interface{} // nil to use the partial
|
||||
}{
|
||||
{
|
||||
"set custom fields",
|
||||
galleryIDs[galleryIdx1WithImage],
|
||||
models.GalleryPartial{
|
||||
CustomFields: models.CustomFieldsInput{
|
||||
Full: testCustomFields,
|
||||
},
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"clear custom fields",
|
||||
galleryIDs[galleryIdx1WithImage],
|
||||
models.GalleryPartial{
|
||||
CustomFields: models.CustomFieldsInput{
|
||||
Full: map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"partial custom fields",
|
||||
galleryIDs[galleryIdxWithTwoTags],
|
||||
models.GalleryPartial{
|
||||
CustomFields: models.CustomFieldsInput{
|
||||
Partial: map[string]interface{}{
|
||||
"string": "bbb",
|
||||
"new_field": "new",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"int": int64(2),
|
||||
"real": 1.2,
|
||||
"string": "bbb",
|
||||
"new_field": "new",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
qb := db.Gallery
|
||||
|
||||
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||
assert := assert.New(t)
|
||||
|
||||
_, err := qb.UpdatePartial(ctx, tt.id, tt.partial)
|
||||
if err != nil {
|
||||
t.Errorf("GalleryStore.UpdatePartial() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// ensure custom fields are correct
|
||||
cf, err := qb.GetCustomFields(ctx, tt.id)
|
||||
if err != nil {
|
||||
t.Errorf("GalleryStore.GetCustomFields() error = %v", err)
|
||||
return
|
||||
}
|
||||
if tt.expected == nil {
|
||||
assert.Equal(tt.partial.CustomFields.Full, cf)
|
||||
} else {
|
||||
assert.Equal(tt.expected, cf)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_galleryQueryBuilder_Destroy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
|
|
|||
|
|
@ -131,6 +131,7 @@ var (
|
|||
|
||||
type GroupStore struct {
|
||||
blobJoinQueryBuilder
|
||||
customFieldsStore
|
||||
tagRelationshipStore
|
||||
groupRelationshipStore
|
||||
|
||||
|
|
@ -143,6 +144,10 @@ func NewGroupStore(blobStore *BlobStore) *GroupStore {
|
|||
blobStore: blobStore,
|
||||
joinTable: groupTable,
|
||||
},
|
||||
customFieldsStore: customFieldsStore{
|
||||
table: groupsCustomFieldsTable,
|
||||
fk: groupsCustomFieldsTable.Col(groupIDColumn),
|
||||
},
|
||||
tagRelationshipStore: tagRelationshipStore{
|
||||
idRelationshipStore: idRelationshipStore{
|
||||
joinTable: groupsTagsTableMgr,
|
||||
|
|
@ -235,6 +240,10 @@ func (qb *GroupStore) UpdatePartial(ctx context.Context, id int, partial models.
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if err := qb.SetCustomFields(ctx, id, partial.CustomFields); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return qb.find(ctx, id)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -84,6 +84,13 @@ func (qb *groupFilterHandler) criterionHandler() criterionHandler {
|
|||
×tampCriterionHandler{groupFilter.CreatedAt, "groups.created_at", nil},
|
||||
×tampCriterionHandler{groupFilter.UpdatedAt, "groups.updated_at", nil},
|
||||
|
||||
&customFieldsFilterHandler{
|
||||
table: groupsCustomFieldsTable.GetTable(),
|
||||
fkCol: groupIDColumn,
|
||||
c: groupFilter.CustomFields,
|
||||
idCol: "groups.id",
|
||||
},
|
||||
|
||||
&relatedFilterHandler{
|
||||
relatedIDCol: "groups_scenes.scene_id",
|
||||
relatedRepo: sceneRepository.repository,
|
||||
|
|
|
|||
|
|
@ -566,6 +566,79 @@ func Test_groupQueryBuilder_UpdatePartial(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func Test_GroupStore_UpdatePartialCustomFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
id int
|
||||
partial models.GroupPartial
|
||||
expected map[string]interface{} // nil to use the partial
|
||||
}{
|
||||
{
|
||||
"set custom fields",
|
||||
groupIDs[groupIdxWithChild],
|
||||
models.GroupPartial{
|
||||
CustomFields: models.CustomFieldsInput{
|
||||
Full: testCustomFields,
|
||||
},
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"clear custom fields",
|
||||
groupIDs[groupIdxWithChild],
|
||||
models.GroupPartial{
|
||||
CustomFields: models.CustomFieldsInput{
|
||||
Full: map[string]interface{}{},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"partial custom fields",
|
||||
groupIDs[groupIdxWithTwoTags],
|
||||
models.GroupPartial{
|
||||
CustomFields: models.CustomFieldsInput{
|
||||
Partial: map[string]interface{}{
|
||||
"string": "bbb",
|
||||
"new_field": "new",
|
||||
},
|
||||
},
|
||||
},
|
||||
map[string]interface{}{
|
||||
"int": int64(3),
|
||||
"real": 0.3,
|
||||
"string": "bbb",
|
||||
"new_field": "new",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
qb := db.Group
|
||||
|
||||
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||
assert := assert.New(t)
|
||||
|
||||
_, err := qb.UpdatePartial(ctx, tt.id, tt.partial)
|
||||
if err != nil {
|
||||
t.Errorf("GroupStore.UpdatePartial() error = %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// ensure custom fields are correct
|
||||
cf, err := qb.GetCustomFields(ctx, tt.id)
|
||||
if err != nil {
|
||||
t.Errorf("GroupStore.GetCustomFields() error = %v", err)
|
||||
return
|
||||
}
|
||||
if tt.expected == nil {
|
||||
assert.Equal(tt.partial.CustomFields.Full, cf)
|
||||
} else {
|
||||
assert.Equal(tt.expected, cf)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupFindByName(t *testing.T) {
|
||||
withTxn(func(ctx context.Context) error {
|
||||
mqb := db.Group
|
||||
|
|
@ -1917,6 +1990,245 @@ func TestGroupFindSubGroupIDs(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestGroupQueryCustomFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
filter *models.GroupFilterType
|
||||
includeIdxs []int
|
||||
excludeIdxs []int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
"equals",
|
||||
&models.GroupFilterType{
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "string",
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
Value: []any{getGroupStringValue(groupIdxWithChild, "custom")},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]int{groupIdxWithChild},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"not equals",
|
||||
&models.GroupFilterType{
|
||||
Name: &models.StringCriterionInput{
|
||||
Value: getGroupStringValue(groupIdxWithChild, "Name"),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "string",
|
||||
Modifier: models.CriterionModifierNotEquals,
|
||||
Value: []any{getGroupStringValue(groupIdxWithChild, "custom")},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
[]int{groupIdxWithChild},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"includes",
|
||||
&models.GroupFilterType{
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "string",
|
||||
Modifier: models.CriterionModifierIncludes,
|
||||
Value: []any{getGroupStringValue(groupIdxWithChild, "custom")[9:]},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]int{groupIdxWithChild},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"excludes",
|
||||
&models.GroupFilterType{
|
||||
Name: &models.StringCriterionInput{
|
||||
Value: getGroupStringValue(groupIdxWithChild, "Name"),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "string",
|
||||
Modifier: models.CriterionModifierExcludes,
|
||||
Value: []any{getGroupStringValue(groupIdxWithChild, "custom")[9:]},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
[]int{groupIdxWithChild},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"regex",
|
||||
&models.GroupFilterType{
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "string",
|
||||
Modifier: models.CriterionModifierMatchesRegex,
|
||||
Value: []any{".*11_custom"},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]int{groupIdxWithChildWithScene},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid regex",
|
||||
&models.GroupFilterType{
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "string",
|
||||
Modifier: models.CriterionModifierMatchesRegex,
|
||||
Value: []any{"["},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"not matches regex",
|
||||
&models.GroupFilterType{
|
||||
Name: &models.StringCriterionInput{
|
||||
Value: getGroupStringValue(groupIdxWithChildWithScene, "Name"),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "string",
|
||||
Modifier: models.CriterionModifierNotMatchesRegex,
|
||||
Value: []any{".*11_custom"},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
[]int{groupIdxWithChildWithScene},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"invalid not matches regex",
|
||||
&models.GroupFilterType{
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "string",
|
||||
Modifier: models.CriterionModifierNotMatchesRegex,
|
||||
Value: []any{"["},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
"null",
|
||||
&models.GroupFilterType{
|
||||
Name: &models.StringCriterionInput{
|
||||
Value: getGroupStringValue(groupIdxWithGrandParent, "Name"),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "not existing",
|
||||
Modifier: models.CriterionModifierIsNull,
|
||||
},
|
||||
},
|
||||
},
|
||||
[]int{groupIdxWithGrandParent},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"not null",
|
||||
&models.GroupFilterType{
|
||||
Name: &models.StringCriterionInput{
|
||||
Value: getGroupStringValue(groupIdxWithGrandParent, "Name"),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "string",
|
||||
Modifier: models.CriterionModifierNotNull,
|
||||
},
|
||||
},
|
||||
},
|
||||
[]int{groupIdxWithGrandParent},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"between",
|
||||
&models.GroupFilterType{
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "real",
|
||||
Modifier: models.CriterionModifierBetween,
|
||||
Value: []any{0.15, 0.25},
|
||||
},
|
||||
},
|
||||
},
|
||||
[]int{groupIdxWithTag},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
"not between",
|
||||
&models.GroupFilterType{
|
||||
Name: &models.StringCriterionInput{
|
||||
Value: getGroupStringValue(groupIdxWithTag, "Name"),
|
||||
Modifier: models.CriterionModifierEquals,
|
||||
},
|
||||
CustomFields: []models.CustomFieldCriterionInput{
|
||||
{
|
||||
Field: "real",
|
||||
Modifier: models.CriterionModifierNotBetween,
|
||||
Value: []any{0.15, 0.25},
|
||||
},
|
||||
},
|
||||
},
|
||||
nil,
|
||||
[]int{groupIdxWithTag},
|
||||
false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
runWithRollbackTxn(t, tt.name, func(t *testing.T, ctx context.Context) {
|
||||
assert := assert.New(t)
|
||||
|
||||
groups, _, err := db.Group.Query(ctx, tt.filter, nil)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("GroupStore.Query() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
ids := groupsToIDs(groups)
|
||||
include := indexesToIDs(groupIDs, tt.includeIdxs)
|
||||
exclude := indexesToIDs(groupIDs, tt.excludeIdxs)
|
||||
|
||||
for _, i := range include {
|
||||
assert.Contains(ids, i)
|
||||
}
|
||||
for _, e := range exclude {
|
||||
assert.NotContains(ids, e)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Update
|
||||
// TODO Destroy - ensure image is destroyed
|
||||
// TODO Find
|
||||
|
|
|
|||
9
pkg/sqlite/migrations/82_group_custom_fields.up.sql
Normal file
9
pkg/sqlite/migrations/82_group_custom_fields.up.sql
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
CREATE TABLE `group_custom_fields` (
|
||||
`group_id` integer NOT NULL,
|
||||
`field` varchar(64) NOT NULL,
|
||||
`value` BLOB NOT NULL,
|
||||
PRIMARY KEY (`group_id`, `field`),
|
||||
foreign key(`group_id`) references `groups`(`id`) on delete CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX `index_group_custom_fields_field_value` ON `group_custom_fields` (`field`, `value`);
|
||||
|
|
@ -1457,6 +1457,18 @@ func getGroupEmptyString(index int, field string) string {
|
|||
return v.String
|
||||
}
|
||||
|
||||
func getGroupCustomFields(index int) map[string]interface{} {
|
||||
if index%5 == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"string": getGroupStringValue(index, "custom"),
|
||||
"int": int64(index % 5),
|
||||
"real": float64(index) / 10,
|
||||
}
|
||||
}
|
||||
|
||||
// createGroups creates n groups with plain Name and o groups with camel cased NaMe included
|
||||
func createGroups(ctx context.Context, mqb models.GroupReaderWriter, n int, o int) error {
|
||||
const namePlain = "Name"
|
||||
|
|
@ -1489,6 +1501,13 @@ func createGroups(ctx context.Context, mqb models.GroupReaderWriter, n int, o in
|
|||
return fmt.Errorf("Error creating group [%d] %v+: %s", i, group, err.Error())
|
||||
}
|
||||
|
||||
customFields := getGroupCustomFields(i)
|
||||
if customFields != nil {
|
||||
if err := mqb.SetCustomFields(ctx, group.ID, models.CustomFieldsInput{Full: customFields}); err != nil {
|
||||
return fmt.Errorf("Error setting custom fields for group %d: %s", group.ID, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
groupIDs = append(groupIDs, group.ID)
|
||||
groupNames = append(groupNames, group.Name)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ var (
|
|||
groupsURLsJoinTable = goqu.T(groupURLsTable)
|
||||
groupsTagsJoinTable = goqu.T(groupsTagsTable)
|
||||
groupRelationsJoinTable = goqu.T(groupRelationsTable)
|
||||
groupsCustomFieldsTable = goqu.T("group_custom_fields")
|
||||
|
||||
tagsAliasesJoinTable = goqu.T(tagAliasesTable)
|
||||
tagRelationsJoinTable = goqu.T(tagRelationsTable)
|
||||
|
|
|
|||
Loading…
Reference in a new issue