@murumets-ee/imports 0.15.4 → 0.16.1

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/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- import{a as e,i as t,n,r,t as i}from"./transform-BUGBTotp.mjs";import{a,i as o,n as s,o as c,r as l,s as u,t as d}from"./runner-D9FtnIBn.mjs";export{d as DEFAULT_BATCH_SIZE,a as DEFAULT_MAX_PATTERNS,c as DEFAULT_MAX_SAMPLES_PER_PATTERN,u as ErrorTracker,t as IMPORT_RUN_STATUSES,e as ImportRun,i as TransformRegistry,l as countDataRows,n as getTransformRegistry,r as registerImportTransform,s as runImport,o as streamFeed};
1
+ import{a as e,i as t,n,r,t as i}from"./transform-CSTDgiGK.mjs";import{a,i as o,n as s,o as c,r as l,s as u,t as d}from"./runner-D9FtnIBn.mjs";export{d as DEFAULT_BATCH_SIZE,a as DEFAULT_MAX_PATTERNS,c as DEFAULT_MAX_SAMPLES_PER_PATTERN,u as ErrorTracker,t as IMPORT_RUN_STATUSES,e as ImportRun,i as TransformRegistry,l as countDataRows,n as getTransformRegistry,r as registerImportTransform,s as runImport,o as streamFeed};
package/dist/plugin.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import{a as e,n as t}from"./transform-BUGBTotp.mjs";import{registerImportsStorageFactory as n}from"./storage.mjs";import{createImportsRunDeadListener as r,createRunImportHandler as i,importsRunJob as a}from"./worker.mjs";const o=Symbol.for(`@murumets-ee/imports:dead-listener-unsubscribe`);function s(s){let c=s.esIndex??`parts`;return s.storage&&n(s.storage),{name:`@murumets-ee/imports`,server:{entities:[e],init:async n=>{if(!n.plugins.all().some(e=>e.name===`@murumets-ee/queue`)){n.logger.warn(`imports: queue() plugin not in plugins array — imports:run jobs will not be processed`);return}let l=await import(`@murumets-ee/queue/client`),{createAdminClient:u}=await import(`@murumets-ee/core/clients`),d=u(e,n),f=t();l.registerJob(a,i({importRuns:d,transforms:f,esClient:s.esClient,esIndex:c,...s.resolveFilePath!==void 0&&{resolveFilePath:s.resolveFilePath},logger:n.logger.child({pkg:`imports`})}));let p=globalThis[o];p&&p();let m=l.addQueueTerminalListener(r({importRuns:d,logger:n.logger.child({pkg:`imports`})}));globalThis[o]=m,n.logger.info({esIndex:c},`Imports plugin initialized`)}}}}export{s as imports};
1
+ import{a as e,n as t}from"./transform-CSTDgiGK.mjs";import{registerImportsStorageFactory as n}from"./storage.mjs";import{createImportsRunDeadListener as r,createRunImportHandler as i,importsRunJob as a}from"./worker.mjs";const o=Symbol.for(`@murumets-ee/imports:dead-listener-unsubscribe`);function s(s){let c=s.esIndex??`parts`;return s.storage&&n(s.storage),{name:`@murumets-ee/imports`,server:{entities:[e],init:async n=>{if(!n.plugins.all().some(e=>e.name===`@murumets-ee/queue`)){n.logger.warn(`imports: queue() plugin not in plugins array — imports:run jobs will not be processed`);return}let l=await import(`@murumets-ee/queue/client`),{createAdminClient:u}=await import(`@murumets-ee/core/clients`),d=u(e,n),f=t();l.registerJob(a,i({importRuns:d,transforms:f,esClient:s.esClient,esIndex:c,...s.resolveFilePath!==void 0&&{resolveFilePath:s.resolveFilePath},logger:n.logger.child({pkg:`imports`})}));let p=globalThis[o];p&&p();let m=l.addQueueTerminalListener(r({importRuns:d,logger:n.logger.child({pkg:`imports`})}));globalThis[o]=m,n.logger.info({esIndex:c},`Imports plugin initialized`)}}}}export{s as imports};
2
2
  //# sourceMappingURL=plugin.mjs.map
