diff --git a/schema/api.graphql b/schema/api.graphql index 1938acaea..df34cf0b0 100644 --- a/schema/api.graphql +++ b/schema/api.graphql @@ -19,7 +19,9 @@ directive @bind( enum CallingConvention { FirstArgCurrentUser, - FirstArgCurrentObject + FirstArgInput, + FirstArgCurrentObject, + InputAsArgs } enum AuthHook { @@ -347,7 +349,7 @@ type Timeslot implements Node & MyRadioObject @auth(hook: ViewShow) { photo: String messages: [Message] webpage: String! - + uploadState: String @meta(key: "upload_state") tracklist: [TracklistItem!] @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_TracklistItem", method: "getTracklistForTimeslot", callingConvention: FirstArgCurrentObject) uploadState: String @meta(key: "upload_state") } @@ -462,8 +464,70 @@ type Query { allGenres: [Genre!] @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Scheduler", method: "getGenres") @auth(constants: []) allCreditTypes: [CreditType!] @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Scheduler", method: "getCreditTypes") @auth(constants: []) + + findMemberByName(name: String!, limit: Int): [MemberSearchResult!] @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_User", method: "findByName") @auth(constants: ["AUTH_APPLYFORSHOW"]) # TODO not 100% sure this is the best constant + findShowByTitle(term: String!, limit: Int!): [FindShowByTitleResult!] @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Scheduler", method: "findShowByTitle") @auth(constants: ["AUTH_APPLYFORSHOW"]) # TODO ditto +} + +#input ShowCreditInput { # this is icky +# memberid: [Int!]! +# credittype: [Int!]! +#} +# +#input CreateShowInput { +# title: String! +# description: HTMLString! +# credits: ShowCreditInput! +# genres: [Int!] +# tags: [String!] +# podcast_explicit: Boolean +# subtype: String! # TODO: not a string +# mixclouder: Boolean +# "Unused" +# location: Int +#} +# +#input CreateSeasonWeeks { +# wk1: Boolean! +# wk2: Boolean! +# wk3: Boolean! +# wk4: Boolean! +# wk5: Boolean! +# wk6: Boolean! +# wk7: Boolean! +# wk8: Boolean! +# wk9: Boolean! +# wk10: Boolean! +#} +# +#input CreateSeasonTimes { +# day: [Int!]! +# "Seconds since midnight" +# stime: [Int!]! +# "Seconds since midnight" +# etime: [Int!]! +#} +# +#input CreateSeasonInput { +# show_id: Int! +# weeks: CreateSeasonWeeks! +# times: CreateSeasonTimes! +# tags: [String!] +# description: HTMLString +# subtype: String +#} + +input SendMessageInput { + timeslotId: Int! + message: String! +} + +type Mutation { +# createShow(input: CreateShowInput): Show @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Show", method: "create", callingConvention: FirstArgInput) + sendMessageToTimeslot(input: SendMessageInput): Show @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Timeslot", method: "sendMessageToTimeslot", callingConvention: InputAsArgs) } schema { query: Query + mutation: Mutation } diff --git a/src/Classes/MyRadio/GraphQLUtils.php b/src/Classes/MyRadio/GraphQLUtils.php index 8cd861f13..a55d1a40d 100644 --- a/src/Classes/MyRadio/GraphQLUtils.php +++ b/src/Classes/MyRadio/GraphQLUtils.php @@ -108,7 +108,7 @@ public static function isAuthorisedToAccess( $resolvedObject = null ) { $caller = MyRadio_Swagger2::getAPICaller(); - if ($caller === null) { + if (empty($caller)) { throw new MyRadioException('No valid authentication data provided', 401); } if ($caller->hasAuth(AUTH_APISUDO)) { diff --git a/src/Classes/ServiceAPI/MyRadio_Season.php b/src/Classes/ServiceAPI/MyRadio_Season.php index b24aa7c50..8be3f85cc 100644 --- a/src/Classes/ServiceAPI/MyRadio_Season.php +++ b/src/Classes/ServiceAPI/MyRadio_Season.php @@ -180,7 +180,13 @@ public static function create($params = []) throw new MyRadioException('Parameter '.$field.' was not provided.', 400); } } - $tags = (!empty($params['tags'])) ? CoreUtils::explodeTags($params['tags']) : []; + if (empty($params['tags'])) { + $tags = []; + } else if (is_array($params['tags'])) { + $tags = $params['tags']; + } else { + $tags = CoreUtils::explodeTags($params['tags']); + } /** * Select an appropriate value for $term_id. diff --git a/src/Classes/ServiceAPI/MyRadio_Show.php b/src/Classes/ServiceAPI/MyRadio_Show.php index 6fa986b51..e2c64a1d2 100755 --- a/src/Classes/ServiceAPI/MyRadio_Show.php +++ b/src/Classes/ServiceAPI/MyRadio_Show.php @@ -253,7 +253,7 @@ public static function create($params = []) $params['genres'] = []; } if (!isset($params['tags'])) { - $params['tags'] = ''; + $params['tags'] = []; } // Support API calls where there is no session. @@ -312,7 +312,10 @@ public static function create($params = []) } // Explode the tags - $tags = CoreUtils::explodeTags($params['tags']); + $tags = $params['tags']; + if(!(is_array($tags))) { + $tags = CoreUtils::explodeTags($params['tags']); + } foreach ($tags as $tag) { self::$db->query( 'INSERT INTO schedule.show_metadata diff --git a/src/Classes/ServiceAPI/MyRadio_ShowSubtype.php b/src/Classes/ServiceAPI/MyRadio_ShowSubtype.php index f6e94ec7e..70381355f 100644 --- a/src/Classes/ServiceAPI/MyRadio_ShowSubtype.php +++ b/src/Classes/ServiceAPI/MyRadio_ShowSubtype.php @@ -107,7 +107,7 @@ public static function getAll() $subtypes[] = new self($row); } - return CoreUtils::setToDataSource($subtypes); + return $subtypes; } /** diff --git a/src/Classes/ServiceAPI/MyRadio_Timeslot.php b/src/Classes/ServiceAPI/MyRadio_Timeslot.php index 388555b7a..605e2f202 100644 --- a/src/Classes/ServiceAPI/MyRadio_Timeslot.php +++ b/src/Classes/ServiceAPI/MyRadio_Timeslot.php @@ -1264,6 +1264,18 @@ public function sendMessage($message) return $this; } + /** + * Ditto, but a helper for GraphQL + * @param int $timeslotid + * @param string $message + */ + public static function sendMessageToTimeslot($timeslotid, $message) + { + /** @var self $timeslot */ + $timeslot = self::getInstance($timeslotid); + $timeslot->sendMessage($message); + } + /** * Signs the given user into the timeslot to say they were * on air at this time, if they haven't been signed in already. diff --git a/src/Controllers/api/graphql.php b/src/Controllers/api/graphql.php index 8f7c8e24d..01a530689 100644 --- a/src/Controllers/api/graphql.php +++ b/src/Controllers/api/graphql.php @@ -108,47 +108,7 @@ ] ); }; - - $typeConfig['resolveField'] = function ($source, $args, GraphQLContext $context, ResolveInfo $info) { - $fieldName = $info->fieldName; - // If we're on the Query type, we're entering the graph, so we'll want a static method. - // Unlike elsewhere in the graph, we can assume everything on Query will have an @bind. - $bindDirective = GraphQLUtils::getDirectiveByName($info, 'bind'); - if (!$bindDirective) { - throw new MyRadioException("Tried to resolve $fieldName on Query but it didn't have an @bind"); - } - $bindArgs = GraphQLUtils::getDirectiveArguments($bindDirective); - if (isset($bindArgs['class'])) { - // we know class is a string - /** @noinspection PhpPossiblePolymorphicInvocationInspection */ - $className = $bindArgs['class']->value; - } else { - throw new MyRadioException( - "Tried to resolve $fieldName on Query but its @bind didn't have a class" - ); - } - if (isset($bindArgs['method'])) { - $methodName = $bindArgs['method']->value; - } else { - throw new MyRadioException( - "Tried to resolve $fieldName on Query but its @bind didn't have a method" - ); - } - // Wonderful! - $clazz = new ReflectionClass($className); - $meth = $clazz->getMethod($methodName); - if (GraphQLUtils::isAuthorisedToAccess($info, $className, $methodName)) { - return GraphQLUtils::processScalarIfNecessary( - $info, - GraphQLUtils::invokeNamed($meth, null, $args) - ); - } else { - return GraphQLUtils::returnNullOrThrowForbiddenException($info); - } - }; break; - case "Mutation": - throw new MyRadioException('Mutations not supported'); } return $typeConfig; }; @@ -166,6 +126,60 @@ function graphQlResolver($source, $args, GraphQLContext $context, ResolveInfo $i { $typeName = $info->parentType->name; $fieldName = $info->fieldName; + // Query and Mutation deserve special handling + if ($typeName === 'Query' || $typeName === 'Mutation') { + // If we're on the Query or Mutation type, we're entering the graph, so we'll want a static method. + // Unlike elsewhere in the graph, we can assume everything on Query/Mutation will have an @bind. + $bindDirective = GraphQLUtils::getDirectiveByName($info, 'bind'); + if (!$bindDirective) { + throw new MyRadioException("Tried to resolve $fieldName on $typeName but it didn't have an @bind"); + } + $bindArgs = GraphQLUtils::getDirectiveArguments($bindDirective); + if (isset($bindArgs['class'])) { + // we know class is a string + /** @noinspection PhpPossiblePolymorphicInvocationInspection */ + $className = $bindArgs['class']->value; + } else { + throw new MyRadioException( + "Tried to resolve $fieldName on $typeName but its @bind didn't have a class" + ); + } + if (isset($bindArgs['method'])) { + $methodName = $bindArgs['method']->value; + } else { + throw new MyRadioException( + "Tried to resolve $fieldName on $typeName but its @bind didn't have a method" + ); + } + // Wonderful! + $clazz = new ReflectionClass($className); + // (We'll get a ReflectionException here if it's inaccessible. But That's Okay. + $meth = $clazz->getMethod($methodName); + if (GraphQLUtils::isAuthorisedToAccess($info, $className, $methodName)) { + // First, though, check if we should be using a calling convention + if (isset($bindArgs['callingConvention'])) { + $cc = $bindArgs['callingConvention']->value; + switch ($cc) { + case 'FirstArgCurrentUser': + $val = $meth->invokeArgs(null, [MyRadio_User::getInstance()->getID()]); + break; + case 'FirstArgInput': + $val = $meth->invokeArgs(null, [$args['input']]); + break; + case 'InputAsArgs': + $val = GraphQLUtils::invokeNamed($meth, null, $args['input']); + break; + default: + throw new MyRadioException("Unknown calling convention $cc"); + } + } else { + $val = GraphQLUtils::invokeNamed($meth, $source, $args); + } + return GraphQLUtils::processScalarIfNecessary($info, $val); + } else { + return GraphQLUtils::returnNullOrThrowForbiddenException($info); + } + } // First up, check if we have a bind directive $bindDirective = GraphQLUtils::getDirectiveByName($info, 'bind'); if ($bindDirective) { @@ -376,7 +390,12 @@ function graphQlResolver($source, $args, GraphQLContext $context, ResolveInfo $i $warnings = $ctx->getWarnings(); if (count($warnings) > 0) { - $result['warnings'] = $warnings; + $result['extensions'] = array_merge( + $result['extensions'] ?? [], + [ + 'warnings' => $warnings + ] + ); } $corsWhitelistOrigins = [