1- import Sequelize from 'sequelize' ;
2- import shimmer from 'shimmer' ;
1+ import assert from 'assert' ;
32import DataLoader from 'dataloader' ;
4- import { groupBy , property , values , clone , isEmpty , uniq } from 'lodash' ;
3+ import { clone , groupBy , isEmpty , property , uniq , values } from 'lodash' ;
54import LRU from 'lru-cache' ;
6- import assert from 'assert' ;
7- import { methods } from './helper' ;
5+ import Sequelize from 'sequelize' ;
6+ import shimmer from 'shimmer' ;
7+ import { methods } from './helper' ;
88
99const versionTestRegEx = / ^ [ 4 5 6 ] / ;
1010
@@ -176,9 +176,22 @@ function loaderForModel(model, attribute, attributeField, options = {}) {
176176 } ) ;
177177}
178178
179+ function loaderForFindOneWithConditions ( model , attribute , otherWhere , options = { } ) {
180+ return new DataLoader ( keys => {
181+ const findOptions = Object . assign ( { } , options ) ;
182+ delete findOptions . rejectOnEmpty ;
183+ findOptions . where = Object . assign ( { [ attribute ] : keys } , otherWhere ) ;
184+
185+ return model . findAll ( findOptions ) . then ( mapResult . bind ( null , attribute , keys , findOptions ) ) ;
186+ } , {
187+ cache : options . cache
188+ } ) ;
189+ }
190+
179191function shimModel ( target ) {
180192 if ( target . findByPk ? target . findByPk . __wrapped : target . findById . __wrapped ) return ;
181193
194+ // findByPk
182195 shimmer . massWrap ( target , methods ( Sequelize . version ) . findByPk , original => {
183196 return function batchedFindById ( id , options = { } ) {
184197 if ( options . transaction || options . include || activeClsTransaction ( ) || ! options [ EXPECTED_OPTIONS_KEY ] ) {
@@ -204,6 +217,114 @@ function shimModel(target) {
204217 } ) . then ( rejectOnEmpty . bind ( null , options ) ) ;
205218 } ;
206219 } ) ;
220+
221+ // findOne
222+ shimmer . massWrap ( target , methods ( Sequelize . version ) . findOne , ( ) => {
223+ return function batchedFindOne ( options = { } ) {
224+ const keys = Object . keys ( options . where || { } ) ;
225+ const symbolKeys = Object . getOwnPropertySymbols ( options . where || { } ) ;
226+ const id = options . where && options . where [ this . primaryKeyAttribute ] ;
227+ const batchByAttribute = options [ BATCH_BY_ATTRIBUTE ] ;
228+
229+ if ( options . transaction || options . include || activeClsTransaction ( ) || ! options [ EXPECTED_OPTIONS_KEY ] ) {
230+ const findOptions = Object . assign ( { plain : true , rejectOnEmpty : false } , options ) ;
231+ if ( findOptions . limit === undefined ) {
232+ const pkVal = findOptions . where && findOptions . where [ this . primaryKeyAttribute ] ;
233+ if ( ! findOptions . where || ! ( typeof pkVal === 'number' || typeof pkVal === 'string' || Buffer . isBuffer ( pkVal ) ) ) {
234+ findOptions . limit = 1 ;
235+ }
236+ }
237+ return this . findAll ( findOptions ) ;
238+ }
239+
240+ // BATCH_BY_ATTRIBUTE path: user explicitly declares which field to batch by
241+ if ( batchByAttribute ) {
242+ const batchValue = options . where && options . where [ batchByAttribute ] ;
243+ if ( ! [ 'string' , 'number' ] . includes ( typeof batchValue ) ) {
244+ const findOptions = Object . assign ( { plain : true , rejectOnEmpty : false } , options ) ;
245+ if ( findOptions . limit === undefined ) findOptions . limit = 1 ;
246+ return this . findAll ( findOptions ) ;
247+ }
248+
249+ return Promise . resolve ( ) . then ( ( ) => {
250+ const loaders = options [ EXPECTED_OPTIONS_KEY ] . loaders ;
251+ const otherWhere = Object . assign ( { } , options . where ) ;
252+ delete otherWhere [ batchByAttribute ] ;
253+
254+ const loaderOptions = { where : otherWhere , order : options . order , raw : options . raw , paranoid : options . paranoid } ;
255+ const cacheKey = getCacheKey ( this , batchByAttribute , loaderOptions ) ;
256+ let loader = loaders . autogenerated . get ( cacheKey ) ;
257+ if ( ! loader ) {
258+ loader = loaderForFindOneWithConditions ( this , batchByAttribute , otherWhere , {
259+ order : options . order ,
260+ raw : options . raw ,
261+ paranoid : options . paranoid ,
262+ logging : options . logging ,
263+ cache : true
264+ } ) ;
265+ loaders . autogenerated . set ( cacheKey , loader ) ;
266+ }
267+ return loader . load ( batchValue ) ;
268+ } ) . then ( rejectOnEmpty . bind ( null , options ) ) ;
269+ }
270+
271+ if ( ! [ 'string' , 'number' ] . includes ( typeof id ) ) {
272+ // Call this.findAll() directly rather than the original findOne, because
273+ // Sequelize v3's findOne internally calls Model.prototype.findAll (bypassing
274+ // instance-level overrides like sinon spies). Calling this.findAll() with the
275+ // same options findOne would use is semantically equivalent across all versions.
276+ const findOptions = Object . assign ( { plain : true , rejectOnEmpty : false } , options ) ;
277+ if ( findOptions . limit === undefined ) {
278+ const pkVal = findOptions . where && findOptions . where [ this . primaryKeyAttribute ] ;
279+ if ( ! findOptions . where || ! ( typeof pkVal === 'number' || typeof pkVal === 'string' || Buffer . isBuffer ( pkVal ) ) ) {
280+ findOptions . limit = 1 ;
281+ }
282+ }
283+ return this . findAll ( findOptions ) ;
284+ }
285+
286+ return Promise . resolve ( ) . then ( ( ) => {
287+ if ( [ null , undefined ] . indexOf ( id ) !== - 1 ) {
288+ return Promise . resolve ( null ) ;
289+ }
290+
291+ const loaders = options [ EXPECTED_OPTIONS_KEY ] . loaders ;
292+
293+ if ( keys . length === 1 && symbolKeys . length === 0 ) {
294+ // Simple case: only PK in where — use byPrimaryKey loader
295+ let loader = loaders [ this . name ] . byPrimaryKey ;
296+ if ( options . raw || options . paranoid === false ) {
297+ const cacheKey = getCacheKey ( this , this . primaryKeyAttribute , { raw : options . raw , paranoid : options . paranoid } ) ;
298+ loader = loaders . autogenerated . get ( cacheKey ) ;
299+ if ( ! loader ) {
300+ loader = createModelAttributeLoader ( this , this . primaryKeyAttribute , { raw : options . raw , paranoid : options . paranoid , logging : options . logging } ) ;
301+ loaders . autogenerated . set ( cacheKey , loader ) ;
302+ }
303+ }
304+ return loader . load ( id ) ;
305+ } else {
306+ // Extended case: PK + extra conditions — batch calls with identical extra conditions together
307+ const otherWhere = Object . assign ( { } , options . where ) ;
308+ delete otherWhere [ this . primaryKeyAttribute ] ;
309+
310+ const loaderOptions = { where : otherWhere , order : options . order , raw : options . raw , paranoid : options . paranoid } ;
311+ const cacheKey = getCacheKey ( this , this . primaryKeyAttribute , loaderOptions ) ;
312+ let loader = loaders . autogenerated . get ( cacheKey ) ;
313+ if ( ! loader ) {
314+ loader = loaderForFindOneWithConditions ( this , this . primaryKeyAttribute , otherWhere , {
315+ order : options . order ,
316+ raw : options . raw ,
317+ paranoid : options . paranoid ,
318+ logging : options . logging ,
319+ cache : true
320+ } ) ;
321+ loaders . autogenerated . set ( cacheKey , loader ) ;
322+ }
323+ return loader . load ( id ) ;
324+ }
325+ } ) . then ( rejectOnEmpty . bind ( null , options ) ) ;
326+ } ;
327+ } ) ;
207328}
208329
209330function shimBelongsTo ( target ) {
@@ -425,6 +546,7 @@ function activeClsTransaction() {
425546}
426547
427548export const EXPECTED_OPTIONS_KEY = 'dataloader_sequelize_context' ;
549+ export const BATCH_BY_ATTRIBUTE = 'dataloader_sequelize_batch_by_attribute' ;
428550export function createContext ( sequelize , options = { } ) {
429551 const loaders = { } ;
430552
0 commit comments