@expo/entity-database-adapter-knex 0.54.0 → 0.57.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/AuthorizationResultBasedKnexEntityLoader.d.ts +279 -0
- package/build/src/AuthorizationResultBasedKnexEntityLoader.js +127 -0
- package/build/src/AuthorizationResultBasedKnexEntityLoader.js.map +1 -0
- package/build/src/BasePostgresEntityDatabaseAdapter.d.ts +150 -0
- package/build/src/BasePostgresEntityDatabaseAdapter.js +119 -0
- package/build/src/BasePostgresEntityDatabaseAdapter.js.map +1 -0
- package/build/src/BaseSQLQueryBuilder.d.ts +61 -0
- package/build/src/BaseSQLQueryBuilder.js +87 -0
- package/build/src/BaseSQLQueryBuilder.js.map +1 -0
- package/build/src/EnforcingKnexEntityLoader.d.ts +124 -0
- package/build/src/EnforcingKnexEntityLoader.js +166 -0
- package/build/src/EnforcingKnexEntityLoader.js.map +1 -0
- package/build/src/KnexEntityLoaderFactory.d.ts +25 -0
- package/build/src/KnexEntityLoaderFactory.js +39 -0
- package/build/src/KnexEntityLoaderFactory.js.map +1 -0
- package/build/src/PaginationStrategy.d.ts +30 -0
- package/build/src/PaginationStrategy.js +35 -0
- package/build/src/PaginationStrategy.js.map +1 -0
- package/build/src/PostgresEntity.d.ts +25 -0
- package/build/src/PostgresEntity.js +39 -0
- package/build/src/PostgresEntity.js.map +1 -0
- package/build/src/PostgresEntityDatabaseAdapter.d.ts +12 -5
- package/build/src/PostgresEntityDatabaseAdapter.js +32 -11
- package/build/src/PostgresEntityDatabaseAdapter.js.map +1 -1
- package/build/src/PostgresEntityDatabaseAdapterProvider.d.ts +9 -0
- package/build/src/PostgresEntityDatabaseAdapterProvider.js +5 -1
- package/build/src/PostgresEntityDatabaseAdapterProvider.js.map +1 -1
- package/build/src/ReadonlyPostgresEntity.d.ts +25 -0
- package/build/src/ReadonlyPostgresEntity.js +39 -0
- package/build/src/ReadonlyPostgresEntity.js.map +1 -0
- package/build/src/SQLOperator.d.ts +261 -0
- package/build/src/SQLOperator.js +464 -0
- package/build/src/SQLOperator.js.map +1 -0
- package/build/src/index.d.ts +15 -0
- package/build/src/index.js +15 -0
- package/build/src/index.js.map +1 -1
- package/build/src/internal/EntityKnexDataManager.d.ts +147 -0
- package/build/src/internal/EntityKnexDataManager.js +453 -0
- package/build/src/internal/EntityKnexDataManager.js.map +1 -0
- package/build/src/internal/getKnexDataManager.d.ts +3 -0
- package/build/src/internal/getKnexDataManager.js +19 -0
- package/build/src/internal/getKnexDataManager.js.map +1 -0
- package/build/src/internal/getKnexEntityLoaderFactory.d.ts +3 -0
- package/build/src/internal/getKnexEntityLoaderFactory.js +11 -0
- package/build/src/internal/getKnexEntityLoaderFactory.js.map +1 -0
- package/build/src/internal/utilityTypes.d.ts +5 -0
- package/build/src/internal/utilityTypes.js +5 -0
- package/build/src/internal/utilityTypes.js.map +1 -0
- package/build/src/internal/weakMaps.d.ts +9 -0
- package/build/src/internal/weakMaps.js +20 -0
- package/build/src/internal/weakMaps.js.map +1 -0
- package/build/src/knexLoader.d.ts +18 -0
- package/build/src/knexLoader.js +31 -0
- package/build/src/knexLoader.js.map +1 -0
- package/package.json +6 -5
- package/src/AuthorizationResultBasedKnexEntityLoader.ts +538 -0
- package/src/BasePostgresEntityDatabaseAdapter.ts +317 -0
- package/src/BaseSQLQueryBuilder.ts +114 -0
- package/src/EnforcingKnexEntityLoader.ts +271 -0
- package/src/KnexEntityLoaderFactory.ts +130 -0
- package/src/PaginationStrategy.ts +32 -0
- package/src/PostgresEntity.ts +118 -0
- package/src/PostgresEntityDatabaseAdapter.ts +78 -24
- package/src/PostgresEntityDatabaseAdapterProvider.ts +11 -1
- package/src/ReadonlyPostgresEntity.ts +115 -0
- package/src/SQLOperator.ts +603 -0
- package/src/__integration-tests__/EntityCreationUtils-test.ts +25 -31
- package/src/__integration-tests__/PostgresEntityIntegration-test.ts +3192 -330
- package/src/__integration-tests__/PostgresEntityQueryContextProvider-test.ts +7 -7
- package/src/__testfixtures__/PostgresTestEntity.ts +17 -3
- package/src/__tests__/AuthorizationResultBasedKnexEntityLoader-test.ts +1167 -0
- package/src/__tests__/BasePostgresEntityDatabaseAdapter-test.ts +160 -0
- package/src/__tests__/EnforcingKnexEntityLoader-test.ts +384 -0
- package/src/__tests__/EntityFields-test.ts +1 -1
- package/src/__tests__/PostgresEntity-test.ts +172 -0
- package/src/__tests__/ReadonlyEntity-test.ts +32 -0
- package/src/__tests__/SQLOperator-test.ts +831 -0
- package/src/__tests__/fixtures/StubPostgresDatabaseAdapter.ts +302 -0
- package/src/__tests__/fixtures/StubPostgresDatabaseAdapterProvider.ts +17 -0
- package/src/__tests__/fixtures/TestEntity.ts +131 -0
- package/src/__tests__/fixtures/TestPaginationEntity.ts +107 -0
- package/src/__tests__/fixtures/createUnitTestPostgresEntityCompanionProvider.ts +42 -0
- package/src/index.ts +15 -0
- package/src/internal/EntityKnexDataManager.ts +832 -0
- package/src/internal/__tests__/EntityKnexDataManager-test.ts +378 -0
- package/src/internal/__tests__/weakMaps-test.ts +25 -0
- package/src/internal/getKnexDataManager.ts +43 -0
- package/src/internal/getKnexEntityLoaderFactory.ts +60 -0
- package/src/internal/utilityTypes.ts +11 -0
- package/src/internal/weakMaps.ts +19 -0
- package/src/knexLoader.ts +110 -0
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computeIfAbsent,
|
|
3
|
+
EntityConfiguration,
|
|
4
|
+
FieldTransformerMap,
|
|
5
|
+
getDatabaseFieldForEntityField,
|
|
6
|
+
IntField,
|
|
7
|
+
mapMap,
|
|
8
|
+
StringField,
|
|
9
|
+
transformFieldsToDatabaseObject,
|
|
10
|
+
} from '@expo/entity';
|
|
11
|
+
import invariant from 'invariant';
|
|
12
|
+
import { v7 as uuidv7 } from 'uuid';
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
BasePostgresEntityDatabaseAdapter,
|
|
16
|
+
NullsOrdering,
|
|
17
|
+
OrderByOrdering,
|
|
18
|
+
TableFieldMultiValueEqualityCondition,
|
|
19
|
+
TableFieldSingleValueEqualityCondition,
|
|
20
|
+
TableOrderByClause,
|
|
21
|
+
TableQuerySelectionModifiers,
|
|
22
|
+
} from '../../BasePostgresEntityDatabaseAdapter';
|
|
23
|
+
import { SQLFragment } from '../../SQLOperator';
|
|
24
|
+
|
|
25
|
+
export class StubPostgresDatabaseAdapter<
|
|
26
|
+
TFields extends Record<string, any>,
|
|
27
|
+
TIDField extends keyof TFields,
|
|
28
|
+
> extends BasePostgresEntityDatabaseAdapter<TFields, TIDField> {
|
|
29
|
+
constructor(
|
|
30
|
+
private readonly entityConfiguration2: EntityConfiguration<TFields, TIDField>,
|
|
31
|
+
private readonly dataStore: Map<string, Readonly<{ [key: string]: any }>[]>,
|
|
32
|
+
) {
|
|
33
|
+
super(entityConfiguration2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public static convertFieldObjectsToDataStore<
|
|
37
|
+
TFields extends Record<string, any>,
|
|
38
|
+
TIDField extends keyof TFields,
|
|
39
|
+
>(
|
|
40
|
+
entityConfiguration: EntityConfiguration<TFields, TIDField>,
|
|
41
|
+
dataStore: Map<string, Readonly<TFields>[]>,
|
|
42
|
+
): Map<string, Readonly<{ [key: string]: any }>[]> {
|
|
43
|
+
return mapMap(dataStore, (objectsForTable) =>
|
|
44
|
+
objectsForTable.map((objectForTable) =>
|
|
45
|
+
transformFieldsToDatabaseObject(entityConfiguration, new Map(), objectForTable),
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
public getObjectCollectionForTable(tableName: string): { [key: string]: any }[] {
|
|
51
|
+
return computeIfAbsent(this.dataStore, tableName, () => []);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
protected getFieldTransformerMap(): FieldTransformerMap {
|
|
55
|
+
return new Map();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
private static uniqBy<T>(a: T[], keyExtractor: (k: T) => string): T[] {
|
|
59
|
+
const seen = new Set();
|
|
60
|
+
return a.filter((item) => {
|
|
61
|
+
const k = keyExtractor(item);
|
|
62
|
+
if (seen.has(k)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
seen.add(k);
|
|
66
|
+
return true;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
protected async fetchManyWhereInternalAsync(
|
|
71
|
+
_queryInterface: any,
|
|
72
|
+
tableName: string,
|
|
73
|
+
tableColumns: readonly string[],
|
|
74
|
+
tableTuples: (readonly any[])[],
|
|
75
|
+
): Promise<object[]> {
|
|
76
|
+
const objectCollection = this.getObjectCollectionForTable(tableName);
|
|
77
|
+
const results = StubPostgresDatabaseAdapter.uniqBy(tableTuples, (tuple) =>
|
|
78
|
+
tuple.join(':'),
|
|
79
|
+
).reduce(
|
|
80
|
+
(acc, tableTuple) => {
|
|
81
|
+
return acc.concat(
|
|
82
|
+
objectCollection.filter((obj) => {
|
|
83
|
+
return tableColumns.every((tableColumn, index) => {
|
|
84
|
+
return obj[tableColumn] === tableTuple[index];
|
|
85
|
+
});
|
|
86
|
+
}),
|
|
87
|
+
);
|
|
88
|
+
},
|
|
89
|
+
[] as { [key: string]: any }[],
|
|
90
|
+
);
|
|
91
|
+
return [...results];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
protected async fetchOneWhereInternalAsync(
|
|
95
|
+
queryInterface: any,
|
|
96
|
+
tableName: string,
|
|
97
|
+
tableColumns: readonly string[],
|
|
98
|
+
tableTuple: readonly any[],
|
|
99
|
+
): Promise<object | null> {
|
|
100
|
+
const results = await this.fetchManyWhereInternalAsync(
|
|
101
|
+
queryInterface,
|
|
102
|
+
tableName,
|
|
103
|
+
tableColumns,
|
|
104
|
+
[tableTuple],
|
|
105
|
+
);
|
|
106
|
+
return results[0] ?? null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private static compareByOrderBys<TFields extends Record<string, any>>(
|
|
110
|
+
orderBys: TableOrderByClause<TFields>[],
|
|
111
|
+
objectA: { [key: string]: any },
|
|
112
|
+
objectB: { [key: string]: any },
|
|
113
|
+
): 0 | 1 | -1 {
|
|
114
|
+
if (orderBys.length === 0) {
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const currentOrderBy = orderBys[0]!;
|
|
119
|
+
if (!('columnName' in currentOrderBy)) {
|
|
120
|
+
throw new Error('SQL fragment order by not supported for StubDatabaseAdapter');
|
|
121
|
+
}
|
|
122
|
+
const aField = objectA[currentOrderBy.columnName];
|
|
123
|
+
const bField = objectB[currentOrderBy.columnName];
|
|
124
|
+
|
|
125
|
+
// Determine effective nulls ordering:
|
|
126
|
+
// - If explicitly set, use that
|
|
127
|
+
// - Otherwise use PostgreSQL defaults: NULLS LAST for ASC, NULLS FIRST for DESC
|
|
128
|
+
const nullsFirst =
|
|
129
|
+
currentOrderBy.nulls !== undefined
|
|
130
|
+
? currentOrderBy.nulls === NullsOrdering.FIRST
|
|
131
|
+
: currentOrderBy.order === OrderByOrdering.DESCENDING;
|
|
132
|
+
|
|
133
|
+
if (aField === null && bField === null) {
|
|
134
|
+
return this.compareByOrderBys(orderBys.slice(1), objectA, objectB);
|
|
135
|
+
} else if (aField === null) {
|
|
136
|
+
return nullsFirst ? -1 : 1;
|
|
137
|
+
} else if (bField === null) {
|
|
138
|
+
return nullsFirst ? 1 : -1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
switch (currentOrderBy.order) {
|
|
142
|
+
case OrderByOrdering.DESCENDING: {
|
|
143
|
+
return aField > bField
|
|
144
|
+
? -1
|
|
145
|
+
: aField < bField
|
|
146
|
+
? 1
|
|
147
|
+
: this.compareByOrderBys(orderBys.slice(1), objectA, objectB);
|
|
148
|
+
}
|
|
149
|
+
case OrderByOrdering.ASCENDING: {
|
|
150
|
+
return bField > aField
|
|
151
|
+
? -1
|
|
152
|
+
: bField < aField
|
|
153
|
+
? 1
|
|
154
|
+
: this.compareByOrderBys(orderBys.slice(1), objectA, objectB);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
protected async fetchManyByFieldEqualityConjunctionInternalAsync(
|
|
160
|
+
_queryInterface: any,
|
|
161
|
+
tableName: string,
|
|
162
|
+
tableFieldSingleValueEqualityOperands: TableFieldSingleValueEqualityCondition[],
|
|
163
|
+
tableFieldMultiValueEqualityOperands: TableFieldMultiValueEqualityCondition[],
|
|
164
|
+
querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
|
|
165
|
+
): Promise<object[]> {
|
|
166
|
+
let filteredObjects = this.getObjectCollectionForTable(tableName);
|
|
167
|
+
for (const { tableField, tableValue } of tableFieldSingleValueEqualityOperands) {
|
|
168
|
+
filteredObjects = filteredObjects.filter((obj) => obj[tableField] === tableValue);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const { tableField, tableValues } of tableFieldMultiValueEqualityOperands) {
|
|
172
|
+
filteredObjects = filteredObjects.filter((obj) => tableValues.includes(obj[tableField]));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const orderBy = querySelectionModifiers.orderBy;
|
|
176
|
+
if (orderBy !== undefined) {
|
|
177
|
+
filteredObjects = filteredObjects.sort((a, b) =>
|
|
178
|
+
StubPostgresDatabaseAdapter.compareByOrderBys(orderBy, a, b),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const offset = querySelectionModifiers.offset;
|
|
183
|
+
if (offset !== undefined) {
|
|
184
|
+
filteredObjects = filteredObjects.slice(offset);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const limit = querySelectionModifiers.limit;
|
|
188
|
+
if (limit !== undefined) {
|
|
189
|
+
filteredObjects = filteredObjects.slice(0, 0 + limit);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return filteredObjects;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
protected fetchManyByRawWhereClauseInternalAsync(
|
|
196
|
+
_queryInterface: any,
|
|
197
|
+
_tableName: string,
|
|
198
|
+
_rawWhereClause: string,
|
|
199
|
+
_bindings: object | any[],
|
|
200
|
+
_querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
|
|
201
|
+
): Promise<object[]> {
|
|
202
|
+
throw new Error('Raw WHERE clauses not supported for StubDatabaseAdapter');
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
protected fetchManyBySQLFragmentInternalAsync(
|
|
206
|
+
_queryInterface: any,
|
|
207
|
+
_tableName: string,
|
|
208
|
+
_sqlFragment: SQLFragment<TFields>,
|
|
209
|
+
_querySelectionModifiers: TableQuerySelectionModifiers<TFields>,
|
|
210
|
+
): Promise<object[]> {
|
|
211
|
+
throw new Error('SQL fragments not supported for StubDatabaseAdapter');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private generateRandomID(): any {
|
|
215
|
+
const idSchemaField = this.entityConfiguration2.schema.get(this.entityConfiguration2.idField);
|
|
216
|
+
invariant(
|
|
217
|
+
idSchemaField,
|
|
218
|
+
`No schema field found for ${String(this.entityConfiguration2.idField)}`,
|
|
219
|
+
);
|
|
220
|
+
if (idSchemaField instanceof StringField) {
|
|
221
|
+
return uuidv7();
|
|
222
|
+
} else if (idSchemaField instanceof IntField) {
|
|
223
|
+
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
|
|
224
|
+
} else {
|
|
225
|
+
throw new Error(
|
|
226
|
+
`Unsupported ID type for StubPostgresDatabaseAdapter: ${idSchemaField.constructor.name}`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
protected async insertInternalAsync(
|
|
232
|
+
_queryInterface: any,
|
|
233
|
+
tableName: string,
|
|
234
|
+
object: object,
|
|
235
|
+
): Promise<object[]> {
|
|
236
|
+
const objectCollection = this.getObjectCollectionForTable(tableName);
|
|
237
|
+
|
|
238
|
+
const idField = getDatabaseFieldForEntityField(
|
|
239
|
+
this.entityConfiguration2,
|
|
240
|
+
this.entityConfiguration2.idField,
|
|
241
|
+
);
|
|
242
|
+
const objectToInsert = {
|
|
243
|
+
[idField]: this.generateRandomID(),
|
|
244
|
+
...object,
|
|
245
|
+
};
|
|
246
|
+
objectCollection.push(objectToInsert);
|
|
247
|
+
return [objectToInsert];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
protected async updateInternalAsync(
|
|
251
|
+
_queryInterface: any,
|
|
252
|
+
tableName: string,
|
|
253
|
+
tableIdField: string,
|
|
254
|
+
id: any,
|
|
255
|
+
object: object,
|
|
256
|
+
): Promise<object[]> {
|
|
257
|
+
// SQL does not support empty updates, mirror behavior here for better test simulation
|
|
258
|
+
if (Object.keys(object).length === 0) {
|
|
259
|
+
throw new Error(`Empty update (${tableIdField} = ${id})`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const objectCollection = this.getObjectCollectionForTable(tableName);
|
|
263
|
+
|
|
264
|
+
const objectIndex = objectCollection.findIndex((obj) => {
|
|
265
|
+
return obj[tableIdField] === id;
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// SQL updates to a nonexistent row succeed but affect 0 rows,
|
|
269
|
+
// mirror that behavior here for better test simulation
|
|
270
|
+
if (objectIndex < 0) {
|
|
271
|
+
return [];
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
objectCollection[objectIndex] = {
|
|
275
|
+
...objectCollection[objectIndex],
|
|
276
|
+
...object,
|
|
277
|
+
};
|
|
278
|
+
return [objectCollection[objectIndex]];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
protected async deleteInternalAsync(
|
|
282
|
+
_queryInterface: any,
|
|
283
|
+
tableName: string,
|
|
284
|
+
tableIdField: string,
|
|
285
|
+
id: any,
|
|
286
|
+
): Promise<number> {
|
|
287
|
+
const objectCollection = this.getObjectCollectionForTable(tableName);
|
|
288
|
+
|
|
289
|
+
const objectIndex = objectCollection.findIndex((obj) => {
|
|
290
|
+
return obj[tableIdField] === id;
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// SQL deletes to a nonexistent row succeed and affect 0 rows,
|
|
294
|
+
// mirror that behavior here for better test simulation
|
|
295
|
+
if (objectIndex < 0) {
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
objectCollection.splice(objectIndex, 1);
|
|
300
|
+
return 1;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EntityConfiguration,
|
|
3
|
+
EntityDatabaseAdapter,
|
|
4
|
+
IEntityDatabaseAdapterProvider,
|
|
5
|
+
} from '@expo/entity';
|
|
6
|
+
|
|
7
|
+
import { StubPostgresDatabaseAdapter } from './StubPostgresDatabaseAdapter';
|
|
8
|
+
|
|
9
|
+
export class StubPostgresDatabaseAdapterProvider 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 StubPostgresDatabaseAdapter(entityConfiguration, this.objectCollection);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EntityCompanionDefinition,
|
|
3
|
+
EntityConfiguration,
|
|
4
|
+
DateField,
|
|
5
|
+
IntField,
|
|
6
|
+
StringField,
|
|
7
|
+
UUIDField,
|
|
8
|
+
EntityPrivacyPolicy,
|
|
9
|
+
ViewerContext,
|
|
10
|
+
AlwaysAllowPrivacyPolicyRule,
|
|
11
|
+
} from '@expo/entity';
|
|
12
|
+
import { result, Result } from '@expo/results';
|
|
13
|
+
|
|
14
|
+
import { PostgresEntity } from '../../PostgresEntity';
|
|
15
|
+
|
|
16
|
+
export type TestFields = {
|
|
17
|
+
customIdField: string;
|
|
18
|
+
testIndexedField: string;
|
|
19
|
+
stringField: string;
|
|
20
|
+
intField: number;
|
|
21
|
+
dateField: Date;
|
|
22
|
+
nullableField: string | null;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const testEntityConfiguration = new EntityConfiguration<TestFields, 'customIdField'>({
|
|
26
|
+
idField: 'customIdField',
|
|
27
|
+
tableName: 'test_entity_should_not_write_to_db',
|
|
28
|
+
schema: {
|
|
29
|
+
customIdField: new UUIDField({
|
|
30
|
+
columnName: 'custom_id',
|
|
31
|
+
cache: true,
|
|
32
|
+
}),
|
|
33
|
+
testIndexedField: new StringField({
|
|
34
|
+
columnName: 'test_index',
|
|
35
|
+
cache: true,
|
|
36
|
+
}),
|
|
37
|
+
stringField: new StringField({
|
|
38
|
+
columnName: 'string_field',
|
|
39
|
+
}),
|
|
40
|
+
intField: new IntField({
|
|
41
|
+
columnName: 'number_field',
|
|
42
|
+
}),
|
|
43
|
+
dateField: new DateField({
|
|
44
|
+
columnName: 'date_field',
|
|
45
|
+
}),
|
|
46
|
+
nullableField: new StringField({
|
|
47
|
+
columnName: 'nullable_field',
|
|
48
|
+
}),
|
|
49
|
+
},
|
|
50
|
+
databaseAdapterFlavor: 'postgres',
|
|
51
|
+
cacheAdapterFlavor: 'redis',
|
|
52
|
+
compositeFieldDefinitions: [
|
|
53
|
+
{ compositeField: ['stringField', 'intField'], cache: false },
|
|
54
|
+
{ compositeField: ['stringField', 'testIndexedField'], cache: true },
|
|
55
|
+
{ compositeField: ['nullableField', 'testIndexedField'], cache: true },
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export class TestEntityPrivacyPolicy extends EntityPrivacyPolicy<
|
|
60
|
+
TestFields,
|
|
61
|
+
'customIdField',
|
|
62
|
+
ViewerContext,
|
|
63
|
+
TestEntity
|
|
64
|
+
> {
|
|
65
|
+
protected override readonly readRules = [
|
|
66
|
+
new AlwaysAllowPrivacyPolicyRule<TestFields, 'customIdField', ViewerContext, TestEntity>(),
|
|
67
|
+
];
|
|
68
|
+
protected override readonly createRules = [
|
|
69
|
+
new AlwaysAllowPrivacyPolicyRule<TestFields, 'customIdField', ViewerContext, TestEntity>(),
|
|
70
|
+
];
|
|
71
|
+
protected override readonly updateRules = [
|
|
72
|
+
new AlwaysAllowPrivacyPolicyRule<TestFields, 'customIdField', ViewerContext, TestEntity>(),
|
|
73
|
+
];
|
|
74
|
+
protected override readonly deleteRules = [
|
|
75
|
+
new AlwaysAllowPrivacyPolicyRule<TestFields, 'customIdField', ViewerContext, TestEntity>(),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export class TestEntity extends PostgresEntity<TestFields, 'customIdField', ViewerContext> {
|
|
80
|
+
static defineCompanionDefinition(): EntityCompanionDefinition<
|
|
81
|
+
TestFields,
|
|
82
|
+
'customIdField',
|
|
83
|
+
ViewerContext,
|
|
84
|
+
TestEntity,
|
|
85
|
+
TestEntityPrivacyPolicy
|
|
86
|
+
> {
|
|
87
|
+
return {
|
|
88
|
+
entityClass: TestEntity,
|
|
89
|
+
entityConfiguration: testEntityConfiguration,
|
|
90
|
+
privacyPolicyClass: TestEntityPrivacyPolicy,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
getBlah(): string {
|
|
95
|
+
return 'Hello World!';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
static async helloAsync(
|
|
99
|
+
viewerContext: ViewerContext,
|
|
100
|
+
testValue: string,
|
|
101
|
+
): Promise<Result<TestEntity>> {
|
|
102
|
+
const fields = {
|
|
103
|
+
customIdField: testValue,
|
|
104
|
+
testIndexedField: 'hello',
|
|
105
|
+
stringField: 'hello',
|
|
106
|
+
intField: 1,
|
|
107
|
+
dateField: new Date(),
|
|
108
|
+
nullableField: null,
|
|
109
|
+
};
|
|
110
|
+
return result(
|
|
111
|
+
new TestEntity({
|
|
112
|
+
viewerContext,
|
|
113
|
+
id: testValue,
|
|
114
|
+
databaseFields: fields,
|
|
115
|
+
selectedFields: fields,
|
|
116
|
+
}),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
static async returnErrorAsync(_viewerContext: ViewerContext): Promise<Result<TestEntity>> {
|
|
121
|
+
return result(new Error('return entity'));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
static async throwErrorAsync(_viewerContext: ViewerContext): Promise<Result<TestEntity>> {
|
|
125
|
+
throw new Error('threw entity');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
static async nonResultAsync(_viewerContext: ViewerContext, testValue: string): Promise<string> {
|
|
129
|
+
return testValue;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EntityCompanionDefinition,
|
|
3
|
+
EntityConfiguration,
|
|
4
|
+
EntityPrivacyPolicy,
|
|
5
|
+
ViewerContext,
|
|
6
|
+
EntityPrivacyPolicyEvaluationContext,
|
|
7
|
+
EntityQueryContext,
|
|
8
|
+
UUIDField,
|
|
9
|
+
StringField,
|
|
10
|
+
DateField,
|
|
11
|
+
IntField,
|
|
12
|
+
RuleEvaluationResult,
|
|
13
|
+
} from '@expo/entity';
|
|
14
|
+
|
|
15
|
+
import { PostgresEntity } from '../../PostgresEntity';
|
|
16
|
+
|
|
17
|
+
export interface TestPaginationFields {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
status: string;
|
|
21
|
+
createdAt: Date;
|
|
22
|
+
score: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const testPaginationEntityConfiguration = new EntityConfiguration<
|
|
26
|
+
TestPaginationFields,
|
|
27
|
+
'id'
|
|
28
|
+
>({
|
|
29
|
+
idField: 'id',
|
|
30
|
+
tableName: 'test_pagination_entities',
|
|
31
|
+
schema: {
|
|
32
|
+
id: new UUIDField({
|
|
33
|
+
columnName: 'id',
|
|
34
|
+
cache: true,
|
|
35
|
+
}),
|
|
36
|
+
name: new StringField({
|
|
37
|
+
columnName: 'name',
|
|
38
|
+
}),
|
|
39
|
+
status: new StringField({
|
|
40
|
+
columnName: 'status',
|
|
41
|
+
}),
|
|
42
|
+
createdAt: new DateField({
|
|
43
|
+
columnName: 'created_at',
|
|
44
|
+
}),
|
|
45
|
+
score: new IntField({
|
|
46
|
+
columnName: 'score',
|
|
47
|
+
}),
|
|
48
|
+
},
|
|
49
|
+
databaseAdapterFlavor: 'postgres',
|
|
50
|
+
cacheAdapterFlavor: 'redis',
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Privacy policy that conditionally fails authorization based on the 'status' field.
|
|
55
|
+
* Entities with status 'unauthorized' will fail authorization.
|
|
56
|
+
*/
|
|
57
|
+
export class TestPaginationPrivacyPolicy extends EntityPrivacyPolicy<
|
|
58
|
+
TestPaginationFields,
|
|
59
|
+
'id',
|
|
60
|
+
ViewerContext,
|
|
61
|
+
TestPaginationEntity
|
|
62
|
+
> {
|
|
63
|
+
protected override readonly readRules = [
|
|
64
|
+
{
|
|
65
|
+
async evaluateAsync(
|
|
66
|
+
_viewerContext: ViewerContext,
|
|
67
|
+
_queryContext: EntityQueryContext,
|
|
68
|
+
_evaluationContext: EntityPrivacyPolicyEvaluationContext<
|
|
69
|
+
TestPaginationFields,
|
|
70
|
+
'id',
|
|
71
|
+
ViewerContext,
|
|
72
|
+
TestPaginationEntity
|
|
73
|
+
>,
|
|
74
|
+
entity: TestPaginationEntity,
|
|
75
|
+
): Promise<RuleEvaluationResult> {
|
|
76
|
+
// Fail authorization for entities with status 'unauthorized'
|
|
77
|
+
return entity.getField('status') === 'unauthorized'
|
|
78
|
+
? RuleEvaluationResult.DENY
|
|
79
|
+
: RuleEvaluationResult.ALLOW;
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
protected override readonly createRules = [];
|
|
85
|
+
protected override readonly updateRules = [];
|
|
86
|
+
protected override readonly deleteRules = [];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export class TestPaginationEntity extends PostgresEntity<
|
|
90
|
+
TestPaginationFields,
|
|
91
|
+
'id',
|
|
92
|
+
ViewerContext
|
|
93
|
+
> {
|
|
94
|
+
static defineCompanionDefinition(): EntityCompanionDefinition<
|
|
95
|
+
TestPaginationFields,
|
|
96
|
+
'id',
|
|
97
|
+
ViewerContext,
|
|
98
|
+
TestPaginationEntity,
|
|
99
|
+
TestPaginationPrivacyPolicy
|
|
100
|
+
> {
|
|
101
|
+
return {
|
|
102
|
+
entityClass: TestPaginationEntity,
|
|
103
|
+
entityConfiguration: testPaginationEntityConfiguration,
|
|
104
|
+
privacyPolicyClass: TestPaginationPrivacyPolicy,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {
|
|
2
|
+
EntityCompanionProvider,
|
|
3
|
+
IEntityMetricsAdapter,
|
|
4
|
+
NoOpEntityMetricsAdapter,
|
|
5
|
+
} from '@expo/entity';
|
|
6
|
+
import {
|
|
7
|
+
InMemoryFullCacheStubCacheAdapterProvider,
|
|
8
|
+
StubQueryContextProvider,
|
|
9
|
+
} from '@expo/entity-testing-utils';
|
|
10
|
+
|
|
11
|
+
import { StubPostgresDatabaseAdapterProvider } from './StubPostgresDatabaseAdapterProvider';
|
|
12
|
+
|
|
13
|
+
const queryContextProvider = new StubQueryContextProvider();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Entity companion provider for use in knex unit tests. All database and cache implementations
|
|
17
|
+
* are replaced with in-memory simulations.
|
|
18
|
+
*/
|
|
19
|
+
export const createUnitTestPostgresEntityCompanionProvider = (
|
|
20
|
+
metricsAdapter: IEntityMetricsAdapter = new NoOpEntityMetricsAdapter(),
|
|
21
|
+
): EntityCompanionProvider => {
|
|
22
|
+
return new EntityCompanionProvider(
|
|
23
|
+
metricsAdapter,
|
|
24
|
+
new Map([
|
|
25
|
+
[
|
|
26
|
+
'postgres',
|
|
27
|
+
{
|
|
28
|
+
adapterProvider: new StubPostgresDatabaseAdapterProvider(),
|
|
29
|
+
queryContextProvider,
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
]),
|
|
33
|
+
new Map([
|
|
34
|
+
[
|
|
35
|
+
'redis',
|
|
36
|
+
{
|
|
37
|
+
cacheAdapterProvider: new InMemoryFullCacheStubCacheAdapterProvider(),
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
]),
|
|
41
|
+
);
|
|
42
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -4,8 +4,23 @@
|
|
|
4
4
|
* @module @expo/entity-database-adapter-knex
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
export * from './AuthorizationResultBasedKnexEntityLoader';
|
|
8
|
+
export * from './BasePostgresEntityDatabaseAdapter';
|
|
9
|
+
export * from './BaseSQLQueryBuilder';
|
|
10
|
+
export * from './EnforcingKnexEntityLoader';
|
|
7
11
|
export * from './EntityFields';
|
|
12
|
+
export * from './KnexEntityLoaderFactory';
|
|
13
|
+
export * from './knexLoader';
|
|
14
|
+
export * from './PaginationStrategy';
|
|
15
|
+
export * from './PostgresEntity';
|
|
8
16
|
export * from './PostgresEntityDatabaseAdapter';
|
|
9
17
|
export * from './PostgresEntityDatabaseAdapterProvider';
|
|
10
18
|
export * from './PostgresEntityQueryContextProvider';
|
|
19
|
+
export * from './ReadonlyPostgresEntity';
|
|
20
|
+
export * from './SQLOperator';
|
|
11
21
|
export * from './errors/wrapNativePostgresCallAsync';
|
|
22
|
+
export * from './internal/EntityKnexDataManager';
|
|
23
|
+
export * from './internal/getKnexDataManager';
|
|
24
|
+
export * from './internal/getKnexEntityLoaderFactory';
|
|
25
|
+
export * from './internal/utilityTypes';
|
|
26
|
+
export * from './internal/weakMaps';
|