Update GQLGen and break up the schema.graphql file

This commit is contained in:
Stash Dev
2019-03-26 08:35:06 -07:00
parent 2e57c2a17a
commit 763424bc40
76 changed files with 12244 additions and 16328 deletions

4
go.mod
View File

@@ -1,7 +1,7 @@
module github.com/stashapp/stash
require (
github.com/99designs/gqlgen v0.4.5-0.20190127090136-055fb4bc9a6a
github.com/99designs/gqlgen v0.8.2
github.com/PuerkitoBio/goquery v1.5.0
github.com/bmatcuk/doublestar v1.1.1
github.com/disintegration/imaging v1.6.0
@@ -17,6 +17,6 @@ require (
github.com/sirupsen/logrus v1.3.0
github.com/spf13/afero v1.2.0 // indirect
github.com/spf13/viper v1.3.2
github.com/vektah/gqlparser v1.1.0
github.com/vektah/gqlparser v1.1.2
golang.org/x/image v0.0.0-20190118043309-183bebdce1b2 // indirect
)

4
go.sum
View File

@@ -6,6 +6,8 @@ git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGy
git.apache.org/thrift.git v0.0.0-20180924222215-a9235805469b/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/99designs/gqlgen v0.4.5-0.20190127090136-055fb4bc9a6a h1:oTsAt8YXjEk1fo7uZR7gya1jrH48oPulx5oF6zWTHRw=
github.com/99designs/gqlgen v0.4.5-0.20190127090136-055fb4bc9a6a/go.mod h1:st7qHA6ssU3uRZkmv+wzrzgX4srvIqEIdE5iuRW8GhE=
github.com/99designs/gqlgen v0.8.2 h1:xOkDPWn/MZjkQ32pu6Axx15mNah0NAq9WalFqT+RavA=
github.com/99designs/gqlgen v0.8.2/go.mod h1:aLyJw9xUgdJxZ8EqNQxo2pGFhXXJ/hq8t7J4yn8TgI4=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -439,6 +441,8 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
github.com/vektah/dataloaden v0.2.0/go.mod h1:vxM6NuRlgiR0M6wbVTJeKp9vQIs81ZMfCYO+4yq/jbE=
github.com/vektah/gqlparser v1.1.0 h1:3668p2gUlO+PiS81x957Rpr3/FPRWG6cxgCXAvTS1hw=
github.com/vektah/gqlparser v1.1.0/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
github.com/vektah/gqlparser v1.1.2 h1:ZsyLGn7/7jDNI+y4SEhI4yAxRChlv15pUHMjijT+e68=
github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I=
github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=

View File

@@ -1,10 +1,8 @@
# .gqlgen.yml example
#
# Refer to https://gqlgen.com/config/
# for detailed .gqlgen.yml documentation.
# Refer to https://gqlgen.com/config/ for detailed .gqlgen.yml documentation.
schema:
- schema/schema.graphql
- "graphql/schema/types/*.graphql"
- "graphql/schema/*.graphql"
exec:
filename: pkg/models/generated_exec.go
model:

View File

@@ -0,0 +1,103 @@
"""The query root for this schema"""
type Query {
"""Find a scene by ID or Checksum"""
findScene(id: ID, checksum: String): Scene
"""A function which queries Scene objects"""
findScenes(scene_filter: SceneFilterType, scene_ids: [Int!], filter: FindFilterType): FindScenesResultType!
"""A function which queries SceneMarker objects"""
findSceneMarkers(scene_marker_filter: SceneMarkerFilterType filter: FindFilterType): FindSceneMarkersResultType!
"""Find a performer by ID"""
findPerformer(id: ID!): Performer
"""A function which queries Performer objects"""
findPerformers(performer_filter: PerformerFilterType, filter: FindFilterType): FindPerformersResultType!
"""Find a studio by ID"""
findStudio(id: ID!): Studio
"""A function which queries Studio objects"""
findStudios(filter: FindFilterType): FindStudiosResultType!
findGallery(id: ID!): Gallery
findGalleries(filter: FindFilterType): FindGalleriesResultType!
findTag(id: ID!): Tag
"""Retrieve random scene markers for the wall"""
markerWall(q: String): [SceneMarker!]!
"""Retrieve random scenes for the wall"""
sceneWall(q: String): [Scene!]!
"""Get marker strings"""
markerStrings(q: String, sort: String): [MarkerStringsResultType]!
"""Get the list of valid galleries for a given scene ID"""
validGalleriesForScene(scene_id: ID): [Gallery!]!
"""Get stats"""
stats: StatsResultType!
"""Organize scene markers by tag for a given scene ID"""
sceneMarkerTags(scene_id: ID!): [SceneMarkerTag!]!
# Scrapers
"""Scrape a performer using Freeones"""
scrapeFreeones(performer_name: String!): ScrapedPerformer
"""Scrape a list of performers from a query"""
scrapeFreeonesPerformerList(query: String!): [String!]!
# Config
"""Returns the current, complete configuration"""
configuration: ConfigResult!
"""Returns an array of paths for the given path"""
directories(path: String): [String!]!
# Metadata
"""Start an import. Returns the job ID"""
metadataImport: String!
"""Start an export. Returns the job ID"""
metadataExport: String!
"""Start a scan. Returns the job ID"""
metadataScan: String!
"""Start generating content. Returns the job ID"""
metadataGenerate(input: GenerateMetadataInput!): String!
"""Clean metadata. Returns the job ID"""
metadataClean: String!
# Get everything
allPerformers: [Performer!]!
allStudios: [Studio!]!
allTags: [Tag!]!
}
type Mutation {
sceneUpdate(input: SceneUpdateInput!): Scene
sceneMarkerCreate(input: SceneMarkerCreateInput!): SceneMarker
sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker
sceneMarkerDestroy(id: ID!): Boolean!
performerCreate(input: PerformerCreateInput!): Performer
performerUpdate(input: PerformerUpdateInput!): Performer
studioCreate(input: StudioCreateInput!): Studio
studioUpdate(input: StudioUpdateInput!): Studio
tagCreate(input: TagCreateInput!): Tag
tagUpdate(input: TagUpdateInput!): Tag
tagDestroy(input: TagDestroyInput!): Boolean!
"""Change general configuration options"""
configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult!
}
type Subscription {
"""Update from the metadata manager"""
metadataUpdate: String!
}
schema {
query: Query
mutation: Mutation
subscription: Subscription
}

View File

@@ -0,0 +1,22 @@
input ConfigGeneralInput {
"""Array of file paths to content"""
stashes: [String!]
"""Path to the SQLite database"""
databasePath: String
"""Path to generated files"""
generatedPath: String
}
type ConfigGeneralResult {
"""Array of file paths to content"""
stashes: [String!]!
"""Path to the SQLite database"""
databasePath: String!
"""Path to generated files"""
generatedPath: String!
}
"""All configuration settings"""
type ConfigResult {
general: ConfigGeneralResult!
}

View File

@@ -0,0 +1,75 @@
enum SortDirectionEnum {
ASC
DESC
}
input FindFilterType {
q: String
page: Int
per_page: Int
sort: String
direction: SortDirectionEnum
}
enum ResolutionEnum {
"240p", LOW
"480p", STANDARD
"720p", STANDARD_HD
"1080p", FULL_HD
"4k", FOUR_K
}
input PerformerFilterType {
"""Filter by favorite"""
filter_favorites: Boolean
}
input SceneMarkerFilterType {
"""Filter to only include scene markers with this tag"""
tag_id: ID
"""Filter to only include scene markers with these tags"""
tags: [ID!]
"""Filter to only include scene markers attached to a scene with these tags"""
scene_tags: [ID!]
"""Filter to only include scene markers with these performers"""
performers: [ID!]
}
input SceneFilterType {
"""Filter by rating"""
rating: IntCriterionInput
"""Filter by resolution"""
resolution: ResolutionEnum
"""Filter to only include scenes which have markers. `true` or `false`"""
has_markers: String
"""Filter to only include scenes missing this property"""
is_missing: String
"""Filter to only include scenes with this studio"""
studio_id: ID
"""Filter to only include scenes with these tags"""
tags: [ID!]
"""Filter to only include scenes with this performer"""
performer_id: ID
}
enum CriterionModifier {
"""="""
EQUALS,
"""!="""
NOT_EQUALS,
""">"""
GREATER_THAN,
"""<"""
LESS_THAN,
"""IS NULL"""
IS_NULL,
"""IS NOT NULL"""
NOT_NULL,
INCLUDES,
EXCLUDES,
}
input IntCriterionInput {
value: Int!
modifier: CriterionModifier!
}

View File

@@ -0,0 +1,21 @@
"""Gallery type"""
type Gallery {
id: ID!
checksum: String!
path: String!
title: String
"""The files in the gallery"""
files: [GalleryFilesType!]! # Resolver
}
type GalleryFilesType {
index: Int!
name: String
path: String
}
type FindGalleriesResultType {
count: Int!
galleries: [Gallery!]!
}

View File

@@ -0,0 +1,6 @@
input GenerateMetadataInput {
sprites: Boolean!
previews: Boolean!
markers: Boolean!
transcodes: Boolean!
}

View File

@@ -0,0 +1,72 @@
type Performer {
id: ID!
checksum: String!
name: String
url: String
twitter: String
instagram: String
birthdate: String
ethnicity: String
country: String
eye_color: String
height: String
measurements: String
fake_tits: String
career_length: String
tattoos: String
piercings: String
aliases: String
favorite: Boolean!
image_path: String # Resolver
scene_count: Int # Resolver
scenes: [Scene!]!
}
input PerformerCreateInput {
name: String
url: String
birthdate: String
ethnicity: String
country: String
eye_color: String
height: String
measurements: String
fake_tits: String
career_length: String
tattoos: String
piercings: String
aliases: String
twitter: String
instagram: String
favorite: Boolean
"""This should be base64 encoded"""
image: String!
}
input PerformerUpdateInput {
id: ID!
name: String
url: String
birthdate: String
ethnicity: String
country: String
eye_color: String
height: String
measurements: String
fake_tits: String
career_length: String
tattoos: String
piercings: String
aliases: String
twitter: String
instagram: String
favorite: Boolean
"""This should be base64 encoded"""
image: String
}
type FindPerformersResultType {
count: Int!
performers: [Performer!]!
}

View File

@@ -0,0 +1,4 @@
type SceneMarkerTag {
tag: Tag!
scene_markers: [SceneMarker!]!
}

View File

@@ -0,0 +1,41 @@
type SceneMarker {
id: ID!
scene: Scene!
title: String!
seconds: Float!
primary_tag: Tag!
tags: [Tag!]!
"""The path to stream this marker"""
stream: String! # Resolver
"""The path to the preview image for this marker"""
preview: String! # Resolver
}
input SceneMarkerCreateInput {
title: String!
seconds: Float!
scene_id: ID!
primary_tag_id: ID!
tag_ids: [ID!]
}
input SceneMarkerUpdateInput {
id: ID!
title: String!
seconds: Float!
scene_id: ID!
primary_tag_id: ID!
tag_ids: [ID!]
}
type FindSceneMarkersResultType {
count: Int!
scene_markers: [SceneMarker!]!
}
type MarkerStringsResultType {
count: Int!
id: ID!
title: String!
}

View File

@@ -0,0 +1,59 @@
type SceneFileType {
size: String
duration: Float
video_codec: String
audio_codec: String
width: Int
height: Int
framerate: Float
bitrate: Int
}
type ScenePathsType {
screenshot: String # Resolver
preview: String # Resolver
stream: String # Resolver
webp: String # Resolver
vtt: String # Resolver
chapters_vtt: String # Resolver
}
type Scene {
id: ID!
checksum: String!
title: String
details: String
url: String
date: String
rating: Int
path: String!
file: SceneFileType! # Resolver
paths: ScenePathsType! # Resolver
is_streamable: Boolean! # Resolver
scene_markers: [SceneMarker!]!
gallery: Gallery
studio: Studio
tags: [Tag!]!
performers: [Performer!]!
}
input SceneUpdateInput {
clientMutationId: String
id: ID!
title: String
details: String
url: String
date: String
rating: Int
studio_id: ID
gallery_id: ID
performer_ids: [ID!]
tag_ids: [ID!]
}
type FindScenesResultType {
count: Int!
scenes: [Scene!]!
}

View File

@@ -0,0 +1,18 @@
"""A performer from a scraping operation..."""
type ScrapedPerformer {
name: String
url: String
twitter: String
instagram: String
birthdate: String
ethnicity: String
country: String
eye_color: String
height: String
measurements: String
fake_tits: String
career_length: String
tattoos: String
piercings: String
aliases: String
}

View File

@@ -0,0 +1,7 @@
type StatsResultType {
scene_count: Int!
gallery_count: Int!
performer_count: Int!
studio_count: Int!
tag_count: Int!
}

View File

@@ -0,0 +1,29 @@
type Studio {
id: ID!
checksum: String!
name: String!
url: String
image_path: String # Resolver
scene_count: Int # Resolver
}
input StudioCreateInput {
name: String!
url: String
"""This should be base64 encoded"""
image: String!
}
input StudioUpdateInput {
id: ID!
name: String
url: String
"""This should be base64 encoded"""
image: String
}
type FindStudiosResultType {
count: Int!
studios: [Studio!]!
}

View File

@@ -0,0 +1,20 @@
type Tag {
id: ID!
name: String!
scene_count: Int # Resolver
scene_marker_count: Int # Resolver
}
input TagCreateInput {
name: String!
}
input TagUpdateInput {
id: ID!
name: String!
}
input TagDestroyInput {
id: ID!
}

View File

@@ -84,7 +84,7 @@ func (r *queryResolver) ValidGalleriesForScene(ctx context.Context, scene_id *st
return validGalleries, nil
}
func (r *queryResolver) Stats(ctx context.Context) (models.StatsResultType, error) {
func (r *queryResolver) Stats(ctx context.Context) (*models.StatsResultType, error) {
scenesQB := models.NewSceneQueryBuilder()
scenesCount, _ := scenesQB.Count()
galleryQB := models.NewGalleryQueryBuilder()
@@ -95,7 +95,7 @@ func (r *queryResolver) Stats(ctx context.Context) (models.StatsResultType, erro
studiosCount, _ := studiosQB.Count()
tagsQB := models.NewTagQueryBuilder()
tagsCount, _ := tagsQB.Count()
return models.StatsResultType{
return &models.StatsResultType{
SceneCount: scenesCount,
GalleryCount: galleryCount,
PerformerCount: performersCount,

View File

@@ -3,13 +3,8 @@ package api
import (
"context"
"github.com/stashapp/stash/pkg/models"
"strconv"
)
func (r *galleryResolver) ID(ctx context.Context, obj *models.Gallery) (string, error) {
return strconv.Itoa(obj.ID), nil
}
func (r *galleryResolver) Title(ctx context.Context, obj *models.Gallery) (*string, error) {
return nil, nil // TODO remove this from schema
}

View File

@@ -4,13 +4,8 @@ import (
"context"
"github.com/stashapp/stash/pkg/api/urlbuilders"
"github.com/stashapp/stash/pkg/models"
"strconv"
)
func (r *performerResolver) ID(ctx context.Context, obj *models.Performer) (string, error) {
return strconv.Itoa(obj.ID), nil
}
func (r *performerResolver) Name(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Name.Valid {
return &obj.Name.String, nil

View File

@@ -6,13 +6,8 @@ import (
"github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils"
"strconv"
)
func (r *sceneResolver) ID(ctx context.Context, obj *models.Scene) (string, error) {
return strconv.Itoa(obj.ID), nil
}
func (r *sceneResolver) Title(ctx context.Context, obj *models.Scene) (*string, error) {
if obj.Title.Valid {
return &obj.Title.String, nil
@@ -50,11 +45,11 @@ func (r *sceneResolver) Rating(ctx context.Context, obj *models.Scene) (*int, er
return nil, nil
}
func (r *sceneResolver) File(ctx context.Context, obj *models.Scene) (models.SceneFileType, error) {
func (r *sceneResolver) File(ctx context.Context, obj *models.Scene) (*models.SceneFileType, error) {
width := int(obj.Width.Int64)
height := int(obj.Height.Int64)
bitrate := int(obj.Bitrate.Int64)
return models.SceneFileType{
return &models.SceneFileType{
Size: &obj.Size.String,
Duration: &obj.Duration.Float64,
VideoCodec: &obj.VideoCodec.String,
@@ -66,7 +61,7 @@ func (r *sceneResolver) File(ctx context.Context, obj *models.Scene) (models.Sce
}, nil
}
func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (models.ScenePathsType, error) {
func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (*models.ScenePathsType, error) {
baseURL, _ := ctx.Value(BaseURLCtxKey).(string)
builder := urlbuilders.NewSceneURLBuilder(baseURL, obj.ID)
screenshotPath := builder.GetScreenshotURL()
@@ -75,7 +70,7 @@ func (r *sceneResolver) Paths(ctx context.Context, obj *models.Scene) (models.Sc
webpPath := builder.GetStreamPreviewImageURL()
vttPath := builder.GetSpriteVTTURL()
chaptersVttPath := builder.GetChaptersVTTURL()
return models.ScenePathsType{
return &models.ScenePathsType{
Screenshot: &screenshotPath,
Preview: &previewPath,
Stream: &streamPath,

View File

@@ -4,30 +4,25 @@ import (
"context"
"github.com/stashapp/stash/pkg/api/urlbuilders"
"github.com/stashapp/stash/pkg/models"
"strconv"
)
func (r *sceneMarkerResolver) ID(ctx context.Context, obj *models.SceneMarker) (string, error) {
return strconv.Itoa(obj.ID), nil
}
func (r *sceneMarkerResolver) Scene(ctx context.Context, obj *models.SceneMarker) (models.Scene, error) {
func (r *sceneMarkerResolver) Scene(ctx context.Context, obj *models.SceneMarker) (*models.Scene, error) {
if !obj.SceneID.Valid {
panic("Invalid scene id")
}
qb := models.NewSceneQueryBuilder()
sceneID := int(obj.SceneID.Int64)
scene, err := qb.Find(sceneID)
return *scene, err
return scene, err
}
func (r *sceneMarkerResolver) PrimaryTag(ctx context.Context, obj *models.SceneMarker) (models.Tag, error) {
func (r *sceneMarkerResolver) PrimaryTag(ctx context.Context, obj *models.SceneMarker) (*models.Tag, error) {
qb := models.NewTagQueryBuilder()
if !obj.PrimaryTagID.Valid {
panic("TODO no primary tag id")
}
tag, err := qb.Find(int(obj.PrimaryTagID.Int64), nil) // TODO make primary tag id not null in DB
return *tag, err
return tag, err
}
func (r *sceneMarkerResolver) Tags(ctx context.Context, obj *models.SceneMarker) ([]models.Tag, error) {

View File

@@ -4,13 +4,8 @@ import (
"context"
"github.com/stashapp/stash/pkg/api/urlbuilders"
"github.com/stashapp/stash/pkg/models"
"strconv"
)
func (r *studioResolver) ID(ctx context.Context, obj *models.Studio) (string, error) {
return strconv.Itoa(obj.ID), nil
}
func (r *studioResolver) Name(ctx context.Context, obj *models.Studio) (string, error) {
if obj.Name.Valid {
return obj.Name.String, nil

View File

@@ -3,13 +3,8 @@ package api
import (
"context"
"github.com/stashapp/stash/pkg/models"
"strconv"
)
func (r *tagResolver) ID(ctx context.Context, obj *models.Tag) (string, error) {
return strconv.Itoa(obj.ID), nil
}
func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag) (*int, error) {
qb := models.NewSceneQueryBuilder()
if obj == nil {

View File

@@ -9,7 +9,7 @@ import (
"path/filepath"
)
func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.ConfigGeneralInput) (models.ConfigGeneralResult, error) {
func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.ConfigGeneralInput) (*models.ConfigGeneralResult, error) {
if len(input.Stashes) > 0 {
for _, stashPath := range input.Stashes {
exists, err := utils.DirExists(stashPath)

View File

@@ -7,7 +7,7 @@ import (
"github.com/stashapp/stash/pkg/utils"
)
func (r *queryResolver) Configuration(ctx context.Context) (models.ConfigResult, error) {
func (r *queryResolver) Configuration(ctx context.Context) (*models.ConfigResult, error) {
return makeConfigResult(), nil
}
@@ -19,14 +19,14 @@ func (r *queryResolver) Directories(ctx context.Context, path *string) ([]string
return utils.ListDir(dirPath), nil
}
func makeConfigResult() models.ConfigResult {
return models.ConfigResult{
General: makeConfigGeneralResult(),
func makeConfigResult() *models.ConfigResult {
return &models.ConfigResult{
General: *makeConfigGeneralResult(),
}
}
func makeConfigGeneralResult() models.ConfigGeneralResult {
return models.ConfigGeneralResult{
func makeConfigGeneralResult() *models.ConfigGeneralResult {
return &models.ConfigGeneralResult{
Stashes: config.GetStashPaths(),
DatabasePath: config.GetDatabasePath(),
GeneratedPath: config.GetGeneratedPath(),

View File

@@ -12,10 +12,10 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (*models.Gal
return qb.Find(idInt)
}
func (r *queryResolver) FindGalleries(ctx context.Context, filter *models.FindFilterType) (models.FindGalleriesResultType, error) {
func (r *queryResolver) FindGalleries(ctx context.Context, filter *models.FindFilterType) (*models.FindGalleriesResultType, error) {
qb := models.NewGalleryQueryBuilder()
galleries, total := qb.Query(filter)
return models.FindGalleriesResultType{
return &models.FindGalleriesResultType{
Count: total,
Galleries: galleries,
}, nil

View File

@@ -12,10 +12,10 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (*models.P
return qb.Find(idInt)
}
func (r *queryResolver) FindPerformers(ctx context.Context, performer_filter *models.PerformerFilterType, filter *models.FindFilterType) (models.FindPerformersResultType, error) {
func (r *queryResolver) FindPerformers(ctx context.Context, performerFilter *models.PerformerFilterType, filter *models.FindFilterType) (*models.FindPerformersResultType, error) {
qb := models.NewPerformerQueryBuilder()
performers, total := qb.Query(performer_filter, filter)
return models.FindPerformersResultType{
performers, total := qb.Query(performerFilter, filter)
return &models.FindPerformersResultType{
Count: total,
Performers: performers,
}, nil

View File

@@ -19,10 +19,10 @@ func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *str
return scene, err
}
func (r *queryResolver) FindScenes(ctx context.Context, scene_filter *models.SceneFilterType, scene_ids []int, filter *models.FindFilterType) (models.FindScenesResultType, error) {
func (r *queryResolver) FindScenes(ctx context.Context, sceneFilter *models.SceneFilterType, sceneIds []int, filter *models.FindFilterType) (*models.FindScenesResultType, error) {
qb := models.NewSceneQueryBuilder()
scenes, total := qb.Query(scene_filter, filter)
return models.FindScenesResultType{
scenes, total := qb.Query(sceneFilter, filter)
return &models.FindScenesResultType{
Count: total,
Scenes: scenes,
}, nil

View File

@@ -5,10 +5,10 @@ import (
"github.com/stashapp/stash/pkg/models"
)
func (r *queryResolver) FindSceneMarkers(ctx context.Context, scene_marker_filter *models.SceneMarkerFilterType, filter *models.FindFilterType) (models.FindSceneMarkersResultType, error) {
func (r *queryResolver) FindSceneMarkers(ctx context.Context, sceneMarkerFilter *models.SceneMarkerFilterType, filter *models.FindFilterType) (*models.FindSceneMarkersResultType, error) {
qb := models.NewSceneMarkerQueryBuilder()
sceneMarkers, total := qb.Query(scene_marker_filter, filter)
return models.FindSceneMarkersResultType{
sceneMarkers, total := qb.Query(sceneMarkerFilter, filter)
return &models.FindSceneMarkersResultType{
Count: total,
SceneMarkers: sceneMarkers,
}, nil

View File

@@ -12,10 +12,10 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (*models.Stud
return qb.Find(idInt, nil)
}
func (r *queryResolver) FindStudios(ctx context.Context, filter *models.FindFilterType) (models.FindStudiosResultType, error) {
func (r *queryResolver) FindStudios(ctx context.Context, filter *models.FindFilterType) (*models.FindStudiosResultType, error) {
qb := models.NewStudioQueryBuilder()
studios, total := qb.Query(filter)
return models.FindStudiosResultType{
return &models.FindStudiosResultType{
Count: total,
Studios: studios,
}, nil

File diff suppressed because it is too large Load Diff

View File

@@ -1,541 +0,0 @@
#######################################
# Gallery
#######################################
"""Gallery type"""
type Gallery {
id: ID!
checksum: String!
path: String!
title: String
"""The files in the gallery"""
files: [GalleryFilesType!]! # Resolver
}
type GalleryFilesType {
index: Int!
name: String
path: String
}
type FindGalleriesResultType {
count: Int!
galleries: [Gallery!]!
}
#######################################
# Performer
#######################################
type Performer {
id: ID!
checksum: String!
name: String
url: String
twitter: String
instagram: String
birthdate: String
ethnicity: String
country: String
eye_color: String
height: String
measurements: String
fake_tits: String
career_length: String
tattoos: String
piercings: String
aliases: String
favorite: Boolean!
image_path: String # Resolver
scene_count: Int # Resolver
scenes: [Scene!]!
}
input PerformerCreateInput {
name: String
url: String
birthdate: String
ethnicity: String
country: String
eye_color: String
height: String
measurements: String
fake_tits: String
career_length: String
tattoos: String
piercings: String
aliases: String
twitter: String
instagram: String
favorite: Boolean
"""This should be base64 encoded"""
image: String!
}
input PerformerUpdateInput {
id: ID!
name: String
url: String
birthdate: String
ethnicity: String
country: String
eye_color: String
height: String
measurements: String
fake_tits: String
career_length: String
tattoos: String
piercings: String
aliases: String
twitter: String
instagram: String
favorite: Boolean
"""This should be base64 encoded"""
image: String
}
type FindPerformersResultType {
count: Int!
performers: [Performer!]!
}
#######################################
# Scene Marker Tag
#######################################
type SceneMarkerTag {
tag: Tag!
scene_markers: [SceneMarker!]!
}
#######################################
# Scene Marker
#######################################
type SceneMarker {
id: ID!
scene: Scene!
title: String!
seconds: Float!
primary_tag: Tag!
tags: [Tag!]!
"""The path to stream this marker"""
stream: String! # Resolver
"""The path to the preview image for this marker"""
preview: String! # Resolver
}
input SceneMarkerCreateInput {
title: String!
seconds: Float!
scene_id: ID!
primary_tag_id: ID!
tag_ids: [ID!]
}
input SceneMarkerUpdateInput {
id: ID!
title: String!
seconds: Float!
scene_id: ID!
primary_tag_id: ID!
tag_ids: [ID!]
}
type FindSceneMarkersResultType {
count: Int!
scene_markers: [SceneMarker!]!
}
type MarkerStringsResultType {
count: Int!
id: ID!
title: String!
}
#######################################
# Scene
#######################################
type SceneFileType {
size: String
duration: Float
video_codec: String
audio_codec: String
width: Int
height: Int
framerate: Float
bitrate: Int
}
type ScenePathsType {
screenshot: String # Resolver
preview: String # Resolver
stream: String # Resolver
webp: String # Resolver
vtt: String # Resolver
chapters_vtt: String # Resolver
}
type Scene {
id: ID!
checksum: String!
title: String
details: String
url: String
date: String
rating: Int
path: String!
file: SceneFileType! # Resolver
paths: ScenePathsType! # Resolver
is_streamable: Boolean! # Resolver
scene_markers: [SceneMarker!]!
gallery: Gallery
studio: Studio
tags: [Tag!]!
performers: [Performer!]!
}
input SceneUpdateInput {
clientMutationId: String
id: ID!
title: String
details: String
url: String
date: String
rating: Int
studio_id: ID
gallery_id: ID
performer_ids: [ID!]
tag_ids: [ID!]
}
type FindScenesResultType {
count: Int!
scenes: [Scene!]!
}
#######################################
# Scraped Performer
#######################################
"""A performer from a scraping operation..."""
type ScrapedPerformer {
name: String
url: String
twitter: String
instagram: String
birthdate: String
ethnicity: String
country: String
eye_color: String
height: String
measurements: String
fake_tits: String
career_length: String
tattoos: String
piercings: String
aliases: String
}
#######################################
# Stats
#######################################
type StatsResultType {
scene_count: Int!
gallery_count: Int!
performer_count: Int!
studio_count: Int!
tag_count: Int!
}
#######################################
# Studio
#######################################
type Studio {
id: ID!
checksum: String!
name: String!
url: String
image_path: String # Resolver
scene_count: Int # Resolver
}
input StudioCreateInput {
name: String!
url: String
"""This should be base64 encoded"""
image: String!
}
input StudioUpdateInput {
id: ID!
name: String
url: String
"""This should be base64 encoded"""
image: String
}
type FindStudiosResultType {
count: Int!
studios: [Studio!]!
}
#######################################
# Tag
#######################################
type Tag {
id: ID!
name: String!
scene_count: Int # Resolver
scene_marker_count: Int # Resolver
}
input TagCreateInput {
name: String!
}
input TagUpdateInput {
id: ID!
name: String!
}
input TagDestroyInput {
id: ID!
}
#######################################
# Filters
#######################################
enum SortDirectionEnum {
ASC
DESC
}
input FindFilterType {
q: String
page: Int
per_page: Int
sort: String
direction: SortDirectionEnum
}
enum ResolutionEnum {
"240p", LOW
"480p", STANDARD
"720p", STANDARD_HD
"1080p", FULL_HD
"4k", FOUR_K
}
input PerformerFilterType {
"""Filter by favorite"""
filter_favorites: Boolean
}
input SceneMarkerFilterType {
"""Filter to only include scene markers with this tag"""
tag_id: ID
"""Filter to only include scene markers with these tags"""
tags: [ID!]
"""Filter to only include scene markers attached to a scene with these tags"""
scene_tags: [ID!]
"""Filter to only include scene markers with these performers"""
performers: [ID!]
}
input SceneFilterType {
"""Filter by rating"""
rating: IntCriterionInput
"""Filter by resolution"""
resolution: ResolutionEnum
"""Filter to only include scenes which have markers. `true` or `false`"""
has_markers: String
"""Filter to only include scenes missing this property"""
is_missing: String
"""Filter to only include scenes with this studio"""
studio_id: ID
"""Filter to only include scenes with these tags"""
tags: [ID!]
"""Filter to only include scenes with this performer"""
performer_id: ID
}
enum CriterionModifier {
"""="""
EQUALS,
"""!="""
NOT_EQUALS,
""">"""
GREATER_THAN,
"""<"""
LESS_THAN,
"""IS NULL"""
IS_NULL,
"""IS NOT NULL"""
NOT_NULL,
INCLUDES,
EXCLUDES,
}
input IntCriterionInput {
value: Int!
modifier: CriterionModifier!
}
#######################################
# Config
#######################################
input ConfigGeneralInput {
"""Array of file paths to content"""
stashes: [String!]
"""Path to the SQLite database"""
databasePath: String
"""Path to generated files"""
generatedPath: String
}
type ConfigGeneralResult {
"""Array of file paths to content"""
stashes: [String!]!
"""Path to the SQLite database"""
databasePath: String!
"""Path to generated files"""
generatedPath: String!
}
"""All configuration settings"""
type ConfigResult {
general: ConfigGeneralResult!
}
#######################################
# Metadata
#######################################
input GenerateMetadataInput {
sprites: Boolean!
previews: Boolean!
markers: Boolean!
transcodes: Boolean!
}
#############
# Root Schema
#############
"""The query root for this schema"""
type Query {
"""Find a scene by ID or Checksum"""
findScene(id: ID, checksum: String): Scene
"""A function which queries Scene objects"""
findScenes(scene_filter: SceneFilterType, scene_ids: [Int!], filter: FindFilterType): FindScenesResultType!
"""A function which queries SceneMarker objects"""
findSceneMarkers(scene_marker_filter: SceneMarkerFilterType filter: FindFilterType): FindSceneMarkersResultType!
"""Find a performer by ID"""
findPerformer(id: ID!): Performer
"""A function which queries Performer objects"""
findPerformers(performer_filter: PerformerFilterType, filter: FindFilterType): FindPerformersResultType!
"""Find a studio by ID"""
findStudio(id: ID!): Studio
"""A function which queries Studio objects"""
findStudios(filter: FindFilterType): FindStudiosResultType!
findGallery(id: ID!): Gallery
findGalleries(filter: FindFilterType): FindGalleriesResultType!
findTag(id: ID!): Tag
"""Retrieve random scene markers for the wall"""
markerWall(q: String): [SceneMarker!]!
"""Retrieve random scenes for the wall"""
sceneWall(q: String): [Scene!]!
"""Get marker strings"""
markerStrings(q: String, sort: String): [MarkerStringsResultType]!
"""Get the list of valid galleries for a given scene ID"""
validGalleriesForScene(scene_id: ID): [Gallery!]!
"""Get stats"""
stats: StatsResultType!
"""Organize scene markers by tag for a given scene ID"""
sceneMarkerTags(scene_id: ID!): [SceneMarkerTag!]!
# Scrapers
"""Scrape a performer using Freeones"""
scrapeFreeones(performer_name: String!): ScrapedPerformer
"""Scrape a list of performers from a query"""
scrapeFreeonesPerformerList(query: String!): [String!]!
# Config
"""Returns the current, complete configuration"""
configuration: ConfigResult!
"""Returns an array of paths for the given path"""
directories(path: String): [String!]!
# Metadata
"""Start an import. Returns the job ID"""
metadataImport: String!
"""Start an export. Returns the job ID"""
metadataExport: String!
"""Start a scan. Returns the job ID"""
metadataScan: String!
"""Start generating content. Returns the job ID"""
metadataGenerate(input: GenerateMetadataInput!): String!
"""Clean metadata. Returns the job ID"""
metadataClean: String!
# Get everything
allPerformers: [Performer!]!
allStudios: [Studio!]!
allTags: [Tag!]!
}
type Mutation {
sceneUpdate(input: SceneUpdateInput!): Scene
sceneMarkerCreate(input: SceneMarkerCreateInput!): SceneMarker
sceneMarkerUpdate(input: SceneMarkerUpdateInput!): SceneMarker
sceneMarkerDestroy(id: ID!): Boolean!
performerCreate(input: PerformerCreateInput!): Performer
performerUpdate(input: PerformerUpdateInput!): Performer
studioCreate(input: StudioCreateInput!): Studio
studioUpdate(input: StudioUpdateInput!): Studio
tagCreate(input: TagCreateInput!): Tag
tagUpdate(input: TagUpdateInput!): Tag
tagDestroy(input: TagDestroyInput!): Boolean!
"""Change general configuration options"""
configureGeneral(input: ConfigGeneralInput!): ConfigGeneralResult!
}
type Subscription {
"""Update from the metadata manager"""
metadataUpdate: String!
}
schema {
query: Query
mutation: Mutation
subscription: Subscription
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
schema: ../../schema/schema.graphql
schema: "../../graphql/schema/**/*.graphql"
overwrite: true
generates:
./../../schema/schema.json:
- introspection
./src/app/core/graphql-generated.ts:
documents: ./../../schema/documents/**/*.graphql
documents: ./../../graphql/documents/**/*.graphql
plugins:
- add: "/* tslint:disable */"
- time

View File

@@ -1,6 +1,6 @@
overwrite: true
schema: "../../schema/schema.graphql"
documents: "../../schema/documents/**/*.graphql"
schema: "../../graphql/schema/**/*.graphql"
documents: "../../graphql/documents/**/*.graphql"
generates:
src/core/generated-graphql.tsx:
config:

View File

@@ -12,6 +12,7 @@ import (
type Resolver func(ctx context.Context) (res interface{}, err error)
type FieldMiddleware func(ctx context.Context, next Resolver) (res interface{}, err error)
type RequestMiddleware func(ctx context.Context, next func(ctx context.Context) []byte) []byte
type ComplexityLimitFunc func(ctx context.Context) int
type RequestContext struct {
RawQuery string
@@ -71,12 +72,10 @@ const (
)
func GetRequestContext(ctx context.Context) *RequestContext {
val := ctx.Value(request)
if val == nil {
return nil
if val, ok := ctx.Value(request).(*RequestContext); ok {
return val
}
return val.(*RequestContext)
return nil
}
func WithRequestContext(ctx context.Context, rc *RequestContext) context.Context {
@@ -95,6 +94,8 @@ type ResolverContext struct {
Index *int
// The result object of resolver
Result interface{}
// IsMethod indicates if the resolver is a method
IsMethod bool
}
func (r *ResolverContext) Path() []interface{} {
@@ -117,8 +118,10 @@ func (r *ResolverContext) Path() []interface{} {
}
func GetResolverContext(ctx context.Context) *ResolverContext {
val, _ := ctx.Value(resolver).(*ResolverContext)
if val, ok := ctx.Value(resolver).(*ResolverContext); ok {
return val
}
return nil
}
func WithResolverContext(ctx context.Context, rc *ResolverContext) context.Context {
@@ -132,6 +135,24 @@ func CollectFieldsCtx(ctx context.Context, satisfies []string) []CollectedField
return CollectFields(ctx, resctx.Field.Selections, satisfies)
}
// CollectAllFields returns a slice of all GraphQL field names that were selected for the current resolver context.
// The slice will contain the unique set of all field names requested regardless of fragment type conditions.
func CollectAllFields(ctx context.Context) []string {
resctx := GetResolverContext(ctx)
collected := CollectFields(ctx, resctx.Field.Selections, nil)
uniq := make([]string, 0, len(collected))
Next:
for _, f := range collected {
for _, name := range uniq {
if name == f.Name {
continue Next
}
}
uniq = append(uniq, f.Name)
}
return uniq
}
// Errorf sends an error string to the client, passing it through the formatter.
func (c *RequestContext) Errorf(ctx context.Context, format string, args ...interface{}) {
c.errorsMu.Lock()

View File

@@ -14,7 +14,9 @@ type ExtendedError interface {
func DefaultErrorPresenter(ctx context.Context, err error) *gqlerror.Error {
if gqlerr, ok := err.(*gqlerror.Error); ok {
if gqlerr.Path == nil {
gqlerr.Path = GetResolverContext(ctx).Path()
}
return gqlerr
}

View File

@@ -16,6 +16,9 @@ type ExecutableSchema interface {
Subscription(ctx context.Context, op *ast.OperationDefinition) func() *Response
}
// CollectFields returns the set of fields from an ast.SelectionSet where all collected fields satisfy at least one of the GraphQL types
// passed through satisfies. Providing an empty or nil slice for satisfies will return collect all fields regardless of fragment
// type conditions.
func CollectFields(ctx context.Context, selSet ast.SelectionSet, satisfies []string) []CollectedField {
return collectFields(GetRequestContext(ctx), selSet, satisfies, map[string]bool{})
}
@@ -35,7 +38,10 @@ func collectFields(reqCtx *RequestContext, selSet ast.SelectionSet, satisfies []
f.Selections = append(f.Selections, sel.SelectionSet...)
case *ast.InlineFragment:
if !shouldIncludeNode(sel.Directives, reqCtx.Variables) || !instanceOf(sel.TypeCondition, satisfies) {
if !shouldIncludeNode(sel.Directives, reqCtx.Variables) {
continue
}
if len(satisfies) > 0 && !instanceOf(sel.TypeCondition, satisfies) {
continue
}
for _, childField := range collectFields(reqCtx, sel.SelectionSet, satisfies, visited) {
@@ -59,7 +65,7 @@ func collectFields(reqCtx *RequestContext, selSet ast.SelectionSet, satisfies []
panic(fmt.Errorf("missing fragment %s", fragmentName))
}
if !instanceOf(fragment.TypeCondition, satisfies) {
if len(satisfies) > 0 && !instanceOf(fragment.TypeCondition, satisfies) {
continue
}

View File

@@ -34,3 +34,24 @@ func UnmarshalID(v interface{}) (string, error) {
return "", fmt.Errorf("%T is not a string", v)
}
}
func MarshalIntID(i int) Marshaler {
return WriterFunc(func(w io.Writer) {
writeQuotedString(w, strconv.Itoa(i))
})
}
func UnmarshalIntID(v interface{}) (int, error) {
switch v := v.(type) {
case string:
return strconv.Atoi(v)
case int:
return v, nil
case int64:
return int(v), nil
case json.Number:
return strconv.Atoi(string(v))
default:
return 0, fmt.Errorf("%T is not an int", v)
}
}

View File

@@ -27,3 +27,53 @@ func UnmarshalInt(v interface{}) (int, error) {
return 0, fmt.Errorf("%T is not an int", v)
}
}
func MarshalInt64(i int64) Marshaler {
return WriterFunc(func(w io.Writer) {
io.WriteString(w, strconv.FormatInt(i, 10))
})
}
func UnmarshalInt64(v interface{}) (int64, error) {
switch v := v.(type) {
case string:
return strconv.ParseInt(v, 10, 64)
case int:
return int64(v), nil
case int64:
return v, nil
case json.Number:
return strconv.ParseInt(string(v), 10, 64)
default:
return 0, fmt.Errorf("%T is not an int", v)
}
}
func MarshalInt32(i int32) Marshaler {
return WriterFunc(func(w io.Writer) {
io.WriteString(w, strconv.FormatInt(int64(i), 10))
})
}
func UnmarshalInt32(v interface{}) (int32, error) {
switch v := v.(type) {
case string:
iv, err := strconv.ParseInt(v, 10, 32)
if err != nil {
return 0, err
}
return int32(iv), nil
case int:
return int32(v), nil
case int64:
return int32(v), nil
case json.Number:
iv, err := strconv.ParseInt(string(v), 10, 32)
if err != nil {
return 0, err
}
return int32(iv), nil
default:
return 0, fmt.Errorf("%T is not an int", v)
}
}

View File

@@ -62,9 +62,9 @@ func (t *Type) Description() string {
func (t *Type) Fields(includeDeprecated bool) []Field {
if t.def == nil || (t.def.Kind != ast.Object && t.def.Kind != ast.Interface) {
return nil
return []Field{}
}
var fields []Field
fields := []Field{}
for _, f := range t.def.Fields {
if strings.HasPrefix(f.Name, "__") {
continue
@@ -93,10 +93,10 @@ func (t *Type) Fields(includeDeprecated bool) []Field {
func (t *Type) InputFields() []InputValue {
if t.def == nil || t.def.Kind != ast.InputObject {
return nil
return []InputValue{}
}
var res []InputValue
res := []InputValue{}
for _, f := range t.def.Fields {
res = append(res, InputValue{
Name: f.Name,
@@ -118,10 +118,10 @@ func defaultValue(value *ast.Value) *string {
func (t *Type) Interfaces() []Type {
if t.def == nil || t.def.Kind != ast.Object {
return nil
return []Type{}
}
var res []Type
res := []Type{}
for _, intf := range t.def.Interfaces {
res = append(res, *WrapTypeFromDef(t.schema, t.schema.Types[intf]))
}
@@ -131,10 +131,10 @@ func (t *Type) Interfaces() []Type {
func (t *Type) PossibleTypes() []Type {
if t.def == nil || (t.def.Kind != ast.Interface && t.def.Kind != ast.Union) {
return nil
return []Type{}
}
var res []Type
res := []Type{}
for _, pt := range t.schema.GetPossibleTypes(t.def) {
res = append(res, *WrapTypeFromDef(t.schema, pt))
}
@@ -143,10 +143,10 @@ func (t *Type) PossibleTypes() []Type {
func (t *Type) EnumValues(includeDeprecated bool) []EnumValue {
if t.def == nil || t.def.Kind != ast.Enum {
return nil
return []EnumValue{}
}
var res []EnumValue
res := []EnumValue{}
for _, val := range t.def.EnumValues {
res = append(res, EnumValue{
Name: val.Name,

7
vendor/github.com/99designs/gqlgen/graphql/root.go generated vendored Normal file
View File

@@ -0,0 +1,7 @@
package graphql
type Query struct{}
type Mutation struct{}
type Subscription struct{}

View File

@@ -1,3 +1,3 @@
package graphql
const Version = "dev"
const Version = "v0.8.2"

View File

@@ -7,6 +7,7 @@ import (
"io"
"net/http"
"strings"
"time"
"github.com/99designs/gqlgen/complexity"
"github.com/99designs/gqlgen/graphql"
@@ -33,7 +34,9 @@ type Config struct {
requestHook graphql.RequestMiddleware
tracer graphql.Tracer
complexityLimit int
complexityLimitFunc graphql.ComplexityLimitFunc
disableIntrospection bool
connectionKeepAlivePingInterval time.Duration
}
func (c *Config) newRequestContext(es graphql.ExecutableSchema, doc *ast.QueryDocument, op *ast.OperationDefinition, query string, variables map[string]interface{}) *graphql.RequestContext {
@@ -60,7 +63,7 @@ func (c *Config) newRequestContext(es graphql.ExecutableSchema, doc *ast.QueryDo
reqCtx.Tracer = hook
}
if c.complexityLimit > 0 {
if c.complexityLimit > 0 || c.complexityLimitFunc != nil {
reqCtx.ComplexityLimit = c.complexityLimit
operationComplexity := complexity.Calculate(es, op, variables)
reqCtx.OperationComplexity = operationComplexity
@@ -108,6 +111,15 @@ func ComplexityLimit(limit int) Option {
}
}
// ComplexityLimitFunc allows you to define a function to dynamically set the maximum query complexity that is allowed
// to be executed.
// If a query is submitted that exceeds the limit, a 422 status code will be returned.
func ComplexityLimitFunc(complexityLimitFunc graphql.ComplexityLimitFunc) Option {
return func(cfg *Config) {
cfg.complexityLimitFunc = complexityLimitFunc
}
}
// ResolverMiddleware allows you to define a function that will be called around every resolver,
// useful for logging.
func ResolverMiddleware(middleware graphql.FieldMiddleware) Option {
@@ -239,11 +251,23 @@ func CacheSize(size int) Option {
}
}
// WebsocketKeepAliveDuration allows you to reconfigure the keepalive behavior.
// By default, keepalive is enabled with a DefaultConnectionKeepAlivePingInterval
// duration. Set handler.connectionKeepAlivePingInterval = 0 to disable keepalive
// altogether.
func WebsocketKeepAliveDuration(duration time.Duration) Option {
return func(cfg *Config) {
cfg.connectionKeepAlivePingInterval = duration
}
}
const DefaultCacheSize = 1000
const DefaultConnectionKeepAlivePingInterval = 25 * time.Second
func GraphQL(exec graphql.ExecutableSchema, options ...Option) http.HandlerFunc {
cfg := &Config{
cacheSize: DefaultCacheSize,
connectionKeepAlivePingInterval: DefaultConnectionKeepAlivePingInterval,
upgrader: websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
@@ -297,6 +321,7 @@ func (gh *graphqlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
w.Header().Set("Content-Type", "application/json")
var reqParams params
switch r.Method {
case http.MethodGet:
@@ -318,7 +343,6 @@ func (gh *graphqlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "application/json")
ctx := r.Context()
@@ -367,6 +391,10 @@ func (gh *graphqlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}()
if gh.cfg.complexityLimitFunc != nil {
reqCtx.ComplexityLimit = gh.cfg.complexityLimitFunc(ctx)
}
if reqCtx.ComplexityLimit > 0 && reqCtx.OperationComplexity > reqCtx.ComplexityLimit {
sendErrorf(w, http.StatusUnprocessableEntity, "operation has complexity %d, which exceeds the limit of %d", reqCtx.OperationComplexity, reqCtx.ComplexityLimit)
return

View File

@@ -11,9 +11,12 @@ var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
<meta charset=utf-8/>
<meta name="viewport" content="user-scalable=no, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, minimal-ui">
<link rel="shortcut icon" href="https://graphcool-playground.netlify.com/favicon.png">
<link rel="stylesheet" href="//cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/css/index.css"/>
<link rel="shortcut icon" href="//cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/favicon.png"/>
<script src="//cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/js/middleware.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/css/index.css"
integrity="{{ .cssSRI }}" crossorigin="anonymous"/>
<link rel="shortcut icon" href="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/favicon.png"
integrity="{{ .faviconSRI }}" crossorigin="anonymous"/>
<script src="https://cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/js/middleware.js"
integrity="{{ .jsSRI }}" crossorigin="anonymous"></script>
<title>{{.title}}</title>
</head>
<body>
@@ -42,10 +45,14 @@ var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
func Playground(title string, endpoint string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html")
err := page.Execute(w, map[string]string{
"title": title,
"endpoint": endpoint,
"version": "1.7.8",
"version": "1.7.20",
"cssSRI": "sha256-cS9Vc2OBt9eUf4sykRWukeFYaInL29+myBmFDSa7F/U=",
"faviconSRI": "sha256-GhTyE+McTU79R4+pRO6ih+4TfsTOrpPwD8ReKFzb3PM=",
"jsSRI": "sha256-4QG1Uza2GgGdlBL3RCBCGtGeZB6bDbsw8OltCMGeJsA=",
})
if err != nil {
panic(err)

View File

@@ -8,6 +8,7 @@ import (
"log"
"net/http"
"sync"
"time"
"github.com/99designs/gqlgen/graphql"
"github.com/gorilla/websocket"
@@ -28,7 +29,7 @@ const (
dataMsg = "data" // Server -> Client
errorMsg = "error" // Server -> Client
completeMsg = "complete" // Server -> Client
//connectionKeepAliveMsg = "ka" // Server -> Client TODO: keepalives
connectionKeepAliveMsg = "ka" // Server -> Client
)
type operationMessage struct {
@@ -45,6 +46,7 @@ type wsConnection struct {
mu sync.Mutex
cfg *Config
cache *lru.Cache
keepAliveTicker *time.Ticker
initPayload InitPayload
}
@@ -112,6 +114,20 @@ func (c *wsConnection) write(msg *operationMessage) {
}
func (c *wsConnection) run() {
// We create a cancellation that will shutdown the keep-alive when we leave
// this function.
ctx, cancel := context.WithCancel(c.ctx)
defer cancel()
// Create a timer that will fire every interval to keep the connection alive.
if c.cfg.connectionKeepAlivePingInterval != 0 {
c.mu.Lock()
c.keepAliveTicker = time.NewTicker(c.cfg.connectionKeepAlivePingInterval)
c.mu.Unlock()
go c.keepAlive(ctx)
}
for {
message := c.readOp()
if message == nil {
@@ -144,6 +160,18 @@ func (c *wsConnection) run() {
}
}
func (c *wsConnection) keepAlive(ctx context.Context) {
for {
select {
case <-ctx.Done():
c.keepAliveTicker.Stop()
return
case <-c.keepAliveTicker.C:
c.write(&operationMessage{Type: connectionKeepAliveMsg})
}
}
}
func (c *wsConnection) subscribe(message *operationMessage) bool {
var reqParams params
if err := jsonDecode(bytes.NewReader(message.Payload), &reqParams); err != nil {

View File

@@ -184,13 +184,19 @@ func validateDefinition(schema *Schema, def *Definition) *gqlerror.Error {
}
}
for _, intf := range def.Interfaces {
intDef := schema.Types[intf]
if intDef == nil {
return gqlerror.ErrorPosf(def.Position, "Undefined type %s.", strconv.Quote(intf))
for _, typ := range def.Types {
typDef := schema.Types[typ]
if typDef == nil {
return gqlerror.ErrorPosf(def.Position, "Undefined type %s.", strconv.Quote(typ))
}
if intDef.Kind != Interface {
return gqlerror.ErrorPosf(def.Position, "%s is a non interface type %s.", strconv.Quote(intf), intDef.Kind)
if !isValidKind(typDef.Kind, Object) {
return gqlerror.ErrorPosf(def.Position, "%s type %s must be %s.", def.Kind, strconv.Quote(typ), kindList(Object))
}
}
for _, intf := range def.Interfaces {
if err := validateImplements(schema, def, intf); err != nil {
return err
}
}
@@ -199,6 +205,13 @@ func validateDefinition(schema *Schema, def *Definition) *gqlerror.Error {
if len(def.Fields) == 0 {
return gqlerror.ErrorPosf(def.Position, "%s must define one or more fields.", def.Kind)
}
for _, field := range def.Fields {
if typ, ok := schema.Types[field.Type.Name()]; ok {
if !isValidKind(typ.Kind, Scalar, Object, Interface, Union, Enum) {
return gqlerror.ErrorPosf(field.Position, "%s field must be one of %s.", def.Kind, kindList(Scalar, Object, Interface, Union, Enum))
}
}
}
case Enum:
if len(def.EnumValues) == 0 {
return gqlerror.ErrorPosf(def.Position, "%s must define one or more unique enum values.", def.Kind)
@@ -207,6 +220,13 @@ func validateDefinition(schema *Schema, def *Definition) *gqlerror.Error {
if len(def.Fields) == 0 {
return gqlerror.ErrorPosf(def.Position, "%s must define one or more input fields.", def.Kind)
}
for _, field := range def.Fields {
if typ, ok := schema.Types[field.Type.Name()]; ok {
if !isValidKind(typ.Kind, Scalar, Enum, InputObject) {
return gqlerror.ErrorPosf(field.Position, "%s field must be one of %s.", def.Kind, kindList(Scalar, Enum, InputObject))
}
}
}
}
for idx, field1 := range def.Fields {
@@ -244,6 +264,16 @@ func validateArgs(schema *Schema, args ArgumentDefinitionList, currentDirective
if err := validateTypeRef(schema, arg.Type); err != nil {
return err
}
def := schema.Types[arg.Type.Name()]
if !def.IsInputType() {
return gqlerror.ErrorPosf(
arg.Position,
"cannot use %s as argument %s because %s is not a valid input type",
arg.Type.String(),
arg.Name,
def.Kind,
)
}
if err := validateDirectives(schema, arg.Directives, currentDirective); err != nil {
return err
}
@@ -268,9 +298,104 @@ func validateDirectives(schema *Schema, dirs DirectiveList, currentDirective *Di
return nil
}
func validateImplements(schema *Schema, def *Definition, intfName string) *gqlerror.Error {
// see validation rules at the bottom of
// https://facebook.github.io/graphql/June2018/#sec-Objects
intf := schema.Types[intfName]
if intf == nil {
return gqlerror.ErrorPosf(def.Position, "Undefined type %s.", strconv.Quote(intfName))
}
if intf.Kind != Interface {
return gqlerror.ErrorPosf(def.Position, "%s is a non interface type %s.", strconv.Quote(intfName), intf.Kind)
}
for _, requiredField := range intf.Fields {
foundField := def.Fields.ForName(requiredField.Name)
if foundField == nil {
return gqlerror.ErrorPosf(def.Position,
`For %s to implement %s it must have a field called %s.`,
def.Name, intf.Name, requiredField.Name,
)
}
if !isCovariant(schema, requiredField.Type, foundField.Type) {
return gqlerror.ErrorPosf(foundField.Position,
`For %s to implement %s the field %s must have type %s.`,
def.Name, intf.Name, requiredField.Name, requiredField.Type.String(),
)
}
for _, requiredArg := range requiredField.Arguments {
foundArg := foundField.Arguments.ForName(requiredArg.Name)
if foundArg == nil {
return gqlerror.ErrorPosf(foundField.Position,
`For %s to implement %s the field %s must have the same arguments but it is missing %s.`,
def.Name, intf.Name, requiredField.Name, requiredArg.Name,
)
}
if !requiredArg.Type.IsCompatible(foundArg.Type) {
return gqlerror.ErrorPosf(foundArg.Position,
`For %s to implement %s the field %s must have the same arguments but %s has the wrong type.`,
def.Name, intf.Name, requiredField.Name, requiredArg.Name,
)
}
}
for _, foundArgs := range foundField.Arguments {
if requiredField.Arguments.ForName(foundArgs.Name) == nil && foundArgs.Type.NonNull && foundArgs.DefaultValue == nil {
return gqlerror.ErrorPosf(foundArgs.Position,
`For %s to implement %s any additional arguments on %s must be optional or have a default value but %s is required.`,
def.Name, intf.Name, foundField.Name, foundArgs.Name,
)
}
}
}
return nil
}
func isCovariant(schema *Schema, required *Type, actual *Type) bool {
if required.NonNull && !actual.NonNull {
return false
}
if required.NamedType != "" {
if required.NamedType == actual.NamedType {
return true
}
for _, pt := range schema.PossibleTypes[required.NamedType] {
if pt.Name == actual.NamedType {
return true
}
}
return false
}
if required.Elem != nil && actual.Elem == nil {
return false
}
return isCovariant(schema, required.Elem, actual.Elem)
}
func validateName(pos *Position, name string) *gqlerror.Error {
if strings.HasPrefix(name, "__") {
return gqlerror.ErrorPosf(pos, `Name "%s" must not begin with "__", which is reserved by GraphQL introspection.`, name)
}
return nil
}
func isValidKind(kind DefinitionKind, valid ...DefinitionKind) bool {
for _, k := range valid {
if kind == k {
return true
}
}
return false
}
func kindList(kinds ...DefinitionKind) string {
s := make([]string, len(kinds))
for i, k := range kinds {
s[i] = string(k)
}
return strings.Join(s, ", ")
}

View File

@@ -89,6 +89,18 @@ object types:
message: 'Name "__bar" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 2, column: 7}]
- name: must not allow input object as field type
input: |
input Input {
id: ID
}
type Query {
input: Input!
}
error:
message: 'OBJECT field must be one of SCALAR, OBJECT, INTERFACE, UNION, ENUM.'
locations: [{line: 5, column: 3}]
interfaces:
- name: must exist
input: |
@@ -148,6 +160,121 @@ interfaces:
message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 1, column: 11}]
- name: must not allow input object as field type
input: |
input Input {
id: ID
}
type Query {
foo: Foo!
}
interface Foo {
input: Input!
}
error:
message: 'INTERFACE field must be one of SCALAR, OBJECT, INTERFACE, UNION, ENUM.'
locations: [{line: 8, column: 3}]
- name: must have all fields from interface
input: |
type Bar implements BarInterface {
someField: Int!
}
interface BarInterface {
id: ID!
}
error:
message: 'For Bar to implement BarInterface it must have a field called id.'
locations: [{line: 1, column: 6}]
- name: must have same type of fields
input: |
type Bar implements BarInterface {
id: Int!
}
interface BarInterface {
id: ID!
}
error:
message: 'For Bar to implement BarInterface the field id must have type ID!.'
locations: [{line: 2, column: 5}]
- name: must have all required arguments
input: |
type Bar implements BarInterface {
id: ID!
}
interface BarInterface {
id(ff: Int!): ID!
}
error:
message: 'For Bar to implement BarInterface the field id must have the same arguments but it is missing ff.'
locations: [{line: 2, column: 5}]
- name: must have same argument types
input: |
type Bar implements BarInterface {
id(ff: ID!): ID!
}
interface BarInterface {
id(ff: Int!): ID!
}
error:
message: 'For Bar to implement BarInterface the field id must have the same arguments but ff has the wrong type.'
locations: [{line: 2, column: 8}]
- name: may defined additional nullable arguments
input: |
type Bar implements BarInterface {
id(opt: Int): ID!
}
interface BarInterface {
id: ID!
}
- name: may defined additional required arguments with defaults
input: |
type Bar implements BarInterface {
id(opt: Int! = 1): ID!
}
interface BarInterface {
id: ID!
}
- name: must not define additional required arguments without defaults
input: |
type Bar implements BarInterface {
id(opt: Int!): ID!
}
interface BarInterface {
id: ID!
}
error:
message: 'For Bar to implement BarInterface any additional arguments on id must be optional or have a default value but opt is required.'
locations: [{line: 2, column: 8}]
- name: can have covariant argument types
input: |
union U = A|B
type A { name: String }
type B { name: String }
type Bar implements BarInterface {
f: A!
}
interface BarInterface {
f: U!
}
inputs:
- name: must define one or more input fields
input: |
@@ -177,6 +304,70 @@ inputs:
message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 1, column: 7}]
- name: fields cannot be Objects
input: |
type Object { id: ID }
input Foo { a: Object! }
error:
message: INPUT_OBJECT field must be one of SCALAR, ENUM, INPUT_OBJECT.
locations: [{line: 2, column: 13}]
- name: fields cannot be Interfaces
input: |
interface Interface { id: ID! }
input Foo { a: Interface! }
error:
message: INPUT_OBJECT field must be one of SCALAR, ENUM, INPUT_OBJECT.
locations: [{line: 2, column: 13}]
- name: fields cannot be Unions
input: |
type Object { id: ID }
union Union = Object
input Foo { a: Union! }
error:
message: INPUT_OBJECT field must be one of SCALAR, ENUM, INPUT_OBJECT.
locations: [{line: 3, column: 13}]
args:
- name: Valid arg types
input: |
input Input { id: ID }
enum Enum { A }
scalar Scalar
type Query {
f(a: Input, b: Scalar, c: Enum): Boolean!
}
- name: Objects not allowed
input: |
type Object { id: ID }
type Query { f(a: Object): Boolean! }
error:
message: 'cannot use Object as argument a because OBJECT is not a valid input type'
locations: [{line: 2, column: 16}]
- name: Union not allowed
input: |
type Object { id: ID }
union Union = Object
type Query { f(a: Union): Boolean! }
error:
message: 'cannot use Union as argument a because UNION is not a valid input type'
locations: [{line: 3, column: 16}]
- name: Interface not allowed
input: |
interface Interface { id: ID }
type Query { f(a: Interface): Boolean! }
error:
message: 'cannot use Interface as argument a because INTERFACE is not a valid input type'
locations: [{line: 2, column: 16}]
enums:
- name: must define one or more unique enum values
input: |
@@ -207,6 +398,26 @@ enums:
message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 1, column: 6}]
unions:
- name: union types must be defined
input: |
union Foo = Bar | Baz
type Bar {
id: ID
}
error:
message: "Undefined type \"Baz\"."
locations: [{line: 1, column: 7}]
- name: union types must be objects
input: |
union Foo = Baz
interface Baz {
id: ID
}
error:
message: "UNION type \"Baz\" must be OBJECT."
locations: [{line: 1, column: 7}]
type extensions:
- name: cannot extend non existant types
input: |
@@ -258,6 +469,42 @@ directives:
message: 'Name "__A" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 1, column: 12}]
- name: Valid arg types
input: |
input Input { id: ID }
enum Enum { A }
scalar Scalar
directive @A(a: Input, b: Scalar, c: Enum) on FIELD_DEFINITION
- name: Objects not allowed
input: |
type Object { id: ID }
directive @A(a: Object) on FIELD_DEFINITION
error:
message: 'cannot use Object as argument a because OBJECT is not a valid input type'
locations: [{line: 2, column: 14}]
- name: Union not allowed
input: |
type Object { id: ID }
union Union = Object
directive @A(a: Union) on FIELD_DEFINITION
error:
message: 'cannot use Union as argument a because UNION is not a valid input type'
locations: [{line: 3, column: 14}]
- name: Interface not allowed
input: |
interface Interface { id: ID }
directive @A(a: Interface) on FIELD_DEFINITION
error:
message: 'cannot use Interface as argument a because INTERFACE is not a valid input type'
locations: [{line: 2, column: 14}]
entry points:
- name: multiple schema entry points
input: |

4
vendor/modules.txt vendored
View File

@@ -1,4 +1,4 @@
# github.com/99designs/gqlgen v0.4.5-0.20190127090136-055fb4bc9a6a
# github.com/99designs/gqlgen v0.8.2
github.com/99designs/gqlgen/handler
github.com/99designs/gqlgen/graphql
github.com/99designs/gqlgen/graphql/introspection
@@ -124,7 +124,7 @@ github.com/spf13/jwalterweatherman
github.com/spf13/pflag
# github.com/spf13/viper v1.3.2
github.com/spf13/viper
# github.com/vektah/gqlparser v1.1.0
# github.com/vektah/gqlparser v1.1.2
github.com/vektah/gqlparser
github.com/vektah/gqlparser/ast
github.com/vektah/gqlparser/gqlerror