{"id":18966767,"url":"https://github.com/txstate-etc/dataloader-factory","last_synced_at":"2025-10-09T01:36:27.987Z","repository":{"id":35417983,"uuid":"216217121","full_name":"txstate-etc/dataloader-factory","owner":"txstate-etc","description":"DataLoader classes to make it easier to write complex graphql resolvers.","archived":false,"fork":false,"pushed_at":"2025-07-12T20:01:08.000Z","size":635,"stargazers_count":12,"open_issues_count":0,"forks_count":1,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-09-19T00:42:57.318Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/txstate-etc.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2019-10-19T14:14:36.000Z","updated_at":"2025-07-12T20:00:51.000Z","dependencies_parsed_at":"2024-06-19T22:54:10.308Z","dependency_job_id":"37f9967d-5cf5-4aa9-b34f-d86f63c485de","html_url":"https://github.com/txstate-etc/dataloader-factory","commit_stats":{"total_commits":79,"total_committers":3,"mean_commits":"26.333333333333332","dds":0.1392405063291139,"last_synced_commit":"506aef576f19310daa70754ca6185f56a07582d0"},"previous_names":[],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/txstate-etc/dataloader-factory","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/txstate-etc%2Fdataloader-factory","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/txstate-etc%2Fdataloader-factory/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/txstate-etc%2Fdataloader-factory/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/txstate-etc%2Fdataloader-factory/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/txstate-etc","download_url":"https://codeload.github.com/txstate-etc/dataloader-factory/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/txstate-etc%2Fdataloader-factory/sbom","scorecard":{"id":904602,"data":{"date":"2025-08-11","repo":{"name":"github.com/txstate-etc/dataloader-factory","commit":"e23c457d77171ed0bbce217ba1c5a81b7d3adb01"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3,"checks":[{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Token-Permissions","score":-1,"reason":"No tokens found","details":null,"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Maintained","score":0,"reason":"1 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"Dangerous-Workflow","score":-1,"reason":"no workflows found","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Pinned-Dependencies","score":-1,"reason":"no dependencies found","details":null,"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-24T16:52:57.563Z","repository_id":35417983,"created_at":"2025-08-24T16:52:57.567Z","updated_at":"2025-08-24T16:52:57.567Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":279000718,"owners_count":26082895,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-08T02:00:06.501Z","response_time":56,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-11-08T14:38:27.448Z","updated_at":"2025-10-09T01:36:27.966Z","avatar_url":"https://github.com/txstate-etc.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# dataloader-factory\nA Factory pattern designed to be used to implement GraphQL resolvers efficiently.\n\nThe basic concept of this library is that you will create a new factory instance per request and\nstash it in the graphql request context. Then you can ask the factory any time you need a dataloader\ninstance, and it will either generate one or return one it already created.\n\nIn addition, it takes on the burden of putting results into the correct order for dataloader, by\nhaving you specify an `extractId` or `extractKey` function to teach it where the id is in your data\nobject.\n\n## Basic Usage (Load by Primary Key)\nYou can spread your `*Loader` configurations out into any file structure you like, just\ncreate and export an instance of each type of loader, so you can import it in your resolvers.\n\n```javascript\nimport { PrimaryKeyLoader } from 'dataloader-factory'\nexport const authorLoader = new PrimaryKeyLoader({\n  fetch: async ids =\u003e {\n    return db.query(`SELECT * FROM authors WHERE id IN (${ids.map(id =\u003e '?').join(',')})`, ids)\n  },\n  extractId: book =\u003e book.id\n})\n```\nThen the factory should be added to context on each request, e.g.:\n```javascript\nimport { DataLoaderFactory } from 'dataloader-factory'\nnew ApolloServer({\n  context: req =\u003e {\n    return { dataLoaderFactory: new DataLoaderFactory() }\n  }\n})\n```\nThen it may be used in resolvers:\n```javascript\nimport { authorLoader } from './authorLoader.js' // or wherever you put it\nexport const bookAuthorResolver = (book, args, context) =\u003e {\n  return context.dataLoaderFactory.get(authorLoader).load(book.authorId)\n}\n```\nWhen you pass an instance of one of the `*Loader` classes to `dataLoaderFactory.get()`,\ndataloader-factory will use the configuration information it contains to create and\ncache instances of `DataLoader` for you, generating a brand new set of `DataLoader`s for\nevery new request.\n### Options\nThe `PrimaryKeyLoader` constructor accepts the following input:\n```typescript\nconst myLoader = new PrimaryKeyLoader\u003cIdType, ObjectType\u003e({\n  // the core batch function for DataLoader, except DataLoaderFactory handles\n  // putting it back together in order, so all you need to do is fetch\n  // see below for discussion of context parameter\n  fetch: (ids: IdType[], context) =\u003e ObjectType[],\n\n  // a function for extracting the id from each item returned by fetch\n  // if not specified, it guesses with this default function\n  extractId: item =\u003e item.id || item._id,\n\n  // specify one or more primary key loaders and they will automatically\n  // be primed with any results gathered\n  // NOTE: if the above fetch function returns result objects that differ from those of\n  // the specified loader(s), it's going to cause you problems\n  idLoader: PrimaryKeyLoader|PrimaryKeyLoader[],\n\n  // the options object to pass to dataloader upon creation\n  // see dataloader documentation for details\n  options: DataLoader.Options\n})\n```\n### Note about extractId\nLet's take a moment to discuss the `extractId` configuration option as shown in the above\nexample. I find that it is a common sticking point for people first learning about building\ngraphql APIs with dataloader-factory.\n\nIf you've read the [documentation for dataloader](https://github.com/graphql/dataloader) (and\nI hope you have!), you'll recall that the batch function you give to new DataLoader is required\nto return results in the exact same order as the keys that were provided, and the return array\nshould have undefined in it for any keys that didn't turn out to exist.\n\nSo if you get the array `[2, 1, 3]`, and 3 doesn't exist in your database, you must return\n`[{ myKey: 2, ...moredata }, { myKey: 1, ...moredata }, undefined]`\n\nThe typical performant pattern for that is:\n```javascript\nconst myKeyLoader = new DataLoader(async (keys) =\u003e {\n  const rows = await lookupKeysInDatabase(keys)\n  const rowsByKey = results.reduce((rowsByKey, row) =\u003e ({ ...rowsByKey, [row.myKey]: row }), {})\n  return keys.map(k =\u003e rowsByKey[k])\n})\n```\nI find that pattern to be quite repetitive and ugly, even if you replace the reduce with\n`lodash.groupBy` or the like. Furthermore, the one-to-many and many-to-many patterns are a\nlittle more complicated to unwind. So dataloader-factory handles that part for you. Your `fetch`\nfunction only has to return a set of rows, and they'll get properly sorted and formatted for\ndataloader automatically.\n\nThe only thing I need from you to make that happen is a function that tells me which property\nrepresents the key that we were batching on. That's why `extractId` (or `extractKey(s)` in the\nother loader types) is part of the configuration. In the example above, we were batching on `myKey`\nso the `extractId` function would be `row =\u003e row.myKey`.\n\nYou can also specify a string `extractId: 'myKey'` instead of a function and that'll work. Or\nif your key is the highly typical `'id'` or `'_id'`, you don't even need to specify `extractId`\nbecause I'll look for those anyway (in that order of preference).\n\nOur above pattern becomes much simpler:\n```javascript\nconst myKeyLoader = new PrimaryKeyLoader({\n  fetch: async (keys) =\u003e await lookupKeysInDatabase(keys),\n  extractId: row =\u003e row.myKey\n})\n```\n\n### Pass-through Context\nIn the Options section above, you may have noticed that the `fetch` function receives a `context`\nparameter. This is an extra option for you to be able to pass information from the request context\nthrough to your `fetch` function, in case you have filters that are context-sensitive, like a dataloader\nfor fetching friendships of the current user or one that needs to fetch only records the current\nuser is authorized to read.\n\nTo set the pass-through `context`, pass it in when you construct a new `dataLoaderFactory` instance,\nand then it will be passed to your fetch functions.\n```javascript\nnew ApolloServer({\n  context: req =\u003e {\n    const userinfo = parseBearerToken(req.headers.authorization)\n    return { dataLoaderFactory: new DataLoaderFactory(userinfo) }\n  }\n})\n```\n### Note about idLoader\nThe idLoader configuration is there to help you keep your dataloaders properly primed. For\ninstance, say you have a loader for fetching books by their authorId. Well, those books\nthat got returned have all their data, so there's no reason why you should go back to the\ndatabase if a later part of the query asks for one of the books by id. To make sure we can\nre-use books fetched by authorId, we need to prime the id loader with the results of the\nauthorId loader. We do that with the `idLoader` configuration option on the authorId loader:\n\n```typescript\nconst booksByIdLoader = new PrimaryKeyLoader({ ... configuration ... })\nconst booksByAuthorIdLoader = new OneToManyLoader({\n  // ... regular configuration for the loader ...\n  idLoader: booksByIdLoader\n})\n```\nNow dataloader-factory will automatically keep `booksByIdLoader` primed with anything fetched\nby booksByAuthorIdLoader.\n\nIt's also possible that you have two good unique keys for a data type. For example, a book might\nhave an internal auto_increment id in your system, plus its ISBN identifier used by the rest\nof the world. You probably want to have the book cached no matter which identifier gets queried.\n\nIt's a little more complicated to pull this off because you can't set the idLoader on whichever\nloader you make first, because the second loader won't exist yet. For this case, PrimaryKeyLoader\noffers a method `.addIdLoader(loader)` so you can add it after creation:\n```typescript\nconst booksByIdLoader = new PrimaryKeyLoader({ /* configuration */ })\nconst booksByISBNLoader = new PrimaryKeyLoader({\n  // ... configuration ...\n  idLoader: booksByIdLoader\n})\nbooksByIdLoader.addIdLoader(booksByISBNLoader)\n```\n## One-to-Many DataLoaders\nThe `PrimaryKeyLoader` is only appropriate for primary key lookups (or another key that\nidentifies exactly one record). To fetch relations that return an array, a more complex pattern\nis required.\n\nThe first of these patterns is the one-to-many pattern. Use it when you have a one to many\nrelationship you're trying to map, and your fetch function returns objects that will each map to\na single key value. For instance, a page always exists inside a single book, so the\n`pagesByBookId` implementation might look like this:\n```javascript\nimport { OneToManyLoader } from 'dataloader-factory'\nconst pagesByBookIdLoader = new OneToManyLoader({\n  fetch: async bookids =\u003e {\n    return db.query(\n      `SELECT * FROM pages WHERE bookId IN (${bookids.map(id =\u003e '?').join(',')})`,\n    bookids)\n  },\n  extractKey: page =\u003e page.bookId\n})\n```\nThe resolver might then look like this.\n```javascript\nexport const bookPagesResolver = (book, args, context) =\u003e {\n  return context.dataLoaderFactory.get(pagesByBookIdLoader, args).load(book.id)\n}\n```\nNote that this is also useful for many-to-many relationships that have a named intermediary. For\ninstance, the relationship between a book and a library might be represented as an `Acquisition` that\nlinks a book and a library and additionally lists a date the book was purchased.  In this case the\ndataloader for `book -\u003e acquisition` is one-to-many, the dataloader for `library -\u003e acquisition` is\none-to-many, and for `book -\u003e library` the developer has the option of chaining\n`book -\u003e acquisition -\u003e library` or creating a new many-to-many dataloader that uses a database join\nto save a round trip (see the \"Many-to-Many-Joined\" section below).\n\n### Options\nThe `OneToManyLoader` constructor accepts the following inputs. All of the *-to-many patterns accept the\nsame options, except as specifically noted in their section of the documentation.\n```typescript\nconst myOneToManyLoader = new OneToManyLoader\u003cKeyType, ObjectType, FilterType\u003e({\n  // accept arbitrary foreign keys and arbitrary arguments and return results\n  // the keys MUST appear in the result objects so that your\n  // extractKey function can retrieve them\n  // see PrimaryKeyLoader options for discussion of context parameter\n  fetch: async (keys: KeyType[], filters: FilterType, context) =\u003e ObjectType[] // required\n\n  // function that can pull the foreign key out of the result object\n  // must match the type of the keys you're using in your fetch function\n  extractKey: (item: ObjectType) =\u003e item.authorId // required\n\n  // advanced usage only, covered later in this readme\n  matchKey: (key: KeyType, item: ObjectType) =\u003e boolean\n\n  // see below section titled \"Note about filter and batch overlap\"\n  keysFromFilter: (filters: FilterType) =\u003e KeyType[]\n\n  // generated dataloaders will not keep a cache, batch only\n  skipCache: false\n\n  // maxBatchSize to be passed to each DataLoader\n  maxBatchSize: 1000\n\n  // cacheKeyFn to be passed to each DataLoader, default is fast-json-stable-stringify\n  // which should be good for almost any case\n  cacheKeyFn: (key: KeyType) =\u003e stringify(key)\n\n  // specify one or more primary key loaders and they will automatically\n  // be primed with any results gathered\n  // NOTE: if the above fetch function returns result objects that differ from those of\n  // the specified loader(s), it's going to cause you problems\n  idLoader: PrimaryKeyLoader|PrimaryKeyLoader[]\n})\n```\nNote that `KeyType` can be anything serializable, so you can use arrays or objects for any compound keys you may have.\n\n## Many-to-Many DataLoaders\nFor DataLoaderFactory, the Many-to-Many pattern is split into two use-cases: one targeted at\ndocument-oriented databases like MongoDB (this section), another for relational databases like MySQL or\nOracle (see the next section, \"Many-to-Many-Joined\").\n\nIn document-oriented databases a typical pattern for a simple many-to-many relationship is to store an\narray of keys inside one of the documents. For instance, a book might be represented like this:\n```javascript\n{\n  id: 1,\n  title: 'Great American Novel',\n  genreIds: [1,3,8]\n}\n```\nThe `Book.genres` resolver is trivial, you can use the primary key loader with your `genres` array.\nHowever, `Genre.books` requires a special treatment from DataLoaderFactory that asks you for\n`extractKeys` instead of `extractKey` (all other options are identical):\n```javascript\nimport { ManyToManyLoader } from 'dataloader-factory'\nconst booksByGenreIdLoader = new ManyToManyLoader({\n  fetch: async genreIds =\u003e {\n    // this example is for mongodb client\n    return db.collection('books').find({ genreIds: { $in: genreIds } }).toArray()\n  },\n  extractKeys: book =\u003e book.genreIds\n})\n```\nand the resolver\n```javascript\nexport const genreBooksResolver = (genre, args, context) =\u003e {\n  return context.dataLoaderFactory.get(booksByGenreIdLoader).load(genre.id)\n}\n```\nNote that it is also possible to use a named intermediary in document-oriented databases. Depending on\nthe database, you may still find the Many-to-Many-Joined pattern useful in those cases.\n\n## Many-to-Many-Joined DataLoaders\nIt is technically possible to handle SQL many to many relationships with the OneToManyLoader, like this:\n```javascript\nimport { OneToManyLoader } from 'dataloader-factory'\nconst booksByGenreIdLoader = new OneToManyLoader({\n  fetch: async genreIds =\u003e {\n    const books = await db.get(`\n      SELECT b.*, bg.genre_id as genreId\n      FROM books b\n      INNER JOIN books_genres bg ON b.id = bg.book_id\n      WHERE bg.genre_id IN (${genreIds.map(id =\u003e '?').join(',')})`)\n    return books\n  },\n  extractKey: book =\u003e book.genreId\n})\n```\nThis will work but it means the book object being passed to the rest of your code has a `genreId`\nproperty that doesn't really belong there. This is especially annoying when using Typescript as you need\nto create a new interface like `BookWithGenreId` to represent this not-quite-a-book object.\n\nLuckily DataLoaderFactory provides a cleaner pattern with `ManyJoinedLoader`:\n```javascript\nimport { ManyJoinedLoader } from 'dataloader-factory'\nconst booksByGenreIdLoader = new ManyJoinedLoader({\n  fetch: async genreIds =\u003e {\n    const books = await db.get(`\n      SELECT b.*, bg.genre_id as genreId\n      FROM books b\n      INNER JOIN books_genres bg ON b.id = bg.book_id\n      WHERE bg.genre_id IN (${genreIds.map(id =\u003e '?').join(',')})`)\n    return books.map(row =\u003e ({ key: row.genreId, value: new Book(row) }))\n  }\n})\n```\nYou no longer provide an `extractKey` function because you return it with each row. DataLoaderFactory\nwill use the key you provide to put the data back together and then discard it, returning only the\npristine `Book` from the `value` field back to the `.load()` call in your resolver.\n\n## Best-Match DataLoaders\nIn some rare cases, you may have a key that provides a fuzzy match to an object. For instance, to\ncreate a self-healing link, you might have a key like `{ id: 9, path: '/about' }`. You're looking\nfor an object that has id 9, or if nothing has id 9, then an object with path `/about`.\n\nThis is really complicated to dataload, so this library provides a `BestMatchLoader` especially for\nthis case.\n\nHere's an example where I have a book's id, title, and author, but I'm not sure whether the id has\nchanged in my database. So I want to fall back to title and author when id doesn't match.\n```typescript\nimport { BestMatchLoader } from 'dataloader-factory'\nconst bookByLinkLoader = new BestMatchLoader({\n  fetch: async (keys: { id: string, title: string, author: string }[]) =\u003e await db.get(`\n      SELECT * FROM books\n      WHERE id IN (${keys.map(key =\u003e '?').join(',')})\n      OR (title, author) IN ((${keys.map(key =\u003e '?,?').join('),(')}))\n    `, [...keys.map(key =\u003e key.id), ...keys.flatMap(key =\u003e [key.title, key.author])]),\n  scoreMatch: (key, book) =\u003e key.id === book.id\n    ? 2\n    : (key.author === book.author \u0026\u0026 key.title === book.title\n        ? 1\n        : 0)\n})\n```\nThen I can use the loader like normal, expecting AT MOST ONE result per key (it can still end up with no matches).\n```typescript\nexport const libraryItemLinkResolver = (libraryItem, args, context) =\u003e {\n  const book = await context.dataLoaderFactory.get(bookByLinkLoader).load({\n    id: libraryItem.bookId,\n    title: libraryItem.title,\n    author: libraryItem.author\n  })\n  return book\n}\n```\n\n### Options\n```typescript\nnew BestMatchLoader\u003cKeyType, ObjectType\u003e({\n  // see PrimaryKeyLoader options for discussion of context parameter\n  fetch: async (keys: KeyType[], context) =\u003e ObjectType[] // required\n\n  // take a key and an object and determine how well they match\n  // each key will only pick one \"winner\" which will be the item that returns\n  // the highest score\n  // return a score of 0 when the key and item do not match at all\n  scoreMatch: (key: KeyType, item: ObjectType) =\u003e number\n\n  // specify one or more primary key loaders and they will automatically\n  // be primed with any results gathered\n  // NOTE: if the above fetch function returns result objects that differ from those of\n  // the specified loader(s), it's going to cause you problems\n  idLoader: PrimaryKeyLoader|PrimaryKeyLoader[],\n\n  // the options object to pass to dataloader upon creation\n  // see dataloader documentation for details\n  // the default batch size will be 100 here, to help limit the impact of the O(n^2)\n  // nature of the BestMatchLoader's scoring strategy\n  options: DataLoader.Options\n})\n```\n## Parent Document DataLoaders\nIn document-oriented database, you may sometimes have arrays of globally unique addressable objects that\ncanonically live inside a larger document. For example, if you stored books in a single collection with\nall their pages, maybe your document would look like:\n```javascript\n{ id: 'uieYY09j6P', name: 'The Grapes of Wrath', pages: [{ id: 'nyT694sm23', text: '...' }] }\n```\nThe page belongs to the book and does not exist anywhere else. If you want to look up the book based on\nthe id of one of its pages, you might try to write a OneToManyLoader, but it will return an array and ask\nfor filters when those are not expected. Additionally, the loadMany function will return multiple copies\nof the parent document.\n\nTo solve all of these problems, a ParentDocumentLoader is provided.\n\n### Options\n```typescript\nnew ParentDocumentLoader\u003cKeyType, ObjectType\u003e({\n  // see PrimaryKeyLoader options for discussion of context parameter\n  fetch: async (childIds: KeyType[], context) =\u003e ObjectType[], // required\n\n  // return the unique ids of the child objects inside the parent\n  // document, these should be globally unique ids that match the\n  // childIds passed to the fetch function\n  childIds: (parentDoc: ObjectType) =\u003e KeyType[],\n\n  // return the unique id of of the parent document\n  // only needed when using loadMany and will guess '_id' or 'id' the same\n  // as extractId\n  parentId?: keyof ObjectType | (item =\u003e string),\n\n  // specify one or more primary key loaders for the parent document\n  // automatically priming a primary key loader for the child documents is not\n  // supported but you can do it yourself\n  idLoader?: PrimaryKeyLoader|PrimaryKeyLoader[],\n\n  // the options object to pass to dataloader upon creation\n  // see dataloader documentation for details\n  // the default batch size will be 100 here, to help limit the impact of the O(n^2)\n  // nature of the BestMatchLoader's scoring strategy\n  options?: DataLoader.Options\n})\n```\n\n## Parameter-based filtering\nNow we get to the part where this library can really save your bacon. Consider the following GraphQL\nquery:\n```graphql\n{ authors { books(genre: \"mystery\") { title } } }\n```\nWithout `dataloader-factory`, the typical pattern for the authors.books dataloader looks like this:\n```javascript\nconst booksByAuthorId = new DataLoader(async (authorIds) =\u003e {\n  const books = await db.query(\n    `SELECT * FROM books WHERE authorId IN (${authorIds.map(id =\u003e '?').join(',')})`\n  , authorIds)\n  const bookMap = lodash.groupBy(books, 'authorId')\n  return authorIds.map(id =\u003e bookMap[id] || [])\n})\n```\nEasy so far, but adding the `genre: \"mystery\"` filter is not obvious and can be very confusing to\nimplement. You can only batch on one key at a time, so how are you supposed to batch when you have\nboth an authorId and a genre to filter with?\n\nSince `genre: \"mystery\"` is the argument, and `authors` is the array in this query, clearly we want\nto batch on `authorId`. But if I call `.load(authorId)`, how am I going to pass the genre to the\nbatch function? I could try something like `.load({ authorId, genre })`, but transforming that into\nSQL is going to be a nightmare as the number of filters gets larger.\n\nThe answer is actually to create a new DataLoader instance for each unique set of extra filtering\narguments. So in the genre case, we need a DataLoader for mysteries, another DataLoader for fantasy,\nand so on.  Since we don't want to hard-code an exhaustive list of genres, we need a function that\ncan generate new DataLoaders, and since we don't want to use infinite RAM, it should do so on demand.\nWell we're in luck, because that's exactly what dataloader-factory was built to do!\n\nSo using dataloader-factory, it becomes fairly simple; the resolver would look like this (ignore the\noverly simplistic data model):\n```javascript\nimport { OneToManyLoader } from 'dataloader-factory'\nconst booksByAuthorIdLoader = new OneToManyLoader({\n  fetch: (authorIds, filters) =\u003e {\n    const query = `SELECT * FROM books WHERE authorId IN (${authorIds.map('?').join(',')})`\n    const params = [...authorIds]\n    if (filters.genre) {\n      query += ' AND genre=?'\n      params.push(filters.genre)\n    }\n    return db.query(query, params)\n  },\n  extractKey: book =\u003e book.authorId\n})\nexport const authorBooksResolver = (author, filters, context) =\u003e {\n  return context.dataLoaderFactory.get(booksByAuthorIdLoader, filters).load(author.id)\n}\n```\nNote that the arguments we get from GraphQL get passed to the `.get()` function, NOT the\n`.load()` function. The `.load()` function belongs to DataLoader. I don't change it in any way.\nThe `.get()` function stringifies the filters and uses that as a key to determine whether a new\nDataLoader should be constructed. Then the filters get passed (unstringified) to the fetch\nfunction so it can use them to do its work.\n\nWhat this does is generate a distinct DataLoader instance for each set of GraphQL arguments we\nencounter. Generating so many DataLoaders may seem like a problem, but remember - graphql\nqueries are always finite, so there can only be a few sets of arguments in any one request,\nand each request gets a new factory. So the number of possible dataloaders generated per request\nis manageable, and they all get garbage collected after the request.\n\n## Advanced Usage Example\nMany GraphQL data types will have more than one other type referencing them. In those\ncases, it will probably be useful to create a single function that accepts a set of `filters`\nand uses it to construct and execute a database query. Then each `fetch` function can simply\nmerge the batch of `keys` into the `filters` object, and pass the merged object to the\nquery function. This example should help illustrate:\n```javascript\nconst executeBookQuery = filters =\u003e {\n  const where = []\n  const params = []\n  if (filters.ids) {\n    where.push(`id IN (${filters.ids.map(id =\u003e '?').join(',')})`)\n    params.push(...filters.ids)\n  }\n  if (filters.authorIds) {\n    where.push(`authorId IN (${filters.authorIds.map(id =\u003e '?').join(',')})`)\n    params.push(...filters.authorIds)\n  }\n  if (filters.genres) {\n    where.push(`genres IN (${filters.genres.map(id =\u003e '?').join(',')})`)\n    params.push(...filters.genres)\n  }\n  const wherestr = where.length \u0026\u0026 `WHERE (${where.join(') AND (')})`\n  return db.query(`SELECT * FROM books ${wherestr}`, params)\n}\nconst booksByAuthorIdLoader = new OneToManyLoader({\n  fetch: (authorIds, filters) {\n    return executeBookQuery({ ...filters, authorIds })\n  },\n  extractKey: item =\u003e item.authorId\n})\nconst booksByGenreLoader = new OneToManyLoader({\n  fetch: (genres, filters) =\u003e {\n    return executeBookQuery({ ...filters, genres })\n  },\n  extractKey: item =\u003e item.genre\n})\n```\nSee how executeBookQuery is re-usable no matter how many different types of filtering we add? You don't\nhave to use this pattern but I find it very helpful.\n\n### Note about filter and batch overlap\nOne situation that pops up when you start using this strategy and allowing filters on field resolvers\nis your users can start writing queries that don't make a lot of sense:\n```graphql\n{ genres {\n    name\n    books (genres: [\"mystery\", \"fantasy\"]) {\n      name\n    }\n}}\n```\nThis doesn't make a lot of sense because you're going to get results like:\n```javascript\n[{\n  name: 'nonfiction',\n  books: [ /* only books tagged with nonfiction AND mystery or fantasy */ ]\n},{\n  name: 'mystery',\n  books: [ /* all the books you'd expect */ ]\n}, /* etc */]\n```\nEven then, making it work that way is complicated. It translates to SQL like\n`genre IN (... batchedGenres ...) AND genre IN ('mystery','fantasy')`, which can\nbe a pain to try to generate.\n\ndataloader-factory has one more trick up its sleeve for this if you opt to use it. Simply\nprovide a `keysFromFilter` function that takes your `filters` object and returns the key or\nkeys exactly as `extractKey` would return the key from the model:\n```typescript\nconst booksByGenreLoader = new ManyToManyLoader({\n  fetch: async (genres, filters) =\u003e await executeBookQuery({ ...filters, genres }),\n  extractKeys: book =\u003e book.genres,\n  keysFromFilter: filters =\u003e filters.genres\n})\n```\nWhen you provide `keysFromFilter`, dataloader-factory will perform application-side filtering to\nremove any objects that don't match your filter (it uses the cacheKeyFn to compare them). Your\ndatabase query need only worry about the batch keys.\n\nNote that in my fetch function above I write `executeBookQuery({ ...filters, genres })`. Order is\nimportant in that merge operation, as the batch keys MUST overwrite the corresponding filter\nparameter, not the other way around.\n\n## Compound Keys\nCompound Keys are fully supported. Any key object will be accepted. It is up to your `fetch` and\n`extractKey` functions to treat it properly. Internally, a stable JSON stringify is used to cache\nresults, so it will construct the same string even if two objects' keys have mismatching ordering.\n\n## matchKey\nIn rare cases it may be that you are unable to provide an `extractKey` function because a key\ncannot be extracted from an item because an irreversible operation is involved (like evaluating\ngreater than or less than)\n\nIn those cases, you can provide a `matchKey` function that examines whether the result object is\na match for the given key. The answer will help us put the fetched dataset back together properly.\n\nPlease note that this makes the batched load an O(n^2) operation so `extractKey` is\npreferred whenever possible. If you use `matchKey`, the `maxBatchSize` default will change to 50\nto help limit scaling problems.\n```javascript\nconst booksAfterYearLoader = new OneToManyLoader({\n  fetch: (years, filters) =\u003e {\n    const ors = years.map(parseInt).map(year =\u003e `published \u003e DATE('${year}0101')`)\n    return db.query(`SELECT * FROM books WHERE (${ors.join(') OR (')})`\n  },\n  matchKey: (year, book) =\u003e book.published.getTime() \u003e= new Date(year, 0, 1)\n})\n```\n\n## loadMany\nDataloader has a `.loadMany` method that can be used to retrieve objects based on an\narray of keys. However, it is designed to catch and return Errors for any keys that throw an\nerror, so you have to be aware of that any time you use it. Additionally, keys that do not\npoint at any data will come back undefined, and you will have undefined values in the returned\narray.\n\nTo avoid both of these problems, a `.loadMany` method exists on the factory for your convenience.\nIts return array only contains objects that exist - no undefined values. Additionally, if\nany key throws an error, `loadMany` throws the error instead of catching it and inserting it\ninto the array.\n```javascript\nconst books = await ctx.loaders.loadMany(bookLoader, bookIds)\n```\nIt also works for *ToMany loaders and flattens the results (this would return an array\nof books written by any of the specified authors):\n```javascript\nconst books = await ctx.loaders.loadMany(booksByAuthorLoader, authorIds, filters)\n```\n\n## Mutations\nIn graphql, mutations have return values, and the user is able to query any number and depth of\nproperties in that return object. In effect the user has a chance to make a new query after the mutation,\nand you may have already done some dataloading (e.g. to find related objects to help\nauthorize the mutation). You will want to get rid of any cached objects that were fetched before\nthe mutation completed. Creating a new DataLoaderFactory instance is one way, but probably\ncumbersome. Instead of replacing your instance, you can call `.clear()` on your instance and all\nyour existing dataloaders will be tossed so that your post-mutation query can run against fresh\ndata.\n\n## TypeScript\nThis library is written in typescript and provides its own types. When you create a new loader type,\nyou can choose whether to provide your types as generics, which will help you write your `fetch`\nfunction properly, or you can write your `fetch` function and its input/return types will be used\nimplicitly for everything else.\n```typescript\nimport { PrimaryKeyLoader } from 'dataloader-factory'\nconst bookLoader = new PrimaryKeyLoader({\n  fetch: async (ids: string[]) =\u003e { // provide the key type either here or as a generic\n    ... // return type will infer based on what you return here, or you can set it as a generic\n  },\n  extractId: (item) =\u003e { // typescript should know you'll receive IBook\n    ... // typescript should know you need to return string\n  }\n})\n\nexport const bookResolver = async (book, args, context) =\u003e {\n  // typescript should know load() accepts a string\n  // and that bookResolver will return Promise\u003cIBook\u003e\n  return await context.dataLoaderFactory.get(bookLoader).load(book.authorId)\n}\n```\nThe *ToMany classes work the same way, with a third generic for FilterType:\n```typescript\nconst booksByAuthorIdLoader = new OneToManyLoader({\n  fetch: (authorIds: string[], filters: BookFilters) {\n    // typescript will detect ReturnType = IBook if this returns Promise\u003cIBook[]\u003e\n    return executeBookQuery({ ...filters, authorIds })\n  },\n  extractKey: item =\u003e item.authorId\n})\nexport const authorBooksResolver = (author, args, context) =\u003e {\n  // typescript should know load() accepts a string\n  // and that authorBooksResolver will return Promise\u003cIBook[]\u003e\n  return context.dataLoaderFactory.get(authorBooksLoader, args).load(author.id)\n}\n```\n\n## Upgrading from 3.0\n\nThe 4.0 release is a major API revision that focuses on the typescript-safe API outlined in this\nREADME. The old string-based `.register` and `.get` and `.getOneToMany` and etc are all gone. You'll\nneed to update your code to change over to the new API, but the configuration options have not changed\n(except `matchKey` has been removed from the `ManyJoinedLoader` since it doesn't make sense).\n\n```javascript\nDataLoaderFactory.register('youruniquestring', { /* your config */ })\n// inside your resolvers\n  factory.get('youruniquestring').load(id)\n```\nshould be replaced with\n```javascript\nconst myLoader = new PrimaryKeyLoader({ /* your config */ })\n// inside your resolvers\n  factory.get(myLoader).load(id)\n```\nThe transition is very similar for the array-returning types, except all the `.getOneToMany`,\n`.getManyToMany`, etc, have been removed since `.get()` is simpler and can cover everything\nin 4.0.\n\n### If you were using typesafe classes already\nIf you were aready on the typesafe API, all you need to handle is that `factory.getMany` has\nbeen replaced in all cases by `factory.get`.\n```javascript\nfactory.getMany(myOneToManyLoader, args).load(id)\n```\nshould be replaced with\n```javascript\nfactory.get(myOneToManyLoader, args).load(id)\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftxstate-etc%2Fdataloader-factory","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftxstate-etc%2Fdataloader-factory","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftxstate-etc%2Fdataloader-factory/lists"}