@expo/entity 0.49.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/src/EntityMutationValidator.js +2 -0
- package/build/src/EntityMutationValidator.js.map +1 -1
- package/build/src/IEntityCacheAdapter.js +2 -0
- package/build/src/IEntityCacheAdapter.js.map +1 -1
- package/build/src/IEntityCacheAdapterProvider.js +2 -0
- package/build/src/IEntityCacheAdapterProvider.js.map +1 -1
- package/build/src/IEntityDatabaseAdapterProvider.js +2 -0
- package/build/src/IEntityDatabaseAdapterProvider.js.map +1 -1
- package/build/src/IEntityGenericCacher.js +2 -0
- package/build/src/IEntityGenericCacher.js.map +1 -1
- package/package.json +2 -2
- package/src/EntityMutationValidator.ts +4 -0
- package/src/IEntityCacheAdapter.ts +4 -0
- package/src/IEntityCacheAdapterProvider.ts +4 -0
- package/src/IEntityDatabaseAdapterProvider.ts +4 -0
- package/src/IEntityGenericCacher.ts +4 -0
- package/src/__tests__/GenericEntityCacheAdapter-test.ts +24 -0
- package/src/__tests__/GenericSecondaryEntityCache-test.ts +180 -0
- package/src/utils/__testfixtures__/StubDatabaseAdapter.ts +11 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/* c8 ignore start - abstract class only */
|
|
2
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
4
|
exports.EntityMutationValidator = void 0;
|
|
4
5
|
/**
|
|
@@ -8,4 +9,5 @@ exports.EntityMutationValidator = void 0;
|
|
|
8
9
|
class EntityMutationValidator {
|
|
9
10
|
}
|
|
10
11
|
exports.EntityMutationValidator = EntityMutationValidator;
|
|
12
|
+
/* c8 ignore stop - abstract class only */
|
|
11
13
|
//# sourceMappingURL=EntityMutationValidator.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EntityMutationValidator.js","sourceRoot":"","sources":["../../src/EntityMutationValidator.ts"],"names":[],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"EntityMutationValidator.js","sourceRoot":"","sources":["../../src/EntityMutationValidator.ts"],"names":[],"mappings":";AAAA,2CAA2C;;;AAO3C;;;GAGG;AACH,MAAsB,uBAAuB;CAmB5C;AAnBD,0DAmBC;AAED,0CAA0C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IEntityCacheAdapter.js","sourceRoot":"","sources":["../../src/IEntityCacheAdapter.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"IEntityCacheAdapter.js","sourceRoot":"","sources":["../../src/IEntityCacheAdapter.ts"],"names":[],"mappings":";AAAA,sCAAsC;;AAwEtC,qCAAqC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IEntityCacheAdapterProvider.js","sourceRoot":"","sources":["../../src/IEntityCacheAdapterProvider.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"IEntityCacheAdapterProvider.js","sourceRoot":"","sources":["../../src/IEntityCacheAdapterProvider.ts"],"names":[],"mappings":";AAAA,sCAAsC;;AAkBtC,qCAAqC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IEntityDatabaseAdapterProvider.js","sourceRoot":"","sources":["../../src/IEntityDatabaseAdapterProvider.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"IEntityDatabaseAdapterProvider.js","sourceRoot":"","sources":["../../src/IEntityDatabaseAdapterProvider.ts"],"names":[],"mappings":";AAAA,sCAAsC;;AAkBtC,qCAAqC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IEntityGenericCacher.js","sourceRoot":"","sources":["../../src/IEntityGenericCacher.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"IEntityGenericCacher.js","sourceRoot":"","sources":["../../src/IEntityGenericCacher.ts"],"names":[],"mappings":";AAAA,sCAAsC;;AA4EtC,qCAAqC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/entity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
4
4
|
"description": "A privacy-first data model",
|
|
5
5
|
"files": [
|
|
6
6
|
"build",
|
|
@@ -44,5 +44,5 @@
|
|
|
44
44
|
"uuid": "^8.3.0",
|
|
45
45
|
"uuidv7": "^1.0.0"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "59e024eb19fd7a6f6d0b8f1577970b26049bc411"
|
|
48
48
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* c8 ignore start - abstract class only */
|
|
2
|
+
|
|
1
3
|
import { EntityValidatorMutationInfo } from './EntityMutationInfo';
|
|
2
4
|
import { EntityTransactionalQueryContext } from './EntityQueryContext';
|
|
3
5
|
import { ReadonlyEntity } from './ReadonlyEntity';
|
|
@@ -27,3 +29,5 @@ export abstract class EntityMutationValidator<
|
|
|
27
29
|
>,
|
|
28
30
|
): Promise<void>;
|
|
29
31
|
}
|
|
32
|
+
|
|
33
|
+
/* c8 ignore stop - abstract class only */
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* c8 ignore start - interface only */
|
|
2
|
+
|
|
1
3
|
import { IEntityLoadKey, IEntityLoadValue } from './internal/EntityLoadInterfaces';
|
|
2
4
|
import { CacheLoadResult } from './internal/ReadThroughEntityCache';
|
|
3
5
|
|
|
@@ -67,3 +69,5 @@ export interface IEntityCacheAdapter<
|
|
|
67
69
|
values: readonly TLoadValue[],
|
|
68
70
|
): Promise<void>;
|
|
69
71
|
}
|
|
72
|
+
|
|
73
|
+
/* c8 ignore stop - interface only */
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* c8 ignore start - interface only */
|
|
2
|
+
|
|
1
3
|
import { EntityConfiguration } from './EntityConfiguration';
|
|
2
4
|
import { IEntityCacheAdapter } from './IEntityCacheAdapter';
|
|
3
5
|
|
|
@@ -13,3 +15,5 @@ export interface IEntityCacheAdapterProvider {
|
|
|
13
15
|
entityConfiguration: EntityConfiguration<TFields, TIDField>,
|
|
14
16
|
): IEntityCacheAdapter<TFields, TIDField>;
|
|
15
17
|
}
|
|
18
|
+
|
|
19
|
+
/* c8 ignore stop - interface only */
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* c8 ignore start - interface only */
|
|
2
|
+
|
|
1
3
|
import { EntityConfiguration } from './EntityConfiguration';
|
|
2
4
|
import { EntityDatabaseAdapter } from './EntityDatabaseAdapter';
|
|
3
5
|
|
|
@@ -13,3 +15,5 @@ export interface IEntityDatabaseAdapterProvider {
|
|
|
13
15
|
entityConfiguration: EntityConfiguration<TFields, TIDField>,
|
|
14
16
|
): EntityDatabaseAdapter<TFields, TIDField>;
|
|
15
17
|
}
|
|
18
|
+
|
|
19
|
+
/* c8 ignore stop - interface only */
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* c8 ignore start - interface only */
|
|
2
|
+
|
|
1
3
|
import { IEntityLoadKey, IEntityLoadValue } from './internal/EntityLoadInterfaces';
|
|
2
4
|
import { CacheLoadResult } from './internal/ReadThroughEntityCache';
|
|
3
5
|
|
|
@@ -71,3 +73,5 @@ export interface IEntityGenericCacher<
|
|
|
71
73
|
value: TLoadValue,
|
|
72
74
|
): readonly string[];
|
|
73
75
|
}
|
|
76
|
+
|
|
77
|
+
/* c8 ignore stop - interface only */
|
|
@@ -3,6 +3,7 @@ import { anything, deepEqual, instance, mock, verify, when } from 'ts-mockito';
|
|
|
3
3
|
|
|
4
4
|
import { GenericEntityCacheAdapter } from '../GenericEntityCacheAdapter';
|
|
5
5
|
import { IEntityGenericCacher } from '../IEntityGenericCacher';
|
|
6
|
+
import { EntityCacheAdapterTransientError } from '../errors/EntityCacheAdapterError';
|
|
6
7
|
import { CacheStatus } from '../internal/ReadThroughEntityCache';
|
|
7
8
|
import {
|
|
8
9
|
SingleFieldHolder,
|
|
@@ -68,6 +69,29 @@ describe(GenericEntityCacheAdapter, () => {
|
|
|
68
69
|
);
|
|
69
70
|
expect(results).toEqual(new SingleFieldValueHolderMap(new Map()));
|
|
70
71
|
});
|
|
72
|
+
|
|
73
|
+
it('rethrows EntityCacheAdapterTransientError from underlying cacher', async () => {
|
|
74
|
+
const mockGenericCacher = mock<IEntityGenericCacher<BlahFields, 'id'>>();
|
|
75
|
+
when(
|
|
76
|
+
mockGenericCacher.makeCacheKeyForStorage(
|
|
77
|
+
deepEqualEntityAware(new SingleFieldHolder('id')),
|
|
78
|
+
anything(),
|
|
79
|
+
),
|
|
80
|
+
).thenCall((fieldHolder, fieldValueHolder) => {
|
|
81
|
+
return `${fieldHolder.fieldName}.${fieldValueHolder.fieldValue}`;
|
|
82
|
+
});
|
|
83
|
+
const expectedError = new EntityCacheAdapterTransientError('Transient error');
|
|
84
|
+
when(mockGenericCacher.loadManyAsync(deepEqual(['id.wat']))).thenReject(expectedError);
|
|
85
|
+
|
|
86
|
+
const cacheAdapter = new GenericEntityCacheAdapter(instance(mockGenericCacher));
|
|
87
|
+
await expect(
|
|
88
|
+
cacheAdapter.loadManyAsync(new SingleFieldHolder('id'), [
|
|
89
|
+
new SingleFieldValueHolder('wat'),
|
|
90
|
+
]),
|
|
91
|
+
).rejects.toThrow(expectedError);
|
|
92
|
+
|
|
93
|
+
verify(mockGenericCacher.loadManyAsync(anything())).once();
|
|
94
|
+
});
|
|
71
95
|
});
|
|
72
96
|
|
|
73
97
|
describe('cacheManyAsync', () => {
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, it, expect } from '@jest/globals';
|
|
2
|
+
import nullthrows from 'nullthrows';
|
|
3
|
+
|
|
4
|
+
import { EntitySecondaryCacheLoader } from '../EntitySecondaryCacheLoader';
|
|
5
|
+
import { GenericSecondaryEntityCache } from '../GenericSecondaryEntityCache';
|
|
6
|
+
import { IEntityGenericCacher } from '../IEntityGenericCacher';
|
|
7
|
+
import { ViewerContext } from '../ViewerContext';
|
|
8
|
+
import { IEntityLoadKey, IEntityLoadValue } from '../internal/EntityLoadInterfaces';
|
|
9
|
+
import { CacheLoadResult, CacheStatus } from '../internal/ReadThroughEntityCache';
|
|
10
|
+
import {
|
|
11
|
+
TestEntity,
|
|
12
|
+
TestEntityPrivacyPolicy,
|
|
13
|
+
TestFields,
|
|
14
|
+
} from '../utils/__testfixtures__/TestEntity';
|
|
15
|
+
import { createUnitTestEntityCompanionProvider } from '../utils/__testfixtures__/createUnitTestEntityCompanionProvider';
|
|
16
|
+
import { mapMapAsync } from '../utils/collections/maps';
|
|
17
|
+
|
|
18
|
+
type TestLoadParams = { intValue: number };
|
|
19
|
+
|
|
20
|
+
const DOES_NOT_EXIST = Symbol('doesNotExist');
|
|
21
|
+
|
|
22
|
+
class TestGenericCacher implements IEntityGenericCacher<TestFields, 'customIdField'> {
|
|
23
|
+
private readonly localMemoryCache = new Map<
|
|
24
|
+
string,
|
|
25
|
+
Readonly<TestFields> | typeof DOES_NOT_EXIST
|
|
26
|
+
>();
|
|
27
|
+
|
|
28
|
+
public async loadManyAsync(
|
|
29
|
+
keys: readonly string[],
|
|
30
|
+
): Promise<ReadonlyMap<string, CacheLoadResult<TestFields>>> {
|
|
31
|
+
const cacheResults = new Map<string, CacheLoadResult<TestFields>>();
|
|
32
|
+
for (const key of keys) {
|
|
33
|
+
const cacheResult = this.localMemoryCache.get(key);
|
|
34
|
+
if (cacheResult === DOES_NOT_EXIST) {
|
|
35
|
+
cacheResults.set(key, {
|
|
36
|
+
status: CacheStatus.NEGATIVE,
|
|
37
|
+
});
|
|
38
|
+
} else if (cacheResult) {
|
|
39
|
+
cacheResults.set(key, {
|
|
40
|
+
status: CacheStatus.HIT,
|
|
41
|
+
item: cacheResult as unknown as TestFields,
|
|
42
|
+
});
|
|
43
|
+
} else {
|
|
44
|
+
cacheResults.set(key, {
|
|
45
|
+
status: CacheStatus.MISS,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return cacheResults;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public async cacheManyAsync(objectMap: ReadonlyMap<string, Readonly<TestFields>>): Promise<void> {
|
|
53
|
+
for (const [key, item] of objectMap) {
|
|
54
|
+
this.localMemoryCache.set(key, item);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public async cacheDBMissesAsync(keys: readonly string[]): Promise<void> {
|
|
59
|
+
for (const key of keys) {
|
|
60
|
+
this.localMemoryCache.set(key, DOES_NOT_EXIST);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public async invalidateManyAsync(keys: readonly string[]): Promise<void> {
|
|
65
|
+
for (const key of keys) {
|
|
66
|
+
this.localMemoryCache.delete(key);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
makeCacheKeyForStorage<
|
|
71
|
+
TLoadKey extends IEntityLoadKey<TestFields, 'customIdField', TSerializedLoadValue, TLoadValue>,
|
|
72
|
+
TSerializedLoadValue,
|
|
73
|
+
TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
|
|
74
|
+
>(_key: TLoadKey, _value: TLoadValue): string {
|
|
75
|
+
throw new Error('Method not used by this test.');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
makeCacheKeysForInvalidation<
|
|
79
|
+
TLoadKey extends IEntityLoadKey<TestFields, 'customIdField', TSerializedLoadValue, TLoadValue>,
|
|
80
|
+
TSerializedLoadValue,
|
|
81
|
+
TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
|
|
82
|
+
>(_key: TLoadKey, _value: TLoadValue): readonly string[] {
|
|
83
|
+
throw new Error('Method not used by this test.');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
class TestSecondaryEntityCache<
|
|
88
|
+
TFields extends Record<string, any>,
|
|
89
|
+
TIDField extends keyof TFields,
|
|
90
|
+
TLoadParams,
|
|
91
|
+
> extends GenericSecondaryEntityCache<TFields, TIDField, TLoadParams> {}
|
|
92
|
+
|
|
93
|
+
class TestSecondaryCacheLoader extends EntitySecondaryCacheLoader<
|
|
94
|
+
TestLoadParams,
|
|
95
|
+
TestFields,
|
|
96
|
+
'customIdField',
|
|
97
|
+
ViewerContext,
|
|
98
|
+
TestEntity,
|
|
99
|
+
TestEntityPrivacyPolicy
|
|
100
|
+
> {
|
|
101
|
+
public databaseLoadCount = 0;
|
|
102
|
+
|
|
103
|
+
protected override async fetchObjectsFromDatabaseAsync(
|
|
104
|
+
loadParamsArray: readonly Readonly<TestLoadParams>[],
|
|
105
|
+
): Promise<ReadonlyMap<Readonly<Readonly<TestLoadParams>>, Readonly<TestFields> | null>> {
|
|
106
|
+
this.databaseLoadCount += loadParamsArray.length;
|
|
107
|
+
|
|
108
|
+
const emptyMap = new Map(loadParamsArray.map((p) => [p, null]));
|
|
109
|
+
return await mapMapAsync(emptyMap, async (_value, loadParams) => {
|
|
110
|
+
return (
|
|
111
|
+
(
|
|
112
|
+
await this.entityLoader.loadManyByFieldEqualityConjunctionAsync([
|
|
113
|
+
{ fieldName: 'intField', fieldValue: loadParams.intValue },
|
|
114
|
+
])
|
|
115
|
+
)[0]
|
|
116
|
+
?.enforceValue()
|
|
117
|
+
?.getAllFields() ?? null
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
describe(GenericSecondaryEntityCache, () => {
|
|
124
|
+
it('Loads through secondary loader, caches, and invalidates', async () => {
|
|
125
|
+
const viewerContext = new ViewerContext(createUnitTestEntityCompanionProvider());
|
|
126
|
+
|
|
127
|
+
const createdEntity = await TestEntity.creator(viewerContext)
|
|
128
|
+
.setField('intField', 1)
|
|
129
|
+
.createAsync();
|
|
130
|
+
|
|
131
|
+
const secondaryCacheLoader = new TestSecondaryCacheLoader(
|
|
132
|
+
new TestSecondaryEntityCache(
|
|
133
|
+
new TestGenericCacher(),
|
|
134
|
+
(params) => `intValue.${params.intValue}`,
|
|
135
|
+
),
|
|
136
|
+
TestEntity.loaderWithAuthorizationResults(viewerContext),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const loadParams = { intValue: 1 };
|
|
140
|
+
const results = await secondaryCacheLoader.loadManyAsync([loadParams]);
|
|
141
|
+
expect(nullthrows(results.get(loadParams)).enforceValue().getID()).toEqual(
|
|
142
|
+
createdEntity.getID(),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(secondaryCacheLoader.databaseLoadCount).toEqual(1);
|
|
146
|
+
|
|
147
|
+
const results2 = await secondaryCacheLoader.loadManyAsync([loadParams]);
|
|
148
|
+
expect(nullthrows(results2.get(loadParams)).enforceValue().getID()).toEqual(
|
|
149
|
+
createdEntity.getID(),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
expect(secondaryCacheLoader.databaseLoadCount).toEqual(1);
|
|
153
|
+
|
|
154
|
+
await secondaryCacheLoader.invalidateManyAsync([loadParams]);
|
|
155
|
+
|
|
156
|
+
const results3 = await secondaryCacheLoader.loadManyAsync([loadParams]);
|
|
157
|
+
expect(nullthrows(results3.get(loadParams)).enforceValue().getID()).toEqual(
|
|
158
|
+
createdEntity.getID(),
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
expect(secondaryCacheLoader.databaseLoadCount).toEqual(2);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('correctly handles uncached and unfetchable load params', async () => {
|
|
165
|
+
const viewerContext = new ViewerContext(createUnitTestEntityCompanionProvider());
|
|
166
|
+
|
|
167
|
+
const secondaryCacheLoader = new TestSecondaryCacheLoader(
|
|
168
|
+
new TestSecondaryEntityCache(
|
|
169
|
+
new TestGenericCacher(),
|
|
170
|
+
(params) => `intValue.${params.intValue}`,
|
|
171
|
+
),
|
|
172
|
+
TestEntity.loaderWithAuthorizationResults(viewerContext),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
const loadParams = { intValue: 2 };
|
|
176
|
+
const results = await secondaryCacheLoader.loadManyAsync([loadParams]);
|
|
177
|
+
expect(results.size).toBe(1);
|
|
178
|
+
expect(results.get(loadParams)).toBe(null);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -234,7 +234,13 @@ export class StubDatabaseAdapter<
|
|
|
234
234
|
const objectIndex = objectCollection.findIndex((obj) => {
|
|
235
235
|
return obj[tableIdField] === id;
|
|
236
236
|
});
|
|
237
|
-
|
|
237
|
+
|
|
238
|
+
// SQL updates to a nonexistent row succeed but affect 0 rows,
|
|
239
|
+
// mirror that behavior here for better test simulation
|
|
240
|
+
if (objectIndex < 0) {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
|
|
238
244
|
objectCollection[objectIndex] = {
|
|
239
245
|
...objectCollection[objectIndex],
|
|
240
246
|
...object,
|
|
@@ -253,9 +259,13 @@ export class StubDatabaseAdapter<
|
|
|
253
259
|
const objectIndex = objectCollection.findIndex((obj) => {
|
|
254
260
|
return obj[tableIdField] === id;
|
|
255
261
|
});
|
|
262
|
+
|
|
263
|
+
// SQL deletes to a nonexistent row succeed and affect 0 rows,
|
|
264
|
+
// mirror that behavior here for better test simulation
|
|
256
265
|
if (objectIndex < 0) {
|
|
257
266
|
return 0;
|
|
258
267
|
}
|
|
268
|
+
|
|
259
269
|
objectCollection.splice(objectIndex, 1);
|
|
260
270
|
return 1;
|
|
261
271
|
}
|