diff --git a/ballerina-tests/Dependencies.toml b/ballerina-tests/Dependencies.toml index 8e5aeb37b..4e2977acf 100644 --- a/ballerina-tests/Dependencies.toml +++ b/ballerina-tests/Dependencies.toml @@ -76,16 +76,19 @@ dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "jwt"}, {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.regexp"}, {org = "ballerina", name = "lang.value"}, {org = "ballerina", name = "log"}, {org = "ballerina", name = "mime"}, {org = "ballerina", name = "oauth2"}, {org = "ballerina", name = "task"}, {org = "ballerina", name = "time"}, + {org = "ballerina", name = "uuid"}, {org = "ballerina", name = "websocket"} ] modules = [ {org = "ballerina", packageName = "graphql", moduleName = "graphql"}, + {org = "ballerina", packageName = "graphql", moduleName = "graphql.dataloader"}, {org = "ballerina", packageName = "graphql", moduleName = "graphql.parser"}, {org = "ballerina", packageName = "graphql", moduleName = "graphql.subgraph"} ] @@ -367,7 +370,6 @@ modules = [ org = "ballerina" name = "uuid" version = "1.6.0" -scope = "testOnly" dependencies = [ {org = "ballerina", name = "crypto"}, {org = "ballerina", name = "jballerina.java"}, diff --git a/ballerina-tests/tests/41_dataloader.bal b/ballerina-tests/tests/41_dataloader.bal new file mode 100644 index 000000000..f567675ed --- /dev/null +++ b/ballerina-tests/tests/41_dataloader.bal @@ -0,0 +1,150 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/graphql; +import ballerina/websocket; +import ballerina/test; + +@test:Config { + groups: ["dataloader", "query"], + after: resetDispatchCounters +} +isolated function testDataLoaderWithQuery() returns error? { + graphql:Client graphqlClient = check new ("localhost:9090/dataloader"); + string document = check getGraphqlDocumentFromFile("dataloader_with_query"); + json response = check graphqlClient->execute(document); + json expectedPayload = check getJsonContentFromFile("dataloader_with_query"); + assertJsonValuesWithOrder(response, expectedPayload); + assertDispatchCountForAuthorLoader(1); + assertDispatchCountForBookLoader(1); +} + +@test:Config { + groups: ["dataloader", "query"], + after: resetDispatchCounters +} +isolated function testDataLoaderWithDifferentAliasForSameField() returns error? { + graphql:Client graphqlClient = check new ("localhost:9090/dataloader"); + string document = check getGraphqlDocumentFromFile("dataloader_with_different_alias_for_same_field"); + json response = check graphqlClient->execute(document); + json expectedPayload = check getJsonContentFromFile("dataloader_with_different_alias_for_same_field"); + assertJsonValuesWithOrder(response, expectedPayload); + assertDispatchCountForAuthorLoader(1); + assertDispatchCountForBookLoader(1); +} + +@test:Config { + groups: ["dataloader", "subscription"], + after: resetDispatchCounters +} +isolated function testDataLoaderWithSubscription() returns error? { + string document = check getGraphqlDocumentFromFile("dataloader_with_subscription"); + websocket:ClientConfiguration config = {subProtocols: [GRAPHQL_TRANSPORT_WS]}; + websocket:Client wsClient = check new ("ws://localhost:9090/dataloader", config); + check initiateGraphqlWsConnection(wsClient); + check sendSubscriptionMessage(wsClient, document, "1"); + json[] authorSequence = check getJsonContentFromFile("data_loader_with_subscription").ensureType(); + foreach int i in 0 ..< 5 { + json expectedMsgPayload = {data: {authors: authorSequence[i]}}; + check validateNextMessage(wsClient, expectedMsgPayload, id = "1"); + } + assertDispatchCountForBookLoader(5); +} + +@test:Config { + groups: ["dataloader", "mutation"], + dependsOn: [testDataLoaderWithQuery, testDataLoaderWithSubscription], + after: resetDispatchCounters +} +isolated function testDataLoaderWithMutation() returns error? { + graphql:Client graphqlClient = check new ("localhost:9090/dataloader"); + string document = check getGraphqlDocumentFromFile("dataloader_with_mutation"); + json response = check graphqlClient->execute(document); + json expectedPayload = check getJsonContentFromFile("dataloader_with_mutation"); + assertJsonValuesWithOrder(response, expectedPayload); + assertDispatchCountForUpdateAuthorLoader(1); + assertDispatchCountForBookLoader(1); +} + +@test:Config { + groups: ["dataloader", "interceptor"], + after: resetDispatchCounters +} +isolated function testDataLoaderWithInterceptors() returns error? { + graphql:Client graphqlClient = check new ("localhost:9090/dataloader_with_interceptor"); + string document = check getGraphqlDocumentFromFile("dataloader_with_interceptor"); + json response = check graphqlClient->execute(document); + json expectedPayload = check getJsonContentFromFile("dataloader_with_interceptor"); + assertJsonValuesWithOrder(response, expectedPayload); + assertDispatchCountForAuthorLoader(1); + assertDispatchCountForBookLoader(1); +} + +@test:Config { + groups: ["dataloader", "dispatch-error"], + after: resetDispatchCounters +} +isolated function testBatchFunctionReturningErrors() returns error? { + graphql:Client graphqlClient = check new ("localhost:9090/dataloader"); + string document = check getGraphqlDocumentFromFile("batch_function_returing_errors"); + json response = check graphqlClient->execute(document); + json expectedPayload = check getJsonContentFromFile("batch_function_returing_errors"); + assertJsonValuesWithOrder(response, expectedPayload); + assertDispatchCountForAuthorLoader(1); + assertDispatchCountForBookLoader(0); +} + +@test:Config { + groups: ["dataloader", "dispatch-error"], + after: resetDispatchCounters +} +isolated function testBatchFunctionReturingNonMatchingNumberOfResults() returns error? { + graphql:Client graphqlClient = check new ("localhost:9090/dataloader_with_faulty_batch_function"); + string document = check getGraphqlDocumentFromFile("batch_function_returning_non_matcing_number_of_results"); + json response = check graphqlClient->execute(document); + json expectedPayload = check getJsonContentFromFile("batch_function_returning_non_matcing_number_of_results"); + assertJsonValuesWithOrder(response, expectedPayload); +} + +isolated function resetDispatchCounters() { + lock { + dispatchCountOfAuthorLoader = 0; + } + lock { + dispatchCountOfBookLoader = 0; + } + lock { + dispatchCountOfUpdateAuthorLoader = 0; + } +} + +isolated function assertDispatchCountForBookLoader(int expectedCount) { + lock { + test:assertEquals(dispatchCountOfBookLoader, expectedCount); + } +} + +isolated function assertDispatchCountForUpdateAuthorLoader(int expectedCount) { + lock { + test:assertEquals(dispatchCountOfUpdateAuthorLoader, expectedCount); + } +} + +isolated function assertDispatchCountForAuthorLoader(int expectedCount) { + lock { + test:assertEquals(dispatchCountOfAuthorLoader, expectedCount); + } +} diff --git a/ballerina-tests/tests/41_parallel_execution.bal b/ballerina-tests/tests/41_parallel_execution.bal deleted file mode 100644 index 96884a6d0..000000000 --- a/ballerina-tests/tests/41_parallel_execution.bal +++ /dev/null @@ -1,12 +0,0 @@ -import ballerina/test; - -@test:Config { - groups: ["parallel"] -} -function testResolversExecutesParallelly() returns error? { - string url = "http://localhost:9090/parallel"; - string document = "query { a b }"; - json actualPayload = check getJsonPayloadFromService(url, document); - json expectedPayload = {data: { a: "Hello World!", b: "Hello World"} }; - assertJsonValuesWithOrder(actualPayload, expectedPayload); -} diff --git a/ballerina-tests/tests/batch_load_functions.bal b/ballerina-tests/tests/batch_load_functions.bal new file mode 100644 index 000000000..698ed1f7c --- /dev/null +++ b/ballerina-tests/tests/batch_load_functions.bal @@ -0,0 +1,73 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +isolated function authorLoaderFunction(readonly & anydata[] ids) returns AuthorRow[]|error { + readonly & int[] keys = check ids.ensureType(); + // Simulate query: SELECT * FROM authors WHERE id IN (...keys); + lock { + dispatchCountOfAuthorLoader += 1; + } + lock { + readonly & int[] validKeys = keys.'filter(key => authorTable.hasKey(key)).cloneReadOnly(); + return keys.length() != validKeys.length() ? error("Invalid keys found for authors") + : validKeys.'map(key => authorTable.get(key)); + } +}; + +isolated function bookLoaderFunction(readonly & anydata[] ids) returns BookRow[][]|error { + final readonly & int[] keys = check ids.ensureType(); + // Simulate query: SELECT * FROM books WHERE author IN (...keys); + lock { + dispatchCountOfBookLoader += 1; + } + return keys.'map(isolated function(readonly & int key) returns BookRow[] { + lock { + return bookTable.'filter(book => book.author == key).toArray().clone(); + } + }); +}; + +isolated function authorUpdateLoaderFunction(readonly & anydata[] idNames) returns AuthorRow[]|error { + readonly & [int, string][] idValuePair = check idNames.ensureType(); + // Simulate batch udpate + lock { + dispatchCountOfUpdateAuthorLoader += 1; + } + lock { + foreach [int, string] [key, _] in idValuePair { + if !authorTable.hasKey(key) { + return error(string `Invalid key author key found: ${key}`); + } + } + + AuthorRow[] updatedAuthorRows = []; + foreach [int, string] [key, name] in idValuePair { + AuthorRow authorRow = {id: key, name}; + authorTable.put(authorRow); + updatedAuthorRows.push(authorRow.clone()); + } + return updatedAuthorRows.clone(); + } +}; + +isolated function faultyAuthorLoaderFunction(readonly & anydata[] ids) returns AuthorRow[]|error { + readonly & int[] keys = check ids.ensureType(); + lock { + readonly & int[] validKeys = keys.'filter(key => authorTable.hasKey(key)).cloneReadOnly(); + // This method may return an array of size not equal to the input array (ids) size. + return validKeys.'map(key => authorTable.get(key)); + } +}; diff --git a/ballerina-tests/tests/interceptors.bal b/ballerina-tests/tests/interceptors.bal index 62bdeb219..dc3fbdee3 100644 --- a/ballerina-tests/tests/interceptors.bal +++ b/ballerina-tests/tests/interceptors.bal @@ -868,3 +868,23 @@ readonly service class LogSubfields { return context.resolve('field); } } + +readonly service class AuthorInterceptor { + *graphql:Interceptor; + + isolated remote function execute(graphql:Context context, graphql:Field 'field) returns anydata { + var data = context.resolve('field); + // Return only the first author + return ('field.getName() == "authors" && data is anydata[]) ? [data[0]] : data; + } +} + +readonly service class BookInterceptor { + *graphql:Interceptor; + + isolated remote function execute(graphql:Context context, graphql:Field 'field) returns anydata { + var books = context.resolve('field); + // Return only the first book + return (books is anydata[]) ? [books[0]] : books; + } +} diff --git a/ballerina-tests/tests/object_types.bal b/ballerina-tests/tests/object_types.bal index 3fd71cec5..c34b02e37 100644 --- a/ballerina-tests/tests/object_types.bal +++ b/ballerina-tests/tests/object_types.bal @@ -15,6 +15,7 @@ // under the License. import ballerina/graphql; +import ballerina/graphql.dataloader; import ballerina/lang.runtime; public type PeopleService StudentService|TeacherService; @@ -393,3 +394,71 @@ public distinct isolated service class CustomerAddress { return self.city; } } + +public isolated distinct service class AuthorData { + private final readonly & AuthorRow author; + + isolated function init(AuthorRow author) { + self.author = author.cloneReadOnly(); + } + + isolated resource function get name() returns string { + return self.author.name; + } + + isolated function preBooks(graphql:Context ctx) { + dataloader:DataLoader bookLoader = ctx.getDataLoader(BOOK_LOADER); + bookLoader.add(self.author.id); + } + + isolated resource function get books(graphql:Context ctx) returns BookData[]|error { + dataloader:DataLoader bookLoader = ctx.getDataLoader(BOOK_LOADER); + BookRow[] bookrows = check bookLoader.get(self.author.id); + return from BookRow bookRow in bookrows + select new BookData(bookRow); + } +} + +public isolated distinct service class AuthorDetail { + private final readonly & AuthorRow author; + + isolated function init(AuthorRow author) { + self.author = author.cloneReadOnly(); + } + + isolated resource function get name() returns string { + return self.author.name; + } + + isolated function prefetchBooks(graphql:Context ctx) { + dataloader:DataLoader bookLoader = ctx.getDataLoader(BOOK_LOADER); + bookLoader.add(self.author.id); + } + + @graphql:ResourceConfig { + interceptors: new BookInterceptor(), + prefetchMethodName: "prefetchBooks" + } + isolated resource function get books(graphql:Context ctx) returns BookData[]|error { + dataloader:DataLoader bookLoader = ctx.getDataLoader(BOOK_LOADER); + BookRow[] bookrows = check bookLoader.get(self.author.id); + return from BookRow bookRow in bookrows + select new BookData(bookRow); + } +} + +public isolated distinct service class BookData { + private final readonly & BookRow book; + + isolated function init(BookRow book) { + self.book = book.cloneReadOnly(); + } + + isolated resource function get id() returns int { + return self.book.id; + } + + isolated resource function get title() returns string { + return self.book.title; + } +} diff --git a/ballerina-tests/tests/records.bal b/ballerina-tests/tests/records.bal index 05bf20406..b48f6f6e5 100644 --- a/ballerina-tests/tests/records.bal +++ b/ballerina-tests/tests/records.bal @@ -351,3 +351,14 @@ public type DeprecatedAddress record {| # City field is deprecated string city; |}; + +type BookRow record {| + readonly int id; + string title; + int author; +|}; + +type AuthorRow record {| + readonly int id; + string name; +|}; diff --git a/ballerina-tests/tests/resources/documents/batch_function_returing_errors.graphql b/ballerina-tests/tests/resources/documents/batch_function_returing_errors.graphql new file mode 100644 index 000000000..46f6bd993 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/batch_function_returing_errors.graphql @@ -0,0 +1,9 @@ +query { + authors(ids: [1, 2, 3, 4, 5, 6]) { + name + books { + id + title + } + } +} diff --git a/ballerina-tests/tests/resources/documents/batch_function_returning_non_matcing_number_of_results.graphql b/ballerina-tests/tests/resources/documents/batch_function_returning_non_matcing_number_of_results.graphql new file mode 100644 index 000000000..46f6bd993 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/batch_function_returning_non_matcing_number_of_results.graphql @@ -0,0 +1,9 @@ +query { + authors(ids: [1, 2, 3, 4, 5, 6]) { + name + books { + id + title + } + } +} diff --git a/ballerina-tests/tests/resources/documents/dataloader_with_different_alias_for_same_field.graphql b/ballerina-tests/tests/resources/documents/dataloader_with_different_alias_for_same_field.graphql new file mode 100644 index 000000000..5766ed17c --- /dev/null +++ b/ballerina-tests/tests/resources/documents/dataloader_with_different_alias_for_same_field.graphql @@ -0,0 +1,16 @@ +query { + firstThree: authors(ids: [1, 2, 3]) { + name + books { + id + title + } + } + lastTwo: authors(ids: [4, 5]) { + name + books { + id + title + } + } +} diff --git a/ballerina-tests/tests/resources/documents/dataloader_with_interceptor.graphql b/ballerina-tests/tests/resources/documents/dataloader_with_interceptor.graphql new file mode 100644 index 000000000..7b22bbd86 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/dataloader_with_interceptor.graphql @@ -0,0 +1,14 @@ +query { + one: authors(ids: [1, 2, 3]) { + name + books { + title + } + } + four: authors(ids: [4, 5]) { + name + books { + title + } + } +} diff --git a/ballerina-tests/tests/resources/documents/dataloader_with_mutation.graphql b/ballerina-tests/tests/resources/documents/dataloader_with_mutation.graphql new file mode 100644 index 000000000..72b2160ee --- /dev/null +++ b/ballerina-tests/tests/resources/documents/dataloader_with_mutation.graphql @@ -0,0 +1,16 @@ +mutation { + sabthar: updateAuthorName(id: 1, name: "Sabthar") { + name + books { + id + title + } + } + mahroof: updateAuthorName(id: 2, name: "Mahroof") { + name + books { + id + title + } + } +} diff --git a/ballerina-tests/tests/resources/documents/dataloader_with_query.graphql b/ballerina-tests/tests/resources/documents/dataloader_with_query.graphql new file mode 100644 index 000000000..c3677e0ca --- /dev/null +++ b/ballerina-tests/tests/resources/documents/dataloader_with_query.graphql @@ -0,0 +1,9 @@ +query { + authors(ids: [1, 2, 3]) { + name + books { + id + title + } + } +} diff --git a/ballerina-tests/tests/resources/documents/dataloader_with_subscription.graphql b/ballerina-tests/tests/resources/documents/dataloader_with_subscription.graphql new file mode 100644 index 000000000..a7b831ad9 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/dataloader_with_subscription.graphql @@ -0,0 +1,9 @@ +subscription { + authors { + name + books { + id + title + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/batch_function_returing_errors.json b/ballerina-tests/tests/resources/expected_results/batch_function_returing_errors.json new file mode 100644 index 000000000..9c3c5eef5 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/batch_function_returing_errors.json @@ -0,0 +1,15 @@ +{ + "errors": [ + { + "message": "Invalid keys found for authors", + "locations": [ + { + "line": 2, + "column": 3 + } + ], + "path": ["authors"] + } + ], + "data": null +} diff --git a/ballerina-tests/tests/resources/expected_results/batch_function_returning_non_matcing_number_of_results.json b/ballerina-tests/tests/resources/expected_results/batch_function_returning_non_matcing_number_of_results.json new file mode 100644 index 000000000..ba8bf5796 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/batch_function_returning_non_matcing_number_of_results.json @@ -0,0 +1,10 @@ +{ + "errors": [ + { + "message": "The batch function should return a number of results equal to the number of keys", + "locations": [{ "line": 2, "column": 3 }], + "path": ["authors"] + } + ], + "data": null +} diff --git a/ballerina-tests/tests/resources/expected_results/data_loader_with_subscription.json b/ballerina-tests/tests/resources/expected_results/data_loader_with_subscription.json new file mode 100644 index 000000000..818abb42c --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/data_loader_with_subscription.json @@ -0,0 +1,26 @@ +[ + { + "name": "Author 1", + "books": [ + { "id": 1, "title": "Book 1" }, + { "id": 2, "title": "Book 2" }, + { "id": 3, "title": "Book 3" } + ] + }, + { + "name": "Author 2", + "books": [ + { "id": 4, "title": "Book 4" }, + { "id": 5, "title": "Book 5" } + ] + }, + { + "name": "Author 3", + "books": [ + { "id": 6, "title": "Book 6" }, + { "id": 7, "title": "Book 7" } + ] + }, + { "name": "Author 4", "books": [{ "id": 8, "title": "Book 8" }] }, + { "name": "Author 5", "books": [{ "id": 9, "title": "Book 9" }] } +] diff --git a/ballerina-tests/tests/resources/expected_results/dataloader_with_different_alias_for_same_field.json b/ballerina-tests/tests/resources/expected_results/dataloader_with_different_alias_for_same_field.json new file mode 100644 index 000000000..0307afcab --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/dataloader_with_different_alias_for_same_field.json @@ -0,0 +1,69 @@ +{ + "data": { + "firstThree": [ + { + "name": "Author 1", + "books": [ + { + "id": 1, + "title": "Book 1" + }, + { + "id": 2, + "title": "Book 2" + }, + { + "id": 3, + "title": "Book 3" + } + ] + }, + { + "name": "Author 2", + "books": [ + { + "id": 4, + "title": "Book 4" + }, + { + "id": 5, + "title": "Book 5" + } + ] + }, + { + "name": "Author 3", + "books": [ + { + "id": 6, + "title": "Book 6" + }, + { + "id": 7, + "title": "Book 7" + } + ] + } + ], + "lastTwo": [ + { + "name": "Author 4", + "books": [ + { + "id": 8, + "title": "Book 8" + } + ] + }, + { + "name": "Author 5", + "books": [ + { + "id": 9, + "title": "Book 9" + } + ] + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/dataloader_with_interceptor.json b/ballerina-tests/tests/resources/expected_results/dataloader_with_interceptor.json new file mode 100644 index 000000000..5a65121dc --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/dataloader_with_interceptor.json @@ -0,0 +1,24 @@ +{ + "data": { + "one": [ + { + "name": "Author 1", + "books": [ + { + "title": "Book 1" + } + ] + } + ], + "four": [ + { + "name": "Author 4", + "books": [ + { + "title": "Book 8" + } + ] + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/dataloader_with_mutation.json b/ballerina-tests/tests/resources/expected_results/dataloader_with_mutation.json new file mode 100644 index 000000000..dc842c15c --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/dataloader_with_mutation.json @@ -0,0 +1,34 @@ +{ + "data": { + "sabthar": { + "name": "Sabthar", + "books": [ + { + "id": 1, + "title": "Book 1" + }, + { + "id": 2, + "title": "Book 2" + }, + { + "id": 3, + "title": "Book 3" + } + ] + }, + "mahroof": { + "name": "Mahroof", + "books": [ + { + "id": 4, + "title": "Book 4" + }, + { + "id": 5, + "title": "Book 5" + } + ] + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/dataloader_with_query.json b/ballerina-tests/tests/resources/expected_results/dataloader_with_query.json new file mode 100644 index 000000000..4735acf37 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/dataloader_with_query.json @@ -0,0 +1,49 @@ +{ + "data": { + "authors": [ + { + "name": "Author 1", + "books": [ + { + "id": 1, + "title": "Book 1" + }, + { + "id": 2, + "title": "Book 2" + }, + { + "id": 3, + "title": "Book 3" + } + ] + }, + { + "name": "Author 2", + "books": [ + { + "id": 4, + "title": "Book 4" + }, + { + "id": 5, + "title": "Book 5" + } + ] + }, + { + "name": "Author 3", + "books": [ + { + "id": 6, + "title": "Book 6" + }, + { + "id": 7, + "title": "Book 7" + } + ] + } + ] + } +} diff --git a/ballerina-tests/tests/test_services.bal b/ballerina-tests/tests/test_services.bal index 59a0542ce..3386a1bce 100644 --- a/ballerina-tests/tests/test_services.bal +++ b/ballerina-tests/tests/test_services.bal @@ -15,9 +15,10 @@ // under the License. import ballerina/graphql; +import ballerina/graphql.dataloader; import ballerina/http; -import ballerina/uuid; import ballerina/lang.runtime; +import ballerina/uuid; graphql:Service graphiqlDefaultPathConfigService = @graphql:ServiceConfig { @@ -2121,19 +2122,6 @@ service /annotations on wrappedListener { } } -service /parallel on wrappedListener { - private string data = "Hello"; - resource function get a() returns string { - runtime:sleep(1); - self.data += "!"; - return self.data; - } - resource function get b() returns string { - self.data += " World"; - return self.data; - } -} - @graphql:ServiceConfig { validation: false } @@ -2351,3 +2339,98 @@ public distinct service class Student5 { return self.name; } } + +const AUTHOR_LOADER = "authorLoader"; +const AUTHOR_UPDATE_LOADER = "authorUpdateLoader"; +const BOOK_LOADER = "bookLoader"; + +isolated function initContext(http:RequestContext requestContext, http:Request request) returns graphql:Context|error { + graphql:Context ctx = new; + ctx.registerDataLoader(AUTHOR_LOADER, new dataloader:DefaultDataLoader(authorLoaderFunction)); + ctx.registerDataLoader(AUTHOR_UPDATE_LOADER, new dataloader:DefaultDataLoader(authorUpdateLoaderFunction)); + ctx.registerDataLoader(BOOK_LOADER, new dataloader:DefaultDataLoader(bookLoaderFunction)); + return ctx; +} + +@graphql:ServiceConfig { + contextInit: initContext +} +service /dataloader on wrappedListener { + function preAuthors(graphql:Context ctx, int[] ids) { + addAuthorIdsToAuthorLoader(ctx, ids); + } + + resource function get authors(graphql:Context ctx, int[] ids) returns AuthorData[]|error { + dataloader:DataLoader authorLoader = ctx.getDataLoader(AUTHOR_LOADER); + AuthorRow[] authorRows = check trap ids.map(id => check authorLoader.get(id, AuthorRow)); + return from AuthorRow authorRow in authorRows + select new (authorRow); + } + + function preUpdateAuthorName(graphql:Context ctx, int id, string name) { + [int, string] key = [id, name]; + dataloader:DataLoader authorUpdateLoader = ctx.getDataLoader(AUTHOR_UPDATE_LOADER); + authorUpdateLoader.add(key); + } + + remote function updateAuthorName(graphql:Context ctx, int id, string name) returns AuthorData|error { + [int, string] key = [id, name]; + dataloader:DataLoader authorUpdateLoader = ctx.getDataLoader(AUTHOR_UPDATE_LOADER); + AuthorRow authorRow = check authorUpdateLoader.get(key); + return new (authorRow); + } + + resource function subscribe authors() returns stream { + lock { + readonly & AuthorRow[] authorRows = authorTable.toArray().cloneReadOnly(); + return authorRows.'map(authorRow => new AuthorData(authorRow)).toStream(); + } + } +} + +@graphql:ServiceConfig { + interceptors: new AuthorInterceptor(), + contextInit: initContext +} +service /dataloader_with_interceptor on wrappedListener { + function preAuthors(graphql:Context ctx, int[] ids) { + addAuthorIdsToAuthorLoader(ctx, ids); + } + + resource function get authors(graphql:Context ctx, int[] ids) returns AuthorDetail[]|error { + dataloader:DataLoader authorLoader = ctx.getDataLoader(AUTHOR_LOADER); + AuthorRow[] authorRows = check trap ids.map(id => check authorLoader.get(id, AuthorRow)); + return from AuthorRow authorRow in authorRows + select new (authorRow); + } +} + +@graphql:ServiceConfig { + interceptors: new AuthorInterceptor(), + contextInit: isolated function (http:RequestContext requestContext, http:Request request) returns graphql:Context { + graphql:Context ctx = new; + ctx.registerDataLoader(AUTHOR_LOADER, new dataloader:DefaultDataLoader(faultyAuthorLoaderFunction)); + ctx.registerDataLoader(AUTHOR_UPDATE_LOADER, new dataloader:DefaultDataLoader(authorUpdateLoaderFunction)); + ctx.registerDataLoader(BOOK_LOADER, new dataloader:DefaultDataLoader(bookLoaderFunction)); + return ctx; + } +} +service /dataloader_with_faulty_batch_function on wrappedListener { + function preAuthors(graphql:Context ctx, int[] ids) { + addAuthorIdsToAuthorLoader(ctx, ids); + } + + resource function get authors(graphql:Context ctx, int[] ids) returns AuthorData[]|error { + dataloader:DataLoader authorLoader = ctx.getDataLoader(AUTHOR_LOADER); + AuthorRow[] authorRows = check trap ids.map(id => check authorLoader.get(id, AuthorRow)); + return from AuthorRow authorRow in authorRows + select new (authorRow); + } +} + +function addAuthorIdsToAuthorLoader(graphql:Context ctx, int[] ids) { + dataloader:DataLoader authorLoader = ctx.getDataLoader(AUTHOR_LOADER); + ids.forEach(function(int id) { + authorLoader.add(id); + }); +} diff --git a/ballerina-tests/tests/values.bal b/ballerina-tests/tests/values.bal index 5337c4b8b..9001a801a 100644 --- a/ballerina-tests/tests/values.bal +++ b/ballerina-tests/tests/values.bal @@ -270,3 +270,27 @@ const WS_COMPLETE = "complete"; // WebSocket Sub Protocols const GRAPHQL_TRANSPORT_WS = "graphql-transport-ws"; const WS_SUB_PROTOCOL = "Sec-WebSocket-Protocol"; + +final isolated table key(id) authorTable = table [ + {id: 1, name: "Author 1"}, + {id: 2, name: "Author 2"}, + {id: 3, name: "Author 3"}, + {id: 4, name: "Author 4"}, + {id: 5, name: "Author 5"} +]; + +final isolated table key(id) bookTable = table [ + {id: 1, title: "Book 1", author: 1}, + {id: 2, title: "Book 2", author: 1}, + {id: 3, title: "Book 3", author: 1}, + {id: 4, title: "Book 4", author: 2}, + {id: 5, title: "Book 5", author: 2}, + {id: 6, title: "Book 6", author: 3}, + {id: 7, title: "Book 7", author: 3}, + {id: 8, title: "Book 8", author: 4}, + {id: 9, title: "Book 9", author: 5} +]; + +isolated int dispatchCountOfBookLoader = 0; +isolated int dispatchCountOfAuthorLoader = 0; +isolated int dispatchCountOfUpdateAuthorLoader = 0; diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index de4cc68a1..0e6d183bb 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -3,7 +3,7 @@ org = "ballerina" name = "graphql" version = "1.9.1" authors = ["Ballerina"] -export=["graphql", "graphql.subgraph"] +export=["graphql", "graphql.subgraph", "graphql.dataloader"] keywords = ["gql", "network", "query", "service"] repository = "https://github.com/ballerina-platform/module-ballerina-graphql" icon = "icon.png" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index 19d0cb406..9184f5226 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -76,6 +76,7 @@ dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "jwt"}, {org = "ballerina", name = "lang.array"}, + {org = "ballerina", name = "lang.regexp"}, {org = "ballerina", name = "lang.value"}, {org = "ballerina", name = "log"}, {org = "ballerina", name = "mime"}, @@ -83,10 +84,12 @@ dependencies = [ {org = "ballerina", name = "task"}, {org = "ballerina", name = "test"}, {org = "ballerina", name = "time"}, + {org = "ballerina", name = "uuid"}, {org = "ballerina", name = "websocket"} ] modules = [ {org = "ballerina", packageName = "graphql", moduleName = "graphql"}, + {org = "ballerina", packageName = "graphql", moduleName = "graphql.dataloader"}, {org = "ballerina", packageName = "graphql", moduleName = "graphql.parser"}, {org = "ballerina", packageName = "graphql", moduleName = "graphql.subgraph"} ] @@ -220,6 +223,9 @@ version = "0.0.0" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] +modules = [ + {org = "ballerina", packageName = "lang.regexp", moduleName = "lang.regexp"} +] [[package]] org = "ballerina" @@ -353,6 +359,20 @@ dependencies = [ {org = "ballerina", name = "jballerina.java"} ] +[[package]] +org = "ballerina" +name = "uuid" +version = "1.6.0" +dependencies = [ + {org = "ballerina", name = "crypto"}, + {org = "ballerina", name = "jballerina.java"}, + {org = "ballerina", name = "lang.int"}, + {org = "ballerina", name = "time"} +] +modules = [ + {org = "ballerina", packageName = "uuid", moduleName = "uuid"} +] + [[package]] org = "ballerina" name = "websocket" diff --git a/ballerina/annotation_processor.bal b/ballerina/annotation_processor.bal index ab3915729..3b5bd2b84 100644 --- a/ballerina/annotation_processor.bal +++ b/ballerina/annotation_processor.bal @@ -105,6 +105,15 @@ isolated function getFieldInterceptors(service object {} serviceObj, parser:Root return []; } +isolated function getPrefetchMethodName(service object {} serviceObj, Field 'field) returns string? { + GraphqlResourceConfig? resourceConfig = getResourceAnnotation(serviceObj, + 'field.getOperationType(), 'field.getResourcePath(), 'field.getName()); + if resourceConfig is GraphqlResourceConfig { + return resourceConfig.prefetchMethodName; + } + return; +} + isolated function isGlobalInterceptor(readonly & Interceptor interceptor) returns boolean { GraphqlInterceptorConfig? interceptorConfig = getInterceptorConfig(interceptor); if interceptorConfig is GraphqlInterceptorConfig { diff --git a/ballerina/annotations.bal b/ballerina/annotations.bal index c5f3f40cd..1df693185 100644 --- a/ballerina/annotations.bal +++ b/ballerina/annotations.bal @@ -43,8 +43,10 @@ public annotation GraphqlServiceConfig ServiceConfig on service; # Provides a set of configurations for the GraphQL resolvers. # # + interceptors - GraphQL field level interceptors +# + prefetchMethodName - The name of the instance method to be used for prefetching public type GraphqlResourceConfig record {| readonly (readonly & Interceptor)|(readonly & Interceptor)[] interceptors = []; + string prefetchMethodName?; |}; # The annotation to configure a GraphQL resolver. diff --git a/ballerina/common_utils.bal b/ballerina/common_utils.bal index 044f627be..8e16aa09f 100644 --- a/ballerina/common_utils.bal +++ b/ballerina/common_utils.bal @@ -14,9 +14,10 @@ // specific language governing permissions and limitations // under the License. -import ballerina/http; import graphql.parser; +import ballerina/http; + // Error messages const UNABLE_TO_PERFORM_DATA_BINDING = "Unable to perform data binding"; diff --git a/ballerina/constants.bal b/ballerina/constants.bal index a4a44602b..9a85726d5 100644 --- a/ballerina/constants.bal +++ b/ballerina/constants.bal @@ -78,3 +78,6 @@ const decimal PONG_MESSAGE_HANDLER_SCHEDULE_INTERVAL = 15; // Constants used in the executor visitor const OPERATION_TYPE = "operationType"; const PATH = "path"; + +// Constants related to the DataLoader +const DEFAULT_PREFETCH_METHOD_NAME_PREFIX = "pre"; diff --git a/ballerina/context.bal b/ballerina/context.bal index 8c8c5e59f..8a3db244f 100644 --- a/ballerina/context.bal +++ b/ballerina/context.bal @@ -14,6 +14,8 @@ // specific language governing permissions and limitations // under the License. +import graphql.dataloader; + import ballerina/http; import ballerina/jballerina.java; import ballerina/lang.value; @@ -25,6 +27,12 @@ public isolated class Context { private Engine? engine; private int nextInterceptor; private boolean hasFileInfo = false; // This field value changed by setFileInfo method + private map idDataLoaderMap = {}; // Provides mapping between user defined id and DataLoader + private map uuidPlaceholderMap = {}; + private Placeholder[] unResolvedPlaceholders = []; + private boolean containPlaceholders = false; + private int unResolvedPlaceholderCount = 0; // Tracks the number of Placeholders needs to be resolved + private int unResolvedPlaceholderNodeCount = 0; // Tracks the number of nodes to be replaced in the value tree public isolated function init(map attributes = {}, Engine? engine = (), int nextInterceptor = 0) { @@ -90,6 +98,26 @@ public isolated class Context { } } + # Register a given DataLoader instance for a given key in the GraphQL context. + # + # + key - The key for the DataLoader to be registered + # + dataloader - The DataLoader instance to be registered + public isolated function registerDataLoader(string key, dataloader:DataLoader dataloader) { + lock { + self.idDataLoaderMap[key] = dataloader; + } + } + + # Retrieves a DataLoader instance using the given key from the GraphQL context. + # + # + key - The key corresponding to the required DataLoader instance + # + return - The DataLoader instance if the key is present in the context otherwise panics + public isolated function getDataLoader(string key) returns dataloader:DataLoader { + lock { + return self.idDataLoaderMap.get(key); + } + } + isolated function addError(ErrorDetail err) { lock { self.errors.push(err.clone()); @@ -120,7 +148,7 @@ public isolated class Context { public isolated function resolve(Field 'field) returns anydata { Engine? engine = self.getEngine(); if engine is Engine { - return engine.resolve(self, 'field); + return engine.resolve(self, 'field, false); } return; } @@ -185,13 +213,74 @@ public isolated class Context { } } - isolated function cloneWithoutErrors() returns Context { + isolated function addUnresolvedPlaceholder(string uuid, Placeholder placeholder) { + lock { + self.containPlaceholders = true; + self.uuidPlaceholderMap[uuid] = placeholder; + self.unResolvedPlaceholders.push(placeholder); + self.unResolvedPlaceholderCount += 1; + self.unResolvedPlaceholderNodeCount += 1; + } + } + + isolated function resolvePlaceholders() { lock { - Context clonedContext = new(self.attributes, self.engine, self.nextInterceptor); - if self.hasFileInfo { - clonedContext.setFileInfo(self.getFileInfo()); + string[] nonDispatchedDataLoaderIds = self.idDataLoaderMap.keys(); + Placeholder[] unResolvedPlaceholders = self.unResolvedPlaceholders; + self.unResolvedPlaceholders = []; + foreach string dataLoaderId in nonDispatchedDataLoaderIds { + self.idDataLoaderMap.get(dataLoaderId).dispatch(); + } + foreach Placeholder placeholder in unResolvedPlaceholders { + Engine? engine = self.getEngine(); + if engine is () { + continue; + } + anydata resolvedValue = engine.resolve(self, 'placeholder.getField(), false); + placeholder.setValue(resolvedValue); + self.unResolvedPlaceholderCount -= 1; } - return clonedContext; + } + } + + isolated function getPlaceholderValue(string uuid) returns anydata { + lock { + return self.uuidPlaceholderMap.remove(uuid).getValue(); + } + } + + isolated function getUnresolvedPlaceholderCount() returns int { + lock { + return self.unResolvedPlaceholderCount; + } + } + + isolated function getUnresolvedPlaceholderNodeCount() returns int { + lock { + return self.unResolvedPlaceholderNodeCount; + } + } + + isolated function decrementUnresolvedPlaceholderNodeCount() { + lock { + self.unResolvedPlaceholderNodeCount-=1; + } + } + + isolated function hasPlaceholders() returns boolean { + lock { + return self.containPlaceholders; + } + } + + isolated function clearDataLoadersCachesAndPlaceholders() { + // This function is called at the end of each subscription loop execution to prevent using old values + // from DataLoader caches in the next iteration and to avoid filling up the idPlaceholderMap. + lock { + self.idDataLoaderMap.forEach(dataloader => dataloader.clearAll()); + self.unResolvedPlaceholders.removeAll(); + self.uuidPlaceholderMap.removeAll(); + self.containPlaceholders = false; } } } diff --git a/ballerina/engine.bal b/ballerina/engine.bal index 9176b41ed..429529fc8 100644 --- a/ballerina/engine.bal +++ b/ballerina/engine.bal @@ -14,10 +14,11 @@ // specific language governing permissions and limitations // under the License. -import ballerina/jballerina.java; - import graphql.parser; +import ballerina/jballerina.java; +import ballerina/uuid; + isolated class Engine { private final readonly & __Schema schema; private final int? maxQueryDepth; @@ -242,54 +243,54 @@ isolated class Engine { } } - isolated function resolve(Context context, Field 'field) returns anydata { + isolated function resolve(Context context, Field 'field, boolean executePrefetchMethod = true) returns anydata { parser:FieldNode fieldNode = 'field.getInternalNode(); - parser:RootOperationType operationType = 'field.getOperationType(); + + if executePrefetchMethod { + service object {}? serviceObject = 'field.getServiceObject(); + if serviceObject is service object {} { + string prefetchMethodName = getPrefetchMethodName(serviceObject, 'field) + ?: getDefaultPrefetchMethodName(fieldNode.getName()); + if self.hasPrefetchMethod(serviceObject, prefetchMethodName) { + return self.getResultFromPrefetchMethodExecution(context, 'field, serviceObject, prefetchMethodName); + } + } + } + (readonly & Interceptor)? interceptor = context.getNextInterceptor('field); __Type fieldType = 'field.getFieldType(); ResponseGenerator responseGenerator = new (self, context, fieldType, 'field.getPath().clone()); - any|error fieldValue; - if operationType == parser:OPERATION_QUERY { - if interceptor is () { - fieldValue = self.resolveResourceMethod(context, 'field, responseGenerator); - } else { + do { + if interceptor is readonly & Interceptor { any|error result = self.executeInterceptor(interceptor, 'field, context); - anydata|error interceptValue = validateInterceptorReturnValue(fieldType, result, - self.getInterceptorName(interceptor)); - if interceptValue is error { - fieldValue = interceptValue; - } else { - return interceptValue; - } + string interceptorName = self.getInterceptorName(interceptor); + return check validateInterceptorReturnValue(fieldType, result, interceptorName); } - } else if operationType == parser:OPERATION_MUTATION { - if interceptor is () { - fieldValue = self.resolveRemoteMethod(context, 'field, responseGenerator); + any fieldValue; + if 'field.getOperationType() == parser:OPERATION_QUERY { + fieldValue = check self.resolveResourceMethod(context, 'field, responseGenerator); + } else if 'field.getOperationType() == parser:OPERATION_MUTATION { + fieldValue = check self.resolveRemoteMethod(context, 'field, responseGenerator); } else { - any|error result = self.executeInterceptor(interceptor, 'field, context); - anydata|error interceptValue = validateInterceptorReturnValue(fieldType, result, - self.getInterceptorName(interceptor)); - if interceptValue is error { - fieldValue = interceptValue; - } else { - return interceptValue; - } - } - } else { - if interceptor is () { - fieldValue = 'field.getFieldValue(); - } else { - any|error result = self.executeInterceptor(interceptor, 'field, context); - anydata|error interceptValue = validateInterceptorReturnValue(fieldType, result, - self.getInterceptorName(interceptor)); - if interceptValue is error { - fieldValue = interceptValue; - } else { - return interceptValue; - } + fieldValue = check 'field.getFieldValue(); } + return responseGenerator.getResult(fieldValue, fieldNode); + } on fail error errorValue { + return responseGenerator.getResult(errorValue, fieldNode); + } + } + + private isolated function getResultFromPrefetchMethodExecution(Context context, Field 'field, + service object {} serviceObject, string prefetchMethodName) returns PlaceholderNode? { + handle? prefetchMethodHandle = self.getMethod(serviceObject, prefetchMethodName); + if prefetchMethodHandle is () { + return (); } - return responseGenerator.getResult(fieldValue, fieldNode); + self.executePrefetchMethod(context, serviceObject, prefetchMethodHandle, 'field); + string uuid = uuid:createType1AsString(); + Placeholder placeholder = new ('field); + context.addUnresolvedPlaceholder(uuid, placeholder); + return {__uuid: uuid}; } isolated function resolveResourceMethod(Context context, Field 'field, ResponseGenerator responseGenerator) returns any|error { @@ -363,6 +364,11 @@ isolated class Engine { 'class: "io.ballerina.stdlib.graphql.runtime.engine.Engine" } external; + isolated function getMethod(service object {} serviceObject, string methodName) + returns handle? = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Engine" + } external; + isolated function executeQueryResource(Context context, service object {} serviceObject, handle resourceMethod, Field 'field, ResponseGenerator responseGenerator, boolean validation) returns any|error = @java:Method { @@ -387,4 +393,14 @@ isolated class Engine { isolated function getInterceptorName(readonly & Interceptor interceptor) returns string = @java:Method { 'class: "io.ballerina.stdlib.graphql.runtime.engine.Engine" } external; + + isolated function hasPrefetchMethod(service object {} serviceObject, string prefetchMethodName) + returns boolean = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Engine" + } external; + + isolated function executePrefetchMethod(Context context, service object {} serviceObject, + handle prefetchMethodHandle, Field 'field) = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Engine" + } external; } diff --git a/ballerina/engine_utils.bal b/ballerina/engine_utils.bal index 2292d34a7..306b2c886 100644 --- a/ballerina/engine_utils.bal +++ b/ballerina/engine_utils.bal @@ -14,10 +14,12 @@ // specific language governing permissions and limitations // under the License. -import ballerina/jballerina.java; import graphql.parser; import graphql.subgraph; +import ballerina/jballerina.java; +import ballerina/lang.regexp; + isolated function getOutputObjectFromErrorDetail(ErrorDetail|ErrorDetail[] errorDetail) returns OutputObject { if errorDetail is ErrorDetail { return { @@ -111,3 +113,9 @@ public isolated function getSdlString(string encodedSchemaString, map dataMap = {[OPERATION_TYPE] : operationNode.getKind(), [PATH] : path}; - return self.visitSelectionsParallelly(operationNode, dataMap.cloneReadOnly()); - } foreach parser:SelectionNode selection in operationNode.getSelections() { if selection is parser:FieldNode { path.push(selection.getName()); @@ -93,9 +86,6 @@ isolated class ExecutorVisitor { public isolated function visitFragment(parser:FragmentNode fragmentNode, anydata data = ()) { parser:RootOperationType operationType = self.getOperationTypeFromData(data); - if operationType != parser:OPERATION_MUTATION { - return self.visitSelectionsParallelly(fragmentNode, data.cloneReadOnly()); - } string[] path = self.getSelectionPathFromData(data); foreach parser:SelectionNode selection in fragmentNode.getSelections() { string[] clonedPath = path.clone(); @@ -106,43 +96,6 @@ isolated class ExecutorVisitor { selection.accept(self, dataMap); } } - - private isolated function visitSelectionsParallelly(parser:SelectionParentNode selectionParentNode, - readonly & anydata data = ()) { - parser:RootOperationType operationType = self.getOperationTypeFromData(data); - [parser:SelectionNode, future<()>][] selectionFutures = []; - foreach parser:SelectionNode selection in selectionParentNode.getSelections() { - string[] path = self.getSelectionPathFromData(data); - if selection is parser:FieldNode { - path.push(selection.getName()); - } - map dataMap = {[OPERATION_TYPE] : operationType, [PATH] : path}; - future<()> 'future = start selection.accept(self, dataMap.cloneReadOnly()); - selectionFutures.push([selection, 'future]); - } - foreach [parser:SelectionNode, future<()>] [selection, 'future] in selectionFutures { - error? err = wait 'future; - if err is () { - continue; - } - log:printError("Error occured while attempting to resolve selection future", err, - stackTrace = err.stackTrace()); - lock { - if selection is parser:FieldNode { - string[] path = self.getSelectionPathFromData(data); - path.push(selection.getName()); - ErrorDetail errorDetail = { - message: err.message(), - locations: [selection.getLocation()], - path: path.clone() - }; - self.data[selection.getAlias()] = (); - self.errors.push(errorDetail); - } - } - } - } - public isolated function visitDirective(parser:DirectiveNode directiveNode, anydata data = ()) {} public isolated function visitVariable(parser:VariableNode variableNode, anydata data = ()) {} @@ -160,18 +113,20 @@ isolated class ExecutorVisitor { } Field 'field = getFieldObject(fieldNode, operationType, schema, engine, result); context.resetInterceptorCount(); - Context clonedContext = context.cloneWithoutErrors(); - anydata resolvedResult = engine.resolve(clonedContext, 'field); - context.addErrors(clonedContext.getErrors()); + readonly & anydata resolvedResult = engine.resolve(context, 'field); lock { - self.errors = self.context.getErrors(); self.data[fieldNode.getAlias()] = resolvedResult is ErrorDetail ? () : resolvedResult.cloneReadOnly(); } } isolated function getOutput() returns OutputObject { lock { - return getOutputObject(self.data.clone(), self.errors.clone()); + if !self.context.hasPlaceholders() { + // Avoid rebuilding the value tree if there are no place holders + return getOutputObject(self.data.clone(), self.context.getErrors().clone()); + } + ValueTreeBuilder valueTreeBuilder = new (self.context, self.data); + return getOutputObject(valueTreeBuilder.build(), self.context.getErrors().clone()); } } diff --git a/ballerina/modules/dataloader/dataloader.bal b/ballerina/modules/dataloader/dataloader.bal new file mode 100644 index 000000000..6d0b0e70e --- /dev/null +++ b/ballerina/modules/dataloader/dataloader.bal @@ -0,0 +1,132 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/jballerina.java; + +# Represents the type of the batch function to be used in the DataLoader. +public type BatchLoadFunction isolated function (readonly & anydata[] keys) returns anydata[]|error; + +# Represents a DataLoader object that can be used to load data from a data source. +public type DataLoader isolated object { + + # Collects a key to perform a batch operation at a later time. + # + # + key - The key to load later + public isolated function add(anydata key); + + # Retrieves the result for a particular key. + # + # + key - The key to retrieve the result + # + 'type - The type of the result + # + return - The result for the key on success, error on failure + public isolated function get(anydata key, typedesc 'type = <>) returns 'type|error; + + # Dispatches a user-defined batch load operation for all keys that have been collected. + public isolated function dispatch(); + + # Clears all the keys and results from the data loader cache. + public isolated function clearAll(); +}; + +# Represents a default implementation of the DataLoader. +public isolated class DefaultDataLoader { + *DataLoader; + private final table key(key) keyTable = table []; + private final table key(key) resultTable = table []; + private final BatchLoadFunction batchFunction; + + # Initializes the DataLoader with the given batch function. + # + # + loadFunction - The batch function to be used + public isolated function init(BatchLoadFunction loadFunction) { + self.batchFunction = loadFunction; + } + + # Collects a key to perform a batch operation at a later time. + # + # + key - The key to load later + public isolated function add(anydata key) { + readonly & anydata clonedKey = key.cloneReadOnly(); + lock { + // Avoid duplicating keys and get values from cache if available + if self.keyTable.hasKey(clonedKey) || self.resultTable.hasKey(clonedKey) { + return; + } + self.keyTable.add({key: clonedKey}); + } + } + + # Retrieves the result for a particular key. + # + # + key - The key to retrieve the result + # + 'type - The type of the result + # + return - The result for the key on success, error on failure + public isolated function get(anydata key, typedesc 'type = <>) returns 'type|error = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.DataLoader" + } external; + + private isolated function processGet(anydata key, typedesc 'type) returns anydata|error { + readonly & anydata clonedKey = key.cloneReadOnly(); + lock { + if self.resultTable.hasKey(clonedKey) { + anydata|error result = self.resultTable.get(clonedKey).value; + if result is error { + return result.clone(); + } + return (check result.ensureType('type)).clone(); + } + } + return error(string `No result found for the given key ${key.toString()}`); + } + + # Dispatches a user-defined batch load operation for all keys that have been collected. + public isolated function dispatch() { + lock { + if self.keyTable.length() == 0 { + return; + } + readonly & anydata[] batchKeys = self.keyTable.toArray().'map((key) => key.key).cloneReadOnly(); + self.keyTable.removeAll(); + anydata[]|error batchResult = self.batchFunction(batchKeys); + if batchResult is anydata[] && batchKeys.length() != batchResult.length() { + batchResult = error("The batch function should return a number of results equal to the number of keys"); + } + foreach int i in 0 ..< batchKeys.length() { + if self.resultTable.hasKey(batchKeys[i]) { + continue; + } + self.resultTable.add({key: batchKeys[i], value: batchResult is error ? batchResult : batchResult[i]}); + } + } + } + + # Clears all the keys and results from the data loader cache. + public isolated function clearAll() { + lock { + self.keyTable.removeAll(); + self.resultTable.removeAll(); + } + } +} + +type Result record {| + readonly anydata key; + anydata|error value; +|}; + +type Key record {| + readonly anydata key; +|}; diff --git a/ballerina/modules/dataloader/tests/dataloader_tests.bal b/ballerina/modules/dataloader/tests/dataloader_tests.bal new file mode 100644 index 000000000..d974ce499 --- /dev/null +++ b/ballerina/modules/dataloader/tests/dataloader_tests.bal @@ -0,0 +1,87 @@ +import ballerina/test; + +@test:Config +isolated function testDispatch() returns error? { + final DataLoader loader = new DefaultDataLoader(authorLoaderFunction); + lock { + readonly & int[] keys = [...authorTable.keys(), 1]; + keys.forEach(key => loader.add(key)); + loader.dispatch(); + foreach int key in keys { + AuthorRow author = check loader.get(key); + test:assertEquals(author, authorTable.get(key)); + } + } +} + +@test:Config +isolated function testDataBindingError() returns error? { + final DataLoader loader = new DefaultDataLoader(authorLoaderFunction); + loader.add(1); + lock { + loader.dispatch(); + int|error author = loader.get(1); + test:assertTrue(author is error); + } +} + +@test:Config +isolated function testBatchLoadFunctionReturingError() returns error? { + final DataLoader loader = new DefaultDataLoader(authorLoaderFunction); + loader.add(10); + lock { + loader.dispatch(); + AuthorRow|error author = loader.get(10); + test:assertTrue(author is error); + if author is error { + test:assertEquals(author.message(), "Invalid keys found for authors"); + } + } +} + +@test:Config +isolated function testClearAll() returns error? { + final DataLoader loader = new DefaultDataLoader(authorLoaderFunction); + lock { + authorTable.keys().forEach(key => loader.add(key)); + loader.dispatch(); + loader.clearAll(); + foreach int key in authorTable.keys() { + AuthorRow|error author = loader.get(key); + test:assertTrue(author is error); + if author is error { + test:assertEquals(author.message(), string `No result found for the given key ${key}`); + } + } + } +} + +@test:Config +isolated function testDataLoaderHavingFaultyBatchLoadFunction() returns error? { + final DataLoader loader = new DefaultDataLoader(faultyAuthorLoaderFunction); + lock { + authorTable.keys().forEach(key => loader.add(key)); + loader.dispatch(); + foreach int key in authorTable.keys() { + AuthorRow|error author = loader.get(key); + test:assertTrue(author is error); + if author is error { + test:assertEquals(author.message(), "The batch function should return a number of results equal to the number of keys"); + } + } + } +} + +isolated function authorLoaderFunction(readonly & anydata[] ids) returns AuthorRow[]|error { + readonly & int[] keys = check ids.ensureType(); + // Simulate query: SELECT * FROM authors WHERE id IN (...keys); + lock { + readonly & int[] validKeys = keys.'filter(key => authorTable.hasKey(key)).cloneReadOnly(); + return keys.length() != validKeys.length() ? error("Invalid keys found for authors") + : validKeys.'map(key => authorTable.get(key)); + } +}; + +isolated function faultyAuthorLoaderFunction(readonly & anydata[] ids) returns AuthorRow[]|error { + return []; +}; diff --git a/ballerina/modules/dataloader/tests/values.bal b/ballerina/modules/dataloader/tests/values.bal new file mode 100644 index 000000000..a5c73eb19 --- /dev/null +++ b/ballerina/modules/dataloader/tests/values.bal @@ -0,0 +1,12 @@ +type AuthorRow record {| + readonly int id; + string name; +|}; + +final isolated table key(id) authorTable = table [ + {id: 1, name: "Author 1"}, + {id: 2, name: "Author 2"}, + {id: 3, name: "Author 3"}, + {id: 4, name: "Author 4"}, + {id: 5, name: "Author 5"} +]; diff --git a/ballerina/modules/parser/utils.bal b/ballerina/modules/parser/utils.bal index 07da424f0..6499b2c8a 100644 --- a/ballerina/modules/parser/utils.bal +++ b/ballerina/modules/parser/utils.bal @@ -96,5 +96,5 @@ isolated function isAsciiDigit(int codePoint) returns boolean { } public isolated function getHashCode(object {} obj) returns string = @java:Method { - 'class: "io.ballerina.stdlib.graphql.runtime.parser.ParserUtils" + 'class: "io.ballerina.stdlib.graphql.runtime.utils.Utils" } external; diff --git a/ballerina/place_holder.bal b/ballerina/place_holder.bal new file mode 100644 index 000000000..2fae8869c --- /dev/null +++ b/ballerina/place_holder.bal @@ -0,0 +1,48 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.org) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/jballerina.java; + +isolated class Placeholder { + private Field? 'field = (); + private anydata value = (); + + isolated function init(Field 'field) { + self.setField('field); + } + + isolated function setValue(anydata value) { + lock { + self.value = value.clone(); + } + } + + isolated function getValue() returns anydata { + lock { + return self.value.clone(); + } + } + + isolated function setField(Field 'field) = @java:Method { + name: "setFieldValue", + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Placeholder" + } external; + + isolated function getField() returns Field = @java:Method { + name: "getFieldValue", + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Placeholder" + } external; +}; diff --git a/ballerina/records.bal b/ballerina/records.bal index 15e1fa689..951622f52 100644 --- a/ballerina/records.bal +++ b/ballerina/records.bal @@ -271,3 +271,7 @@ type ParseResult record {| parser:DocumentNode document; ErrorDetail[] validationErrors; |}; + +type PlaceholderNode record {| + string __uuid; +|}; diff --git a/ballerina/response_generator.bal b/ballerina/response_generator.bal index 3c75650a4..b9564a99a 100644 --- a/ballerina/response_generator.bal +++ b/ballerina/response_generator.bal @@ -78,11 +78,8 @@ class ResponseGenerator { clonedPath.push(fieldNode.getName()); __Type fieldType = getFieldTypeFromParentType(self.fieldType, self.engine.getSchema().types, fieldNode); Field 'field = new (fieldNode, fieldType, parentValue, clonedPath); - Context context = self.context.cloneWithoutErrors(); - context.resetInterceptorCount(); - anydata result = self.engine.resolve(context, 'field); - self.context.addErrors(context.getErrors()); - return result; + self.context.resetInterceptorCount(); + return self.engine.resolve(self.context, 'field); } } @@ -151,11 +148,8 @@ class ResponseGenerator { (string|int)[] clonedPath = self.path.clone(); clonedPath.push(fieldNode.getName()); Field 'field = new (fieldNode, fieldType, path = clonedPath, fieldValue = fieldValue); - Context context = self.context.cloneWithoutErrors(); - context.resetInterceptorCount(); - anydata result = self.engine.resolve(context, 'field); - self.context.addErrors(context.getErrors()); - return result; + self.context.resetInterceptorCount(); + return self.engine.resolve(self.context, 'field); } isolated function getResultFromArray((any|error)[] parentValue, parser:FieldNode parentNode) returns anydata { diff --git a/ballerina/value_tree_builder.bal b/ballerina/value_tree_builder.bal new file mode 100644 index 000000000..20520f9a3 --- /dev/null +++ b/ballerina/value_tree_builder.bal @@ -0,0 +1,83 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +isolated class ValueTreeBuilder { + private final Context context; + private final Data placeholderTree; + + isolated function init(Context context, Data placeholderTree) { + self.context = context; + self.placeholderTree = placeholderTree.clone(); + } + + isolated function build() returns Data { + lock { + return self.buildValueTree(self.context, self.placeholderTree).clone(); + } + } + + isolated function buildValueTree(Context context, anydata partialValue) returns anydata { + if context.getUnresolvedPlaceholderNodeCount() == 0 { + return partialValue; + } + while context.getUnresolvedPlaceholderCount() > 0 { + context.resolvePlaceholders(); + } + if partialValue is ErrorDetail { + return partialValue; + } + if partialValue is PlaceholderNode { + anydata value = context.getPlaceholderValue(partialValue.__uuid); + context.decrementUnresolvedPlaceholderNodeCount(); + return self.buildValueTree(context, value); + } + if partialValue is map && isMap(partialValue) { + return self.buildValueTreeFromMap(context, partialValue); + } + if partialValue is record {} { + return self.buildValueTreeFromRecord(context, partialValue); + } + if partialValue is anydata[] { + return self.buildValueTreeFromArray(context, partialValue); + } + return partialValue; + } + + isolated function buildValueTreeFromMap(Context context, map partialValue) returns map { + map data = {}; + foreach [string, anydata] [key, value] in partialValue.entries() { + data[key] = self.buildValueTree(context, value); + } + return data; + } + + isolated function buildValueTreeFromRecord(Context context, record {} partialValue) returns record {} { + record {} data = {}; + foreach [string, anydata] [key, value] in partialValue.entries() { + data[key] = self.buildValueTree(context, value); + } + return data; + } + + isolated function buildValueTreeFromArray(Context context, anydata[] partialValue) returns anydata[] { + anydata[] data = []; + foreach anydata element in partialValue { + anydata newVal = self.buildValueTree(context, element); + data.push(newVal); + } + return data; + } +} diff --git a/ballerina/websocket_utils.bal b/ballerina/websocket_utils.bal index d4ee9897b..7c421e9f0 100644 --- a/ballerina/websocket_utils.bal +++ b/ballerina/websocket_utils.bal @@ -37,6 +37,7 @@ isolated function executeOperation(Engine engine, Context context, readonly & __ } any|error resultValue = next is error ? next : next.value; OutputObject outputObject = engine.getResult(node, context, resultValue); + context.clearDataLoadersCachesAndPlaceholders(); if outputObject.hasKey(DATA_FIELD) || outputObject.hasKey(ERRORS_FIELD) { NextMessage response = {'type: 'WS_NEXT, id: handler.getId(), payload: outputObject.toJson()}; check writeMessage(caller, response); diff --git a/build-config/resources/Ballerina.toml b/build-config/resources/Ballerina.toml index 46b34abdc..e141e233f 100644 --- a/build-config/resources/Ballerina.toml +++ b/build-config/resources/Ballerina.toml @@ -3,7 +3,7 @@ org = "ballerina" name = "graphql" version = "@toml.version@" authors = ["Ballerina"] -export=["graphql", "graphql.subgraph"] +export=["graphql", "graphql.subgraph", "graphql.dataloader"] keywords = ["gql", "network", "query", "service"] repository = "https://github.com/ballerina-platform/module-ballerina-graphql" icon = "icon.png" diff --git a/changelog.md b/changelog.md index 337abffa6..4cb0bb528 100644 --- a/changelog.md +++ b/changelog.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ### Added - [[#2998] Add '@deprecated' Directive Support for Output Object Defined using Record Types](https://github.com/ballerina-platform/ballerina-standard-library/issues/2998) - [[#4586] Add Support for Printing GraphiQL Url to Stdout](https://github.com/ballerina-platform/ballerina-standard-library/issues/4586) +- [[#4569] Introduce DataLoader for Ballerina GraphQL](https://github.com/ballerina-platform/ballerina-standard-library/issues/4569) ### Fixed - [[#4627] Fix Schema Generation Failure when Service has Type Alias](https://github.com/ballerina-platform/ballerina-standard-library/issues/4627) diff --git a/commons/src/main/java/io/ballerina/stdlib/graphql/commons/utils/Utils.java b/commons/src/main/java/io/ballerina/stdlib/graphql/commons/utils/Utils.java index 2ca5082e4..eddc8e940 100644 --- a/commons/src/main/java/io/ballerina/stdlib/graphql/commons/utils/Utils.java +++ b/commons/src/main/java/io/ballerina/stdlib/graphql/commons/utils/Utils.java @@ -43,6 +43,7 @@ public class Utils { public static final String SUBGRAPH_SUB_MODULE_NAME = "graphql.subgraph"; public static final String PACKAGE_ORG = "ballerina"; public static final String SERVICE_NAME = "Service"; + private static final String DATA_LOADER_SUB_MODULE_NAME = "graphql.dataloader"; public static boolean isGraphqlService(SyntaxNodeAnalysisContext context) { ServiceDeclarationNode node = (ServiceDeclarationNode) context.node(); diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/graphql/compiler/SchemaGenerationTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/graphql/compiler/SchemaGenerationTest.java index f07fcd226..67904c145 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/graphql/compiler/SchemaGenerationTest.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/graphql/compiler/SchemaGenerationTest.java @@ -200,6 +200,13 @@ public void testGraphqlFederationSubgraph() { Assert.assertEquals(diagnosticResult.errorCount(), 0); } + @Test + public void testGraphqlDataLoader() { + String packagePath = "24_dataloader"; + DiagnosticResult diagnosticResult = getDiagnosticResult(packagePath); + Assert.assertEquals(diagnosticResult.errorCount(), 0); + } + private DiagnosticResult getDiagnosticResult(String path) { Path projectDirPath = RESOURCE_DIRECTORY.resolve(path); BuildProject project = BuildProject.load(getEnvironmentBuilder(), projectDirPath); diff --git a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/graphql/compiler/ServiceValidationTest.java b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/graphql/compiler/ServiceValidationTest.java index c847d2653..655b77712 100644 --- a/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/graphql/compiler/ServiceValidationTest.java +++ b/compiler-plugin-tests/src/test/java/io/ballerina/stdlib/graphql/compiler/ServiceValidationTest.java @@ -1129,9 +1129,97 @@ public void testValidUsageOfIdAnnotation() { Assert.assertEquals(diagnosticResult.errorCount(), 0); } + @Test(groups = "invalid") + public void testPrefetchMethodWithoutContextParameter() { + String packagePath = "64_prefetch_method_without_context_parameter"; + DiagnosticResult diagnosticResult = getDiagnosticResult(packagePath); + Assert.assertEquals(diagnosticResult.errorCount(), 1); + Iterator diagnosticIterator = diagnosticResult.errors().iterator(); + + Diagnostic diagnostic = diagnosticIterator.next(); + String message = getErrorMessage(CompilationDiagnostic.MISSING_GRAPHQL_CONTEXT_PARAMETER, "preBooks"); + assertErrorMessage(diagnostic, message, 30, 23); + } + + @Test(groups = "invalid") + public void testPrefetchMethodWithInvalidParameters() { + String packagePath = "65_prefetch_method_with_invalid_parameters"; + DiagnosticResult diagnosticResult = getDiagnosticResult(packagePath); + Assert.assertEquals(diagnosticResult.errorCount(), 1); + Iterator diagnosticIterator = diagnosticResult.errors().iterator(); + + Diagnostic diagnostic = diagnosticIterator.next(); + String message = getErrorMessage(CompilationDiagnostic.INVALID_PARAMETER_IN_PREFETCH_METHOD, "int id", + "preBooks", "books"); + assertErrorMessage(diagnostic, message, 26, 23); + } + + @Test(groups = "invalid") + public void testPrefetchMethodWithInvalidReturnType() { + String packagePath = "66_prefetch_method_with_invalid_return_type"; + DiagnosticResult diagnosticResult = getDiagnosticResult(packagePath); + Assert.assertEquals(diagnosticResult.errorCount(), 1); + Iterator diagnosticIterator = diagnosticResult.errors().iterator(); + + Diagnostic diagnostic = diagnosticIterator.next(); + String message = getErrorMessage(CompilationDiagnostic.INVALID_RETURN_TYPE_IN_PREFETCH_METHOD, "int", + "preBooks"); + assertErrorMessage(diagnostic, message, 30, 23); + } + + @Test(groups = "valid") + public void testServiceWithValidDataLoaderConfiguration() { + String packagePath = "67_service_with_valid_data_loader_configuration"; + DiagnosticResult diagnosticResult = getDiagnosticResult(packagePath); + Assert.assertEquals(diagnosticResult.errorCount(), 0); + } + + @Test(groups = "invalid") + public void testServiceWithInvalidPrefetchMethodNameConfig() { + String packagePath = "68_service_with_invalid_prefetch_method_name_config"; + DiagnosticResult diagnosticResult = getDiagnosticResult(packagePath); + Assert.assertEquals(diagnosticResult.errorCount(), 2); + Iterator diagnosticIterator = diagnosticResult.errors().iterator(); + + Diagnostic diagnostic = diagnosticIterator.next(); + String message = getErrorMessage(CompilationDiagnostic.UNABLE_TO_FIND_PREFETCH_METHOD, "loadBooks", "books"); + assertErrorMessage(diagnostic, message, 46, 32); + + diagnostic = diagnosticIterator.next(); + message = getErrorMessage(CompilationDiagnostic.UNABLE_TO_FIND_PREFETCH_METHOD, "prefetchUpdateAuthor", + "updateAuthor"); + assertErrorMessage(diagnostic, message, 37, 21); + } + + @Test(groups = "invalid") + public void testSubscriptionWithInvalidPrefetchMethodNameConfig() { + String packagePath = "69_subscription_with_invalid_prefetch_method_name_config"; + DiagnosticResult diagnosticResult = getDiagnosticResult(packagePath); + Assert.assertEquals(diagnosticResult.errorCount(), 1); + Iterator diagnosticIterator = diagnosticResult.errors().iterator(); + + Diagnostic diagnostic = diagnosticIterator.next(); + String message = getErrorMessage(CompilationDiagnostic.INVALID_USAGE_OF_PREFETCH_METHOD_NAME_CONFIG, + "prefetchMethodName", "authors"); + assertErrorMessage(diagnostic, message, 24, 5); + } + + @Test(groups = "invalid") + public void testPrefetchMethodConfigurationUsingVariableValue() { + String packagePath = "70_prefetch_method_configuration_using_variable_value"; + DiagnosticResult diagnosticResult = getDiagnosticResult(packagePath); + Assert.assertEquals(diagnosticResult.warningCount(), 1); + Iterator diagnosticIterator = diagnosticResult.warnings().iterator(); + + Diagnostic diagnostic = diagnosticIterator.next(); + String message = getErrorMessage(CompilationDiagnostic.UNABLE_TO_VALIDATE_PREFETCH_METHOD, "prefetchMethodName", + "updateAuthor"); + assertWarningMessage(diagnostic, message, 36, 5); + } + @Test(groups = "invalid") public void testInvalidUsageDeprecatedDirective() { - String packagePath = "64_invalid_usages_of_deprecated_directive"; + String packagePath = "71_invalid_usages_of_deprecated_directive"; DiagnosticResult diagnosticResult = getDiagnosticResult(packagePath); Assert.assertEquals(diagnosticResult.warningCount(), 2); Iterator diagnosticIterator = diagnosticResult.warnings().iterator(); diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/64_invalid_usages_of_deprecated_directive/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/24_dataloader/Ballerina.toml similarity index 100% rename from compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/64_invalid_usages_of_deprecated_directive/Ballerina.toml rename to compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/24_dataloader/Ballerina.toml diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/24_dataloader/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/24_dataloader/service.bal new file mode 100644 index 000000000..0c292e9dd --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/generator_tests/24_dataloader/service.bal @@ -0,0 +1,102 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/graphql; +import ballerina/graphql.dataloader; +import ballerina/http; + +@graphql:ServiceConfig { + contextInit: isolated function (http:RequestContext requestContext, http:Request request) returns graphql:Context|error { + graphql:Context ctx = new; + ctx.registerDataLoader("authorLoader", new dataloader:DefaultDataLoader(authorLoaderFunction)); + ctx.registerDataLoader("authorUpdateLoader", new dataloader:DefaultDataLoader(authorUpdateLoaderFunction)); + ctx.registerDataLoader("bookLoader", new dataloader:DefaultDataLoader(bookLoaderFunction)); + return ctx; + } +} +service on new graphql:Listener(9090) { + function preAuthors(graphql:Context ctx, int[] ids) { + } + + resource function get authors(graphql:Context ctx, int[] ids) returns Author[]|error { + return error("No implementation provided for authors"); + } + + function preUpdateAuthorName(graphql:Context ctx, int id, string name) { + } + + remote function updateAuthorName(graphql:Context ctx, int id, string name) returns Author|error { + return error("No implementation provided for updateAuthorName"); + } +} + +isolated distinct service class Author { + private final readonly & AuthorRow author; + + isolated function init(AuthorRow author) { + self.author = author.cloneReadOnly(); + } + + isolated resource function get name() returns string { + return self.author.name; + } + + isolated function preBooks(graphql:Context ctx) { + } + + isolated resource function get books(graphql:Context ctx) returns Book[]|error { + return error("No implementation provided for books"); + } +} + +isolated distinct service class Book { + private final readonly & BookRow book; + + isolated function init(BookRow book) { + self.book = book.cloneReadOnly(); + } + + isolated resource function get id() returns int { + return self.book.id; + } + + isolated resource function get title() returns string { + return self.book.title; + } +} + +isolated function authorLoaderFunction(readonly & anydata[] ids) returns AuthorRow[]|error { + return []; +}; + +isolated function bookLoaderFunction(readonly & anydata[] ids) returns BookRow[][]|error { + return []; +}; + +isolated function authorUpdateLoaderFunction(readonly & anydata[] idNames) returns AuthorRow[]|error { + return []; +}; + +public type BookRow record { + readonly int id; + string title; + int author; +}; + +public type AuthorRow record { + readonly int id; + string name; +}; diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/64_prefetch_method_without_context_parameter/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/64_prefetch_method_without_context_parameter/Ballerina.toml new file mode 100644 index 000000000..b58c76324 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/64_prefetch_method_without_context_parameter/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "graphql_test" +name = "test_package" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/64_prefetch_method_without_context_parameter/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/64_prefetch_method_without_context_parameter/service.bal new file mode 100644 index 000000000..f2a72db8a --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/64_prefetch_method_without_context_parameter/service.bal @@ -0,0 +1,40 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/graphql; + +service on new graphql:Listener(9090) { + resource function get authors(int[] ids) returns Author[] { + return []; + } +} + +isolated distinct service class Author { + isolated resource function get books() returns Book[] { + return []; + } + + isolated function preBooks() { + } +} + +isolated function bookLoaderFunction(readonly & anydata[] ids) returns anydata[] { + return []; +}; + +public type Book record {| + string title; +|}; diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/65_prefetch_method_with_invalid_parameters/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/65_prefetch_method_with_invalid_parameters/Ballerina.toml new file mode 100644 index 000000000..b58c76324 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/65_prefetch_method_with_invalid_parameters/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "graphql_test" +name = "test_package" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/65_prefetch_method_with_invalid_parameters/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/65_prefetch_method_with_invalid_parameters/service.bal new file mode 100644 index 000000000..24e265245 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/65_prefetch_method_with_invalid_parameters/service.bal @@ -0,0 +1,40 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/graphql; + +service on new graphql:Listener(9090) { + resource function get authors(int[] ids) returns Author[] { + return []; + } +} + +isolated distinct service class Author { + isolated function preBooks(graphql:Context ctx, int id) { + } + + isolated resource function get books(graphql:Context ctx) returns Book[] { + return []; + } +} + +isolated function bookLoaderFunction(readonly & anydata[] ids) returns anydata[] { + return []; +}; + +public type Book record {| + string title; +|}; diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/66_prefetch_method_with_invalid_return_type/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/66_prefetch_method_with_invalid_return_type/Ballerina.toml new file mode 100644 index 000000000..b58c76324 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/66_prefetch_method_with_invalid_return_type/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "graphql_test" +name = "test_package" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/66_prefetch_method_with_invalid_return_type/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/66_prefetch_method_with_invalid_return_type/service.bal new file mode 100644 index 000000000..4fb63bd3d --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/66_prefetch_method_with_invalid_return_type/service.bal @@ -0,0 +1,41 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/graphql; + +service on new graphql:Listener(9090) { + resource function get authors(int[] ids) returns Author[] { + return []; + } +} + +isolated distinct service class Author { + isolated resource function get books(graphql:Context ctx) returns Book[] { + return []; + } + + isolated function preBooks(graphql:Context ctx) returns int { + return 1; + } +} + +isolated function bookLoaderFunction(readonly & anydata[] ids) returns anydata[] { + return []; +}; + +public type Book record {| + string title; +|}; diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/67_service_with_valid_data_loader_configuration/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/67_service_with_valid_data_loader_configuration/Ballerina.toml new file mode 100644 index 000000000..b58c76324 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/67_service_with_valid_data_loader_configuration/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "graphql_test" +name = "test_package" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/67_service_with_valid_data_loader_configuration/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/67_service_with_valid_data_loader_configuration/service.bal new file mode 100644 index 000000000..3c156a322 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/67_service_with_valid_data_loader_configuration/service.bal @@ -0,0 +1,69 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/graphql; +import ballerina/graphql.dataloader; +import ballerina/http; + +@graphql:ServiceConfig { + contextInit: isolated function (http:RequestContext requestContext, http:Request request) returns graphql:Context|error { + graphql:Context ctx = new; + ctx.registerDataLoader("authorLoader", new dataloader:DefaultDataLoader(authorLoaderFunction)); + ctx.registerDataLoader("bookLoader", new dataloader:DefaultDataLoader(bookLoaderFunction)); + return ctx; + } +} +service on new graphql:Listener(9090) { + resource function get authors(int[] ids) returns Author[] { + return []; + } + + function preUpdateAuthor(graphql:Context ctx, int id) { + dataloader:DataLoader authorLoader = ctx.getDataLoader("authorLoader"); + authorLoader.add(id); + } + + remote function updateAuthor(graphql:Context ctx, int id, string name) returns Author|error { + return error("No implementation provided for updateAuthor"); + } +} + +isolated distinct service class Author { + isolated function preBooks(graphql:Context ctx) { + dataloader:DataLoader bookLoader = ctx.getDataLoader("bookLoader"); + bookLoader.add(1); + } + + isolated resource function get books(graphql:Context ctx) returns Book[] { + return []; + } +} + +isolated function bookLoaderFunction(readonly & anydata[] ids) returns Book[][] { + return []; +}; + +isolated function authorLoaderFunction(readonly & anydata[] ids) returns AuthorData[] { + return []; +}; + +public type Book record {| + string title; +|}; + +type AuthorData record {| + string name; +|}; diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/68_service_with_invalid_prefetch_method_name_config/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/68_service_with_invalid_prefetch_method_name_config/Ballerina.toml new file mode 100644 index 000000000..b58c76324 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/68_service_with_invalid_prefetch_method_name_config/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "graphql_test" +name = "test_package" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/68_service_with_invalid_prefetch_method_name_config/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/68_service_with_invalid_prefetch_method_name_config/service.bal new file mode 100644 index 000000000..5cd1535f9 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/68_service_with_invalid_prefetch_method_name_config/service.bal @@ -0,0 +1,65 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/graphql; +import ballerina/graphql.dataloader; +import ballerina/http; + +@graphql:ServiceConfig { + contextInit: isolated function (http:RequestContext requestContext, http:Request request) returns graphql:Context|error { + graphql:Context ctx = new; + ctx.registerDataLoader("authorLoader", new dataloader:DefaultDataLoader(authorLoaderFunction)); + ctx.registerDataLoader("bookLoader", new dataloader:DefaultDataLoader(bookLoaderFunction)); + return ctx; + } +} +service on new graphql:Listener(9090) { + resource function get authors(int[] ids) returns Author[] { + return []; + } + + @graphql:ResourceConfig { + prefetchMethodName : "prefetchUpdateAuthor" + } + remote function updateAuthor(graphql:Context ctx, int id, string name) returns Author|error { + return error("No implementation provided for updateAuthor"); + } +} + +isolated distinct service class Author { + @graphql:ResourceConfig { + prefetchMethodName : "loadBooks" + } + isolated resource function get books(graphql:Context ctx) returns Book[] { + return []; + } +} + +isolated function bookLoaderFunction(readonly & anydata[] ids) returns Book[][] { + return []; +}; + +isolated function authorLoaderFunction(readonly & anydata[] ids) returns AuthorData[] { + return []; +}; + +public type Book record {| + string title; +|}; + +type AuthorData record {| + string name; +|}; diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/69_subscription_with_invalid_prefetch_method_name_config/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/69_subscription_with_invalid_prefetch_method_name_config/Ballerina.toml new file mode 100644 index 000000000..b58c76324 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/69_subscription_with_invalid_prefetch_method_name_config/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "graphql_test" +name = "test_package" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/69_subscription_with_invalid_prefetch_method_name_config/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/69_subscription_with_invalid_prefetch_method_name_config/service.bal new file mode 100644 index 000000000..7e8fc9617 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/69_subscription_with_invalid_prefetch_method_name_config/service.bal @@ -0,0 +1,39 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/graphql; + +service on new graphql:Listener(9090) { + resource function get authors(int[] ids) returns Author[] { + return []; + } + + @graphql:ResourceConfig { + prefetchMethodName : "prefetchAuthor" + } + resource function subscribe authors() returns stream { + return [].toStream(); + } + + function prefetchAuthor(graphql:Context ctx) { + } +} + +isolated distinct service class Author { + isolated resource function get books(graphql:Context ctx) returns string[] { + return []; + } +} diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/70_prefetch_method_configuration_using_variable_value/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/70_prefetch_method_configuration_using_variable_value/Ballerina.toml new file mode 100644 index 000000000..b58c76324 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/70_prefetch_method_configuration_using_variable_value/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "graphql_test" +name = "test_package" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/70_prefetch_method_configuration_using_variable_value/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/70_prefetch_method_configuration_using_variable_value/service.bal new file mode 100644 index 000000000..2a40acfcb --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/70_prefetch_method_configuration_using_variable_value/service.bal @@ -0,0 +1,74 @@ +// Copyright (c) 2023 WSO2 LLC. (http://www.wso2.com) All Rights Reserved. +// +// WSO2 LLC. licenses this file to you under the Apache License, +// Version 2.0 (the "License"); you may not use this file except +// in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +import ballerina/graphql; +import ballerina/graphql.dataloader; +import ballerina/http; + +const string prefetchMethodName = "prefetchUpdateAuthor"; + +@graphql:ServiceConfig { + contextInit: isolated function (http:RequestContext requestContext, http:Request request) returns graphql:Context|error { + graphql:Context ctx = new; + ctx.registerDataLoader("authorLoader", new dataloader:DefaultDataLoader(authorLoaderFunction)); + ctx.registerDataLoader("bookLoader", new dataloader:DefaultDataLoader(bookLoaderFunction)); + return ctx; + } +} +service on new graphql:Listener(9090) { + resource function get authors(int[] ids) returns Author[] { + return []; + } + + @graphql:ResourceConfig { + prefetchMethodName + } + remote function updateAuthor(graphql:Context ctx, int id, string name) returns Author|error { + return error("No implementation provided for updateAuthor"); + } + + function prefetchUpdateAuthor(graphql:Context ctx, int id) { + dataloader:DataLoader authorLoader = ctx.getDataLoader("authorLoader"); + authorLoader.add(id); + } +} + +isolated distinct service class Author { + isolated resource function get books(graphql:Context ctx) returns Book[] { + return []; + } + + isolated function preBooks(graphql:Context ctx) { + dataloader:DataLoader bookLoader = ctx.getDataLoader("bookLoader"); + bookLoader.add(1); + } +} + +isolated function bookLoaderFunction(readonly & anydata[] ids) returns Book[][] { + return []; +}; + +isolated function authorLoaderFunction(readonly & anydata[] ids) returns AuthorData[] { + return []; +}; + +public type Book record {| + string title; +|}; + +type AuthorData record {| + string name; +|}; diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/71_invalid_usages_of_deprecated_directive/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/71_invalid_usages_of_deprecated_directive/Ballerina.toml new file mode 100644 index 000000000..b58c76324 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/71_invalid_usages_of_deprecated_directive/Ballerina.toml @@ -0,0 +1,4 @@ +[package] +org = "graphql_test" +name = "test_package" +version = "0.1.0" diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/64_invalid_usages_of_deprecated_directive/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/71_invalid_usages_of_deprecated_directive/service.bal similarity index 100% rename from compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/64_invalid_usages_of_deprecated_directive/service.bal rename to compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/71_invalid_usages_of_deprecated_directive/service.bal diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/Utils.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/Utils.java index f534c89ae..64a9e786d 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/Utils.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/Utils.java @@ -73,8 +73,8 @@ public final class Utils { public static final String SUBGRAPH_ANNOTATION_NAME = "Subgraph"; public static final String UUID_RECORD_NAME = "Uuid"; private static final String ORG_NAME = "ballerina"; - private static final String GRAPHQL_MODULE_NAME = "graphql"; private static final String UUID_MODULE_NAME = "uuid"; + private static final String RESOURCE_CONFIG_ANNOTATION = "ResourceConfig"; private Utils() { } @@ -227,8 +227,17 @@ public static boolean isValidGraphqlParameter(TypeSymbol typeSymbol) { if (!isGraphqlModuleSymbol(typeSymbol)) { return false; } - String typeName = typeSymbol.getName().get(); - return FIELD_IDENTIFIER.equals(typeName) || CONTEXT_IDENTIFIER.equals(typeName); + if (isContextParameter(typeSymbol)) { + return true; + } + return FIELD_IDENTIFIER.equals(typeSymbol.getName().get()); + } + + public static boolean isContextParameter(TypeSymbol typeSymbol) { + if (typeSymbol.getName().isEmpty()) { + return false; + } + return isGraphqlModuleSymbol(typeSymbol) && CONTEXT_IDENTIFIER.equals(typeSymbol.getName().get()); } public static String getAccessor(ResourceMethodSymbol resourceMethodSymbol) { @@ -327,4 +336,10 @@ public static boolean isPrimitiveTypeSymbol(TypeSymbol typeSymbol) { } return false; } + + public static boolean hasResourceConfigAnnotation(MethodSymbol resourceMethodSymbol) { + return resourceMethodSymbol.annotations().stream().anyMatch( + annotationSymbol -> isGraphqlModuleSymbol(annotationSymbol) && annotationSymbol.getName().isPresent() + && annotationSymbol.getName().get().equals(RESOURCE_CONFIG_ANNOTATION)); + } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/diagnostics/CompilationDiagnostic.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/diagnostics/CompilationDiagnostic.java index a1c735e6e..ec7bc6e04 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/diagnostics/CompilationDiagnostic.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/diagnostics/CompilationDiagnostic.java @@ -71,15 +71,26 @@ public enum CompilationDiagnostic { INVALID_USE_OF_RESERVED_TYPE_AS_OUTPUT_TYPE(DiagnosticMessage.ERROR_135, DiagnosticCode.GRAPHQL_135, DiagnosticSeverity.ERROR), INVALID_USE_OF_RESERVED_TYPE_AS_INPUT_TYPE(DiagnosticMessage.ERROR_136, DiagnosticCode.GRAPHQL_136, - DiagnosticSeverity.ERROR), + DiagnosticSeverity.ERROR), FAILED_TO_ADD_ENTITY_RESOLVER(DiagnosticMessage.ERROR_137, DiagnosticCode.GRAPHQL_137, DiagnosticSeverity.ERROR), FAILED_TO_ADD_SERVICE_RESOLVER(DiagnosticMessage.ERROR_138, DiagnosticCode.GRAPHQL_138, DiagnosticSeverity.ERROR), UNSUPPORTED_TYPE_ALIAS(DiagnosticMessage.ERROR_139, DiagnosticCode.GRAPHQL_139, DiagnosticSeverity.ERROR), INVALID_USE_OF_ID_ANNOTATION(DiagnosticMessage.ERROR_140, DiagnosticCode.GRAPHQL_140, DiagnosticSeverity.ERROR), + MISSING_GRAPHQL_CONTEXT_PARAMETER(DiagnosticMessage.ERROR_141, DiagnosticCode.GRAPHQL_141, + DiagnosticSeverity.ERROR), + INVALID_PARAMETER_IN_PREFETCH_METHOD(DiagnosticMessage.ERROR_142, DiagnosticCode.GRAPHQL_142, + DiagnosticSeverity.ERROR), + INVALID_RETURN_TYPE_IN_PREFETCH_METHOD(DiagnosticMessage.ERROR_143, DiagnosticCode.GRAPHQL_143, + DiagnosticSeverity.ERROR), + UNABLE_TO_FIND_PREFETCH_METHOD(DiagnosticMessage.ERROR_144, DiagnosticCode.GRAPHQL_144, DiagnosticSeverity.ERROR), + INVALID_USAGE_OF_PREFETCH_METHOD_NAME_CONFIG(DiagnosticMessage.ERROR_145, DiagnosticCode.GRAPHQL_145, + DiagnosticSeverity.ERROR), // Warnings UNSUPPORTED_INPUT_FIELD_DEPRECATION(DiagnosticMessage.WARNING_201, DiagnosticCode.GRAPHQL_201, - DiagnosticSeverity.WARNING); + DiagnosticSeverity.WARNING), + UNABLE_TO_VALIDATE_PREFETCH_METHOD(DiagnosticMessage.WARNING_202, DiagnosticCode.GRAPHQL_202, + DiagnosticSeverity.WARNING); private final String diagnostic; private final String diagnosticCode; diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/diagnostics/DiagnosticCode.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/diagnostics/DiagnosticCode.java index 19004ed21..d188772a3 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/diagnostics/DiagnosticCode.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/diagnostics/DiagnosticCode.java @@ -62,5 +62,11 @@ public enum DiagnosticCode { GRAPHQL_138, GRAPHQL_139, GRAPHQL_140, - GRAPHQL_201 + GRAPHQL_141, + GRAPHQL_142, + GRAPHQL_143, + GRAPHQL_144, + GRAPHQL_145, + GRAPHQL_201, + GRAPHQL_202 } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/diagnostics/DiagnosticMessage.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/diagnostics/DiagnosticMessage.java index a833434e8..9f2f94843 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/diagnostics/DiagnosticMessage.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/diagnostics/DiagnosticMessage.java @@ -43,8 +43,9 @@ public enum DiagnosticMessage { + "\", which is reserved by GraphQL introspection"), ERROR_112("invalid type found in the GraphQL field ''{0}''. A GraphQL field cannot have \"any\" or \"anydata\" as " + "the type, instead use specific types"), - ERROR_113("a GraphQL service must have at least one resource method with a '''" + RESOURCE_FUNCTION_GET + "''' " - + "accessor"), + ERROR_113("a GraphQL service must include at least one resource method with the accessor '''" + + RESOURCE_FUNCTION_GET + "''' that does not have the @dataloader:Loader annotation attached" + + " to it"), ERROR_114("the GraphQL field ''{0}'' use input type ''{1}'' as an output type. A GraphQL field cannot use an input " + "type as an output type"), ERROR_115("the GraphQL field ''{0}'' use output type ''{1}'' as an input type. A GraphQL field cannot use an output" @@ -84,8 +85,21 @@ public enum DiagnosticMessage { ERROR_139("failed to generate schema for type ''{0}''. Type alias for type ''{1}'' is not supported"), ERROR_140("invalid usage of @graphql:ID annotation. @graphql:ID annotation can only be used with string, " + "int, float, decimal and uuid:Uuid types"), + ERROR_141("invalid method signature found in ''{0}'' prefetch method. The method requires a parameter of type " + + "''graphql:Context''"), + ERROR_142("invalid parameter ''{0}'' found in prefetch method ''{1}''. No matching parameter found in" + + " the GraphQL field ''{2}''"), + ERROR_143("invalid return type ''{0}'' found in prefetch method ''{1}''. The data loader " + + "method must not return any value"), + ERROR_144("no prefetch method found with name ''{0}'' for the GraphQL field ''{1}''"), + ERROR_145("invalid usage of ''{0}'' configuration found in subscription resource ''{1}''. ''{0}'' configuration is" + + " only supported for 'remote' methods and 'get' resource methods"), + WARNING_201("invalid usage of @deprecated directive found in ''{0}''. Input object field(s) deprecation " - + "is not supported by the current GraphQL spec."); + + "is not supported by the current GraphQL spec."), + WARNING_202("unable to validate ''{0}'' configuration of the GraphQL field ''{1}''. Pass a string literal to " + + "the ''{0}'' configuration to resolve this warning"); + private final String message; DiagnosticMessage(String message) { diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/service/validator/FunctionDefinitionNodeVisitor.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/service/validator/FunctionDefinitionNodeVisitor.java new file mode 100644 index 000000000..953ce0d8f --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/service/validator/FunctionDefinitionNodeVisitor.java @@ -0,0 +1,54 @@ +package io.ballerina.stdlib.graphql.compiler.service.validator; + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.MethodSymbol; +import io.ballerina.compiler.api.symbols.Symbol; +import io.ballerina.compiler.syntax.tree.AnnotationNode; +import io.ballerina.compiler.syntax.tree.FunctionDefinitionNode; +import io.ballerina.compiler.syntax.tree.NodeList; +import io.ballerina.compiler.syntax.tree.NodeVisitor; + +import java.util.Optional; + +/** + * Obtains ResourceConfig AnnotationNode node from the syntax tree. + */ +public class FunctionDefinitionNodeVisitor extends NodeVisitor { + private static final String RESOURCE_CONFIG_ANNOTATION = "ResourceConfig"; + private final SemanticModel semanticModel; + private final MethodSymbol methodSymbol; + private AnnotationNode annotationNode; + + public FunctionDefinitionNodeVisitor(SemanticModel semanticModel, MethodSymbol methodSymbol) { + this.semanticModel = semanticModel; + this.methodSymbol = methodSymbol; + } + + @Override + public void visit(FunctionDefinitionNode functionDefinitionNode) { + if (this.annotationNode != null) { + return; + } + Optional functionSymbol = this.semanticModel.symbol(functionDefinitionNode); + if (functionSymbol.isEmpty() || functionSymbol.get().hashCode() != this.methodSymbol.hashCode()) { + return; + } + if (functionDefinitionNode.metadata().isPresent()) { + NodeList annotations = functionDefinitionNode.metadata().get().annotations(); + for (AnnotationNode annotation : annotations) { + Optional annotationSymbol = this.semanticModel.symbol(annotation); + if (annotationSymbol.isPresent() && annotationSymbol.get().getName().orElse("") + .equals(RESOURCE_CONFIG_ANNOTATION)) { + this.annotationNode = annotation; + } + } + } + } + + public Optional getAnnotationNode() { + if (this.annotationNode == null) { + return Optional.empty(); + } + return Optional.of(this.annotationNode); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/service/validator/ResourceConfigAnnotationFinder.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/service/validator/ResourceConfigAnnotationFinder.java new file mode 100644 index 000000000..77a501562 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/service/validator/ResourceConfigAnnotationFinder.java @@ -0,0 +1,51 @@ +package io.ballerina.stdlib.graphql.compiler.service.validator; + + +import io.ballerina.compiler.api.SemanticModel; +import io.ballerina.compiler.api.symbols.MethodSymbol; +import io.ballerina.compiler.syntax.tree.AnnotationNode; +import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.projects.DocumentId; +import io.ballerina.projects.Module; +import io.ballerina.projects.ModuleId; +import io.ballerina.projects.Project; +import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; + +import java.util.Collection; +import java.util.Optional; + +/** + * Find ResourceConfig AnnotationNode node from the syntax tree. + */ +public class ResourceConfigAnnotationFinder { + private final MethodSymbol methodSymbol; + private final SemanticModel semanticModel; + private final Project project; + private final ModuleId moduleId; + + public ResourceConfigAnnotationFinder(SyntaxNodeAnalysisContext context, MethodSymbol methodSymbol) { + this.semanticModel = context.semanticModel(); + this.methodSymbol = methodSymbol; + this.project = context.currentPackage().project(); + this.moduleId = context.moduleId(); + } + + public Optional find() { + return getAnnotationNodeFromModule(); + } + + private Optional getAnnotationNodeFromModule() { + Module currentModule = this.project.currentPackage().module(this.moduleId); + Collection documentIds = currentModule.documentIds(); + FunctionDefinitionNodeVisitor functionDefinitionNodeVisitor = new FunctionDefinitionNodeVisitor( + this.semanticModel, this.methodSymbol); + for (DocumentId documentId : documentIds) { + Node rootNode = currentModule.document(documentId).syntaxTree().rootNode(); + rootNode.accept(functionDefinitionNodeVisitor); + if (functionDefinitionNodeVisitor.getAnnotationNode().isPresent()) { + break; + } + } + return functionDefinitionNodeVisitor.getAnnotationNode(); + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/service/validator/ServiceValidator.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/service/validator/ServiceValidator.java index a5a016ae0..00affc68c 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/service/validator/ServiceValidator.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/service/validator/ServiceValidator.java @@ -44,9 +44,17 @@ import io.ballerina.compiler.api.symbols.resourcepath.PathSegmentList; import io.ballerina.compiler.api.symbols.resourcepath.ResourcePath; import io.ballerina.compiler.api.symbols.resourcepath.util.PathSegment; +import io.ballerina.compiler.syntax.tree.AnnotationNode; +import io.ballerina.compiler.syntax.tree.BasicLiteralNode; +import io.ballerina.compiler.syntax.tree.ExpressionNode; +import io.ballerina.compiler.syntax.tree.IdentifierToken; +import io.ballerina.compiler.syntax.tree.MappingConstructorExpressionNode; +import io.ballerina.compiler.syntax.tree.MappingFieldNode; import io.ballerina.compiler.syntax.tree.Node; +import io.ballerina.compiler.syntax.tree.NodeList; import io.ballerina.compiler.syntax.tree.ObjectConstructorExpressionNode; import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; +import io.ballerina.compiler.syntax.tree.SpecificFieldNode; import io.ballerina.compiler.syntax.tree.SyntaxKind; import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; import io.ballerina.stdlib.graphql.commons.types.Schema; @@ -61,11 +69,15 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import static io.ballerina.stdlib.graphql.compiler.Utils.getAccessor; import static io.ballerina.stdlib.graphql.compiler.Utils.getEffectiveType; import static io.ballerina.stdlib.graphql.compiler.Utils.getEffectiveTypes; +import static io.ballerina.stdlib.graphql.compiler.Utils.hasResourceConfigAnnotation; +import static io.ballerina.stdlib.graphql.compiler.Utils.isContextParameter; import static io.ballerina.stdlib.graphql.compiler.Utils.isDistinctServiceClass; import static io.ballerina.stdlib.graphql.compiler.Utils.isDistinctServiceReference; import static io.ballerina.stdlib.graphql.compiler.Utils.isFileUploadParameter; @@ -86,9 +98,6 @@ * Validate functions in Ballerina GraphQL services. */ public class ServiceValidator { - private static final String FIELD_PATH_SEPARATOR = "."; - private static final String ID_ANNOT_NAME = "ID"; - private static final String[] allowedIDTypes = {"int", "string", "float", "decimal", "uuid:UUID"}; private final Set visitedClassesAndObjectTypeDefinitions = new HashSet<>(); private final List existingInputObjectTypes = new ArrayList<>(); private final List existingReturnTypes = new ArrayList<>(); @@ -100,9 +109,12 @@ public class ServiceValidator { private boolean hasQueryType; private final boolean isSubgraph; private TypeSymbol rootInputParameterTypeSymbol; - private final List currentFieldPath; + private static final String FIELD_PATH_SEPARATOR = "."; + private static final String PREFETCH_METHOD_PREFIX = "pre"; + private static final String PREFETCH_METHOD_NAME_CONFIG = "prefetchMethodName"; + public ServiceValidator(SyntaxNodeAnalysisContext context, Node serviceNode, InterfaceEntityFinder interfaceEntityFinder, boolean isSubgraph) { this.context = context; @@ -123,12 +135,11 @@ public void validate() { } private void validateServiceObject() { - ObjectConstructorExpressionNode objectConstructorExpressionNode = (ObjectConstructorExpressionNode) serviceNode; - for (Node node : objectConstructorExpressionNode.members()) { - validateServiceMember(node); - } + ObjectConstructorExpressionNode objectConstructorExpNode = (ObjectConstructorExpressionNode) serviceNode; + List serviceMethodNodes = getServiceMethodNodes(objectConstructorExpNode.members()); + validateRootServiceMethods(serviceMethodNodes, objectConstructorExpNode.location()); if (!this.hasQueryType) { - addDiagnostic(CompilationDiagnostic.MISSING_RESOURCE_FUNCTIONS, objectConstructorExpressionNode.location()); + addDiagnostic(CompilationDiagnostic.MISSING_RESOURCE_FUNCTIONS, objectConstructorExpNode.location()); } validateEntitiesResolverReturnTypes(); } @@ -151,32 +162,151 @@ public boolean isErrorOccurred() { private void validateService() { ServiceDeclarationNode serviceDeclarationNode = (ServiceDeclarationNode) this.context.node(); - for (Node node : serviceDeclarationNode.members()) { - validateServiceMember(node); - } + List serviceMethodNodes = getServiceMethodNodes(serviceDeclarationNode.members()); + validateRootServiceMethods(serviceMethodNodes, serviceDeclarationNode.location()); if (!this.hasQueryType) { addDiagnostic(CompilationDiagnostic.MISSING_RESOURCE_FUNCTIONS, serviceDeclarationNode.location()); } validateEntitiesResolverReturnTypes(); } - private void validateServiceMember(Node node) { + private List getServiceMethodNodes(NodeList serviceMembers) { + return serviceMembers.stream().filter(this::isServiceMethod).collect(Collectors.toList()); + } + + private boolean isServiceMethod(Node node) { if (this.context.semanticModel().symbol(node).isEmpty()) { - return; + return false; } Symbol symbol = this.context.semanticModel().symbol(node).get(); - Location location = node.location(); - if (symbol.kind() == SymbolKind.METHOD) { - MethodSymbol methodSymbol = (MethodSymbol) symbol; + return symbol.kind() == SymbolKind.RESOURCE_METHOD || symbol.kind() == SymbolKind.METHOD; + } + + private void validateRootServiceMethods(List serviceMethods, Location location) { + List methodSymbols = getMethodSymbols(serviceMethods); + for (Node methodNode : serviceMethods) { + // No need to check fo isEmpty(), already validated in getRemoteOrResourceMethodSymbols + // noinspection OptionalGetWithoutIsPresent + MethodSymbol methodSymbol = (MethodSymbol) this.context.semanticModel().symbol(methodNode).get(); + Location methodLocation = methodNode.location(); + if (isRemoteMethod(methodSymbol)) { this.currentFieldPath.add(TypeName.MUTATION.getName()); - validateRemoteMethod(methodSymbol, location); + validateRemoteMethod(methodSymbol, methodLocation); this.currentFieldPath.remove(TypeName.MUTATION.getName()); + validatePrefetchMethodMapping(methodSymbol, methodSymbols, location); + } else if (isResourceMethod(methodSymbol)) { + validateRootServiceResourceMethod((ResourceMethodSymbol) methodSymbol, methodLocation); + validatePrefetchMethodMapping(methodSymbol, methodSymbols, location); + } + } + } + + private void validatePrefetchMethodMapping(MethodSymbol methodSymbol, List serviceMethods, + Location location) { + String graphqlFieldName = getGraphqlFieldName(methodSymbol); + String prefetchMethodName = getDefaultPrefetchMethodName(graphqlFieldName); + Location methodLocation = getLocation(methodSymbol, location); + boolean hasPrefetchMethodConfig = false; + + if (hasResourceConfigAnnotation(methodSymbol)) { + ResourceConfigAnnotationFinder resourceConfigAnnotationFinder = new ResourceConfigAnnotationFinder( + this.context, methodSymbol); + Optional annotation = resourceConfigAnnotationFinder.find(); + hasPrefetchMethodConfig = annotation.isPresent() && hasPrefetchMethodNameConfig(annotation.get()); + if (hasPrefetchMethodConfig) { + prefetchMethodName = getPrefetchMethodName(annotation.get()); + if (prefetchMethodName == null) { + addDiagnostic(CompilationDiagnostic.UNABLE_TO_VALIDATE_PREFETCH_METHOD, annotation.get().location(), + PREFETCH_METHOD_NAME_CONFIG, graphqlFieldName); + return; + } + if (isSubscription(methodSymbol)) { + addDiagnostic(CompilationDiagnostic.INVALID_USAGE_OF_PREFETCH_METHOD_NAME_CONFIG, + annotation.get().location(), PREFETCH_METHOD_NAME_CONFIG, graphqlFieldName); + return; + } + } + } + if (isSubscription(methodSymbol)) { + return; + } + MethodSymbol prefetchMethod = findPrefetchMethod(prefetchMethodName, serviceMethods); + if (prefetchMethod == null) { + if (hasPrefetchMethodConfig) { + addDiagnostic(CompilationDiagnostic.UNABLE_TO_FIND_PREFETCH_METHOD, methodLocation, prefetchMethodName, + graphqlFieldName); + } + return; + } + validatePrefetchMethodSignature(prefetchMethod, methodSymbol, location); + } + + private boolean isSubscription(MethodSymbol methodSymbol) { + return isResourceMethod(methodSymbol) && RESOURCE_FUNCTION_SUBSCRIBE.equals( + getAccessor((ResourceMethodSymbol) methodSymbol)); + } + + private String getGraphqlFieldName(MethodSymbol methodSymbol) { + return isResourceMethod(methodSymbol) ? getFieldPath((ResourceMethodSymbol) methodSymbol) : + methodSymbol.getName().orElse(""); + } + + private String getPrefetchMethodName(AnnotationNode annotation) { + // noinspection OptionalGetWithoutIsPresent + MappingConstructorExpressionNode mappingConstructorExpressionNode = annotation.annotValue().get(); + for (MappingFieldNode field : mappingConstructorExpressionNode.fields()) { + if (field.kind() == SyntaxKind.SPECIFIC_FIELD) { + SpecificFieldNode specificFieldNode = (SpecificFieldNode) field; + Node fieldName = specificFieldNode.fieldName(); + if (fieldName.kind() == SyntaxKind.IDENTIFIER_TOKEN) { + IdentifierToken identifierToken = (IdentifierToken) fieldName; + String identifierName = identifierToken.text(); + if (PREFETCH_METHOD_NAME_CONFIG.equals(identifierName)) { + return getStringValue(specificFieldNode); + } + } } - } else if (symbol.kind() == SymbolKind.RESOURCE_METHOD) { - ResourceMethodSymbol resourceMethodSymbol = (ResourceMethodSymbol) symbol; - validateRootServiceResourceMethod(resourceMethodSymbol, location); } + return null; + } + + private String getStringValue(SpecificFieldNode specificFieldNode) { + if (specificFieldNode.valueExpr().isEmpty()) { + return null; + } + ExpressionNode valueExpression = specificFieldNode.valueExpr().get(); + if (valueExpression.kind() == SyntaxKind.STRING_LITERAL) { + BasicLiteralNode stringLiteralNode = (BasicLiteralNode) valueExpression; + String stringLiteral = stringLiteralNode.toSourceCode().trim(); + return stringLiteral.substring(1, stringLiteral.length() - 1); + } + return null; + } + + private boolean hasPrefetchMethodNameConfig(AnnotationNode annotation) { + if (annotation.annotValue().isEmpty()) { + return false; + } + MappingConstructorExpressionNode mappingConstructorExpressionNode = annotation.annotValue().get(); + for (MappingFieldNode field : mappingConstructorExpressionNode.fields()) { + if (field.kind() == SyntaxKind.SPECIFIC_FIELD) { + SpecificFieldNode specificFieldNode = (SpecificFieldNode) field; + Node fieldName = specificFieldNode.fieldName(); + if (fieldName.kind() == SyntaxKind.IDENTIFIER_TOKEN) { + IdentifierToken identifierToken = (IdentifierToken) fieldName; + String identifierName = identifierToken.text(); + return PREFETCH_METHOD_NAME_CONFIG.equals(identifierName); + } + } + } + return false; + } + + private MethodSymbol findPrefetchMethod(String prefetchMethodName, List serviceMethods) { + return serviceMethods.stream() + .filter(method -> method.kind() == SymbolKind.METHOD && !isRemoteMethod(method) && method.getName() + .orElse("").equals(prefetchMethodName)).findFirst().orElse(null); } private void validateEntitiesResolverReturnTypes() { @@ -226,6 +356,12 @@ private void validateRootServiceResourceMethod(ResourceMethodSymbol methodSymbol } } + private List getMethodSymbols(List serviceMembers) { + return serviceMembers.stream().filter(this::isServiceMethod) + .map(methodNode -> (MethodSymbol) this.context.semanticModel().symbol(methodNode).get()) + .collect(Collectors.toList()); + } + private void validateResourceMethod(ResourceMethodSymbol methodSymbol, Location location) { String accessor = getAccessor(methodSymbol); if (!RESOURCE_FUNCTION_GET.equals(accessor)) { @@ -233,7 +369,7 @@ private void validateResourceMethod(ResourceMethodSymbol methodSymbol, Location addDiagnostic(CompilationDiagnostic.INVALID_RESOURCE_FUNCTION_ACCESSOR, accessorLocation, accessor, getFieldPath(methodSymbol)); } - validateGetResource(methodSymbol, location); + validateGetResource(methodSymbol, getLocation(methodSymbol, location)); } private void validateGetResource(ResourceMethodSymbol methodSymbol, Location location) { @@ -264,6 +400,50 @@ private void validateSubscribeResource(ResourceMethodSymbol methodSymbol, Locati validateInputParameters(methodSymbol, location); } + private void validatePrefetchMethodSignature(MethodSymbol prefetchMethod, MethodSymbol resolverMethod, + Location location) { + Location prefetchMethodLocation = getLocation(prefetchMethod, location); + validatePrefetchMethodParams(prefetchMethod, prefetchMethodLocation, resolverMethod); + validatePrefetchMethodReturnType(prefetchMethod, prefetchMethodLocation); + } + + private void validatePrefetchMethodParams(MethodSymbol prefetchMethod, Location prefetchMethodLocation, + MethodSymbol resolverMethod) { + String prefetchMethodName = prefetchMethod.getName().orElse(""); + Set fieldMethodParamSignatures = resolverMethod.typeDescriptor().params().isPresent() ? + resolverMethod.typeDescriptor().params().get().stream().map(ParameterSymbol::signature) + .map(String::trim).collect(Collectors.toSet()) : new HashSet<>(); + List parameterSymbols = prefetchMethod.typeDescriptor().params().isPresent() ? + prefetchMethod.typeDescriptor().params().get() : new ArrayList<>(); + boolean hasContextParam = false; + for (ParameterSymbol symbol : parameterSymbols) { + if (isContextParameter(symbol.typeDescriptor())) { + hasContextParam = true; + } else if (!fieldMethodParamSignatures.contains(symbol.signature().trim())) { + addDiagnostic(CompilationDiagnostic.INVALID_PARAMETER_IN_PREFETCH_METHOD, prefetchMethodLocation, + symbol.signature(), prefetchMethodName, + isResourceMethod(resolverMethod) ? getFieldPath((ResourceMethodSymbol) resolverMethod) : + resolverMethod.getName().orElse(resolverMethod.signature())); + } + } + if (!hasContextParam) { + addDiagnostic(CompilationDiagnostic.MISSING_GRAPHQL_CONTEXT_PARAMETER, prefetchMethodLocation, + prefetchMethod.getName().orElse(prefetchMethod.signature())); + } + } + + private void validatePrefetchMethodReturnType(MethodSymbol prefetchMethod, Location prefetchMethodLocation) { + if (prefetchMethod.typeDescriptor().returnTypeDescriptor().isPresent()) { + TypeSymbol returnType = prefetchMethod.typeDescriptor().returnTypeDescriptor().get(); + if (returnType.typeKind() == TypeDescKind.NIL) { + return; + } + addDiagnostic(CompilationDiagnostic.INVALID_RETURN_TYPE_IN_PREFETCH_METHOD, + getLocation(returnType, prefetchMethodLocation), returnType.signature(), + prefetchMethod.getName().orElse("")); + } + } + private void validateSubscriptionMethod(ResourceMethodSymbol methodSymbol, Location location) { if (methodSymbol.typeDescriptor().returnTypeDescriptor().isEmpty()) { return; @@ -502,17 +682,19 @@ private void validateInterfaceObjectTypeDefinition(TypeDefinitionSymbol typeDefi String typeName = typeDefinitionSymbol.getName().orElse("$anonymous"); addDiagnostic(CompilationDiagnostic.NON_DISTINCT_INTERFACE, location, typeName); } - for (MethodSymbol methodSymbol : objectTypeSymbol.methods().values()) { - Location methodLocation = getLocation(methodSymbol, location); + List methodSymbols = new ArrayList<>(objectTypeSymbol.methods().values()); + for (MethodSymbol methodSymbol : methodSymbols) { if (methodSymbol.kind() == SymbolKind.RESOURCE_METHOD) { resourceMethodFound = true; - validateResourceMethod((ResourceMethodSymbol) methodSymbol, methodLocation); + validateResourceMethod((ResourceMethodSymbol) methodSymbol, location); } else if (isRemoteMethod(methodSymbol)) { // noinspection OptionalGetWithoutIsPresent String interfaceName = typeDefinitionSymbol.getName().get(); String remoteMethodName = methodSymbol.getName().orElse(methodSymbol.signature()); - addDiagnostic(CompilationDiagnostic.INVALID_FUNCTION, methodLocation, interfaceName, remoteMethodName); + addDiagnostic(CompilationDiagnostic.INVALID_FUNCTION, getLocation(methodSymbol, location), + interfaceName, remoteMethodName); } + validatePrefetchMethodMapping(methodSymbol, methodSymbols, location); } if (!resourceMethodFound) { addDiagnostic(CompilationDiagnostic.MISSING_RESOURCE_FUNCTIONS, location); @@ -567,12 +749,25 @@ private void validateInputParameters(MethodSymbol methodSymbol, Location locatio } if (parameter.annotations().isEmpty()) { validateInputParameterType(parameter.typeDescriptor(), inputLocation, - isResourceMethod(methodSymbol)); + isResourceMethod(methodSymbol)); } } } } + private String getDefaultPrefetchMethodName(String graphqlFieldName) { + return PREFETCH_METHOD_PREFIX + uppercaseFirstChar(graphqlFieldName); + } + + private String uppercaseFirstChar(String string) { + if (string == null || string.length() == 0) { + return string; + } + char[] chars = string.toCharArray(); + chars[0] = Character.toUpperCase(chars[0]); + return new String(chars); + } + private void validateInputParameterType(TypeSymbol typeSymbol, Location location, boolean isResourceMethod) { if (isFileUploadParameter(typeSymbol)) { String methodName = currentFieldPath.get(currentFieldPath.size() - 1); @@ -746,16 +941,17 @@ private void validateServiceClassDefinition(ClassSymbol classSymbol, Location lo getCurrentFieldPath(), className); } boolean resourceMethodFound = false; - for (MethodSymbol methodSymbol : classSymbol.methods().values()) { - Location methodLocation = getLocation(methodSymbol, location); + List methodSymbols = new ArrayList<>(classSymbol.methods().values()); + for (MethodSymbol methodSymbol : methodSymbols) { if (methodSymbol.kind() == SymbolKind.RESOURCE_METHOD) { resourceMethodFound = true; - validateResourceMethod((ResourceMethodSymbol) methodSymbol, methodLocation); + validateResourceMethod((ResourceMethodSymbol) methodSymbol, location); } else if (isRemoteMethod(methodSymbol)) { // noinspection OptionalGetWithoutIsPresent - addDiagnostic(CompilationDiagnostic.INVALID_FUNCTION, methodLocation, className, + addDiagnostic(CompilationDiagnostic.INVALID_FUNCTION, getLocation(methodSymbol, location), className, methodSymbol.getName().get()); } + validatePrefetchMethodMapping(methodSymbol, methodSymbols, location); } if (!resourceMethodFound) { addDiagnostic(CompilationDiagnostic.MISSING_RESOURCE_FUNCTIONS, location); diff --git a/docs/proposals/graphql-dataloader.md b/docs/proposals/graphql-dataloader.md new file mode 100644 index 000000000..a8d46600e --- /dev/null +++ b/docs/proposals/graphql-dataloader.md @@ -0,0 +1,310 @@ +# Proposal: Introduce GraphQL Interceptors + +_Owners_: @MohamedSabthar @ThisaruGuruge +_Reviewers_: @shafreenAnfar @ThisaruGuruge +_Created_: 2023/06/14 +_Updated_: 2023/06/14 +_Issue_: [#4569](https://github.com/ballerina-platform/ballerina-standard-library/issues/4569) + +## Summary +DataLoader is a versatile tool used for accessing various remote data sources in GraphQL. Within the realm of GraphQL, DataLoader is extensively employed to address the N+1 problem. The aim of this proposal is to incorporate a DataLoader functionality into the Ballerina GraphQL package. + +## Goals +- Implement DataLoader as a sub-module of the Ballerina GraphQL package. + +## Motivation + +### The N+1 problem +The N+1 problem can be exemplified in a scenario involving authors and their books. Imagine a book catalog application that displays a list of authors and their respective books. When encountering the N+1 problem, retrieving the list of authors requires an initial query to fetch author information (N), followed by separate queries for each author to retrieve their books (1 query per author). + +This results in N+1 queries being executed, where N represents the number of authors, leading to increased overhead and potential performance issues. Following is a GraphQL book catalog application written in Ballerina which susceptible to N +1 problem + +```ballerina +import ballerina/graphql; +import ballerina/sql; +import ballerina/io; +import ballerinax/java.jdbc; +import ballerinax/mysql.driver as _; + +service on new graphql:Listener(9090) { + resource function get authors() returns Author[]|error { + var query = sql:queryConcat(`SELECT * FROM authors`); + io:println(query); + stream authorStream = dbClient->query(query); + return from AuthorRow authorRow in authorStream + select new (authorRow); + } +} + +isolated distinct service class Author { + private final readonly & AuthorRow author; + + isolated function init(AuthorRow author) { + self.author = author.cloneReadOnly(); + } + + isolated resource function get name() returns string { + return self.author.name; + } + + isolated resource function get books() returns Book[]|error { + int authorId = self.author.id; + var query = sql:queryConcat(`SELECT * FROM books WHERE author = ${authorId}`); + io:println(query); + stream bookStream = dbClient->query(query); + return from BookRow bookRow in bookStream + select new Book(bookRow); + } +} + +isolated distinct service class Book { + private final readonly & BookRow book; + + isolated function init(BookRow book) { + self.book = book.cloneReadOnly(); + } + + isolated resource function get id() returns int { + return self.book.id; + } + + isolated resource function get title() returns string { + return self.book.title; + } +} + +final jdbc:Client dbClient = check new ("jdbc:mysql://localhost:3306/mydatabase", "root", "password"); + +public type AuthorRow record { + int id; + string name; +}; + +public type BookRow record { + int id; + string title; +}; +``` +Executing the query +```graphql +{ + authors { + name + books { + title + } + } +} +``` +on the above service will print the following SQL queries in the terminal +``` +SELECT * FROM authors +SELECT * FROM books WHERE author = 10 +SELECT * FROM books WHERE author = 9 +SELECT * FROM books WHERE author = 8 +SELECT * FROM books WHERE author = 7 +SELECT * FROM books WHERE author = 6 +SELECT * FROM books WHERE author = 5 +SELECT * FROM books WHERE author = 4 +SELECT * FROM books WHERE author = 3 +SELECT * FROM books WHERE author = 2 +SELECT * FROM books WHERE author = 1 +``` +where the first query returns 10 authors then for each author a separate query is executed to obtain the book details resulting in a total of 11 queries which leads to inefficient database querying. The DataLoader allows us to overcome this problem. + +### DataLoader +The DataLoader is the solution found by the original developers of the GraphQL spec. The primary purpose of DataLoader is to optimize data fetching and mitigate performance issues, especially the N+1 problem commonly encountered in GraphQL APIs. It achieves this by batching and caching data requests, reducing the number of queries sent to the underlying data sources. DataLoader helps minimize unnecessary overhead and improves the overall efficiency and response time of data retrieval operations. + +## Success Metrics +In almost all GraphQL implementations, the DataLoader is a major requirement. Since the Ballerina GraphQL package is now spec-compliant, we are looking for ways to improve the user experience in the Ballerina GraphQL package. Implementing a DataLoader in Ballerina will improve the user experience drastically. + + +## Description +The DataLoader batches and caches operations for data fetchers from different data sources.The DataLoader requires users to provide a batch function that accepts an array of keys as input and retrieves the corresponding array of values for those keys. + +### API +#### DataLoader object +This object defines the public APIs accessible to users. +```ballerina +public type DataLoader isolated object { + # Collects a key to perform a batch operation at a later time. + pubic isolated function load(anydata key); + + # Retrieves the result for a particular key. + public isolated function get(anydata key, typedesc t = <>) returns t|error; + + # Executes the user-defined batch function. + public isolated function dispatch(); +}; +``` + +#### DefaultDataLoader class +This class provides a default implementation for the DataLoader +```ballerina +isolated class DefaultDataLoader { + *DataLoader; + + private final table key(key) keys = table []; + private table key(key) resultTable = table []; + private final (isolated function (readonly & anydata[] keys) returns anydata[]|error) batchLoadFunction; + + public isolated function init(isolated function (readonly & anydata[] keys) returns anydata[]|error batchLoadFunction) { + self.batchLoadFunction = batchLoadFunction; + } + + // … implementations of load, get and dispatch methods +} + +type Result record {| + readonly anydata key; + anydata|error value; +|}; +``` +The DefaultDataLoader class is an implementation of the DataLoader with the following characteristics: + +- Inherits from DataLoader. +- Maintains a key table to collect keys for batch execution. +- Stores/caches results in a resultTable. +- Requires an isolated function `batchLoadFunction` to be provided during initialization. + +##### `init` method + +The `init` method instantiates the DefaultDataLoader and accepts a `batchLoadFunction` function pointer as a parameter. The `batchLoadFunction` function pointer has the following type: +```ballerina +isolated function (readonly & anydata[] keys) returns anydata[]|error +``` +Users are expected to define the logic for the `batchLoadFunction`, which handles the batching of operations. The `batchLoadFunction` should return an array of anydata where each element corresponds to a key in the input `keys` array upon successful execution. + +##### `load` method + +The `load` method takes an `anydata` key parameter and adds it to the key table for batch execution. If a result is already cached for the given key in the result table, the key will not be added to the key table again. + +##### `get` method + +The `get` method takes an `anydata` key as a parameter and retrieves the associated value by looking up the result in the result table. If a result is found for the given key, this method attempts to perform data binding and returns the result. If a result cannot be found or data binding fails, an error is returned. + +##### `dispatch` method + +The `dispatch` method invokes the user-defined `batchLoadFunction`. It passes the collected keys as an input array to the `batchLoadFunction`, retrieves the result array, and stores the key-to-value mapping in the resultTable. + +#### Requirements to Engaging DataLoader in GraphQL Module + +To integrate the DataLoader with the GraphQL module, users need to follow these three steps: +1. Identify the resource method (GraphQL field) that requires the use of the DataLoader. Then, add a new parameter `map` to its parameter list. +2. Define a matching remote/resource method called loadXXX, where XXX represents the Pascal-cased name of the GraphQL field identified in the previous step. This method may include all/some of the required parameters from the graphql field and the `map` parameter. This function is executed as a prefetch step before executing the corresponding resource method of GraphQL field. (Note that both the loadXXX method and the XXX method should have same resource accessor or should be remote methods) +3. Annotate the loadXXX method written in step two with `@dataloader:Loader` annotation and pass the required configuration. This annotation helps avoid adding loadXXX as a field in the GraphQL schema and also provides DataLoader configuration. + +#### `Loader` annotation +```ballerina +# Provides a set of configurations for the load resource method. +public type LoaderConfig record {| + # Facilitates a connection between a data loader key and a batch function. + # The data loader key enables the reuse of the same data loader across resolvers + map batchFunctions; +|}; + +# The annotation to configure the load resource method with a DataLoader +public annotation LoaderConfig Loader on object function; +``` +The following section demonstrates the usage of DataLoader in Ballerina GraphQL. + +##### Modifying the Book Catalog Application to Use DataLoader +In the previous Book Catalog Application example `SELECT * FROM books WHERE author = ${authorId}` was executed each time for N = 10 authors. To batch these database calls to a single request we need to use a DataLoader at the books field. The following code block demonstrates the changes made to the books field and Author service class. + +```ballerina +import ballerina/graphql.dataloader; + +isolated distinct service class Author { + private final readonly & AuthorRow author; + + isolated function init(AuthorRow author) { + self.author = author.cloneReadOnly(); + } + + isolated resource function get name() returns string { + return self.author.name; + } + + // 1. Add a map parameter to it’s parameter list + isolated resource function get books(map loaders) returns Book[]|error { + dataloader:DataLoader bookLoader = loaders.get("bookLoader"); + BookRow[] bookrows = check bookLoader.get(self.author.id); // get the value from DataLoader for the key + return from BookRow bookRow in bookrows + select new Book(bookRow); + } + + // 3. add dataloader:Loader annotation to the loadXXX method. + @dataloader:Loader { + batchFunctions: {"bookLoader": bookLoaderFunction} + } + // 2. create a loadXXX method + isolated resource function get loadBooks(map loaders) { + dataloader:DataLoader bookLoader = loaders.get("bookLoader"); + bookLoader.load(self.author.id); // pass the key so it can be collected and batched later + } + +} + +// User written code to batch the books +isolated function bookLoaderFunction(readonly & anydata[] ids) returns BookRow[][]|error { + readonly & int[] keys = ids; + var query = sql:queryConcat(`SELECT * FROM books WHERE author IN (`, sql:arrayFlattenQuery(keys), `)`); + io:println(query); + stream bookStream = dbClient->query(query); + map authorsBooks = {}; + checkpanic from BookRow bookRow in bookStream + do { + string key = bookRow.author.toString(); + if !authorsBooks.hasKey(key) { + authorsBooks[key] = []; + } + authorsBooks.get(key).push(bookRow); + }; + final readonly & map clonedMap = authorsBooks.cloneReadOnly(); + return keys.'map(key => clonedMap[key.toString()] ?: []); +}; +``` + +executing the following query +```graphql +{ + authors { + name + books { + title + } + } +``` +after incorporating DataLoader will now include only two database queries. +``` +SELECT * FROM authors +SELECT * FROM books WHERE author IN (1,2,3,4,5,6,7,8,9,10) +``` + +### Engaging DataLoader with GraphQL Engine +At a high level the GraphQL Engine breaks the query into subproblems and then constructs the value for the query by solving the subproblems as shown in the below diagram. +![image](https://github.com/ballerina-platform/ballerina-standard-library/assets/43032716/531bfb6b-4aab-4a44-aca9-4e4372be8cb7) +Following algorithm demonstrates how the GraphQL engine engages the DataLoader at a high level. +1. The GraphQL engine searches for the associated resource/remote function for each field in the query. +2. If a matching resource/remote function with the pattern `loadXXX` (where `XXX` is the field name) is found, the engine: + - Creates a map of DataLoader instances using the provided batch loader functions in the `@dataloader:Loader` annotation. + - Makes this map of DataLoader instances available for both the `XXX` and `loadXXX` functions. + - Executes the `loadXXX` resource method and generates a placeholder value for that field. +3. If no matching `loadXXX` function is found, the engine executes the corresponding `XXX` resource function for that field. +4. After completing the above steps, the engine generates a partial value tree with placeholders. +5. The engine then executes the `dispatch()` function of all the created DataLoaders. +6. For each non-resolved field (placeholder) in the partial value tree: + - Executes the corresponding resource function (`XXX`). + - Obtains the resolved value and replaces the placeholder with the resolved value. + - If the resolved value is still a non-resolved field (placeholder), the process repeats steps 1-7. +7. Finally, the fully constructed value tree is returned. + +## Future Plans +The DataLoader object will be enhanced with the following public methods: + +- loadMany: This method allows adding multiple keys to the key table for future data loading. +- getMany: Given an array of keys, this method retrieves the corresponding values from the DataLoader's result table, returning them as an array of anydata values or an error, if applicable. The return type is (anydata | error)[]. +- clear: This method takes a key as a parameter and removes the corresponding result from the result table, effectively clearing the cached data for that key. +- clearAll: This method removes all cached results from the result table, providing a way to clear the entire cache in one operation. +- prime: This method takes a key and an anydata value as arguments and replaces or stores the key-value mapping in the result table. This allows preloading or priming specific data into the cache for efficient retrieval later. +These methods enhance the functionality of the DataLoader, providing more flexibility and control over data loading, caching, and result management. \ No newline at end of file diff --git a/docs/spec/spec.md b/docs/spec/spec.md index bfb5fbb0d..691fa790f 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -3,7 +3,7 @@ _Owners_: @shafreenAnfar @DimuthuMadushan @ThisaruGuruge @MohamedSabthar \ _Reviewers_: @shafreenAnfar @ThisaruGuruge @DimuthuMadushan @ldclakmal \ _Created_: 2022/01/06 \ -_Updated_: 2023/07/12 \ +_Updated_: 2023/08/03 \ _Edition_: Swan Lake \ _GraphQL Specification_: [October 2021](https://spec.graphql.org/October2021/) @@ -106,8 +106,10 @@ The conforming implementation of the specification is released and included in t * 7.1.8 [Constraint Configurations](#718-constraint-configurations) * 7.2 [Resource Configuration](#72-resource-configuration) * 7.2.1 [Field Interceptors](#721-field-interceptors) + * 7.2.2 [Prefetch Method Name Configuration](#722-prefetch-method-name-configuration) * 7.3 [Interceptor Configuration](#73-interceptor-configuration) * 7.3.1 [Scope Configuration](#731-scope-configuration) + * 7.4 [ID Annotation](#74-id-annotation) 8. [Security](#8-security) * 8.1 [Service Authentication and Authorization](#81-service-authentication-and-authorization) * 8.1.1 [Declarative Approach](#811-declarative-approach) @@ -144,6 +146,8 @@ The conforming implementation of the specification is released and included in t * 10.1.1.1 [Set Attribute in Context](#10111-set-attribute-in-context) * 10.1.1.2 [Get Context Attribute](#10112-get-attribute-from-context) * 10.1.1.3 [Remove Attribute from Context](#10113-remove-attribute-from-context) + * 10.1.1.4 [Register DataLoader in Context](#10114-register-dataloader-in-context) + * 10.1.1.5 [Get DataLoader from Context](#10115-get-dataloader-from-context) * 10.1.2 [Accessing the Context](#1012-accessing-the-context-object) * 10.1.3 [Resolving Field Value](#1013-resolving-field-value) * 10.2 [Field Object](#102-field-object) @@ -176,6 +180,20 @@ The conforming implementation of the specification is released and included in t * 10.5.1.1 [The `@subgraph:Subgraph` Annotation](#10511-the-subgraphsubgraph-annotation) * 10.5.1.2 [The `@subgraph:Entity` Annotation](#10512-the-subgraphentity-annotation) * 10.5.1.3 [The `subgraph:ReferenceResolver` Function Type](#10513-the-subgraphreferenceresolver-function-type) +11. [Experimental Features](#11-experimental-features) + * 11.1 [DataLoader](#111-dataloader) + * 11.1.1 [DataLoader API](#1111-dataloader-api) + * 11.1.1.1 [The `load` method](#11111-the-load-method) + * 11.1.1.2 [The `get` method](#11112-the-get-method) + * 11.1.1.3 [The `dispatch` method](#11113-the-dispatch-method) + * 11.1.1.4 [The `clearAll` method](#11114-the-clearall-method) + * 11.1.2 [The DefaultDataLoader](#1112-the-defaultdataloader) + * 11.1.2.1 [The `init` Method](#11121-the-init-method) + * 11.1.2.1.1 [The BatchLoadFunction](#111211-the-batchloadfunction) + * 11.1.3. [Engaging DataLoaders](#1113-engaging-dataloaders) + * 11.1.3.1 [Import `graphql.dataloader` Submodule](#11131-import-graphqldataloader-submodule) + * 11.1.3.2 [Register DataLoaders to Context via ContextInit Function](#11132-register-dataloaders-to-context-via-contextinit-function) + * 11.1.3.3 [Define the Corresponding `prefetch` Method](#11133-define-the-corresponding-prefetch-method) ## 1. Overview @@ -1767,6 +1785,28 @@ service on new graphql:Listener(9090) { } ``` +#### 7.2.2 Prefetch Method Name Configuration + +The `prefetchMethodName` field is used override the default prefetch method name. To know more about the prefetch method, refer to the [Define the Corresponding `prefetch` Method](#11123-define-the-corresponding-prefetch-method) section. + +###### Example: Override Prefetch Method Name + +```ballerina +service on new graphql:Listener(9090) { + + function loadBooks(graphql:Context ctx) { + // ... + } + + @graphql:ResourceConfig { + prefetchMethodName: "loadBooks" + } + resource function get books(graphql:Context ctx) returns Book[] { + // ... + } +} +``` + ### 7.3 Interceptor Configuration The configurations stated in the `graphql:InterceptorConfig`, are used to change the behavior of a particular GraphQL interceptor. @@ -2616,6 +2656,35 @@ graphql:Error? result = context.remove("key"); >**Note:** Even though the functionalities are provided to update/remove attributes in the context, it is discouraged to do such operations. The reason is that destructive modifications may cause issues in parallel executions of the Query operations. +##### 10.1.1.4 Register DataLoader in Context + +To register a [DataLoader](#111-dataloader) in the `graphql:Context` object, you can use the `registerDataLoader()` method, which requires two parameters. + +- `key`: The key used to identify a specific DataLoader instance. This key can later be used to retrieve the DataLoader instance when needed. The `key` must be a `string`. +- `dataloader`: The DataLoader instance. + +###### Example: Register DataLoader in Context + +```ballerina +graphql:Context context = new; + +context.registerDataLoader("authorLoader", new dataloader:DefaultDataLoader(authorBatchFunction)); +``` + +##### 10.1.1.5 Get DataLoader from Context + +To obtain a DataLoader from the `graphql:Context` object, you can use the `getDataLoader()` method, which takes one parameter. + +- `key`: This is the key of the DataLoader instance that needs to be retrieved. + +If the specified key does not exist in the context, the `getDataLoader()` method will raise a panic. + +###### Example: Get DataLoader from Context + +```ballerina +dataloader:DataLoader authorLoader = context.getDataLoader("authorLoader"); +``` + #### 10.1.2 Accessing the Context Object The `graphql:Context` can be accessed inside any resolver. When needed, the `graphql:Context` should be added as a parameter of the `resource` or `remote` method representing a GraphQL field. @@ -3274,3 +3343,293 @@ type Product record { ``` >**Note:** If the reference resolver returns an entity of a different type than the entity being resolved, a runtime error will be returned to the router. For example, if the resolver returns a `User` for a `Product` entity, a runtime error will occur. + +## 11. Experimental Features + +This section includes the experimental features in the Ballerina GraphQL package. There _might be_ backward-incompatible changes to these features in future releases. Once a feature is stabilized, it will be graduated as a standard feature. + +### 11.1 DataLoader + +The Ballerina GraphQL module allows efficient batching of data retrieval from datasources and enables caching of fetched data using the `graphql.dataloader` submodule. + +#### 11.1.1 DataLoader API + +The `graphql.dataloader` submodule provides the `DataLoader` object, which is used to batch and cache data requests from a data source. The `DataLoader` object type has the following public methods/APIs. + +##### 11.1.1.1 The `add` method + +This method takes an `anydata` parameter called `key`, which is used to identify the data to be loaded. This method collects and stores the `key` to dispatch a batch operation at a later time. It does not return any values. The following is the method definition of this method. + +```ballerina +public isolated function add(anydata key); +``` + +##### 11.1.1.2 The `get` method + +This method takes a `key` parameter and retrieves the result for the provided `key`. It performs data binding by examining the type of the assigned variable. In case of failure to retrieve the result or perform data binding, the method returns an error. The following is the method definition of this method. + +```ballerina +public isolated function get(anydata key, typedesc 'type = <>) returns 'type|error; +``` + +##### 11.1.1.3 The `dispatch` method + +This method does not take any parameters and does not return any values. This method is invoked by the GraphQL Engine to dispatch a user-defined batch load function for all the collected keys. For more information about the batch load function, refer to the [Implement the Batch Load Function](#11125-implement-the-batch-load-function) section. The following is the method definition of the `dispatch` method. + +```ballerina +public isolated function dispatch(); +``` + +##### 11.1.1.4 The `clearAll` method + +This method does not take any parameters and does not return any values. The purpose of this method is to clear all the collected keys and cached values from the DataLoader cache. The following is the method definition of the this method. + +```ballerina +public isolated function clearAll(); +``` + +#### 11.1.2 The DefaultDataLoader + +The `DefaultDataLoader` is a built-in implementation of the `DataLoader` object available via the `graphql.dataloader` submodule. Users can use this implementation to batch and cache data loading operations. + +#### 11.1.2.1 The `init` Method + +The `init` method of the `DefaultDataLoader` object takes a function pointer of type `BatchLoadFunction`. The following is the method definition of the `init` method. + +```ballerina +public isolated function init(BatchLoadFunction loadFunction); +``` + +###### Example: Initializing a DefaultDataLoader + +```ballerina +dataloader:DefaultDataLoader bookLoader = new (batchBooksForAuthors); +``` + +##### 11.1.2.1.1 The BatchLoadFunction + +The batch load function is responsible for retrieving data based on an array of keys and returning an array of corresponding results or an `error` if the operation fails. The following is the type definition of the batch load function. + +```ballerina +public type BatchLoadFunction isolated function (readonly anydata[] keys) returns anydata[]|error; +``` + +When implementing a batch load function, it is important to ensure that the function returns an array of results that match the length of the keys array provided as input. **If the lengths do not match, the DataLoader will return an error when the `get` method is called**. + +###### Example: Writing a Batch Load Function + +```ballerina +isolated function batchBooksForAuthors(readonly & anydata[] ids) returns Book[][]|error { + final readonly & int[] authorIds = ids; + // Logic to retrieve books from the data source for the given author ids + // Book[][] books = ... + return books; +}; +``` + +#### 11.1.3 Engaging DataLoaders + +To engage a DataLoader with a GraphQL service, follow the steps discussed in the below sections. + +##### 11.1.3.1 Import `graphql.dataloader` Submodule + +In order to engage the dataloader with a GraphQL service, the `graphql.dataloader` submodule must be imported. This submodule provides the `DataLoader` object, which is used to batch and cache data loading operations. + +###### Example: Importing `graphql.dataloader` Submodule + +```ballerina +import graphql.dataloader; +``` + +##### 11.1.3.2 Register DataLoaders to Context via ContextInit Function + +Users should register the `DataLoader` objects via the `graphql:ContextInit` function. The `DataLoader` objects are meant to be used per request. Therefore, the `graphql:ContextInit` function is the ideal place to register the `DataLoader` objects. By registering the `DataLoader` objects to the `graphql:Context` object, these objects become accessible to all resolver functions of the GraphQL service. + +###### Example: Registering DataLoaders to Context via ContextInit Function + +```ballerina +@graphql:ServiceConfig { + contextInit: isolated function (http:RequestContext requestContext, http:Request request) returns graphql:Context { + graphql:Context context = new; + context.registerDataLoader("bookLoader", new dataloader:DefaultDataLoader(batchBooks)); + return context; + } +} +service on new graphql:Listener(9090) { + // ... +} +``` + +##### 11.1.3.3 Define the Corresponding `prefetch` Method + +To engage the DataLoader with a GraphQL field (let's assume the field name is `foo`), define a corresponding _prefetch_ method named `preFoo` in the service, where `Foo` represents the Pascal-cased name of the GraphQL field. The `preFoo` method can include some or all of the parameters from the GraphQL field and must include the `graphql:Context` parameter. Adding the parameters of the GraphQL `foo` field to the `preFoo` method is optional. However, if these parameters are added, the GraphQL Engine will make the same parameter values of the GraphQL field available to the `preFoo` method. + +The GraphQL Engine guarantees the execution of the `preFoo` method prior to the `foo` method. By default, the GraphQL engine searches for a method named `preFoo` in the service class before executing the `foo` method. If the method name is different, the user can override the prefetch method name using the [`prefetchMethodName`](#722-prefetch-method-name-configuration) configuration of the `@graphql:ResourceConfig` annotation. + +The user is responsible for implementing the logic to collect the keys of the data to be loaded into the `DataLoader` in the `preFoo` method. Subsequently, the user can implement the logic to retrieve the data from the `DataLoader` within the `foo` method. + +###### Example: Defining the Corresponding `prefetch` Method + +```ballerina +distinct service class Author { + function preBooks(graphql:Context ctx) { + // ... + } + + resource function get books(graphql:Context ctx) returns Book[] { + // ... + } +} +``` + +###### Example: Overriding the Defalut `prefetch` Method Name + +```ballerina +distinct service class Author { + function addBooks(graphql:Context ctx) { + // ... + } + + @graphql:ResourceConfig { + prefetchMethodName: "addBooks" + } + resource function get books(graphql:Context ctx) returns Book[] { + // ... + } +} +``` + +Bringing everything together, the subsequent examples demonstrates how to engage a DataLoader with a GraphQL service. + +###### Example: Utilizing a DataLoader in a GraphQL Service + +```ballerina +import ballerina/graphql; +import ballerina/graphql.dataloader; +import ballerina/http; + +@graphql:ServiceConfig { + contextInit: isolated function (http:RequestContext requestContext, http:Request request) returns graphql:Context { + graphql:Context context = new; + context.registerDataLoader("bookLoader", new dataloader:DefaultDataLoader(batchBooksForAuthors)); + return context; + } +} +service on new graphql:Listener(9090) { + resource function get authors() returns Author[] { + return getAllAuthors(); + } +} + +distinct service class Author { + private final int authorId; + + function init(int authorId) { + self.authorId = authorId; + } + + resource function get preBooks(graphql:Context ctx) { + dataloader:DataLoader bookLoader = ctx.getDataLoader("bookLoader"); + // Load author id to the DataLoader + bookLoader.add(self.authorId); + } + + resource function get books(graphql:Context ctx) returns Book[] { + dataloader:DataLoader bookLoader = ctx.getDataLoader("bookLoader"); + // Obtain the books from the DataLoader by passing the author id + Book[] books = bookLoader.get(self.authorId); + return books; + } +} + +isolated function batchBooksForAuthors(readonly & anydata[] ids) returns Book[][]|error { + final readonly & int[] authorIds = ids; + // Logic to retrieve books from the data source for the given author ids + // Book[][] books = ... + return books; +}; +``` + +In the given example, both the `books` resource function and the `preBooks` function receive the `graphql:Context` parameter, which grants access to the `DataLoader` objects. By using the `ctx.getDataLoader("bookLoader")` syntax, the specific `DataLoader` object associated with the unique identifier "bookLoader" can be obtained and assigned to the `bookLoader` variable. + +###### Example: Utilizing Multiple DataLoaders in a GraphQL Service + +```ballerina +import ballerina/graphql; +import ballerina/graphql.dataloader; +import ballerina/http; + +@graphql:ServiceConfig { + contextInit: isolated function (http:RequestContext requestContext, http:Request request) returns graphql:Context { + graphql:Context context = new; + context.registerDataLoader("postsLoader", new dataloader:DefaultDataLoader(postsLoaderFunction)); + context.registerDataLoader("rePostsLoader", new dataloader:DefaultDataLoader(rePostsLoaderFunction)); + context.registerDataLoader("followersLoader", new dataloader:DefaultDataLoader(followersLoaderFunction)); + return context; + } +} +service on new graphql:Listener(9090) { + resource function get users() returns User[] { + return getAllUsers(); + } +} + +isolated distinct service class User { + private final int userId; + + isolated function init(int userId) { + self.userId = userId; + } + + isolated resource function get prePosts(graphql:Context ctx) { + dataloader:DataLoader postsLoader = ctx.getDataLoader("postsLoader"); + postsLoader.add(self.userId); + + dataloader:DataLoader rePostsLoader = ctx.getDataLoader("rePostsLoader"); + rePostsLoader.add(self.userId); + } + + isolated resource function get posts(graphql:Context ctx) returns Post[]|error { + dataloader:DataLoader postsLoader = ctx.getDataLoader("postsLoader"); + Post[] posts = check postsLoader.get(self.userId); + + dataloader:DataLoader rePostsLoader = ctx.getDataLoader("rePostsLoader"); + Post[] rePosts = check rePostsLoader.get(self.userId); + + return [...posts, ...rePosts]; + } + + isolated resource function get preFollowers(graphql:Context ctx) { + dataloader:DataLoader followersLoader = ctx.getDataLoader("followersLoader"); + followersLoader.add(self.userId); + } + + isolated resource function get followers(graphql:Context ctx) returns Follower[]|error { + dataloader:DataLoader followersLoader = ctx.getDataLoader("followersLoader"); + return check followersLoader.get(self.userId); + } +} + +isolated function postsLoaderFunction(readonly & anydata[] ids) returns Post[][]|error { + final readonly & int[] keys = ids; + // Logic to retrieve posts from the data source for the given user ids + // Post[][] posts = ... + return posts; +}; + +isolated function rePostsLoaderFunction(readonly & anydata[] ids) returns Post[][]|error { + final readonly & int[] keys = ids; + // Logic to retrieve re posted items from the data source for the given user ids + // Post[][] rePosts = ... + return rePosts; +}; + +isolated function followersLoaderFunction(readonly & anydata[] ids) returns Follower[][]|error { + final readonly & int[] keys = ids; + // Logic to retrieve followers from the data source for the given user ids + // Follower[][] followers = ... + return followers; +}; +``` + +The above example utilizes three DataLoader instances: `postsLoader`, `rePostsLoader`, and `followersLoader`. These DataLoaders are associated with the batch load functions `postsLoaderFunction`, `rePostsLoaderFunction`, and `followersLoaderFunction`. The 'post' field in the example utilizes the `postsLoader` and `rePostsLoader` DataLoaders, while the 'followers' field utilizes the `followersLoader` DataLoader. This demonstrates how different fields can utilize specific DataLoaders to efficiently load and retrieve related data in GraphQL resolvers. \ No newline at end of file diff --git a/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/ArgumentHandler.java b/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/ArgumentHandler.java index 6e104c8a7..39c64df87 100644 --- a/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/ArgumentHandler.java +++ b/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/ArgumentHandler.java @@ -85,6 +85,8 @@ public class ArgumentHandler { private static final String ADD_CONSTRAINT_ERRORS_METHOD = "addConstraintValidationErrors"; private static final String CONSTRAINT_ERROR_MESSAGE = "Constraint validation errors found."; + private static final BString KIND_FIELD = StringUtils.fromString("kind"); + // graphql.parser types private static final int T_STRING = 2; private static final int T_INT = 3; @@ -368,7 +370,7 @@ private BMap getInputObjectArgument(BObject argumentNode, Recor } private Object getJsonArgument(BObject argumentNode) { - int kind = (int) argumentNode.getIntValue(StringUtils.fromString("kind")); + int kind = (int) argumentNode.getIntValue(KIND_FIELD); Object valueField = argumentNode.get(VALUE_FIELD); switch (kind) { case T_STRING: diff --git a/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/DataLoader.java b/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/DataLoader.java new file mode 100644 index 000000000..16d181932 --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/DataLoader.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2023, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.graphql.runtime.engine; + +import io.ballerina.runtime.api.Environment; +import io.ballerina.runtime.api.Future; +import io.ballerina.runtime.api.async.Callback; +import io.ballerina.runtime.api.creators.TypeCreator; +import io.ballerina.runtime.api.types.ObjectType; +import io.ballerina.runtime.api.types.Type; +import io.ballerina.runtime.api.utils.TypeUtils; +import io.ballerina.runtime.api.values.BObject; +import io.ballerina.runtime.api.values.BTypedesc; + +import static io.ballerina.runtime.api.PredefinedTypes.TYPE_ANYDATA; +import static io.ballerina.runtime.api.PredefinedTypes.TYPE_ERROR; + +/** + * This class provides native implementations of the Ballerina DataLoader class. + */ +public class DataLoader { + private static final String DATA_LOADER_PROCESSES_GET_METHOD_NAME = "processGet"; + + private DataLoader() { + } + + public static Object get(Environment env, BObject dataLoader, Object key, BTypedesc typedesc) { + Future balFuture = env.markAsync(); + ObjectType clientType = (ObjectType) TypeUtils.getReferredType(TypeUtils.getType(dataLoader)); + Object[] paramFeed = getProcessGetMethodParams(key, typedesc); + Callback executionCallback = new ExecutionCallback(balFuture); + Type returnType = TypeCreator.createUnionType(TYPE_ANYDATA, TYPE_ERROR); + if (clientType.isIsolated() && clientType.isIsolated(DATA_LOADER_PROCESSES_GET_METHOD_NAME)) { + env.getRuntime() + .invokeMethodAsyncConcurrently(dataLoader, DATA_LOADER_PROCESSES_GET_METHOD_NAME, null, null, + executionCallback, null, returnType, paramFeed); + return null; + } + env.getRuntime().invokeMethodAsyncSequentially(dataLoader, DATA_LOADER_PROCESSES_GET_METHOD_NAME, null, null, + executionCallback, null, returnType, paramFeed); + return null; + } + + private static Object[] getProcessGetMethodParams(Object key, BTypedesc typedesc) { + return new Object[]{key, true, typedesc, true}; + } +} diff --git a/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/Engine.java b/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/Engine.java index 623a6798e..1831a55ba 100644 --- a/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/Engine.java +++ b/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/Engine.java @@ -44,6 +44,7 @@ import java.io.ObjectInputStream; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.Base64; import java.util.List; @@ -68,6 +69,7 @@ * This handles Ballerina GraphQL Engine. */ public class Engine { + private Engine() { } @@ -247,6 +249,20 @@ private static List getPathList(BArray pathArray) { return result; } + public static Object getMethod(BObject service, BString methodName) { + ServiceType serviceType = (ServiceType) TypeUtils.getType(service); + return getMethod(serviceType, methodName.getValue()); + } + + private static MethodType getMethod(ServiceType serviceType, String methodName) { + for (MethodType serviceMethod : serviceType.getMethods()) { + if (methodName.equals(serviceMethod.getName())) { + return serviceMethod; + } + } + return null; + } + private static RemoteMethodType getRemoteMethod(ServiceType serviceType, String methodName) { for (RemoteMethodType remoteMethod : serviceType.getRemoteMethods()) { if (remoteMethod.getName().equals(methodName)) { @@ -273,7 +289,7 @@ public static BString getInterceptorName(BObject interceptor) { public static Object getResourceAnnotation(BObject service, BString operationType, BArray path, BString methodName) { ServiceType serviceType = (ServiceType) TypeUtils.getType(service); - MethodType methodType = null; + MethodType methodType; if (OPERATION_QUERY.equals(operationType.getValue())) { methodType = getResourceMethod(serviceType, getPathList(path), GET_ACCESSOR); } else if (OPERATION_SUBSCRIPTION.equals(operationType.getValue())) { @@ -287,4 +303,28 @@ public static Object getResourceAnnotation(BObject service, BString operationTyp } return null; } + + public static boolean hasPrefetchMethod(BObject serviceObject, BString prefetchMethodName) { + ServiceType serviceType = (ServiceType) serviceObject.getOriginalType(); + return Arrays.stream(serviceType.getMethods()) + .anyMatch(methodType -> methodType.getName().equals(prefetchMethodName.getValue())); + } + + public static void executePrefetchMethod(Environment environment, BObject context, BObject service, + MethodType resourceMethod, BObject fieldObject) { + Future future = environment.markAsync(); + ExecutionCallback executionCallback = new ExecutionCallback(future); + ServiceType serviceType = (ServiceType) TypeUtils.getType(service); + ArgumentHandler argumentHandler = new ArgumentHandler(resourceMethod, context, fieldObject, null, false); + Object[] arguments = argumentHandler.getArguments(); + if (serviceType.isIsolated() && serviceType.isIsolated(resourceMethod.getName())) { + environment.getRuntime() + .invokeMethodAsyncConcurrently(service, resourceMethod.getName(), null, RESOURCE_EXECUTION_STRAND, + executionCallback, null, null, arguments); + } else { + environment.getRuntime() + .invokeMethodAsyncSequentially(service, resourceMethod.getName(), null, RESOURCE_EXECUTION_STRAND, + executionCallback, null, null, arguments); + } + } } diff --git a/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/ExecutionCallback.java b/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/ExecutionCallback.java index 5e92ea65e..0fa6d93ce 100644 --- a/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/ExecutionCallback.java +++ b/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/ExecutionCallback.java @@ -23,7 +23,7 @@ import io.ballerina.runtime.api.values.BError; /** - * Callback class for executing GraphQL fields. + * Callback class for executing Ballerina dependently type methods. */ public class ExecutionCallback implements Callback { private final Future future; diff --git a/native/src/main/java/io/ballerina/stdlib/graphql/runtime/parser/ParserUtils.java b/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/Placeholder.java similarity index 50% rename from native/src/main/java/io/ballerina/stdlib/graphql/runtime/parser/ParserUtils.java rename to native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/Placeholder.java index 1996ae66b..2cb6db734 100644 --- a/native/src/main/java/io/ballerina/stdlib/graphql/runtime/parser/ParserUtils.java +++ b/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/Placeholder.java @@ -1,7 +1,7 @@ /* - * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2023, WSO2 LLC. (http://www.wso2.com) All Rights Reserved. * - * WSO2 Inc. licenses this file to you under the Apache License, + * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except * in compliance with the License. * You may obtain a copy of the License at @@ -16,19 +16,26 @@ * under the License. */ -package io.ballerina.stdlib.graphql.runtime.parser; +package io.ballerina.stdlib.graphql.runtime.engine; import io.ballerina.runtime.api.utils.StringUtils; import io.ballerina.runtime.api.values.BObject; import io.ballerina.runtime.api.values.BString; /** - * This class is used to implement utility functions for the Ballerina GraphQL parser. + * This class provides native implementations of the Ballerina Placeholder class. */ -public class ParserUtils { - private ParserUtils() {} +public class Placeholder { + private static final BString PLACE_HOLDER_FIELD_OBJECT = StringUtils.fromString("field"); - public static BString getHashCode(BObject object) { - return StringUtils.fromString(Integer.toString(object.hashCode())); + private Placeholder() { + } + + public static void setFieldValue(BObject placeholder, BObject field) { + placeholder.set(PLACE_HOLDER_FIELD_OBJECT, field); + } + + public static BObject getFieldValue(BObject placeholder) { + return (BObject) placeholder.get(PLACE_HOLDER_FIELD_OBJECT); } } diff --git a/native/src/main/java/io/ballerina/stdlib/graphql/runtime/utils/Utils.java b/native/src/main/java/io/ballerina/stdlib/graphql/runtime/utils/Utils.java index 0997924cb..01883f103 100644 --- a/native/src/main/java/io/ballerina/stdlib/graphql/runtime/utils/Utils.java +++ b/native/src/main/java/io/ballerina/stdlib/graphql/runtime/utils/Utils.java @@ -25,6 +25,7 @@ import io.ballerina.runtime.api.types.Type; import io.ballerina.runtime.api.utils.StringUtils; import io.ballerina.runtime.api.values.BError; +import io.ballerina.runtime.api.values.BObject; import io.ballerina.runtime.api.values.BString; import static io.ballerina.stdlib.graphql.runtime.utils.ModuleUtils.getModule; @@ -37,12 +38,14 @@ private Utils() { } // Inter-op function names - static final String EXECUTE_RESOURCE_FUNCTION = "executeQueryResource"; - static final String EXECUTE_INTERCEPTOR_FUNCTION = "executeInterceptor"; + private static final String EXECUTE_RESOURCE_FUNCTION = "executeQueryResource"; + private static final String EXECUTE_INTERCEPTOR_FUNCTION = "executeInterceptor"; + // Internal type names public static final String ERROR_TYPE = "Error"; public static final String CONTEXT_OBJECT = "Context"; public static final String FIELD_OBJECT = "Field"; + public static final String DATA_LOADER_OBJECT = "DataLoader"; public static final String UPLOAD = "Upload"; public static final BString INTERNAL_NODE = StringUtils.fromString("internalNode"); @@ -104,4 +107,8 @@ private static boolean hasExpectedModuleName(Type type, String expectedModuleNam return type.getPackage().getOrg().equals(expectedOrgName) && type.getPackage().getName() .equals(expectedModuleName); } + + public static BString getHashCode(BObject object) { + return StringUtils.fromString(Integer.toString(object.hashCode())); + } }