@autofleet/sheilta 2.6.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.cjs CHANGED
@@ -1,13 +1,15 @@
1
- var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`sequelize`);c=s(c);let l=require(`@autofleet/logger`);l=s(l);let u=require(`@autofleet/errors`);u=s(u);let d=require(`joi`);d=s(d);let f=require(`@autofleet/common-types`);f=s(f);let p=require(`node:crypto`);p=s(p);let m=require(`redis`);m=s(m);let h=require(`express`);h=s(h);let g=require(`zod`);g=s(g);let _=require(`node:events`);_=s(_);const v=[`eq`,`ne`,`gte`,`gt`,`lte`,`lt`,`not`,`in`,`notIn`,`is`,`like`,`iLike`,`notLike`,`between`,`and`,`or`,`overlap`,`contains`],y=`$`,b={$eq:`=`,$ne:`!=`,$gte:`>=`,$gt:`>`,$lte:`<=`,$lt:`<`,$not:`NOT`,$in:`IN`,$notIn:`NOT IN`,$is:`IS`,$like:`LIKE`,$iLike:`ILIKE`,$notLike:`NOT LIKE`,$and:`AND`,$or:`OR`},x=(e={Op:c.Op})=>{let{Op:t}=e;return Object.fromEntries(v.map(e=>[`${`$`+e}`,t[e]]))},S=`-`,ee=`.`,C=`$`,w=20,te=1,ne=100,re=1,ie=1,ae=e=>`\$${e}\$`,T=(e,t)=>e.includes(`.`)&&t.includes(e.split(`.`,1)[0]),E=(e,t)=>{let n=e;return e.includes(`-`)&&([,n]=n.split(`-`,2)),T(n,t)&&([n]=n.split(`.`,1)),n},D=e=>e.includes(`-`),O=e=>{throw new u.BadRequest([Error(e)])},oe=e=>e.split(`.`,2)[1],k=(e=5)=>Array.from({length:e},()=>`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`.charAt((0,p.randomInt)(52))).join(``),A=(e,t)=>Object.fromEntries(t.map(t=>[t,e[t]])),j={length:`length`};function M(e){return e.replace(/(?!^)[A-Z]/g,e=>`_${e.toLowerCase()}`)}function se(e){switch(e.action){case j.length:return[(0,c.literal)(`jsonb_array_length(${M(e.columnName)})`),e.alias];default:return e.action,[]}}function ce(e){return e.map(e=>se(e))}function le(e){return e.map(e=>{let t=M(e.columnName),n=`json_build_object(${e.keys.map(e=>`'${e}', ${t} -> '${e}'`).join(`, `)})`,r=e.alias||e.columnName;return[(0,c.literal)(n),r]})}function ue({select:e=[],computed:t=[]}={}){let n=le(e),r=ce(t);return[...n,...r]}const de=`id`,N=`DESC`,fe=`ASC`,P=`customFields.`,{CUSTOM_FIELDS_FILTER_SCOPE:F,CUSTOM_FIELDS_SORT_SCOPE:I}=f.customFields,pe=e=>[`string`,`number`].includes(typeof e)||Array.isArray(e)?e:Object.entries(e).map(([e,t])=>({operator:b[e],value:t})),me=(e,t={})=>{let{literalAttributes:n=[],DBFormatter:r=void 0}=t,[i,a]=e.reduce((e,t)=>{let[i,a=`ASC`]=Array.isArray(t)?t:[t],o=n?.find(e=>e.attribute===i);if(o){let t=r?r(`"${o.attribute}" ${a}`):`${o.attribute} ${a}`;e[1].push(o.literal),e[0].push([t])}else e[0].push(t);return e},[[],[]]);return[i,a]},L=e=>{let t={};return Object.entries(e).forEach(([e,n])=>{let r=k();if(t[r]=e.split(P,2)[1],Array.isArray(n))n.forEach(e=>{let n=k();t[n]=typeof e==`string`?e:e.value});else if(typeof n==`string`||typeof n==`number`){let e=k();t[e]=n}else if(n?.operator){let e=k();t[e]=n.value}}),t},he=e=>{let t={};return e.forEach(e=>{if(e.startsWith(P)){let n=k();t[n]=e.split(P,2)[1]}else if(e.substring(1).startsWith(P)){let n=k();t[n]=e.substring(1).split(P,2)[1]}}),t},ge=(e,t)=>({...he(e),...L(t)}),_e=({order:e,associationModels:t=[],replacementsMap:n={}})=>{let r=[],i=new Map;return e.forEach(e=>{if([e,e.substring(1)].some(e=>e.startsWith(P))){i.has(I)||i.set(I,{});let t=e.split(P,2)[1];i.get(I)[t]=D(e)?N:`ASC`;return}let n=[E(e,t)],a=D(e);T(a?e.split(`-`,2)[1]:e,t)&&n.push(oe(e)),a&&n.push(N),r.push(n)}),{formattedOrders:r,replacementsMap:n,orderScopes:Array.from(i.entries()).map(([e,t])=>t?{method:[e,{replacementsMap:n,scopeValue:t}]}:e)}},ve=e=>e||1,ye=e=>e||20,R=(e,t={})=>{let n=e.map(e=>{let n=t[typeof e==`string`?e:e.association||e.model];return{...typeof e!=`string`&&e,association:n,required:typeof e==`string`||e.required!==!1,...typeof e!=`string`&&e.include&&{include:R(e.include,n?.target?.associations)}}});return n=n.map(({model:e,...t})=>t),n},be=(e,t,n,r=[])=>{let i={},a={},o=new Map;return Object.entries(e).forEach(([e,n])=>{if(e.startsWith(P)){o.has(F)||o.set(F,{});let t=e.split(P,2)[1];o.get(F)[t]=pe(n);return}if(r.includes(e)){a[e]=n;return}let s=T(e,t)?ae(e):e;i[s]=n}),{formattedQuery:i,externalQueryValues:a,formattedScopes:Array.from(o.entries()).map(([e,t])=>t?{method:[e,{replacementsMap:n,scopeValue:t}]}:e)}},xe=(e,t,n)=>({$and:e.split(` `).map(e=>({$or:t.filter(e=>n[e].type.key===`STRING`).map(t=>({[t]:{$iLike:`%${e}%`}}))}))}),Se=({order:e=[],page:t=1,perPage:n=20,include:r=[],query:i={},attributes:a=null,searchTerm:o=null,jsonAttributes:s={}},c,l)=>{let u=ge(e,i),d=Object.keys(c?.associations||{}),{formattedOrders:f,orderScopes:p}=_e({order:[...e,`id`],associationModels:d,replacementsMap:u}),[m,h]=me(f,l),g=ue(s),_=[...h,...a??[],...g],v=a?.length?_:{include:_},y=R(r,c?.associations),b=ve(t),x=ye(n),S=be(i,d,u,l?.additionalAllowedAttributes),{formattedScopes:ee,externalQueryValues:C}=S,{formattedQuery:w}=S;if(o&&!l?.skipSearchTermFormat){let e=xe(o,a?.length?a:Object.keys(c.rawAttributes||{}),c.rawAttributes);w=!w||Object.keys(w).length===0?e:{$and:[w,e]}}return{query:w,order:m,page:b,perPage:x,include:y,scopes:[...ee,...p],...v&&{attributes:v},...Object.keys(C).length>0&&{externalQueryValues:C}}};var Ce=Se;const we=e=>v.includes(e.split(`$`,2)[1]),z=(e,t=[],n=[],r=[])=>{let i=e.startsWith(`$`)&&e.endsWith(`$`)?e.slice(1,-1):e;return[...t,...n].includes(i.includes(`.`)?i.split(`.`,1)[0]:i)||r.includes(i)},Te=(e,t,n,r={})=>{let i=D(e);i&&!e.startsWith(`-`)&&O(`- must be only at the beginning of the word`);let a=i?e.split(`-`,2)[1]:e,o=T(a,n),s=E(e,n),c=r?.literalAttributes?.map(e=>e.attribute)?.includes(a);!o&&s.includes(`.`)&&([s]=s.split(`.`,1)),t.includes(s)||o||c||O(`${e} is invalid. isLiteralAttribute: ${c}`)},Ee=(e,t)=>{t.includes(e)||O(`${e} is invalid`)},B=(e,t,n=[],r={})=>{e.forEach(e=>Te(e,t,n,r))},V=(e,t)=>{e.forEach(e=>Ee(e,t))},De=(e,t)=>{V([...e.select?.map(e=>e.columnName)??[],...e.computed?.map(e=>e.columnName)??[]],t)},Oe=(e,t)=>{let n=Array.isArray(e)?e:Object.keys(e);if(!n?.length)return;let r=n.find(e=>!t?.enrichmentAttributes?.includes(e));r&&O(`enrichment attribute ${r} is invalid`)},H=(e,t,n=[],r=[])=>{Object.entries(e).forEach(([e,i])=>{Array.isArray(i)?i[0]&&typeof i[0]==`object`&&i.map(e=>H(e,t,n,r)):we(e)||z(e,t,n,r)?i&&typeof i==`object`&&H(i,t,[],r):O(`invalid key: ${e}`)})},U=({page:e,perPage:t})=>{e<1&&O(`Page must be greater than 0`),(t>100||t<1)&&O(`PerPage must be between 1 to 100`)},ke=(e,t)=>{let n=Object.keys(t);e.forEach(e=>{z(e.model,n);let r=t[e.model]?.target;r||O(`model not found in associations`);let{rawAttributes:i}=r,a=Object.keys(i);e.where&&H(e.where,a),e.order&&B(e.order,a),e.attributes&&V(e.attributes,a),[null,void 0,!0,!1].includes(e.required)||O(`include.required must be a boolean`)})},W=({query:e={},order:t=[],attributes:n=[],include:r=[],page:i=1,perPage:a=20,enrichments:o=[],group:s=[],jsonAttributes:c={}},l,u={})=>{let d=Object.keys(l.rawAttributes),f=Object.keys(l?.associations||{});return!n||n.length===0?n=d:V(n,d),B(t,d,f,u),H(e,d,f,u.additionalAllowedAttributes),Oe(o,u),De(c,d),Array.isArray(s)||O(`group must be an array`),r.length&&typeof r==`object`?ke(r,l?.associations):r&&typeof r!=`object`&&O(`include must be an array`),U({page:i,perPage:a}),!0},{object:G,string:K,number:q,any:Ae,array:J,alternatives:je}=d.default.types(),Me=(0,l.default)(),Ne=G.keys({query:G,attributes:J.items(K),order:J.items(K),page:q,perPage:q,include:J.items(Ae),searchTerm:K,group:J.items(K),enrichments:je.try(J.items(K),G.pattern(K,{exclude:J.items(K)})),jsonAttributes:d.default.object({select:d.default.array().items(d.default.object({columnName:d.default.string().required(),keys:d.default.array().items(d.default.string().required()).required(),alias:d.default.string().optional()})).default([]),computed:d.default.array().items(d.default.object({columnName:d.default.string().required(),action:d.default.string().valid(...Object.values(j)).required(),alias:d.default.string().required()})).default([])}).default({})}),Y=(e,t,n={})=>{let{query:r,attributes:i,order:a,page:o,perPage:s,include:c,group:l,enrichments:d,jsonAttributes:f}=t,p=Ne.validate(t);if(p.error)throw new u.BadRequest([p.error]);W({query:r,attributes:i,order:a,page:o,perPage:s,include:c,enrichments:d,group:l,jsonAttributes:f},e,n)},Pe=(e,t={},n=`body`)=>(r,i,a)=>{try{Y(e,r[n],t),a()}catch(e){let{query:a,attributes:o,order:s}=r[n];(0,u.handleError)(e,i,{logger:t.logger??Me,message:`error in query middleware`,payload:{error:e,query:a,attributes:o,order:s}})}},X=(e,t,n={})=>{let{order:r,page:i,perPage:a,include:o,query:s,attributes:c,searchTerm:l,jsonAttributes:u}=t,{query:d,externalQueryValues:f,order:p,page:m,perPage:h,include:g,scopes:_,attributes:v}=Ce({query:s,order:r,page:i,perPage:a,include:o,attributes:c,searchTerm:l,jsonAttributes:u},e,n);t.query=d,t.externalQueryValues=f,t.order=p,t.attributes=v,t.page=m,t.perPage=h,t.include=g,t.scopes=_,n.includeRawPayload&&(t.rawPayload={order:r,page:i,perPage:a,include:o,query:s,attributes:c,searchTerm:l})},Fe=(e,t={},n=`body`)=>(r,i,a)=>{X(e,r[n],t),a()},Ie=({model:e,logger:t,validationOptions:n,formatOptions:r,modelName:i=e.constructor?.name,additionalScopes:a=[],modifyQueryValues:o,onRowsRetrieved:s})=>async(c,l)=>{try{Y(e,c.body,{...n,logger:t})}catch(e){(0,u.handleError)(e,l,{logger:t,message:`error in query endpoint`,payload:A(c.body,[`query`,`order`,`attributes`])});return}try{X(e,c.body,r);let n=Object.assign(A(c.body,[`query`,`externalQueryValues`,`order`,`attributes`,`page`,`perPage`,`include`,`scopes`,`enrichments`]),{distinct:!0});t.info(`querying ${i}`,{queryValues:n});let u=o?.(n)??n,{scopes:d=[],query:f,perPage:p,page:m,enrichments:h,externalQueryValues:g,..._}=u,v=await e.scope([...a,...d]).findAndCountAll({where:f,limit:p,offset:(m-1)*p,..._});if(!v.rows.length||!s){l.json(v);return}let y=await s(v,u);l.json(y)}catch(e){(0,u.handleError)(new u.UnexpectedError(e),l,{logger:t,message:`Error while querying ${i}`,payload:{query:c.body}})}};var Z=class e extends Error{constructor(e,t=!1){super(e),this.name=`BulkerError`,this.retryable=t,Object.setPrototypeOf(this,new.target.prototype)}static isBulkerError(t){return t instanceof e}static wrap(t,n,r=!1){return t instanceof e?new e(n??t.message,r||t.retryable):new e(n?`${n}: ${t instanceof Error?t.message:String(t)}`:String(t),r)}static retryable(t){return new e(t,!0)}static nonRetryable(t){return new e(t,!1)}};const Le=e=>e.length===0?`No keys provided`:e.length===1?`Key "${e[0]}" is required`:`Exactly one of [${e.join(`, `)}] must be provided`;function Re(e,t){return g.z.object(e).refine(e=>t.filter(t=>e[t]!==void 0&&e[t]!==null).length===1,{message:Le(t)})}const Q=g.z.string().uuid();function ze(e){return Re(Object.fromEntries(e.map(e=>[e,g.z.union([Q,g.z.array(Q)]).optional()])),e)}const Be=[`businessModelId`,`fleetId`,`demandSourceId`,`contextId`,`userId`,`businessAccountId`,`activeBusinessModelId`];var Ve=class{constructor(e,t){if(this.bulker=e,this.opts=t,this.rabbitQueueName=null,this.rabbit=null,this.bulkHandler=async(e,t,n)=>{try{let{query:n,payload:r,preview:i}=e.body??{},a=r;if(r&&this.payloadSchema)try{a=this.payloadSchema.parse(r)}catch(e){if(e instanceof g.z.ZodError)return t.status(400).json({error:`invalid_payload`,details:e.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`).join(`, `)});throw e}try{Y(this.model,n,{logger:this.bulker.logger})}catch(e){return t.status(400).json({error:`invalid_query`,details:e.message||e})}let o=this.identityScopeSchema.safeParse(n?.query||{});if(!o.success)return t.status(400).json({error:`invalid_identity_scope`,details:o.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`).join(`, `)});X(this.model,n,{includeRawPayload:!0});let{query:s,...c}=Object.assign(A(n,[`query`,`externalQueryValues`,`order`,`include`,`scopes`,`enrichments`]),{distinct:!0}),l=[];if(this.opts.additionalIdsHook){let{rawPayload:e}=n;try{l=await this.opts.additionalIdsHook({rawPayload:e,payload:a}),this.bulker.logger.info(`additionalIdsHook returned ${l.length} IDs for action ${this.action}`)}catch(e){return this.bulker.logger.error(`Error in additionalIdsHook for action ${this.action}: ${e.message||e}`,{err:e}),t.status(500).json({error:`additional_ids_hook_error`,details:e.message||e})}}let u=[];s&&typeof s==`object`&&Object.keys(s).length>0&&u.push(s),Array.isArray(l)&&l.length>0&&u.push({id:{$in:l}});let d;if(u.length===0)return t.status(400).json({error:`no_query`,details:`No valid query provided to select records`});if(u.length===1){let[e]=u;d=e}else d={$or:u};this.bulker.logger.info(`Constructed final where clause for action ${this.action}`,{where:d});let f={where:d,...c},m=await this.model.scope(this.modelScopes).count({...f,col:this.idField});if(i)return t.json({estimatedCount:m});let h=(0,p.randomUUID)();return await this.bulker.jobManager.initJob(h,{status:`queued`,total:m,action:this.action}),this.bulker.emitEvent(`job:created`,{jobId:h,action:this.action,total:m}),setImmediate(()=>{this.rabbitScanAndEnqueue(h,f,a).catch(e=>this.bulker.logger.error(e))}),t.status(202).json({jobId:h,estimatedCount:m})}catch(e){return this.bulker.logger.error(`Error in bulkHandler for action ${this.action}: ${e.message||e}`,{err:e}),n(e)}},this.action=t.action,this.model=t.model,this.modelScopes=t.modelScopes??[],this.consumer=t.consumer,this.rabbit=e.rabbit,this.rabbitQueueName=t.rabbitQueueName??`bulk-${this.action}-queue`,this.consumerOptions={enableRabbitTrace:!0,...this.opts.consumerOptions},this.payloadSchema=t.payloadSchema,!/^[a-zA-Z0-9-_]+$/.test(this.action))throw Error(`BulkRoute action must be alphanumeric`);if(!this.model||typeof this.model.findAll!=`function`||typeof this.model.count!=`function`)throw Error(`BulkRoute model must be a valid Sequelize model`);if(typeof this.consumer!=`function`)throw Error(`BulkRoute consumer must be a function`);if(this.payloadSchema&&!(this.payloadSchema instanceof g.z.ZodType))throw Error(`BulkRoute payloadSchema must be a Zod schema`);if(this.modelScopes&&!Array.isArray(this.modelScopes))throw Error(`BulkRoute scopes must be an array of strings`);if(this.opts.additionalIdsHook&&typeof this.opts.additionalIdsHook!=`function`)throw Error(`BulkRoute additionalIdsHook must be a function`);this.idField=t.idField??e.defaults.idField,this.pageSize=t.pageSize??e.defaults.pageSize;let n=this.model.rawAttributes||{},r=(t.identityScopes&&t.identityScopes.length>0?t.identityScopes:Be).filter(e=>!!n[e]);r.length===0&&this.bulker.logger.warn(`BulkRoute for action ${this.action} has no valid identityScopes configured - all records will be accessible`),this.bulker.logger.info(`BulkRoute for action ${this.action} using idField ${this.idField}, pageSize ${this.pageSize}, identityScopes: ${r.join(`, `)}`),this.identityScopeSchema=ze(r),this.startRabbitWorker().catch(e=>{this.bulker.logger.error(`Failed to start RabbitMQ worker for queue ${this.rabbitQueueName}: ${e.message||e}`)})}async getUserJobs(e,t=20){return this.bulker.jobManager.getUserJobs(e,t)}async rabbitScanAndEnqueue(e,t,n){if(!this.rabbit)throw Error(`RabbitMQ not configured in Bulker`);let r=Date.now().toString();if(this.bulker.getUserId){let t=this.bulker.getUserId();t&&await this.bulker.jobManager.addUserJob(t,e)}await this.bulker.jobManager.setJobFields(e,{status:`running`,startTime:r}),this.bulker.emitEvent(`job:started`,{jobId:e,action:this.action});let i=null,a=0,o=0;for(;;){let r=await this.bulker.jobManager.getJobField(e,`status`);if(!r||r===`canceled`){this.bulker.logger.info(`Job ${e} was canceled, stopping scan and enqueue`);break}let s=i?{[c.Op.and]:[t.where,{[this.idField]:{[c.Op.gt]:i}}]}:t.where,l=await this.model.scope(this.modelScopes).findAll({where:s,...A(t,[`include`,`order`,`scopes`]),attributes:[this.idField],order:[[this.idField,`ASC`]],limit:this.pageSize,raw:!0,subQuery:!1});if(l.length===0)break;let u=l.map(t=>({jobId:e,id:t[this.idField],payload:n}));this.bulker.logger.info(`Enqueuing ${u.length} messages to RabbitMQ queue ${this.rabbitQueueName}`);let d=(await Promise.allSettled(u.map(e=>this.rabbit.sendToQueue(this.rabbitQueueName,e)))).filter(e=>e.status===`rejected`);if(d.length>0)throw this.bulker.logger.error(`Failed to enqueue ${d.length} messages to RabbitMQ`,{rejected:d}),Error(`Failed to enqueue ${d.length} messages to RabbitMQ`);a+=l.length,await this.bulker.jobManager.incrJobField(e,`queued`,l.length),i=l[l.length-1][this.idField],o+=1,this.bulker.emitEvent(`scan:page`,{jobId:e,action:this.action,pageNumber:o,itemsInPage:l.length});let f=await this.bulker.jobManager.getJob(e);f&&this.bulker.emitEvent(`job:queued`,{jobId:e,action:this.action,queued:a,total:f.total})}a===0&&await this.bulker.jobManager.completeEmptyJob(e)}async startRabbitWorker(){return this.bulker.logger.info(`Starting RabbitMQ consumer for queue ${this.rabbitQueueName}`),this.bulker.emitEvent(`worker:started`,{action:this.action,queueName:this.rabbitQueueName}),this.rabbit?.consume(this.rabbitQueueName,async(e,t,n)=>{if(!e)return;let{jobId:r,id:i,payload:a}=e.content,o=!1,s=async()=>{o||(o=!0,await Promise.all([this.bulker.jobManager.ack(r),t()]),this.bulker.emitEvent(`item:processed`,{jobId:r,action:this.action,id:i}))},c=async(e,t)=>{if(o)return;o=!0;let s=t;s===void 0&&(s=Z.isBulkerError(e)?e.retryable:!1),await Promise.all([this.bulker.jobManager.nack(r,e?e.message||String(e):`nacked`,{id:i,payload:a}),n(void 0,{skipRetry:!s})]),s?this.bulker.emitEvent(`item:retrying`,{jobId:r,action:this.action,id:i}):this.bulker.emitEvent(`item:failed`,{jobId:r,action:this.action,id:i,error:e?e.message||String(e):`nacked`})},l=await this.bulker.jobManager.getJobField(r,`status`);if(!l||l===`canceled`){await this.bulker.jobManager.incrJobField(r,`processed`,1);return}try{this.bulker.logger.info(`Processing job ${r} action ${this.action} id ${i}`),this.bulker.emitEvent(`item:processing`,{jobId:r,action:this.action,id:i}),await this.consumer({jobId:r,id:i,payload:a},s,c),await s()}catch(e){this.bulker.logger.error(`Error processing job ${r} action ${this.action} id ${i}: ${e.message||e}`,{err:e}),this.bulker.emitEvent(`worker:error`,{action:this.action,error:e.message||String(e)}),await c(e,!1)}},this.consumerOptions)}},He=class{constructor(e,t,n=`/bulk-actions`){this.routes=[],this.actionsToRouteMap=new Map,this.bulkHandler=async(e,t,n)=>{try{let{action:r}=e.body??{};if(!r)return t.status(400).json({error:`missing_action`,message:`Request body must include an "action" field`});let i=this.actionsToRouteMap.get(r);return i?i.bulkHandler(e,t,n):t.status(404).json({error:`unknown_action`,message:`Action "${r}" is not registered`,availableActions:Array.from(this.actionsToRouteMap.keys())})}catch(e){return this.bulker.logger.error(`Error in BulkRouter bulkHandler: ${e.message||e}`,{err:e}),n(e),null}},this.getJobHandler=async(e,t,n)=>{try{if(!e.params.id)return t.status(400).json({error:`missing_id`});let n=await this.bulker.jobManager.getJob(e.params.id);return n?t.json(n):t.status(404).json({error:`not_found`})}catch(e){return n(e)}},this.cancelJobHandler=async(e,t,n)=>{try{return e.params.id?await this.bulker.jobManager.cancelJob(e.params.id)?(this.bulker.logger.info(`Job ${e.params.id} cancel requested`),t.json({ok:!0})):t.status(404).json({error:`not_found`}):t.status(400).json({error:`missing_id`})}catch(e){return n(e)}},this.getMyJobsHandler=async(e,t,n)=>{try{if(!this.bulker.getUserId)return t.status(400).json({error:`user_id_function_not_configured`,message:`Bulker instance does not have a getUserId function configured`});let e=this.bulker.getUserId();if(!e)return t.json([]);let n=(await this.bulker.jobManager.getUserJobs(e)).filter(e=>e!==null);return n.sort((e,t)=>{let n=e.createdAt?new Date(e.createdAt).getTime():0;return(t.createdAt?new Date(t.createdAt).getTime():0)-n}),t.json(n)}catch(e){return n(e)}},this.bulker=e,this.router=t??(0,h.Router)(),this.staticRoute=n,this.registerStaticRoutes()}registerStaticRoutes(){this.router.post(this.staticRoute,this.bulkHandler),this.router.get(`/jobs/:id`,this.getJobHandler),this.router.get(`/jobs`,this.getMyJobsHandler),this.router.post(`/jobs/:id/cancel`,this.cancelJobHandler)}addAction(e,t){let n=new Ve(this.bulker,{action:e,...t});if(this.bulker.logger.info(`Registering action handler: ${e}`),this.actionsToRouteMap.has(e))throw Error(`Action "${e}" is already registered`);return this.actionsToRouteMap.set(e,n),this.routes.push(n),n}getRouter(){return this.router}};const Ue=`
1
+ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));let c=require(`sequelize`);c=s(c);let l=require(`@autofleet/logger`);l=s(l);let u=require(`@autofleet/errors`);u=s(u);let d=require(`joi`);d=s(d);let f=require(`@autofleet/common-types`);f=s(f);let p=require(`node:crypto`);p=s(p);let m=require(`redis`);m=s(m);let h=require(`express`);h=s(h);let g=require(`zod`);g=s(g);let _=require(`node:events`);_=s(_);const v=[`eq`,`ne`,`gte`,`gt`,`lte`,`lt`,`not`,`in`,`notIn`,`is`,`like`,`iLike`,`notLike`,`between`,`and`,`or`,`overlap`,`contains`],y=`$`,b={$eq:`=`,$ne:`!=`,$gte:`>=`,$gt:`>`,$lte:`<=`,$lt:`<`,$not:`NOT`,$in:`IN`,$notIn:`NOT IN`,$is:`IS`,$like:`LIKE`,$iLike:`ILIKE`,$notLike:`NOT LIKE`,$and:`AND`,$or:`OR`},x=(e={Op:c.Op})=>{let{Op:t}=e;return Object.fromEntries(v.map(e=>[`${`$`+e}`,t[e]]))},S=`-`,ee=`.`,C=`$`,w=20,te=1,ne=100,re=1,ie=1,ae=e=>`\$${e}\$`,T=(e,t)=>e.includes(`.`)&&t.includes(e.split(`.`,1)[0]),E=(e,t)=>{let n=e;return e.includes(`-`)&&([,n]=n.split(`-`,2)),T(n,t)&&([n]=n.split(`.`,1)),n},D=e=>e.includes(`-`),O=e=>{throw new u.BadRequest([Error(e)])},oe=e=>e.split(`.`,2)[1],k=(e=5)=>Array.from({length:e},()=>`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz`.charAt((0,p.randomInt)(52))).join(``),A=(e,t)=>Object.fromEntries(t.map(t=>[t,e[t]])),j={length:`length`};function M(e){return e.replace(/(?!^)[A-Z]/g,e=>`_${e.toLowerCase()}`)}function se(e){switch(e.action){case j.length:return[(0,c.literal)(`jsonb_array_length(${M(e.columnName)})`),e.alias];default:return e.action,[]}}function ce(e){return e.map(e=>se(e))}function le(e){return e.map(e=>{let t=M(e.columnName),n=`json_build_object(${e.keys.map(e=>`'${e}', ${t} -> '${e}'`).join(`, `)})`,r=e.alias||e.columnName;return[(0,c.literal)(n),r]})}function ue({select:e=[],computed:t=[]}={}){let n=le(e),r=ce(t);return[...n,...r]}const de=`id`,N=`DESC`,fe=`ASC`,P=`customFields.`,{CUSTOM_FIELDS_FILTER_SCOPE:F,CUSTOM_FIELDS_SORT_SCOPE:I}=f.customFields,pe=e=>[`string`,`number`].includes(typeof e)||Array.isArray(e)?e:Object.entries(e).map(([e,t])=>({operator:b[e],value:t})),me=(e,t={})=>{let{literalAttributes:n=[],DBFormatter:r=void 0}=t,[i,a]=e.reduce((e,t)=>{let[i,a=`ASC`]=Array.isArray(t)?t:[t],o=n?.find(e=>e.attribute===i);if(o){let t=r?r(`"${o.attribute}" ${a}`):`${o.attribute} ${a}`;e[1].push(o.literal),e[0].push([t])}else e[0].push(t);return e},[[],[]]);return[i,a]},L=e=>{let t={};return Object.entries(e).forEach(([e,n])=>{let r=k();if(t[r]=e.split(P,2)[1],Array.isArray(n))n.forEach(e=>{let n=k();t[n]=typeof e==`string`?e:e.value});else if(typeof n==`string`||typeof n==`number`){let e=k();t[e]=n}else if(n?.operator){let e=k();t[e]=n.value}}),t},he=e=>{let t={};return e.forEach(e=>{if(e.startsWith(P)){let n=k();t[n]=e.split(P,2)[1]}else if(e.substring(1).startsWith(P)){let n=k();t[n]=e.substring(1).split(P,2)[1]}}),t},ge=(e,t)=>({...he(e),...L(t)}),_e=({order:e,associationModels:t=[],replacementsMap:n={}})=>{let r=[],i=new Map;return e.forEach(e=>{if([e,e.substring(1)].some(e=>e.startsWith(P))){i.has(I)||i.set(I,{});let t=e.split(P,2)[1];i.get(I)[t]=D(e)?N:`ASC`;return}let n=[E(e,t)],a=D(e);T(a?e.split(`-`,2)[1]:e,t)&&n.push(oe(e)),a&&n.push(N),r.push(n)}),{formattedOrders:r,replacementsMap:n,orderScopes:Array.from(i.entries()).map(([e,t])=>t?{method:[e,{replacementsMap:n,scopeValue:t}]}:e)}},ve=e=>e||1,ye=e=>e||20,R=(e,t={})=>{let n=e.map(e=>{let n=t[typeof e==`string`?e:e.association||e.model];return{...typeof e!=`string`&&e,association:n,required:typeof e==`string`||e.required!==!1,...typeof e!=`string`&&e.include&&{include:R(e.include,n?.target?.associations)}}});return n=n.map(({model:e,...t})=>t),n},be=(e,t,n,r=[])=>{let i={},a={},o=new Map;return Object.entries(e).forEach(([e,n])=>{if(e.startsWith(P)){o.has(F)||o.set(F,{});let t=e.split(P,2)[1];o.get(F)[t]=pe(n);return}if(r.includes(e)){a[e]=n;return}let s=T(e,t)?ae(e):e;i[s]=n}),{formattedQuery:i,externalQueryValues:a,formattedScopes:Array.from(o.entries()).map(([e,t])=>t?{method:[e,{replacementsMap:n,scopeValue:t}]}:e)}},xe=(e,t,n)=>({$and:e.split(` `).map(e=>({$or:t.filter(e=>n[e].type.key===`STRING`).map(t=>({[t]:{$iLike:`%${e}%`}}))}))}),Se=({order:e=[],page:t=1,perPage:n=20,include:r=[],query:i={},attributes:a=null,searchTerm:o=null,jsonAttributes:s={}},c,l)=>{let u=ge(e,i),d=Object.keys(c?.associations||{}),{formattedOrders:f,orderScopes:p}=_e({order:[...e,`id`],associationModels:d,replacementsMap:u}),[m,h]=me(f,l),g=ue(s),_=[...h,...a??[],...g],v=a?.length?_:{include:_},y=R(r,c?.associations),b=ve(t),x=ye(n),S=be(i,d,u,l?.additionalAllowedAttributes),{formattedScopes:ee,externalQueryValues:C}=S,{formattedQuery:w}=S;if(o&&!l?.skipSearchTermFormat){let e=xe(o,a?.length?a:Object.keys(c.rawAttributes||{}),c.rawAttributes);w=!w||Object.keys(w).length===0?e:{$and:[w,e]}}return{query:w,order:m,page:b,perPage:x,include:y,scopes:[...ee,...p],...v&&{attributes:v},...Object.keys(C).length>0&&{externalQueryValues:C}}};var Ce=Se;const we=e=>v.includes(e.split(`$`,2)[1]),z=(e,t=[],n=[],r=[])=>{let i=e.startsWith(`$`)&&e.endsWith(`$`)?e.slice(1,-1):e;return[...t,...n].includes(i.includes(`.`)?i.split(`.`,1)[0]:i)||r.includes(i)},Te=(e,t,n,r={})=>{let i=D(e);i&&!e.startsWith(`-`)&&O(`- must be only at the beginning of the word`);let a=i?e.split(`-`,2)[1]:e,o=T(a,n),s=E(e,n),c=r?.literalAttributes?.map(e=>e.attribute)?.includes(a);!o&&s.includes(`.`)&&([s]=s.split(`.`,1)),t.includes(s)||o||c||O(`${e} is invalid. isLiteralAttribute: ${c}`)},Ee=(e,t)=>{t.includes(e)||O(`${e} is invalid`)},B=(e,t,n=[],r={})=>{e.forEach(e=>Te(e,t,n,r))},V=(e,t)=>{e.forEach(e=>Ee(e,t))},De=(e,t)=>{V([...e.select?.map(e=>e.columnName)??[],...e.computed?.map(e=>e.columnName)??[]],t)},Oe=(e,t)=>{let n=Array.isArray(e)?e:Object.keys(e);if(!n?.length)return;let r=n.find(e=>!t?.enrichmentAttributes?.includes(e));r&&O(`enrichment attribute ${r} is invalid`)},H=(e,t,n=[],r=[])=>{Object.entries(e).forEach(([e,i])=>{Array.isArray(i)?i[0]&&typeof i[0]==`object`&&i.map(e=>H(e,t,n,r)):we(e)||z(e,t,n,r)?i&&typeof i==`object`&&H(i,t,[],r):O(`invalid key: ${e}`)})},U=({page:e,perPage:t})=>{e<1&&O(`Page must be greater than 0`),(t>100||t<1)&&O(`PerPage must be between 1 to 100`)},ke=(e,t)=>{let n=Object.keys(t);e.forEach(e=>{z(e.model,n);let r=t[e.model]?.target;r||O(`model not found in associations`);let{rawAttributes:i}=r,a=Object.keys(i);e.where&&H(e.where,a),e.order&&B(e.order,a),e.attributes&&V(e.attributes,a),[null,void 0,!0,!1].includes(e.required)||O(`include.required must be a boolean`)})},W=({query:e={},order:t=[],attributes:n=[],include:r=[],page:i=1,perPage:a=20,enrichments:o=[],group:s=[],jsonAttributes:c={}},l,u={})=>{let d=Object.keys(l.rawAttributes),f=Object.keys(l?.associations||{});return!n||n.length===0?n=d:V(n,d),B(t,d,f,u),H(e,d,f,u.additionalAllowedAttributes),Oe(o,u),De(c,d),Array.isArray(s)||O(`group must be an array`),r.length&&typeof r==`object`?ke(r,l?.associations):r&&typeof r!=`object`&&O(`include must be an array`),U({page:i,perPage:a}),!0},{object:G,string:K,number:q,any:Ae,array:J,alternatives:je}=d.default.types(),Me=(0,l.default)(),Ne=G.keys({query:G,attributes:J.items(K),order:J.items(K),page:q,perPage:q,include:J.items(Ae),searchTerm:K,group:J.items(K),enrichments:je.try(J.items(K),G.pattern(K,{exclude:J.items(K)})),jsonAttributes:d.default.object({select:d.default.array().items(d.default.object({columnName:d.default.string().required(),keys:d.default.array().items(d.default.string().required()).required(),alias:d.default.string().optional()})).default([]),computed:d.default.array().items(d.default.object({columnName:d.default.string().required(),action:d.default.string().valid(...Object.values(j)).required(),alias:d.default.string().required()})).default([])}).default({})}),Y=(e,t,n={})=>{let{query:r,attributes:i,order:a,page:o,perPage:s,include:c,group:l,enrichments:d,jsonAttributes:f}=t,p=Ne.validate(t);if(p.error)throw new u.BadRequest([p.error]);W({query:r,attributes:i,order:a,page:o,perPage:s,include:c,enrichments:d,group:l,jsonAttributes:f},e,n)},Pe=(e,t={},n=`body`)=>(r,i,a)=>{try{Y(e,r[n],t),a()}catch(e){let{query:a,attributes:o,order:s}=r[n];(0,u.handleError)(e,i,{logger:t.logger??Me,message:`error in query middleware`,payload:{error:e,query:a,attributes:o,order:s}})}},X=(e,t,n={})=>{let{order:r,page:i,perPage:a,include:o,query:s,attributes:c,searchTerm:l,jsonAttributes:u}=t,{query:d,externalQueryValues:f,order:p,page:m,perPage:h,include:g,scopes:_,attributes:v}=Ce({query:s,order:r,page:i,perPage:a,include:o,attributes:c,searchTerm:l,jsonAttributes:u},e,n);t.query=d,t.externalQueryValues=f,t.order=p,t.attributes=v,t.page=m,t.perPage=h,t.include=g,t.scopes=_,n.includeRawPayload&&(t.rawPayload={order:r,page:i,perPage:a,include:o,query:s,attributes:c,searchTerm:l})},Fe=(e,t={},n=`body`)=>(r,i,a)=>{X(e,r[n],t),a()},Ie=({model:e,logger:t,validationOptions:n,formatOptions:r,modelName:i=e.constructor?.name,additionalScopes:a=[],modifyQueryValues:o,onRowsRetrieved:s})=>async(c,l)=>{try{Y(e,c.body,{...n,logger:t})}catch(e){(0,u.handleError)(e,l,{logger:t,message:`error in query endpoint`,payload:A(c.body,[`query`,`order`,`attributes`])});return}try{X(e,c.body,r);let n=Object.assign(A(c.body,[`query`,`externalQueryValues`,`order`,`attributes`,`page`,`perPage`,`include`,`scopes`,`enrichments`]),{distinct:!0});t.info(`querying ${i}`,{queryValues:n});let u=o?.(n)??n,{scopes:d=[],query:f,perPage:p,page:m,enrichments:h,externalQueryValues:g,..._}=u,v=await e.scope([...a,...d]).findAndCountAll({where:f,limit:p,offset:(m-1)*p,..._});if(!v.rows.length||!s){l.json(v);return}let y=await s(v,u);l.json(y)}catch(e){(0,u.handleError)(new u.UnexpectedError(e),l,{logger:t,message:`Error while querying ${i}`,payload:{query:c.body}})}};var Z=class e extends Error{constructor(e,t=!1){super(e),this.name=`BulkerError`,this.retryable=t,Object.setPrototypeOf(this,new.target.prototype)}static isBulkerError(t){return t instanceof e}static wrap(t,n,r=!1){return t instanceof e?new e(n??t.message,r||t.retryable):new e(n?`${n}: ${t instanceof Error?t.message:String(t)}`:String(t),r)}static retryable(t){return new e(t,!0)}static nonRetryable(t){return new e(t,!1)}};const Le=e=>e.length===0?`No keys provided`:e.length===1?`Key "${e[0]}" is required`:`Exactly one of [${e.join(`, `)}] must be provided`;function Re(e,t){return g.z.object(e).refine(e=>t.filter(t=>e[t]!==void 0&&e[t]!==null).length===1,{message:Le(t)})}const Q=g.z.string().uuid();function ze(e){return Re(Object.fromEntries(e.map(e=>[e,g.z.union([Q,g.z.array(Q)]).optional()])),e)}const Be=[`businessModelId`,`fleetId`,`demandSourceId`,`contextId`,`userId`,`businessAccountId`,`activeBusinessModelId`];var Ve=class{constructor(e,t){if(this.bulker=e,this.opts=t,this.rabbitQueueName=null,this.rabbit=null,this.bulkHandler=async(e,t,n)=>{try{let{query:n,payload:r,preview:i}=e.body??{},a=r;if(r&&this.payloadSchema)try{a=this.payloadSchema.parse(r)}catch(e){if(e instanceof g.z.ZodError)return t.status(400).json({error:`invalid_payload`,details:e.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`).join(`, `)});throw e}try{Y(this.model,n,{logger:this.bulker.logger})}catch(e){return t.status(400).json({error:`invalid_query`,details:e.message||e})}let o=this.identityScopeSchema.safeParse(n?.query||{});if(!o.success)return t.status(400).json({error:`invalid_identity_scope`,details:o.error.issues.map(e=>`${e.path.join(`.`)}: ${e.message}`).join(`, `)});X(this.model,n,{includeRawPayload:!0});let{query:s,...c}=Object.assign(A(n,[`query`,`externalQueryValues`,`order`,`include`,`scopes`,`enrichments`]),{distinct:!0}),l=[];if(this.opts.additionalIdsHook){let{rawPayload:e}=n;try{l=await this.opts.additionalIdsHook({rawPayload:e,payload:a}),this.bulker.logger.info(`additionalIdsHook returned ${l.length} IDs for action ${this.action}`)}catch(e){return this.bulker.logger.error(`Error in additionalIdsHook for action ${this.action}: ${e.message||e}`,{err:e}),t.status(500).json({error:`additional_ids_hook_error`,details:e.message||e})}}let u=[];s&&typeof s==`object`&&Object.keys(s).length>0&&u.push(s),Array.isArray(l)&&l.length>0&&u.push({id:{$in:l}});let d;if(u.length===0)return t.status(400).json({error:`no_query`,details:`No valid query provided to select records`});if(u.length===1){let[e]=u;d=e}else d={$or:u};this.bulker.logger.info(`Constructed final where clause for action ${this.action}`,{where:d});let f={where:d,...c},m=await this.model.scope(this.modelScopes).count({...f,col:this.idField});if(i)return t.json({estimatedCount:m});let h=(0,p.randomUUID)();return await this.bulker.jobManager.initJob(h,{status:`queued`,total:m,action:this.action}),this.bulker.emitEvent(`job:created`,{jobId:h,action:this.action,total:m}),setImmediate(()=>{this.rabbitScanAndEnqueue(h,f,a).catch(e=>this.bulker.logger.error(e))}),t.status(202).json({jobId:h,estimatedCount:m})}catch(e){return this.bulker.logger.error(`Error in bulkHandler for action ${this.action}: ${e.message||e}`,{err:e}),n(e)}},this.action=t.action,this.model=t.model,this.modelScopes=t.modelScopes??[],this.consumer=t.consumer,this.rabbit=e.rabbit,this.rabbitQueueName=t.rabbitQueueName??`bulk-${this.action}-queue`,this.consumerOptions={enableRabbitTrace:!0,...this.opts.consumerOptions},this.payloadSchema=t.payloadSchema,!/^[a-zA-Z0-9-_]+$/.test(this.action))throw Error(`BulkRoute action must be alphanumeric`);if(!this.model||typeof this.model.findAll!=`function`||typeof this.model.count!=`function`)throw Error(`BulkRoute model must be a valid Sequelize model`);if(typeof this.consumer!=`function`)throw Error(`BulkRoute consumer must be a function`);if(this.payloadSchema&&!(this.payloadSchema instanceof g.z.ZodType))throw Error(`BulkRoute payloadSchema must be a Zod schema`);if(this.modelScopes&&!Array.isArray(this.modelScopes))throw Error(`BulkRoute scopes must be an array of strings`);if(this.opts.additionalIdsHook&&typeof this.opts.additionalIdsHook!=`function`)throw Error(`BulkRoute additionalIdsHook must be a function`);this.idField=t.idField??e.defaults.idField,this.pageSize=t.pageSize??e.defaults.pageSize,this.consumerBatchSize=t.consumerBatchSize??e.defaults.consumerBatchSize;let n=this.model.rawAttributes||{},r=(t.identityScopes&&t.identityScopes.length>0?t.identityScopes:Be).filter(e=>!!n[e]);r.length===0&&this.bulker.logger.warn(`BulkRoute for action ${this.action} has no valid identityScopes configured - all records will be accessible`),this.bulker.logger.info(`BulkRoute for action ${this.action} using idField ${this.idField}, pageSize ${this.pageSize},
2
+ consumerBatchSize ${this.consumerBatchSize}, identityScopes: ${r.join(`, `)}`),this.identityScopeSchema=ze(r),this.startRabbitWorker().catch(e=>{this.bulker.logger.error(`Failed to start RabbitMQ worker for queue ${this.rabbitQueueName}: ${e.message||e}`)})}async getUserJobs(e,t=20){return this.bulker.jobManager.getUserJobs(e,t)}batchIds(e){let t=[];for(let n=0;n<e.length;n+=this.consumerBatchSize)t.push(e.slice(n,n+this.consumerBatchSize));return t}async rabbitScanAndEnqueue(e,t,n){if(!this.rabbit)throw Error(`RabbitMQ not configured in Bulker`);let r=Date.now().toString();if(this.bulker.getUserId){let t=this.bulker.getUserId();t&&await this.bulker.jobManager.addUserJob(t,e)}await this.bulker.jobManager.setJobFields(e,{status:`running`,startTime:r}),this.bulker.emitEvent(`job:started`,{jobId:e,action:this.action});let i=null,a=0,o=0;for(;;){let r=await this.bulker.jobManager.getJobField(e,`status`);if(!r||r===`canceled`){this.bulker.logger.info(`Job ${e} was canceled, stopping scan and enqueue`);break}let s=i?{[c.Op.and]:[t.where,{[this.idField]:{[c.Op.gt]:i}}]}:t.where,l=await this.model.scope(this.modelScopes).findAll({where:s,...A(t,[`include`,`order`,`scopes`]),attributes:[this.idField],order:[[this.idField,`ASC`]],limit:this.pageSize,raw:!0,subQuery:!1});if(l.length===0)break;let u,d=l.map(e=>e[this.idField]);this.consumerBatchSize===1&&d.length===1&&(u=d[0]);let f=this.batchIds(d).map(t=>({jobId:e,ids:t,payload:n,id:u}));this.bulker.logger.info(`Enqueuing ${f.length} batched messages (${d.length} total IDs) to RabbitMQ queue ${this.rabbitQueueName}`);let p=(await Promise.allSettled(f.map(e=>this.rabbit.sendToQueue(this.rabbitQueueName,e)))).filter(e=>e.status===`rejected`);if(p.length>0)throw this.bulker.logger.error(`Failed to enqueue ${p.length} messages to RabbitMQ`,{rejected:p}),Error(`Failed to enqueue ${p.length} messages to RabbitMQ`);a+=l.length,await this.bulker.jobManager.incrJobField(e,`queued`,l.length),i=l[l.length-1][this.idField],o+=1,this.bulker.emitEvent(`scan:page`,{jobId:e,action:this.action,pageNumber:o,itemsInPage:l.length});let m=await this.bulker.jobManager.getJob(e);m&&this.bulker.emitEvent(`job:queued`,{jobId:e,action:this.action,queued:a,total:m.total})}a===0&&await this.bulker.jobManager.completeEmptyJob(e)}async startRabbitWorker(){return this.bulker.logger.info(`Starting RabbitMQ consumer for queue ${this.rabbitQueueName}`),this.bulker.emitEvent(`worker:started`,{action:this.action,queueName:this.rabbitQueueName}),this.rabbit?.consume(this.rabbitQueueName,async(e,t,n)=>{if(!e)return;let{jobId:r,ids:i,payload:a}=e.content,o=!1,s=async()=>{if(!o){o=!0,await Promise.all([this.bulker.jobManager.ack(r,i.length),t()]);for(let e of i)this.bulker.emitEvent(`item:processed`,{jobId:r,action:this.action,id:e})}},c=async(e,t)=>{if(o)return;o=!0;let s=t;s===void 0&&(s=Z.isBulkerError(e)?e.retryable:!1),await Promise.all([this.bulker.jobManager.nack(r,e?e.message||String(e):`nacked`,{ids:i,payload:a},i.length),n(void 0,{skipRetry:!s})]);for(let t of i)s?this.bulker.emitEvent(`item:retrying`,{jobId:r,action:this.action,id:t}):this.bulker.emitEvent(`item:failed`,{jobId:r,action:this.action,id:t,error:e?e.message||String(e):`nacked`})},l=await this.bulker.jobManager.getJobField(r,`status`);if(!l||l===`canceled`){await this.bulker.jobManager.incrJobField(r,`processed`,i.length);return}try{this.bulker.logger.info(`Processing job ${r} action ${this.action} ids ${i.join(`,`)}`);for(let e of i)this.bulker.emitEvent(`item:processing`,{jobId:r,action:this.action,id:e});await this.consumer({jobId:r,ids:i,id:this.consumerBatchSize===1?i[0]:void 0,payload:a},s,c),await s()}catch(e){this.bulker.logger.error(`Error processing job ${r} action ${this.action} ids ${i.join(`,`)}: ${e.message||e}`,{err:e}),this.bulker.emitEvent(`worker:error`,{action:this.action,error:e.message||String(e)}),await c(e,!1)}},this.consumerOptions)}},He=class{constructor(e,t,n=`/bulk-actions`){this.routes=[],this.actionsToRouteMap=new Map,this.bulkHandler=async(e,t,n)=>{try{let{action:r}=e.body??{};if(!r)return t.status(400).json({error:`missing_action`,message:`Request body must include an "action" field`});let i=this.actionsToRouteMap.get(r);return i?i.bulkHandler(e,t,n):t.status(404).json({error:`unknown_action`,message:`Action "${r}" is not registered`,availableActions:Array.from(this.actionsToRouteMap.keys())})}catch(e){return this.bulker.logger.error(`Error in BulkRouter bulkHandler: ${e.message||e}`,{err:e}),n(e),null}},this.getJobHandler=async(e,t,n)=>{try{if(!e.params.id)return t.status(400).json({error:`missing_id`});let n=await this.bulker.jobManager.getJob(e.params.id);return n?t.json(n):t.status(404).json({error:`not_found`})}catch(e){return n(e)}},this.cancelJobHandler=async(e,t,n)=>{try{return e.params.id?await this.bulker.jobManager.cancelJob(e.params.id)?(this.bulker.logger.info(`Job ${e.params.id} cancel requested`),t.json({ok:!0})):t.status(404).json({error:`not_found`}):t.status(400).json({error:`missing_id`})}catch(e){return n(e)}},this.getMyJobsHandler=async(e,t,n)=>{try{if(!this.bulker.getUserId)return t.status(400).json({error:`user_id_function_not_configured`,message:`Bulker instance does not have a getUserId function configured`});let e=this.bulker.getUserId();if(!e)return t.json([]);let n=(await this.bulker.jobManager.getUserJobs(e)).filter(e=>e!==null);return n.sort((e,t)=>{let n=e.createdAt?new Date(e.createdAt).getTime():0;return(t.createdAt?new Date(t.createdAt).getTime():0)-n}),t.json(n)}catch(e){return n(e)}},this.bulker=e,this.router=t??(0,h.Router)(),this.staticRoute=n,this.registerStaticRoutes()}registerStaticRoutes(){this.router.post(this.staticRoute,this.bulkHandler),this.router.get(`/jobs/:id`,this.getJobHandler),this.router.get(`/jobs`,this.getMyJobsHandler),this.router.post(`/jobs/:id/cancel`,this.cancelJobHandler)}addAction(e,t){let n=new Ve(this.bulker,{action:e,...t});if(this.bulker.logger.info(`Registering action handler: ${e}`),this.actionsToRouteMap.has(e))throw Error(`Action "${e}" is already registered`);return this.actionsToRouteMap.set(e,n),this.routes.push(n),n}getRouter(){return this.router}};const Ue=`
2
3
  local jobKey = KEYS[1]
3
- local updatedAt = ARGV[1]
4
+ local count = tonumber(ARGV[1]) or 1
5
+ local updatedAt = ARGV[2]
4
6
  local total = tonumber(redis.call('HGET', jobKey, 'total'))
5
7
  if not total then
6
8
  return 0
7
9
  end
8
10
 
9
- local processed = redis.call('HINCRBY', jobKey, 'processed', 1)
10
- redis.call('HINCRBY', jobKey, 'succeeded', 1)
11
+ local processed = redis.call('HINCRBY', jobKey, 'processed', count)
12
+ redis.call('HINCRBY', jobKey, 'succeeded', count)
11
13
  redis.call('HSET', jobKey, 'updatedAt', updatedAt)
12
14
 
13
15
  if processed >= total then
@@ -21,32 +23,44 @@ local jobKey = KEYS[1]
21
23
  local errorMsg = ARGV[1]
22
24
  local errorData = ARGV[2]
23
25
  local errorLogLimit = tonumber(ARGV[3])
24
- local updatedAt = ARGV[4]
26
+ local count = tonumber(ARGV[4]) or 1
27
+ local updatedAt = ARGV[5]
25
28
 
26
29
  local total = tonumber(redis.call('HGET', jobKey, 'total'))
27
30
  if not total then
28
31
  return 0
29
32
  end
30
33
 
31
- -- Add error
34
+ -- Add errors (one per failed item in the batch)
32
35
  local errorsJson = redis.call('HGET', jobKey, 'errors') or '[]'
33
36
  local errors = cjson.decode(errorsJson)
37
+ local errorData = cjson.decode(errorData)
38
+
39
+ -- Add one error entry per failed item
40
+ for i = 1, count do
41
+ if #errors >= errorLogLimit then
42
+ -- Keep only the last (errorLogLimit - 1) entries
43
+ local newErrors = {}
44
+ for j = #errors - errorLogLimit + 2, #errors do
45
+ table.insert(newErrors, errors[j])
46
+ end
47
+ errors = newErrors
48
+ end
34
49
 
35
- if #errors >= errorLogLimit then
36
- -- Keep only the last (errorLogLimit - 1) entries
37
- local newErrors = {}
38
- for i = #errors - errorLogLimit + 2, #errors do
39
- table.insert(newErrors, errors[i])
50
+ -- Add error with individual ID if ids array is provided, otherwise use batch data
51
+ local errorEntry = { message = errorMsg, data = errorData }
52
+ if errorData.ids and errorData.ids[i] then
53
+ errorEntry.data = { id = errorData.ids[i], payload = errorData.payload }
40
54
  end
41
- errors = newErrors
55
+
56
+ table.insert(errors, errorEntry)
42
57
  end
43
58
 
44
- table.insert(errors, { message = errorMsg, data = cjson.decode(errorData) })
45
59
  redis.call('HSET', jobKey, 'errors', cjson.encode(errors))
46
60
 
47
61
  -- Update counters
48
- redis.call('HINCRBY', jobKey, 'failed', 1)
49
- local processed = redis.call('HINCRBY', jobKey, 'processed', 1)
62
+ redis.call('HINCRBY', jobKey, 'failed', count)
63
+ local processed = redis.call('HINCRBY', jobKey, 'processed', count)
50
64
  redis.call('HSET', jobKey, 'updatedAt', updatedAt)
51
65
 
52
66
  if processed >= total then
@@ -55,16 +69,17 @@ if processed >= total then
55
69
  end
56
70
 
57
71
  return processed
58
- `;var $=class e{constructor(e,t){this.redis=e,this.defaults={maxJobsPerUser:t?.maxJobsPerUser??-1,jobTtlSeconds:t?.jobTtlSeconds??168*3600,errorLogLimit:t?.errorLogLimit??10}}setEventEmitter(e){this.eventEmitter=e}static jobKey(e){return`bulker:job:${e}`}static userJobsKey(e){return`bulker:user:${e}:jobs`}async initJob(t,n){let r=Date.now().toString(),i=e.jobKey(t),a=this.redis.multi();a.hSet(i,{status:n.status,total:String(n.total),queued:`0`,processed:`0`,succeeded:`0`,failed:`0`,action:n.action,errors:JSON.stringify([]),createdAt:r,updatedAt:r,startTime:r,endTime:``}),a.expire(i,this.defaults.jobTtlSeconds),await a.exec()}async getJob(t){let n=e.jobKey(t),r=await this.redis.hGetAll(n);if(!r||Object.keys(r).length===0)return null;let i=r.startTime?Number(r.startTime):null,a=r.endTime&&r.endTime!==``?Number(r.endTime):null,o=Date.now(),s={startTime:i?new Date(i).toISOString():null,endTime:a?new Date(a).toISOString():null,durationMs:i?(a||o)-i:null};return{jobId:t,status:r.status,action:r.action,total:Number(r.total??0),queued:Number(r.queued??0),processed:Number(r.processed??0),succeeded:Number(r.succeeded??0),failed:Number(r.failed??0),errors:r.errors?JSON.parse(r.errors):[],createdAt:r.createdAt?new Date(Number(r.createdAt)).toISOString():void 0,updatedAt:r.updatedAt?new Date(Number(r.updatedAt)).toISOString():void 0,duration:s}}async setJobField(t,n,r){let i=e.jobKey(t);if(!await this.redis.exists(i))return!1;let a=this.redis.multi();return a.hSet(i,n,r),a.hSet(i,`updatedAt`,Date.now().toString()),await a.exec(),!0}async setJobFields(t,n){let r=e.jobKey(t);if(!await this.redis.exists(r))return!1;let i=this.redis.multi();return i.hSet(r,n),i.hSet(r,`updatedAt`,Date.now().toString()),await i.exec(),!0}async incrJobField(t,n,r=1){let i=e.jobKey(t),a=this.redis.multi();a.hIncrBy(i,n,r),a.hSet(i,`updatedAt`,Date.now().toString());let o=await a.exec();return o?.[0]&&o[0][0]===null?o[0][1]:0}async getJobField(t,n){let r=e.jobKey(t);return this.redis.hGet(r,n)}async cancelJob(e){return this.setJobField(e,`status`,`canceled`)}async completeEmptyJob(e){let t=Date.now().toString();await this.setJobFields(e,{status:`completed`,endTime:t})}async ack(t){let n=e.jobKey(t);await this.redis.eval(`
72
+ `;var $=class e{constructor(e,t){this.redis=e,this.defaults={maxJobsPerUser:t?.maxJobsPerUser??-1,jobTtlSeconds:t?.jobTtlSeconds??168*3600,errorLogLimit:t?.errorLogLimit??10}}setEventEmitter(e){this.eventEmitter=e}static jobKey(e){return`bulker:job:${e}`}static userJobsKey(e){return`bulker:user:${e}:jobs`}async initJob(t,n){let r=Date.now().toString(),i=e.jobKey(t),a=this.redis.multi();a.hSet(i,{status:n.status,total:String(n.total),queued:`0`,processed:`0`,succeeded:`0`,failed:`0`,action:n.action,errors:JSON.stringify([]),createdAt:r,updatedAt:r,startTime:r,endTime:``}),a.expire(i,this.defaults.jobTtlSeconds),await a.exec()}async getJob(t){let n=e.jobKey(t),r=await this.redis.hGetAll(n);if(!r||Object.keys(r).length===0)return null;let i=r.startTime?Number(r.startTime):null,a=r.endTime&&r.endTime!==``?Number(r.endTime):null,o=Date.now(),s={startTime:i?new Date(i).toISOString():null,endTime:a?new Date(a).toISOString():null,durationMs:i?(a||o)-i:null};return{jobId:t,status:r.status,action:r.action,total:Number(r.total??0),queued:Number(r.queued??0),processed:Number(r.processed??0),succeeded:Number(r.succeeded??0),failed:Number(r.failed??0),errors:r.errors?JSON.parse(r.errors):[],createdAt:r.createdAt?new Date(Number(r.createdAt)).toISOString():void 0,updatedAt:r.updatedAt?new Date(Number(r.updatedAt)).toISOString():void 0,duration:s}}async setJobField(t,n,r){let i=e.jobKey(t);if(!await this.redis.exists(i))return!1;let a=this.redis.multi();return a.hSet(i,n,r),a.hSet(i,`updatedAt`,Date.now().toString()),await a.exec(),!0}async setJobFields(t,n){let r=e.jobKey(t);if(!await this.redis.exists(r))return!1;let i=this.redis.multi();return i.hSet(r,n),i.hSet(r,`updatedAt`,Date.now().toString()),await i.exec(),!0}async incrJobField(t,n,r=1){let i=e.jobKey(t),a=this.redis.multi();a.hIncrBy(i,n,r),a.hSet(i,`updatedAt`,Date.now().toString());let o=await a.exec();return o?.[0]&&o[0][0]===null?o[0][1]:0}async getJobField(t,n){let r=e.jobKey(t);return this.redis.hGet(r,n)}async cancelJob(e){return this.setJobField(e,`status`,`canceled`)}async completeEmptyJob(e){let t=Date.now().toString();await this.setJobFields(e,{status:`completed`,endTime:t})}async ack(t,n=1){let r=e.jobKey(t);await this.redis.eval(`
59
73
  local jobKey = KEYS[1]
60
- local updatedAt = ARGV[1]
74
+ local count = tonumber(ARGV[1]) or 1
75
+ local updatedAt = ARGV[2]
61
76
  local total = tonumber(redis.call('HGET', jobKey, 'total'))
62
77
  if not total then
63
78
  return 0
64
79
  end
65
80
 
66
- local processed = redis.call('HINCRBY', jobKey, 'processed', 1)
67
- redis.call('HINCRBY', jobKey, 'succeeded', 1)
81
+ local processed = redis.call('HINCRBY', jobKey, 'processed', count)
82
+ redis.call('HINCRBY', jobKey, 'succeeded', count)
68
83
  redis.call('HSET', jobKey, 'updatedAt', updatedAt)
69
84
 
70
85
  if processed >= total then
@@ -73,37 +88,49 @@ if processed >= total then
73
88
  end
74
89
 
75
90
  return processed
76
- `,{keys:[n],arguments:[Date.now().toString()]});let r=await this.getJob(t);if(r&&r.status===`completed`){let e=r.duration.durationMs||0;this.eventEmitter?.emitEvent(`job:completed`,{jobId:t,action:r.action,processed:r.processed,failed:r.failed,duration:e})}}async nack(t,n,r){let i=e.jobKey(t);await this.redis.eval(`
91
+ `,{keys:[r],arguments:[n.toString(),Date.now().toString()]});let i=await this.getJob(t);if(i&&i.status===`completed`){let e=i.duration.durationMs||0;this.eventEmitter?.emitEvent(`job:completed`,{jobId:t,action:i.action,processed:i.processed,failed:i.failed,duration:e})}}async nack(t,n,r,i=1){let a=e.jobKey(t);await this.redis.eval(`
77
92
  local jobKey = KEYS[1]
78
93
  local errorMsg = ARGV[1]
79
94
  local errorData = ARGV[2]
80
95
  local errorLogLimit = tonumber(ARGV[3])
81
- local updatedAt = ARGV[4]
96
+ local count = tonumber(ARGV[4]) or 1
97
+ local updatedAt = ARGV[5]
82
98
 
83
99
  local total = tonumber(redis.call('HGET', jobKey, 'total'))
84
100
  if not total then
85
101
  return 0
86
102
  end
87
103
 
88
- -- Add error
104
+ -- Add errors (one per failed item in the batch)
89
105
  local errorsJson = redis.call('HGET', jobKey, 'errors') or '[]'
90
106
  local errors = cjson.decode(errorsJson)
107
+ local errorData = cjson.decode(errorData)
108
+
109
+ -- Add one error entry per failed item
110
+ for i = 1, count do
111
+ if #errors >= errorLogLimit then
112
+ -- Keep only the last (errorLogLimit - 1) entries
113
+ local newErrors = {}
114
+ for j = #errors - errorLogLimit + 2, #errors do
115
+ table.insert(newErrors, errors[j])
116
+ end
117
+ errors = newErrors
118
+ end
91
119
 
92
- if #errors >= errorLogLimit then
93
- -- Keep only the last (errorLogLimit - 1) entries
94
- local newErrors = {}
95
- for i = #errors - errorLogLimit + 2, #errors do
96
- table.insert(newErrors, errors[i])
120
+ -- Add error with individual ID if ids array is provided, otherwise use batch data
121
+ local errorEntry = { message = errorMsg, data = errorData }
122
+ if errorData.ids and errorData.ids[i] then
123
+ errorEntry.data = { id = errorData.ids[i], payload = errorData.payload }
97
124
  end
98
- errors = newErrors
125
+
126
+ table.insert(errors, errorEntry)
99
127
  end
100
128
 
101
- table.insert(errors, { message = errorMsg, data = cjson.decode(errorData) })
102
129
  redis.call('HSET', jobKey, 'errors', cjson.encode(errors))
103
130
 
104
131
  -- Update counters
105
- redis.call('HINCRBY', jobKey, 'failed', 1)
106
- local processed = redis.call('HINCRBY', jobKey, 'processed', 1)
132
+ redis.call('HINCRBY', jobKey, 'failed', count)
133
+ local processed = redis.call('HINCRBY', jobKey, 'processed', count)
107
134
  redis.call('HSET', jobKey, 'updatedAt', updatedAt)
108
135
 
109
136
  if processed >= total then
@@ -112,5 +139,5 @@ if processed >= total then
112
139
  end
113
140
 
114
141
  return processed
115
- `,{keys:[i],arguments:[n,JSON.stringify(r),String(this.defaults.errorLogLimit),Date.now().toString()]});let a=await this.getJob(t);if(a&&a.status===`completed`){let e=a.duration.durationMs||0;this.eventEmitter?.emitEvent(`job:completed`,{jobId:t,action:a.action,processed:a.processed,failed:a.failed,duration:e})}}async addUserJob(t,n){let r=e.userJobsKey(t),i=this.redis.multi();i.lPush(r,n),this.defaults.maxJobsPerUser>0&&i.lTrim(r,0,this.defaults.maxJobsPerUser-1),i.expire(r,3600),await i.exec()}async removeUserJob(t,n){let r=e.userJobsKey(t);await this.redis.lRem(r,0,n)}async getUserJobs(t,n=20){let r=e.userJobsKey(t),i=await this.redis.lRange(r,0,n-1);return await Promise.all(i.map(e=>this.getJob(e)))}};const Ge={emitEvent:()=>{}};var Ke=class extends _.EventEmitter{constructor(e){if(super(),this.bulkRouters=[],this.sequelize=e.sequelize,this.logger=e.logger,this.rabbit=e.rabbit,this.eventsEnabled=e.emitEvents??!1,e.redis&&typeof e.redis.connect==`function`)this.redis=e.redis,this.redis.isOpen||this.redis.connect().catch(e=>{this.logger.error(`Error connecting to Redis:`,e)});else{let t=e.redis;this.redis=(0,m.createClient)({socket:{host:t.host,port:t.port},commandsQueueMaxLength:5e3,disableOfflineQueue:!1,password:t.password,database:t.db}),this.redis.connect().catch(e=>{this.logger.error(`Error connecting to Redis:`,e)})}this.getUserId=e.getUserId??(()=>null),this.defaults={maxJobsPerUser:e.defaults?.maxJobsPerUser??-1,pageSize:e.defaults?.pageSize??1e3,idField:e.defaults?.idField??`id`,workerConcurrency:e.defaults?.workerConcurrency??16,jobTtlSeconds:e.defaults?.jobTtlSeconds??168*3600,errorLogLimit:e.defaults?.errorLogLimit??10},this.jobManager=new $(this.redis,{maxJobsPerUser:this.defaults.maxJobsPerUser,jobTtlSeconds:this.defaults.jobTtlSeconds,errorLogLimit:this.defaults.errorLogLimit}),this.jobManager.setEventEmitter(this.eventsEnabled?{emitEvent:this.emitEvent.bind(this)}:Ge)}pingRedis(){return this.redis.ping()}createBulkRouter(e,t){let n=new He(this,e,t);return this.bulkRouters.push(n),n}emitEvent(e,...t){this.eventsEnabled&&this.emit(e,...t)}};exports.Bulker=Ke,exports.BulkerError=Z,exports.BulkerJobManager=$,exports.formatOperators=x,exports.generateFilterReplacements=L,exports.queryFormatMiddleware=Fe,exports.queryHandler=Ie,exports.queryValidationMiddleware=Pe,exports.validatePayload=W,Object.defineProperty(exports,`z`,{enumerable:!0,get:function(){return g.z}});
142
+ `,{keys:[a],arguments:[n,JSON.stringify(r),String(this.defaults.errorLogLimit),i.toString(),Date.now().toString()]});let o=await this.getJob(t);if(o&&o.status===`completed`){let e=o.duration.durationMs||0;this.eventEmitter?.emitEvent(`job:completed`,{jobId:t,action:o.action,processed:o.processed,failed:o.failed,duration:e})}}async addUserJob(t,n){let r=e.userJobsKey(t),i=this.redis.multi();i.lPush(r,n),this.defaults.maxJobsPerUser>0&&i.lTrim(r,0,this.defaults.maxJobsPerUser-1),i.expire(r,3600),await i.exec()}async removeUserJob(t,n){let r=e.userJobsKey(t);await this.redis.lRem(r,0,n)}async getUserJobs(t,n=20){let r=e.userJobsKey(t),i=await this.redis.lRange(r,0,n-1);return await Promise.all(i.map(e=>this.getJob(e)))}};const Ge={emitEvent:()=>{}};var Ke=class extends _.EventEmitter{constructor(e){if(super(),this.bulkRouters=[],this.sequelize=e.sequelize,this.logger=e.logger,this.rabbit=e.rabbit,this.eventsEnabled=e.emitEvents??!1,e.redis&&typeof e.redis.connect==`function`)this.redis=e.redis,this.redis.isOpen||this.redis.connect().catch(e=>{this.logger.error(`Error connecting to Redis:`,e)});else{let t=e.redis;this.redis=(0,m.createClient)({socket:{host:t.host,port:t.port},commandsQueueMaxLength:5e3,disableOfflineQueue:!1,password:t.password,database:t.db}),this.redis.connect().catch(e=>{this.logger.error(`Error connecting to Redis:`,e)})}this.getUserId=e.getUserId??(()=>null),this.defaults={maxJobsPerUser:e.defaults?.maxJobsPerUser??-1,pageSize:e.defaults?.pageSize??1e3,idField:e.defaults?.idField??`id`,consumerBatchSize:e.defaults?.consumerBatchSize??1,workerConcurrency:e.defaults?.workerConcurrency??16,jobTtlSeconds:e.defaults?.jobTtlSeconds??168*3600,errorLogLimit:e.defaults?.errorLogLimit??10},this.jobManager=new $(this.redis,{maxJobsPerUser:this.defaults.maxJobsPerUser,jobTtlSeconds:this.defaults.jobTtlSeconds,errorLogLimit:this.defaults.errorLogLimit}),this.jobManager.setEventEmitter(this.eventsEnabled?{emitEvent:this.emitEvent.bind(this)}:Ge)}pingRedis(){return this.redis.ping()}createBulkRouter(e,t){let n=new He(this,e,t);return this.bulkRouters.push(n),n}emitEvent(e,...t){this.eventsEnabled&&this.emit(e,...t)}};exports.Bulker=Ke,exports.BulkerError=Z,exports.BulkerJobManager=$,exports.formatOperators=x,exports.generateFilterReplacements=L,exports.queryFormatMiddleware=Fe,exports.queryHandler=Ie,exports.queryValidationMiddleware=Pe,exports.validatePayload=W,Object.defineProperty(exports,`z`,{enumerable:!0,get:function(){return g.z}});
116
143
  //# sourceMappingURL=index.cjs.map