UNPKG

3.09 kBPlain TextView Raw
1import { Pool, ClientBase, QueryResult, QueryConfig } from "pg";
2import DataLoader from "dataloader";
3import { NotFoundError } from "./errors";
4import { StrategyCollection } from "./StrategyCollection";
5import createHash from "object-hash";
6
7export class DataLoaderExecutor<
8 T extends ClientBase | Pool = ClientBase | Pool
9> {
10 public connection: T;
11 public readonly strategies: StrategyCollection;
12
13 constructor(connection: T, strategies: StrategyCollection) {
14 this.connection = connection;
15 this.strategies = strategies;
16 }
17}
18
19export type ReadonlyDataLoaderExecutor<
20 T extends ClientBase | Pool = ClientBase | Pool
21> = DataLoaderExecutor<T> & {
22 readonly connection: T;
23};
24
25export type Reader<M> = (
26 executor: DataLoaderExecutor,
27 ids: readonly string[]
28) => Promise<M[]>;
29
30export class DataLoaderCache<M extends { id: string }> {
31 private _map: WeakMap<DataLoaderExecutor, DataLoader<string, M>>;
32 private readonly _read: Reader<M>;
33
34 constructor(read: Reader<M>) {
35 this._map = new WeakMap();
36 this._read = read;
37 }
38
39 get(executor: DataLoaderExecutor): DataLoader<string, M> {
40 let loader = this._map.get(executor);
41 if (loader) {
42 return loader;
43 }
44
45 const read = this._read;
46 loader = new DataLoader<string, M>(async function (
47 ids: readonly string[]
48 ): Promise<(M | Error)[]> {
49 // Get the results from the read in whatever order the database found
50 // most efficient.
51 const results = await read(executor, ids);
52
53 // Index the results by ID.
54 const resultsById = new Map<string, M>();
55 for (let i = 0; i < results.length; i++) {
56 const result = results[i];
57 resultsById.set(result.id, result);
58 }
59
60 // Normalize the order and format to comply with the DataLoader interface.
61 const returnValue = new Array(ids.length);
62 for (let i = 0; i < ids.length; i++) {
63 const id = ids[i];
64 returnValue[i] = resultsById.get(id) ?? new NotFoundError();
65 }
66
67 return returnValue;
68 });
69 this._map.set(executor, loader);
70 return loader;
71 }
72}
73
74export class QueryCache<T> {
75 private _map = new WeakMap<
76 DataLoaderExecutor,
77 Map<string, Promise<QueryResult<T>>>
78 >();
79
80 query(
81 tx: Pool | ClientBase | DataLoaderExecutor,
82 query: string | QueryConfig<unknown[]>,
83 parameters: unknown[]
84 ): Promise<QueryResult<T>> {
85 // Queries using a direct connection or connection pool bypass the cache.
86 if (!(tx instanceof DataLoaderExecutor)) {
87 return tx.query(query, parameters);
88 }
89
90 // Load a cache map keyed by the executor.
91 let cache = this._map.get(tx);
92 if (!cache) {
93 cache = new Map();
94 this._map.set(tx, cache);
95 }
96
97 // A hash of the entire query and parameters is used as the key.
98 const hash = createHash({ query, parameters });
99
100 // Return cached results or populate the cache with new pending results.
101 let result = cache.get(hash);
102 if (!result) {
103 result = tx.connection.query(query, parameters);
104 cache.set(hash, result);
105 }
106
107 return result;
108 }
109}