initial commit with cache support & example

This commit is contained in:
Kingkor Roy Tirtho
2022-02-07 00:11:15 +06:00
commit c7169b04c7
65 changed files with 7155 additions and 0 deletions
+362
View File
@@ -0,0 +1,362 @@
import 'package:gql_exec/gql_exec.dart';
import 'package:gql/language.dart';
import 'package:fl_query/fl_query.dart' show Fragment;
import 'package:fl_query/src/utilities/helpers.dart';
import 'package:http/http.dart' as http;
import 'package:http_parser/http_parser.dart';
const String rawOperationKey = 'rawOperationKey';
class TestCase {
TestCase({
required this.data,
required String operation,
Map<String, dynamic> variables = const <String, dynamic>{},
this.normalizedEntities,
}) : request = Request(
operation: Operation(document: parseString(operation)),
variables: variables,
context: Context(),
);
Request request;
/// data to write to cache
Map<String, dynamic> data;
/// entities to inspect the store for, if any
List<Map<String, dynamic>>? normalizedEntities;
}
final basicTest = TestCase(
operation: r'''{
a {
__typename
id
# union
list {
__typename
value
... on Item { id }
}
b {
__typename
id
c {
__typename
id,
cField
}
bField { field }
},
d {
id,
dField {field}
}
aField { field }
}
}''',
data: {
'a': {
'__typename': 'A',
'id': 1,
'list': [
{'__typename': 'Num', 'value': 1},
{'__typename': 'Num', 'value': 2},
{'__typename': 'Num', 'value': 3},
{'__typename': 'Item', 'id': 4, 'value': 4}
],
'b': {
'__typename': 'B',
'id': 5,
'c': {
'__typename': 'C',
'id': 6,
'cField': 'value',
},
'bField': {'field': true}
},
'd': {
'id': 9,
'dField': {'field': true}
},
'aField': {'field': false}
},
},
);
/// https://github.com/gql-dart/gql/blob/master/links/gql_http_link/test/multipart_upload_test.dart
final fileVarsTest = TestCase(
data: {
"multipleUpload": [
{
"id": "r1odc4PAz",
"filename": "sample_upload.jpg",
"mimetype": "image/jpeg",
"path": "./uploads/r1odc4PAz-sample_upload.jpg"
},
{
"id": "5Ea18qlMur",
"filename": "sample_upload.txt",
"mimetype": "text/plain",
"path": "./uploads/5Ea18qlMur-sample_upload.txt"
}
],
},
operation: r"""
mutation($files: [Upload!]!) {
multipleUpload(files: $files) {
id
filename
mimetype
path
}
}
""",
variables: {
'files': [
http.MultipartFile.fromBytes(
"",
[0, 1, 254, 255],
filename: "sample_upload.jpg",
contentType: MediaType("image", "jpeg"),
),
http.MultipartFile.fromString(
"",
"just plain text",
filename: "sample_upload.txt",
contentType: MediaType("text", "plain"),
),
],
},
);
final originalCValue = <String, dynamic>{
'__typename': 'C',
'id': 6,
'cField': 'value',
};
final originalCFragment = Fragment(
document: parseString(
r'''
fragment partialC on C {
__typename
id
cField
}
''',
),
);
final updatedCFragment = Fragment(
document: parseString(
r'''
fragment partialC on C {
__typename
id
new
cField
}
''',
),
);
final updatedCValue = <String, dynamic>{
'__typename': 'C',
'id': 6,
'new': 'field',
'cField': 'changed value',
};
final Map? updatedCBasicTestData = deeplyMergeLeft([
basicTest.data,
{
'a': {
'b': {
'c': {
'__typename': 'C',
'id': 6,
'cField': 'changed value',
},
},
},
},
]);
final basicTestSubsetAValue = TestCase(
operation: r'''{
a {
__typename
id
list {
__typename
value
... on Item { id }
}
d { id }
}
}''',
data: {
'a': {
'__typename': 'A',
'id': 1,
'list': [
{'__typename': 'Num', 'value': 5},
{'__typename': 'Num', 'value': 6},
{'__typename': 'Num', 'value': 7},
{
'__typename': 'Item',
'id': 8,
'value': 8,
}
],
'd': {
'id': 10,
},
},
},
);
getUpdatedSubsetOperationData({withUpdatedC = false}) => {
'a': {
'__typename': 'A',
'id': 1,
'list': basicTestSubsetAValue.data['a']['list'],
'b': {
'__typename': 'B',
'id': 5,
'c': {
'__typename': 'C',
'id': 6,
'cField': '${withUpdatedC ? "changed " : ""}value',
},
'bField': {'field': true}
},
'd': {
'id': 10,
'dField': {'field': true}
},
'aField': {'field': false}
},
};
final cyclicalTest = TestCase(operation: r'''{
a {
__typename
id
b {
__typename
id
as {
__typename
id
}
}
}
}''', data: {
'a': {
'__typename': 'A',
'id': 1,
'b': {
'__typename': 'B',
'id': 5,
'as': [
{
'__typename': 'A',
'id': 1,
},
]
},
},
}, normalizedEntities: [
{
'__typename': 'A',
'id': 1,
'b': {r"$ref": 'B:5'}
},
{
'__typename': 'B',
'id': 5,
'as': [
{r"$ref": 'A:1'}
],
},
]);
Map<String, dynamic> get cyclicalObjOperationData {
Map<String, dynamic> a;
Map<String, dynamic> b;
a = {
'__typename': 'A',
'id': 1,
};
b = {
'__typename': 'B',
'id': 5,
'as': [a]
};
a['b'] = b;
return {'a': a};
}
final typelessTest = TestCase(
operation: r'''{
a {
# union
list {
#__typename
value
#... on Item { id }
}
b {
id
c {
id,
cField
}
bField { field }
},
d {
id,
dField {field}
}
aField { field }
}
}''',
data: {
'a': {
'list': [
{
//'__typename': 'Num',
'value': 1,
},
{
//'__typename': 'Num',
'value': 2,
},
{
//'__typename': 'Num',
'value': 3,
},
{
//'__typename': 'Item',
//'id': 4,
'value': 4,
}
],
'b': {
'id': 5,
'c': {
'id': 6,
'cField': 'value',
},
'bField': {'field': true}
},
'd': {
'id': 9,
'dField': {'field': true}
},
'aField': {'field': false}
},
},
);
+313
View File
@@ -0,0 +1,313 @@
import 'package:fl_query/src/cache/_normalizing_data_proxy.dart';
import 'package:normalize/normalize.dart' show PartialDataException;
import 'package:test/test.dart';
import 'package:fl_query/src/cache/cache.dart';
import '../helpers.dart';
import './cache_data.dart';
typedef CacheTransaction = JSONDataProxy Function(JSONDataProxy proxy);
void main() {
if (debuggingUnexpectedTestFailures) {
print(
'DEBUGGING UNEXPECTED TEST FAILURES: $debuggingUnexpectedTestFailures.\n'
'RUNNING TESTS WITH returnPartialData SET TO TRUE.\n',
);
}
group('Normalizes writes', () {
late QueryCache cache;
setUp(() {
cache = getTestCache();
});
test('.writeQuery .readQuery round trip', () {
cache.writeQuery(basicTest.request, data: basicTest.data);
expect(
cache.readQuery(basicTest.request),
equals(basicTest.data),
);
});
test('typeless .writeQuery .readQuery round trip', () {
cache.writeQuery(typelessTest.request, data: typelessTest.data);
expect(
cache.readQuery(typelessTest.request),
equals(typelessTest.data),
);
});
test('typeless custom dataIdFromObject', () {
cache.writeQuery(typelessTest.request, data: typelessTest.data);
expect(
cache.readQuery(typelessTest.request),
equals(typelessTest.data),
);
});
test('.writeQuery should fail on missing fields', () {
expect(
() => cache.writeQuery(basicTest.request, data: <String, dynamic>{
...basicTest.data,
'a': <String, dynamic>{
...basicTest.data['a'],
'b': <String, dynamic>{
'id': 5,
}
},
}),
throwsA(isA<PartialDataException>().having(
(e) => e.path,
'An accurate path to the first missing subfield',
['a', 'b', '__typename'],
)),
);
});
test('updating nested normalized fragment changes top level operation', () {
cache.writeQuery(basicTest.request, data: basicTest.data);
final idFields = {
'__typename': updatedCValue['__typename'],
'id': updatedCValue['id'],
};
cache.writeFragment(
updatedCFragment.asRequest(
idFields: idFields,
),
data: updatedCValue,
);
expect(
cache.readQuery(basicTest.request),
equals(updatedCBasicTestData),
);
expect(
cache.readFragment(
updatedCFragment.asRequest(
idFields: idFields,
),
),
updatedCValue,
);
});
test('updating subset query only partially overrides superset query', () {
cache.writeQuery(basicTest.request, data: basicTest.data);
cache.writeQuery(
basicTestSubsetAValue.request,
data: basicTestSubsetAValue.data,
);
expect(
cache.readQuery(basicTest.request),
equals(getUpdatedSubsetOperationData()),
);
});
});
group('Handles cyclical references', () {
final QueryCache cache = getTestCache();
test('lazily reads cyclical references', () {
cache.writeQuery(cyclicalTest.request, data: cyclicalTest.data);
for (final normalized in cyclicalTest.normalizedEntities!) {
final dataId = "${normalized['__typename']}:${normalized['id']}";
expect(cache.readNormalized(dataId), equals(normalized));
}
});
});
group('Handles Object/pointer self-references/cycles', () {
final QueryCache cache = getTestCache();
test('correctly reads cyclical references', () {
cyclicalTest.data = cyclicalObjOperationData;
cache.writeQuery(cyclicalTest.request, data: cyclicalTest.data);
for (final normalized in cyclicalTest.normalizedEntities!) {
final dataId = "${normalized['__typename']}:${normalized['id']}";
expect(cache.readNormalized(dataId), equals(normalized));
}
});
});
group(
'.recordOptimisticTransaction',
() {
late QueryCache cache;
setUp(() {
cache = getTestCache();
});
test(
'OptimisticCache.readQuery and .readFragment pass through',
() {
cache.writeQuery(basicTest.request, data: basicTest.data);
cache.broadcastRequested = false;
cache.recordOptimisticTransaction(
(proxy) {
expect(
proxy.readQuery(basicTest.request),
equals(basicTest.data),
);
final idFields = {
'__typename': originalCValue['__typename'],
'id': originalCValue['id'],
};
expect(
proxy.readFragment(originalCFragment.asRequest(
idFields: idFields,
)),
originalCValue,
);
expect(
(proxy as NormalizingDataProxy).broadcastRequested,
isFalse,
);
return proxy;
},
'1',
);
// no edits
expect(cache.broadcastRequested, isFalse);
expect(cache.optimisticPatches.first.id, equals('1'));
expect(cache.optimisticPatches.first.data, equals({}));
},
);
test(
'.writeQuery, .readQuery(optimistic: true) round trip',
() {
cache.recordOptimisticTransaction(
(proxy) => proxy
..writeQuery(
basicTest.request,
data: basicTest.data,
),
'1',
);
expect(
cache.readQuery(basicTest.request, optimistic: true),
equals(basicTest.data),
);
},
);
recordCFragmentUpdate(QueryCache cache) =>
cache.recordOptimisticTransaction(
(proxy) => proxy
..writeFragment(
updatedCFragment.asRequest(idFields: {
'__typename': updatedCValue['__typename'],
'id': updatedCValue['id'],
}),
data: updatedCValue,
),
'2',
);
test(
'updating nested normalized fragment changes top level operation',
() {
cache.writeQuery(basicTest.request, data: basicTest.data);
recordCFragmentUpdate(cache);
expect(
cache.readQuery(basicTest.request),
equals(updatedCBasicTestData),
);
},
);
recordBasicSubsetData(QueryCache cache) =>
cache.recordOptimisticTransaction(
(proxy) => proxy
..writeQuery(
basicTestSubsetAValue.request,
data: basicTestSubsetAValue.data,
),
'3',
);
test(
'updating subset query partially overrides superset query',
() {
cache.writeQuery(basicTest.request, data: basicTest.data);
recordCFragmentUpdate(cache);
recordBasicSubsetData(cache);
expect(
cache.readQuery(basicTest.request, optimistic: true),
equals(getUpdatedSubsetOperationData(withUpdatedC: true)),
);
},
);
test(
'.removeOptimisticPatch results in data from lower layers on readQuery',
() {
cache.writeQuery(basicTest.request, data: basicTest.data);
recordCFragmentUpdate(cache);
recordBasicSubsetData(cache);
cache.removeOptimisticPatch('2');
cache.removeOptimisticPatch('3');
expect(
cache.readQuery(basicTest.request, optimistic: true),
equals(basicTest.data),
);
},
);
},
);
group('Handles MultipartFile variables', () {
late QueryCache cache;
setUp(() {
cache = getTestCache();
});
test('.writeQuery .readQuery round trip', () {
cache.writeQuery(fileVarsTest.request, data: fileVarsTest.data);
expect(
cache.readQuery(fileVarsTest.request),
equals(fileVarsTest.data),
);
});
});
group('custom dataIdFromObject', () {
/// Uses a `/` instead of the default `:`
String? customDataIdFromObject(Object object) {
if (object is Map<String, Object> &&
object.containsKey('__typename') &&
object.containsKey('id'))
return "${object['__typename']}/${object['id']}";
return null;
}
late QueryCache cache;
setUp(() {
cache = QueryCache(
dataIdFromObject: customDataIdFromObject,
partialDataPolicy: PartialDataCachePolicy.reject,
);
});
test('.writeQuery .readQuery round trip', () {
cache.writeQuery(basicTest.request, data: basicTest.data);
expect(
cache.readQuery(basicTest.request),
equals(basicTest.data),
);
});
test('typeless .writeQuery .readQuery round trip', () {
cache.writeQuery(typelessTest.request, data: typelessTest.data);
expect(
cache.readQuery(typelessTest.request),
equals(typelessTest.data),
);
});
});
}
+79
View File
@@ -0,0 +1,79 @@
import 'dart:io';
import 'package:fl_query/fl_query.dart';
import 'package:fl_query/src/utilities/helpers.dart';
import 'package:test/test.dart';
void main() {
group('InMemoryStore', () {
final data = {
'id': {'key': 'value'},
'id2': {'otherKey': false}
};
test('basic methods', () {
final store = InMemoryStore();
store.put('id', data['id']);
expect(store.get('id'), equals(data['id']));
store.delete('id');
expect(store.data, equals({}));
});
test('bulk methods', () {
final store = InMemoryStore();
store.putAll(data);
expect(store.data, equals(data));
expect(store.toMap(), equals(data));
store.reset();
expect(data['id'], notNull); // no mutations
});
});
group('HiveStore', () {
final data = {
'id': {'key': 'value'},
'id2': {'otherKey': false}
};
final path = './test/cache/test_hive_boxes/';
test('basic methods', () async {
final store =
await HiveStore.open(boxName: 'basic', path: path + 'basic');
store.put('id', data['id']);
expect(store.get('id'), equals(data['id']));
store.delete('id');
expect(store.toMap(), equals({}));
await store.box.deleteFromDisk();
});
test('bulk methods', () async {
final store = await HiveStore.open(boxName: 'bulk', path: path + 'bulk');
store.putAll(data);
expect(store.toMap(), equals(data));
await store.reset();
expect(store.toMap(), equals({}));
expect(data['id'], notNull); // no mutations
await store.box.deleteFromDisk();
});
test('box rereferencing', () async {
final store = await HiveStore.open(path: path);
store.putAll(data);
expect(HiveStore().toMap(), equals(data));
await store.box.deleteFromDisk();
});
tearDownAll(() async {
await Directory(path).delete(recursive: true);
});
});
}