Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement GraphQL mutations #927

Open
wants to merge 31 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
41702cb
Implement mutations
markspolakovs Jul 12, 2020
b5135b7
oops
markspolakovs Jul 12, 2020
353c602
oops 2
markspolakovs Jul 12, 2020
a806ee6
oops 3
markspolakovs Jul 12, 2020
038d60b
oops 4
markspolakovs Jul 12, 2020
2e4962a
MyRadio_Show::create: allow tags to be an array
markspolakovs Jul 13, 2020
03793ae
oops
markspolakovs Jul 13, 2020
f4fd5c1
nulls bad mmkay
markspolakovs Jul 13, 2020
f45b6c4
oopsies
markspolakovs Jul 13, 2020
d3355a6
Expose genres and subtypes to GQL
markspolakovs Jul 20, 2020
349d99a
Expose credit types
markspolakovs Jul 20, 2020
3b480d1
Fix a small fubar in MyRadio_ShowSubtype::getAll
markspolakovs Jul 20, 2020
02bc3d1
Expose member search
markspolakovs Jul 21, 2020
471f3fa
Update api.graphql
markspolakovs Jul 21, 2020
5ade6f4
Expose findShowByTitle
markspolakovs Jul 21, 2020
fdbbfdd
Merge branch 'master' into markspolakovs-gql-mutations
markspolakovs Jul 22, 2020
0ba7adf
Stuff can be overridden for seasons and timeslots
markspolakovs Aug 3, 2020
d2503e0
Expose isTerm
markspolakovs Aug 3, 2020
2995437
Expose getPreviousTimeslots
markspolakovs Aug 3, 2020
ec32877
Expose upload_state
markspolakovs Aug 3, 2020
834f47f
Expose Timeslot photo
markspolakovs Aug 3, 2020
4b9165d
Merge branch 'master' into markspolakovs-gql-mutations
markspolakovs Aug 10, 2020
fe49a57
Merge master
markspolakovs Aug 10, 2020
f60f4dd
Expose includeMemberships
markspolakovs Aug 10, 2020
7074163
Merge branch 'master' into markspolakovs-gql-mutations
markspolakovs Aug 10, 2020
2d7dbf6
Move warnings into extensions of result
markspolakovs Aug 10, 2020
3a55c60
Merge branch 'master' into markspolakovs-gql-mutations
markspolakovs Aug 16, 2020
25a45ef
Update some outdated comments and errors
markspolakovs Aug 16, 2020
97c6e4e
Merge branch 'master' into markspolakovs-gql-mutations
markspolakovs Dec 5, 2020
67cf048
Disable createShow and add sendMessageToTimeslot
markspolakovs Dec 5, 2020
d643350
Merge branch 'master' into markspolakovs-gql-mutations
mstratford Jan 16, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 66 additions & 2 deletions schema/api.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@ directive @bind(

enum CallingConvention {
FirstArgCurrentUser,
FirstArgCurrentObject
FirstArgInput,
FirstArgCurrentObject,
InputAsArgs
}

enum AuthHook {
Expand Down Expand Up @@ -347,7 +349,7 @@ type Timeslot implements Node & MyRadioObject @auth(hook: ViewShow) {
photo: String
messages: [Message]
webpage: String!

uploadState: String @meta(key: "upload_state")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

duplicated?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

woops

tracklist: [TracklistItem!] @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_TracklistItem", method: "getTracklistForTimeslot", callingConvention: FirstArgCurrentObject)
uploadState: String @meta(key: "upload_state")
}
Expand Down Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess out of scope, but the fact this uses a completely separate auth list to V2 makes me a bit sad.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It still defaults to V2 auth unless you override it.

The reason for this is that V2 auth is evaluated once (when you call the endpoint), while GraphQL auth is evaluated for every sub-method in the graph. The reason for that is that some object properties are really just methods (for example User.phoneNumber), which need an auth check of their own (for example, you may want anyone to be able to see basic details of a user, but only see the phone number if you or they are on committee). In V2 this is handled either by changing the shape of the response (in the former case) or by mixins (in the latter) - neither of which are a viable solution for GraphQL because of the strict typing.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lots of commented out stuff :/

# 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
}
2 changes: 1 addition & 1 deletion src/Classes/MyRadio/GraphQLUtils.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
8 changes: 7 additions & 1 deletion src/Classes/ServiceAPI/MyRadio_Season.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 5 additions & 2 deletions src/Classes/ServiceAPI/MyRadio_Show.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/Classes/ServiceAPI/MyRadio_ShowSubtype.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public static function getAll()
$subtypes[] = new self($row);
}

return CoreUtils::setToDataSource($subtypes);
return $subtypes;
}

/**
Expand Down
12 changes: 12 additions & 0 deletions src/Classes/ServiceAPI/MyRadio_Timeslot.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
101 changes: 60 additions & 41 deletions src/Controllers/api/graphql.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
Expand All @@ -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) {
Expand Down Expand Up @@ -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 = [
Expand Down