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)
return val
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 {
gqlerr.Path = GetResolverContext(ctx).Path()
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"
@ -25,15 +26,17 @@ type params struct {
}
type Config struct {
cacheSize int
upgrader websocket.Upgrader
recover graphql.RecoverFunc
errorPresenter graphql.ErrorPresenterFunc
resolverHook graphql.FieldMiddleware
requestHook graphql.RequestMiddleware
tracer graphql.Tracer
complexityLimit int
disableIntrospection bool
cacheSize int
upgrader websocket.Upgrader
recover graphql.RecoverFunc
errorPresenter graphql.ErrorPresenterFunc
resolverHook graphql.FieldMiddleware
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,
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",
"title": title,
"endpoint": endpoint,
"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 {
@ -38,13 +39,14 @@ type operationMessage struct {
}
type wsConnection struct {
ctx context.Context
conn *websocket.Conn
exec graphql.ExecutableSchema
active map[string]context.CancelFunc
mu sync.Mutex
cfg *Config
cache *lru.Cache
ctx context.Context
conn *websocket.Conn
exec graphql.ExecutableSchema
active map[string]context.CancelFunc
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