feathersjs-ecosystem / feathers-batch

Batch multiple Feathers service calls into one
MIT License
96 stars 10 forks source link

Feature Request - Build instructions asynchronously #57

Closed DaddyWarbucks closed 3 years ago

DaddyWarbucks commented 4 years ago

This is not a bug, and not even necessarily a feature request. But I wanted to share some code that I thought feathers-batch may benefit from. It uses Facebooks DataLoader to construct the "instructions" sent to the server in an async manner. This would allow the developer to more easily create the "instructions" inside hooks and things like feathers-schema.

The following code is a snippet from a thought experiment outlined here: https://github.com/DaddyWarbucks/test-feathers-client-joins

Note this code is just kinda cobbled together and was also not built for feathers-batch specifically. It was built to match a much simpler (less powerful) server side "batch" service that only handles finds for the sake of the project mentioned above. So the key serialization, etc would need to change. It also does some processHooks.call stuff that probably doesn't belong here and could be handled elsewhere. It is also important to note that this is doing some batchLoader stuff with the { _id: { $in: [] } }. That is not relevant to implementing this in feathers-batch.

import DataLoader from 'dataloader';
const { processHooks } = require('@feathersjs/commons').hooks

// This is an experimental idea of merging batch/data loaders and the
// feathers-batch concept. It offers the nice API of batchloader but
// also groups all the loaders up and sends them to the server to be executed.
// I could't think of a better name than GroupLoader....
export default class GroupLoader {
  constructor(context) {
    this.app = context.app;
    this.groupLoader = new DataLoader(async keys => {
      const groupResults  = await this.app.service('api/batch').create(keys);
      return keys.map(key => {
        return groupResults[key] || new Error(`No result for ${key}`)
      });
    });
    this.loaders = {};
  }

  service(serviceName, params = {}) {
    return {
      load: (id) => {
        const loaderName = JSON.stringify({ serviceName, params });
        const loader = this.loaders[loaderName]
          || new DataLoader(async keys => {
            const service = this.app.service(serviceName);
            const beforeHooks = service.__hooks.before.find;
            const afterHooks = service.__hooks.after.find;

            // TODO: Technically this should run through the app.hooks too
            // console.log(this.app.__hooks)

            const group = {
              service: serviceName,
              query: { ...params.query, _id: { $in: keys } }
            }

            if (beforeHooks) {
              const beforeHook = await processHooks.call(service, beforeHooks, {
                app: this.app,
                type: 'before',
                method: 'find',
                service,
                params
              });

              group.query = { ...group.query, ...beforeHook.query };
            }

            const groupKey = JSON.stringify(group);

            const groupResults = await this.groupLoader.load(groupKey);

            if (afterHooks) {
              const afterHook = await processHooks.call(service, afterHooks, {
                app: this.app,
                type: 'after',
                method: 'find',
                service,
                params,
                result: groupResults
              });

              return keys.map(key => {
                const result = afterHook.result.find(result => {
                  return result._id === key;
                });
                return result || new Error(`No result for ${key}`);
              });

            }

            return keys.map(key => {
              const result = groupResults.find(result => {
                return result._id === key;
              });
              return result || new Error(`No result for ${key}`);
            });

          });

        this.loaders[loaderName] = loader;

        return loader.load(id);
      }
    };
  }
}

And ti would be used as follows

const setupGroupLoader = context => {
  context.params.groupLoader = new GroupLoader(context);
}

// Using fast-join, withResults, feathers-schema, or some other mechanism that the
// developer is using to async resolve things
const withResultsGroupLoader = withResult({
    user: (post, context) => {
      return context.params.groupLoader.service('api/users').load(post.user_id);
    },
    category: (post, context) => {
      return context.params.groupLoader.service('api/categories').load(post.category_id);
    }
});

I know there is some code in this example that is not relevant to feathers-batch, but I just copy pasted it from the other project. If this does not make sense let me know and I will type out a more specific implementation.

stale[bot] commented 4 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Apologies if the issue could not be resolved. FeathersJS ecosystem modules are community maintained so there may be a chance that there isn't anybody available to address the issue at the moment. For other ways to get help see here.