Skip to content

Commit f7da2c3

Browse files
authored
Implement function loaders (#290)
1 parent 67d9498 commit f7da2c3

5 files changed

Lines changed: 178 additions & 3 deletions

File tree

README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,45 @@ const loader = new Loader<string>({
161161
const classifier = await loader.get('1')
162162
```
163163

164+
### Simplified loader syntax
165+
166+
It is also possible to inline datasource definition:
167+
168+
```ts
169+
const loader = new Loader<string>({
170+
// this cache will be checked first
171+
inMemoryCache: {
172+
cacheType: 'lru-object', // you can choose between lru and fifo caches, fifo being 10% slightly faster
173+
ttlInMsecs: 1000 * 60,
174+
maxItems: 100,
175+
},
176+
177+
// this cache will be checked if in-memory one returns undefined
178+
asyncCache: new RedisCache(ioRedis, {
179+
json: true, // this instructs loader to serialize passed objects as string and deserialize them back to objects
180+
ttlInMsecs: 1000 * 60 * 10,
181+
}),
182+
183+
// data source will be generated from one or both provided data loading functions
184+
dataSourceGetOneFn: async (key: string) => {
185+
const results = await this.db('classifiers')
186+
.select('*')
187+
.where({
188+
id: parseInt(key),
189+
})
190+
return results[0]
191+
},
192+
dataSourceGetManyFn: (keys: string[]) => {
193+
return this.db('classifiers')
194+
.select('*')
195+
.whereIn('id', keys.map(parseInt))
196+
}
197+
})
198+
199+
// If cache is empty, but there is data in the DB, after this operation is completed, both caches will be populated
200+
const classifier = await loader.get('1')
201+
```
202+
164203
## Loader API
165204

166205
Loader has the following config parameters:

lib/GeneratedDataSource.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { DataSource } from './types/DataSources'
2+
3+
export type GeneratedDataSourceParams<LoadedValue, LoaderParams = undefined> = {
4+
name?: string
5+
dataSourceGetOneFn?: (key: string, loadParams?: LoaderParams) => Promise<LoadedValue | undefined | null>
6+
dataSourceGetManyFn?: (keys: string[], loadParams?: LoaderParams) => Promise<LoadedValue[]>
7+
}
8+
9+
export class GeneratedDataSource<LoadedValue, LoadParams = undefined> implements DataSource<LoadedValue, LoadParams> {
10+
private readonly getOneFn: (key: string, loadParams?: LoadParams) => Promise<LoadedValue | undefined | null>
11+
private readonly getManyFn: (keys: string[], loadParams?: LoadParams) => Promise<LoadedValue[]>
12+
public readonly name: string
13+
constructor(params: GeneratedDataSourceParams<LoadedValue, LoadParams>) {
14+
this.name = params.name ?? 'Generated loader'
15+
this.getOneFn =
16+
params.dataSourceGetOneFn ??
17+
function () {
18+
throw new Error('Retrieval of a single entity is not implemented')
19+
}
20+
21+
this.getManyFn =
22+
params.dataSourceGetManyFn ??
23+
function () {
24+
throw new Error('Retrieval of multiple entities is not implemented')
25+
}
26+
}
27+
28+
get(key: string, loadParams: LoadParams | undefined): Promise<LoadedValue | undefined | null> {
29+
return this.getOneFn(key, loadParams)
30+
}
31+
32+
getMany(keys: string[], loadParams: LoadParams | undefined): Promise<LoadedValue[]> {
33+
return this.getManyFn(keys, loadParams)
34+
}
35+
}

lib/Loader.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type { InMemoryGroupCacheConfiguration } from './memory/InMemoryGroupCach
66
import type { SynchronousCache, SynchronousGroupCache, GetManyResult } from './types/SyncDataSources'
77
import type { NotificationPublisher } from './notifications/NotificationPublisher'
88
import type { GroupNotificationPublisher } from './notifications/GroupNotificationPublisher'
9+
import { GeneratedDataSource } from './GeneratedDataSource'
910

1011
export type LoaderConfig<
1112
LoadedValue,
@@ -23,6 +24,9 @@ export type LoaderConfig<
2324
| GroupNotificationPublisher<LoadedValue> = NotificationPublisher<LoadedValue>,
2425
> = {
2526
dataSources?: readonly DataSourceType[]
27+
dataSourceGetOneFn?: (key: string, loadParams?: LoaderParams) => Promise<LoadedValue | undefined | null>
28+
dataSourceGetManyFn?: (keys: string[], loadParams?: LoaderParams) => Promise<LoadedValue[]>
29+
dataSourceName?: string
2630
throwIfLoadError?: boolean
2731
throwIfUnresolved?: boolean
2832
} & CommonCacheConfig<LoadedValue, CacheType, InMemoryCacheConfigType, InMemoryCacheType, NotificationPublisherType>
@@ -35,7 +39,30 @@ export class Loader<LoadedValue, LoaderParams = undefined> extends AbstractFlatC
3539

3640
constructor(config: LoaderConfig<LoadedValue, Cache<LoadedValue>, LoaderParams>) {
3741
super(config)
38-
this.dataSources = config.dataSources ?? []
42+
43+
// generated datasource
44+
if (config.dataSourceGetManyFn || config.dataSourceGetOneFn) {
45+
if (config.dataSources) {
46+
throw new Error('Cannot set both "dataSources" and "dataSourceGetManyFn"/"dataSourceGetOneFn" parameters.')
47+
}
48+
49+
this.dataSources = [
50+
new GeneratedDataSource({
51+
dataSourceGetOneFn: config.dataSourceGetOneFn,
52+
dataSourceGetManyFn: config.dataSourceGetManyFn,
53+
name: config.dataSourceName,
54+
}),
55+
]
56+
}
57+
// defined datasource
58+
else if (config.dataSources) {
59+
this.dataSources = config.dataSources
60+
}
61+
// no datasource
62+
else {
63+
this.dataSources = []
64+
}
65+
3966
this.throwIfLoadError = config.throwIfLoadError ?? true
4067
this.throwIfUnresolved = config.throwIfUnresolved ?? false
4168
this.isKeyRefreshing = new Set()

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@
5353
"toad-cache": "^3.3.1"
5454
},
5555
"devDependencies": {
56-
"@types/node": "^20.10.1",
56+
"@types/node": "^20.10.2",
5757
"@typescript-eslint/eslint-plugin": "^6.13.1",
5858
"@typescript-eslint/parser": "^6.13.1",
5959
"@vitest/coverage-v8": "0.34.6",
6060
"del-cli": "^5.1.0",
61-
"eslint": "^8.54.0",
61+
"eslint": "^8.55.0",
6262
"eslint-config-prettier": "^9.0.0",
6363
"eslint-plugin-import": "^2.29.0",
6464
"eslint-plugin-prettier": "^5.0.1",

test/Loader-main.spec.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,19 @@ describe('Loader Main', () => {
360360
})
361361
})
362362

363+
describe('constructor', () => {
364+
it('throws an error if both datasource and datasource fns are provided', () => {
365+
expect(() => {
366+
new Loader({
367+
dataSources: [],
368+
dataSourceGetOneFn: () => {
369+
return Promise.resolve('x')
370+
},
371+
})
372+
}).toThrow(/Cannot set both/)
373+
})
374+
})
375+
363376
describe('get', () => {
364377
it('returns undefined when fails to resolve value', async () => {
365378
const operation = new Loader({})
@@ -461,6 +474,36 @@ describe('Loader Main', () => {
461474
expect(result).toBe('value')
462475
})
463476

477+
it('returns value when resolved via generated loader', async () => {
478+
const operation = new Loader<string>({
479+
inMemoryCache: IN_MEMORY_CACHE_CONFIG,
480+
dataSourceGetOneFn: (key) => {
481+
if (key === 'key') {
482+
return Promise.resolve('value')
483+
}
484+
throw new Error('Not found')
485+
},
486+
})
487+
488+
const result = await operation.get('key')
489+
490+
expect(result).toBe('value')
491+
})
492+
493+
it('throws an error if requested generated loader is not set', async () => {
494+
const operation = new Loader<string>({
495+
inMemoryCache: IN_MEMORY_CACHE_CONFIG,
496+
dataSourceGetManyFn: (keys) => {
497+
if (keys[0] === 'key') {
498+
return Promise.resolve(['value'])
499+
}
500+
throw new Error('Not found')
501+
},
502+
})
503+
504+
await expect(operation.get('key')).rejects.toThrow(/Retrieval of a single entity is not/)
505+
})
506+
464507
it('returns value when resolved via multiple loaders', async () => {
465508
const asyncCache = new DummyCache(undefined)
466509

@@ -664,6 +707,37 @@ describe('Loader Main', () => {
664707
expect(result).toEqual(['value'])
665708
})
666709

710+
it('returns value when resolved via generated loader', async () => {
711+
const operation = new Loader<string>({
712+
inMemoryCache: IN_MEMORY_CACHE_CONFIG,
713+
dataSourceGetManyFn: (keys: string[]) => {
714+
if (keys.includes('key')) {
715+
return Promise.resolve(['value'])
716+
}
717+
718+
throw new Error('Not found')
719+
},
720+
})
721+
722+
const result = await operation.getMany(['key'], idResolver)
723+
724+
expect(result).toEqual(['value'])
725+
})
726+
727+
it('throws an error if requested generated loader is not set', async () => {
728+
const operation = new Loader<string>({
729+
inMemoryCache: IN_MEMORY_CACHE_CONFIG,
730+
dataSourceGetOneFn: (key) => {
731+
if (key === 'key') {
732+
return Promise.resolve('value')
733+
}
734+
throw new Error('Not found')
735+
},
736+
})
737+
738+
await expect(operation.getMany(['key'], idResolver)).rejects.toThrow(/Retrieval of multiple entities/)
739+
})
740+
667741
it('returns value when resolved via multiple caches', async () => {
668742
const asyncCache = new DummyCache(undefined)
669743

0 commit comments

Comments
 (0)