mirror of https://github.com/stashapp/stash.git
Update GQLGen and break up the schema.graphql file
This commit is contained in:
parent
2e57c2a17a
commit
763424bc40
4
go.mod
4
go.mod
|
@ -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
4
go.sum
|
@ -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=
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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!
|
||||
}
|
|
@ -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!
|
||||
}
|
|
@ -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!]!
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
input GenerateMetadataInput {
|
||||
sprites: Boolean!
|
||||
previews: Boolean!
|
||||
markers: Boolean!
|
||||
transcodes: Boolean!
|
||||
}
|
|
@ -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!]!
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
type SceneMarkerTag {
|
||||
tag: Tag!
|
||||
scene_markers: [SceneMarker!]!
|
||||
}
|
|
@ -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!
|
||||
}
|
|
@ -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!]!
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
type StatsResultType {
|
||||
scene_count: Int!
|
||||
gallery_count: Int!
|
||||
performer_count: Int!
|
||||
studio_count: Int!
|
||||
tag_count: Int!
|
||||
}
|
|
@ -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!]!
|
||||
}
|
|
@ -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!
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
@ -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
|
||||
}
|
4071
schema/schema.json
4071
schema/schema.json
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package graphql
|
||||
|
||||
type Query struct{}
|
||||
|
||||
type Mutation struct{}
|
||||
|
||||
type Subscription struct{}
|
|
@ -1,3 +1,3 @@
|
|||
package graphql
|
||||
|
||||
const Version = "dev"
|
||||
const Version = "v0.8.2"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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, ", ")
|
||||
}
|
||||
|
|
|
@ -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: |
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue