@expo/entity-testing-utils 0.41.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.
Files changed (48) hide show
  1. package/README.md +3 -0
  2. package/build/PrivacyPolicyRuleTestUtils.d.ts +24 -0
  3. package/build/PrivacyPolicyRuleTestUtils.js +52 -0
  4. package/build/PrivacyPolicyRuleTestUtils.js.map +1 -0
  5. package/build/StubCacheAdapter.d.ts +25 -0
  6. package/build/StubCacheAdapter.js +103 -0
  7. package/build/StubCacheAdapter.js.map +1 -0
  8. package/build/StubDatabaseAdapter.d.ts +23 -0
  9. package/build/StubDatabaseAdapter.js +158 -0
  10. package/build/StubDatabaseAdapter.js.map +1 -0
  11. package/build/StubDatabaseAdapterProvider.d.ts +5 -0
  12. package/build/StubDatabaseAdapterProvider.js +14 -0
  13. package/build/StubDatabaseAdapterProvider.js.map +1 -0
  14. package/build/StubQueryContextProvider.d.ts +6 -0
  15. package/build/StubQueryContextProvider.js +16 -0
  16. package/build/StubQueryContextProvider.js.map +1 -0
  17. package/build/TSMockitoExtensions.d.ts +9 -0
  18. package/build/TSMockitoExtensions.js +63 -0
  19. package/build/TSMockitoExtensions.js.map +1 -0
  20. package/build/createUnitTestEntityCompanionProvider.d.ts +6 -0
  21. package/build/createUnitTestEntityCompanionProvider.js +35 -0
  22. package/build/createUnitTestEntityCompanionProvider.js.map +1 -0
  23. package/build/describeFieldTestCase.d.ts +2 -0
  24. package/build/describeFieldTestCase.js +18 -0
  25. package/build/describeFieldTestCase.js.map +1 -0
  26. package/build/index.d.ts +12 -0
  27. package/build/index.js +38 -0
  28. package/build/index.js.map +1 -0
  29. package/build/tsconfig.build.tsbuildinfo +1 -0
  30. package/package.json +48 -0
  31. package/src/PrivacyPolicyRuleTestUtils.ts +138 -0
  32. package/src/StubCacheAdapter.ts +167 -0
  33. package/src/StubDatabaseAdapter.ts +249 -0
  34. package/src/StubDatabaseAdapterProvider.ts +17 -0
  35. package/src/StubQueryContextProvider.ts +19 -0
  36. package/src/TSMockitoExtensions.ts +69 -0
  37. package/src/__testfixtures__/DateIDTestEntity.ts +62 -0
  38. package/src/__testfixtures__/SimpleTestEntity.ts +95 -0
  39. package/src/__testfixtures__/TestEntity.ts +130 -0
  40. package/src/__testfixtures__/TestEntityNumberKey.ts +62 -0
  41. package/src/__tests__/FileConsistentcyWithEntity-test.ts +32 -0
  42. package/src/__tests__/PrivacyPolicyRuleTestUtils-test.ts +44 -0
  43. package/src/__tests__/StubCacheAdapter-test.ts +134 -0
  44. package/src/__tests__/StubDatabaseAdapter-test.ts +607 -0
  45. package/src/__tests__/TSMockitoExtensions-test.ts +65 -0
  46. package/src/createUnitTestEntityCompanionProvider.ts +40 -0
  47. package/src/describeFieldTestCase.ts +21 -0
  48. package/src/index.ts +14 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"describeFieldTestCase.js","sourceRoot":"","sources":["../src/describeFieldTestCase.ts"],"names":[],"mappings":";;AAEA,wCAkBC;AAlBD,SAAwB,qBAAqB,CAC3C,eAA8C,EAC9C,WAAgB,EAChB,aAAoB;IAEpB,QAAQ,CAAC,eAAe,CAAC,WAAW,CAAC,IAAI,EAAE,GAAG,EAAE;QAC9C,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC,GAAG,eAAe,CAAC,WAAW,CAAC,IAAI,WAAW,EAAE,CAAC,KAAK,EAAE,EAAE;gBAC/E,MAAM,CAAC,eAAe,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC/D,CAAC,CAAC,CAAC;QACL,CAAC;QAED,IAAI,aAAa,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC7B,IAAI,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,GAAG,eAAe,CAAC,WAAW,CAAC,IAAI,aAAa,EAAE,CAAC,KAAK,EAAE,EAAE;gBACnF,MAAM,CAAC,eAAe,CAAC,kBAAkB,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAChE,CAAC,CAAC,CAAC;QACL,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @packageDocumentation
3
+ * @module @expo/entity-testing-utils
4
+ */
5
+ export * from './createUnitTestEntityCompanionProvider';
6
+ export { default as describeFieldTestCase } from './describeFieldTestCase';
7
+ export * from './PrivacyPolicyRuleTestUtils';
8
+ export * from './StubCacheAdapter';
9
+ export { default as StubDatabaseAdapter } from './StubDatabaseAdapter';
10
+ export { default as StubDatabaseAdapterProvider } from './StubDatabaseAdapterProvider';
11
+ export { default as StubQueryContextProvider } from './StubQueryContextProvider';
12
+ export * from './TSMockitoExtensions';
package/build/index.js ADDED
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ /* eslint-disable tsdoc/syntax */
3
+ /**
4
+ * @packageDocumentation
5
+ * @module @expo/entity-testing-utils
6
+ */
7
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
8
+ if (k2 === undefined) k2 = k;
9
+ var desc = Object.getOwnPropertyDescriptor(m, k);
10
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
11
+ desc = { enumerable: true, get: function() { return m[k]; } };
12
+ }
13
+ Object.defineProperty(o, k2, desc);
14
+ }) : (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ o[k2] = m[k];
17
+ }));
18
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
19
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
20
+ };
21
+ var __importDefault = (this && this.__importDefault) || function (mod) {
22
+ return (mod && mod.__esModule) ? mod : { "default": mod };
23
+ };
24
+ Object.defineProperty(exports, "__esModule", { value: true });
25
+ exports.StubQueryContextProvider = exports.StubDatabaseAdapterProvider = exports.StubDatabaseAdapter = exports.describeFieldTestCase = void 0;
26
+ __exportStar(require("./createUnitTestEntityCompanionProvider"), exports);
27
+ var describeFieldTestCase_1 = require("./describeFieldTestCase");
28
+ Object.defineProperty(exports, "describeFieldTestCase", { enumerable: true, get: function () { return __importDefault(describeFieldTestCase_1).default; } });
29
+ __exportStar(require("./PrivacyPolicyRuleTestUtils"), exports);
30
+ __exportStar(require("./StubCacheAdapter"), exports);
31
+ var StubDatabaseAdapter_1 = require("./StubDatabaseAdapter");
32
+ Object.defineProperty(exports, "StubDatabaseAdapter", { enumerable: true, get: function () { return __importDefault(StubDatabaseAdapter_1).default; } });
33
+ var StubDatabaseAdapterProvider_1 = require("./StubDatabaseAdapterProvider");
34
+ Object.defineProperty(exports, "StubDatabaseAdapterProvider", { enumerable: true, get: function () { return __importDefault(StubDatabaseAdapterProvider_1).default; } });
35
+ var StubQueryContextProvider_1 = require("./StubQueryContextProvider");
36
+ Object.defineProperty(exports, "StubQueryContextProvider", { enumerable: true, get: function () { return __importDefault(StubQueryContextProvider_1).default; } });
37
+ __exportStar(require("./TSMockitoExtensions"), exports);
38
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,iCAAiC;AACjC;;;GAGG;;;;;;;;;;;;;;;;;;;;AAEH,0EAAwD;AACxD,iEAA2E;AAAlE,+IAAA,OAAO,OAAyB;AACzC,+DAA6C;AAC7C,qDAAmC;AACnC,6DAAuE;AAA9D,2IAAA,OAAO,OAAuB;AACvC,6EAAuF;AAA9E,2JAAA,OAAO,OAA+B;AAC/C,uEAAiF;AAAxE,qJAAA,OAAO,OAA4B;AAC5C,wDAAsC"}
@@ -0,0 +1 @@
1
+ {"root":["../src/privacypolicyruletestutils.ts","../src/stubcacheadapter.ts","../src/stubdatabaseadapter.ts","../src/stubdatabaseadapterprovider.ts","../src/stubquerycontextprovider.ts","../src/tsmockitoextensions.ts","../src/createunittestentitycompanionprovider.ts","../src/describefieldtestcase.ts","../src/index.ts"],"version":"5.8.3"}
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@expo/entity-testing-utils",
3
+ "version": "0.41.0",
4
+ "description": "A package containing utilities for testing applications that use Entity",
5
+ "files": [
6
+ "build",
7
+ "src"
8
+ ],
9
+ "main": "build/index.js",
10
+ "types": "build/index.d.ts",
11
+ "scripts": {
12
+ "tsc": "tsc",
13
+ "build": "tsc -b tsconfig.build.json",
14
+ "clean": "rm -rf build coverage coverage-integration",
15
+ "lint": "eslint src",
16
+ "lint-fix": "eslint src --fix",
17
+ "test": "jest --rootDir . --config ../../resources/jest.config.js",
18
+ "ctix": "ctix build --config ../../.ctirc && ../../resources/prepend-barrel.sh '@expo/entity-testing-utils'"
19
+ },
20
+ "engines": {
21
+ "node": ">=16"
22
+ },
23
+ "keywords": [
24
+ "entity"
25
+ ],
26
+ "author": "Expo",
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "@expo/entity": "workspace:^",
30
+ "lodash": "^4.17.21",
31
+ "ts-mockito": "^2.6.1"
32
+ },
33
+ "devDependencies": {
34
+ "@types/jest": "^29.5.14",
35
+ "@types/lodash": "^4.17.16",
36
+ "@types/node": "^20.14.1",
37
+ "ctix": "^2.7.0",
38
+ "eslint": "^8.57.1",
39
+ "eslint-config-universe": "^14.0.0",
40
+ "eslint-plugin-tsdoc": "^0.3.0",
41
+ "jest": "^29.7.0",
42
+ "prettier": "^3.3.3",
43
+ "prettier-plugin-organize-imports": "^4.1.0",
44
+ "ts-jest": "^29.3.1",
45
+ "ts-mockito": "^2.6.1",
46
+ "typescript": "^5.8.3"
47
+ }
48
+ }
@@ -0,0 +1,138 @@
1
+ import {
2
+ EntityPrivacyPolicyEvaluationContext,
3
+ EntityQueryContext,
4
+ ReadonlyEntity,
5
+ ViewerContext,
6
+ PrivacyPolicyRule,
7
+ RuleEvaluationResult,
8
+ } from '@expo/entity';
9
+
10
+ export interface Case<
11
+ TFields extends Record<string, any>,
12
+ TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
13
+ TViewerContext extends ViewerContext,
14
+ TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
15
+ TSelectedFields extends keyof TFields,
16
+ > {
17
+ viewerContext: TViewerContext;
18
+ queryContext: EntityQueryContext;
19
+ evaluationContext: EntityPrivacyPolicyEvaluationContext<
20
+ TFields,
21
+ TIDField,
22
+ TViewerContext,
23
+ TEntity,
24
+ TSelectedFields
25
+ >;
26
+ entity: TEntity;
27
+ }
28
+
29
+ export type CaseMap<
30
+ TFields extends Record<string, any>,
31
+ TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
32
+ TViewerContext extends ViewerContext,
33
+ TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
34
+ TSelectedFields extends keyof TFields,
35
+ > = Map<string, () => Promise<Case<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>>>;
36
+
37
+ /**
38
+ * Useful for defining test cases that have async preconditions.
39
+ */
40
+ export const describePrivacyPolicyRuleWithAsyncTestCase = <
41
+ TFields extends Record<string, any>,
42
+ TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
43
+ TViewerContext extends ViewerContext,
44
+ TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
45
+ TSelectedFields extends keyof TFields = keyof TFields,
46
+ >(
47
+ privacyPolicyRule: PrivacyPolicyRule<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>,
48
+ {
49
+ allowCases = new Map(),
50
+ skipCases = new Map(),
51
+ denyCases = new Map(),
52
+ }: {
53
+ allowCases?: CaseMap<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>;
54
+ skipCases?: CaseMap<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>;
55
+ denyCases?: CaseMap<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>;
56
+ },
57
+ ): void => {
58
+ describe(privacyPolicyRule.constructor.name, () => {
59
+ if (allowCases && allowCases.size > 0) {
60
+ describe('allow cases', () => {
61
+ test.each(Array.from(allowCases.keys()))('%p', async (caseKey) => {
62
+ const { viewerContext, queryContext, evaluationContext, entity } =
63
+ await allowCases.get(caseKey)!();
64
+ await expect(
65
+ privacyPolicyRule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity),
66
+ ).resolves.toEqual(RuleEvaluationResult.ALLOW);
67
+ });
68
+ });
69
+ }
70
+
71
+ if (skipCases && skipCases.size > 0) {
72
+ describe('skip cases', () => {
73
+ test.each(Array.from(skipCases.keys()))('%p', async (caseKey) => {
74
+ const { viewerContext, queryContext, evaluationContext, entity } =
75
+ await skipCases.get(caseKey)!();
76
+ await expect(
77
+ privacyPolicyRule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity),
78
+ ).resolves.toEqual(RuleEvaluationResult.SKIP);
79
+ });
80
+ });
81
+ }
82
+
83
+ if (denyCases && denyCases.size > 0) {
84
+ describe('deny cases', () => {
85
+ test.each(Array.from(denyCases.keys()))('%p', async (caseKey) => {
86
+ const { viewerContext, queryContext, evaluationContext, entity } =
87
+ await denyCases.get(caseKey)!();
88
+ await expect(
89
+ privacyPolicyRule.evaluateAsync(viewerContext, queryContext, evaluationContext, entity),
90
+ ).resolves.toEqual(RuleEvaluationResult.DENY);
91
+ });
92
+ });
93
+ }
94
+ });
95
+ };
96
+
97
+ /**
98
+ * For test simple privacy rules that don't have complex async preconditions.
99
+ */
100
+ export const describePrivacyPolicyRule = <
101
+ TFields extends Record<string, any>,
102
+ TIDField extends keyof NonNullable<Pick<TFields, TSelectedFields>>,
103
+ TViewerContext extends ViewerContext,
104
+ TEntity extends ReadonlyEntity<TFields, TIDField, TViewerContext, TSelectedFields>,
105
+ TSelectedFields extends keyof TFields = keyof TFields,
106
+ >(
107
+ privacyPolicyRule: PrivacyPolicyRule<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>,
108
+ {
109
+ allowCases = [],
110
+ skipCases = [],
111
+ denyCases = [],
112
+ }: {
113
+ allowCases?: Case<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>[];
114
+ skipCases?: Case<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>[];
115
+ denyCases?: Case<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>[];
116
+ },
117
+ ): void => {
118
+ const makeCasesMap = (
119
+ cases: Case<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>[],
120
+ ): CaseMap<TFields, TIDField, TViewerContext, TEntity, TSelectedFields> =>
121
+ cases.reduce(
122
+ (
123
+ acc: CaseMap<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>,
124
+ testCase: Case<TFields, TIDField, TViewerContext, TEntity, TSelectedFields>,
125
+ index,
126
+ ) => {
127
+ acc.set(`case ${index}`, async () => testCase);
128
+ return acc;
129
+ },
130
+ new Map(),
131
+ );
132
+
133
+ describePrivacyPolicyRuleWithAsyncTestCase(privacyPolicyRule, {
134
+ allowCases: makeCasesMap(allowCases),
135
+ skipCases: makeCasesMap(skipCases),
136
+ denyCases: makeCasesMap(denyCases),
137
+ });
138
+ };
@@ -0,0 +1,167 @@
1
+ import {
2
+ EntityConfiguration,
3
+ IEntityCacheAdapter,
4
+ IEntityCacheAdapterProvider,
5
+ IEntityLoadKey,
6
+ IEntityLoadValue,
7
+ CacheStatus,
8
+ CacheLoadResult,
9
+ } from '@expo/entity';
10
+ import invariant from 'invariant';
11
+
12
+ export class NoCacheStubCacheAdapterProvider implements IEntityCacheAdapterProvider {
13
+ getCacheAdapter<TFields extends Record<string, any>, TIDField extends keyof TFields>(
14
+ _entityConfiguration: EntityConfiguration<TFields, TIDField>,
15
+ ): IEntityCacheAdapter<TFields, TIDField> {
16
+ return new NoCacheStubCacheAdapter();
17
+ }
18
+ }
19
+
20
+ export class NoCacheStubCacheAdapter<
21
+ TFields extends Record<string, any>,
22
+ TIDField extends keyof TFields,
23
+ > implements IEntityCacheAdapter<TFields, TIDField>
24
+ {
25
+ public async loadManyAsync<
26
+ TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
27
+ TSerializedLoadValue,
28
+ TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
29
+ >(
30
+ key: TLoadKey,
31
+ values: readonly TLoadValue[],
32
+ ): Promise<ReadonlyMap<TLoadValue, CacheLoadResult<TFields>>> {
33
+ return values.reduce((acc: Map<TLoadValue, CacheLoadResult<TFields>>, v) => {
34
+ acc.set(v, {
35
+ status: CacheStatus.MISS,
36
+ });
37
+ return acc;
38
+ }, key.vendNewLoadValueMap<CacheLoadResult<TFields>>());
39
+ }
40
+
41
+ public async cacheManyAsync<
42
+ TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
43
+ TSerializedLoadValue,
44
+ TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
45
+ >(_key: TLoadKey, _objectMap: ReadonlyMap<TLoadValue, Readonly<TFields>>): Promise<void> {}
46
+
47
+ public async cacheDBMissesAsync<
48
+ TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
49
+ TSerializedLoadValue,
50
+ TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
51
+ >(_key: TLoadKey, _values: readonly TLoadValue[]): Promise<void> {}
52
+
53
+ public async invalidateManyAsync<
54
+ TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
55
+ TSerializedLoadValue,
56
+ TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
57
+ >(_key: TLoadKey, _values: readonly TLoadValue[]): Promise<void> {}
58
+ }
59
+
60
+ // Sentinel value we store in the in-memory cache to negatively cache a database miss.
61
+ // The sentinel value is distinct from any (positively) cached value.
62
+ export const DOES_NOT_EXIST = Symbol('inMemoryCacheDoesNotExistValue');
63
+
64
+ export class InMemoryFullCacheStubCacheAdapterProvider implements IEntityCacheAdapterProvider {
65
+ private readonly cache: Map<string, Readonly<object> | typeof DOES_NOT_EXIST> = new Map();
66
+
67
+ getCacheAdapter<TFields extends Record<string, any>, TIDField extends keyof TFields>(
68
+ entityConfiguration: EntityConfiguration<TFields, TIDField>,
69
+ ): IEntityCacheAdapter<TFields, TIDField> {
70
+ return new InMemoryFullCacheStubCacheAdapter(
71
+ entityConfiguration,
72
+ this.cache as Map<string, Readonly<TFields>>,
73
+ );
74
+ }
75
+ }
76
+
77
+ export class InMemoryFullCacheStubCacheAdapter<
78
+ TFields extends Record<string, any>,
79
+ TIDField extends keyof TFields,
80
+ > implements IEntityCacheAdapter<TFields, TIDField>
81
+ {
82
+ constructor(
83
+ private readonly entityConfiguration: EntityConfiguration<TFields, TIDField>,
84
+ private readonly cache: Map<string, Readonly<TFields> | typeof DOES_NOT_EXIST>,
85
+ ) {}
86
+
87
+ public async loadManyAsync<
88
+ TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
89
+ TSerializedLoadValue,
90
+ TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
91
+ >(
92
+ key: TLoadKey,
93
+ values: readonly TLoadValue[],
94
+ ): Promise<ReadonlyMap<TLoadValue, CacheLoadResult<TFields>>> {
95
+ const results = key.vendNewLoadValueMap<CacheLoadResult<TFields>>();
96
+ values.forEach((value) => {
97
+ const cacheKey = this.createCacheKey(key, value);
98
+ if (!this.cache.has(cacheKey)) {
99
+ results.set(value, {
100
+ status: CacheStatus.MISS,
101
+ });
102
+ } else {
103
+ const objectForFieldValue = this.cache.get(cacheKey);
104
+ invariant(objectForFieldValue !== undefined, 'should have set value for key');
105
+ if (objectForFieldValue === DOES_NOT_EXIST) {
106
+ results.set(value, {
107
+ status: CacheStatus.NEGATIVE,
108
+ });
109
+ } else {
110
+ results.set(value, {
111
+ status: CacheStatus.HIT,
112
+ item: objectForFieldValue,
113
+ });
114
+ }
115
+ }
116
+ });
117
+ return results;
118
+ }
119
+
120
+ public async cacheManyAsync<
121
+ TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
122
+ TSerializedLoadValue,
123
+ TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
124
+ >(key: TLoadKey, objectMap: ReadonlyMap<TLoadValue, Readonly<TFields>>): Promise<void> {
125
+ objectMap.forEach((obj, value) => {
126
+ const cacheKey = this.createCacheKey(key, value);
127
+ this.cache.set(cacheKey, obj);
128
+ });
129
+ }
130
+
131
+ public async cacheDBMissesAsync<
132
+ TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
133
+ TSerializedLoadValue,
134
+ TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
135
+ >(key: TLoadKey, values: readonly TLoadValue[]): Promise<void> {
136
+ values.forEach((value) => {
137
+ const cacheKey = this.createCacheKey(key, value);
138
+ this.cache.set(cacheKey, DOES_NOT_EXIST);
139
+ });
140
+ }
141
+
142
+ public async invalidateManyAsync<
143
+ TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
144
+ TSerializedLoadValue,
145
+ TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
146
+ >(key: TLoadKey, values: readonly TLoadValue[]): Promise<void> {
147
+ values.forEach((value) => {
148
+ const cacheKey = this.createCacheKey(key, value);
149
+ this.cache.delete(cacheKey);
150
+ });
151
+ }
152
+
153
+ private createCacheKey<
154
+ TLoadKey extends IEntityLoadKey<TFields, TIDField, TSerializedLoadValue, TLoadValue>,
155
+ TSerializedLoadValue,
156
+ TLoadValue extends IEntityLoadValue<TSerializedLoadValue>,
157
+ >(key: TLoadKey, value: TLoadValue): string {
158
+ const cacheKeyType = key.getLoadMethodType();
159
+ const parts = key.createCacheKeyPartsForLoadValue(this.entityConfiguration, value);
160
+ return [
161
+ this.entityConfiguration.tableName,
162
+ cacheKeyType,
163
+ `v${this.entityConfiguration.cacheKeyVersion}`,
164
+ ...parts,
165
+ ].join(':');
166
+ }
167
+ }
@@ -0,0 +1,249 @@
1
+ import {
2
+ EntityConfiguration,
3
+ EntityDatabaseAdapter,
4
+ TableFieldSingleValueEqualityCondition,
5
+ TableFieldMultiValueEqualityCondition,
6
+ TableQuerySelectionModifiers,
7
+ OrderByOrdering,
8
+ StringField,
9
+ IntField,
10
+ getDatabaseFieldForEntityField,
11
+ FieldTransformerMap,
12
+ transformFieldsToDatabaseObject,
13
+ computeIfAbsent,
14
+ mapMap,
15
+ } from '@expo/entity';
16
+ import invariant from 'invariant';
17
+ import { uuidv7 } from 'uuidv7';
18
+
19
+ export default class StubDatabaseAdapter<
20
+ TFields extends Record<string, any>,
21
+ TIDField extends keyof TFields,
22
+ > extends EntityDatabaseAdapter<TFields, TIDField> {
23
+ constructor(
24
+ private readonly entityConfiguration2: EntityConfiguration<TFields, TIDField>,
25
+ private readonly dataStore: Map<string, Readonly<{ [key: string]: any }>[]>,
26
+ ) {
27
+ super(entityConfiguration2);
28
+ }
29
+
30
+ public static convertFieldObjectsToDataStore<
31
+ TFields extends Record<string, any>,
32
+ TIDField extends keyof TFields,
33
+ >(
34
+ entityConfiguration: EntityConfiguration<TFields, TIDField>,
35
+ dataStore: Map<string, Readonly<TFields>[]>,
36
+ ): Map<string, Readonly<{ [key: string]: any }>[]> {
37
+ return mapMap(dataStore, (objectsForTable) =>
38
+ objectsForTable.map((objectForTable) =>
39
+ transformFieldsToDatabaseObject(entityConfiguration, new Map(), objectForTable),
40
+ ),
41
+ );
42
+ }
43
+
44
+ public getObjectCollectionForTable(tableName: string): { [key: string]: any }[] {
45
+ return computeIfAbsent(this.dataStore, tableName, () => []);
46
+ }
47
+
48
+ protected getFieldTransformerMap(): FieldTransformerMap {
49
+ return new Map();
50
+ }
51
+
52
+ protected async fetchManyWhereInternalAsync(
53
+ _queryInterface: any,
54
+ tableName: string,
55
+ tableColumns: readonly string[],
56
+ tableTuples: (readonly any[])[],
57
+ ): Promise<object[]> {
58
+ const objectCollection = this.getObjectCollectionForTable(tableName);
59
+ const results = tableTuples.reduce(
60
+ (acc, tableTuple) => {
61
+ return acc.concat(
62
+ objectCollection.filter((obj) => {
63
+ return tableColumns.every((tableColumn, index) => {
64
+ return obj[tableColumn] === tableTuple[index];
65
+ });
66
+ }),
67
+ );
68
+ },
69
+ [] as { [key: string]: any }[],
70
+ );
71
+ return [...results];
72
+ }
73
+
74
+ private static compareByOrderBys(
75
+ orderBys: {
76
+ columnName: string;
77
+ order: OrderByOrdering;
78
+ }[],
79
+ objectA: { [key: string]: any },
80
+ objectB: { [key: string]: any },
81
+ ): 0 | 1 | -1 {
82
+ if (orderBys.length === 0) {
83
+ return 0;
84
+ }
85
+
86
+ const currentOrderBy = orderBys[0]!;
87
+ const aField = objectA[currentOrderBy.columnName];
88
+ const bField = objectB[currentOrderBy.columnName];
89
+ switch (currentOrderBy.order) {
90
+ case OrderByOrdering.DESCENDING: {
91
+ // simulate NULLS FIRST for DESC
92
+ if (aField === null && bField === null) {
93
+ return 0;
94
+ } else if (aField === null) {
95
+ return -1;
96
+ } else if (bField === null) {
97
+ return 1;
98
+ }
99
+
100
+ return aField > bField
101
+ ? -1
102
+ : aField < bField
103
+ ? 1
104
+ : this.compareByOrderBys(orderBys.slice(1), objectA, objectB);
105
+ }
106
+ case OrderByOrdering.ASCENDING: {
107
+ // simulate NULLS LAST for ASC
108
+ if (aField === null && bField === null) {
109
+ return 0;
110
+ } else if (bField === null) {
111
+ return -1;
112
+ } else if (aField === null) {
113
+ return 1;
114
+ }
115
+
116
+ return bField > aField
117
+ ? -1
118
+ : bField < aField
119
+ ? 1
120
+ : this.compareByOrderBys(orderBys.slice(1), objectA, objectB);
121
+ }
122
+ }
123
+ }
124
+
125
+ protected async fetchManyByFieldEqualityConjunctionInternalAsync(
126
+ _queryInterface: any,
127
+ tableName: string,
128
+ tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
129
+ tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
130
+ querySelectionModifiers: TableQuerySelectionModifiers,
131
+ ): Promise<object[]> {
132
+ let filteredObjects = this.getObjectCollectionForTable(tableName);
133
+ for (const { tableField, tableValue } of tableFieldSingleValueEqualityOperands) {
134
+ filteredObjects = filteredObjects.filter((obj) => obj[tableField] === tableValue);
135
+ }
136
+
137
+ for (const { tableField, tableValues } of tableFieldMultiValueEqualityOperands) {
138
+ filteredObjects = filteredObjects.filter((obj) => tableValues.includes(obj[tableField]));
139
+ }
140
+
141
+ const orderBy = querySelectionModifiers.orderBy;
142
+ if (orderBy !== undefined) {
143
+ filteredObjects = filteredObjects.sort((a, b) =>
144
+ StubDatabaseAdapter.compareByOrderBys(orderBy, a, b),
145
+ );
146
+ }
147
+
148
+ const offset = querySelectionModifiers.offset;
149
+ if (offset !== undefined) {
150
+ filteredObjects = filteredObjects.slice(offset);
151
+ }
152
+
153
+ const limit = querySelectionModifiers.limit;
154
+ if (limit !== undefined) {
155
+ filteredObjects = filteredObjects.slice(0, 0 + limit);
156
+ }
157
+
158
+ return filteredObjects;
159
+ }
160
+
161
+ protected fetchManyByRawWhereClauseInternalAsync(
162
+ _queryInterface: any,
163
+ _tableName: string,
164
+ _rawWhereClause: string,
165
+ _bindings: object | any[],
166
+ _querySelectionModifiers: TableQuerySelectionModifiers,
167
+ ): Promise<object[]> {
168
+ throw new Error('Raw WHERE clauses not supported for StubDatabaseAdapter');
169
+ }
170
+
171
+ private generateRandomID(): any {
172
+ const idSchemaField = this.entityConfiguration2.schema.get(this.entityConfiguration2.idField);
173
+ invariant(
174
+ idSchemaField,
175
+ `No schema field found for ${String(this.entityConfiguration2.idField)}`,
176
+ );
177
+ if (idSchemaField instanceof StringField) {
178
+ return uuidv7();
179
+ } else if (idSchemaField instanceof IntField) {
180
+ return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
181
+ } else {
182
+ throw new Error(
183
+ `Unsupported ID type for StubDatabaseAdapter: ${idSchemaField.constructor.name}`,
184
+ );
185
+ }
186
+ }
187
+
188
+ protected async insertInternalAsync(
189
+ _queryInterface: any,
190
+ tableName: string,
191
+ object: object,
192
+ ): Promise<object[]> {
193
+ const objectCollection = this.getObjectCollectionForTable(tableName);
194
+
195
+ const idField = getDatabaseFieldForEntityField(
196
+ this.entityConfiguration2,
197
+ this.entityConfiguration2.idField,
198
+ );
199
+ const objectToInsert = {
200
+ [idField]: this.generateRandomID(),
201
+ ...object,
202
+ };
203
+ objectCollection.push(objectToInsert);
204
+ return [objectToInsert];
205
+ }
206
+
207
+ protected async updateInternalAsync(
208
+ _queryInterface: any,
209
+ tableName: string,
210
+ tableIdField: string,
211
+ id: any,
212
+ object: object,
213
+ ): Promise<object[]> {
214
+ // SQL does not support empty updates, mirror behavior here for better test simulation
215
+ if (Object.keys(object).length === 0) {
216
+ throw new Error(`Empty update (${tableIdField} = ${id})`);
217
+ }
218
+
219
+ const objectCollection = this.getObjectCollectionForTable(tableName);
220
+
221
+ const objectIndex = objectCollection.findIndex((obj) => {
222
+ return obj[tableIdField] === id;
223
+ });
224
+ invariant(objectIndex >= 0, 'should exist');
225
+ objectCollection[objectIndex] = {
226
+ ...objectCollection[objectIndex],
227
+ ...object,
228
+ };
229
+ return [objectCollection[objectIndex]];
230
+ }
231
+
232
+ protected async deleteInternalAsync(
233
+ _queryInterface: any,
234
+ tableName: string,
235
+ tableIdField: string,
236
+ id: any,
237
+ ): Promise<number> {
238
+ const objectCollection = this.getObjectCollectionForTable(tableName);
239
+
240
+ const objectIndex = objectCollection.findIndex((obj) => {
241
+ return obj[tableIdField] === id;
242
+ });
243
+ if (objectIndex < 0) {
244
+ return 0;
245
+ }
246
+ objectCollection.splice(objectIndex, 1);
247
+ return 1;
248
+ }
249
+ }
@@ -0,0 +1,17 @@
1
+ import {
2
+ EntityConfiguration,
3
+ EntityDatabaseAdapter,
4
+ IEntityDatabaseAdapterProvider,
5
+ } from '@expo/entity';
6
+
7
+ import StubDatabaseAdapter from './StubDatabaseAdapter';
8
+
9
+ export default class StubDatabaseAdapterProvider implements IEntityDatabaseAdapterProvider {
10
+ private readonly objectCollection = new Map();
11
+
12
+ getDatabaseAdapter<TFields extends Record<string, any>, TIDField extends keyof TFields>(
13
+ entityConfiguration: EntityConfiguration<TFields, TIDField>,
14
+ ): EntityDatabaseAdapter<TFields, TIDField> {
15
+ return new StubDatabaseAdapter(entityConfiguration, this.objectCollection);
16
+ }
17
+ }