@@ -0,0 +1,2 @@
1
+ import{behavior as e,defineEntity as t,field as n,searchable as r}from"@murumets-ee/entity";const i=[`pending`,`running`,`succeeded`,`failed`,`cancelled`],a=t({name:`import_run`,fields:{label:n.text({required:!0,maxLength:255}),status:n.select({options:i,default:`pending`,indexed:!0}),filePath:n.text({required:!0,maxLength:2048}),transformName:n.text({required:!0,maxLength:128,indexed:!0}),params:n.json(),totals:n.json(),errorSummary:n.json(),startedAt:n.date(),finishedAt:n.date(),queueJobId:n.text({maxLength:64,indexed:!0})},behaviors:[e.auditable(),r({fields:[`label`,`transformName`,`queueJobId`,`status`,`filePath`],projection:e=>({id:e.id,label:e.label,description:`${e.transformName} · ${e.status}`})})],scope:`global`,admin:{group:`imports`,label:`Import runs`,labelSingular:`Import run`,icon:`upload`,hideFromMenu:!0}});var o=class{entries=new Map;register(e,t){if(this.entries.has(e))throw Error(`Transform "${e}" is already registered — two plugins cannot register the same transform name.`);this.entries.set(e,t)}get(e){return this.entries.get(e)}has(e){return this.entries.has(e)}list(){return Array.from(this.entries.keys()).sort()}clear(){this.entries.clear()}};const s=Symbol.for(`@murumets-ee/imports:transforms`);function c(){let e=globalThis,t=e[s];return t||(t=new o,e[s]=t),t}function l(e,t){c().register(e,t)}export{a,i,c as n,l as r,o as t};
2
+ //# sourceMappingURL=transform-CSTDgiGK.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transform-CSTDgiGK.mjs","names":[],"sources":["../src/entities/import-run.ts","../src/transform.ts"],"sourcesContent":["/**\n * ImportRun — one feed-import attempt. Tracks file, transform, status,\n * row totals, and the top error patterns surfaced by `ErrorTracker`.\n *\n * Per PLAN-ECOMMERCE.md PR 7 (PoC scope): generic plumbing entity. The\n * commerce-specific transform parameters (brandId, supplierId, supplier\n * code prefix) live in the opaque `params` JSONB so the imports package\n * stays free of commerce-domain dependencies. PR 8 wires a concrete\n * carmaker-feed transform that reads those params; PR 8a renders a\n * bespoke `/admin/commerce/imports` page over this entity's history.\n *\n * `hideFromMenu: true` because PR 7 ships only the entity + plumbing —\n * the operator-facing surface is PR 8a. The auto-EntityListPage at\n * `/admin/import_run` still resolves for direct lookups, but no sidebar\n * entry points to it.\n */\n\nimport { behavior, defineEntity, field, searchable } from '@murumets-ee/entity'\nimport type { AdminClient } from '@murumets-ee/entity/admin'\n\n/**\n * Lifecycle stages.\n *\n * - `pending` — row exists, queue job has been enqueued, worker hasn't picked it up yet.\n * - `running` — worker is streaming + bulk-writing.\n * - `succeeded` — worker finished cleanly (note: per-row failures still possible — see `totals.failed`).\n * - `failed` — worker threw and the queue marked the job dead. `errorSummary.fatal` carries the cause.\n * - `cancelled` — manual operator action via PR 8a (out of scope for PR 7).\n */\nexport const IMPORT_RUN_STATUSES = ['pending', 'running', 'succeeded', 'failed', 'cancelled'] as const\nexport type ImportRunStatus = (typeof IMPORT_RUN_STATUSES)[number]\n\nexport const ImportRun = defineEntity({\n name: 'import_run',\n fields: {\n /** Operator-visible label, e.g. `\"MERCEDES — ME_20251027075918.txt\"`. Free-form. */\n label: field.text({ required: true, maxLength: 255 }),\n /** See {@link IMPORT_RUN_STATUSES}. */\n status: field.select({\n options: IMPORT_RUN_STATUSES,\n default: 'pending',\n indexed: true,\n }),\n /**\n * Path or storage key of the uploaded feed file. Generic string —\n * could be `/var/lumi/uploads/<id>.txt` for the local-disk PoC or an\n * S3 object key once storage adapter integration lands. The worker\n * reads this with the configured `readFeed` resolver.\n */\n filePath: field.text({ required: true, maxLength: 2048 }),\n /**\n * Name of the registered transform applied to each row. Resolved at\n * worker-dispatch time against the transform registry contributed by\n * the consumer. PoC: `'commerce:carmaker-feed'`.\n */\n transformName: field.text({ required: true, maxLength: 128, indexed: true }),\n /**\n * Opaque per-transform parameters. The carmaker transform expects\n * `{ brandId, supplierId, codePrefix? }`. Validation happens inside\n * the transform — the imports package never inspects this shape so\n * a new transform with different params doesn't require a schema\n * migration.\n */\n params: field.json(),\n /**\n * Row counters: `{ submitted, succeeded, failed, skipped, batches }`.\n * Updated by the worker as each batch completes. Final values are\n * what the operator reads; intermediate progress comes from\n * `toolkit_jobs.progress` via the queue UI.\n */\n totals: field.json(),\n /**\n * Output of `ErrorTracker.getTopPatterns(totalRows)` — the top-50\n * error signatures with up to 5 sample rows each. Empty `[]` until\n * the worker writes it on completion.\n */\n errorSummary: field.json(),\n /** Set when the worker picks up the job. */\n startedAt: field.date(),\n /** Set when the worker finishes (success OR fatal failure). */\n finishedAt: field.date(),\n /**\n * `toolkit_jobs.id` of the queue job processing this run — link\n * back so PR 8a can show live progress without a second lookup.\n */\n queueJobId: field.text({ maxLength: 64, indexed: true }),\n },\n behaviors: [\n behavior.auditable(),\n // Operator-facing search: by label (e.g. \"MERCEDES — ME_*.txt\"),\n // transform name, or queue job id. No FTS — labels are short\n // alphanumeric tags, not prose.\n searchable({\n fields: ['label', 'transformName', 'queueJobId', 'status', 'filePath'],\n projection: (row) => ({\n id: row.id as string,\n label: row.label as string,\n description: `${row.transformName} · ${row.status}`,\n }),\n }),\n ],\n scope: 'global',\n admin: {\n group: 'imports',\n label: 'Import runs',\n labelSingular: 'Import run',\n icon: 'upload',\n hideFromMenu: true,\n },\n})\n\nexport type ImportRunClient = AdminClient<typeof ImportRun.allFields>\n","/**\n * Per-feed transform plugin interface (PLAN-ECOMMERCE.md PR 7 / D14).\n *\n * The streaming reader reads raw rows out of the file (column key →\n * string). The transform turns one row into a typed `OutputDoc` ready\n * for the bulk-write surface (PoC: `PartsDocument` for the parts ES\n * index). The transform is what makes a generic feed-importer\n * commerce-aware — without it, the importer has no opinion on what a\n * row \"means\".\n *\n * Why an interface, not a function:\n * - The transform may need to reject a row (`RowSkip`) without that\n * counting as an error in `ErrorTracker` — e.g. blank lines, header\n * rows mistakenly retained, or \"this is a replacement-code marker\n * row, handled out-of-band\" decisions.\n * - The transform may need to fail a row (`RowError`) with a typed\n * error class so `ErrorTracker` collapses them by `errorType`.\n * - The transform may need access to the run-level params (brand /\n * supplier IDs, code prefix, batch ID) without those being\n * re-derived per row.\n *\n * PR 7 ships only the interface + a registry. PR 8 ships the first\n * concrete carmaker-feed transform.\n */\n\n/** Identifies which transform a registered handler implements. Stable across deploys. */\nexport type TransformName = string\n\n/** Per-run context provided to every row call. Kept narrow on purpose. */\nexport interface TransformContext {\n /** UUID of the `import_run` row. Forward to the output doc as `import_batch_id` for source attribution (D20). */\n importRunId: string\n /** Opaque per-run params copied from `import_run.params`. The transform validates the shape it expects. */\n params: Record<string, unknown>\n /** Operator-supplied label, useful for logging / debugging. */\n runLabel: string\n /** 1-based row number across the whole feed (header counts as row 0). Forward to `RowError.rowNumber`. */\n rowNumber: number\n}\n\n/**\n * One of: a successful `OutputDoc` keyed by a stable `id`, an\n * intentional `skip` (counted in `totals.skipped`), or a row-level\n * error (counted in `totals.failed` AND aggregated by `ErrorTracker`).\n *\n * The `id` field on `success` is used as the bulk-upsert primary key\n * (`_id` in ES). Per D4 the parts index uses\n * `<code_normalized>__<supplier_id>` so a re-run of the same feed\n * idempotently overwrites instead of duplicating.\n */\nexport type RowResult<TDoc> =\n | { kind: 'success'; id: string; doc: TDoc }\n | { kind: 'skip'; reason: string }\n | { kind: 'error'; error: RowError }\n\n/** Row-level error. The aggregator uses `(errorType, field, message)` as the dedup signature. */\nexport interface RowError {\n errorType: string\n message: string\n field?: string | undefined\n}\n\n/**\n * The contract every per-feed transform implements. Pure function of\n * `(row, ctx) → RowResult` — no I/O. Async only because the future\n * carmaker variant might consult an in-memory taxonomy lookup.\n */\nexport interface RowTransform<TDoc> {\n (rawRow: Record<string, string>, ctx: TransformContext): Promise<RowResult<TDoc>>\n}\n\n/**\n * Holds the registered transforms for the current process. Each plugin\n * that ships a transform calls {@link TransformRegistry.register} from\n * its `init` hook.\n *\n * Per CLAUDE.md \"Whitelist, don't blacklist\": the worker dispatches\n * by `import_run.transformName` against this registry. An unregistered\n * name fails the run rather than running with a default transform.\n *\n * **Production callers MUST go through {@link getTransformRegistry}**, not\n * `new TransformRegistry()`. The constructor is exposed only so tests\n * (and rare custom-worker embeds) can build an isolated instance and\n * inject it into {@link createRunImportHandler} directly. Two `new`\n * instances do NOT share state — registering a transform on one will\n * NOT make it visible to a worker resolving against the other. The\n * package's queue handler (registered by `imports()` plugin) always\n * resolves against the singleton.\n */\nexport class TransformRegistry {\n private readonly entries = new Map<TransformName, RowTransform<unknown>>()\n\n /**\n * Register a transform under a stable name. Throws on duplicate\n * names so two plugins can't silently overwrite each other (matches\n * the `SearchRegistry` pattern in `@murumets-ee/search`).\n */\n register<TDoc>(name: TransformName, transform: RowTransform<TDoc>): void {\n if (this.entries.has(name)) {\n throw new Error(\n `Transform \"${name}\" is already registered — two plugins cannot register the same transform name.`,\n )\n }\n this.entries.set(name, transform as RowTransform<unknown>)\n }\n\n /** Look up a transform by name. Returns `undefined` if no plugin has registered one. */\n get(name: TransformName): RowTransform<unknown> | undefined {\n return this.entries.get(name)\n }\n\n /** True iff a transform is registered for the given name. */\n has(name: TransformName): boolean {\n return this.entries.has(name)\n }\n\n /** All registered names. Useful for the admin Catalog tab and for tests. */\n list(): TransformName[] {\n return Array.from(this.entries.keys()).sort()\n }\n\n /** Drop all registrations. Tests only. */\n clear(): void {\n this.entries.clear()\n }\n}\n\n// ---------------------------------------------------------------------------\n// Process-global singleton (mirrors `@murumets-ee/queue`'s handler registry).\n// ---------------------------------------------------------------------------\n//\n// Why a singleton: PR 8's carmaker-feed transform lives in a different\n// plugin from `@murumets-ee/imports`. Under Next.js HMR, ONE of those\n// plugins might re-evaluate while the other does not. Holding the\n// registry on `globalThis` via `Symbol.for` means the same `Map`\n// instance is shared across module evaluations — the carmaker plugin's\n// `register` and the imports worker's `get` see each other regardless\n// of evaluation order. Same fix shape as the queue's handler-registry\n// HMR bug (#186).\n\nconst REGISTRY_KEY = Symbol.for('@murumets-ee/imports:transforms')\n\ninterface GlobalThisWithTransforms {\n [REGISTRY_KEY]?: TransformRegistry\n}\n\n/** Returns the singleton registry, creating it on first access. */\nexport function getTransformRegistry(): TransformRegistry {\n const g = globalThis as GlobalThisWithTransforms\n let reg = g[REGISTRY_KEY]\n if (!reg) {\n reg = new TransformRegistry()\n g[REGISTRY_KEY] = reg\n }\n return reg\n}\n\n/**\n * Convenience wrapper around `getTransformRegistry().register(name, fn)`.\n * Plugins that ship a transform call this from their `init` hook — same\n * shape as `@murumets-ee/queue/client`'s `registerJob`.\n */\nexport function registerImportTransform<TDoc>(\n name: TransformName,\n transform: RowTransform<TDoc>,\n): void {\n getTransformRegistry().register(name, transform)\n}\n"],"mappings":"4FA6BA,MAAa,EAAsB,CAAC,UAAW,UAAW,YAAa,SAAU,YAAY,CAGhF,EAAY,EAAa,CACpC,KAAM,aACN,OAAQ,CAEN,MAAO,EAAM,KAAK,CAAE,SAAU,GAAM,UAAW,IAAK,CAAC,CAErD,OAAQ,EAAM,OAAO,CACnB,QAAS,EACT,QAAS,UACT,QAAS,GACV,CAAC,CAOF,SAAU,EAAM,KAAK,CAAE,SAAU,GAAM,UAAW,KAAM,CAAC,CAMzD,cAAe,EAAM,KAAK,CAAE,SAAU,GAAM,UAAW,IAAK,QAAS,GAAM,CAAC,CAQ5E,OAAQ,EAAM,MAAM,CAOpB,OAAQ,EAAM,MAAM,CAMpB,aAAc,EAAM,MAAM,CAE1B,UAAW,EAAM,MAAM,CAEvB,WAAY,EAAM,MAAM,CAKxB,WAAY,EAAM,KAAK,CAAE,UAAW,GAAI,QAAS,GAAM,CAAC,CACzD,CACD,UAAW,CACT,EAAS,WAAW,CAIpB,EAAW,CACT,OAAQ,CAAC,QAAS,gBAAiB,aAAc,SAAU,WAAW,CACtE,WAAa,IAAS,CACpB,GAAI,EAAI,GACR,MAAO,EAAI,MACX,YAAa,GAAG,EAAI,cAAc,KAAK,EAAI,SAC5C,EACF,CAAC,CACH,CACD,MAAO,SACP,MAAO,CACL,MAAO,UACP,MAAO,cACP,cAAe,aACf,KAAM,SACN,aAAc,GACf,CACF,CAAC,CCpBF,IAAa,EAAb,KAA+B,CAC7B,QAA2B,IAAI,IAO/B,SAAe,EAAqB,EAAqC,CACvE,GAAI,KAAK,QAAQ,IAAI,EAAK,CACxB,MAAU,MACR,cAAc,EAAK,gFACpB,CAEH,KAAK,QAAQ,IAAI,EAAM,EAAmC,CAI5D,IAAI,EAAwD,CAC1D,OAAO,KAAK,QAAQ,IAAI,EAAK,CAI/B,IAAI,EAA8B,CAChC,OAAO,KAAK,QAAQ,IAAI,EAAK,CAI/B,MAAwB,CACtB,OAAO,MAAM,KAAK,KAAK,QAAQ,MAAM,CAAC,CAAC,MAAM,CAI/C,OAAc,CACZ,KAAK,QAAQ,OAAO,GAiBxB,MAAM,EAAe,OAAO,IAAI,kCAAkC,CAOlE,SAAgB,GAA0C,CACxD,IAAM,EAAI,WACN,EAAM,EAAE,GAKZ,OAJK,IACH,EAAM,IAAI,EACV,EAAE,GAAgB,GAEb,EAQT,SAAgB,EACd,EACA,EACM,CACN,GAAsB,CAAC,SAAS,EAAM,EAAU"}
@@ -1 +1 @@
1
- {"version":3,"file":"worker-B7ADOFEV.d.mts","names":[],"sources":["../src/entities/import-run.ts","../src/error-tracker.ts","../src/streaming.ts","../src/runner.ts","../src/worker.ts"],"mappings":";;;;;;;;;;;AAgCA;;;;;;;cAHa,mBAAA;AAAA,KACD,eAAA,WAA0B,mBAAA;AAAA,cAEzB,SAAA,yBAAS,MAAA;MAgEpB,sBAAA,CAAA,OAAA;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAEU,eAAA,GAAkB,WAAA,QAAmB,SAAA,CAAU,SAAA;;;;;;;;;;;;AArE3D;;;;;AACA;;;;;AAEA;;;;;;;;;;;;KCAY,eAAA,sCAKR,eAAA;EAAA,CACG,GAAA,WAAc,eAAA;AAAA;;UAGJ,WAAA;EACf,SAAA;EACA,OAAA,EAAS,eAAA;AAAA;;UAIM,YAAA;EACf,SAAA;EACA,KAAA;EACA,OAAA;EACA,KAAA;EACA,eAAA;EACA,cAAA;EACA,OAAA,EAAS,aAAA,CAAc,WAAA;;EAEvB,UAAA;AAAA;AAAA,UAGe,kBAAA;;EAEf,WAAA;;EAEA,oBAAA;AAAA;;cAIW,oBAAA;AAAA,cACA,+BAAA;AAAA,cAYA,YAAA;EAAA,iBACM,QAAA;EAAA,iBACA,WAAA;EAAA,iBACA,oBAAA;EAAA,QACT,iBAAA;cAEI,MAAA,GAAQ,kBAAA;;;;;;;;;;EAcpB,QAAA,CACE,SAAA,UACA,SAAA,UACA,OAAA,UACA,KAAA,sBACA,OAAA,EAAS,eAAA;;EA+BX,kBAAA,CAAA;;EAOA,uBAAA,CAAA;;;;;;EASA,wBAAA,CAAA;;;;;;EASA,cAAA,CAAA,GAAkB,YAAA;;;;;AD/DpB;ECmFE,QAAA,CAAA,GAAY,oBAAA;AAAA;AAAA,UAUG,oBAAA;EACf,WAAA;EACA,gBAAA;EACA,iBAAA;EACA,QAAA,EAAU,YAAA;AAAA;;;;;;;;;;;;ADtKZ;;;;;AACA;;;;;AAEA;;;;;;;;;;;;;;;;;;;;;;iBEasB,aAAA,CACpB,QAAA,UACA,OAAA;EAAW,SAAA;AAAA,IACV,OAAA;AAAA,UAkBc,iBAAA;EFlCK;EEoCpB,QAAA;EFpCoB;;;;EEyCpB,SAAA;;;;;;;EAOA,SAAA;;;;;;;EAOA,OAAA,GAAU,aAAA;;;;;;EAMV,gBAAA;;;;;;;;;;;;;;;EAeA,KAAA;AAAA;AAAA,UAGe,aAAA;;EAEf,SAAA;;;;;;AFfF;;;;;;;;;EE8BE,GAAA,EAAK,MAAA;AAAA;;ADhGP;;;;;;;iBC2GuB,UAAA,CAAW,OAAA,EAAS,iBAAA,GAAoB,aAAA,CAAc,aAAA;;;;cC5GhE,kBAAA;AAAA,UAEI,gBAAA;;EAEf,WAAA;;EAEA,QAAA;;EAEA,MAAA,EAAQ,MAAA;;EAER,SAAA,EAAW,YAAA,CAAa,IAAA;;EAExB,IAAA,EAAM,iBAAA;;EAEN,QAAA,EAAU,YAAA;;EAEV,OAAA;EHfoB;EGiBpB,SAAA;EHjBoB;;;;;;EGwBpB,UAAA,IAAc,QAAA,EAAU,iBAAA;;EAExB,QAAA;;EAEA,MAAA,GAAS,WAAA;;EAET,YAAA,GAAe,YAAA;;;;;;;;;;;;;;;;EAgBf,MAAA;IACE,IAAA,GAAO,IAAA,EAAM,MAAA,mBAAyB,GAAA;IACtC,IAAA,GAAO,IAAA,EAAM,MAAA,mBAAyB,GAAA;EAAA;AAAA;;;;;;UASzB,iBAAA;EACf,QAAA;EACA,aAAA;EACA,UAAA;EACA,WAAA;EACA,gBAAA;;EAEA,cAAA;;EAEA,aAAA;;EAEA,qBAAA;EHFyB;;;;;;EGSzB,SAAA;AAAA;;;;AF3EF;UEkFiB,eAAA;;EAEf,QAAA;EF/EE;EEiFF,aAAA;EFhFmB;;;AAGrB;;EEmFE,UAAA;EFjFwB;EEmFxB,WAAA;EFnFA;EEqFA,gBAAA;EFrFwB;EEuFxB,MAAA,EAAQ,UAAA,CAAW,YAAA;AAAA;;;;;;;;iBAUC,SAAA,MAAA,CAAgB,OAAA,EAAS,gBAAA,CAAiB,IAAA,IAAQ,OAAA,CAAQ,eAAA;;;;;;;;;;;;;UC9F/D,oBAAA;EACf,EAAA;EACA,OAAA,EAAS,oBAAA;EACT,cAAA,CAAe,IAAA,EAAM,iBAAA;AAAA;;;;;;;cASV,0BAAA,EAA0B,CAAA,CAAA,SAAA;;;;;;;KAG3B,oBAAA,GAAuB,CAAA,CAAE,KAAA,QAAa,0BAAA;;;;;;;;;;;;cAarC,aAAA,EAAe,aAAA,CAAc,oBAAA;;;;;;;KAa9B,gBAAA,SAAyB,YAAA,GAAe,OAAA,CAAQ,YAAA;;;;;;;;;;;;;;;;;;;;;;;;;;KA2BhD,gBAAA,IACV,UAAA,aACG,OAAA;EAAU,SAAA;EAAmB,OAAA,SAAgB,OAAA;AAAA;AAAA,UAEjC,sBAAA;EJpBwB;EIsBvC,UAAA,EAAY,eAAA;EJtBmC;EIwB/C,UAAA,EAAY,iBAAA;EJxBsD;EI0BlE,QAAA,EAAU,gBAAA;;EAEV,OAAA;;AH9FF;;;;EGoGE,eAAA,GAAkB,gBAAA;EH9Fb;EGgGL,MAAA,GAAS,MAAA;AAAA;;AH7FX;;;;;;;;;iBG0GgB,sBAAA,CACd,MAAA,EAAQ,sBAAA,IACN,GAAA,EAAK,oBAAA,KAAyB,OAAA;AAAA,UAoNjB,4BAAA;EACf,UAAA,EAAY,eAAA;EHpTU;EGsTtB,MAAA,GAAS,MAAA;AAAA;;;;;;;;;;;AHjTX;;;;;AAQA;;;;;AACA;;;;;AAYA;;;;;;;;;;;;iBGoUgB,4BAAA,CACd,MAAA,EAAQ,4BAAA,GACP,qBAAA"}
1
+ {"version":3,"file":"worker-B7ADOFEV.d.mts","names":[],"sources":["../src/entities/import-run.ts","../src/error-tracker.ts","../src/streaming.ts","../src/runner.ts","../src/worker.ts"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@murumets-ee/imports",
3
- "version": "0.15.4",
3
+ "version": "0.16.1",
4
4
  "license": "Elastic-2.0",
5
5
  "type": "module",
6
6
  "exports": {
@@ -40,13 +40,13 @@
40
40
  "csv-parse": "^5.5.3",
41
41
  "drizzle-orm": "^0.45.2",
42
42
  "zod": "^3.24.1",
43
- "@murumets-ee/core": "0.15.4",
44
- "@murumets-ee/db": "0.15.4",
45
- "@murumets-ee/entity": "0.15.4",
46
- "@murumets-ee/logging": "0.15.4",
47
- "@murumets-ee/queue": "0.15.4",
48
- "@murumets-ee/search-elasticsearch": "0.15.4",
49
- "@murumets-ee/storage": "0.15.4"
43
+ "@murumets-ee/core": "0.16.1",
44
+ "@murumets-ee/db": "0.16.1",
45
+ "@murumets-ee/entity": "0.16.1",
46
+ "@murumets-ee/logging": "0.16.1",
47
+ "@murumets-ee/queue": "0.16.1",
48
+ "@murumets-ee/search-elasticsearch": "0.16.1",
49
+ "@murumets-ee/storage": "0.16.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@types/node": "^20.19.40",
@@ -1,2 +0,0 @@
1
- import{behavior as e,defineEntity as t,field as n}from"@murumets-ee/entity";const r=[`pending`,`running`,`succeeded`,`failed`,`cancelled`],i=t({name:`import_run`,fields:{label:n.text({required:!0,maxLength:255}),status:n.select({options:r,default:`pending`,indexed:!0}),filePath:n.text({required:!0,maxLength:2048}),transformName:n.text({required:!0,maxLength:128,indexed:!0}),params:n.json(),totals:n.json(),errorSummary:n.json(),startedAt:n.date(),finishedAt:n.date(),queueJobId:n.text({maxLength:64,indexed:!0})},behaviors:[e.auditable()],scope:`global`,admin:{group:`imports`,label:`Import runs`,labelSingular:`Import run`,icon:`upload`,hideFromMenu:!0}});var a=class{entries=new Map;register(e,t){if(this.entries.has(e))throw Error(`Transform "${e}" is already registered — two plugins cannot register the same transform name.`);this.entries.set(e,t)}get(e){return this.entries.get(e)}has(e){return this.entries.has(e)}list(){return Array.from(this.entries.keys()).sort()}clear(){this.entries.clear()}};const o=Symbol.for(`@murumets-ee/imports:transforms`);function s(){let e=globalThis,t=e[o];return t||(t=new a,e[o]=t),t}function c(e,t){s().register(e,t)}export{i as a,r as i,s as n,c as r,a as t};
2
- //# sourceMappingURL=transform-BUGBTotp.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"transform-BUGBTotp.mjs","names":[],"sources":["../src/entities/import-run.ts","../src/transform.ts"],"sourcesContent":["/**\n * ImportRun — one feed-import attempt. Tracks file, transform, status,\n * row totals, and the top error patterns surfaced by `ErrorTracker`.\n *\n * Per PLAN-ECOMMERCE.md PR 7 (PoC scope): generic plumbing entity. The\n * commerce-specific transform parameters (brandId, supplierId, supplier\n * code prefix) live in the opaque `params` JSONB so the imports package\n * stays free of commerce-domain dependencies. PR 8 wires a concrete\n * carmaker-feed transform that reads those params; PR 8a renders a\n * bespoke `/admin/commerce/imports` page over this entity's history.\n *\n * `hideFromMenu: true` because PR 7 ships only the entity + plumbing —\n * the operator-facing surface is PR 8a. The auto-EntityListPage at\n * `/admin/import_run` still resolves for direct lookups, but no sidebar\n * entry points to it.\n */\n\nimport { behavior, defineEntity, field } from '@murumets-ee/entity'\nimport type { AdminClient } from '@murumets-ee/entity/admin'\n\n/**\n * Lifecycle stages.\n *\n * - `pending` — row exists, queue job has been enqueued, worker hasn't picked it up yet.\n * - `running` — worker is streaming + bulk-writing.\n * - `succeeded` — worker finished cleanly (note: per-row failures still possible — see `totals.failed`).\n * - `failed` — worker threw and the queue marked the job dead. `errorSummary.fatal` carries the cause.\n * - `cancelled` — manual operator action via PR 8a (out of scope for PR 7).\n */\nexport const IMPORT_RUN_STATUSES = ['pending', 'running', 'succeeded', 'failed', 'cancelled'] as const\nexport type ImportRunStatus = (typeof IMPORT_RUN_STATUSES)[number]\n\nexport const ImportRun = defineEntity({\n name: 'import_run',\n fields: {\n /** Operator-visible label, e.g. `\"MERCEDES — ME_20251027075918.txt\"`. Free-form. */\n label: field.text({ required: true, maxLength: 255 }),\n /** See {@link IMPORT_RUN_STATUSES}. */\n status: field.select({\n options: IMPORT_RUN_STATUSES,\n default: 'pending',\n indexed: true,\n }),\n /**\n * Path or storage key of the uploaded feed file. Generic string —\n * could be `/var/lumi/uploads/<id>.txt` for the local-disk PoC or an\n * S3 object key once storage adapter integration lands. The worker\n * reads this with the configured `readFeed` resolver.\n */\n filePath: field.text({ required: true, maxLength: 2048 }),\n /**\n * Name of the registered transform applied to each row. Resolved at\n * worker-dispatch time against the transform registry contributed by\n * the consumer. PoC: `'commerce:carmaker-feed'`.\n */\n transformName: field.text({ required: true, maxLength: 128, indexed: true }),\n /**\n * Opaque per-transform parameters. The carmaker transform expects\n * `{ brandId, supplierId, codePrefix? }`. Validation happens inside\n * the transform — the imports package never inspects this shape so\n * a new transform with different params doesn't require a schema\n * migration.\n */\n params: field.json(),\n /**\n * Row counters: `{ submitted, succeeded, failed, skipped, batches }`.\n * Updated by the worker as each batch completes. Final values are\n * what the operator reads; intermediate progress comes from\n * `toolkit_jobs.progress` via the queue UI.\n */\n totals: field.json(),\n /**\n * Output of `ErrorTracker.getTopPatterns(totalRows)` — the top-50\n * error signatures with up to 5 sample rows each. Empty `[]` until\n * the worker writes it on completion.\n */\n errorSummary: field.json(),\n /** Set when the worker picks up the job. */\n startedAt: field.date(),\n /** Set when the worker finishes (success OR fatal failure). */\n finishedAt: field.date(),\n /**\n * `toolkit_jobs.id` of the queue job processing this run — link\n * back so PR 8a can show live progress without a second lookup.\n */\n queueJobId: field.text({ maxLength: 64, indexed: true }),\n },\n behaviors: [behavior.auditable()],\n scope: 'global',\n admin: {\n group: 'imports',\n label: 'Import runs',\n labelSingular: 'Import run',\n icon: 'upload',\n hideFromMenu: true,\n },\n})\n\nexport type ImportRunClient = AdminClient<typeof ImportRun.allFields>\n","/**\n * Per-feed transform plugin interface (PLAN-ECOMMERCE.md PR 7 / D14).\n *\n * The streaming reader reads raw rows out of the file (column key →\n * string). The transform turns one row into a typed `OutputDoc` ready\n * for the bulk-write surface (PoC: `PartsDocument` for the parts ES\n * index). The transform is what makes a generic feed-importer\n * commerce-aware — without it, the importer has no opinion on what a\n * row \"means\".\n *\n * Why an interface, not a function:\n * - The transform may need to reject a row (`RowSkip`) without that\n * counting as an error in `ErrorTracker` — e.g. blank lines, header\n * rows mistakenly retained, or \"this is a replacement-code marker\n * row, handled out-of-band\" decisions.\n * - The transform may need to fail a row (`RowError`) with a typed\n * error class so `ErrorTracker` collapses them by `errorType`.\n * - The transform may need access to the run-level params (brand /\n * supplier IDs, code prefix, batch ID) without those being\n * re-derived per row.\n *\n * PR 7 ships only the interface + a registry. PR 8 ships the first\n * concrete carmaker-feed transform.\n */\n\n/** Identifies which transform a registered handler implements. Stable across deploys. */\nexport type TransformName = string\n\n/** Per-run context provided to every row call. Kept narrow on purpose. */\nexport interface TransformContext {\n /** UUID of the `import_run` row. Forward to the output doc as `import_batch_id` for source attribution (D20). */\n importRunId: string\n /** Opaque per-run params copied from `import_run.params`. The transform validates the shape it expects. */\n params: Record<string, unknown>\n /** Operator-supplied label, useful for logging / debugging. */\n runLabel: string\n /** 1-based row number across the whole feed (header counts as row 0). Forward to `RowError.rowNumber`. */\n rowNumber: number\n}\n\n/**\n * One of: a successful `OutputDoc` keyed by a stable `id`, an\n * intentional `skip` (counted in `totals.skipped`), or a row-level\n * error (counted in `totals.failed` AND aggregated by `ErrorTracker`).\n *\n * The `id` field on `success` is used as the bulk-upsert primary key\n * (`_id` in ES). Per D4 the parts index uses\n * `<code_normalized>__<supplier_id>` so a re-run of the same feed\n * idempotently overwrites instead of duplicating.\n */\nexport type RowResult<TDoc> =\n | { kind: 'success'; id: string; doc: TDoc }\n | { kind: 'skip'; reason: string }\n | { kind: 'error'; error: RowError }\n\n/** Row-level error. The aggregator uses `(errorType, field, message)` as the dedup signature. */\nexport interface RowError {\n errorType: string\n message: string\n field?: string | undefined\n}\n\n/**\n * The contract every per-feed transform implements. Pure function of\n * `(row, ctx) → RowResult` — no I/O. Async only because the future\n * carmaker variant might consult an in-memory taxonomy lookup.\n */\nexport interface RowTransform<TDoc> {\n (rawRow: Record<string, string>, ctx: TransformContext): Promise<RowResult<TDoc>>\n}\n\n/**\n * Holds the registered transforms for the current process. Each plugin\n * that ships a transform calls {@link TransformRegistry.register} from\n * its `init` hook.\n *\n * Per CLAUDE.md \"Whitelist, don't blacklist\": the worker dispatches\n * by `import_run.transformName` against this registry. An unregistered\n * name fails the run rather than running with a default transform.\n *\n * **Production callers MUST go through {@link getTransformRegistry}**, not\n * `new TransformRegistry()`. The constructor is exposed only so tests\n * (and rare custom-worker embeds) can build an isolated instance and\n * inject it into {@link createRunImportHandler} directly. Two `new`\n * instances do NOT share state — registering a transform on one will\n * NOT make it visible to a worker resolving against the other. The\n * package's queue handler (registered by `imports()` plugin) always\n * resolves against the singleton.\n */\nexport class TransformRegistry {\n private readonly entries = new Map<TransformName, RowTransform<unknown>>()\n\n /**\n * Register a transform under a stable name. Throws on duplicate\n * names so two plugins can't silently overwrite each other (matches\n * the `SearchRegistry` pattern in `@murumets-ee/search`).\n */\n register<TDoc>(name: TransformName, transform: RowTransform<TDoc>): void {\n if (this.entries.has(name)) {\n throw new Error(\n `Transform \"${name}\" is already registered — two plugins cannot register the same transform name.`,\n )\n }\n this.entries.set(name, transform as RowTransform<unknown>)\n }\n\n /** Look up a transform by name. Returns `undefined` if no plugin has registered one. */\n get(name: TransformName): RowTransform<unknown> | undefined {\n return this.entries.get(name)\n }\n\n /** True iff a transform is registered for the given name. */\n has(name: TransformName): boolean {\n return this.entries.has(name)\n }\n\n /** All registered names. Useful for the admin Catalog tab and for tests. */\n list(): TransformName[] {\n return Array.from(this.entries.keys()).sort()\n }\n\n /** Drop all registrations. Tests only. */\n clear(): void {\n this.entries.clear()\n }\n}\n\n// ---------------------------------------------------------------------------\n// Process-global singleton (mirrors `@murumets-ee/queue`'s handler registry).\n// ---------------------------------------------------------------------------\n//\n// Why a singleton: PR 8's carmaker-feed transform lives in a different\n// plugin from `@murumets-ee/imports`. Under Next.js HMR, ONE of those\n// plugins might re-evaluate while the other does not. Holding the\n// registry on `globalThis` via `Symbol.for` means the same `Map`\n// instance is shared across module evaluations — the carmaker plugin's\n// `register` and the imports worker's `get` see each other regardless\n// of evaluation order. Same fix shape as the queue's handler-registry\n// HMR bug (#186).\n\nconst REGISTRY_KEY = Symbol.for('@murumets-ee/imports:transforms')\n\ninterface GlobalThisWithTransforms {\n [REGISTRY_KEY]?: TransformRegistry\n}\n\n/** Returns the singleton registry, creating it on first access. */\nexport function getTransformRegistry(): TransformRegistry {\n const g = globalThis as GlobalThisWithTransforms\n let reg = g[REGISTRY_KEY]\n if (!reg) {\n reg = new TransformRegistry()\n g[REGISTRY_KEY] = reg\n }\n return reg\n}\n\n/**\n * Convenience wrapper around `getTransformRegistry().register(name, fn)`.\n * Plugins that ship a transform call this from their `init` hook — same\n * shape as `@murumets-ee/queue/client`'s `registerJob`.\n */\nexport function registerImportTransform<TDoc>(\n name: TransformName,\n transform: RowTransform<TDoc>,\n): void {\n getTransformRegistry().register(name, transform)\n}\n"],"mappings":"4EA6BA,MAAa,EAAsB,CAAC,UAAW,UAAW,YAAa,SAAU,YAAY,CAGhF,EAAY,EAAa,CACpC,KAAM,aACN,OAAQ,CAEN,MAAO,EAAM,KAAK,CAAE,SAAU,GAAM,UAAW,IAAK,CAAC,CAErD,OAAQ,EAAM,OAAO,CACnB,QAAS,EACT,QAAS,UACT,QAAS,GACV,CAAC,CAOF,SAAU,EAAM,KAAK,CAAE,SAAU,GAAM,UAAW,KAAM,CAAC,CAMzD,cAAe,EAAM,KAAK,CAAE,SAAU,GAAM,UAAW,IAAK,QAAS,GAAM,CAAC,CAQ5E,OAAQ,EAAM,MAAM,CAOpB,OAAQ,EAAM,MAAM,CAMpB,aAAc,EAAM,MAAM,CAE1B,UAAW,EAAM,MAAM,CAEvB,WAAY,EAAM,MAAM,CAKxB,WAAY,EAAM,KAAK,CAAE,UAAW,GAAI,QAAS,GAAM,CAAC,CACzD,CACD,UAAW,CAAC,EAAS,WAAW,CAAC,CACjC,MAAO,SACP,MAAO,CACL,MAAO,UACP,MAAO,cACP,cAAe,aACf,KAAM,SACN,aAAc,GACf,CACF,CAAC,CCPF,IAAa,EAAb,KAA+B,CAC7B,QAA2B,IAAI,IAO/B,SAAe,EAAqB,EAAqC,CACvE,GAAI,KAAK,QAAQ,IAAI,EAAK,CACxB,MAAU,MACR,cAAc,EAAK,gFACpB,CAEH,KAAK,QAAQ,IAAI,EAAM,EAAmC,CAI5D,IAAI,EAAwD,CAC1D,OAAO,KAAK,QAAQ,IAAI,EAAK,CAI/B,IAAI,EAA8B,CAChC,OAAO,KAAK,QAAQ,IAAI,EAAK,CAI/B,MAAwB,CACtB,OAAO,MAAM,KAAK,KAAK,QAAQ,MAAM,CAAC,CAAC,MAAM,CAI/C,OAAc,CACZ,KAAK,QAAQ,OAAO,GAiBxB,MAAM,EAAe,OAAO,IAAI,kCAAkC,CAOlE,SAAgB,GAA0C,CACxD,IAAM,EAAI,WACN,EAAM,EAAE,GAKZ,OAJK,IACH,EAAM,IAAI,EACV,EAAE,GAAgB,GAEb,EAQT,SAAgB,EACd,EACA,EACM,CACN,GAAsB,CAAC,SAAS,EAAM,EAAU"}