Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add .select() method #5

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
44 changes: 23 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ Please refer to the documentation for more information.
```js
Promise.all([
app.service('posts').get(1),
app.service('posts').get(2),
app.service('posts').get(3)
app.service('posts').get(1),
app.service('posts').get(2)
]);
```

is slower than

```js
app.service('posts').find({ query: { id: { $in: [1, 2, 3] } } });
app.service('posts').find({ query: { id: { $in: [1, 2] } } });
```

Feathers Dataloader makes it easy and fast to write these kinds of queries. The loader handles coalescing all of the IDs into one request and mapping them back to the proper caller.
Expand All @@ -37,15 +37,15 @@ const loader = new AppLoader({ app: context.app });

Promise.all([
loader.service('posts').load(1),
loader.service('posts').load(2),
loader.service('posts').load(3)
loader.service('posts').load(1),
loader.service('posts').load(2)
]);
```

is automatically converted to

```js
app.service('posts').find({ query: { id: { $in: [1, 2, 3] } } });
app.service('posts').find({ query: { id: { $in: [1, 2] } } });
```


Expand All @@ -54,8 +54,6 @@ app.service('posts').find({ query: { id: { $in: [1, 2, 3] } } });
```js
const { AppLoader } = require('feathers-dataloader');

// See Guide for more information about how to better pass
// loaders from service to service.
const initializeLoader = context => {
if (context.params.loader) {
return context;
Expand All @@ -71,33 +69,37 @@ app.hooks({
all: [initializeLoader]
}
})
```

Loaders are most commonly used in resolvers like @feathersjs/schema, withResults, or fastJoin. See the Guide section for more information and common usecases. Pass the loader to any and all service/loader calls. This maximizes performance by allowing the loader to reuse its cache and batching mechanism as much as possible.

// Loaders are most commonly used in resolvers like @feathersjs/schema,
// withResults, or fastJoin. See the Guide section for more
// information and common usecases.
// Pass the loader to any and all service/loader calls. This maximizes
// performance by allowing the loader to reuse its cache and
// batching mechanism as much as possible.
```js
const { resolveResult, resolve } = require('@feathersjs/schema');

const postResultsResolver = resolve({
properties: {
user: async (value, post, context) => {
const { loader } = context.params;
return await loader.service('users').load(post.userId, { loader });
return context.params.loader
.service('users')
.load(post.userId, { loader });
},
category: async (value, post, context) => {
const { loader } = context.params;
return await loader.service('categories').key('name').load(post.categoryName, { loader });
return context.params.loader
.service('categories')
.key('name')
.load(post.categoryName, { loader });
},
tags: async (value, post, context) => {
const { loader } = context.params;
return await loader.service('tags').load(post.tagIds, { loader });
return context.params.loader
.service('tags')
.load(post.tagIds, { loader });
},
comments: async (value, post, context) => {
const { loader } = context.params;
return await loader.service('comments').multi('postId').load(post.id, { loader });
return context.params.loader
.service('comments')
.multi('postId')
.load(post.id, { loader });
}
}
});
Expand Down
2 changes: 1 addition & 1 deletion docs/guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ app.service('posts').hooks({
The ServiceLoader's underlying DataLoader takes a `maxBatchSize` option. This option limits the number of ids in the `$in` query. Huge arrays of ids can lead to performance issues and even database lockup. By using the `maxBatchSize` you can break those queries into smaller ones. You should monitor your own application to determine the best number to use for your batch size, but setting some maximum is recommended.

```js
const { AppLoader, ServiceLoader } = require('feathers-dataloader');
const { AppLoader } = require('feathers-dataloader');

const loader = new AppLoader({ app, maxBatchSize: 100 });

Expand Down
24 changes: 13 additions & 11 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,13 @@ app.service('posts').hooks({
});
```

The `AppLoader` lazily configures a new `ServiceLoader` per service as you use them. This means that you do not have to configure the lower level `ServiceLoader` classes. But, you can use these classes individually, although it is generally not needed.
The `AppLoader` lazily configures a new `ServiceLoader` per service as you use them. This means that you do not have to configure the lower level `ServiceLoader` classes. You can use these classes individually, although it is generally not needed.

```js

const { ServiceLoader } = require('feathers-dataloader');

const serviceLoader = new ServiceLoader({ app, serviceName: 'users' });
const serviceLoader = new ServiceLoader({ app, path: 'users' });
const user = await serviceLoader.load(1, params);
const user = await serviceLoader.get(1, params);
const users = await serviceLoader.find(params);
Expand Down Expand Up @@ -131,7 +131,7 @@ Create a new app-loader. This is the most commonly used class.
| Argument | Type | Default | Description |
| --------------- | :--------: | ---------- | ------------------------------------------------ |
| `app` | `Object` | | A Feathers app |
| `services` | `Object` | `{}`| An object where each property is a service name and the value is loader options for that service. These options override the `globalLoaderOptions` |
| `services` | `Object` | `{}`| An object where each property is a service name and the value is `loaderOptions` for that service. These options override the `globalLoaderOptions` |
| `ServiceLoader` | `Class` | `ServiceLoader`| A base class that will be used to create each ServiceLoader instance |
| `globalLoaderOptions` | `Object` | {} | Options that will be assigned to every new `ServiceLoader` |

Expand Down Expand Up @@ -164,21 +164,23 @@ Create a new app-loader. This is the most commonly used class.
<!--- class ServiceLoader --------------------------------------------------------------------------->
<h2 id="class-serviceloader">class ServiceLoader( [, options] )</h2>

Create a new service-loader. This class lazily configures underlying `DataLoader` and `FindLoader` for a given service
Create a new service-loader. This class lazily configures underlying `DataLoader` and takes many `DataLoader` options.

- **Arguments:**
- `{Object} [ options ]`
- `{Object} service`
- `{Object} path`
- `{Map} cacheMap`
- `{Map} cacheParamsFn`
- `{Function} cacheParamsFn`
- `{Function} selectFn`
- ...loaderOptions

| Argument | Type | Default | Description |
| --------------- | :--------: | ---------- | ------------------------------------------------ |
| `app` | `Object` | | A Feathers app` |
| `serviceName` | `String` | | The name of the service like "users"` |
| `cacheMap` | `Map` | | A Map like object with methods get, set, and clear to serve as the cache of results.` |
| `app` | `Object` | | A Feathers app |
| `path` | `String` | | The name of the service like "users" |
| `cacheMap` | `Map` | | A Map like object with methods get, set, and clear to serve as the cache of results. |
| `cacheParamsFn` | `Function` | defaultCacheParamsFn | A function that returns a JSON stringifiable set or params to be used in the cacheKey. The default function traverses the params and removes any functions` |
| `selectFn` | `Function` | defaultSelectFn | A function that selects or resolves data after it is returned from the cache |
| `loaderOptions` | `Object` | {} | See `DataLoader` |


Expand All @@ -187,7 +189,7 @@ Create a new service-loader. This class lazily configures underlying `DataLoader

const loader = new ServiceLoader({
app,
serviceName: 'users'
path: 'users'
cacheParamsFn: (params) => {
return {
userId: params.user.id,
Expand Down Expand Up @@ -217,7 +219,7 @@ Create a new service-loader. This class lazily configures underlying `DataLoader
<!--- class DataLoader --------------------------------------------------------------------------->
<h2 id="class-dataloader">class DataLoader( batchLoadFunc [, options] )</h2>

This library re-exports [Dataloader](https://www.npmjs.com/package/dataloader) from its original package. Please see its documentation for more information. `loaderOptions` given to `BatchLoader` will be used to configure Dataloaders. You can also import `Dataloader` along with some helpful utility functions to build custom loaders.
This library re-exports [Dataloader](https://www.npmjs.com/package/dataloader) from its original package. Please see its documentation for more information. `loaderOptions` given to `ServiceLoader` will be used to configure Dataloaders. You can also import `Dataloader` along with some helpful utility functions to build custom loaders.

```js
const { DataLoader uniqueResults, uniqueKeys } = require("feathers-dataloader");
Expand Down
7 changes: 3 additions & 4 deletions docs/loader/service-loader.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,9 @@ TODO: Provide Links to the DataLoader and FindLoader options.

- **options** `{Object}`
- **app** `{Object}` - A Feathers app,
- **serviceName** `{String}` - The name of the service
- **service** `{String}` - The name of the service
- **cacheMap** `{Object}` - Instance of Map (or an object with a similar API) to be used as cache. Defaults to `new Map()`
- **cacheParamsFn** `{Function}` - A function that returns JSON.strinify-able params of a query to be used in the `cacheMap`. This function should return a set of params that will be used to identify this unique query and removes any non-serializable items. The default function returns traverses params and removes any functions. Defaults to `defaultCacheParamsFn`
- **cacheKeyFn** `{Function}` - Normalize keys. `(key) => key && key.toString ? key.toString() : String(key)` Defaults to `defaultCacheKeyFn`

There are two ways to create `ServiceLoader` instances.

Expand All @@ -58,7 +57,7 @@ You can also directly create an instance using the `ServiceLoader` class.
const { ServiceLoader } = require('@feathersjs/loader')

// This is our ServiceLoader instance
const userLoader = new ServiceLoader({ app, serviceName: 'users' })
const userLoader = new ServiceLoader({ app, service: 'users' })
```

## Example
Expand All @@ -71,7 +70,7 @@ const loaderOptions = {}

const loader = new ServiceLoader({
app
serviceName: 'users',
service: 'users',
...loaderOptions
})

Expand Down
25 changes: 15 additions & 10 deletions src/appLoader.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,46 @@ const BaseServiceLoader = require('./serviceLoader')

module.exports = class AppLoader {
constructor({ app, services = {}, ...loaderOptions }) {
this.options = { app, services, loaderOptions }
this.loaders = new Map()
this.options = {
app,
services,
loaderOptions,
loaders: new Map()
}
}

service(serviceName) {
service(path) {
const { app } = this.options
const { ServiceLoader, ...loaderOptions } = {
ServiceLoader: BaseServiceLoader,
...this.options.loaderOptions,
...(this.options.services[serviceName] || {})
...(this.options.services[path] || {})
}
const cachedLoader = this.loaders.get(serviceName)
const cachedLoader = this.options.loaders.get(path)

if (cachedLoader) {
return cachedLoader
}

const loader = new ServiceLoader({
...loaderOptions,
serviceName,
path,
app
})

this.loaders.set(serviceName, loader)
this.options.loaders.set(path, loader)

return loader
}

async clear() {
const { loaders } = this.options
const promises = []
this.loaders.forEach((loader) => {
promises.push(loader.cacheMap.clear())
loaders.forEach((loader) => {
promises.push(loader.clear())
})
await Promise.all(promises)
this.loaders.clear()
loaders.clear()
return this
}
}
Loading