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 module github.com/stashapp/stash
require ( 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/PuerkitoBio/goquery v1.5.0
github.com/bmatcuk/doublestar v1.1.1 github.com/bmatcuk/doublestar v1.1.1
github.com/disintegration/imaging v1.6.0 github.com/disintegration/imaging v1.6.0
@@ -17,6 +17,6 @@ require (
github.com/sirupsen/logrus v1.3.0 github.com/sirupsen/logrus v1.3.0
github.com/spf13/afero v1.2.0 // indirect github.com/spf13/afero v1.2.0 // indirect
github.com/spf13/viper v1.3.2 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 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= 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 h1:oTsAt8YXjEk1fo7uZR7gya1jrH48oPulx5oF6zWTHRw=
github.com/99designs/gqlgen v0.4.5-0.20190127090136-055fb4bc9a6a/go.mod h1:st7qHA6ssU3uRZkmv+wzrzgX4srvIqEIdE5iuRW8GhE= 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/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 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 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/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 h1:3668p2gUlO+PiS81x957Rpr3/FPRWG6cxgCXAvTS1hw=
github.com/vektah/gqlparser v1.1.0/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw= 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/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/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= 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/schema.graphql - "graphql/schema/types/*.graphql"
- "graphql/schema/*.graphql"
exec: exec:
filename: pkg/models/generated_exec.go filename: pkg/models/generated_exec.go
model: 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 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() scenesQB := models.NewSceneQueryBuilder()
scenesCount, _ := scenesQB.Count() scenesCount, _ := scenesQB.Count()
galleryQB := models.NewGalleryQueryBuilder() galleryQB := models.NewGalleryQueryBuilder()
@@ -95,7 +95,7 @@ func (r *queryResolver) Stats(ctx context.Context) (models.StatsResultType, erro
studiosCount, _ := studiosQB.Count() studiosCount, _ := studiosQB.Count()
tagsQB := models.NewTagQueryBuilder() tagsQB := models.NewTagQueryBuilder()
tagsCount, _ := tagsQB.Count() tagsCount, _ := tagsQB.Count()
return models.StatsResultType{ return &models.StatsResultType{
SceneCount: scenesCount, SceneCount: scenesCount,
GalleryCount: galleryCount, GalleryCount: galleryCount,
PerformerCount: performersCount, PerformerCount: performersCount,

View File

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

View File

@@ -4,13 +4,8 @@ import (
"context" "context"
"github.com/stashapp/stash/pkg/api/urlbuilders" "github.com/stashapp/stash/pkg/api/urlbuilders"
"github.com/stashapp/stash/pkg/models" "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) { func (r *performerResolver) Name(ctx context.Context, obj *models.Performer) (*string, error) {
if obj.Name.Valid { if obj.Name.Valid {
return &obj.Name.String, nil return &obj.Name.String, nil

View File

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

View File

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

View File

@@ -4,13 +4,8 @@ import (
"context" "context"
"github.com/stashapp/stash/pkg/api/urlbuilders" "github.com/stashapp/stash/pkg/api/urlbuilders"
"github.com/stashapp/stash/pkg/models" "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) { func (r *studioResolver) Name(ctx context.Context, obj *models.Studio) (string, error) {
if obj.Name.Valid { if obj.Name.Valid {
return obj.Name.String, nil return obj.Name.String, nil

View File

@@ -3,13 +3,8 @@ package api
import ( import (
"context" "context"
"github.com/stashapp/stash/pkg/models" "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) { func (r *tagResolver) SceneCount(ctx context.Context, obj *models.Tag) (*int, error) {
qb := models.NewSceneQueryBuilder() qb := models.NewSceneQueryBuilder()
if obj == nil { if obj == nil {

View File

@@ -9,7 +9,7 @@ import (
"path/filepath" "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 { if len(input.Stashes) > 0 {
for _, stashPath := range input.Stashes { for _, stashPath := range input.Stashes {
exists, err := utils.DirExists(stashPath) exists, err := utils.DirExists(stashPath)

View File

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

View File

@@ -12,10 +12,10 @@ func (r *queryResolver) FindGallery(ctx context.Context, id string) (*models.Gal
return qb.Find(idInt) 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() qb := models.NewGalleryQueryBuilder()
galleries, total := qb.Query(filter) galleries, total := qb.Query(filter)
return models.FindGalleriesResultType{ return &models.FindGalleriesResultType{
Count: total, Count: total,
Galleries: galleries, Galleries: galleries,
}, nil }, nil

View File

@@ -12,10 +12,10 @@ func (r *queryResolver) FindPerformer(ctx context.Context, id string) (*models.P
return qb.Find(idInt) 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() qb := models.NewPerformerQueryBuilder()
performers, total := qb.Query(performer_filter, filter) performers, total := qb.Query(performerFilter, filter)
return models.FindPerformersResultType{ return &models.FindPerformersResultType{
Count: total, Count: total,
Performers: performers, Performers: performers,
}, nil }, nil

View File

@@ -19,10 +19,10 @@ func (r *queryResolver) FindScene(ctx context.Context, id *string, checksum *str
return scene, err 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() qb := models.NewSceneQueryBuilder()
scenes, total := qb.Query(scene_filter, filter) scenes, total := qb.Query(sceneFilter, filter)
return models.FindScenesResultType{ return &models.FindScenesResultType{
Count: total, Count: total,
Scenes: scenes, Scenes: scenes,
}, nil }, nil

View File

@@ -5,10 +5,10 @@ import (
"github.com/stashapp/stash/pkg/models" "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() qb := models.NewSceneMarkerQueryBuilder()
sceneMarkers, total := qb.Query(scene_marker_filter, filter) sceneMarkers, total := qb.Query(sceneMarkerFilter, filter)
return models.FindSceneMarkersResultType{ return &models.FindSceneMarkersResultType{
Count: total, Count: total,
SceneMarkers: sceneMarkers, SceneMarkers: sceneMarkers,
}, nil }, nil

View File

@@ -12,10 +12,10 @@ func (r *queryResolver) FindStudio(ctx context.Context, id string) (*models.Stud
return qb.Find(idInt, nil) 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() qb := models.NewStudioQueryBuilder()
studios, total := qb.Query(filter) studios, total := qb.Query(filter)
return models.FindStudiosResultType{ return &models.FindStudiosResultType{
Count: total, Count: total,
Studios: studios, Studios: studios,
}, nil }, 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 overwrite: true
generates: generates:
./../../schema/schema.json: ./../../schema/schema.json:
- introspection - introspection
./src/app/core/graphql-generated.ts: ./src/app/core/graphql-generated.ts:
documents: ./../../schema/documents/**/*.graphql documents: ./../../graphql/documents/**/*.graphql
plugins: plugins:
- add: "/* tslint:disable */" - add: "/* tslint:disable */"
- time - time

View File

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

View File

@@ -12,6 +12,7 @@ import (
type Resolver func(ctx context.Context) (res interface{}, err error) type Resolver func(ctx context.Context) (res interface{}, err error)
type FieldMiddleware func(ctx context.Context, next Resolver) (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 RequestMiddleware func(ctx context.Context, next func(ctx context.Context) []byte) []byte
type ComplexityLimitFunc func(ctx context.Context) int
type RequestContext struct { type RequestContext struct {
RawQuery string RawQuery string
@@ -71,12 +72,10 @@ const (
) )
func GetRequestContext(ctx context.Context) *RequestContext { func GetRequestContext(ctx context.Context) *RequestContext {
val := ctx.Value(request) if val, ok := ctx.Value(request).(*RequestContext); ok {
if val == nil { return val
return nil
} }
return nil
return val.(*RequestContext)
} }
func WithRequestContext(ctx context.Context, rc *RequestContext) context.Context { func WithRequestContext(ctx context.Context, rc *RequestContext) context.Context {
@@ -95,6 +94,8 @@ type ResolverContext struct {
Index *int Index *int
// The result object of resolver // The result object of resolver
Result interface{} Result interface{}
// IsMethod indicates if the resolver is a method
IsMethod bool
} }
func (r *ResolverContext) Path() []interface{} { func (r *ResolverContext) Path() []interface{} {
@@ -117,8 +118,10 @@ func (r *ResolverContext) Path() []interface{} {
} }
func GetResolverContext(ctx context.Context) *ResolverContext { func GetResolverContext(ctx context.Context) *ResolverContext {
val, _ := ctx.Value(resolver).(*ResolverContext) if val, ok := ctx.Value(resolver).(*ResolverContext); ok {
return val return val
}
return nil
} }
func WithResolverContext(ctx context.Context, rc *ResolverContext) context.Context { 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) 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. // Errorf sends an error string to the client, passing it through the formatter.
func (c *RequestContext) Errorf(ctx context.Context, format string, args ...interface{}) { func (c *RequestContext) Errorf(ctx context.Context, format string, args ...interface{}) {
c.errorsMu.Lock() c.errorsMu.Lock()

View File

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

View File

@@ -16,6 +16,9 @@ type ExecutableSchema interface {
Subscription(ctx context.Context, op *ast.OperationDefinition) func() *Response 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 { func CollectFields(ctx context.Context, selSet ast.SelectionSet, satisfies []string) []CollectedField {
return collectFields(GetRequestContext(ctx), selSet, satisfies, map[string]bool{}) 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...) f.Selections = append(f.Selections, sel.SelectionSet...)
case *ast.InlineFragment: 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 continue
} }
for _, childField := range collectFields(reqCtx, sel.SelectionSet, satisfies, visited) { 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)) panic(fmt.Errorf("missing fragment %s", fragmentName))
} }
if !instanceOf(fragment.TypeCondition, satisfies) { if len(satisfies) > 0 && !instanceOf(fragment.TypeCondition, satisfies) {
continue continue
} }

View File

@@ -34,3 +34,24 @@ func UnmarshalID(v interface{}) (string, error) {
return "", fmt.Errorf("%T is not a string", v) 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) 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 { func (t *Type) Fields(includeDeprecated bool) []Field {
if t.def == nil || (t.def.Kind != ast.Object && t.def.Kind != ast.Interface) { 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 { for _, f := range t.def.Fields {
if strings.HasPrefix(f.Name, "__") { if strings.HasPrefix(f.Name, "__") {
continue continue
@@ -93,10 +93,10 @@ func (t *Type) Fields(includeDeprecated bool) []Field {
func (t *Type) InputFields() []InputValue { func (t *Type) InputFields() []InputValue {
if t.def == nil || t.def.Kind != ast.InputObject { if t.def == nil || t.def.Kind != ast.InputObject {
return nil return []InputValue{}
} }
var res []InputValue res := []InputValue{}
for _, f := range t.def.Fields { for _, f := range t.def.Fields {
res = append(res, InputValue{ res = append(res, InputValue{
Name: f.Name, Name: f.Name,
@@ -118,10 +118,10 @@ func defaultValue(value *ast.Value) *string {
func (t *Type) Interfaces() []Type { func (t *Type) Interfaces() []Type {
if t.def == nil || t.def.Kind != ast.Object { if t.def == nil || t.def.Kind != ast.Object {
return nil return []Type{}
} }
var res []Type res := []Type{}
for _, intf := range t.def.Interfaces { for _, intf := range t.def.Interfaces {
res = append(res, *WrapTypeFromDef(t.schema, t.schema.Types[intf])) res = append(res, *WrapTypeFromDef(t.schema, t.schema.Types[intf]))
} }
@@ -131,10 +131,10 @@ func (t *Type) Interfaces() []Type {
func (t *Type) PossibleTypes() []Type { func (t *Type) PossibleTypes() []Type {
if t.def == nil || (t.def.Kind != ast.Interface && t.def.Kind != ast.Union) { 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) { for _, pt := range t.schema.GetPossibleTypes(t.def) {
res = append(res, *WrapTypeFromDef(t.schema, pt)) res = append(res, *WrapTypeFromDef(t.schema, pt))
} }
@@ -143,10 +143,10 @@ func (t *Type) PossibleTypes() []Type {
func (t *Type) EnumValues(includeDeprecated bool) []EnumValue { func (t *Type) EnumValues(includeDeprecated bool) []EnumValue {
if t.def == nil || t.def.Kind != ast.Enum { if t.def == nil || t.def.Kind != ast.Enum {
return nil return []EnumValue{}
} }
var res []EnumValue res := []EnumValue{}
for _, val := range t.def.EnumValues { for _, val := range t.def.EnumValues {
res = append(res, EnumValue{ res = append(res, EnumValue{
Name: val.Name, 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 package graphql
const Version = "dev" const Version = "v0.8.2"

View File

@@ -7,6 +7,7 @@ import (
"io" "io"
"net/http" "net/http"
"strings" "strings"
"time"
"github.com/99designs/gqlgen/complexity" "github.com/99designs/gqlgen/complexity"
"github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql"
@@ -25,15 +26,17 @@ type params struct {
} }
type Config struct { type Config struct {
cacheSize int cacheSize int
upgrader websocket.Upgrader upgrader websocket.Upgrader
recover graphql.RecoverFunc recover graphql.RecoverFunc
errorPresenter graphql.ErrorPresenterFunc errorPresenter graphql.ErrorPresenterFunc
resolverHook graphql.FieldMiddleware resolverHook graphql.FieldMiddleware
requestHook graphql.RequestMiddleware requestHook graphql.RequestMiddleware
tracer graphql.Tracer tracer graphql.Tracer
complexityLimit int complexityLimit int
disableIntrospection bool 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 { 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 reqCtx.Tracer = hook
} }
if c.complexityLimit > 0 { if c.complexityLimit > 0 || c.complexityLimitFunc != nil {
reqCtx.ComplexityLimit = c.complexityLimit reqCtx.ComplexityLimit = c.complexityLimit
operationComplexity := complexity.Calculate(es, op, variables) operationComplexity := complexity.Calculate(es, op, variables)
reqCtx.OperationComplexity = operationComplexity 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, // ResolverMiddleware allows you to define a function that will be called around every resolver,
// useful for logging. // useful for logging.
func ResolverMiddleware(middleware graphql.FieldMiddleware) Option { 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 DefaultCacheSize = 1000
const DefaultConnectionKeepAlivePingInterval = 25 * time.Second
func GraphQL(exec graphql.ExecutableSchema, options ...Option) http.HandlerFunc { func GraphQL(exec graphql.ExecutableSchema, options ...Option) http.HandlerFunc {
cfg := &Config{ cfg := &Config{
cacheSize: DefaultCacheSize, cacheSize: DefaultCacheSize,
connectionKeepAlivePingInterval: DefaultConnectionKeepAlivePingInterval,
upgrader: websocket.Upgrader{ upgrader: websocket.Upgrader{
ReadBufferSize: 1024, ReadBufferSize: 1024,
WriteBufferSize: 1024, WriteBufferSize: 1024,
@@ -297,6 +321,7 @@ func (gh *graphqlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
w.Header().Set("Content-Type", "application/json")
var reqParams params var reqParams params
switch r.Method { switch r.Method {
case http.MethodGet: case http.MethodGet:
@@ -318,7 +343,6 @@ func (gh *graphqlHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusMethodNotAllowed) w.WriteHeader(http.StatusMethodNotAllowed)
return return
} }
w.Header().Set("Content-Type", "application/json")
ctx := r.Context() 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 { 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) sendErrorf(w, http.StatusUnprocessableEntity, "operation has complexity %d, which exceeds the limit of %d", reqCtx.OperationComplexity, reqCtx.ComplexityLimit)
return return

View File

@@ -11,9 +11,12 @@ var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
<meta charset=utf-8/> <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"> <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="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="stylesheet" href="https://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"/> integrity="{{ .cssSRI }}" crossorigin="anonymous"/>
<script src="//cdn.jsdelivr.net/npm/graphql-playground-react@{{ .version }}/build/static/js/middleware.js"></script> <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> <title>{{.title}}</title>
</head> </head>
<body> <body>
@@ -42,10 +45,14 @@ var page = template.Must(template.New("graphiql").Parse(`<!DOCTYPE html>
func Playground(title string, endpoint string) http.HandlerFunc { func Playground(title string, endpoint string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html")
err := page.Execute(w, map[string]string{ err := page.Execute(w, map[string]string{
"title": title, "title": title,
"endpoint": endpoint, "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 { if err != nil {
panic(err) panic(err)

View File

@@ -8,6 +8,7 @@ import (
"log" "log"
"net/http" "net/http"
"sync" "sync"
"time"
"github.com/99designs/gqlgen/graphql" "github.com/99designs/gqlgen/graphql"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
@@ -28,7 +29,7 @@ const (
dataMsg = "data" // Server -> Client dataMsg = "data" // Server -> Client
errorMsg = "error" // Server -> Client errorMsg = "error" // Server -> Client
completeMsg = "complete" // Server -> Client completeMsg = "complete" // Server -> Client
//connectionKeepAliveMsg = "ka" // Server -> Client TODO: keepalives connectionKeepAliveMsg = "ka" // Server -> Client
) )
type operationMessage struct { type operationMessage struct {
@@ -38,13 +39,14 @@ type operationMessage struct {
} }
type wsConnection struct { type wsConnection struct {
ctx context.Context ctx context.Context
conn *websocket.Conn conn *websocket.Conn
exec graphql.ExecutableSchema exec graphql.ExecutableSchema
active map[string]context.CancelFunc active map[string]context.CancelFunc
mu sync.Mutex mu sync.Mutex
cfg *Config cfg *Config
cache *lru.Cache cache *lru.Cache
keepAliveTicker *time.Ticker
initPayload InitPayload initPayload InitPayload
} }
@@ -112,6 +114,20 @@ func (c *wsConnection) write(msg *operationMessage) {
} }
func (c *wsConnection) run() { 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 { for {
message := c.readOp() message := c.readOp()
if message == nil { 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 { func (c *wsConnection) subscribe(message *operationMessage) bool {
var reqParams params var reqParams params
if err := jsonDecode(bytes.NewReader(message.Payload), &reqParams); err != nil { 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 { for _, typ := range def.Types {
intDef := schema.Types[intf] typDef := schema.Types[typ]
if intDef == nil { if typDef == nil {
return gqlerror.ErrorPosf(def.Position, "Undefined type %s.", strconv.Quote(intf)) return gqlerror.ErrorPosf(def.Position, "Undefined type %s.", strconv.Quote(typ))
} }
if intDef.Kind != Interface { if !isValidKind(typDef.Kind, Object) {
return gqlerror.ErrorPosf(def.Position, "%s is a non interface type %s.", strconv.Quote(intf), intDef.Kind) 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 { if len(def.Fields) == 0 {
return gqlerror.ErrorPosf(def.Position, "%s must define one or more fields.", def.Kind) 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: case Enum:
if len(def.EnumValues) == 0 { if len(def.EnumValues) == 0 {
return gqlerror.ErrorPosf(def.Position, "%s must define one or more unique enum values.", def.Kind) 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 { if len(def.Fields) == 0 {
return gqlerror.ErrorPosf(def.Position, "%s must define one or more input fields.", def.Kind) 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 { 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 { if err := validateTypeRef(schema, arg.Type); err != nil {
return err 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 { if err := validateDirectives(schema, arg.Directives, currentDirective); err != nil {
return err return err
} }
@@ -268,9 +298,104 @@ func validateDirectives(schema *Schema, dirs DirectiveList, currentDirective *Di
return nil 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 { func validateName(pos *Position, name string) *gqlerror.Error {
if strings.HasPrefix(name, "__") { if strings.HasPrefix(name, "__") {
return gqlerror.ErrorPosf(pos, `Name "%s" must not begin with "__", which is reserved by GraphQL introspection.`, name) return gqlerror.ErrorPosf(pos, `Name "%s" must not begin with "__", which is reserved by GraphQL introspection.`, name)
} }
return nil 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.' message: 'Name "__bar" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 2, column: 7}] 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: interfaces:
- name: must exist - name: must exist
input: | input: |
@@ -148,6 +160,121 @@ interfaces:
message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.' message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 1, column: 11}] 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: inputs:
- name: must define one or more input fields - name: must define one or more input fields
input: | input: |
@@ -177,6 +304,70 @@ inputs:
message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.' message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 1, column: 7}] 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: enums:
- name: must define one or more unique enum values - name: must define one or more unique enum values
input: | input: |
@@ -207,6 +398,26 @@ enums:
message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.' message: 'Name "__FooBar" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 1, column: 6}] 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: type extensions:
- name: cannot extend non existant types - name: cannot extend non existant types
input: | input: |
@@ -258,6 +469,42 @@ directives:
message: 'Name "__A" must not begin with "__", which is reserved by GraphQL introspection.' message: 'Name "__A" must not begin with "__", which is reserved by GraphQL introspection.'
locations: [{line: 1, column: 12}] 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: entry points:
- name: multiple schema entry points - name: multiple schema entry points
input: | 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/handler
github.com/99designs/gqlgen/graphql github.com/99designs/gqlgen/graphql
github.com/99designs/gqlgen/graphql/introspection github.com/99designs/gqlgen/graphql/introspection
@@ -124,7 +124,7 @@ github.com/spf13/jwalterweatherman
github.com/spf13/pflag github.com/spf13/pflag
# github.com/spf13/viper v1.3.2 # github.com/spf13/viper v1.3.2
github.com/spf13/viper 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
github.com/vektah/gqlparser/ast github.com/vektah/gqlparser/ast
github.com/vektah/gqlparser/gqlerror github.com/vektah/gqlparser/gqlerror