@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.
@@ -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":";;;AAKA;;;GAGG;AACH,MAAsB,uBAAuB;CAmB5C;AAnBD,0DAmBC"}
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,3 +1,5 @@
1
1
  "use strict";
2
+ /* c8 ignore start - interface only */
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ /* c8 ignore stop - interface only */
3
5
  //# sourceMappingURL=IEntityCacheAdapter.js.map
@@ -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,3 +1,5 @@
1
1
  "use strict";
2
+ /* c8 ignore start - interface only */
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ /* c8 ignore stop - interface only */
3
5
  //# sourceMappingURL=IEntityCacheAdapterProvider.js.map
@@ -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,3 +1,5 @@
1
1
  "use strict";
2
+ /* c8 ignore start - interface only */
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ /* c8 ignore stop - interface only */
3
5
  //# sourceMappingURL=IEntityDatabaseAdapterProvider.js.map
@@ -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,3 +1,5 @@
1
1
  "use strict";
2
+ /* c8 ignore start - interface only */
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ /* c8 ignore stop - interface only */
3
5
  //# sourceMappingURL=IEntityGenericCacher.js.map
@@ -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.49.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": "4ab2cf1cc498f0e06bdb0f792a08aca3f49d27fc"
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
- invariant(objectIndex >= 0, 'should exist');
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
  }