diff --git a/ballerina-tests/Ballerina.toml b/ballerina-tests/Ballerina.toml index e99c8de84..00ab9b27d 100644 --- a/ballerina-tests/Ballerina.toml +++ b/ballerina-tests/Ballerina.toml @@ -1,5 +1,5 @@ [package] org = "ballerina" name = "graphql_tests" -version = "1.10.1" +version = "1.11.0" diff --git a/ballerina-tests/Dependencies.toml b/ballerina-tests/Dependencies.toml index 93db6f16b..9b46347c8 100644 --- a/ballerina-tests/Dependencies.toml +++ b/ballerina-tests/Dependencies.toml @@ -33,7 +33,7 @@ dependencies = [ [[package]] org = "ballerina" name = "constraint" -version = "1.4.0" +version = "1.5.0" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] @@ -44,7 +44,7 @@ modules = [ [[package]] org = "ballerina" name = "crypto" -version = "2.5.0" +version = "2.6.2" dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "time"} @@ -67,9 +67,11 @@ modules = [ [[package]] org = "ballerina" name = "graphql" -version = "1.10.1" +version = "1.11.0" dependencies = [ {org = "ballerina", name = "auth"}, + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, {org = "ballerina", name = "file"}, {org = "ballerina", name = "http"}, {org = "ballerina", name = "io"}, @@ -96,7 +98,7 @@ modules = [ [[package]] org = "ballerina" name = "graphql_tests" -version = "1.10.1" +version = "1.11.0" dependencies = [ {org = "ballerina", name = "constraint"}, {org = "ballerina", name = "file"}, @@ -119,7 +121,7 @@ modules = [ [[package]] org = "ballerina" name = "http" -version = "2.10.5" +version = "2.10.6" dependencies = [ {org = "ballerina", name = "auth"}, {org = "ballerina", name = "cache"}, diff --git a/ballerina-tests/build.gradle b/ballerina-tests/build.gradle index d0cf28bfa..7891e06fc 100644 --- a/ballerina-tests/build.gradle +++ b/ballerina-tests/build.gradle @@ -1,5 +1,3 @@ -import org.apache.tools.ant.taskdefs.condition.Os - /* * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. * diff --git a/ballerina-tests/custom_prefix_without_import.bal b/ballerina-tests/custom_prefix_without_import.bal index b3692fa6e..0cfdfe7a4 100644 --- a/ballerina-tests/custom_prefix_without_import.bal +++ b/ballerina-tests/custom_prefix_without_import.bal @@ -14,7 +14,14 @@ // specific language governing permissions and limitations // under the License. +import ballerina/graphql; + service /custom_prefix on graphqlListener { + @graphql:ResourceConfig { + cacheConfig: { + enabled: true + } + } resource function get greeting() returns string { return "Hello, world"; } diff --git a/ballerina-tests/tests/44_server_caches.bal b/ballerina-tests/tests/44_server_caches.bal new file mode 100644 index 000000000..96e3195a9 --- /dev/null +++ b/ballerina-tests/tests/44_server_caches.bal @@ -0,0 +1,250 @@ +// Copyright (c) 2024 WSO2 LLC. (http://www.wso2.com). +// +// 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/test; +import ballerina/lang.runtime; + +@test:Config { + groups: ["server_cache"], + dataProvider: dataProviderServerCache +} +isolated function testServerSideCache(string documentFile, string[] resourceFileNames, json variables = (), string[] operationNames = []) returns error? { + string url = "http://localhost:9091/server_cache"; + string document = check getGraphqlDocumentFromFile(documentFile); + foreach int i in 0.. { + map<[string, string[], json, string[]]> dataSet = { + "1": ["server_cache", ["server_cache_1", "server_cache_2", "server_cache_3"], (), ["A", "B", "A"]], + "2": ["server_cache_eviction", ["server_cache_2", "server_cache_4"], (), ["B", "A"]], + "3": ["server_cache_with_records", ["server_cache_with_rec_1", "server_cache_with_rec_2", "server_cache_with_rec_1"], (), ["A", "B", "A"]], + "4": ["server_cache_with_service_obj", ["server_cache_with_svc_obj_1", "server_cache_with_svc_obj_2", "server_cache_with_svc_obj_1"], (), ["A", "B", "A"]], + "5": ["server_cache_eviction_with_service_obj", ["server_cache_with_svc_obj_1", "server_cache_with_svc_obj_2", "server_cache_with_svc_obj_3"], (), ["A", "B", "A"]], + "6": ["server_cache_with_arrays", ["server_cache_with_arrays_1", "server_cache_with_arrays_2", "server_cache_with_arrays_3"], (), ["A", "B", "A"]], + "7": ["server_cache_eviction_with_arrays", ["server_cache_with_arrays_1", "server_cache_with_arrays_2", "server_cache_with_arrays_4"], (), ["A", "B", "A"]], + "8": ["server_cache_with_union", ["server_cache_with_union_1", "server_cache_with_union_2", "server_cache_with_union_1"], (), ["A", "B", "A"]], + "9": ["server_cache_eviction_with_union", ["server_cache_with_union_1", "server_cache_with_union_3", "server_cache_with_union_4"], (), ["A", "B", "A"]], + "10": ["server_cache_with_errors", ["server_cache_with_errors_1", "server_cache_with_errors_2"], (), ["A", "B"]], + "11": ["server_cache_with_nullable_inputs", ["server_cache_with_nullable_inputs_1", "server_cache_with_nullable_inputs_2", "server_cache_with_nullable_inputs_1"], (), ["A", "B", "A"]], + "12": ["server_cache_eviction_with_nullable_inputs", ["server_cache_eviction_with_nullable_inputs_1", "server_cache_eviction_with_nullable_inputs_2", "server_cache_eviction_with_nullable_inputs_3"], (), ["A", "B", "A"]], + "13": ["server_cache_with_list_inputs", ["server_cache_eviction_with_list_inputs_1", "server_cache_eviction_with_list_inputs_2", "server_cache_eviction_with_list_inputs_1"], {"names": ["Enemy3"]}, ["A", "B", "A"]], + "14": ["server_cache_eviction_with_list_inputs", ["server_cache_eviction_with_list_inputs_1", "server_cache_eviction_with_list_inputs_2", "server_cache_eviction_with_list_inputs_3"], {"names": ["Enemy3"]}, ["A", "B", "A"]], + "15": ["server_cache_with_null_values", ["server_cache_with_null_values_1", "server_cache_with_null_values_2", "server_cache_with_null_values_3"], (), ["A", "B", "A"]], + "16": ["server_cache_with_input_object", ["server_cache_with_input_object_1", "server_cache_with_input_object_2", "server_cache_with_input_object_3"], (), ["A", "B", "A"]] + }; + return dataSet; +} + +@test:Config { + groups: ["server_cache", "data_loader"], + dataProvider: dataProviderServerCacheWithDataloader +} +isolated function testServerSideCacheWithDataLoader(string documentFile, string[] resourceFileNames, json variables = (), string[] operationNames = []) returns error? { + string url = "http://localhost:9090/caching_with_dataloader"; + string document = check getGraphqlDocumentFromFile(documentFile); + foreach int i in 0..< resourceFileNames.length() { + json actualPayload = check getJsonPayloadFromService(url, document, variables, operationNames[i]); + json expectedPayload = check getJsonContentFromFile(resourceFileNames[i]); + assertJsonValuesWithOrder(actualPayload, expectedPayload); + } + resetDispatchCounters(); +} + +function dataProviderServerCacheWithDataloader() returns map<[string, string[], json, string[]]> { + map<[string, string[], json, string[]]> dataSet = { + "1": ["server_cache_with_dataloader", ["server_cache_with_dataloader_1", "server_cache_with_dataloader_2", "server_cache_with_dataloader_1"], (), ["A", "B", "A"]], + "2": ["server_cache_eviction_with_dataloader", ["server_cache_with_dataloader_1", "server_cache_with_dataloader_2", "server_cache_with_dataloader_3"], (), ["A", "B", "A"]] + }; + return dataSet; +} + +@test:Config { + groups: ["server_cache", "data_loader"], + dataProvider: dataProviderServerCacheWithDataloaderInOperationalLevel +} +isolated function testServerSideCacheWithDataLoaderInOperationalLevel(string documentFile, string[] resourceFileNames, json variables = (), string[] operationNames = []) returns error? { + string url = "http://localhost:9090/caching_with_dataloader_operational"; + string document = check getGraphqlDocumentFromFile(documentFile); + foreach int i in 0..< resourceFileNames.length() { + json actualPayload = check getJsonPayloadFromService(url, document, variables, operationNames[i]); + json expectedPayload = check getJsonContentFromFile(resourceFileNames[i]); + assertJsonValuesWithOrder(actualPayload, expectedPayload); + } + resetDispatchCounters(); +} + +function dataProviderServerCacheWithDataloaderInOperationalLevel() returns map<[string, string[], json, string[]]> { + map<[string, string[], json, string[]]> dataSet = { + "1": ["server_cache_with_dataloader_operational", ["server_cache_with_dataloader_3", "server_cache_with_dataloader_5", "server_cache_with_dataloader_3"], (), ["A", "B", "A"]], + "2": ["server_cache_eviction_with_dataloader_operational", ["server_cache_with_dataloader_3", "server_cache_with_dataloader_5", "server_cache_with_dataloader_4"], (), ["A", "B", "A"]] + }; + return dataSet; +} + +@test:Config { + groups: ["server_cache"], + dataProvider: dataProviderServerCacheOperationalLevel +} +isolated function testServerSideCacheInOperationalLevel(string documentFile, string[] resourceFileNames, json variables = (), string[] operationNames = []) returns error? { + string url = "http://localhost:9091/server_cache_operations"; + string document = check getGraphqlDocumentFromFile(documentFile); + foreach int i in 0.. { + map<[string, string[], json, string[]]> dataSet = { + "1": ["server_cache", ["server_cache_1", "server_cache_2", "server_cache_1"], (), ["A", "B", "A"]], + "2": ["server_cache_eviction", ["server_cache_2", "server_cache_4"], (), ["B", "A"]], + "3": ["server_cache_with_records_operations", ["server_cache_with_rec_1", "server_cache_with_rec_3", "server_cache_with_rec_1"], (), ["A", "B", "A"]], + "4": ["server_cache_with_records_eviction", ["server_cache_with_rec_1", "server_cache_with_rec_5", "server_cache_with_rec_4"], (), ["A", "B", "A"]], + "5": ["server_cache_with_service_obj", ["server_cache_with_svc_obj_1", "server_cache_with_svc_obj_2", "server_cache_with_svc_obj_1"], (), ["A", "B", "A"]], + "6": ["server_cache_eviction_with_service_obj", ["server_cache_with_svc_obj_1", "server_cache_with_svc_obj_2", "server_cache_with_svc_obj_3"], (), ["A", "B", "A"]], + "7": ["server_cache_with_arrays", ["server_cache_with_arrays_5", "server_cache_with_arrays_2", "server_cache_with_arrays_5"], (), ["A", "B", "A"]], + "8": ["server_cache_eviction_with_arrays", ["server_cache_with_arrays_7", "server_cache_with_arrays_2", "server_cache_with_arrays_6"], (), ["A", "B", "A"]], + "9": ["server_cache_with_unions_operational_level", ["server_cache_with_unions_1", "server_cache_with_unions_2", "server_cache_with_unions_1"], (), ["A", "B", "A"]], + "10": ["server_cache_with_unions_operational_level", ["server_cache_with_unions_1", "server_cache_with_unions_2", "server_cache_with_unions_3"], (), ["A", "C", "A"]], + "11": ["server_cache_eviction", ["server_cache_2", "server_cache_4", "server_cache_5", "server_cache_4"], (), ["B", "A", "C", "A"]], + "12": ["server_cache_with_inputs", ["server_cache_with_nullable_inputs_7", "server_cache_with_nullable_inputs_7"], (), ["B", "C"]], + "13": ["server_cache_with_inputs", ["server_cache_with_empty_input_1", "server_cache_with_empty_input_2"], (), ["B", "D"]], + "14": ["server_cache_with_partial_responses", ["server_cache_with_partial_reponses_1", "server_cache_with_partial_reponses_2", "server_cache_with_partial_reponses_1"], (), ["A", "B", "A"]], + "15": ["server_cache_with_partial_responses", ["server_cache_with_partial_reponses_1", "server_cache_with_partial_reponses_2", "server_cache_with_partial_reponses_3"], (), ["A", "C", "A"]], + "16": ["server_cache_with_errors_2", ["server_cache_with_errors_4", "server_cache_with_errors_5", "server_cache_with_errors_3"], (), ["A", "B", "A"]] + }; + return dataSet; +} + +@test:Config { + groups: ["server_cache", "interceptors"], + dataProvider: dataProviderServerCacheWithInterceptors +} +isolated function testServerSideCacheWithInterceptors(string documentFile, string[] resourceFileNames, json variables = (), string[] operationNames = []) returns error? { + string url = "http://localhost:9091/field_caching_with_interceptors"; + string document = check getGraphqlDocumentFromFile(documentFile); + foreach int i in 0..< resourceFileNames.length() { + json actualPayload = check getJsonPayloadFromService(url, document, variables, operationNames[i]); + json expectedPayload = check getJsonContentFromFile(resourceFileNames[i]); + assertJsonValuesWithOrder(actualPayload, expectedPayload); + } +} + +@test:Config { + groups: ["server_cache", "data_loader"], + dataProvider: dataProviderServerCacheWithInterceptors +} +isolated function testServerSideCacheWithInterceptorInOperationalLevel(string documentFile, string[] resourceFileNames, json variables = (), string[] operationNames = []) returns error? { + string url = "http://localhost:9091/caching_with_interceptor_operations"; + string document = check getGraphqlDocumentFromFile(documentFile); + foreach int i in 0..< resourceFileNames.length() { + json actualPayload = check getJsonPayloadFromService(url, document, variables, operationNames[i]); + json expectedPayload = check getJsonContentFromFile(resourceFileNames[i]); + assertJsonValuesWithOrder(actualPayload, expectedPayload); + } +} + +function dataProviderServerCacheWithInterceptors() returns map<[string, string[], json, string[]]> { + map<[string, string[], json, string[]]> dataSet = { + "1": ["server_cache_with_interceptors", ["server_cache_with_interceptors_1", "server_cache_with_interceptors_2", "server_cache_with_interceptors_1"], (), ["A", "B", "A"]], + "2": ["server_cache_eviction_with_interceptors", ["server_cache_with_interceptors_1", "server_cache_with_interceptors_2", "server_cache_with_interceptors_3"], (), ["A", "B", "A"]] + }; + return dataSet; +} + +@test:Config { + groups: ["server_cache"] +} +isolated function testServerSideCacheInOperationalLevelWithTTL() returns error? { + string url = "http://localhost:9091/server_cache_operations"; + string document = check getGraphqlDocumentFromFile("server_cache_operations_with_TTL"); + runtime:sleep(21); + + json actualPayload = check getJsonPayloadFromService(url, document, (), "A"); + json expectedPayload = check getJsonContentFromFile("server_cache_1"); + assertJsonValuesWithOrder(actualPayload, expectedPayload); + + actualPayload = check getJsonPayloadFromService(url, document, (), "B"); + expectedPayload = check getJsonContentFromFile("server_cache_10"); + assertJsonValuesWithOrder(actualPayload, expectedPayload); + + actualPayload = check getJsonPayloadFromService(url, document, (), "A"); + expectedPayload = check getJsonContentFromFile("server_cache_1"); + assertJsonValuesWithOrder(actualPayload, expectedPayload); + + runtime:sleep(21); + + actualPayload = check getJsonPayloadFromService(url, document, (), "A"); + expectedPayload = check getJsonContentFromFile("server_cache_9"); + assertJsonValuesWithOrder(actualPayload, expectedPayload); +} + +@test:Config { + groups: ["server_cache"] +} +isolated function testServerCacheEvictionWithTTL() returns error? { + string url = "http://localhost:9091/field_caching_with_interceptors"; + string document = check getGraphqlDocumentFromFile("server_cache_fields_with_TTL"); + + json actualPayload = check getJsonPayloadFromService(url, document, (), "A"); + json expectedPayload = check getJsonContentFromFile("server_cache_eviction_with_TTL_1"); + assertJsonValuesWithOrder(actualPayload, expectedPayload); + + actualPayload = check getJsonPayloadFromService(url, document, {"name": "Potter"}, "B"); + expectedPayload = check getJsonContentFromFile("server_cache_eviction_with_TTL_2"); + assertJsonValuesWithOrder(actualPayload, expectedPayload); + + actualPayload = check getJsonPayloadFromService(url, document, (), "A"); + expectedPayload = check getJsonContentFromFile("server_cache_eviction_with_TTL_1"); + assertJsonValuesWithOrder(actualPayload, expectedPayload); + + runtime:sleep(11); + + actualPayload = check getJsonPayloadFromService(url, document, (), "A"); + expectedPayload = check getJsonContentFromFile("server_cache_eviction_with_TTL_3"); + assertJsonValuesWithOrder(actualPayload, expectedPayload); +} + +@test:Config { + groups: ["server_cache", "records"], + dataProvider: dataProviderServerSideCacheWithDynamicResponse +} +isolated function testServerSideCacheWithDynamicResponse(string documentFile, string[] resourceFileNames, json variables = (), string[] operationNames = []) returns error? { + string url = "http://localhost:9091/dynamic_response"; + string document = check getGraphqlDocumentFromFile(documentFile); + foreach int i in 0..< resourceFileNames.length() { + json actualPayload = check getJsonPayloadFromService(url, document, variables, operationNames[i]); + json expectedPayload = check getJsonContentFromFile(resourceFileNames[i]); + assertJsonValuesWithOrder(actualPayload, expectedPayload); + } +} + +function dataProviderServerSideCacheWithDynamicResponse() returns map<[string, string[], json, string[]]> { + map<[string, string[], json, string[]]> dataSet = { + "1": ["server_cache_with_dynamic_responses", ["server_cache_with_dynamic_responses_1", "server_cache_with_dynamic_responses_2", "server_cache_with_dynamic_responses_3"], (), ["A", "B", "C"]], + "2": ["server_cache_with_dynamic_responses", ["server_cache_with_dynamic_responses_2", "server_cache_with_dynamic_responses_4", "server_cache_with_dynamic_responses_2"], (), ["B", "D", "B"]], + "3": ["server_cache_with_dynamic_responses", ["server_cache_with_dynamic_responses_3", "server_cache_with_dynamic_responses_4", "server_cache_with_dynamic_responses_3"], (), ["C", "D", "C"]], + "4": ["server_cache_with_dynamic_responses", ["server_cache_with_dynamic_responses_3", "server_cache_with_dynamic_responses_4", "server_cache_with_dynamic_responses_5"], (), ["C", "E", "C"]], + "5": ["server_cache_with_dynamic_responses", ["server_cache_with_dynamic_responses_6", "server_cache_with_dynamic_responses_7", "server_cache_with_dynamic_responses_8"], (), ["B", "F", "B"]] + }; + return dataSet; +} diff --git a/ballerina-tests/tests/batch_load_functions.bal b/ballerina-tests/tests/batch_load_functions.bal index 698ed1f7c..0c1f42323 100644 --- a/ballerina-tests/tests/batch_load_functions.bal +++ b/ballerina-tests/tests/batch_load_functions.bal @@ -71,3 +71,29 @@ isolated function faultyAuthorLoaderFunction(readonly & anydata[] ids) returns A return validKeys.'map(key => authorTable.get(key)); } }; + +isolated function authorLoaderFunction2(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 => authorTable2.hasKey(key)).cloneReadOnly(); + return keys.length() != validKeys.length() ? error("Invalid keys found for authors") + : validKeys.'map(key => authorTable2.get(key)); + } +}; + +isolated function bookLoaderFunction2(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 bookTable2.'filter(book => book.author == key).toArray().clone(); + } + }); +}; diff --git a/ballerina-tests/tests/interceptors.bal b/ballerina-tests/tests/interceptors.bal index dc3fbdee3..477c9419c 100644 --- a/ballerina-tests/tests/interceptors.bal +++ b/ballerina-tests/tests/interceptors.bal @@ -847,7 +847,7 @@ readonly service class ServiceLevelInterceptor { } isolated function grantAccess(string fieldName) returns boolean { - string[] grantedFields = ["profile", "books", "setName", "person", "setAge", "customer", "newBooks"]; + string[] grantedFields = ["profile", "books", "setName", "person", "setAge", "customer", "newBooks", "updatePerson", "person"]; if grantedFields.indexOf(fieldName) is int { return true; } diff --git a/ballerina-tests/tests/object_types.bal b/ballerina-tests/tests/object_types.bal index 0eeebf3e8..f32f03dfd 100644 --- a/ballerina-tests/tests/object_types.bal +++ b/ballerina-tests/tests/object_types.bal @@ -483,3 +483,110 @@ distinct service class NestedField { ], @graphql:ID string[] j = ["id1"]) returns string? => (); } + +public type HumanService FriendService|EnemyService; + +public isolated distinct service class FriendService { + private final string name; + private final int age; + private final boolean isMarried; + + public isolated function init(string name, int age, boolean isMarried) { + self.name = name; + self.age = age; + self.isMarried = isMarried; + } + + @graphql:ResourceConfig { + cacheConfig: { + maxAge: 180 + } + } + isolated resource function get name() returns string { + return self.name; + } + + @graphql:ResourceConfig { + cacheConfig: { + maxAge: 180 + } + } + isolated resource function get age() returns int { + return self.age; + } + + @graphql:ResourceConfig { + cacheConfig: { + maxAge: 180 + } + } + isolated resource function get isMarried() returns boolean { + return self.isMarried; + } +} + +public isolated distinct service class AssociateService { + private final string name; + private final string status; + + public isolated function init(string name, string status) { + self.name = name; + self.status = status; + } + + isolated resource function get name() returns string { + return self.name; + } + + isolated resource function get status() returns string { + return self.status; + } +} + +public isolated distinct service class EnemyService { + private final string name; + private final int age; + private final boolean isMarried; + + public isolated function init(string name, int age, boolean isMarried) { + self.name = name; + self.age = age; + self.isMarried = isMarried; + } + + isolated resource function get name() returns string { + return self.name; + } + + isolated resource function get age() returns int { + return self.age; + } + + isolated resource function get isMarried() returns boolean { + return self.isMarried; + } +} + +public isolated distinct service class AuthorData2 { + 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_2); + bookLoader.add(self.author.id); + } + + isolated resource function get books(graphql:Context ctx) returns BookData[]|error { + dataloader:DataLoader bookLoader = ctx.getDataLoader(BOOK_LOADER_2); + BookRow[] bookrows = check bookLoader.get(self.author.id); + return from BookRow bookRow in bookrows + select new BookData(bookRow); + } +} diff --git a/ballerina-tests/tests/records.bal b/ballerina-tests/tests/records.bal index 007843c17..2af4bd280 100644 --- a/ballerina-tests/tests/records.bal +++ b/ballerina-tests/tests/records.bal @@ -377,3 +377,33 @@ type InputObject2 record {| type InputObject3 record {| int age = 30; |}; + +type Friend record {| + readonly string name; + int age; + boolean isMarried; +|}; + +type Enemy record {| + readonly string name; + int age; + boolean isMarried; +|}; + +type EnemyInput record {| + readonly string name = "Enemy6"; + int age = 12; +|}; + +type Associate record {| + readonly string name; + string status; +|}; + +public type Relationship FriendService|AssociateService; + +type User record {| + int id?; + string name?; + int age?; +|}; diff --git a/ballerina-tests/tests/resources/documents/server_cache.graphql b/ballerina-tests/tests/resources/documents/server_cache.graphql new file mode 100644 index 000000000..462dc9e88 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache.graphql @@ -0,0 +1,8 @@ +query A { + greet + name(id:1) +} + +mutation B { + updateName(name: "John", enableEvict: false) +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_eviction.graphql b/ballerina-tests/tests/resources/documents/server_cache_eviction.graphql new file mode 100644 index 000000000..9c7b30b90 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_eviction.graphql @@ -0,0 +1,12 @@ +query A { + greet + name(id:1) +} + +mutation B { + updateName(name: "John", enableEvict: true) +} + +mutation C { + updateName(name: "Walter White", enableEvict: false) +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_eviction_with_arrays.graphql b/ballerina-tests/tests/resources/documents/server_cache_eviction_with_arrays.graphql new file mode 100644 index 000000000..93946a573 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_eviction_with_arrays.graphql @@ -0,0 +1,13 @@ +query A { + getFriendServices { + name + age + } +} + +mutation B { + updateFriend(name: "Tyler", age: 28, isMarried: true, enableEvict: true) { + name + age + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_eviction_with_dataloader.graphql b/ballerina-tests/tests/resources/documents/server_cache_eviction_with_dataloader.graphql new file mode 100644 index 000000000..de51cf976 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_eviction_with_dataloader.graphql @@ -0,0 +1,26 @@ +query A { + authors(ids: [1, 2, 3]) { + name + books { + id + title + } + } +} + +mutation B { + sabthar: updateAuthorName(id: 1, name: "Sabthar", enableEvict: true) { + name + books { + id + title + } + } + mahroof: updateAuthorName(id: 2, name: "Mahroof") { + name + books { + id + title + } + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_eviction_with_dataloader_operational.graphql b/ballerina-tests/tests/resources/documents/server_cache_eviction_with_dataloader_operational.graphql new file mode 100644 index 000000000..b36289f94 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_eviction_with_dataloader_operational.graphql @@ -0,0 +1,26 @@ +query A { + authors(ids: [1, 2, 3]) { + name + books { + id + title + } + } +} + +mutation B { + harari: updateAuthorName(id: 1, name: "Yual Noah Harari", enableEvict: true) { + name + books { + id + title + } + } + manson: updateAuthorName(id: 2, name: "Mark Manson") { + name + books { + id + title + } + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_eviction_with_interceptors.graphql b/ballerina-tests/tests/resources/documents/server_cache_eviction_with_interceptors.graphql new file mode 100644 index 000000000..9e8b9c029 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_eviction_with_interceptors.graphql @@ -0,0 +1,7 @@ +query A { + enemy +} + +mutation B { + updateEnemy(name: "Snape", enableEvict: true) +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_eviction_with_list_inputs.graphql b/ballerina-tests/tests/resources/documents/server_cache_eviction_with_list_inputs.graphql new file mode 100644 index 000000000..3ae4aca16 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_eviction_with_list_inputs.graphql @@ -0,0 +1,10 @@ +query A ($names:[String!]!){ + isAllMarried(names: $names) +} + +mutation B { + updateEnemy(enemy: {name: "Enemy3", age: 44}, enableEvict: true) { + name + isMarried + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_eviction_with_nullable_inputs.graphql b/ballerina-tests/tests/resources/documents/server_cache_eviction_with_nullable_inputs.graphql new file mode 100644 index 000000000..22469f1b1 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_eviction_with_nullable_inputs.graphql @@ -0,0 +1,13 @@ +query A { + enemies(isMarried: null) { + name + isMarried + } +} + +mutation B { + removeEnemy(name: "Enemy2", enableEvict: false) { + name + isMarried + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_eviction_with_service_obj.graphql b/ballerina-tests/tests/resources/documents/server_cache_eviction_with_service_obj.graphql new file mode 100644 index 000000000..f836c622f --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_eviction_with_service_obj.graphql @@ -0,0 +1,13 @@ +query A { + getFriendService(name: "Jesse Pinkman") { + name + age + } +} + +mutation B { + updateFriend(name: "Jesse Pinkman", age: 30, isMarried: false, enableEvict: true) { + name + age + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_eviction_with_union.graphql b/ballerina-tests/tests/resources/documents/server_cache_eviction_with_union.graphql new file mode 100644 index 000000000..91b6f9bd1 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_eviction_with_union.graphql @@ -0,0 +1,15 @@ +query A { + getServices(isEnemy: true) { + ...on EnemyService { + name + age + } + } +} + +mutation B { + addEnemy(name: "Jake", age: 22, isMarried: false, enableEvict: true) { + name + age + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_fields_with_TTL.graphql b/ballerina-tests/tests/resources/documents/server_cache_fields_with_TTL.graphql new file mode 100644 index 000000000..613ba4736 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_fields_with_TTL.graphql @@ -0,0 +1,7 @@ +query A { + friend +} + +mutation B($name: String!) { + updateFriend(name: $name) +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_input_object_parameters.graphql b/ballerina-tests/tests/resources/documents/server_cache_input_object_parameters.graphql new file mode 100644 index 000000000..2ca1f95b9 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_input_object_parameters.graphql @@ -0,0 +1,18 @@ +input Associate { + name: String! + status: String +} + +query B { + status(associates: [ + { name: "Gus Fring", status: "dead"}, + { name: "Saul Goodman", status: "alive"} + ]) +} + +query C { + status(associates: [ + { name: "Gus Fring", status: "alive"}, + { name: "Saul Goodman", status: "alive"} + ]) +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_list_inputs.graphql b/ballerina-tests/tests/resources/documents/server_cache_list_inputs.graphql new file mode 100644 index 000000000..a6f1a7e24 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_list_inputs.graphql @@ -0,0 +1,8 @@ +{ + cities( + addresses: [ + { name: "Gus Fring", status: "dead" }, + { name: "Saul Goodman", status: "alive" } + ] + ) +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_operations_with_TTL.graphql b/ballerina-tests/tests/resources/documents/server_cache_operations_with_TTL.graphql new file mode 100644 index 000000000..347569bff --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_operations_with_TTL.graphql @@ -0,0 +1,8 @@ +query A { + greet + name(id:1) +} + +mutation B { + updateName(name: "Heisenberg", enableEvict: false) +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_with_arrays.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_arrays.graphql new file mode 100644 index 000000000..493ae50a8 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_arrays.graphql @@ -0,0 +1,13 @@ +query A { + getFriendServices { + name + age + } +} + +mutation B { + updateFriend(name: "Tyler", age: 28, isMarried: true, enableEvict: false) { + name + age + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_with_dataloader.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_dataloader.graphql new file mode 100644 index 000000000..53848eb76 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_dataloader.graphql @@ -0,0 +1,26 @@ +query A { + authors(ids: [1, 2, 3]) { + name + books { + id + title + } + } +} + +mutation B { + 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/server_cache_with_dataloader_operational.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_dataloader_operational.graphql new file mode 100644 index 000000000..7e0792966 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_dataloader_operational.graphql @@ -0,0 +1,26 @@ +query A { + authors(ids: [1, 2, 3]) { + name + books { + id + title + } + } +} + +mutation B { + harari: updateAuthorName(id: 1, name: "Yual Noah Harari") { + name + books { + id + title + } + } + manson: updateAuthorName(id: 2, name: "Mark Manson") { + name + books { + id + title + } + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_with_dynamic_responses.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_dynamic_responses.graphql new file mode 100644 index 000000000..b991f1d98 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_dynamic_responses.graphql @@ -0,0 +1,44 @@ +query A { + user(id:1) { + id + } +} + +query B { + user(id:1) { + id + name + } +} + +query C { + user(id:1) { + id + name + age + } +} + +mutation D { + updateUser(id:1, name:"White", age:45, enableEvict:false) { + id + name + age + } +} + +mutation E { + updateUser(id:1, name:"White", age:45, enableEvict:true) { + id + name + age + } +} + +mutation F { + updateUser(id:1, name:"Walter", age:45, enableEvict:true) { + id + name + age + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_with_errors.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_errors.graphql new file mode 100644 index 000000000..ae9c0dcf9 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_errors.graphql @@ -0,0 +1,7 @@ +query A { + isAdult(age:null) +} + +query B { + isAdult(age:1) +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_with_errors_2.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_errors_2.graphql new file mode 100644 index 000000000..805ee6aaa --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_errors_2.graphql @@ -0,0 +1,13 @@ +query A { + getFriendService(name: "Lalo Salamanca") { + name + age + } +} + +mutation B { + addFriend(name: "Lalo Salamanca", age: 44, isMarried: false, enableEvict: false) { + name + age + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_with_input_object.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_input_object.graphql new file mode 100644 index 000000000..3cd54f189 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_input_object.graphql @@ -0,0 +1,12 @@ +query A { + searchEnemy(enemyInput: {name: "Enemy6", age: 14}) { + name + age + } +} + +mutation B { + removeEnemy(name: "Enemy6", enableEvict: false) { + name + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_with_inputs.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_inputs.graphql new file mode 100644 index 000000000..4feef8842 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_inputs.graphql @@ -0,0 +1,21 @@ +query A { + status(associates: null) +} + +query B { + status(associates: [ + { name: "Gus Fring", status: "dead"}, + { name: "Saul Goodman", status: "alive"} + ]) +} + +query C { + status(associates: [ + { name: "Gus Fring", status: "alive"}, + { name: "Saul Goodman", status: "alive"} + ]) +} + +query D { + status(associates: []) +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_with_interceptors.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_interceptors.graphql new file mode 100644 index 000000000..8bda3a925 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_interceptors.graphql @@ -0,0 +1,7 @@ +query A { + enemy +} + +mutation B { + updateEnemy(name: "Snape", enableEvict: false) +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_with_list_inputs.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_list_inputs.graphql new file mode 100644 index 000000000..521ac7b1b --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_list_inputs.graphql @@ -0,0 +1,10 @@ +query A ($names:[String!]!){ + isAllMarried(names: $names) +} + +mutation B { + updateEnemy(enemy: {name: "Enemy3", age: 44}, enableEvict: false) { + name + isMarried + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_with_null_values.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_null_values.graphql new file mode 100644 index 000000000..6a30b4d6f --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_null_values.graphql @@ -0,0 +1,12 @@ +query A { + searchEnemy(enemyInput: {}) { + name + age + } +} + +mutation B { + addEnemy2(name: "Enemy6", age:12, isMarried: false, enableEvict: false) { + name + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_with_nullable_inputs.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_nullable_inputs.graphql new file mode 100644 index 000000000..c529a74bc --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_nullable_inputs.graphql @@ -0,0 +1,13 @@ +query A { + enemies { + name + isMarried + } +} + +mutation B { + removeEnemy(name: "Enemy1", enableEvict: false) { + name + isMarried + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_with_partial_responses.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_partial_responses.graphql new file mode 100644 index 000000000..1e95f3327 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_partial_responses.graphql @@ -0,0 +1,17 @@ +query A { + getFriendService(name: "Jesse Pinkman") { + age + } +} + +mutation B { + updateFriend(name: "Jesse Pinkman", age: 45, isMarried: false, enableEvict: false) { + age + } +} + +mutation C { + updateFriend(name: "Jesse Pinkman", age: 45, isMarried: false, enableEvict: true) { + age + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_with_records.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_records.graphql new file mode 100644 index 000000000..fb4b8d8eb --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_records.graphql @@ -0,0 +1,13 @@ +query A { + friends(isMarried: true) { + name + age + } +} + +mutation B { + addFriend(name: "John", age: 30, isMarried: true) { + name + age + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_with_records_eviction.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_records_eviction.graphql new file mode 100644 index 000000000..6d1f62656 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_records_eviction.graphql @@ -0,0 +1,13 @@ +query A { + friends(isMarried: true) { + name + age + } +} + +mutation B { + addFriend(name: "Elliott Schwartz", age: 51, isMarried: true, enableEvict: true) { + name + age + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_with_records_operations.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_records_operations.graphql new file mode 100644 index 000000000..167d4b760 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_records_operations.graphql @@ -0,0 +1,13 @@ +query A { + friends(isMarried: true) { + name + age + } +} + +mutation B { + updateFriend(name: "Walter White Jr.", age: 25, isMarried: true, enableEvict: false) { + name + age + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_with_service_obj.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_service_obj.graphql new file mode 100644 index 000000000..5968570a0 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_service_obj.graphql @@ -0,0 +1,13 @@ +query A { + getFriendService(name: "Jesse Pinkman") { + name + age + } +} + +mutation B { + updateFriend(name: "Jesse Pinkman", age: 30, isMarried: false, enableEvict: false) { + name + age + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_with_union.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_union.graphql new file mode 100644 index 000000000..3f842a99c --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_union.graphql @@ -0,0 +1,15 @@ +query A { + getServices(isEnemy: true) { + ...on EnemyService { + name + age + } + } +} + +mutation B { + addEnemy(name: "Gandalf", age: 68, isMarried: false, enableEvict: false) { + name + age + } +} diff --git a/ballerina-tests/tests/resources/documents/server_cache_with_unions_operational_level.graphql b/ballerina-tests/tests/resources/documents/server_cache_with_unions_operational_level.graphql new file mode 100644 index 000000000..85d744583 --- /dev/null +++ b/ballerina-tests/tests/resources/documents/server_cache_with_unions_operational_level.graphql @@ -0,0 +1,32 @@ +query A { + relationship(name: "Gus Fring") { + ...FriendFragment + ...AssociateFragment + } +} + +mutation B { + updateAssociate(name: "Gus Fring", status: "alive", enableEvict: false) { + name + status + } +} + +mutation C { + updateAssociate(name: "Gus Fring", status: "alive", enableEvict: true) { + name + status + } +} + +fragment FriendFragment on FriendService { + name + age + isMarried +} + +fragment AssociateFragment on AssociateService { + name + status +} + diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_1.json new file mode 100644 index 000000000..ef576d03a --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_1.json @@ -0,0 +1,6 @@ +{ + "data": { + "greet": "Hello, Walter White", + "name": "Walter White" + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_10.json b/ballerina-tests/tests/resources/expected_results/server_cache_10.json new file mode 100644 index 000000000..0a02fb9b9 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_10.json @@ -0,0 +1,5 @@ +{ + "data": { + "updateName": "Heisenberg" + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_2.json new file mode 100644 index 000000000..bfb6f958d --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_2.json @@ -0,0 +1,5 @@ +{ + "data": { + "updateName": "John" + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_3.json b/ballerina-tests/tests/resources/expected_results/server_cache_3.json new file mode 100644 index 000000000..c62c98315 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_3.json @@ -0,0 +1,6 @@ +{ + "data": { + "greet": "Hello, John", + "name": "Walter White" + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_4.json b/ballerina-tests/tests/resources/expected_results/server_cache_4.json new file mode 100644 index 000000000..6168dc09b --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_4.json @@ -0,0 +1,6 @@ +{ + "data": { + "greet": "Hello, John", + "name": "John" + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_5.json b/ballerina-tests/tests/resources/expected_results/server_cache_5.json new file mode 100644 index 000000000..e710a09f1 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_5.json @@ -0,0 +1,5 @@ +{ + "data": { + "updateName": "Walter White" + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_6.json b/ballerina-tests/tests/resources/expected_results/server_cache_6.json new file mode 100644 index 000000000..deddf6613 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_6.json @@ -0,0 +1,9 @@ +{ + "data": { + "addFriend": { + "name": "Badger", + "age": 26, + "isMarried": false + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_7.json b/ballerina-tests/tests/resources/expected_results/server_cache_7.json new file mode 100644 index 000000000..89ee82e65 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_7.json @@ -0,0 +1,9 @@ +{ + "data": { + "addFriend": { + "name": "Pete", + "age": 27, + "isMarried": false + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_8.json b/ballerina-tests/tests/resources/expected_results/server_cache_8.json new file mode 100644 index 000000000..679b9418f --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_8.json @@ -0,0 +1,9 @@ +{ + "data": { + "addFriend": { + "name": "Gretchen Schwartz", + "age": 50, + "isMarried": true + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_9.json b/ballerina-tests/tests/resources/expected_results/server_cache_9.json new file mode 100644 index 000000000..f21f624b6 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_9.json @@ -0,0 +1,6 @@ +{ + "data": { + "greet": "Hello, Heisenberg", + "name": "Heisenberg" + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_TTL_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_TTL_1.json new file mode 100644 index 000000000..843b98e6c --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_TTL_1.json @@ -0,0 +1,5 @@ +{ + "data": { + "friend": "Harry" + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_TTL_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_TTL_2.json new file mode 100644 index 000000000..98ca7e850 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_TTL_2.json @@ -0,0 +1,5 @@ +{ + "data": { + "updateFriend": "Potter" + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_TTL_3.json b/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_TTL_3.json new file mode 100644 index 000000000..016420f29 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_TTL_3.json @@ -0,0 +1,5 @@ +{ + "data": { + "friend": "Potter" + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_list_inputs_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_list_inputs_1.json new file mode 100644 index 000000000..cd3c7569e --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_list_inputs_1.json @@ -0,0 +1,5 @@ +{ + "data": { + "isAllMarried": false + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_list_inputs_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_list_inputs_2.json new file mode 100644 index 000000000..952e90639 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_list_inputs_2.json @@ -0,0 +1,8 @@ +{ + "data": { + "updateEnemy": { + "name": "Enemy3", + "isMarried": true + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_list_inputs_3.json b/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_list_inputs_3.json new file mode 100644 index 000000000..cd225b367 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_list_inputs_3.json @@ -0,0 +1,5 @@ +{ + "data": { + "isAllMarried": true + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_nullable_inputs_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_nullable_inputs_1.json new file mode 100644 index 000000000..5d89b1b20 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_nullable_inputs_1.json @@ -0,0 +1,14 @@ +{ + "data": { + "enemies": [ + { + "name": "Enemy2", + "isMarried": true + }, + { + "name": "Enemy3", + "isMarried": false + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_nullable_inputs_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_nullable_inputs_2.json new file mode 100644 index 000000000..c921f0f4b --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_nullable_inputs_2.json @@ -0,0 +1,8 @@ +{ + "data": { + "removeEnemy": { + "name": "Enemy2", + "isMarried": true + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_nullable_inputs_3.json b/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_nullable_inputs_3.json new file mode 100644 index 000000000..5d89b1b20 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_eviction_with_nullable_inputs_3.json @@ -0,0 +1,14 @@ +{ + "data": { + "enemies": [ + { + "name": "Enemy2", + "isMarried": true + }, + { + "name": "Enemy3", + "isMarried": false + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_operation_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_operation_1.json new file mode 100644 index 000000000..6a4ee8a63 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_operation_1.json @@ -0,0 +1,6 @@ +{ + "data": { + "greet": "Hello, Walter White", + "name": "Walter White" + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_1.json new file mode 100644 index 000000000..4d8b6eccd --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_1.json @@ -0,0 +1,22 @@ +{ + "data": { + "getFriendServices": [ + { + "name": "Skyler", + "age": 45 + }, + { + "name": "Walter White Jr.", + "age": 57 + }, + { + "name": "Jesse Pinkman", + "age": 30 + }, + { + "name": "John", + "age": 30 + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_2.json new file mode 100644 index 000000000..20470abc1 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_2.json @@ -0,0 +1,8 @@ +{ + "data": { + "updateFriend": { + "name": "Tyler", + "age": 28 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_3.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_3.json new file mode 100644 index 000000000..4d8b6eccd --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_3.json @@ -0,0 +1,22 @@ +{ + "data": { + "getFriendServices": [ + { + "name": "Skyler", + "age": 45 + }, + { + "name": "Walter White Jr.", + "age": 57 + }, + { + "name": "Jesse Pinkman", + "age": 30 + }, + { + "name": "John", + "age": 30 + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_4.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_4.json new file mode 100644 index 000000000..c82e664c6 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_4.json @@ -0,0 +1,26 @@ +{ + "data": { + "getFriendServices": [ + { + "name": "Skyler", + "age": 45 + }, + { + "name": "Walter White Jr.", + "age": 57 + }, + { + "name": "Jesse Pinkman", + "age": 30 + }, + { + "name": "John", + "age": 30 + }, + { + "name": "Tyler", + "age": 28 + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_5.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_5.json new file mode 100644 index 000000000..e4612f090 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_5.json @@ -0,0 +1,22 @@ +{ + "data": { + "getFriendServices": [ + { + "name": "Skyler", + "age": 45 + }, + { + "name": "Walter White Jr.", + "age": 25 + }, + { + "name": "Jesse Pinkman", + "age": 30 + }, + { + "name": "Elliott Schwartz", + "age": 51 + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_6.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_6.json new file mode 100644 index 000000000..2002f9872 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_6.json @@ -0,0 +1,27 @@ + +{ + "data": { + "getFriendServices": [ + { + "name": "Skyler", + "age": 45 + }, + { + "name": "Walter White Jr.", + "age": 25 + }, + { + "name": "Jesse Pinkman", + "age": 30 + }, + { + "name": "Elliott Schwartz", + "age": 51 + }, + { + "name": "Tyler", + "age": 28 + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_7.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_7.json new file mode 100644 index 000000000..e4612f090 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_arrays_7.json @@ -0,0 +1,22 @@ +{ + "data": { + "getFriendServices": [ + { + "name": "Skyler", + "age": 45 + }, + { + "name": "Walter White Jr.", + "age": 25 + }, + { + "name": "Jesse Pinkman", + "age": 30 + }, + { + "name": "Elliott Schwartz", + "age": 51 + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_dataloader_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_dataloader_1.json new file mode 100644 index 000000000..12e747142 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_dataloader_1.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/resources/expected_results/server_cache_with_dataloader_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_dataloader_2.json new file mode 100644 index 000000000..cf94e5453 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_dataloader_2.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/server_cache_with_dataloader_3.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_dataloader_3.json new file mode 100644 index 000000000..1ea495104 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_dataloader_3.json @@ -0,0 +1,49 @@ +{ + "data": { + "authors": [ + { + "name": "Sabthar", + "books": [ + { + "id": 1, + "title": "Book 1" + }, + { + "id": 2, + "title": "Book 2" + }, + { + "id": 3, + "title": "Book 3" + } + ] + }, + { + "name": "Mahroof", + "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/resources/expected_results/server_cache_with_dataloader_4.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_dataloader_4.json new file mode 100644 index 000000000..1d9976f5d --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_dataloader_4.json @@ -0,0 +1,49 @@ +{ + "data": { + "authors": [ + { + "name": "Yual Noah Harari", + "books": [ + { + "id": 1, + "title": "Book 1" + }, + { + "id": 2, + "title": "Book 2" + }, + { + "id": 3, + "title": "Book 3" + } + ] + }, + { + "name": "Mark Manson", + "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/resources/expected_results/server_cache_with_dataloader_5.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_dataloader_5.json new file mode 100644 index 000000000..c0bd69bf7 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_dataloader_5.json @@ -0,0 +1,34 @@ +{ + "data": { + "harari": { + "name": "Yual Noah Harari", + "books": [ + { + "id": 1, + "title": "Book 1" + }, + { + "id": 2, + "title": "Book 2" + }, + { + "id": 3, + "title": "Book 3" + } + ] + }, + "manson": { + "name": "Mark Manson", + "books": [ + { + "id": 4, + "title": "Book 4" + }, + { + "id": 5, + "title": "Book 5" + } + ] + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_1.json new file mode 100644 index 000000000..2b2795e33 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_1.json @@ -0,0 +1,7 @@ +{ + "data": { + "user": { + "id": 1 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_2.json new file mode 100644 index 000000000..11f024125 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_2.json @@ -0,0 +1,8 @@ +{ + "data": { + "user": { + "id": 1, + "name": "John" + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_3.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_3.json new file mode 100644 index 000000000..2b351b638 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_3.json @@ -0,0 +1,9 @@ +{ + "data": { + "user": { + "id": 1, + "name": "John", + "age": 25 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_4.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_4.json new file mode 100644 index 000000000..466e196a7 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_4.json @@ -0,0 +1,9 @@ +{ + "data": { + "updateUser": { + "id": 1, + "name": "White", + "age": 45 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_5.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_5.json new file mode 100644 index 000000000..2100dc760 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_5.json @@ -0,0 +1,9 @@ +{ + "data": { + "user": { + "id": 1, + "name": "White", + "age": 45 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_6.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_6.json new file mode 100644 index 000000000..ecb46c363 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_6.json @@ -0,0 +1,8 @@ +{ + "data": { + "user": { + "id": 1, + "name": "White" + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_7.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_7.json new file mode 100644 index 000000000..1dc51f786 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_7.json @@ -0,0 +1,9 @@ +{ + "data": { + "updateUser": { + "id": 1, + "name": "Walter", + "age": 45 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_8.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_8.json new file mode 100644 index 000000000..b6dca40cd --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_dynamic_responses_8.json @@ -0,0 +1,8 @@ +{ + "data": { + "user": { + "id": 1, + "name": "Walter" + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_empty_input_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_empty_input_1.json new file mode 100644 index 000000000..de2cc3a4e --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_empty_input_1.json @@ -0,0 +1,5 @@ +{ + "data": { + "status": ["dead", "alive"] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_empty_input_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_empty_input_2.json new file mode 100644 index 000000000..962799bbd --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_empty_input_2.json @@ -0,0 +1,5 @@ +{ + "data": { + "status": [] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_errors_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_errors_1.json new file mode 100644 index 000000000..1ff6f8f35 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_errors_1.json @@ -0,0 +1,17 @@ +{ + "errors": [ + { + "message": "Invalid argument type", + "locations": [ + { + "line": 2, + "column": 5 + } + ], + "path": [ + "isAdult" + ] + } + ], + "data": null +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_errors_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_errors_2.json new file mode 100644 index 000000000..692ac9148 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_errors_2.json @@ -0,0 +1,5 @@ +{ + "data": { + "isAdult": false + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_errors_3.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_errors_3.json new file mode 100644 index 000000000..1d6dd877f --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_errors_3.json @@ -0,0 +1,8 @@ +{ + "data": { + "getFriendService": { + "name": "Lalo Salamanca", + "age": 44 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_errors_4.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_errors_4.json new file mode 100644 index 000000000..2a7a05b12 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_errors_4.json @@ -0,0 +1,15 @@ +{ + "errors":[ + { + "message":"No person found with the name: Lalo Salamanca", + "locations":[ + { + "line":2, + "column":5 + } + ], + "path": ["getFriendService"] + } + ], + "data": null +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_errors_5.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_errors_5.json new file mode 100644 index 000000000..566f6b3d7 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_errors_5.json @@ -0,0 +1,8 @@ +{ + "data": { + "addFriend": { + "name": "Lalo Salamanca", + "age": 44 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_input_object_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_input_object_1.json new file mode 100644 index 000000000..45ebc9d1e --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_input_object_1.json @@ -0,0 +1,8 @@ +{ + "data": { + "searchEnemy": { + "name": "Enemy6", + "age": 12 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_input_object_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_input_object_2.json new file mode 100644 index 000000000..8d7496a71 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_input_object_2.json @@ -0,0 +1,7 @@ +{ + "data": { + "removeEnemy": { + "name": "Enemy6" + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_input_object_3.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_input_object_3.json new file mode 100644 index 000000000..45ebc9d1e --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_input_object_3.json @@ -0,0 +1,8 @@ +{ + "data": { + "searchEnemy": { + "name": "Enemy6", + "age": 12 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_interceptors_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_interceptors_1.json new file mode 100644 index 000000000..9c6746a94 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_interceptors_1.json @@ -0,0 +1,5 @@ +{ + "data": { + "enemy": "Tom Marvolo Riddle - voldemort" + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_interceptors_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_interceptors_2.json new file mode 100644 index 000000000..e7c68ef92 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_interceptors_2.json @@ -0,0 +1,5 @@ +{ + "data": { + "updateEnemy": "Snape" + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_interceptors_3.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_interceptors_3.json new file mode 100644 index 000000000..211a99c20 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_interceptors_3.json @@ -0,0 +1,5 @@ +{ + "data": { + "enemy": "Tom Marvolo Riddle - Snape" + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_null_values_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_null_values_1.json new file mode 100644 index 000000000..c910c4822 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_null_values_1.json @@ -0,0 +1,5 @@ +{ + "data": { + "searchEnemy": null + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_null_values_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_null_values_2.json new file mode 100644 index 000000000..813367d60 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_null_values_2.json @@ -0,0 +1,7 @@ +{ + "data": { + "addEnemy2": { + "name": "Enemy6" + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_null_values_3.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_null_values_3.json new file mode 100644 index 000000000..45ebc9d1e --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_null_values_3.json @@ -0,0 +1,8 @@ +{ + "data": { + "searchEnemy": { + "name": "Enemy6", + "age": 12 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_1.json new file mode 100644 index 000000000..cf8598900 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_1.json @@ -0,0 +1,18 @@ +{ + "data": { + "enemies": [ + { + "name": "Enemy1", + "isMarried": false + }, + { + "name": "Enemy2", + "isMarried": true + }, + { + "name": "Enemy3", + "isMarried": false + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_2.json new file mode 100644 index 000000000..4e92a3a0f --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_2.json @@ -0,0 +1,8 @@ +{ + "data": { + "removeEnemy": { + "name": "Enemy1", + "isMarried": false + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_3.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_3.json new file mode 100644 index 000000000..5d89b1b20 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_3.json @@ -0,0 +1,14 @@ +{ + "data": { + "enemies": [ + { + "name": "Enemy2", + "isMarried": true + }, + { + "name": "Enemy3", + "isMarried": false + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_4.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_4.json new file mode 100644 index 000000000..c921f0f4b --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_4.json @@ -0,0 +1,8 @@ +{ + "data": { + "removeEnemy": { + "name": "Enemy2", + "isMarried": true + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_5.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_5.json new file mode 100644 index 000000000..422c391d3 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_5.json @@ -0,0 +1,10 @@ +{ + "data": { + "enemies": [ + { + "name": "Enemy3", + "isMarried": false + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_6.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_6.json new file mode 100644 index 000000000..6bd22b280 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_6.json @@ -0,0 +1,5 @@ +{ + "data": { + "status": null + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_7.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_7.json new file mode 100644 index 000000000..6a34891b3 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_nullable_inputs_7.json @@ -0,0 +1,5 @@ +{ + "data": { + "status": ["dead", "alive"] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_partial_reponses_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_partial_reponses_1.json new file mode 100644 index 000000000..c177d9d36 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_partial_reponses_1.json @@ -0,0 +1,7 @@ +{ + "data": { + "getFriendService": { + "age": 30 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_partial_reponses_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_partial_reponses_2.json new file mode 100644 index 000000000..e6b5519ff --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_partial_reponses_2.json @@ -0,0 +1,7 @@ +{ + "data": { + "updateFriend": { + "age": 45 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_partial_reponses_3.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_partial_reponses_3.json new file mode 100644 index 000000000..d38e33da6 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_partial_reponses_3.json @@ -0,0 +1,7 @@ +{ + "data": { + "getFriendService": { + "age": 45 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_rec_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_rec_1.json new file mode 100644 index 000000000..47e2c9b67 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_rec_1.json @@ -0,0 +1,14 @@ +{ + "data": { + "friends": [ + { + "name": "Skyler", + "age": 45 + }, + { + "name": "Walter White Jr.", + "age": 57 + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_rec_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_rec_2.json new file mode 100644 index 000000000..683b0567d --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_rec_2.json @@ -0,0 +1,8 @@ +{ + "data": { + "addFriend": { + "name": "John", + "age": 30 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_rec_3.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_rec_3.json new file mode 100644 index 000000000..9712c6d21 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_rec_3.json @@ -0,0 +1,8 @@ +{ + "data": { + "updateFriend": { + "name": "Walter White Jr.", + "age": 25 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_rec_4.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_rec_4.json new file mode 100644 index 000000000..aea7d6176 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_rec_4.json @@ -0,0 +1,18 @@ +{ + "data": { + "friends": [ + { + "name": "Skyler", + "age": 45 + }, + { + "name": "Walter White Jr.", + "age": 25 + }, + { + "name": "Elliott Schwartz", + "age": 51 + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_rec_5.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_rec_5.json new file mode 100644 index 000000000..8a61e7e63 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_rec_5.json @@ -0,0 +1,8 @@ +{ + "data": { + "addFriend": { + "name": "Elliott Schwartz", + "age": 51 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_svc_obj_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_svc_obj_1.json new file mode 100644 index 000000000..6a4380041 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_svc_obj_1.json @@ -0,0 +1,8 @@ +{ + "data": { + "getFriendService": { + "name": "Jesse Pinkman", + "age": 23 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_svc_obj_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_svc_obj_2.json new file mode 100644 index 000000000..d65b2a8b1 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_svc_obj_2.json @@ -0,0 +1,8 @@ +{ + "data": { + "updateFriend": { + "name": "Jesse Pinkman", + "age": 30 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_svc_obj_3.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_svc_obj_3.json new file mode 100644 index 000000000..d63882076 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_svc_obj_3.json @@ -0,0 +1,8 @@ +{ + "data": { + "getFriendService": { + "name": "Jesse Pinkman", + "age": 30 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_ttl_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_ttl_1.json new file mode 100644 index 000000000..fa3324751 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_ttl_1.json @@ -0,0 +1,26 @@ +{ + "data": { + "getAllFriendServices": [ + { + "name": "Skyler", + "age": 45 + }, + { + "name": "Walter White Jr.", + "age": 25 + }, + { + "name": "Jesse Pinkman", + "age": 30 + }, + { + "name": "Elliott Schwartz", + "age": 51 + }, + { + "name": "Tyler", + "age": 28 + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_ttl_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_ttl_2.json new file mode 100644 index 000000000..c82e664c6 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_ttl_2.json @@ -0,0 +1,26 @@ +{ + "data": { + "getFriendServices": [ + { + "name": "Skyler", + "age": 45 + }, + { + "name": "Walter White Jr.", + "age": 57 + }, + { + "name": "Jesse Pinkman", + "age": 30 + }, + { + "name": "John", + "age": 30 + }, + { + "name": "Tyler", + "age": 28 + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_union_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_union_1.json new file mode 100644 index 000000000..2e45e8dbd --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_union_1.json @@ -0,0 +1,10 @@ +{ + "data": { + "getServices": [ + { + "name": "Jesse Pinkman", + "age": 30 + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_union_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_union_2.json new file mode 100644 index 000000000..205d7d0d1 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_union_2.json @@ -0,0 +1,8 @@ +{ + "data": { + "addEnemy": { + "name": "Gandalf", + "age": 68 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_union_3.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_union_3.json new file mode 100644 index 000000000..fa28ba6c2 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_union_3.json @@ -0,0 +1,8 @@ +{ + "data": { + "addEnemy": { + "name": "Jake", + "age": 22 + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_union_4.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_union_4.json new file mode 100644 index 000000000..202d1d960 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_union_4.json @@ -0,0 +1,18 @@ +{ + "data": { + "getServices": [ + { + "name": "Jesse Pinkman", + "age": 30 + }, + { + "name": "Gandalf", + "age": 68 + }, + { + "name": "Jake", + "age": 22 + } + ] + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_unions_1.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_unions_1.json new file mode 100644 index 000000000..966f09c7d --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_unions_1.json @@ -0,0 +1,8 @@ +{ + "data": { + "relationship": { + "name": "Gus Fring", + "status": "dead" + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_unions_2.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_unions_2.json new file mode 100644 index 000000000..f6e414bf5 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_unions_2.json @@ -0,0 +1,8 @@ +{ + "data": { + "updateAssociate": { + "name": "Gus Fring", + "status": "alive" + } + } +} diff --git a/ballerina-tests/tests/resources/expected_results/server_cache_with_unions_3.json b/ballerina-tests/tests/resources/expected_results/server_cache_with_unions_3.json new file mode 100644 index 000000000..ee55131d8 --- /dev/null +++ b/ballerina-tests/tests/resources/expected_results/server_cache_with_unions_3.json @@ -0,0 +1,8 @@ +{ + "data": { + "relationship": { + "name": "Gus Fring", + "status": "alive" + } + } +} diff --git a/ballerina-tests/tests/test_services.bal b/ballerina-tests/tests/test_services.bal index 593309d4e..c995552c6 100644 --- a/ballerina-tests/tests/test_services.bal +++ b/ballerina-tests/tests/test_services.bal @@ -2491,3 +2491,512 @@ class ServiceDeclarationOnObjectField { } } + +service /server_cache on basicListener { + private string name = "Walter White"; + private table key(name) friends = table [ + {name: "Skyler", age: 45, isMarried: true}, + {name: "Walter White Jr.", age: 57, isMarried: true}, + {name: "Jesse Pinkman", age: 23, isMarried: false} + ]; + + private table key(name) enemies = table [ + {name: "Enemy1", age:12, isMarried: false}, + {name: "Enemy2", age:66, isMarried: true}, + {name: "Enemy3", age:33, isMarried: false} + ]; + + isolated resource function get greet() returns string { + return "Hello, " + self.name; + } + + @graphql:ResourceConfig { + cacheConfig: { + enabled: true + } + } + isolated resource function get isAdult(int? age) returns boolean|error { + if age is int { + return age >= 18 ? true: false; + } + return error("Invalid argument type"); + } + + @graphql:ResourceConfig { + cacheConfig: { + enabled: true + } + } + isolated resource function get name(int id) returns string { + return self.name; + } + + @graphql:ResourceConfig { + cacheConfig: { + maxAge: 120 + } + } + isolated resource function get friends(boolean isMarried = false) returns Friend[] { + if isMarried { + return from Friend friend in self.friends + where friend.isMarried == true + select friend; + } + return from Friend friend in self.friends + where friend.isMarried == false + select friend; + } + + isolated resource function get getFriendService(string name) returns FriendService { + Friend[] person = from Friend friend in self.friends + where friend.name == name + select friend; + return new FriendService(person[0].name, person[0].age, person[0].isMarried); + } + + @graphql:ResourceConfig { + cacheConfig: { + maxAge: 180 + } + } + isolated resource function get getFriendServices() returns FriendService[] { + return from Friend friend in self.friends + select new FriendService(friend.name, friend.age, friend.isMarried); + } + + @graphql:ResourceConfig { + cacheConfig: { + maxAge: 180 + } + } + isolated resource function get getServices(boolean isEnemy) returns HumanService[] { + if isEnemy { + return from Friend friend in self.friends + where friend.isMarried == false + select new EnemyService(friend.name, friend.age, friend.isMarried); + } + return from Friend friend in self.friends + where friend.isMarried == true + select new FriendService(friend.name, friend.age, friend.isMarried); + } + + @graphql:ResourceConfig { + cacheConfig: { + maxAge: 120 + } + } + isolated resource function get enemies(boolean? isMarried = ()) returns Enemy[] { + if isMarried is () { + return from Enemy enemy in self.enemies + select enemy; + } + return from Enemy enemy in self.enemies + where enemy.isMarried == isMarried + select enemy; + } + + @graphql:ResourceConfig { + cacheConfig: { + enabled: true + } + } + isolated resource function get isAllMarried(string[] names) returns boolean { + if names is string[] { + foreach string name in names { + if self.enemies.hasKey(name) && !self.enemies.get(name).isMarried { + return false; + } + } + } + return true; + } + + @graphql:ResourceConfig { + cacheConfig: { + enabled: true + } + } + isolated resource function get searchEnemy(EnemyInput enemyInput) returns Enemy? { + if self.enemies.hasKey(enemyInput.name) { + return self.enemies.get(enemyInput.name); + } + return; + } + + isolated remote function updateName(graphql:Context context, string name, boolean enableEvict) returns string|error { + if enableEvict { + check context.invalidate("name"); + } + self.name = name; + return self.name; + } + + isolated remote function updateFriend(graphql:Context context, string name, int age, boolean isMarried, boolean enableEvict) returns FriendService|error { + if enableEvict { + check context.invalidateAll(); + } + self.friends.put({name: name, age: age, isMarried: isMarried}); + return new FriendService(name, age, isMarried); + } + + isolated remote function addFriend(string name, int age, boolean isMarried) returns Friend { + Friend friend = {name: name, age: age, isMarried: isMarried}; + self.friends.add(friend); + return friend; + } + + isolated remote function addEnemy(graphql:Context context, string name, int age, boolean isMarried, boolean enableEvict) returns Friend|error { + if enableEvict { + check context.invalidate("getServices"); + } + Friend friend = {name: name, age: age, isMarried: isMarried}; + self.friends.add(friend); + return friend; + } + + isolated remote function addEnemy2(graphql:Context context, string name, int age, boolean isMarried, boolean enableEvict) returns Enemy|error { + if enableEvict { + check context.invalidate("enemies"); + } + Enemy enemy = {name: name, age: age, isMarried: isMarried}; + self.enemies.add(enemy); + return enemy; + } + + isolated remote function removeEnemy(graphql:Context context, string name, boolean enableEvict) returns Enemy|error { + if enableEvict { + check context.invalidate("enemies"); + } + return self.enemies.remove(name); + } + + isolated remote function updateEnemy(graphql:Context context, EnemyInput enemy, boolean enableEvict) returns Enemy|error { + if enableEvict { + check context.invalidateAll(); + } + Enemy enemyInput = {name: enemy.name, age: enemy.age, isMarried: true}; + self.enemies.put(enemyInput); + return enemyInput; + } +} + +service /evict_server_cache on basicListener { + private string name = "Walter White"; + isolated resource function get greet() returns string { + return "Hello, " + self.name; + } + + @graphql:ResourceConfig { + cacheConfig: { + enabled: true + } + } + isolated resource function get name(int id) returns string { + return self.name; + } + + isolated remote function updateName(graphql:Context context, string name) returns string|error { + check context.invalidate("name"); + self.name = name; + return self.name; + } +} + +@graphql:ServiceConfig { + cacheConfig: { + enabled: true, + maxAge: 20, + maxSize: 15 + }, + contextInit: initContext +} +service /server_cache_operations on basicListener { + private string name = "Walter White"; + private table key(name) friends = table [ + {name: "Skyler", age: 45, isMarried: true}, + {name: "Walter White Jr.", age: 57, isMarried: true}, + {name: "Jesse Pinkman", age: 23, isMarried: false} + ]; + + private table key(name) associates = table [ + {name: "Gus Fring", status: "dead"}, + {name: "Tuco Salamanca", status: "dead"}, + {name: "Saul Goodman", status: "alive"} + ]; + + isolated resource function get greet() returns string { + return "Hello, " + self.name; + } + + isolated resource function get name(int id) returns string { + return self.name; + } + + isolated resource function get friends(boolean isMarried = false) returns Friend[] { + if isMarried { + return from Friend friend in self.friends + where friend.isMarried == true + select friend; + } + return from Friend friend in self.friends + where friend.isMarried == false + select friend; + } + + isolated resource function get getFriendService(string name) returns FriendService|error { + Friend[] person = from Friend friend in self.friends + where friend.name == name + select friend; + if person != [] { + return new FriendService(person[0].name, person[0].age, person[0].isMarried); + } else { + return error(string `No person found with the name: ${name}`); + } + } + + isolated resource function get getAssociateService(string name) returns AssociateService { + Associate[] person = from Associate associate in self.associates + where associate.name == name + select associate; + return new AssociateService(person[0].name, person[0].status); + } + + isolated resource function get relationship(string name) returns Relationship { + (Associate|Friend)[] person = from Associate associate in self.associates + where associate.name == name + select associate; + if person.length() == 0 { + person = from Friend friend in self.friends + where friend.name == name + select friend; + return new FriendService(person[0].name, (person[0]).age, (person[0]).isMarried); + } + return new AssociateService(person[0].name, (person[0]).status); + } + + isolated resource function get getFriendServices() returns FriendService[] { + return from Friend friend in self.friends + select new FriendService(friend.name, friend.age, friend.isMarried); + } + + isolated resource function get getAllFriendServices(graphql:Context context, boolean enableEvict) returns FriendService[]|error { + if enableEvict { + check context.invalidateAll(); + } + return from Friend friend in self.friends + select new FriendService(friend.name, friend.age, friend.isMarried); + } + + isolated remote function updateName(graphql:Context context, string name, boolean enableEvict) returns string|error { + if enableEvict { + check context.invalidateAll(); + } + self.name = name; + return self.name; + } + + isolated remote function updateFriend(graphql:Context context, string name, int age, boolean isMarried, boolean enableEvict) returns FriendService|error { + if enableEvict { + check context.invalidateAll(); + } + self.friends.put({name: name, age: age, isMarried: isMarried}); + return new FriendService(name, age, isMarried); + } + + isolated remote function updateAssociate(graphql:Context context, string name, string status, boolean enableEvict) returns AssociateService|error { + if enableEvict { + check context.invalidateAll(); + } + self.associates.put({name: name, status: status}); + return new AssociateService(name, status); + } + + isolated remote function addFriend(graphql:Context context, string name, int age, boolean isMarried, boolean enableEvict) returns Friend|error { + if enableEvict { + check context.invalidate("getFriendService"); + check context.invalidate("getAllFriendServices"); + check context.invalidate("friends"); + } + Friend friend = {name: name, age: age, isMarried: isMarried}; + self.friends.add(friend); + return friend; + } + + resource function get status(Associate[]? associates) returns string[]? { + if associates is Associate[] { + return associates.map(associate => associate.status); + } + return; + } +} + +const AUTHOR_LOADER_2 = "authorLoader2"; +const BOOK_LOADER_2 = "bookLoader2"; + +isolated function initContext2(http:RequestContext requestContext, http:Request request) returns graphql:Context|error { + graphql:Context ctx = new; + ctx.registerDataLoader(AUTHOR_LOADER_2, new dataloader:DefaultDataLoader(authorLoaderFunction2)); + ctx.registerDataLoader(BOOK_LOADER_2, new dataloader:DefaultDataLoader(bookLoaderFunction2)); + return ctx; +} + +function addAuthorIdsToAuthorLoader2(graphql:Context ctx, int[] ids) { + dataloader:DataLoader authorLoader = ctx.getDataLoader(AUTHOR_LOADER_2); + ids.forEach(function(int id) { + authorLoader.add(id); + }); +} + +@graphql:ServiceConfig { + contextInit: initContext2 +} +service /caching_with_dataloader on wrappedListener { + function preAuthors(graphql:Context ctx, int[] ids) { + addAuthorIdsToAuthorLoader2(ctx, ids); + } + + @graphql:ResourceConfig { + cacheConfig:{ + enabled: true + } + } + resource function get authors(graphql:Context ctx, int[] ids) returns AuthorData2[]|error { + dataloader:DataLoader authorLoader = ctx.getDataLoader(AUTHOR_LOADER_2); + AuthorRow[] authorRows = check trap ids.map(id => check authorLoader.get(id, AuthorRow)); + return from AuthorRow authorRow in authorRows + select new (authorRow); + } + + isolated remote function updateAuthorName(graphql:Context ctx, int id, string name, boolean enableEvict = false) returns AuthorData2|error { + if enableEvict { + check ctx.invalidate("authors"); + } + AuthorRow authorRow = {id: id, name}; + lock { + authorTable2.put(authorRow.cloneReadOnly()); + } + return new (authorRow); + } +} + +@graphql:ServiceConfig { + cacheConfig:{ + enabled: true + }, + contextInit: initContext2 +} +service /caching_with_dataloader_operational on wrappedListener { + function preAuthors(graphql:Context ctx, int[] ids) { + addAuthorIdsToAuthorLoader2(ctx, ids); + } + + resource function get authors(graphql:Context ctx, int[] ids) returns AuthorData2[]|error { + dataloader:DataLoader authorLoader = ctx.getDataLoader(AUTHOR_LOADER_2); + AuthorRow[] authorRows = check trap ids.map(id => check authorLoader.get(id, AuthorRow)); + return from AuthorRow authorRow in authorRows + select new (authorRow); + } + + isolated remote function updateAuthorName(graphql:Context ctx, int id, string name, boolean enableEvict = false) returns AuthorData2|error { + if enableEvict { + check ctx.invalidate("authors"); + } + AuthorRow authorRow = {id: id, name}; + lock { + authorTable2.put(authorRow.cloneReadOnly()); + } + return new (authorRow); + } +} + +service /field_caching_with_interceptors on basicListener { + private string enemy = "voldemort"; + private string friend = "Harry"; + + @graphql:ResourceConfig { + interceptors: [new StringInterceptor1(), new StringInterceptor2(), new StringInterceptor3()], + cacheConfig:{ + enabled: true, + maxAge: 15 + } + } + resource function get enemy() returns string { + return self.enemy; + } + + @graphql:ResourceConfig { + cacheConfig:{ + enabled: true, + maxAge: 10 + } + } + resource function get friend() returns string { + return self.friend; + } + + remote function updateEnemy(graphql:Context context, string name, boolean enableEvict) returns string|error { + if enableEvict { + check context.invalidate("enemy"); + } + self.enemy = name; + return self.enemy; + } + + remote function updateFriend(string name) returns string|error { + self.friend = name; + return self.friend; + } +} + +@graphql:ServiceConfig { + cacheConfig:{ + enabled: true + } +} +service /caching_with_interceptor_operations on basicListener { + private string name = "voldemort"; + + @graphql:ResourceConfig { + interceptors: [new StringInterceptor1(), new StringInterceptor2(), new StringInterceptor3()] + } + resource function get enemy() returns string { + return self.name; + } + + remote function updateEnemy(graphql:Context context, string name, boolean enableEvict) returns string|error { + if enableEvict { + check context.invalidate("enemy"); + } + self.name = name; + return self.name; + } +} + +service /dynamic_response on basicListener { + private User user = {id: 1, name: "John", age: 25}; + + @graphql:ResourceConfig { + cacheConfig: { + enabled: true, + maxAge: 600 + } + } + resource function get user(graphql:Field 'field, int id) returns User { + string[] sub = 'field.getSubfieldNames(); + if sub.length() == 1 { + return {id: self.user.id}; + }else if sub.length() == 2 { + return {id: self.user.id, name: self.user.name}; + } else { + return self.user; + } + } + + remote function updateUser(graphql:Context context, int id, string name, int age, boolean enableEvict) returns User|error { + if enableEvict { + check context.invalidate("user"); + } + self.user = {id:id, name:name, age:age}; + return self.user; + } +} diff --git a/ballerina-tests/tests/values.bal b/ballerina-tests/tests/values.bal index f7ddb48dd..85333b483 100644 --- a/ballerina-tests/tests/values.bal +++ b/ballerina-tests/tests/values.bal @@ -296,3 +296,23 @@ isolated int dispatchCountOfAuthorLoader = 0; isolated int dispatchCountOfUpdateAuthorLoader = 0; const int DEFAULT_INT_VALUE = 20; + +final isolated table key(id) authorTable2 = 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) bookTable2 = 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} +]; diff --git a/ballerina/Ballerina.toml b/ballerina/Ballerina.toml index 91fe0e75b..c14f61630 100644 --- a/ballerina/Ballerina.toml +++ b/ballerina/Ballerina.toml @@ -1,7 +1,7 @@ [package] org = "ballerina" name = "graphql" -version = "1.10.1" +version = "1.11.0" authors = ["Ballerina"] export=["graphql", "graphql.subgraph", "graphql.dataloader"] keywords = ["gql", "network", "query", "service"] @@ -16,11 +16,11 @@ graalvmCompatible = true [[platform.java17.dependency]] groupId = "io.ballerina.stdlib" artifactId = "graphql-native" -version = "1.10.1" -path = "../native/build/libs/graphql-native-1.10.1-SNAPSHOT.jar" +version = "1.11.0" +path = "../native/build/libs/graphql-native-1.11.0-SNAPSHOT.jar" [[platform.java17.dependency]] groupId = "io.ballerina.stdlib" artifactId = "graphql-commons" -version = "1.10.1" -path = "../commons/build/libs/graphql-commons-1.10.1-SNAPSHOT.jar" +version = "1.11.0" +path = "../commons/build/libs/graphql-commons-1.11.0-SNAPSHOT.jar" diff --git a/ballerina/CompilerPlugin.toml b/ballerina/CompilerPlugin.toml index 3ec76ed4a..32ece7c09 100644 --- a/ballerina/CompilerPlugin.toml +++ b/ballerina/CompilerPlugin.toml @@ -3,7 +3,7 @@ id = "graphql-compiler-plugin" class = "io.ballerina.stdlib.graphql.compiler.GraphqlCompilerPlugin" [[dependency]] -path = "../compiler-plugin/build/libs/graphql-compiler-plugin-1.10.1-SNAPSHOT.jar" +path = "../compiler-plugin/build/libs/graphql-compiler-plugin-1.11.0-SNAPSHOT.jar" [[dependency]] -path = "../commons/build/libs/graphql-commons-1.10.1-SNAPSHOT.jar" +path = "../commons/build/libs/graphql-commons-1.11.0-SNAPSHOT.jar" diff --git a/ballerina/Dependencies.toml b/ballerina/Dependencies.toml index e23833369..1b645fb20 100644 --- a/ballerina/Dependencies.toml +++ b/ballerina/Dependencies.toml @@ -32,11 +32,14 @@ dependencies = [ {org = "ballerina", name = "task"}, {org = "ballerina", name = "time"} ] +modules = [ + {org = "ballerina", packageName = "cache", moduleName = "cache"} +] [[package]] org = "ballerina" name = "constraint" -version = "1.4.0" +version = "1.5.0" dependencies = [ {org = "ballerina", name = "jballerina.java"} ] @@ -44,11 +47,14 @@ dependencies = [ [[package]] org = "ballerina" name = "crypto" -version = "2.5.0" +version = "2.6.2" dependencies = [ {org = "ballerina", name = "jballerina.java"}, {org = "ballerina", name = "time"} ] +modules = [ + {org = "ballerina", packageName = "crypto", moduleName = "crypto"} +] [[package]] org = "ballerina" @@ -67,9 +73,11 @@ modules = [ [[package]] org = "ballerina" name = "graphql" -version = "1.10.1" +version = "1.11.0" dependencies = [ {org = "ballerina", name = "auth"}, + {org = "ballerina", name = "cache"}, + {org = "ballerina", name = "crypto"}, {org = "ballerina", name = "file"}, {org = "ballerina", name = "http"}, {org = "ballerina", name = "io"}, @@ -97,7 +105,7 @@ modules = [ [[package]] org = "ballerina" name = "http" -version = "2.10.5" +version = "2.10.6" dependencies = [ {org = "ballerina", name = "auth"}, {org = "ballerina", name = "cache"}, diff --git a/ballerina/annotation_processor.bal b/ballerina/annotation_processor.bal index 3b5bd2b84..beb4c74fc 100644 --- a/ballerina/annotation_processor.bal +++ b/ballerina/annotation_processor.bal @@ -127,6 +127,31 @@ isolated function getInterceptorConfig(readonly & Interceptor interceptor) retur return classType.@InterceptorConfig; } +isolated function getCacheConfig(GraphqlServiceConfig? serviceConfig) returns ServerCacheConfig? { + if serviceConfig is GraphqlServiceConfig { + if serviceConfig.cacheConfig is ServerCacheConfig { + return serviceConfig.cacheConfig; + } + } + return; +} + +isolated function getFieldCacheConfigFromServiceConfig(GraphqlServiceConfig? serviceConfig) returns ServerCacheConfig? { + if serviceConfig is GraphqlServiceConfig { + return serviceConfig.fieldCacheConfig; + } + return; +} + +isolated function getFieldCacheConfig(service object {} serviceObj, parser:RootOperationType operationType, + string fieldName, string[] resourcePath) returns ServerCacheConfig? { + GraphqlResourceConfig? resourceConfig = getResourceAnnotation(serviceObj, operationType, resourcePath, fieldName); + if resourceConfig is GraphqlResourceConfig { + return resourceConfig.cacheConfig; + } + return; +} + isolated function getResourceAnnotation(service object {} serviceObject, parser:RootOperationType operationType, string[] path, string methodName) returns GraphqlResourceConfig? = @java:Method { 'class: "io.ballerina.stdlib.graphql.runtime.engine.Engine" diff --git a/ballerina/annotations.bal b/ballerina/annotations.bal index 1df693185..c12dbea09 100644 --- a/ballerina/annotations.bal +++ b/ballerina/annotations.bal @@ -25,6 +25,8 @@ # + interceptors - GraphQL service level interceptors # + introspection - Whether to enable or disable the introspection on the service # + validation - Whether to enable or disable the constraint validation +# + cacheConfig - The cache configurations for the operations +# + fieldCacheConfig - The field cache config derived from the resource annotations. This is auto-generated at the compile time public type GraphqlServiceConfig record {| int maxQueryDepth?; ListenerAuthConfig[] auth?; @@ -35,6 +37,8 @@ public type GraphqlServiceConfig record {| readonly (readonly & Interceptor)|(readonly & Interceptor)[] interceptors = []; boolean introspection = true; boolean validation = true; + ServerCacheConfig cacheConfig?; + readonly ServerCacheConfig? fieldCacheConfig = (); |}; # The annotation to configure a GraphQL service. @@ -44,9 +48,11 @@ public annotation GraphqlServiceConfig ServiceConfig on service; # # + interceptors - GraphQL field level interceptors # + prefetchMethodName - The name of the instance method to be used for prefetching +# + cacheConfig - The cache configurations for the fields public type GraphqlResourceConfig record {| readonly (readonly & Interceptor)|(readonly & Interceptor)[] interceptors = []; string prefetchMethodName?; + ServerCacheConfig cacheConfig?; |}; # The annotation to configure a GraphQL resolver. diff --git a/ballerina/common_utils.bal b/ballerina/common_utils.bal index 8e16aa09f..677dfac7f 100644 --- a/ballerina/common_utils.bal +++ b/ballerina/common_utils.bal @@ -16,6 +16,7 @@ import graphql.parser; +import ballerina/crypto; import ballerina/http; // Error messages @@ -514,3 +515,31 @@ isolated function getKeyArgument(parser:FieldNode fieldNode) returns string? { public isolated function __addError(Context context, ErrorDetail errorDetail) { context.addError(errorDetail); } + +isolated function generateArgHash(parser:ArgumentNode[] arguments, string[] parentArgHashes = [], + string[] optionalFields = []) returns string { + any[] argValues = [...parentArgHashes, ...optionalFields]; + argValues.push(...arguments.'map((arg) => arg.getValue())); + byte[] hash = crypto:hashMd5(argValues.toString().toBytes()); + return hash.toBase64(); +} + +isolated function getNullableFieldsFromType(__Type fieldType) returns string[] { + string[] nullableFields = []; + __Field[]? fields = unwrapNonNullype(fieldType).fields; + if fields is __Field[] { + foreach __Field 'field in fields { + if isNullableField('field.'type) { + nullableFields.push('field.name); + } + } + } + return nullableFields; +} + +isolated function isNullableField(__Type 'type) returns boolean { + if 'type.kind == NON_NULL { + return false; + } + return true; +} diff --git a/ballerina/context.bal b/ballerina/context.bal index 8a3db244f..3f61a4415 100644 --- a/ballerina/context.bal +++ b/ballerina/context.bal @@ -99,7 +99,7 @@ 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) { @@ -109,7 +109,7 @@ public isolated class Context { } # 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 { @@ -118,6 +118,29 @@ public isolated class Context { } } + # Remove cache entries related to the given path. + # + # + path - The path corresponding to the cache entries to be removed (Ex: "person.address.city") + # + return - The error if the cache invalidateion fails or nil otherwise + public isolated function invalidate(string path) returns error? { + Engine? engine = self.getEngine(); + if engine is Engine { + return engine.invalidate(path); + } + return; + } + + # Remove all cache entries. + # + # + return - The error if the cache invalidateion fails or nil otherwise + public isolated function invalidateAll() returns error? { + Engine? engine = self.getEngine(); + if engine is Engine { + return engine.invalidateAll(); + } + return; + } + isolated function addError(ErrorDetail err) { lock { self.errors.push(err.clone()); diff --git a/ballerina/engine.bal b/ballerina/engine.bal index 429529fc8..e7d021f64 100644 --- a/ballerina/engine.bal +++ b/ballerina/engine.bal @@ -16,6 +16,7 @@ import graphql.parser; +import ballerina/cache; import ballerina/jballerina.java; import ballerina/uuid; @@ -25,10 +26,13 @@ isolated class Engine { private final readonly & (readonly & Interceptor)[] interceptors; private final readonly & boolean introspection; private final readonly & boolean validation; + private final cache:Cache? cache; + private final readonly & ServerCacheConfig? cacheConfig; isolated function init(string schemaString, int? maxQueryDepth, Service s, readonly & (readonly & Interceptor)[] interceptors, boolean introspection, - boolean validation) + boolean validation, ServerCacheConfig? cacheConfig = (), + ServerCacheConfig? fieldCacheConfig = ()) returns Error? { if maxQueryDepth is int && maxQueryDepth < 1 { return error Error("Max query depth value must be a positive integer"); @@ -38,6 +42,8 @@ isolated class Engine { self.interceptors = interceptors; self.introspection = introspection; self.validation = validation; + self.cacheConfig = cacheConfig; + self.cache = initCacheTable(cacheConfig, fieldCacheConfig); self.addService(s); } @@ -53,6 +59,26 @@ isolated class Engine { return self.validation; } + isolated function getCacheConfig() returns readonly & ServerCacheConfig? { + return self.cacheConfig; + } + + isolated function addToCache(string key, any value, decimal maxAge) returns any|error { + cache:Cache? cache = self.cache; + if cache is cache:Cache { + return cache.put(key, value, maxAge); + } + return error("Cache table not found!"); + } + + isolated function getFromCache(string key) returns any|error { + cache:Cache? cache = self.cache; + if cache is cache:Cache { + return cache.get(key); + } + return error("Cache table not found!"); + } + isolated function validate(string documentString, string? operationName, map? variables) returns parser:OperationNode|OutputObject { @@ -248,8 +274,8 @@ isolated class Engine { if executePrefetchMethod { service object {}? serviceObject = 'field.getServiceObject(); - if serviceObject is service object {} { - string prefetchMethodName = getPrefetchMethodName(serviceObject, 'field) + 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); @@ -259,7 +285,8 @@ isolated class Engine { (readonly & Interceptor)? interceptor = context.getNextInterceptor('field); __Type fieldType = 'field.getFieldType(); - ResponseGenerator responseGenerator = new (self, context, fieldType, 'field.getPath().clone()); + ResponseGenerator responseGenerator = new (self, context, fieldType, 'field.getPath().clone(), + 'field.getCacheConfig(), 'field.getParentArgHashes()); do { if interceptor is readonly & Interceptor { any|error result = self.executeInterceptor(interceptor, 'field, context); @@ -267,12 +294,20 @@ isolated class Engine { return check validateInterceptorReturnValue(fieldType, result, interceptorName); } 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); + if 'field.getOperationType() == parser:OPERATION_QUERY && 'field.isCacheEnabled() { + string cacheKey = 'field.getCacheKey(); + any|error cachedValue = self.getFromCache(cacheKey); + if cachedValue is any { + fieldValue = cachedValue; + } else { + fieldValue = check self.getFieldValue(context, 'field, responseGenerator); + decimal maxAge = 'field.getCacheMaxAge(); + if maxAge > 0d && fieldValue !is () { + _ = check self.addToCache(cacheKey, fieldValue, maxAge); + } + } } else { - fieldValue = check 'field.getFieldValue(); + fieldValue = check self.getFieldValue(context, 'field, responseGenerator); } return responseGenerator.getResult(fieldValue, fieldNode); } on fail error errorValue { @@ -351,6 +386,36 @@ isolated class Engine { _ = resourcePath.pop(); } + private isolated function getFieldValue(Context context, Field 'field, ResponseGenerator responseGenerator) returns any|error { + if 'field.getOperationType() == parser:OPERATION_QUERY { + return self.resolveResourceMethod(context, 'field, responseGenerator); + } else if 'field.getOperationType() == parser:OPERATION_MUTATION { + return self.resolveRemoteMethod(context, 'field, responseGenerator); + } + return 'field.getFieldValue(); + } + + isolated function invalidate(string path) returns error? { + cache:Cache? cache = self.cache; + if cache is cache:Cache { + string[] keys = cache.keys().filter(isolated function (string key) returns boolean { + return key.startsWith(string `${path}.`); + }); + foreach string key in keys { + _ = check cache.invalidate(key); + } + } + return; + } + + isolated function invalidateAll() returns error? { + cache:Cache? cache = self.cache; + if cache is cache:Cache { + return cache.invalidateAll(); + } + return; + } + isolated function addService(Service s) = @java:Method { 'class: "io.ballerina.stdlib.graphql.runtime.engine.EngineUtils" } external; diff --git a/ballerina/engine_utils.bal b/ballerina/engine_utils.bal index a3c0cfc9e..6eb262470 100644 --- a/ballerina/engine_utils.bal +++ b/ballerina/engine_utils.bal @@ -16,6 +16,7 @@ import graphql.parser; +import ballerina/cache; import ballerina/jballerina.java; import ballerina/lang.regexp; @@ -89,7 +90,9 @@ isolated function getFieldObject(parser:FieldNode fieldNode, parser:RootOperatio string operationTypeName = getOperationTypeNameFromOperationType(operationType); __Type parentType = <__Type>getTypeFromTypeArray(schema.types, operationTypeName); __Type fieldType = getFieldTypeFromParentType(parentType, schema.types, fieldNode); - return new (fieldNode, fieldType, engine.getService(), path, operationType, fieldValue = fieldValue); + string parentArgHashes = generateArgHash(fieldNode.getArguments()); + return new (fieldNode, fieldType, engine.getService(), path, operationType, fieldValue = fieldValue, + cacheConfig = engine.getCacheConfig(), parentArgHashes = [parentArgHashes]); } isolated function createSchema(string schemaString) returns readonly & __Schema|Error = @java:Method { @@ -107,7 +110,7 @@ isolated function getTypeNameFromValue(any value) returns string = @java:Method # Obtains the schema representation of a federated subgraph, expressed in the SDL format. # + encodedSchemaString - Compile time auto generated schema # + return - Subgraph schema in SDL format as a string on success, or an error otherwise -public isolated function getSdlString(string encodedSchemaString) +public isolated function getSdlString(string encodedSchemaString) returns string|error = @java:Method { 'class: "io.ballerina.stdlib.graphql.runtime.engine.EngineUtils" } external; @@ -117,3 +120,17 @@ isolated function getDefaultPrefetchMethodName(string fieldName) returns string return string `${DEFAULT_PREFETCH_METHOD_NAME_PREFIX}${groups[0].substring().toUpperAscii()}`; }); } + +isolated function initCacheTable(ServerCacheConfig? operationCacheConfig, ServerCacheConfig? fieldCacheConfig) returns cache:Cache? { + if operationCacheConfig is ServerCacheConfig && operationCacheConfig.enabled { + return new ({capacity:operationCacheConfig.maxSize, evictionFactor:0.2, defaultMaxAge:operationCacheConfig.maxAge}); + } else if fieldCacheConfig is ServerCacheConfig && fieldCacheConfig.enabled { + return new({capacity:fieldCacheConfig.maxSize, evictionFactor:0.2, defaultMaxAge:fieldCacheConfig.maxAge}); + } + return; +} + +isolated function hasRecordReturnType(service object {} serviceObject, string[] path) + returns boolean = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Engine" +} external; diff --git a/ballerina/field.bal b/ballerina/field.bal index 2609301a5..7380ae8d4 100644 --- a/ballerina/field.bal +++ b/ballerina/field.bal @@ -26,10 +26,16 @@ public class Field { private (string|int)[] path; private string[] resourcePath; private readonly & Interceptor[] fieldInterceptors; + private final ServerCacheConfig? cacheConfig; + private final readonly & string[] parentArgHashes; + private final boolean cacheEnabled; + private final decimal cacheMaxAge; + private boolean hasRequestedNullableFields; isolated function init(parser:FieldNode internalNode, __Type fieldType, service object {}? serviceObject = (), (string|int)[] path = [], parser:RootOperationType operationType = parser:OPERATION_QUERY, - string[] resourcePath = [], any|error fieldValue = ()) { + string[] resourcePath = [], any|error fieldValue = (), ServerCacheConfig? cacheConfig = (), + readonly & string[] parentArgHashes = []) { self.internalNode = internalNode; self.serviceObject = serviceObject; self.fieldType = fieldType; @@ -40,6 +46,20 @@ public class Field { self.resourcePath.push(internalNode.getName()); self.fieldInterceptors = serviceObject is service object {} ? getFieldInterceptors(serviceObject, operationType, internalNode.getName(), self.resourcePath) : []; + ServerCacheConfig? fieldCache = serviceObject is service object {} ? + getFieldCacheConfig(serviceObject, operationType, internalNode.getName(), self.resourcePath) : (); + ServerCacheConfig? updatedCacheConfig = fieldCache is ServerCacheConfig ? fieldCache : cacheConfig; + self.cacheConfig = updatedCacheConfig; + self.parentArgHashes = parentArgHashes; + if updatedCacheConfig is ServerCacheConfig { + self.cacheEnabled = updatedCacheConfig.enabled; + self.cacheMaxAge = updatedCacheConfig.maxAge; + } else { + self.cacheEnabled = false; + self.cacheMaxAge = 0d; + } + self.hasRequestedNullableFields = self.cacheEnabled && serviceObject is service object {} + && hasFields(getOfType(self.fieldType)) && hasRecordReturnType(serviceObject, self.resourcePath); } # Returns the name of the field. @@ -129,7 +149,8 @@ public class Field { foreach __Field 'field in typeFields { if 'field.name == selection.getName() { result.push(new Field(selection, 'field.'type, (),[...currentPath, ...unwrappedPath, 'field.name], - self.operationType.clone(), self.resourcePath.clone())); + self.operationType.clone(), self.resourcePath.clone(), cacheConfig = self.cacheConfig, + parentArgHashes = self.parentArgHashes)); break; } } @@ -160,4 +181,48 @@ public class Field { isolated function getFieldInterceptors() returns readonly & Interceptor[] { return self.fieldInterceptors; } + + isolated function isCacheEnabled() returns boolean { + return self.cacheEnabled; + } + + isolated function getCacheConfig() returns ServerCacheConfig? { + return self.cacheConfig; + } + + isolated function getCacheKey() returns string { + return self.generateCacheKey(); + } + + isolated function getCacheMaxAge() returns decimal { + return self.cacheMaxAge; + } + + isolated function getParentArgHashes() returns readonly & string[] { + return self.parentArgHashes; + } + + private isolated function generateCacheKey() returns string { + string[] requestedNullableFields = []; + if self.hasRequestedNullableFields { + requestedNullableFields = self.getRequestedNullableFields(); + } + string resourcePath = ""; + foreach string|int path in self.path { + resourcePath += string `${path}.`; + } + string hash = generateArgHash(self.internalNode.getArguments(), self.parentArgHashes, requestedNullableFields); + return string `${resourcePath}${hash}`; + } + + private isolated function getRequestedNullableFields() returns string[] { + string[] nullableFields = getNullableFieldsFromType(self.fieldType); + string[] requestedNullableFields = []; + foreach string 'field in nullableFields { + if self.getSubfieldNames().indexOf('field) is int { + requestedNullableFields.push('field); + } + } + return requestedNullableFields.sort(); + } } diff --git a/ballerina/listener.bal b/ballerina/listener.bal index d3c9ac358..2cff796e7 100644 --- a/ballerina/listener.bal +++ b/ballerina/listener.bal @@ -71,11 +71,14 @@ public class Listener { readonly & (readonly & Interceptor)[] interceptors = getServiceInterceptors(serviceConfig); boolean introspection = getIntrospection(serviceConfig); boolean validation = getValidation(serviceConfig); + ServerCacheConfig? operationCacheConfig = getCacheConfig(serviceConfig); + ServerCacheConfig? fieldCacheConfig = getFieldCacheConfigFromServiceConfig(serviceConfig); Engine engine; if self.graphiql.enabled { check validateGraphiqlPath(self.graphiql.path); string gqlServiceBasePath = name is () ? "" : getBasePath(name); - engine = check new (schemaString, maxQueryDepth, s, interceptors, introspection, validation); + engine = check new (schemaString, maxQueryDepth, s, interceptors, introspection, validation, + operationCacheConfig, fieldCacheConfig); __Schema & readonly schema = engine.getSchema(); __Type? subscriptionType = schema.subscriptionType; string graphqlUrl = string `${self.httpEndpoint}/${gqlServiceBasePath}`; @@ -89,7 +92,8 @@ public class Listener { return error Error("Error occurred while attaching the GraphiQL endpoint", result); } } else { - engine = check new (schemaString, maxQueryDepth, s, interceptors, introspection, validation); + engine = check new (schemaString, maxQueryDepth, s, interceptors, introspection, validation, + operationCacheConfig, fieldCacheConfig); } HttpService httpService = getHttpService(engine, serviceConfig); diff --git a/ballerina/response_generator.bal b/ballerina/response_generator.bal index c27ca615d..c54ac1cfd 100644 --- a/ballerina/response_generator.bal +++ b/ballerina/response_generator.bal @@ -23,14 +23,19 @@ class ResponseGenerator { private final Context context; private (string|int)[] path; private final __Type fieldType; + private final readonly & ServerCacheConfig? cacheConfig; + private final readonly & string[] parentArgHashes; private final string functionNameGetFragmentFromService = ""; - isolated function init(Engine engine, Context context, __Type fieldType, (string|int)[] path = []) { + isolated function init(Engine engine, Context context, __Type fieldType, (string|int)[] path = [], + ServerCacheConfig? cacheConfig = (), readonly & string[] parentArgHashes = []) { self.engine = engine; self.context = context; self.path = path; self.fieldType = fieldType; + self.cacheConfig = cacheConfig; + self.parentArgHashes = parentArgHashes; } isolated function getResult(any|error parentValue, parser:FieldNode parentNode) @@ -77,7 +82,8 @@ class ResponseGenerator { (string|int)[] clonedPath = self.path.clone(); clonedPath.push(fieldNode.getAlias()); __Type fieldType = getFieldTypeFromParentType(self.fieldType, self.engine.getSchema().types, fieldNode); - Field 'field = new (fieldNode, fieldType, parentValue, clonedPath); + Field 'field = new (fieldNode, fieldType, parentValue, clonedPath, cacheConfig = self.cacheConfig, + parentArgHashes = self.parentArgHashes); self.context.resetInterceptorCount(); return self.engine.resolve(self.context, 'field); } diff --git a/ballerina/tests/09_cache_utils.bal b/ballerina/tests/09_cache_utils.bal new file mode 100644 index 000000000..25f733a38 --- /dev/null +++ b/ballerina/tests/09_cache_utils.bal @@ -0,0 +1,57 @@ +// Copyright (c) 2024, 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/test; +import graphql.parser; + +@test:Config { + groups: ["server_cache"] +} +function testCacheUtils() returns error? { + parser:FieldNode[] fields = check getFieldNodesFromDocumentFile("cache_utils"); + test:assertTrue(fields.length() == 1); + Field 'field = getField(fields[0], Person, ["person"], {maxAge: 10}); + test:assertTrue('field.isCacheEnabled()); + test:assertEquals('field.getCacheMaxAge(), 10d); + test:assertEquals('field.getCacheKey(), "person.TYoV48w1dQ8BbOFaQ5N2IA=="); +} + +@test:Config { + groups: ["server_cache"] +} +function testCacheConfigInferring() returns error? { + parser:FieldNode[] fields = check getFieldNodesFromDocumentFile("cache_utils"); + test:assertTrue(fields.length() == 1); + Field 'field = getField(fields[0], Person, ["person"], {maxAge: 10}); + test:assertTrue('field.getSubfields() is Field[]); + Field[] subfields = 'field.getSubfields(); + string[] expectedCacheKey = ["person.name.11FxOYiYfpMxmANj4kGJzg==", "person.address.t1JP49rdVkuozxi8sSh+bg=="]; + foreach int i in 0..subSubfields).length(), 1); + Field subSubfield = (subSubfields)[0]; + test:assertTrue(subSubfield.isCacheEnabled()); + test:assertEquals(subSubfield.getCacheMaxAge(), 10d); + test:assertEquals(subSubfield.getCacheKey(), string `person.address.city.11FxOYiYfpMxmANj4kGJzg==`); + } + } +} + diff --git a/ballerina/tests/field_object_types.bal b/ballerina/tests/field_object_types.bal index 790d7eda5..397de4f91 100644 --- a/ballerina/tests/field_object_types.bal +++ b/ballerina/tests/field_object_types.bal @@ -107,3 +107,55 @@ __Type AstronautNonNullList = { ofType: Astronaut } }; + +__Type Person = { + kind: "OBJECT", + name: "Person", + fields: [ + name, + address + ], + interfaces: [] +}; + +__Type Address = { + kind: "OBJECT", + name: "Address", + fields: [ + city + ], + interfaces: [] +}; + +__Field person = { + name: "person", + args: [inputId], + 'type: Person +}; + +__InputValue inputId = { + name: "id", + 'type: NonNullScalarInt +}; + +__Field address = { + name: "address", + args: [includeCity], + 'type: Address +}; + +__InputValue includeCity = { + name: "includeCity", + 'type: NonNullScalarBoolean +}; + +__Type NonNullScalarBoolean = { + kind: "SCALAR", + name: "Boolean" +}; + +__Field city = { + name: "city", + args: [], + 'type: ScalarString +}; diff --git a/ballerina/tests/resources/documents/cache_utils.graphql b/ballerina/tests/resources/documents/cache_utils.graphql new file mode 100644 index 000000000..b690f1dac --- /dev/null +++ b/ballerina/tests/resources/documents/cache_utils.graphql @@ -0,0 +1,8 @@ +query { + person(id: "1") { + name + address(includeCity: true) { + city + } + } +} diff --git a/ballerina/tests/utils.bal b/ballerina/tests/utils.bal index c32c8bf9b..4814f104b 100644 --- a/ballerina/tests/utils.bal +++ b/ballerina/tests/utils.bal @@ -56,6 +56,6 @@ isolated function getFieldNodesFromDocumentFile(string fileName) returns parser: return fieldNodes; } -isolated function getField(parser:FieldNode fieldNode, __Type fieldType, string[] path) returns Field { - return new (fieldNode, fieldType, path = path); +isolated function getField(parser:FieldNode fieldNode, __Type fieldType, string[] path, ServerCacheConfig? cacheConfig = ()) returns Field { + return new (fieldNode, fieldType, path = path, cacheConfig = cacheConfig); } diff --git a/ballerina/types.bal b/ballerina/types.bal index c0e785e23..8931f5589 100644 --- a/ballerina/types.bal +++ b/ballerina/types.bal @@ -65,6 +65,17 @@ public type Graphiql record {| boolean printUrl = true; |}; +# Represent the cache configurations of GraphQL server. +# +# + enabled - State of the caching +# + maxAge - TTL of the cache in seconds +# + maxSize - Maximum number of cache entries +public type ServerCacheConfig readonly & record{| + boolean enabled = true; + decimal maxAge = 60; + int maxSize = 120; +|}; + # Internal HTTP service class for GraphQL services isolated service class HttpService { *http:Service; 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 eb064a5a3..688f44677 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 @@ -294,7 +294,7 @@ public void testInvalidReturnTypes() { diagnostic = diagnosticIterator.next(); message = getErrorMessage(CompilationDiagnostic.INVALID_FUNCTION, "Interceptor", "execute"); - assertErrorMessage(diagnostic, message, 80, 5); + assertErrorMessage(diagnostic, message, 91, 5); diagnostic = diagnosticIterator.next(); message = getErrorMessage(CompilationDiagnostic.MISSING_RESOURCE_FUNCTIONS); @@ -1416,6 +1416,41 @@ public void testInvalidExternalRecordsWithDefaultValuesAsInputObject() { assertWarningMessage(diagnostic, message, 36, 45); } + @Test(groups = "valid") + public void testValidServerSideCacheConfigurations() { + String packagePath = "85_valid_server_side_cache_configurations"; + DiagnosticResult diagnosticResult = getDiagnosticResult(packagePath); + Assert.assertEquals(diagnosticResult.errorCount(), 0); + } + + @Test(groups = "invalid") + public void testInvalidServiceConfigModification() { + String packagePath = "86_invalid_service_config_modification"; + DiagnosticResult diagnosticResult = getDiagnosticResult(packagePath); + Assert.assertEquals(diagnosticResult.errorCount(), 4); + Iterator diagnosticIterator = diagnosticResult.errors().iterator(); + + Diagnostic diagnostic = diagnosticIterator.next(); + String message = + getErrorMessage(CompilationDiagnostic.INVALID_MODIFICATION_OF_SERVICE_CONFIG_FIELD, "schemaString"); + assertErrorMessage(diagnostic, message, 20, 1); + + diagnostic = diagnosticIterator.next(); + message = + getErrorMessage(CompilationDiagnostic.INVALID_MODIFICATION_OF_SERVICE_CONFIG_FIELD, "fieldCacheConfig"); + assertErrorMessage(diagnostic, message, 20, 1); + + diagnostic = diagnosticIterator.next(); + message = + getErrorMessage(CompilationDiagnostic.INVALID_MODIFICATION_OF_SERVICE_CONFIG_FIELD, "schemaString"); + assertErrorMessage(diagnostic, message, 51, 44); + + diagnostic = diagnosticIterator.next(); + message = + getErrorMessage(CompilationDiagnostic.INVALID_MODIFICATION_OF_SERVICE_CONFIG_FIELD, "fieldCacheConfig"); + assertErrorMessage(diagnostic, message, 51, 44); + } + private DiagnosticResult getDiagnosticResult(String packagePath) { Path projectDirPath = RESOURCE_DIRECTORY.resolve(packagePath); BuildProject project = BuildProject.load(getEnvironmentBuilder(), projectDirPath); diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/85_valid_server_side_cache_configurations/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/85_valid_server_side_cache_configurations/Ballerina.toml new file mode 100644 index 000000000..b58c76324 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/85_valid_server_side_cache_configurations/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/85_valid_server_side_cache_configurations/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/85_valid_server_side_cache_configurations/service.bal new file mode 100644 index 000000000..4775a14f2 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/85_valid_server_side_cache_configurations/service.bal @@ -0,0 +1,43 @@ +// Copyright (c) 2024 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; + +@graphql:ServiceConfig { + cacheConfig: {} +} +service graphql:Service on new graphql:Listener(4000) { + private final string name = "Sherlock Holmes"; + private final int age = 54; + + @graphql:ResourceConfig { + cacheConfig: { + maxSize: 10 + } + } + resource function get name(string name) returns string { + return self.name; + } + + @graphql:ResourceConfig { + cacheConfig: { + maxSize: 20 + } + } + resource function get age() returns int { + return self.age; + } +} diff --git a/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/86_invalid_service_config_modification/Ballerina.toml b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/86_invalid_service_config_modification/Ballerina.toml new file mode 100644 index 000000000..b58c76324 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/86_invalid_service_config_modification/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/86_invalid_service_config_modification/service.bal b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/86_invalid_service_config_modification/service.bal new file mode 100644 index 000000000..8e03314c3 --- /dev/null +++ b/compiler-plugin-tests/src/test/resources/ballerina_sources/validator_tests/86_invalid_service_config_modification/service.bal @@ -0,0 +1,73 @@ +// Copyright (c) 2024 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/lang.runtime; + +@graphql:ServiceConfig { + schemaString: "", + fieldCacheConfig: {} +} +service graphql:Service on new graphql:Listener(4000) { + private final string name = "Sherlock Holmes"; + private final int age = 54; + + resource function get name(string name) returns string { + return self.name; + } + + resource function get age() returns int { + return self.age; + } +} + +public distinct service isolated class Person { + private final string name; + + isolated function init(string name) { + self.name = name; + } + + isolated resource function get name() returns string { + return self.name; + } + +} + +class TestService { + private graphql:Service fieldService = @graphql:ServiceConfig { + schemaString: "s", + fieldCacheConfig: {} + } service object { + resource function get greeting() returns Person { + return new Person("Rick"); + } + }; + + public function init() {} + + public function startService() returns error? { + graphql:Listener localListener = check new(9090); + check localListener.attach(self.fieldService); + check localListener.'start(); + runtime:registerListener(localListener); + } +} + +public function main() returns error? { + TestService serviceClass = new (); + check serviceClass.startService(); +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/CacheConfigContext.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/CacheConfigContext.java new file mode 100644 index 000000000..e1d21db66 --- /dev/null +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/CacheConfigContext.java @@ -0,0 +1,28 @@ +package io.ballerina.stdlib.graphql.compiler; + +public class CacheConfigContext { + private Boolean enabled; + private int maxSize; + + CacheConfigContext(Boolean enabled) { + this.enabled = enabled; + this.maxSize = 0; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + public Boolean getEnabled() { + return enabled; + } + + public void setMaxSize(int maxSize) { + if (maxSize > this.maxSize) { + this.maxSize = maxSize; + } + } + + public int getMaxSize() { + return maxSize; + } +} diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ModuleLevelVariableDeclarationAnalysisTask.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ModuleLevelVariableDeclarationAnalysisTask.java index 4e1c3b953..10b103861 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ModuleLevelVariableDeclarationAnalysisTask.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ModuleLevelVariableDeclarationAnalysisTask.java @@ -68,7 +68,7 @@ public void perform(SyntaxNodeAnalysisContext context) { ObjectConstructorExpressionNode graphqlServiceObjectNode = (ObjectConstructorExpressionNode) expressionNode; InterfaceEntityFinder interfaceEntityFinder = getInterfaceEntityFinder(context.semanticModel()); ServiceValidator serviceObjectValidator = getServiceValidator(context, graphqlServiceObjectNode, - interfaceEntityFinder); + interfaceEntityFinder, new CacheConfigContext(false)); if (serviceObjectValidator.isErrorOccurred()) { return; } @@ -76,6 +76,8 @@ public void perform(SyntaxNodeAnalysisContext context) { Schema schema = generateSchema(context, interfaceEntityFinder, graphqlServiceObjectNode, description); DocumentId documentId = context.documentId(); addToModifierContextMap(documentId, moduleVariableDeclarationNode, schema); + addToModifierContextMap(documentId, moduleVariableDeclarationNode, + serviceObjectValidator.getCacheConfigContext()); } public static String getDescription(SemanticModel semanticModel, diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ObjectConstructorAnalysisTask.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ObjectConstructorAnalysisTask.java index a95bbd7e7..11a8f8d5c 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ObjectConstructorAnalysisTask.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ObjectConstructorAnalysisTask.java @@ -55,13 +55,15 @@ public void perform(SyntaxNodeAnalysisContext context) { } InterfaceEntityFinder interfaceEntityFinder = getInterfaceEntityFinder(context.semanticModel()); - ServiceValidator serviceValidator = getServiceValidator(context, node, interfaceEntityFinder); + ServiceValidator serviceValidator = getServiceValidator(context, node, interfaceEntityFinder, + new CacheConfigContext(false)); if (serviceValidator.isErrorOccurred()) { return; } Schema schema = generateSchema(context, interfaceEntityFinder, node, null); DocumentId documentId = context.documentId(); addToModifierContextMap(documentId, node.parent(), schema); + addToModifierContextMap(documentId, node.parent(), serviceValidator.getCacheConfigContext()); } public boolean isGraphQLServiceObjectDeclaration(SemanticModel semanticModel, diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ServiceAnalysisTask.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ServiceAnalysisTask.java index ca2c49f73..29969b09b 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ServiceAnalysisTask.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ServiceAnalysisTask.java @@ -57,10 +57,12 @@ public ServiceAnalysisTask(Map nodeMap) { } public ServiceValidator getServiceValidator(SyntaxNodeAnalysisContext context, Node node, - InterfaceEntityFinder interfaceEntityFinder) { + InterfaceEntityFinder interfaceEntityFinder, + CacheConfigContext cacheConfigContext) { boolean isSubgraph = isSubgraphService(node, context); nodeSubgraphMap.put(node, isSubgraph); - ServiceValidator serviceValidator = new ServiceValidator(context, node, interfaceEntityFinder, isSubgraph); + ServiceValidator serviceValidator = + new ServiceValidator(context, node, interfaceEntityFinder, isSubgraph, cacheConfigContext); serviceValidator.validate(); return serviceValidator; } @@ -93,6 +95,17 @@ public void addToModifierContextMap(DocumentId documentId, Node node, Schema sch } } + public void addToModifierContextMap(DocumentId documentId, Node node, CacheConfigContext cacheConfigContext) { + if (this.modifierContextMap.containsKey(documentId)) { + GraphqlModifierContext modifierContext = this.modifierContextMap.get(documentId); + modifierContext.add(node, cacheConfigContext); + } else { + GraphqlModifierContext modifierContext = new GraphqlModifierContext(); + modifierContext.add(node, cacheConfigContext); + this.modifierContextMap.put(documentId, modifierContext); + } + } + @SuppressWarnings("OptionalGetWithoutIsPresent") private static boolean isSubgraphService(Node serviceNode, SyntaxNodeAnalysisContext context) { List annotations = new ArrayList<>(); diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ServiceDeclarationAnalysisTask.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ServiceDeclarationAnalysisTask.java index d64eff9b0..902ea428a 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ServiceDeclarationAnalysisTask.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/ServiceDeclarationAnalysisTask.java @@ -52,7 +52,8 @@ public void perform(SyntaxNodeAnalysisContext context) { } ServiceDeclarationNode node = (ServiceDeclarationNode) context.node(); InterfaceEntityFinder interfaceEntityFinder = getInterfaceEntityFinder(context.semanticModel()); - ServiceValidator serviceValidator = getServiceValidator(context, node, interfaceEntityFinder); + ServiceValidator serviceValidator = getServiceValidator(context, node, interfaceEntityFinder, + new CacheConfigContext(false)); if (serviceValidator.isErrorOccurred()) { return; } @@ -64,5 +65,6 @@ public void perform(SyntaxNodeAnalysisContext context) { Schema schema = generateSchema(context, interfaceEntityFinder, node, description); DocumentId documentId = context.documentId(); addToModifierContextMap(documentId, node, schema); + addToModifierContextMap(documentId, node, serviceValidator.getCacheConfigContext()); } } 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 0cf7cc99d..f5b376e34 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 @@ -42,9 +42,13 @@ import io.ballerina.compiler.syntax.tree.BasicLiteralNode; import io.ballerina.compiler.syntax.tree.DefaultableParameterNode; 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.ModuleVariableDeclarationNode; import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.ObjectConstructorExpressionNode; +import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode; import io.ballerina.compiler.syntax.tree.RecordFieldWithDefaultValueNode; import io.ballerina.compiler.syntax.tree.SpecificFieldNode; import io.ballerina.compiler.syntax.tree.SyntaxKind; @@ -65,6 +69,7 @@ import java.util.List; import java.util.stream.Collectors; +import static io.ballerina.stdlib.graphql.commons.utils.Utils.PACKAGE_NAME; import static io.ballerina.stdlib.graphql.commons.utils.Utils.hasGraphqlListener; import static io.ballerina.stdlib.graphql.commons.utils.Utils.isGraphQLServiceObjectDeclaration; import static io.ballerina.stdlib.graphql.commons.utils.Utils.isGraphqlModuleSymbol; @@ -90,6 +95,10 @@ public final class Utils { private static final String RESOURCE_CONFIG_ANNOTATION = "ResourceConfig"; private static final String ENTITY_ANNOTATION = "Entity"; + private static final String CACHE_CONFIG_ENABLE_FIELD = "enabled"; + private static final String CACHE_CONFIG_MAX_SIZE_FIELD = "maxSize"; + private static final int DEFAULT_MAX_SIZE = 120; + private Utils() { } @@ -385,6 +394,55 @@ public static String getStringValue(SpecificFieldNode specificFieldNode) { return null; } + public static boolean getBooleanValue(MappingConstructorExpressionNode mappingConstructorNode) { + if (mappingConstructorNode.fields().isEmpty()) { + return true; + } + for (MappingFieldNode field: mappingConstructorNode.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 (CACHE_CONFIG_ENABLE_FIELD.equals(identifierName) && specificFieldNode.valueExpr().isPresent()) { + ExpressionNode valueExpression = specificFieldNode.valueExpr().get(); + if (valueExpression.kind() == SyntaxKind.BOOLEAN_LITERAL) { + BasicLiteralNode stringLiteralNode = (BasicLiteralNode) valueExpression; + return Boolean.parseBoolean(stringLiteralNode.toSourceCode().trim()); + } + } + } + } + } + return true; + } + + public static int getMaxSize(MappingConstructorExpressionNode mappingConstructorNode) { + if (mappingConstructorNode.fields().isEmpty()) { + return DEFAULT_MAX_SIZE; + } + for (MappingFieldNode field: mappingConstructorNode.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 (CACHE_CONFIG_MAX_SIZE_FIELD.equals(identifierName) && + specificFieldNode.valueExpr().isPresent()) { + ExpressionNode valueExpression = specificFieldNode.valueExpr().get(); + if (valueExpression.kind() == SyntaxKind.NUMERIC_LITERAL) { + BasicLiteralNode integerLiteralNode = (BasicLiteralNode) valueExpression; + return Integer.parseInt(integerLiteralNode.toSourceCode().trim()); + } + } + } + } + } + return DEFAULT_MAX_SIZE; + } + public static String getStringValue(BasicLiteralNode expression) { String literalToken = expression.literalToken().text().trim(); return literalToken.substring(1, literalToken.length() - 1); @@ -434,4 +492,15 @@ public static List getTypeInclusions(TypeSymbol typeSymbol) { } return new ArrayList<>(); } + + public static boolean isGraphqlServiceConfig(AnnotationNode annotationNode) { + if (annotationNode.annotReference().kind() != SyntaxKind.QUALIFIED_NAME_REFERENCE) { + return false; + } + QualifiedNameReferenceNode referenceNode = ((QualifiedNameReferenceNode) annotationNode.annotReference()); + if (!PACKAGE_NAME.equals(referenceNode.modulePrefix().text())) { + return false; + } + return SERVICE_CONFIG_IDENTIFIER.equals(referenceNode.identifier().text()); + } } 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 621bc7778..2eb99e3e9 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 @@ -87,6 +87,8 @@ public enum CompilationDiagnostic { DiagnosticSeverity.ERROR), INVALID_EMPTY_RECORD_OBJECT_TYPE(DiagnosticMessage.ERROR_146, DiagnosticCode.GRAPHQL_146, DiagnosticSeverity.ERROR), INVALID_EMPTY_RECORD_INPUT_TYPE(DiagnosticMessage.ERROR_147, DiagnosticCode.GRAPHQL_147, DiagnosticSeverity.ERROR), + INVALID_MODIFICATION_OF_SERVICE_CONFIG_FIELD(DiagnosticMessage.ERROR_148, DiagnosticCode.GRAPHQL_148, + DiagnosticSeverity.ERROR), // Warnings UNSUPPORTED_INPUT_FIELD_DEPRECATION(DiagnosticMessage.WARNING_201, DiagnosticCode.GRAPHQL_201, 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 970726e06..9e948b097 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 @@ -69,6 +69,7 @@ public enum DiagnosticCode { GRAPHQL_145, GRAPHQL_146, GRAPHQL_147, + GRAPHQL_148, GRAPHQL_201, GRAPHQL_202, GRAPHQL_203, 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 4d9bcc27e..ae9fdfa11 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 @@ -96,6 +96,8 @@ public enum DiagnosticMessage { + " only supported for 'remote' methods and 'get' resource methods"), ERROR_146("invalid empty record type ''{0}'' found for GraphQL object type at field ''{1}''"), ERROR_147("invalid empty record type ''{0}'' found for GraphQL input object type at field ''{1}''"), + ERROR_148("field ''{0}'' in ServiceConfig is not allowed to be modified. " + + "The value will be generated automatically by the GraphQL module"), WARNING_201("invalid usage of @deprecated directive found in ''{0}''. Input object field(s) deprecation " + "is not supported by the current GraphQL spec"), diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/schema/generator/GeneratorUtils.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/schema/generator/GeneratorUtils.java index 6837a301f..362fc7085 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/schema/generator/GeneratorUtils.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/schema/generator/GeneratorUtils.java @@ -53,6 +53,9 @@ private GeneratorUtils() { public static final String MAP_KEY_ARGUMENT_DESCRIPTION = "[auto-generated]: The key of the value required from a map"; public static final String SCHEMA_STRING_FIELD = "schemaString"; + public static final String ENABLED_CACHE_FIELD = "enabled"; + public static final String MAX_SIZE_CACHE_FIELD = "maxSize"; + public static final String FIELD_CACHE_CONFIG_FIELD = "fieldCacheConfig"; public static String getTypeName(TypeSymbol typeSymbol) { switch (typeSymbol.typeKind()) { diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/schema/generator/GraphqlModifierContext.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/schema/generator/GraphqlModifierContext.java index f8366c569..15fae6148 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/schema/generator/GraphqlModifierContext.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/schema/generator/GraphqlModifierContext.java @@ -20,6 +20,7 @@ import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.stdlib.graphql.commons.types.Schema; +import io.ballerina.stdlib.graphql.compiler.CacheConfigContext; import java.util.HashMap; import java.util.Map; @@ -30,16 +31,26 @@ */ public class GraphqlModifierContext { private final Map nodeSchemaMap; + private final Map nodeCacheConfigMap; public GraphqlModifierContext() { this.nodeSchemaMap = new HashMap<>(); + this.nodeCacheConfigMap = new HashMap<>(); } public void add(Node node, Schema schema) { this.nodeSchemaMap.put(node, schema); } + public void add(Node node, CacheConfigContext cacheConfig) { + this.nodeCacheConfigMap.put(node, cacheConfig); + } + public Map getNodeSchemaMap() { return this.nodeSchemaMap; } + + public Map getNodeCacheConfigMap() { + return this.nodeCacheConfigMap; + } } diff --git a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/schema/generator/GraphqlSourceModifier.java b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/schema/generator/GraphqlSourceModifier.java index 2277c3f06..cdc0861ea 100644 --- a/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/schema/generator/GraphqlSourceModifier.java +++ b/compiler-plugin/src/main/java/io/ballerina/stdlib/graphql/compiler/schema/generator/GraphqlSourceModifier.java @@ -37,7 +37,6 @@ import io.ballerina.compiler.syntax.tree.NonTerminalNode; import io.ballerina.compiler.syntax.tree.ObjectConstructorExpressionNode; import io.ballerina.compiler.syntax.tree.ObjectFieldNode; -import io.ballerina.compiler.syntax.tree.QualifiedNameReferenceNode; import io.ballerina.compiler.syntax.tree.SeparatedNodeList; import io.ballerina.compiler.syntax.tree.ServiceDeclarationNode; import io.ballerina.compiler.syntax.tree.SimpleNameReferenceNode; @@ -52,6 +51,7 @@ import io.ballerina.projects.plugins.SourceModifierContext; import io.ballerina.stdlib.graphql.commons.types.Schema; import io.ballerina.stdlib.graphql.commons.types.Type; +import io.ballerina.stdlib.graphql.compiler.CacheConfigContext; import io.ballerina.stdlib.graphql.compiler.diagnostics.CompilationDiagnostic; import io.ballerina.tools.diagnostics.Diagnostic; import io.ballerina.tools.diagnostics.DiagnosticFactory; @@ -80,6 +80,10 @@ import static io.ballerina.stdlib.graphql.commons.utils.Utils.PACKAGE_ORG; import static io.ballerina.stdlib.graphql.commons.utils.Utils.SUBGRAPH_SUB_MODULE_NAME; import static io.ballerina.stdlib.graphql.compiler.Utils.SERVICE_CONFIG_IDENTIFIER; +import static io.ballerina.stdlib.graphql.compiler.Utils.isGraphqlServiceConfig; +import static io.ballerina.stdlib.graphql.compiler.schema.generator.GeneratorUtils.ENABLED_CACHE_FIELD; +import static io.ballerina.stdlib.graphql.compiler.schema.generator.GeneratorUtils.FIELD_CACHE_CONFIG_FIELD; +import static io.ballerina.stdlib.graphql.compiler.schema.generator.GeneratorUtils.MAX_SIZE_CACHE_FIELD; import static io.ballerina.stdlib.graphql.compiler.schema.generator.GeneratorUtils.SCHEMA_STRING_FIELD; /** @@ -137,9 +141,10 @@ private ModulePartNode modifyDocument(SourceModifierContext context, ModulePartN try { String schemaString = getSchemaAsEncodedString(schema); Node targetNode = entry.getKey(); + CacheConfigContext cacheConfigContext = modifierContext.getNodeCacheConfigMap().get(targetNode); if (targetNode.kind() == SyntaxKind.SERVICE_DECLARATION) { ServiceDeclarationNode updatedNode = modifyServiceDeclarationNode( - (ServiceDeclarationNode) targetNode, schemaString, prefix); + (ServiceDeclarationNode) targetNode, schemaString, cacheConfigContext, prefix); modifiedNodes.put((NonTerminalNode) targetNode, updatedNode); this.entityTypeNamesMap.put(targetNode, this.entityUnionTypeName); this.entityUnionSuffix++; @@ -147,21 +152,21 @@ private ModulePartNode modifyDocument(SourceModifierContext context, ModulePartN ModuleVariableDeclarationNode graphqlServiceVariableDeclaration = (ModuleVariableDeclarationNode) targetNode; ModuleVariableDeclarationNode updatedNode = modifyModuleLevelServiceDeclarationNode( - schemaString, graphqlServiceVariableDeclaration, prefix); + schemaString, graphqlServiceVariableDeclaration, cacheConfigContext, prefix); modifiedNodes.put((NonTerminalNode) targetNode, updatedNode); this.entityTypeNamesMap.put(targetNode, this.entityUnionTypeName); this.entityUnionSuffix++; } else if (targetNode.kind() == SyntaxKind.LOCAL_VAR_DECL) { VariableDeclarationNode graphqlServiceVariableDeclaration = (VariableDeclarationNode) targetNode; VariableDeclarationNode updatedNode = modifyVariableServiceDeclarationNode( - schemaString, graphqlServiceVariableDeclaration, prefix); + schemaString, graphqlServiceVariableDeclaration, cacheConfigContext, prefix); modifiedNodes.put((NonTerminalNode) targetNode, updatedNode); this.entityTypeNamesMap.put(targetNode, this.entityUnionTypeName); this.entityUnionSuffix++; } else if (targetNode.kind() == SyntaxKind.OBJECT_FIELD) { ObjectFieldNode graphqlServiceFieldDeclaration = (ObjectFieldNode) targetNode; ObjectFieldNode updatedNode = modifyObjectFieldServiceDeclarationNode( - schemaString, graphqlServiceFieldDeclaration, prefix); + schemaString, graphqlServiceFieldDeclaration, cacheConfigContext, prefix); modifiedNodes.put((NonTerminalNode) targetNode, updatedNode); this.entityTypeNamesMap.put(targetNode, this.entityUnionTypeName); this.entityUnionSuffix++; @@ -247,44 +252,49 @@ private ModuleMemberDeclarationNode getEntityTypeDefinition() { private ModuleVariableDeclarationNode modifyModuleLevelServiceDeclarationNode(String schemaString, ModuleVariableDeclarationNode node, + CacheConfigContext cacheConfigContext, String prefix) { // noinspection OptionalGetWithoutIsPresent ObjectConstructorExpressionNode graphqlServiceObject = (ObjectConstructorExpressionNode) node.initializer().get(); ObjectConstructorExpressionNode updatedGraphqlServiceObject = modifyServiceObjectNode(graphqlServiceObject, - schemaString, prefix); + schemaString, cacheConfigContext, prefix); return node.modify().withInitializer(updatedGraphqlServiceObject).apply(); } private VariableDeclarationNode modifyVariableServiceDeclarationNode(String schemaString, VariableDeclarationNode node, + CacheConfigContext cacheConfigContext, String prefix) { ObjectConstructorExpressionNode graphqlServiceObject = (ObjectConstructorExpressionNode) node.initializer().get(); ObjectConstructorExpressionNode updatedGraphqlServiceObject = modifyServiceObjectNode( - graphqlServiceObject, schemaString, prefix); + graphqlServiceObject, schemaString, cacheConfigContext, prefix); return node.modify().withInitializer(updatedGraphqlServiceObject).apply(); } private ObjectFieldNode modifyObjectFieldServiceDeclarationNode(String schemaString, ObjectFieldNode node, + CacheConfigContext cacheConfigContext, String prefix) { ObjectConstructorExpressionNode graphqlServiceObject = (ObjectConstructorExpressionNode) node.expression().get(); ObjectConstructorExpressionNode updatedGraphqlServiceObject = modifyServiceObjectNode( - graphqlServiceObject, schemaString, prefix); + graphqlServiceObject, schemaString, cacheConfigContext, prefix); return node.modify().withExpression(updatedGraphqlServiceObject).apply(); } private ObjectConstructorExpressionNode modifyServiceObjectNode(ObjectConstructorExpressionNode node, - String schemaString, String prefix) { + String schemaString, + CacheConfigContext cacheConfigContext, + String prefix) { NodeList annotations = NodeFactory.createNodeList(); if (node.annotations().isEmpty()) { - AnnotationNode annotationNode = getSchemaStringAnnotation(schemaString, prefix); + AnnotationNode annotationNode = getServiceAnnotation(schemaString, cacheConfigContext, prefix); annotations = annotations.add(annotationNode); } else { for (AnnotationNode annotationNode : node.annotations()) { - annotationNode = updateAnnotationNode(annotationNode, schemaString, prefix); + annotationNode = updateAnnotationNode(annotationNode, schemaString, cacheConfigContext, prefix); annotations = annotations.add(annotationNode); } } @@ -302,8 +312,8 @@ private ObjectConstructorExpressionNode modifyServiceObjectNode(ObjectConstructo } private ServiceDeclarationNode modifyServiceDeclarationNode(ServiceDeclarationNode node, String schemaString, - String prefix) { - MetadataNode metadataNode = getMetadataNode(node, schemaString, prefix); + CacheConfigContext cacheConfigContext, String prefix) { + MetadataNode metadataNode = getMetadataNode(node, schemaString, cacheConfigContext, prefix); ServiceDeclarationNode.ServiceDeclarationNodeModifier modifier = node.modify(); modifier = modifier.withMetadata(metadataNode); NodeList members = node.members(); @@ -382,52 +392,56 @@ private String getEntityTypedescMapInitializer() { return "{" + String.join(", ", mapFields) + "}"; } - private MetadataNode getMetadataNode(ServiceDeclarationNode node, String schemaString, String prefix) { + private MetadataNode getMetadataNode(ServiceDeclarationNode node, String schemaString, + CacheConfigContext cacheConfigContext, + String prefix) { if (node.metadata().isPresent()) { - return getMetadataNodeFromExistingMetadata(node.metadata().get(), schemaString, prefix); + return getMetadataNodeFromExistingMetadata(node.metadata().get(), schemaString, cacheConfigContext, prefix); } else { - return getNewMetadataNode(schemaString, prefix); + return getNewMetadataNode(schemaString, cacheConfigContext, prefix); } } private MetadataNode getMetadataNodeFromExistingMetadata(MetadataNode metadataNode, String schemaString, - String prefix) { + CacheConfigContext cacheConfigContext, String prefix) { NodeList annotationNodes = NodeFactory.createNodeList(); if (metadataNode.annotations().isEmpty()) { - AnnotationNode annotationNode = getSchemaStringAnnotation(schemaString, prefix); + AnnotationNode annotationNode = getServiceAnnotation(schemaString, cacheConfigContext, prefix); annotationNodes = annotationNodes.add(annotationNode); } else { boolean isAnnotationFound = false; for (AnnotationNode annotationNode : metadataNode.annotations()) { if (isGraphqlServiceConfig(annotationNode)) { isAnnotationFound = true; - annotationNode = updateAnnotationNode(annotationNode, schemaString, prefix); + annotationNode = updateAnnotationNode(annotationNode, schemaString, cacheConfigContext, prefix); } annotationNodes = annotationNodes.add(annotationNode); } if (!isAnnotationFound) { - AnnotationNode annotationNode = getSchemaStringAnnotation(schemaString, prefix); + AnnotationNode annotationNode = getServiceAnnotation(schemaString, cacheConfigContext, prefix); annotationNodes = annotationNodes.add(annotationNode); } } return NodeFactory.createMetadataNode(metadataNode.documentationString().orElse(null), annotationNodes); } - private AnnotationNode updateAnnotationNode(AnnotationNode annotationNode, String schemaString, String prefix) { + private AnnotationNode updateAnnotationNode(AnnotationNode annotationNode, String schemaString, + CacheConfigContext cacheConfigContext, String prefix) { if (annotationNode.annotValue().isPresent()) { SeparatedNodeList updatedFields = - getUpdatedFields(annotationNode.annotValue().get(), schemaString); + getUpdatedFields(annotationNode.annotValue().get(), schemaString, cacheConfigContext); MappingConstructorExpressionNode node = NodeFactory.createMappingConstructorExpressionNode( NodeFactory.createToken(SyntaxKind.OPEN_BRACE_TOKEN), updatedFields, NodeFactory.createToken(SyntaxKind.CLOSE_BRACE_TOKEN)); return annotationNode.modify().withAnnotValue(node).apply(); } - return getSchemaStringAnnotation(schemaString, prefix); + return getServiceAnnotation(schemaString, cacheConfigContext, prefix); } private SeparatedNodeList getUpdatedFields(MappingConstructorExpressionNode annotationValue, - String schemaString) { + String schemaString, + CacheConfigContext cacheConfigContext) { List fields = new ArrayList<>(); SeparatedNodeList existingFields = annotationValue.fields(); Token separator = NodeFactory.createToken(SyntaxKind.COMMA_TOKEN); @@ -436,29 +450,39 @@ private SeparatedNodeList getUpdatedFields(MappingConstructorE fields.add(separator); } fields.add(getSchemaStringFieldNode(schemaString)); + fields.add(separator); + fields.add(getFieldCacheConfigNode(cacheConfigContext)); return NodeFactory.createSeparatedNodeList(fields); } - private MetadataNode getNewMetadataNode(String schemaString, String prefix) { - NodeList annotationNodes = NodeFactory.createNodeList(getSchemaStringAnnotation(schemaString, - prefix)); + private MetadataNode getNewMetadataNode(String schemaString, CacheConfigContext cacheConfigContext, String prefix) { + NodeList annotationNodes = + NodeFactory.createNodeList(getServiceAnnotation(schemaString, cacheConfigContext, prefix)); return NodeFactory.createMetadataNode(null, annotationNodes); } - private AnnotationNode getSchemaStringAnnotation(String schemaString, String prefix) { + private AnnotationNode getServiceAnnotation(String schemaString, CacheConfigContext cacheConfigContext, + String prefix) { String configIdentifierString = prefix + SyntaxKind.COLON_TOKEN.stringValue() + SERVICE_CONFIG_IDENTIFIER; IdentifierToken identifierToken = NodeFactory.createIdentifierToken(configIdentifierString); Token atToken = NodeFactory.createToken(SyntaxKind.AT_TOKEN); SimpleNameReferenceNode nameReferenceNode = NodeFactory.createSimpleNameReferenceNode(identifierToken); - MappingConstructorExpressionNode annotValue = getAnnotationExpression(schemaString); + MappingConstructorExpressionNode annotValue = getAnnotationExpression(schemaString, cacheConfigContext); return NodeFactory.createAnnotationNode(atToken, nameReferenceNode, annotValue); } - private MappingConstructorExpressionNode getAnnotationExpression(String schemaString) { + private MappingConstructorExpressionNode getAnnotationExpression(String schemaString, + CacheConfigContext cacheConfigContext) { Token openBraceToken = NodeFactory.createToken(SyntaxKind.OPEN_BRACE_TOKEN); Token closeBraceToken = NodeFactory.createToken(SyntaxKind.CLOSE_BRACE_TOKEN); - SpecificFieldNode specificFieldNode = getSchemaStringFieldNode(schemaString); - SeparatedNodeList separatedNodeList = NodeFactory.createSeparatedNodeList(specificFieldNode); + List fields = new ArrayList<>(); + SpecificFieldNode schemaFieldNode = getSchemaStringFieldNode(schemaString); + Token separator = NodeFactory.createToken(SyntaxKind.COMMA_TOKEN); + fields.add(schemaFieldNode); + fields.add(separator); + SpecificFieldNode cacheFieldNode = getFieldCacheConfigNode(cacheConfigContext); + fields.add(cacheFieldNode); + SeparatedNodeList separatedNodeList = NodeFactory.createSeparatedNodeList(fields); return NodeFactory.createMappingConstructorExpressionNode(openBraceToken, separatedNodeList, closeBraceToken); } @@ -470,6 +494,41 @@ private SpecificFieldNode getSchemaStringFieldNode(String schemaString) { return NodeFactory.createSpecificFieldNode(null, fieldName, colon, fieldValue); } + private SpecificFieldNode getFieldCacheConfigNode(CacheConfigContext cacheConfigContext) { + Node fieldName = NodeFactory.createIdentifierToken(FIELD_CACHE_CONFIG_FIELD); + Token colon = NodeFactory.createToken(SyntaxKind.COLON_TOKEN); + MappingConstructorExpressionNode fieldCacheValue = getFieldCacheConfigValueNode(cacheConfigContext); + return NodeFactory.createSpecificFieldNode(null, fieldName, colon, fieldCacheValue); + } + + private MappingConstructorExpressionNode getFieldCacheConfigValueNode(CacheConfigContext cacheConfigContext) { + Token openBraceToken = NodeFactory.createToken(SyntaxKind.OPEN_BRACE_TOKEN); + Token closeBraceToken = NodeFactory.createToken(SyntaxKind.CLOSE_BRACE_TOKEN); + List fields = new ArrayList<>(); + Token separator = NodeFactory.createToken(SyntaxKind.COMMA_TOKEN); + SpecificFieldNode cacheEnabledFieldNode = getEnabledFieldNode(cacheConfigContext.getEnabled()); + fields.add(cacheEnabledFieldNode); + fields.add(separator); + SpecificFieldNode cacheMaxSizeFieldNode = getMaxSizeFieldNode(cacheConfigContext.getMaxSize()); + fields.add(cacheMaxSizeFieldNode); + SeparatedNodeList separatedNodeList = NodeFactory.createSeparatedNodeList(fields); + return NodeFactory.createMappingConstructorExpressionNode(openBraceToken, separatedNodeList, closeBraceToken); + } + + private SpecificFieldNode getEnabledFieldNode(boolean enabledFieldCache) { + Node fieldName = NodeFactory.createIdentifierToken(ENABLED_CACHE_FIELD); + Token colon = NodeFactory.createToken(SyntaxKind.COLON_TOKEN); + ExpressionNode fieldValue = NodeParser.parseExpression(String.valueOf(enabledFieldCache)); + return NodeFactory.createSpecificFieldNode(null, fieldName, colon, fieldValue); + } + + private SpecificFieldNode getMaxSizeFieldNode(int maxSize) { + Node fieldName = NodeFactory.createIdentifierToken(MAX_SIZE_CACHE_FIELD); + Token colon = NodeFactory.createToken(SyntaxKind.COLON_TOKEN); + ExpressionNode fieldValue = NodeParser.parseExpression(String.valueOf(maxSize)); + return NodeFactory.createSpecificFieldNode(null, fieldName, colon, fieldValue); + } + private String getSchemaAsEncodedString(Schema schema) throws IOException { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ObjectOutputStream objectOutputStream = new ObjectOutputStream(outputStream); @@ -479,17 +538,6 @@ private String getSchemaAsEncodedString(Schema schema) throws IOException { return new String(Base64.getEncoder().encode(outputStream.toByteArray()), StandardCharsets.UTF_8); } - private boolean isGraphqlServiceConfig(AnnotationNode annotationNode) { - if (annotationNode.annotReference().kind() != SyntaxKind.QUALIFIED_NAME_REFERENCE) { - return false; - } - QualifiedNameReferenceNode referenceNode = ((QualifiedNameReferenceNode) annotationNode.annotReference()); - if (!PACKAGE_NAME.equals(referenceNode.modulePrefix().text())) { - return false; - } - return SERVICE_CONFIG_IDENTIFIER.equals(referenceNode.identifier().text()); - } - private void updateContext(SourceModifierContext context, Location location, CompilationDiagnostic compilerDiagnostic, String errorMessage) { DiagnosticInfo diagnosticInfo = new DiagnosticInfo(compilerDiagnostic.getDiagnosticCode(), 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 index 4b657a938..448e11f35 100644 --- 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 @@ -56,6 +56,18 @@ private Optional getAnnotationNodeFromModule() { break; } } - return functionDefinitionNodeVisitor.getAnnotationNode(); + Optional annotationNode = functionDefinitionNodeVisitor.getAnnotationNode(); + if (annotationNode.isEmpty()) { + documentIds = currentModule.testDocumentIds(); + for (DocumentId documentId : documentIds) { + Node rootNode = currentModule.document(documentId).syntaxTree().rootNode(); + rootNode.accept(functionDefinitionNodeVisitor); + if (functionDefinitionNodeVisitor.getAnnotationNode().isPresent()) { + break; + } + } + annotationNode = functionDefinitionNodeVisitor.getAnnotationNode(); + } + return annotationNode; } } 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 85bdae089..8d9cc6418 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 @@ -53,6 +53,7 @@ import io.ballerina.compiler.syntax.tree.ListConstructorExpressionNode; import io.ballerina.compiler.syntax.tree.MappingConstructorExpressionNode; import io.ballerina.compiler.syntax.tree.MappingFieldNode; +import io.ballerina.compiler.syntax.tree.MetadataNode; import io.ballerina.compiler.syntax.tree.Node; import io.ballerina.compiler.syntax.tree.NodeList; import io.ballerina.compiler.syntax.tree.ObjectConstructorExpressionNode; @@ -65,6 +66,7 @@ import io.ballerina.projects.plugins.SyntaxNodeAnalysisContext; import io.ballerina.stdlib.graphql.commons.types.Schema; import io.ballerina.stdlib.graphql.commons.types.TypeName; +import io.ballerina.stdlib.graphql.compiler.CacheConfigContext; import io.ballerina.stdlib.graphql.compiler.FinderContext; import io.ballerina.stdlib.graphql.compiler.Utils; import io.ballerina.stdlib.graphql.compiler.diagnostics.CompilationDiagnostic; @@ -83,11 +85,13 @@ import static io.ballerina.compiler.syntax.tree.SyntaxKind.SPECIFIC_FIELD; import static io.ballerina.compiler.syntax.tree.SyntaxKind.SPREAD_MEMBER; import static io.ballerina.stdlib.graphql.compiler.Utils.getAccessor; +import static io.ballerina.stdlib.graphql.compiler.Utils.getBooleanValue; import static io.ballerina.stdlib.graphql.compiler.Utils.getDefaultableParameterNode; 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.getEntityAnnotationNode; import static io.ballerina.stdlib.graphql.compiler.Utils.getEntityAnnotationSymbol; +import static io.ballerina.stdlib.graphql.compiler.Utils.getMaxSize; import static io.ballerina.stdlib.graphql.compiler.Utils.getRecordFieldWithDefaultValueNode; import static io.ballerina.stdlib.graphql.compiler.Utils.getRecordTypeDefinitionNode; import static io.ballerina.stdlib.graphql.compiler.Utils.getStringValue; @@ -97,11 +101,14 @@ 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; +import static io.ballerina.stdlib.graphql.compiler.Utils.isGraphqlServiceConfig; import static io.ballerina.stdlib.graphql.compiler.Utils.isPrimitiveTypeSymbol; import static io.ballerina.stdlib.graphql.compiler.Utils.isRemoteMethod; import static io.ballerina.stdlib.graphql.compiler.Utils.isResourceMethod; import static io.ballerina.stdlib.graphql.compiler.Utils.isServiceClass; import static io.ballerina.stdlib.graphql.compiler.Utils.isValidGraphqlParameter; +import static io.ballerina.stdlib.graphql.compiler.schema.generator.GeneratorUtils.FIELD_CACHE_CONFIG_FIELD; +import static io.ballerina.stdlib.graphql.compiler.schema.generator.GeneratorUtils.SCHEMA_STRING_FIELD; import static io.ballerina.stdlib.graphql.compiler.service.validator.ValidatorUtils.RESOURCE_FUNCTION_GET; import static io.ballerina.stdlib.graphql.compiler.service.validator.ValidatorUtils.RESOURCE_FUNCTION_SUBSCRIBE; import static io.ballerina.stdlib.graphql.compiler.service.validator.ValidatorUtils.getLocation; @@ -127,6 +134,7 @@ public class ServiceValidator { private final boolean isSubgraph; private TypeSymbol rootInputParameterTypeSymbol; private final List currentFieldPath; + private CacheConfigContext cacheConfigContext; private static final String FIELD_PATH_SEPARATOR = "."; private static final String PREFETCH_METHOD_PREFIX = "pre"; @@ -134,9 +142,11 @@ public class ServiceValidator { private static final String KEY = "key"; private static final String INPUT_OBJECT_FIELD = "input object field"; private static final String PARAMETER = "parameter"; + private static final String CACHE_CONFIG = "cacheConfig"; public ServiceValidator(SyntaxNodeAnalysisContext context, Node serviceNode, - InterfaceEntityFinder interfaceEntityFinder, boolean isSubgraph) { + InterfaceEntityFinder interfaceEntityFinder, boolean isSubgraph, + CacheConfigContext cacheConfigContext) { this.context = context; this.serviceNode = serviceNode; this.interfaceEntityFinder = interfaceEntityFinder; @@ -144,6 +154,7 @@ public ServiceValidator(SyntaxNodeAnalysisContext context, Node serviceNode, this.hasQueryType = false; this.currentFieldPath = new ArrayList<>(); this.isSubgraph = isSubgraph; + this.cacheConfigContext = cacheConfigContext; } public void validate() { @@ -219,6 +230,9 @@ public void validateKeyFieldValue(Node expression) { private void validateServiceObject() { ObjectConstructorExpressionNode objectConstructorExpNode = (ObjectConstructorExpressionNode) serviceNode; List serviceMethodNodes = getServiceMethodNodes(objectConstructorExpNode.members()); + if (!objectConstructorExpNode.annotations().isEmpty()) { + validateAnnotation(objectConstructorExpNode); + } validateRootServiceMethods(serviceMethodNodes, objectConstructorExpNode.location()); if (!this.hasQueryType) { addDiagnostic(CompilationDiagnostic.MISSING_RESOURCE_FUNCTIONS, objectConstructorExpNode.location()); @@ -242,9 +256,16 @@ public boolean isErrorOccurred() { return this.errorOccurred; } + public CacheConfigContext getCacheConfigContext() { + return this.cacheConfigContext; + } + private void validateService() { ServiceDeclarationNode serviceDeclarationNode = (ServiceDeclarationNode) this.context.node(); List serviceMethodNodes = getServiceMethodNodes(serviceDeclarationNode.members()); + if (serviceDeclarationNode.metadata().isPresent()) { + validateAnnotation(serviceDeclarationNode.metadata().get()); + } validateRootServiceMethods(serviceMethodNodes, serviceDeclarationNode.location()); if (!this.hasQueryType) { addDiagnostic(CompilationDiagnostic.MISSING_RESOURCE_FUNCTIONS, serviceDeclarationNode.location()); @@ -252,6 +273,45 @@ private void validateService() { validateEntitiesResolverReturnTypes(); } + private void validateAnnotation(MetadataNode metadataNode) { + for (AnnotationNode annotationNode : metadataNode.annotations()) { + if (isGraphqlServiceConfig(annotationNode)) { + validateServiceAnnotation(annotationNode); + } + } + } + + private void validateAnnotation(ObjectConstructorExpressionNode node) { + for (AnnotationNode annotationNode : node.annotations()) { + if (isGraphqlServiceConfig(annotationNode)) { + validateServiceAnnotation(annotationNode); + } + } + } + + private void validateServiceAnnotation(AnnotationNode annotationNode) { + if (annotationNode.annotValue().isPresent()) { + for (MappingFieldNode field : annotationNode.annotValue().get().fields()) { + validateServiceAnnotationField(field); + } + } + } + + private void validateServiceAnnotationField(MappingFieldNode field) { + if (field.kind() == 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 (SCHEMA_STRING_FIELD.equals(identifierName) || FIELD_CACHE_CONFIG_FIELD.equals(identifierName)) { + addDiagnostic(CompilationDiagnostic.INVALID_MODIFICATION_OF_SERVICE_CONFIG_FIELD, + serviceNode.location(), identifierName); + } + } + } + } + private List getServiceMethodNodes(NodeList serviceMembers) { return serviceMembers.stream().filter(this::isServiceMethod).collect(Collectors.toList()); } @@ -354,6 +414,31 @@ private String getPrefetchMethodName(AnnotationNode annotation) { return null; } + private void updateCacheConfigContextFromAnnot(AnnotationNode annotation) { + // noinspection OptionalGetWithoutIsPresent + MappingConstructorExpressionNode mappingConstructorExpressionNode = annotation.annotValue().get(); + for (MappingFieldNode field : mappingConstructorExpressionNode.fields()) { + if (field.kind() == 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 (CACHE_CONFIG.equals(identifierName) && specificFieldNode.valueExpr().isPresent()) { + boolean enabled = + getBooleanValue((MappingConstructorExpressionNode) specificFieldNode.valueExpr().get()); + if (enabled) { + int maxSize = + getMaxSize((MappingConstructorExpressionNode) specificFieldNode.valueExpr().get()); + this.cacheConfigContext.setEnabled(enabled); + this.cacheConfigContext.setMaxSize(maxSize); + } + } + } + } + } + } + private boolean hasPrefetchMethodNameConfig(AnnotationNode annotation) { if (annotation.annotValue().isEmpty()) { return false; @@ -373,6 +458,27 @@ private boolean hasPrefetchMethodNameConfig(AnnotationNode annotation) { return false; } + private boolean hasCacheConfig(AnnotationNode annotation) { + if (annotation.annotValue().isEmpty()) { + return false; + } + MappingConstructorExpressionNode mappingConstructorExpressionNode = annotation.annotValue().get(); + for (MappingFieldNode field : mappingConstructorExpressionNode.fields()) { + if (field.kind() == 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 (CACHE_CONFIG.equals(identifierName)) { + return true; + } + } + } + } + return false; + } + private MethodSymbol findPrefetchMethod(String prefetchMethodName, List serviceMethods) { return serviceMethods.stream() .filter(method -> method.kind() == SymbolKind.METHOD && !isRemoteMethod(method) && method.getName() @@ -447,9 +553,22 @@ private void validateGetResource(ResourceMethodSymbol methodSymbol, Location loc this.currentFieldPath.add(path); validateResourcePath(methodSymbol, location); validateMethod(methodSymbol, location); + updateCacheConfigContext(methodSymbol); this.currentFieldPath.remove(path); } + private void updateCacheConfigContext(MethodSymbol methodSymbol) { + if (hasResourceConfigAnnotation(methodSymbol)) { + FinderContext finderContext = new FinderContext(this.context); + ResourceConfigAnnotationFinder resourceConfigAnnotationFinder = new ResourceConfigAnnotationFinder( + finderContext, methodSymbol); + Optional annotation = resourceConfigAnnotationFinder.find(); + if (annotation.isPresent() && hasCacheConfig(annotation.get())) { + updateCacheConfigContextFromAnnot(annotation.get()); + } + } + } + private void validateSubscribeResource(ResourceMethodSymbol methodSymbol, Location location) { ResourcePath resourcePath = methodSymbol.resourcePath(); String resourcePathSignature = resourcePath.signature(); diff --git a/docs/proposals/server-side-caching.md b/docs/proposals/server-side-caching.md new file mode 100644 index 000000000..39fda45bf --- /dev/null +++ b/docs/proposals/server-side-caching.md @@ -0,0 +1,142 @@ +# Proposal: GraphQL Server-side Caching + +_Owners_: @DimuthuMadushan \ +_Reviewers_: @shafreenAnfar @ThisaruGuruge \ +_Created_: 2024/02/01 \ +_Updated_: 2024/02/01 \ +_Issue_: [#3621](https://github.com/ballerina-platform/ballerina-library/issues/3621) + +## Summary + +This proposal is to introduce GraphQL server-side caching. The GraphQL server-side caching reduces the number of data fetches from the data source and leads to lower latency responses, which enhances the user experience. + +## Goals + +* Introduce GraphQL server-side in-memory caching + +## Non-Goals + +* Introduce GraphQL server-side distributed caching + +## Motivation + +When it comes to GraphQL services, the latency, number of requests to fetch the data, and scalability are major concerns. In some specific scenarios, achieving lower latency and reducing the number of data source requests is crucial. GraphQL server-side caching can be utilized as a solution to mitigate the impact of these factors. Server-side caching significantly reduces the frequency of data fetching requests, subsequently lowering latency, which enhances the user experience. + +## Description + +GraphQL server-side caching is a crucial feature that helps developers enhance the user experience. This proposal mainly focuses on server-side in-memory caching, which utilizes query operations. To enable more granular-level cache management, it introduces configuration capabilities at operation-level and field-level. The Ballerina cache module is used to handle the cache internally using a single cache table. The GraphQL package automatically generates cache keys based on resource paths and arguments for optimized caching. In specific scenarios, developers may need to remove particular cache entries based on specific operations. Hence, the proposal includes dedicated APIs within the GraphQL context object for efficient cache invalidateion. + +### Cache Configurations + +The cache configuration record includes the configuration that is essential to enabling GraphQL caching. The cache config record consists of three fields that have default values. The enabled field can be used to enable or disable the cache. The default value is set to true. The maxAge field can be used to indicate the TTL. By default, the maxAge is set to 60 seconds. The maxSize field indicates the maximum capacity of the cache table by entries. The default cache table size is 120 entries. + +```ballerina +public type CacheConfig record {| + boolean enabled = true; + decimal maxAge = 60; + int maxSize= 120; +|}; +``` + +### Operation Level Cache + +Operation level caching can be used to cache the entire operation. If the operation-level cache configuration is provided without any field-level cache configurations, all fields from the request will be cached according to the provided operation-level cache configurations. The operation-level cache can be enabled by providing the cache configurations via GraphQL ServiceConfig. It contains an optional field `cacheConfig` that accepts a cacheConfig record type value. + +```ballerina +public type GraphqlServiceConfig record {| + CacheConfig cacheConfig?; +|}; +``` + +### Field Level Cache + +The GraphQL server-side caching can be enabled only for a specific field. This can be enabled by providing the cache configurations via ResourceConfig. The field-level cache configurations are identical to the service-level cache configurations, and it can override the operation-level configurations. + +```ballerina +public type GraphqlResourceConfig record {| + CacheConfig cacheConfig?; +|}; +``` + +### Cache Key Generation + +The cache key is generated using both the resource path and arguments. Argument values are converted into a Base64-encoded hash value, and the path is added as a prefix to the hashed arguments. In cases where there are no arguments, the cache key will be the path. The sub-fields cache key is generated in the same way as described above. It includes all the argument values encountered within that path to generate the hash. + +Ex: + +```shell +profile.name.#[(id)] +profile.friends.#[(id),{...},[ids]] +``` + +### Cache Eviction + +Cache invalidation is a crucial functionality in some cases. Hence, the dedicated APIs for cache invalidation are included in GraphQL context objects. The developers can manually invalidate the cache using the API. If developers possess the exact path for a particular field that requires removal from the cache, they can provide the path as a string to the `invalidate` function. This action will remove all cache entries associated with that specific path. Additionally, developers can clear the entire cache using the `invalidateAll` API. + +```ballerina +public isolated class Context { + + public isolated function invalidate(string path) returns error? {} + + public isolated function invalidateAll() returns error? {} +} +``` + +### Example + +Consider the following GraphQL service that enables the field-level cache. + +```ballerina +service /graphql on new graphql:Listener(9090) { + + @gaphql:ResourceConfig { + cacheConfig: { + maxSize: 30 + } + } + resource function get profile(int id) returns Person { + return new; + } +} + +service class Person { + resource function get name() returns string { + return "X"; + } + + resource function get friends(int[] ids) returns string[] { + return [“a”, “b”, “c”]; + } +} +``` + +For the query: + +```graphql +query A { + profile(id: 1) { + name + friends(ids: [1,4,5]) + } +} +``` + +It generates the following cache map. +| Key| Value| +|---|---| +| profile/name:#[1] | "X"| +| profile/friends:#[1, [1, 4, 5]] | [“a”, “b”, “c”]| + +* If the user wishes to clear the cache entry related to the `name` field, the API call should be as follows. This action will specifically remove the entry associated with the name field. + +```ballerina +context.invalidate(“profile.name”); +``` + +* If the developer intends to clear the cache associated with `profile`, the API call should follow this format. This action will remove all cache entries linked to the profile, including its sub-fields. In the provided example, executing this will clear the entire cache table, as all cache entries are connected to the profile. + +```ballerina +context.invalidate(“profile”); +``` + +* If the provided path does not match any existing cache entries, an error will be returned. Developers can handle this error. In such cases, developers have the option to either directly return an error or log a message, depending on the use case. diff --git a/docs/spec/spec.md b/docs/spec/spec.md index 0b97d52b7..4987bd07d 100644 --- a/docs/spec/spec.md +++ b/docs/spec/spec.md @@ -104,9 +104,14 @@ The conforming implementation of the specification is released and included in t * 7.1.6 [Service Interceptors](#716-service-interceptors) * 7.1.7 [Introspection Configurations](#717-introspection-configurations) * 7.1.8 [Constraint Configurations](#718-constraint-configurations) + * 7.1.9 [Operation-level Cache Configurations](#719-operation-level-cache-configurations) + * 7.1.9.1 [The `enabled` Field](#7191-the-enabled-field) + * 7.1.9.2 [The `maxAge` Field](#7192-the-maxage-field) + * 7.1.9.3 [The `maxSize` Field](#7193-the-maxsize-field) * 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.2.3 [Field-level Cache Configurations](#723-field-level-cache-configuration) * 7.3 [Interceptor Configuration](#73-interceptor-configuration) * 7.3.1 [Scope Configuration](#731-scope-configuration) * 7.4 [ID Annotation](#74-id-annotation) @@ -148,6 +153,8 @@ The conforming implementation of the specification is released and included in t * 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.1.6 [Invalidate Cache from Context](#10116-invalidate-cache-from-context) + * 10.1.1.7 [Invalidate All Caches from Context](#10117-invalidate-all-caches-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) @@ -193,6 +200,13 @@ The conforming implementation of the specification is released and included in t * 10.6.3.1 [Import `graphql.dataloader` Submodule](#10631-import-graphqldataloader-submodule) * 10.6.3.2 [Register DataLoaders to Context via ContextInit Function](#10632-register-dataloaders-to-context-via-contextinit-function) * 10.6.3.3 [Define the Corresponding `prefetch` Method](#10633-define-the-corresponding-prefetch-method) + * 10.7 [Caching](#107-caching) + * 10.7.1 [Server-side Caching](#1071-server-side-caching) + * 10.7.1.1 [Operation-level Caching](#10711-operation-level-caching) + * 10.7.1.2 [Field-level Caching](#10712-field-level-caching) + * 10.7.1.3 [Cache Invalidation](#10713-cache-invalidation) + * 10.7.1.3.1 [The `invalidate` Method](#107131-the-invalidate-method) + * 10.7.1.3.2 [The `invalidateAll` Method](#107132-the-invalidateall-method) ## 1. Overview @@ -1760,6 +1774,48 @@ service on new graphql:Listener(9090) { } ``` +#### 7.1.9 Operation-level Cache Configurations + +The `cacheConfig` field is used to provide the operation-level cache configuration to enable the [GraphQL caching](#10711-operation-level-caching) for `query` operations. + +###### Example: Enable Operation-level Cache with Default Values + +```ballerina +@graphql:ServiceConfig { + cacheConfig: {} +} +service on new graphql:Listener(9090) { + // ... +} +``` + +###### Example: Operation-level Cache Configurations + +```ballerina +@graphql:ServiceConfig { + cacheConfig: { + enabled: true + maxAge: 100, + maxSize: 150 + } +} +service on new graphql:Listener(9090) { + // ... +} +``` + +##### 7.1.9.1 The `enabled` Field + +The optinal field `enabled` accepts a `boolean` that denotes whether the server-side operation cache is enabled or not. By default, it has been set to `true`. + +##### 7.1.9.2 The `maxAge` Field + +The optional field `maxAge` accepts a valid `decimal` value which is considerd as the TTL(Time To Live) in seconds. The default maxAge is `60` seconds. + +##### 7.1.9.3 The `maxSize` Field + +The optional field `maxSize` accepts an int that denotes the maximum number of cache entries in the cache table. By default, it has been set to `120`. + ### 7.2 Resource Configuration The configurations stated in the `graphql:ResourceConfig`, are used to change the behavior of a particular GraphQL resolver. These configurations are applied to the resolver functions. @@ -1820,6 +1876,28 @@ service on new graphql:Listener(9090) { } ``` +#### 7.2.3 Field-level Cache Configuration + +The `cacheConfig` field is used to provide the [field-level cache](#10712-field-level-caching) configs. The fields are as same as the operation cache configs. + +###### Example: Field-level Cache Configs + +```ballerina +service on new graphql:Listener(9090) { + + @graphql:ResourceConfig { + cacheConfig: { + enabled: true, + maxAge: 90, + maxSize: 80 + } + } + resource function get name(int id) returns string { + // ... + } +} +``` + ### 7.3 Interceptor Configuration The configurations stated in the `graphql:InterceptorConfig`, are used to change the behavior of a particular GraphQL interceptor. @@ -2698,6 +2776,30 @@ If the specified key does not exist in the context, the `getDataLoader()` method dataloader:DataLoader authorLoader = context.getDataLoader("authorLoader"); ``` +##### 10.1.1.6 Invalidate Cache from Context + +The `invalidate()` method can be used to invalidate cache entries from the cache that are related to a particular field. It requires one parameter: + +- `path` - The path of the field that needs to be invalidated from the cache. The path should be specified as path segments combined with periods. + +If the provided path does not match any existing cache entries, an error will be returned. + +###### Example: Invalidate Cache from Context + +```ballerina +check context.invalidate("profile.address.city"); +``` + +##### 10.1.1.7 Invalidate All Caches from Context + +To clear the entire cache table, you can use the `invalidateAll` method. This method does not take any arguments. An error will be returned if the cache table cannot be cleared. + +###### Example: Invalidate All Caches from Context + +```ballerina +check context.invalidateAll(); +``` + #### 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. @@ -3496,7 +3598,7 @@ distinct service class Author { } ``` -###### Example: Overriding the Defalut `prefetch` Method Name +###### Example: Overriding the Default `prefetch` Method Name ```ballerina distinct service class Author { @@ -3513,7 +3615,7 @@ distinct service class Author { } ``` -Bringing everything together, the subsequent examples demonstrates how to engage a DataLoader with a GraphQL service. +Bringing everything together, the subsequent examples demonstrate how to engage a DataLoader with a GraphQL service. ###### Example: Utilizing a DataLoader in a GraphQL Service @@ -3647,3 +3749,189 @@ isolated function followersLoaderFunction(readonly & anydata[] ids) returns Foll ``` 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. + +### 10.7 Caching + +This section describes the caching mechanisms in the Ballerina GraphQL module. + +#### 10.7.1 Server-side Caching + +The Ballerina GraphQL module offers built-in server-side caching for GraphQL `query` operations. The caching operates as in-memory caching, implemented using the Ballerina cache module. The GraphQL module generates cache keys based on the arguments and the path. In server-side caching, the `errors` and `null` values are skipped when caching. There are two different ways called `operation-level caching` and `field-level caching` to enable server-side caching. + +##### 10.7.1.1 Operation-level Caching + +Operation-level caching can be used to cache the entire operation, and this can be enabled by providing the [operation cache configurations](#719-operation-level-cache-configurations). Once enabled, the GraphQL server initiates caching for all subfields of `query` operations. The fields requested through query operations will be cached based on the specified cache configurations + +##### 10.7.1.2 Field-level Caching + +The GraphQL field-level caching can be enabled only for a specific field. This can be done by providing the [field cache configurations](#723-field-level-cache-configuration). Once the field-level caching is enabled for a field, it will be applied to the sub-fields of that field. The field-level cache configuration can be used to override the operation-level cache configurations. + +#### 10.7.1.3 Cache Invalidation + +Since server-side caching is implemented using the Ballerina cache module, the default eviction policy will utilize the `Least Recently Used (LRU)` mechanism. In addition to LRU cache eviction, the GraphQL module provides APIs for manual cache eviction. Currently, it provides `invalidate` and `invalidateAll` APIs for manual cache eviction. These APIs can be accessed through the [graphql:Context](#101-context-object) object. + +##### 10.7.1.3.1 The `invalidate` Method + +The `invalidate` method accepts a string-type path as an argument. This method removes all cache entries related to the given path. If the provided path does not match any existing cache entries, an error will be returned. + +```ballerina +public isolated function invalidate(string path) returns error? {} +``` + +##### 10.7.1.3.2 The `invalidateAll` Method + +The `invalidateAll` method can be used to clear the entire cache table. This method does not take any arguments. An error will be returned if the cache table cannot be cleared. + +```ballerina +public isolated function invalidateAll() returns error? {} +``` + +###### Example: Operation-level Cache Enabling and Invalidation + +```ballerina +import ballerina/graphql; + +@graphql:ServiceConfig { + cacheConfig: { + enabled: true, + maxAge: 50 + } +} +service /graphql on new graphql:Listener(9090) { + private string name = "Ballerina GraphQL"; + private string 'type = "code first"; + + resource function get name() returns string { + return self.name; + } + + resource function get 'type() returns string { + return self.'type; + } + + remote function updateName(graphql:Context context, string name) returns string|error { + check context.invalidate("name"); + self.name = name + return self.name; + } +} +``` + +In this example, caching is enabled at the operation level. Therefore, the field `name` and `type` will be cached. When updating the name with a mutation, the cached values become invalid. Hence, the `invalidate` function can be used to invalidate the existing cache values. + +###### Example: Field-level Cache Enabling and Invalidation + +```ballerina +import ballerina/graphql; + +type Friend record {| + readonly string name; + int age; + boolean isMarried; +|}; + +service /graphql on new graphql:Listener(9090) { + private table key(name) friends = table [ + {name: "Skyler", age: 45, isMarried: true}, + {name: "Jesse Pinkman", age: 23, isMarried: false} + ]; + + @graphql:ResourceConfig { + cacheConfig: { + enabled: true, + maxAge 20 + } + } + isolated resource function get friends(boolean isMarried = false) returns Person[] { + if isMarried { + return from Friend friend in self.friends + where friend.isMarried == true + select new Person(friend.name, friend.age, isMarried); + } + return from Friend friend in self.friends + where friend.isMarried == false + select new Person(friend.name, friend.age, isMarried); + } + + isolated remote function updateAge(graphql:Context context, string name, int age) returns Person|error { + check context.invalidate("friends.age"); + Friend friend = self.friends.get(name); + self.friends.put({name: name, age: age, isMarried: friend.isMarried}); + return new Person(name, age, friend.isMarried); + } +} + +public isolated distinct service class Person { + private final string name; + private final int age; + private final boolean isMarried; + + public isolated function init(string name, int age, boolean isMarried) { + self.name = name; + self.age = age; + self.isMarried = isMarried; + } + + isolated resource function get name() returns string { + return self.name; + } + + isolated resource function get age() returns int { + return self.age; + } + + @graphql:ResourceConfig { + cacheConfig: { + enabled: false + } + } + isolated resource function get isMarried() returns boolean { + return self.isMarried; + } +} +``` + +In this example, GraphQL field-level caching is enabled for the `friends` field via the resource configurations. The configuration applies to its subfields, the `name` and `age` fields will be cached. Since the caching is disabled for the field `isMarried`, it will not be cached. When the age is changed using the `updateAge` operation, the `invalidate` method is used to remove the existing cache entries related to the age field. + +###### Example: Overrides Operation-level Cache Config + +```ballerina +import ballerina/graphql; + +@graphql:ServiceConfig { + cacheConfig: { + enabled: true, + maxAge: 50 + } +} +service /graphql on new graphql:Listener(9090) { + private string name = "Ballerina GraphQL"; + private string 'type = "code first"; + private string version = "V1.11.0"; + + resource function get name() returns string { + return self.name; + } + + resource function get 'type() returns string { + return self.'type; + } + + @graphql:ServiceConfig { + cacheConfig: { + enabled: false + } + } + resource function get version() returns string { + return self.'type; + } + + remote function updateName(graphql:Context context, string name) returns string|error { + check context.invalidate("name"); + self.name = name + return self.name; + } +} +``` + +In this example, caching is enabled at the operation level. Therefore, the field `name` and `type` will be cached. Since the field-level cache configuration overrides the parent cache configurations, the field `version` will not be cached. When updating the name with a mutation, the cached values become invalid. Hence, the `invalidate` function can be used to ivalidate the existing cache values. diff --git a/gradle.properties b/gradle.properties index 686e7fff8..6e401afd6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ org.gradle.caching=true group=io.ballerina.stdlib -version=1.10.1-SNAPSHOT +version=1.11.0-SNAPSHOT ballerinaLangVersion=2201.8.0 checkstylePluginVersion=10.12.0 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 1255e5dbc..f8120700b 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 @@ -516,7 +516,7 @@ private Object[] getArgumentsForMethod() { return result; } - private static Type getEffectiveType(IntersectionType intersectionType) { + static Type getEffectiveType(IntersectionType intersectionType) { for (Type constituentType : intersectionType.getConstituentTypes()) { if (constituentType.getTag() != TypeTags.READONLY_TAG) { return constituentType; 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 1831a55ba..b369d0347 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 @@ -21,8 +21,10 @@ import io.ballerina.runtime.api.Environment; import io.ballerina.runtime.api.Future; import io.ballerina.runtime.api.PredefinedTypes; +import io.ballerina.runtime.api.TypeTags; import io.ballerina.runtime.api.creators.ErrorCreator; import io.ballerina.runtime.api.creators.TypeCreator; +import io.ballerina.runtime.api.types.IntersectionType; import io.ballerina.runtime.api.types.MethodType; import io.ballerina.runtime.api.types.ObjectType; import io.ballerina.runtime.api.types.RemoteMethodType; @@ -48,6 +50,7 @@ import java.util.Base64; import java.util.List; +import static io.ballerina.stdlib.graphql.runtime.engine.ArgumentHandler.getEffectiveType; import static io.ballerina.stdlib.graphql.runtime.engine.EngineUtils.COLON; import static io.ballerina.stdlib.graphql.runtime.engine.EngineUtils.GET_ACCESSOR; import static io.ballerina.stdlib.graphql.runtime.engine.EngineUtils.INTERCEPTOR_EXECUTE; @@ -327,4 +330,28 @@ public static void executePrefetchMethod(Environment environment, BObject contex executionCallback, null, null, arguments); } } + + public static boolean hasRecordReturnType(BObject serviceObject, BArray path) { + ResourceMethodType resourceMethod = (ResourceMethodType) getResourceMethod(serviceObject, path); + if (resourceMethod == null) { + return false; + } + return isRecordReturnType(resourceMethod.getType().getReturnType()); + } + + static boolean isRecordReturnType(Type returnType) { + if (returnType.getTag() == TypeTags.UNION_TAG) { + for (Type memberType : ((UnionType) returnType).getMemberTypes()) { + if (isRecordReturnType(memberType)) { + return true; + } + } + } else if (returnType.getTag() == TypeTags.INTERSECTION_TAG) { + Type effectiveType = TypeUtils.getReferredType(getEffectiveType((IntersectionType) returnType)); + return isRecordReturnType(effectiveType); + } else if (returnType.getTag() == TypeTags.TYPE_REFERENCED_TYPE_TAG) { + return isRecordReturnType(TypeUtils.getReferredType(returnType)); + } + return returnType.getTag() == TypeTags.RECORD_TYPE_TAG; + } }