@murumets-ee/imports 0.32.3 → 0.33.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/dist/{cleanup-CFo7Pw0t.mjs → cleanup-WNOeVksI.mjs} +2 -2
- package/dist/{cleanup-CFo7Pw0t.mjs.map → cleanup-WNOeVksI.mjs.map} +1 -1
- package/dist/index.d.mts +121 -76
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -1
- package/dist/index.mjs.map +1 -0
- package/dist/mapping/index.d.mts +50 -19
- package/dist/mapping/index.d.mts.map +1 -1
- package/dist/mapping/index.mjs +1 -1
- package/dist/mapping/index.mjs.map +1 -1
- package/dist/{parts-schema-RI_ltubl.d.mts → parts-schema-B7Cbkl9T.d.mts} +18 -3
- package/dist/parts-schema-B7Cbkl9T.d.mts.map +1 -0
- package/dist/parts-schema-Cuaq2_At.mjs +2 -0
- package/dist/parts-schema-Cuaq2_At.mjs.map +1 -0
- package/dist/plugin.d.mts +1 -1
- package/dist/plugin.mjs +1 -1
- package/dist/runner-Cb1QPSt8.mjs +2 -0
- package/dist/runner-Cb1QPSt8.mjs.map +1 -0
- package/dist/schema-registry-DtXbLhOg.mjs +2 -0
- package/dist/schema-registry-DtXbLhOg.mjs.map +1 -0
- package/dist/storage-resolver.d.mts +1 -1
- package/dist/transform-DdhsE3ZI.mjs +2 -0
- package/dist/transform-DdhsE3ZI.mjs.map +1 -0
- package/dist/transforms.d.mts +1 -1
- package/dist/transforms.mjs +1 -1
- package/dist/{worker-C-5Xqf8_.d.mts → worker-D2Pf-R9o.d.mts} +31 -3
- package/dist/worker-D2Pf-R9o.d.mts.map +1 -0
- package/dist/worker.d.mts +1 -1
- package/dist/worker.mjs +1 -1
- package/dist/worker.mjs.map +1 -1
- package/dist/{xlsx-reader-B2_F37jA.mjs → xlsx-reader-Co4ulrnu.mjs} +2 -2
- package/dist/{xlsx-reader-B2_F37jA.mjs.map → xlsx-reader-Co4ulrnu.mjs.map} +1 -1
- package/package.json +8 -8
- package/dist/parts-schema-DQeMGPBC.mjs +0 -2
- package/dist/parts-schema-DQeMGPBC.mjs.map +0 -1
- package/dist/parts-schema-RI_ltubl.d.mts.map +0 -1
- package/dist/runner-DvKNfQJO.mjs +0 -2
- package/dist/runner-DvKNfQJO.mjs.map +0 -1
- package/dist/transform-DloItB80.mjs +0 -2
- package/dist/transform-DloItB80.mjs.map +0 -1
- package/dist/worker-C-5Xqf8_.d.mts.map +0 -1
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{t as e}from"./rolldown-runtime-DK3Fl9T5.mjs";import{getImportsStorage as t}from"./storage.mjs";var n=e({cleanupOrphanedImports:()=>a,cleanupProcessedImports:()=>s});const r=1e3;function i(e){let{ttlOrRetention:t,ttlLabel:n,limit:i=100}=e;if(!Number.isFinite(t)||t<0)throw Error(`imports cleanup: ${n} must be a finite, non-negative number (got ${t})`);if(!Number.isFinite(i)||!Number.isInteger(i)||i<1||i>r)throw Error(`imports cleanup: limit must be an integer in [1, ${r}] (got ${i})`);return{ttlOrRetention:t,limit:i}}async function a(e){let{app:n,bucket:r,logger:a}=e,o=n.db.readWrite,{ttlOrRetention:s,limit:c}=i({ttlOrRetention:e.ttlHours,ttlLabel:`ttlHours`,...e.limit!==void 0&&{limit:e.limit}}),{and:l,asc:u,eq:d,isNull:f,lt:p,getTableColumns:m}=await import(`drizzle-orm`),{toolkitFiles:h}=await import(`@murumets-ee/storage`),{createAdminClient:g}=await import(`@murumets-ee/core/clients`),{ImportRun:_}=await import(`./transform-
|
|
2
|
-
//# sourceMappingURL=cleanup-
|
|
1
|
+
import{t as e}from"./rolldown-runtime-DK3Fl9T5.mjs";import{getImportsStorage as t}from"./storage.mjs";var n=e({cleanupOrphanedImports:()=>a,cleanupProcessedImports:()=>s});const r=1e3;function i(e){let{ttlOrRetention:t,ttlLabel:n,limit:i=100}=e;if(!Number.isFinite(t)||t<0)throw Error(`imports cleanup: ${n} must be a finite, non-negative number (got ${t})`);if(!Number.isFinite(i)||!Number.isInteger(i)||i<1||i>r)throw Error(`imports cleanup: limit must be an integer in [1, ${r}] (got ${i})`);return{ttlOrRetention:t,limit:i}}async function a(e){let{app:n,bucket:r,logger:a}=e,o=n.db.readWrite,{ttlOrRetention:s,limit:c}=i({ttlOrRetention:e.ttlHours,ttlLabel:`ttlHours`,...e.limit!==void 0&&{limit:e.limit}}),{and:l,asc:u,eq:d,isNull:f,lt:p,getTableColumns:m}=await import(`drizzle-orm`),{toolkitFiles:h}=await import(`@murumets-ee/storage`),{createAdminClient:g}=await import(`@murumets-ee/core/clients`),{ImportRun:_}=await import(`./transform-DdhsE3ZI.mjs`).then(e=>e.o),v=g(_).getTable(),y=m(h),b=y.key,x=y.bucket,S=y.createdAt;if(!b||!x||!S)throw Error(`cleanupOrphanedImports: toolkit_files schema missing expected columns (key/bucket/createdAt)`);let C=m(v),w=C.filePath,T=C.id;if(!w||!T)throw Error(`cleanupOrphanedImports: import_run schema missing expected columns (filePath/id)`);let E=new Date(Date.now()-s*60*60*1e3),D=await o.select({key:b}).from(h).leftJoin(v,d(w,b)).where(l(d(x,r),p(S,E),f(T))).orderBy(u(S)).limit(c);if(D.length===0)return a.debug({bucket:r,ttlHours:s},`No orphaned imports uploads found`),0;let O=await t(),k=0;for(let e of D){let t=e.key;try{await O.delete(t),k++}catch(e){a.warn({key:t,err:e},`Failed to delete orphaned imports upload`)}}return a.info({cleaned:k,total:D.length,bucket:r,ttlHours:s},`Cleaned up orphaned imports uploads`),k}const o=[`succeeded`,`failed`,`cancelled`];async function s(e){let{app:n,bucket:r,logger:a}=e,s=n.db.readWrite,{ttlOrRetention:c,limit:l}=i({ttlOrRetention:e.retentionDays,ttlLabel:`retentionDays`,...e.limit!==void 0&&{limit:e.limit}}),{and:u,asc:d,eq:f,getTableColumns:p,inArray:m,lt:h}=await import(`drizzle-orm`),{toolkitFiles:g}=await import(`@murumets-ee/storage`),{createAdminClient:_}=await import(`@murumets-ee/core/clients`),{ImportRun:v}=await import(`./transform-DdhsE3ZI.mjs`).then(e=>e.o),y=_(v).getTable(),b=p(g),x=b.key,S=b.bucket;if(!x||!S)throw Error(`cleanupProcessedImports: toolkit_files schema missing expected columns (key/bucket)`);let C=p(y),w=C.filePath,T=C.status,E=C.finishedAt;if(!w||!T||!E)throw Error(`cleanupProcessedImports: import_run schema missing expected columns (filePath/status/finishedAt)`);let D=new Date(Date.now()-c*24*60*60*1e3),O=await s.selectDistinctOn([x],{key:x}).from(g).innerJoin(y,f(w,x)).where(u(f(S,r),m(T,[...o]),h(E,D))).orderBy(d(x),d(E)).limit(l);if(O.length===0)return a.debug({bucket:r,retentionDays:c},`No processed-imports uploads ready for cleanup`),0;let k=await t(),A=0;for(let e of O){let t=e.key;try{await k.delete(t),A++}catch(e){a.warn({key:t,err:e},`Failed to delete processed-imports upload`)}}return a.info({cleaned:A,total:O.length,bucket:r,retentionDays:c},`Cleaned up processed imports uploads`),A}export{s as n,n as r,a as t};
|
|
2
|
+
//# sourceMappingURL=cleanup-WNOeVksI.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cleanup-CFo7Pw0t.mjs","names":[],"sources":["../src/admin/cleanup.ts"],"sourcesContent":["/**\n * Cleanup orphaned imports uploads.\n *\n * Finds rows in `toolkit_files` within the configured imports bucket\n * that:\n * - were created more than `ttlHours` ago, AND\n * - have no matching `import_run.filePath` reference\n *\n * For each match, calls `StorageClient.delete(key)` — which removes\n * BOTH the adapter bytes (local file / R2 object) AND the\n * `toolkit_files` metadata row in one transactional unit (see\n * `@murumets-ee/storage` `StorageClient.delete`).\n *\n * Why this exists: the Tus upload flow stores bytes + metadata BEFORE\n * the client's downstream `register` step inserts an `import_run` row.\n * If the register call fails (validation, MIME mismatch, network\n * blip, agent abandons the workflow), the storage row is left\n * dangling. The commerce-side register handler does opportunistic\n * cleanup on its OWN failure paths, but rejections from the Zod\n * schema (before the handler runs) and rejections where the agent\n * never calls register at all both leak.\n *\n * Scheduling: driven by the `defineJob({ schedule: '@every 1h' })`\n * declaration in `plugin.ts`. The queue worker bootstraps a\n * `toolkit_scheduled_jobs` row for this job at startup and its cron\n * loop (`processDueSchedules`) fires the registered handler on each\n * tick. The handler does NOT re-enqueue itself — the schedule is\n * driven by the worker, not the handler.\n */\n\nimport type { ToolkitApp } from '@murumets-ee/core'\nimport type { Logger } from '@murumets-ee/logging'\nimport { getImportsStorage } from '../storage.js'\n\nexport interface CleanupOrphanedImportsArgs {\n /** Toolkit app (provides db). */\n app: ToolkitApp\n /**\n * Bucket name the imports flow uses. Rows in `toolkit_files` are\n * filtered to this bucket so the cleanup doesn't touch media,\n * ticketing attachments, or any other consumer's uploads even if\n * they share the same Postgres instance.\n */\n bucket: string\n /**\n * Hours after which an unreferenced upload is considered orphaned.\n * Operator-grace window: should be long enough that a normal\n * upload-then-register round-trip never wipes a fresh upload, but\n * short enough that storage waste from failed uploads doesn't\n * accumulate. 2 hours is the default in the imports plugin\n * config — calibrated against the typical \"operator clicks\n * upload, walks away, comes back to check\" workflow.\n */\n ttlHours: number\n /**\n * Maximum rows to process per run. Bounded so a sudden flood of\n * orphans (e.g. after a brand-new supplier with unrecognised\n * filenames floods the queue with rejected uploads) doesn't\n * block the queue worker on a single multi-hour cleanup.\n */\n limit?: number\n /** Logger for diagnostics. */\n logger: Pick<Logger, 'debug' | 'info' | 'warn'>\n}\n\n/** Default per-run cap. Tuned against ticketing's `cleanupOrphanedUploads` (also 100). */\nconst DEFAULT_LIMIT = 100\n\n/** Hard cap on per-run limit. Protects against pathological caller input. */\nconst MAX_LIMIT = 1000\n\n/**\n * Validate the numeric inputs every cleanup helper accepts. Returns\n * the sanitised values OR throws — the queue worker logs thrown\n * errors against the scheduled-job row so a misconfigured TTL\n * doesn't silently delete every bucket file (which would happen if\n * a negative `ttlHours` reached the date arithmetic and pushed the\n * cutoff into the future).\n */\nfunction validateCleanupArgs(\n raw: { ttlOrRetention: number; ttlLabel: string; limit?: number },\n): { ttlOrRetention: number; limit: number } {\n const { ttlOrRetention, ttlLabel, limit = DEFAULT_LIMIT } = raw\n if (!Number.isFinite(ttlOrRetention) || ttlOrRetention < 0) {\n throw new Error(\n `imports cleanup: ${ttlLabel} must be a finite, non-negative number (got ${ttlOrRetention})`,\n )\n }\n if (!Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1 || limit > MAX_LIMIT) {\n throw new Error(\n `imports cleanup: limit must be an integer in [1, ${MAX_LIMIT}] (got ${limit})`,\n )\n }\n return { ttlOrRetention, limit }\n}\n\n/**\n * Run one cleanup pass. Returns the count of rows actually deleted\n * (which may be less than the count of orphans found if some\n * `storage.delete` calls failed — see the `warn` log inside the loop).\n */\nexport async function cleanupOrphanedImports(args: CleanupOrphanedImportsArgs): Promise<number> {\n const { app, bucket, logger } = args\n const db = app.db.readWrite\n const { ttlOrRetention: ttlHours, limit } = validateCleanupArgs({\n ttlOrRetention: args.ttlHours,\n ttlLabel: 'ttlHours',\n // `exactOptionalPropertyTypes` rejects an explicit `undefined`\n // for the optional `limit?: number` slot. Spread the field only\n // when defined so the validator's default-via-destructure kicks in.\n ...(args.limit !== undefined && { limit: args.limit }),\n })\n\n // Cross-table query: `toolkit_files` LEFT JOIN `import_run` — same\n // documented exception for cross-entity JOINs that ticketing's\n // cleanup uses. Two-table joins can't go through a single-entity\n // AdminClient (per CLAUDE.md \"Database Queries\").\n const { and, asc, eq, isNull, lt, getTableColumns } = await import('drizzle-orm')\n const { toolkitFiles } = await import('@murumets-ee/storage')\n const { createAdminClient } = await import('@murumets-ee/core/clients')\n const { ImportRun } = await import('../entities/import-run.js')\n type AnyPgColumn = import('drizzle-orm/pg-core').AnyPgColumn\n\n const importRunsTable = createAdminClient(ImportRun).getTable()\n\n // Both `toolkitFiles` and the `importRunsTable` are exported as\n // generic PgTables so direct dot-access widens to `T | undefined`.\n // Pull the column refs through `getTableColumns` (same shape as\n // ticketing's cleanup + the entity-version helper).\n const fileCols = getTableColumns(toolkitFiles) as Record<string, AnyPgColumn>\n const fileKey = fileCols.key\n const fileBucket = fileCols.bucket\n const fileCreatedAt = fileCols.createdAt\n if (!fileKey || !fileBucket || !fileCreatedAt) {\n throw new Error(\n 'cleanupOrphanedImports: toolkit_files schema missing expected columns (key/bucket/createdAt)',\n )\n }\n const importRunCols = getTableColumns(importRunsTable) as Record<string, AnyPgColumn>\n const irFilePath = importRunCols.filePath\n const irId = importRunCols.id\n if (!irFilePath || !irId) {\n throw new Error(\n 'cleanupOrphanedImports: import_run schema missing expected columns (filePath/id)',\n )\n }\n\n const cutoff = new Date(Date.now() - ttlHours * 60 * 60 * 1000)\n\n // Find toolkit_files rows in the imports bucket, older than cutoff,\n // where the storage key is NOT referenced by any import_run row.\n // ORDER BY createdAt ASC delivers strict FIFO so a sustained\n // orphan stream makes deterministic progress on the oldest rows\n // first — without the orderBy, Postgres returns rows in heap\n // order and a LIMIT 100 sweep could keep recycling the same\n // young rows while ancient orphans never get touched.\n const orphans = await db\n .select({ key: fileKey })\n .from(toolkitFiles)\n .leftJoin(importRunsTable, eq(irFilePath, fileKey))\n .where(and(eq(fileBucket, bucket), lt(fileCreatedAt, cutoff), isNull(irId)))\n .orderBy(asc(fileCreatedAt))\n .limit(limit)\n\n if (orphans.length === 0) {\n logger.debug({ bucket, ttlHours }, 'No orphaned imports uploads found')\n return 0\n }\n\n const storage = await getImportsStorage()\n let cleaned = 0\n for (const row of orphans) {\n const key = row.key as unknown as string\n try {\n await storage.delete(key)\n cleaned++\n } catch (err) {\n logger.warn({ key, err }, 'Failed to delete orphaned imports upload')\n }\n }\n\n logger.info(\n { cleaned, total: orphans.length, bucket, ttlHours },\n 'Cleaned up orphaned imports uploads',\n )\n return cleaned\n}\n\n/** Statuses considered \"done\" for the post-success/failure cleanup. */\nconst TERMINAL_IMPORT_RUN_STATUSES = ['succeeded', 'failed', 'cancelled'] as const\n\nexport interface CleanupProcessedImportsArgs {\n /** Toolkit app (provides db). */\n app: ToolkitApp\n /** Bucket name to scope the cleanup. Same semantics as `cleanupOrphanedImports`. */\n bucket: string\n /**\n * Days after which a terminal-status `import_run`'s source file is\n * deleted. Operator-grace window: long enough that an operator can\n * inspect the source file if a transform bug surfaces shortly after\n * a successful import; short enough that storage doesn't accumulate\n * months of supplier feeds.\n *\n * The plugin default is 7. Set to 0 to delete the source file\n * immediately at the next cleanup tick after the import row\n * reaches terminal status — useful when storage cost matters more\n * than forensic reprocess capability.\n */\n retentionDays: number\n /** Max rows per run. Defaults to 100 (same cap as the orphan sweep). */\n limit?: number\n logger: Pick<Logger, 'debug' | 'info' | 'warn'>\n}\n\n/**\n * Cleanup source files for `import_run`s that finished (succeeded /\n * failed / cancelled) more than `retentionDays` days ago.\n *\n * Counterpart to `cleanupOrphanedImports`: that one targets uploads\n * with NO matching `import_run` (handshake never completed); this one\n * targets uploads whose `import_run` row IS present and terminal.\n * Together they cover every \"useless storage row\" case the imports\n * flow can produce.\n *\n * Deletes ONLY the storage row (via `StorageClient.delete`); the\n * `import_run` row is kept so the History panel + audit trail still\n * surface the past run. The `import_run.filePath` value points at a\n * now-deleted key after this runs — consumers that try to re-download\n * the source file (e.g. a future \"reprocess\" button) will get a\n * `getMetadata` → null and should show \"source file unavailable\" in\n * the UI rather than crashing.\n */\nexport async function cleanupProcessedImports(args: CleanupProcessedImportsArgs): Promise<number> {\n const { app, bucket, logger } = args\n const db = app.db.readWrite\n const { ttlOrRetention: retentionDays, limit } = validateCleanupArgs({\n ttlOrRetention: args.retentionDays,\n ttlLabel: 'retentionDays',\n // `exactOptionalPropertyTypes` rejects an explicit `undefined`\n // for the optional `limit?: number` slot. Spread the field only\n // when defined so the validator's default-via-destructure kicks in.\n ...(args.limit !== undefined && { limit: args.limit }),\n })\n\n const { and, asc, eq, getTableColumns, inArray, lt } = await import('drizzle-orm')\n const { toolkitFiles } = await import('@murumets-ee/storage')\n const { createAdminClient } = await import('@murumets-ee/core/clients')\n const { ImportRun } = await import('../entities/import-run.js')\n type AnyPgColumn = import('drizzle-orm/pg-core').AnyPgColumn\n\n const importRunsTable = createAdminClient(ImportRun).getTable()\n\n const fileCols = getTableColumns(toolkitFiles) as Record<string, AnyPgColumn>\n const fileKey = fileCols.key\n const fileBucket = fileCols.bucket\n if (!fileKey || !fileBucket) {\n throw new Error(\n 'cleanupProcessedImports: toolkit_files schema missing expected columns (key/bucket)',\n )\n }\n const importRunCols = getTableColumns(importRunsTable) as Record<string, AnyPgColumn>\n const irFilePath = importRunCols.filePath\n const irStatus = importRunCols.status\n const irFinishedAt = importRunCols.finishedAt\n if (!irFilePath || !irStatus || !irFinishedAt) {\n throw new Error(\n 'cleanupProcessedImports: import_run schema missing expected columns (filePath/status/finishedAt)',\n )\n }\n\n const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000)\n\n // INNER JOIN so we only consider keys that DO have a matching\n // import_run row — the inverse of the orphan-cleanup query. Filter\n // on terminal status + finishedAt cutoff so a long-running batch\n // doesn't get its still-being-processed file ripped out from under\n // the worker.\n //\n // `selectDistinctOn([fileKey])` is the right primitive here:\n //\n // (a) **Dedupe before LIMIT.** `toolkit_files.key` is UNIQUE\n // within its own table, but `import_run.filePath` has no\n // unique constraint — a future re-run / retry flow that\n // creates two terminal `import_run` rows pointing at the\n // same `storageKey` would make the INNER JOIN project the\n // key twice. Without dedupe in SQL, a pathological cluster\n // of 100 duplicates sharing 1 storageKey would burn the\n // LIMIT cap to clean 1 file.\n //\n // (b) **Deterministic ordering** (NOT strict global FIFO).\n // `DISTINCT ON` requires the ORDER BY to start with the\n // distinct columns (Postgres rule). So we sort by `fileKey`\n // first, then `irFinishedAt` within each key group. The\n // LIMIT then picks the first N keys in `fileKey` order —\n // i.e. alphabetical by storage path, not globally oldest by\n // finishedAt. In practice this is roughly chronological\n // anyway: storage keys are `uploads/YYYY/MM/<uuid>/...` so\n // earlier months sort first. Within a month, ordering is\n // UUID-random — but every key eventually gets cleaned\n // (no starvation), so over ~10 ticks the entire backlog\n // drains either way. A subquery-wrapped \"true global FIFO\"\n // (DISTINCT ON inner, ORDER BY finishedAt outer) is\n // possible but adds complexity for marginal benefit; the\n // practical-FIFO-via-key-prefix property is acceptable.\n const candidates = await db\n .selectDistinctOn([fileKey], { key: fileKey })\n .from(toolkitFiles)\n .innerJoin(importRunsTable, eq(irFilePath, fileKey))\n .where(\n and(\n eq(fileBucket, bucket),\n inArray(irStatus, [...TERMINAL_IMPORT_RUN_STATUSES]),\n lt(irFinishedAt, cutoff),\n ),\n )\n .orderBy(asc(fileKey), asc(irFinishedAt))\n .limit(limit)\n\n if (candidates.length === 0) {\n logger.debug({ bucket, retentionDays }, 'No processed-imports uploads ready for cleanup')\n return 0\n }\n\n const storage = await getImportsStorage()\n let cleaned = 0\n for (const row of candidates) {\n const key = row.key as unknown as string\n try {\n await storage.delete(key)\n cleaned++\n } catch (err) {\n logger.warn({ key, err }, 'Failed to delete processed-imports upload')\n }\n }\n\n logger.info(\n { cleaned, total: candidates.length, bucket, retentionDays },\n 'Cleaned up processed imports uploads',\n )\n return cleaned\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cleanup-WNOeVksI.mjs","names":[],"sources":["../src/admin/cleanup.ts"],"sourcesContent":["/**\n * Cleanup orphaned imports uploads.\n *\n * Finds rows in `toolkit_files` within the configured imports bucket\n * that:\n * - were created more than `ttlHours` ago, AND\n * - have no matching `import_run.filePath` reference\n *\n * For each match, calls `StorageClient.delete(key)` — which removes\n * BOTH the adapter bytes (local file / R2 object) AND the\n * `toolkit_files` metadata row in one transactional unit (see\n * `@murumets-ee/storage` `StorageClient.delete`).\n *\n * Why this exists: the Tus upload flow stores bytes + metadata BEFORE\n * the client's downstream `register` step inserts an `import_run` row.\n * If the register call fails (validation, MIME mismatch, network\n * blip, agent abandons the workflow), the storage row is left\n * dangling. The commerce-side register handler does opportunistic\n * cleanup on its OWN failure paths, but rejections from the Zod\n * schema (before the handler runs) and rejections where the agent\n * never calls register at all both leak.\n *\n * Scheduling: driven by the `defineJob({ schedule: '@every 1h' })`\n * declaration in `plugin.ts`. The queue worker bootstraps a\n * `toolkit_scheduled_jobs` row for this job at startup and its cron\n * loop (`processDueSchedules`) fires the registered handler on each\n * tick. The handler does NOT re-enqueue itself — the schedule is\n * driven by the worker, not the handler.\n */\n\nimport type { ToolkitApp } from '@murumets-ee/core'\nimport type { Logger } from '@murumets-ee/logging'\nimport { getImportsStorage } from '../storage.js'\n\nexport interface CleanupOrphanedImportsArgs {\n /** Toolkit app (provides db). */\n app: ToolkitApp\n /**\n * Bucket name the imports flow uses. Rows in `toolkit_files` are\n * filtered to this bucket so the cleanup doesn't touch media,\n * ticketing attachments, or any other consumer's uploads even if\n * they share the same Postgres instance.\n */\n bucket: string\n /**\n * Hours after which an unreferenced upload is considered orphaned.\n * Operator-grace window: should be long enough that a normal\n * upload-then-register round-trip never wipes a fresh upload, but\n * short enough that storage waste from failed uploads doesn't\n * accumulate. 2 hours is the default in the imports plugin\n * config — calibrated against the typical \"operator clicks\n * upload, walks away, comes back to check\" workflow.\n */\n ttlHours: number\n /**\n * Maximum rows to process per run. Bounded so a sudden flood of\n * orphans (e.g. after a brand-new supplier with unrecognised\n * filenames floods the queue with rejected uploads) doesn't\n * block the queue worker on a single multi-hour cleanup.\n */\n limit?: number\n /** Logger for diagnostics. */\n logger: Pick<Logger, 'debug' | 'info' | 'warn'>\n}\n\n/** Default per-run cap. Tuned against ticketing's `cleanupOrphanedUploads` (also 100). */\nconst DEFAULT_LIMIT = 100\n\n/** Hard cap on per-run limit. Protects against pathological caller input. */\nconst MAX_LIMIT = 1000\n\n/**\n * Validate the numeric inputs every cleanup helper accepts. Returns\n * the sanitised values OR throws — the queue worker logs thrown\n * errors against the scheduled-job row so a misconfigured TTL\n * doesn't silently delete every bucket file (which would happen if\n * a negative `ttlHours` reached the date arithmetic and pushed the\n * cutoff into the future).\n */\nfunction validateCleanupArgs(\n raw: { ttlOrRetention: number; ttlLabel: string; limit?: number },\n): { ttlOrRetention: number; limit: number } {\n const { ttlOrRetention, ttlLabel, limit = DEFAULT_LIMIT } = raw\n if (!Number.isFinite(ttlOrRetention) || ttlOrRetention < 0) {\n throw new Error(\n `imports cleanup: ${ttlLabel} must be a finite, non-negative number (got ${ttlOrRetention})`,\n )\n }\n if (!Number.isFinite(limit) || !Number.isInteger(limit) || limit < 1 || limit > MAX_LIMIT) {\n throw new Error(\n `imports cleanup: limit must be an integer in [1, ${MAX_LIMIT}] (got ${limit})`,\n )\n }\n return { ttlOrRetention, limit }\n}\n\n/**\n * Run one cleanup pass. Returns the count of rows actually deleted\n * (which may be less than the count of orphans found if some\n * `storage.delete` calls failed — see the `warn` log inside the loop).\n */\nexport async function cleanupOrphanedImports(args: CleanupOrphanedImportsArgs): Promise<number> {\n const { app, bucket, logger } = args\n const db = app.db.readWrite\n const { ttlOrRetention: ttlHours, limit } = validateCleanupArgs({\n ttlOrRetention: args.ttlHours,\n ttlLabel: 'ttlHours',\n // `exactOptionalPropertyTypes` rejects an explicit `undefined`\n // for the optional `limit?: number` slot. Spread the field only\n // when defined so the validator's default-via-destructure kicks in.\n ...(args.limit !== undefined && { limit: args.limit }),\n })\n\n // Cross-table query: `toolkit_files` LEFT JOIN `import_run` — same\n // documented exception for cross-entity JOINs that ticketing's\n // cleanup uses. Two-table joins can't go through a single-entity\n // AdminClient (per CLAUDE.md \"Database Queries\").\n const { and, asc, eq, isNull, lt, getTableColumns } = await import('drizzle-orm')\n const { toolkitFiles } = await import('@murumets-ee/storage')\n const { createAdminClient } = await import('@murumets-ee/core/clients')\n const { ImportRun } = await import('../entities/import-run.js')\n type AnyPgColumn = import('drizzle-orm/pg-core').AnyPgColumn\n\n const importRunsTable = createAdminClient(ImportRun).getTable()\n\n // Both `toolkitFiles` and the `importRunsTable` are exported as\n // generic PgTables so direct dot-access widens to `T | undefined`.\n // Pull the column refs through `getTableColumns` (same shape as\n // ticketing's cleanup + the entity-version helper).\n const fileCols = getTableColumns(toolkitFiles) as Record<string, AnyPgColumn>\n const fileKey = fileCols.key\n const fileBucket = fileCols.bucket\n const fileCreatedAt = fileCols.createdAt\n if (!fileKey || !fileBucket || !fileCreatedAt) {\n throw new Error(\n 'cleanupOrphanedImports: toolkit_files schema missing expected columns (key/bucket/createdAt)',\n )\n }\n const importRunCols = getTableColumns(importRunsTable) as Record<string, AnyPgColumn>\n const irFilePath = importRunCols.filePath\n const irId = importRunCols.id\n if (!irFilePath || !irId) {\n throw new Error(\n 'cleanupOrphanedImports: import_run schema missing expected columns (filePath/id)',\n )\n }\n\n const cutoff = new Date(Date.now() - ttlHours * 60 * 60 * 1000)\n\n // Find toolkit_files rows in the imports bucket, older than cutoff,\n // where the storage key is NOT referenced by any import_run row.\n // ORDER BY createdAt ASC delivers strict FIFO so a sustained\n // orphan stream makes deterministic progress on the oldest rows\n // first — without the orderBy, Postgres returns rows in heap\n // order and a LIMIT 100 sweep could keep recycling the same\n // young rows while ancient orphans never get touched.\n const orphans = await db\n .select({ key: fileKey })\n .from(toolkitFiles)\n .leftJoin(importRunsTable, eq(irFilePath, fileKey))\n .where(and(eq(fileBucket, bucket), lt(fileCreatedAt, cutoff), isNull(irId)))\n .orderBy(asc(fileCreatedAt))\n .limit(limit)\n\n if (orphans.length === 0) {\n logger.debug({ bucket, ttlHours }, 'No orphaned imports uploads found')\n return 0\n }\n\n const storage = await getImportsStorage()\n let cleaned = 0\n for (const row of orphans) {\n const key = row.key as unknown as string\n try {\n await storage.delete(key)\n cleaned++\n } catch (err) {\n logger.warn({ key, err }, 'Failed to delete orphaned imports upload')\n }\n }\n\n logger.info(\n { cleaned, total: orphans.length, bucket, ttlHours },\n 'Cleaned up orphaned imports uploads',\n )\n return cleaned\n}\n\n/** Statuses considered \"done\" for the post-success/failure cleanup. */\nconst TERMINAL_IMPORT_RUN_STATUSES = ['succeeded', 'failed', 'cancelled'] as const\n\nexport interface CleanupProcessedImportsArgs {\n /** Toolkit app (provides db). */\n app: ToolkitApp\n /** Bucket name to scope the cleanup. Same semantics as `cleanupOrphanedImports`. */\n bucket: string\n /**\n * Days after which a terminal-status `import_run`'s source file is\n * deleted. Operator-grace window: long enough that an operator can\n * inspect the source file if a transform bug surfaces shortly after\n * a successful import; short enough that storage doesn't accumulate\n * months of supplier feeds.\n *\n * The plugin default is 7. Set to 0 to delete the source file\n * immediately at the next cleanup tick after the import row\n * reaches terminal status — useful when storage cost matters more\n * than forensic reprocess capability.\n */\n retentionDays: number\n /** Max rows per run. Defaults to 100 (same cap as the orphan sweep). */\n limit?: number\n logger: Pick<Logger, 'debug' | 'info' | 'warn'>\n}\n\n/**\n * Cleanup source files for `import_run`s that finished (succeeded /\n * failed / cancelled) more than `retentionDays` days ago.\n *\n * Counterpart to `cleanupOrphanedImports`: that one targets uploads\n * with NO matching `import_run` (handshake never completed); this one\n * targets uploads whose `import_run` row IS present and terminal.\n * Together they cover every \"useless storage row\" case the imports\n * flow can produce.\n *\n * Deletes ONLY the storage row (via `StorageClient.delete`); the\n * `import_run` row is kept so the History panel + audit trail still\n * surface the past run. The `import_run.filePath` value points at a\n * now-deleted key after this runs — consumers that try to re-download\n * the source file (e.g. a future \"reprocess\" button) will get a\n * `getMetadata` → null and should show \"source file unavailable\" in\n * the UI rather than crashing.\n */\nexport async function cleanupProcessedImports(args: CleanupProcessedImportsArgs): Promise<number> {\n const { app, bucket, logger } = args\n const db = app.db.readWrite\n const { ttlOrRetention: retentionDays, limit } = validateCleanupArgs({\n ttlOrRetention: args.retentionDays,\n ttlLabel: 'retentionDays',\n // `exactOptionalPropertyTypes` rejects an explicit `undefined`\n // for the optional `limit?: number` slot. Spread the field only\n // when defined so the validator's default-via-destructure kicks in.\n ...(args.limit !== undefined && { limit: args.limit }),\n })\n\n const { and, asc, eq, getTableColumns, inArray, lt } = await import('drizzle-orm')\n const { toolkitFiles } = await import('@murumets-ee/storage')\n const { createAdminClient } = await import('@murumets-ee/core/clients')\n const { ImportRun } = await import('../entities/import-run.js')\n type AnyPgColumn = import('drizzle-orm/pg-core').AnyPgColumn\n\n const importRunsTable = createAdminClient(ImportRun).getTable()\n\n const fileCols = getTableColumns(toolkitFiles) as Record<string, AnyPgColumn>\n const fileKey = fileCols.key\n const fileBucket = fileCols.bucket\n if (!fileKey || !fileBucket) {\n throw new Error(\n 'cleanupProcessedImports: toolkit_files schema missing expected columns (key/bucket)',\n )\n }\n const importRunCols = getTableColumns(importRunsTable) as Record<string, AnyPgColumn>\n const irFilePath = importRunCols.filePath\n const irStatus = importRunCols.status\n const irFinishedAt = importRunCols.finishedAt\n if (!irFilePath || !irStatus || !irFinishedAt) {\n throw new Error(\n 'cleanupProcessedImports: import_run schema missing expected columns (filePath/status/finishedAt)',\n )\n }\n\n const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1000)\n\n // INNER JOIN so we only consider keys that DO have a matching\n // import_run row — the inverse of the orphan-cleanup query. Filter\n // on terminal status + finishedAt cutoff so a long-running batch\n // doesn't get its still-being-processed file ripped out from under\n // the worker.\n //\n // `selectDistinctOn([fileKey])` is the right primitive here:\n //\n // (a) **Dedupe before LIMIT.** `toolkit_files.key` is UNIQUE\n // within its own table, but `import_run.filePath` has no\n // unique constraint — a future re-run / retry flow that\n // creates two terminal `import_run` rows pointing at the\n // same `storageKey` would make the INNER JOIN project the\n // key twice. Without dedupe in SQL, a pathological cluster\n // of 100 duplicates sharing 1 storageKey would burn the\n // LIMIT cap to clean 1 file.\n //\n // (b) **Deterministic ordering** (NOT strict global FIFO).\n // `DISTINCT ON` requires the ORDER BY to start with the\n // distinct columns (Postgres rule). So we sort by `fileKey`\n // first, then `irFinishedAt` within each key group. The\n // LIMIT then picks the first N keys in `fileKey` order —\n // i.e. alphabetical by storage path, not globally oldest by\n // finishedAt. In practice this is roughly chronological\n // anyway: storage keys are `uploads/YYYY/MM/<uuid>/...` so\n // earlier months sort first. Within a month, ordering is\n // UUID-random — but every key eventually gets cleaned\n // (no starvation), so over ~10 ticks the entire backlog\n // drains either way. A subquery-wrapped \"true global FIFO\"\n // (DISTINCT ON inner, ORDER BY finishedAt outer) is\n // possible but adds complexity for marginal benefit; the\n // practical-FIFO-via-key-prefix property is acceptable.\n const candidates = await db\n .selectDistinctOn([fileKey], { key: fileKey })\n .from(toolkitFiles)\n .innerJoin(importRunsTable, eq(irFilePath, fileKey))\n .where(\n and(\n eq(fileBucket, bucket),\n inArray(irStatus, [...TERMINAL_IMPORT_RUN_STATUSES]),\n lt(irFinishedAt, cutoff),\n ),\n )\n .orderBy(asc(fileKey), asc(irFinishedAt))\n .limit(limit)\n\n if (candidates.length === 0) {\n logger.debug({ bucket, retentionDays }, 'No processed-imports uploads ready for cleanup')\n return 0\n }\n\n const storage = await getImportsStorage()\n let cleaned = 0\n for (const row of candidates) {\n const key = row.key as unknown as string\n try {\n await storage.delete(key)\n cleaned++\n } catch (err) {\n logger.warn({ key, err }, 'Failed to delete processed-imports upload')\n }\n }\n\n logger.info(\n { cleaned, total: candidates.length, bucket, retentionDays },\n 'Cleaned up processed imports uploads',\n )\n return cleaned\n}\n"],"mappings":"4KAkEA,MAGM,EAAY,IAUlB,SAAS,EACP,EAC2C,CAC3C,GAAM,CAAE,iBAAgB,WAAU,QAAQ,KAAkB,EAC5D,GAAI,CAAC,OAAO,SAAS,CAAc,GAAK,EAAiB,EACvD,MAAU,MACR,oBAAoB,EAAS,8CAA8C,EAAe,EAC5F,EAEF,GAAI,CAAC,OAAO,SAAS,CAAK,GAAK,CAAC,OAAO,UAAU,CAAK,GAAK,EAAQ,GAAK,EAAQ,EAC9E,MAAU,MACR,oDAAoD,EAAU,SAAS,EAAM,EAC/E,EAEF,MAAO,CAAE,iBAAgB,OAAM,CACjC,CAOA,eAAsB,EAAuB,EAAmD,CAC9F,GAAM,CAAE,MAAK,SAAQ,UAAW,EAC1B,EAAK,EAAI,GAAG,UACZ,CAAE,eAAgB,EAAU,SAAU,EAAoB,CAC9D,eAAgB,EAAK,SACrB,SAAU,WAIV,GAAI,EAAK,QAAU,IAAA,IAAa,CAAE,MAAO,EAAK,KAAM,CACtD,CAAC,EAMK,CAAE,MAAK,MAAK,KAAI,SAAQ,KAAI,mBAAoB,MAAM,OAAO,eAC7D,CAAE,gBAAiB,MAAM,OAAO,wBAChC,CAAE,qBAAsB,MAAM,OAAO,6BACrC,CAAE,aAAc,MAAM,OAAO,2BAA4B,CAAA,KAAA,GAAA,EAAA,CAAA,EAGzD,EAAkB,EAAkB,CAAS,CAAC,CAAC,SAAS,EAMxD,EAAW,EAAgB,CAAY,EACvC,EAAU,EAAS,IACnB,EAAa,EAAS,OACtB,EAAgB,EAAS,UAC/B,GAAI,CAAC,GAAW,CAAC,GAAc,CAAC,EAC9B,MAAU,MACR,8FACF,EAEF,IAAM,EAAgB,EAAgB,CAAe,EAC/C,EAAa,EAAc,SAC3B,EAAO,EAAc,GAC3B,GAAI,CAAC,GAAc,CAAC,EAClB,MAAU,MACR,kFACF,EAGF,IAAM,EAAS,IAAI,KAAK,KAAK,IAAI,EAAI,EAAW,GAAK,GAAK,GAAI,EASxD,EAAU,MAAM,EACnB,OAAO,CAAE,IAAK,CAAQ,CAAC,CAAC,CACxB,KAAK,CAAY,CAAC,CAClB,SAAS,EAAiB,EAAG,EAAY,CAAO,CAAC,CAAC,CAClD,MAAM,EAAI,EAAG,EAAY,CAAM,EAAG,EAAG,EAAe,CAAM,EAAG,EAAO,CAAI,CAAC,CAAC,CAAC,CAC3E,QAAQ,EAAI,CAAa,CAAC,CAAC,CAC3B,MAAM,CAAK,EAEd,GAAI,EAAQ,SAAW,EAErB,OADA,EAAO,MAAM,CAAE,SAAQ,UAAS,EAAG,mCAAmC,EAC/D,EAGT,IAAM,EAAU,MAAM,EAAkB,EACpC,EAAU,EACd,IAAK,IAAM,KAAO,EAAS,CACzB,IAAM,EAAM,EAAI,IAChB,GAAI,CACF,MAAM,EAAQ,OAAO,CAAG,EACxB,GACF,OAAS,EAAK,CACZ,EAAO,KAAK,CAAE,MAAK,KAAI,EAAG,0CAA0C,CACtE,CACF,CAMA,OAJA,EAAO,KACL,CAAE,UAAS,MAAO,EAAQ,OAAQ,SAAQ,UAAS,EACnD,qCACF,EACO,CACT,CAGA,MAAM,EAA+B,CAAC,YAAa,SAAU,WAAW,EA2CxE,eAAsB,EAAwB,EAAoD,CAChG,GAAM,CAAE,MAAK,SAAQ,UAAW,EAC1B,EAAK,EAAI,GAAG,UACZ,CAAE,eAAgB,EAAe,SAAU,EAAoB,CACnE,eAAgB,EAAK,cACrB,SAAU,gBAIV,GAAI,EAAK,QAAU,IAAA,IAAa,CAAE,MAAO,EAAK,KAAM,CACtD,CAAC,EAEK,CAAE,MAAK,MAAK,KAAI,kBAAiB,UAAS,MAAO,MAAM,OAAO,eAC9D,CAAE,gBAAiB,MAAM,OAAO,wBAChC,CAAE,qBAAsB,MAAM,OAAO,6BACrC,CAAE,aAAc,MAAM,OAAO,2BAA4B,CAAA,KAAA,GAAA,EAAA,CAAA,EAGzD,EAAkB,EAAkB,CAAS,CAAC,CAAC,SAAS,EAExD,EAAW,EAAgB,CAAY,EACvC,EAAU,EAAS,IACnB,EAAa,EAAS,OAC5B,GAAI,CAAC,GAAW,CAAC,EACf,MAAU,MACR,qFACF,EAEF,IAAM,EAAgB,EAAgB,CAAe,EAC/C,EAAa,EAAc,SAC3B,EAAW,EAAc,OACzB,EAAe,EAAc,WACnC,GAAI,CAAC,GAAc,CAAC,GAAY,CAAC,EAC/B,MAAU,MACR,kGACF,EAGF,IAAM,EAAS,IAAI,KAAK,KAAK,IAAI,EAAI,EAAgB,GAAK,GAAK,GAAK,GAAI,EAkClE,EAAa,MAAM,EACtB,iBAAiB,CAAC,CAAO,EAAG,CAAE,IAAK,CAAQ,CAAC,CAAC,CAC7C,KAAK,CAAY,CAAC,CAClB,UAAU,EAAiB,EAAG,EAAY,CAAO,CAAC,CAAC,CACnD,MACC,EACE,EAAG,EAAY,CAAM,EACrB,EAAQ,EAAU,CAAC,GAAG,CAA4B,CAAC,EACnD,EAAG,EAAc,CAAM,CACzB,CACF,CAAC,CACA,QAAQ,EAAI,CAAO,EAAG,EAAI,CAAY,CAAC,CAAC,CACxC,MAAM,CAAK,EAEd,GAAI,EAAW,SAAW,EAExB,OADA,EAAO,MAAM,CAAE,SAAQ,eAAc,EAAG,gDAAgD,EACjF,EAGT,IAAM,EAAU,MAAM,EAAkB,EACpC,EAAU,EACd,IAAK,IAAM,KAAO,EAAY,CAC5B,IAAM,EAAM,EAAI,IAChB,GAAI,CACF,MAAM,EAAQ,OAAO,CAAG,EACxB,GACF,OAAS,EAAK,CACZ,EAAO,KAAK,CAAE,MAAK,KAAI,EAAG,2CAA2C,CACvE,CACF,CAMA,OAJA,EAAO,KACL,CAAE,UAAS,MAAO,EAAW,OAAQ,SAAQ,eAAc,EAC3D,sCACF,EACO,CACT"}
|
package/dist/index.d.mts
CHANGED
|
@@ -1,10 +1,89 @@
|
|
|
1
|
-
import { A as
|
|
1
|
+
import { A as ErrorTrackerConfig, C as countDataRows, D as ErrorPattern, E as DEFAULT_MAX_SAMPLES_PER_PATTERN, M as ImportRun, N as ImportRunClient, O as ErrorSample, P as ImportRunStatus, S as StreamFeedRow, T as DEFAULT_MAX_PATTERNS, _ as FeedReaderConfig, b as readFeed, f as DEFAULT_BATCH_SIZE, g as runImport, h as RunImportResult, j as IMPORT_RUN_STATUSES, k as ErrorTracker, m as RunImportOptions, n as EsClientResolver, p as ImportRunProgress, r as FilePathResolver, t as EnsureIndexResolver, v as ReadFeedOptions, w as streamFeed, x as StreamFeedOptions, y as feedReaderConfigSchema } from "./worker-D2Pf-R9o.mjs";
|
|
2
2
|
import { a as TransformName, c as registerImportTransform, i as TransformContext, n as RowResult, o as TransformRegistry, r as RowTransform, s as getTransformRegistry, t as RowError } from "./transform-dO4z0Ze6.mjs";
|
|
3
3
|
import { Behavior } from "@murumets-ee/entity";
|
|
4
4
|
import { ToolkitApp } from "@murumets-ee/core";
|
|
5
5
|
import { Logger as Logger$1 } from "@murumets-ee/logging";
|
|
6
6
|
import { AdminClient } from "@murumets-ee/entity/admin";
|
|
7
7
|
|
|
8
|
+
//#region src/admin/cleanup.d.ts
|
|
9
|
+
interface CleanupOrphanedImportsArgs {
|
|
10
|
+
/** Toolkit app (provides db). */
|
|
11
|
+
app: ToolkitApp;
|
|
12
|
+
/**
|
|
13
|
+
* Bucket name the imports flow uses. Rows in `toolkit_files` are
|
|
14
|
+
* filtered to this bucket so the cleanup doesn't touch media,
|
|
15
|
+
* ticketing attachments, or any other consumer's uploads even if
|
|
16
|
+
* they share the same Postgres instance.
|
|
17
|
+
*/
|
|
18
|
+
bucket: string;
|
|
19
|
+
/**
|
|
20
|
+
* Hours after which an unreferenced upload is considered orphaned.
|
|
21
|
+
* Operator-grace window: should be long enough that a normal
|
|
22
|
+
* upload-then-register round-trip never wipes a fresh upload, but
|
|
23
|
+
* short enough that storage waste from failed uploads doesn't
|
|
24
|
+
* accumulate. 2 hours is the default in the imports plugin
|
|
25
|
+
* config — calibrated against the typical "operator clicks
|
|
26
|
+
* upload, walks away, comes back to check" workflow.
|
|
27
|
+
*/
|
|
28
|
+
ttlHours: number;
|
|
29
|
+
/**
|
|
30
|
+
* Maximum rows to process per run. Bounded so a sudden flood of
|
|
31
|
+
* orphans (e.g. after a brand-new supplier with unrecognised
|
|
32
|
+
* filenames floods the queue with rejected uploads) doesn't
|
|
33
|
+
* block the queue worker on a single multi-hour cleanup.
|
|
34
|
+
*/
|
|
35
|
+
limit?: number;
|
|
36
|
+
/** Logger for diagnostics. */
|
|
37
|
+
logger: Pick<Logger$1, 'debug' | 'info' | 'warn'>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Run one cleanup pass. Returns the count of rows actually deleted
|
|
41
|
+
* (which may be less than the count of orphans found if some
|
|
42
|
+
* `storage.delete` calls failed — see the `warn` log inside the loop).
|
|
43
|
+
*/
|
|
44
|
+
declare function cleanupOrphanedImports(args: CleanupOrphanedImportsArgs): Promise<number>;
|
|
45
|
+
interface CleanupProcessedImportsArgs {
|
|
46
|
+
/** Toolkit app (provides db). */
|
|
47
|
+
app: ToolkitApp;
|
|
48
|
+
/** Bucket name to scope the cleanup. Same semantics as `cleanupOrphanedImports`. */
|
|
49
|
+
bucket: string;
|
|
50
|
+
/**
|
|
51
|
+
* Days after which a terminal-status `import_run`'s source file is
|
|
52
|
+
* deleted. Operator-grace window: long enough that an operator can
|
|
53
|
+
* inspect the source file if a transform bug surfaces shortly after
|
|
54
|
+
* a successful import; short enough that storage doesn't accumulate
|
|
55
|
+
* months of supplier feeds.
|
|
56
|
+
*
|
|
57
|
+
* The plugin default is 7. Set to 0 to delete the source file
|
|
58
|
+
* immediately at the next cleanup tick after the import row
|
|
59
|
+
* reaches terminal status — useful when storage cost matters more
|
|
60
|
+
* than forensic reprocess capability.
|
|
61
|
+
*/
|
|
62
|
+
retentionDays: number;
|
|
63
|
+
/** Max rows per run. Defaults to 100 (same cap as the orphan sweep). */
|
|
64
|
+
limit?: number;
|
|
65
|
+
logger: Pick<Logger$1, 'debug' | 'info' | 'warn'>;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Cleanup source files for `import_run`s that finished (succeeded /
|
|
69
|
+
* failed / cancelled) more than `retentionDays` days ago.
|
|
70
|
+
*
|
|
71
|
+
* Counterpart to `cleanupOrphanedImports`: that one targets uploads
|
|
72
|
+
* with NO matching `import_run` (handshake never completed); this one
|
|
73
|
+
* targets uploads whose `import_run` row IS present and terminal.
|
|
74
|
+
* Together they cover every "useless storage row" case the imports
|
|
75
|
+
* flow can produce.
|
|
76
|
+
*
|
|
77
|
+
* Deletes ONLY the storage row (via `StorageClient.delete`); the
|
|
78
|
+
* `import_run` row is kept so the History panel + audit trail still
|
|
79
|
+
* surface the past run. The `import_run.filePath` value points at a
|
|
80
|
+
* now-deleted key after this runs — consumers that try to re-download
|
|
81
|
+
* the source file (e.g. a future "reprocess" button) will get a
|
|
82
|
+
* `getMetadata` → null and should show "source file unavailable" in
|
|
83
|
+
* the UI rather than crashing.
|
|
84
|
+
*/
|
|
85
|
+
declare function cleanupProcessedImports(args: CleanupProcessedImportsArgs): Promise<number>;
|
|
86
|
+
//#endregion
|
|
8
87
|
//#region src/entities/import-mapping.d.ts
|
|
9
88
|
/**
|
|
10
89
|
* Registered target schemas a mapping may write into. This is the FIRST of two
|
|
@@ -20,14 +99,14 @@ declare const ImportMapping: import("@murumets-ee/entity").Entity<{
|
|
|
20
99
|
id: import("@murumets-ee/entity").IdField;
|
|
21
100
|
} & import("@murumets-ee/entity").AuditableFields & Record<never, never> & {
|
|
22
101
|
/** FK to commerce `supplier` (by name string — no cross-package import). Required. */supplierId: import("@murumets-ee/entity").ReferenceField & {
|
|
23
|
-
cardinality: "
|
|
102
|
+
cardinality: "one" | "many";
|
|
24
103
|
} & {
|
|
25
104
|
readonly entity: "supplier";
|
|
26
105
|
readonly required: true;
|
|
27
106
|
readonly indexed: true;
|
|
28
107
|
}; /** FK to commerce `brand`. NULL = supplier default (applies to all that supplier's brands). */
|
|
29
108
|
brandId: import("@murumets-ee/entity").ReferenceField & {
|
|
30
|
-
cardinality: "
|
|
109
|
+
cardinality: "one" | "many";
|
|
31
110
|
} & {
|
|
32
111
|
readonly entity: "brand";
|
|
33
112
|
readonly indexed: true;
|
|
@@ -92,87 +171,53 @@ declare const ImportMapping: import("@murumets-ee/entity").Entity<{
|
|
|
92
171
|
readonly default: true;
|
|
93
172
|
readonly indexed: true;
|
|
94
173
|
};
|
|
95
|
-
}, "import_mapping", [Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").SearchableBehavior, Behavior<Record<never, never>, "imports:import-mapping-unique">]>;
|
|
174
|
+
}, "import_mapping", [Behavior<import("@murumets-ee/entity").AuditableFields, string>, import("@murumets-ee/entity").SearchableBehavior, Behavior<Record<never, never>, "imports:import-mapping-unique">, Behavior<Record<never, never>, "imports:import-mapping-validation">]>;
|
|
96
175
|
type ImportMappingClient = AdminClient<typeof ImportMapping.allFields>;
|
|
97
176
|
//#endregion
|
|
98
|
-
//#region src/
|
|
99
|
-
interface CleanupOrphanedImportsArgs {
|
|
100
|
-
/** Toolkit app (provides db). */
|
|
101
|
-
app: ToolkitApp;
|
|
102
|
-
/**
|
|
103
|
-
* Bucket name the imports flow uses. Rows in `toolkit_files` are
|
|
104
|
-
* filtered to this bucket so the cleanup doesn't touch media,
|
|
105
|
-
* ticketing attachments, or any other consumer's uploads even if
|
|
106
|
-
* they share the same Postgres instance.
|
|
107
|
-
*/
|
|
108
|
-
bucket: string;
|
|
109
|
-
/**
|
|
110
|
-
* Hours after which an unreferenced upload is considered orphaned.
|
|
111
|
-
* Operator-grace window: should be long enough that a normal
|
|
112
|
-
* upload-then-register round-trip never wipes a fresh upload, but
|
|
113
|
-
* short enough that storage waste from failed uploads doesn't
|
|
114
|
-
* accumulate. 2 hours is the default in the imports plugin
|
|
115
|
-
* config — calibrated against the typical "operator clicks
|
|
116
|
-
* upload, walks away, comes back to check" workflow.
|
|
117
|
-
*/
|
|
118
|
-
ttlHours: number;
|
|
119
|
-
/**
|
|
120
|
-
* Maximum rows to process per run. Bounded so a sudden flood of
|
|
121
|
-
* orphans (e.g. after a brand-new supplier with unrecognised
|
|
122
|
-
* filenames floods the queue with rejected uploads) doesn't
|
|
123
|
-
* block the queue worker on a single multi-hour cleanup.
|
|
124
|
-
*/
|
|
125
|
-
limit?: number;
|
|
126
|
-
/** Logger for diagnostics. */
|
|
127
|
-
logger: Pick<Logger$1, 'debug' | 'info' | 'warn'>;
|
|
128
|
-
}
|
|
177
|
+
//#region src/entities/import-mapping-validation.d.ts
|
|
129
178
|
/**
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
179
|
+
* Throw a descriptive error unless the mapping is structurally valid AND honours its
|
|
180
|
+
* schema's field whitelist + required targets. The thrown message is operator-facing
|
|
181
|
+
* (surfaced verbatim by the admin CRUD route as a 4xx body).
|
|
133
182
|
*/
|
|
134
|
-
declare function
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
183
|
+
declare function assertValidMapping(input: {
|
|
184
|
+
targetSchema: unknown;
|
|
185
|
+
numberFormat: unknown;
|
|
186
|
+
columnMap: unknown;
|
|
187
|
+
}): void;
|
|
188
|
+
/**
|
|
189
|
+
* Behavior wrapping {@link assertValidMapping}. Typed `Behavior<Record<never,
|
|
190
|
+
* never>, …>` (NOT plain `Behavior`, whose wide `fields?` would collapse the
|
|
191
|
+
* entity's inferred field types — the documented no-field-behavior gotcha).
|
|
192
|
+
* Contributes a hook, no fields.
|
|
193
|
+
*/
|
|
194
|
+
declare const importMappingValidation: Behavior<Record<never, never>, 'imports:import-mapping-validation'>;
|
|
195
|
+
//#endregion
|
|
196
|
+
//#region src/preview-feed.d.ts
|
|
197
|
+
/** Hard ceiling on preview rows regardless of caller request — bounds work + payload. */
|
|
198
|
+
declare const MAX_PREVIEW_ROWS = 100;
|
|
199
|
+
/** Default sample size when the caller doesn't specify. */
|
|
200
|
+
declare const DEFAULT_PREVIEW_ROWS = 20;
|
|
201
|
+
interface PreviewFeedOptions extends ReadFeedOptions {
|
|
202
|
+
/** Rows to sample. Defaults to {@link DEFAULT_PREVIEW_ROWS}; clamped to [1, {@link MAX_PREVIEW_ROWS}]. */
|
|
203
|
+
maxRows?: number;
|
|
204
|
+
}
|
|
205
|
+
interface FeedPreview {
|
|
140
206
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
* a successful import; short enough that storage doesn't accumulate
|
|
145
|
-
* months of supplier feeds.
|
|
146
|
-
*
|
|
147
|
-
* The plugin default is 7. Set to 0 to delete the source file
|
|
148
|
-
* immediately at the next cleanup tick after the import row
|
|
149
|
-
* reaches terminal status — useful when storage cost matters more
|
|
150
|
-
* than forensic reprocess capability.
|
|
207
|
+
* Source column names in first-seen order. With a header these are the (trimmed)
|
|
208
|
+
* header names; headerless feeds yield positional `"0"`, `"1"`, … keys. The union
|
|
209
|
+
* across sampled rows covers feeds whose first row omits trailing empty cells.
|
|
151
210
|
*/
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
|
|
155
|
-
logger: Pick<Logger$1, 'debug' | 'info' | 'warn'>;
|
|
211
|
+
columns: string[];
|
|
212
|
+
/** Up to `maxRows` sampled data rows, each a `{ column → cell }` map (cells are strings). */
|
|
213
|
+
rows: Array<Record<string, string>>;
|
|
156
214
|
}
|
|
157
215
|
/**
|
|
158
|
-
*
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
* Counterpart to `cleanupOrphanedImports`: that one targets uploads
|
|
162
|
-
* with NO matching `import_run` (handshake never completed); this one
|
|
163
|
-
* targets uploads whose `import_run` row IS present and terminal.
|
|
164
|
-
* Together they cover every "useless storage row" case the imports
|
|
165
|
-
* flow can produce.
|
|
166
|
-
*
|
|
167
|
-
* Deletes ONLY the storage row (via `StorageClient.delete`); the
|
|
168
|
-
* `import_run` row is kept so the History panel + audit trail still
|
|
169
|
-
* surface the past run. The `import_run.filePath` value points at a
|
|
170
|
-
* now-deleted key after this runs — consumers that try to re-download
|
|
171
|
-
* the source file (e.g. a future "reprocess" button) will get a
|
|
172
|
-
* `getMetadata` → null and should show "source file unavailable" in
|
|
173
|
-
* the UI rather than crashing.
|
|
216
|
+
* Read up to `maxRows` data rows from a feed file and return the detected columns +
|
|
217
|
+
* sample rows. Throws whatever the underlying reader throws (bad sheet index,
|
|
218
|
+
* duplicate header, unreadable file) — the caller maps that to a 4xx.
|
|
174
219
|
*/
|
|
175
|
-
declare function
|
|
220
|
+
declare function previewFeed(options: PreviewFeedOptions): Promise<FeedPreview>;
|
|
176
221
|
//#endregion
|
|
177
|
-
export { type CleanupOrphanedImportsArgs, type CleanupProcessedImportsArgs, DEFAULT_BATCH_SIZE, DEFAULT_MAX_PATTERNS, DEFAULT_MAX_SAMPLES_PER_PATTERN, type EnsureIndexResolver, type ErrorPattern, type ErrorSample, ErrorTracker, type ErrorTrackerConfig, type EsClientResolver, type FilePathResolver, IMPORT_MAPPING_FORMATS, IMPORT_MAPPING_TARGET_SCHEMAS, IMPORT_RUN_STATUSES, ImportMapping, type ImportMappingClient, type ImportMappingFormat, type ImportMappingTargetSchema, ImportRun, type ImportRunClient, type ImportRunProgress, type ImportRunStatus, type ReadFeedOptions, type RowError, type RowResult, type RowTransform, type RunImportOptions, type RunImportResult, type StreamFeedOptions, type StreamFeedRow, type TransformContext, type TransformName, TransformRegistry, cleanupOrphanedImports, cleanupProcessedImports, countDataRows, getTransformRegistry, readFeed, registerImportTransform, runImport, streamFeed };
|
|
222
|
+
export { type CleanupOrphanedImportsArgs, type CleanupProcessedImportsArgs, DEFAULT_BATCH_SIZE, DEFAULT_MAX_PATTERNS, DEFAULT_MAX_SAMPLES_PER_PATTERN, DEFAULT_PREVIEW_ROWS, type EnsureIndexResolver, type ErrorPattern, type ErrorSample, ErrorTracker, type ErrorTrackerConfig, type EsClientResolver, type FeedPreview, type FeedReaderConfig, type FilePathResolver, IMPORT_MAPPING_FORMATS, IMPORT_MAPPING_TARGET_SCHEMAS, IMPORT_RUN_STATUSES, ImportMapping, type ImportMappingClient, type ImportMappingFormat, type ImportMappingTargetSchema, ImportRun, type ImportRunClient, type ImportRunProgress, type ImportRunStatus, MAX_PREVIEW_ROWS, type PreviewFeedOptions, type ReadFeedOptions, type RowError, type RowResult, type RowTransform, type RunImportOptions, type RunImportResult, type StreamFeedOptions, type StreamFeedRow, type TransformContext, type TransformName, TransformRegistry, assertValidMapping, cleanupOrphanedImports, cleanupProcessedImports, countDataRows, feedReaderConfigSchema, getTransformRegistry, importMappingValidation, previewFeed, readFeed, registerImportTransform, runImport, streamFeed };
|
|
178
223
|
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/entities/import-mapping.ts","../src/
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/admin/cleanup.ts","../src/entities/import-mapping.ts","../src/entities/import-mapping-validation.ts","../src/preview-feed.ts"],"mappings":";;;;;;;;UAkCiB,0BAAA;EAmE+D;EAjE9E,GAAA,EAAK,UAAA;EAiEgF;AA0FvF;;;;;EApJE,MAAA;EAwKY;;;;;;;;;EA9JZ,QAAA;EA8JmB;AAAA;AAqBrB;;;;EA5KE,KAAA;EA4K4C;EA1K5C,MAAA,EAAQ,IAAA,CAAK,QAAA;AAAA;AA0K0E;;;;ACjMzF;ADiMyF,iBAnInE,sBAAA,CAAuB,IAAA,EAAM,0BAAA,GAA6B,OAAO;AAAA,UA0FtE,2BAAA;ECxJgE;ED0J/E,GAAA,EAAK,UAAA;ECzJK;ED2JV,MAAA;;;AC3J2E;AAG7E;;;;AAAqE;AACrE;;;;EDoKE,aAAA;ECpHW;EDsHX,KAAA;EACA,MAAA,EAAQ,IAAA,CAAK,QAAA;AAAA;;;;;;;;;;;;;;;;;;;iBAqBO,uBAAA,CAAwB,IAAA,EAAM,2BAAA,GAA8B,OAAO;;;;;;;;cCjM5E,6BAAA;AAAA,KACD,yBAAA,WAAoC,6BAA6B;ADuJ7E;AAAA,cCpJa,sBAAA;AAAA,KACD,mBAAA,WAA8B,sBAAsB;AAAA,cAgDnD,aAAA,gCAAa,MAAA;;;EDuHZ;;;;;;KAAJ;;;;;;KAqB+E;;;;;;;;;;;KCjMR;;;;;;;EAIZ;;;AAAA;;;KACL;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAwHpD,mBAAA,GAAsB,WAAW,QAAQ,aAAA,CAAc,SAAA;;;;;;;;iBCnInD,kBAAA,CAAmB,KAAA;EACjC,YAAA;EACA,YAAA;EACA,SAAA;AAAA;;;;;;;cA4DW,uBAAA,EAAyB,QAAQ,CAC5C,MAAA;;;;cC/EW,gBAAA;;cAEA,oBAAA;AAAA,UAEI,kBAAA,SAA2B,eAAe;EHsCzD;EGpCA,OAAO;AAAA;AAAA,UAGQ,WAAA;EHmCI;AAAA;AAuCrB;;;EGpEE,OAAA;EHoEiD;EGlEjD,IAAA,EAAM,KAAK,CAAC,MAAA;AAAA;;AHkEyE;AA0FvF;;;iBGpJsB,WAAA,CAAY,OAAA,EAAS,kBAAA,GAAqB,OAAA,CAAQ,WAAA"}
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{n as e,t}from"./cleanup-WNOeVksI.mjs";import{a as n,c as r,d as i,i as a,l as o,n as s,r as c,s as l,t as u,u as d}from"./transform-DdhsE3ZI.mjs";import{c as f,i as p,l as m,n as h,o as g,r as _,s as v,t as y,u as b}from"./runner-Cb1QPSt8.mjs";const x=100,S=20;async function C(e){let{maxRows:t,...n}=e,r=t??20,i=Math.min(100,Math.max(1,Number.isFinite(r)?Math.floor(r):20)),a=[],o=new Map;for await(let{row:e}of p({...n,relaxColumnCount:!0})){for(let t of Object.keys(e))o.set(t,!0);if(a.push(e),a.length>=i)break}return{columns:[...o.keys()],rows:a}}export{y as DEFAULT_BATCH_SIZE,f as DEFAULT_MAX_PATTERNS,m as DEFAULT_MAX_SAMPLES_PER_PATTERN,S as DEFAULT_PREVIEW_ROWS,b as ErrorTracker,l as IMPORT_MAPPING_FORMATS,r as IMPORT_MAPPING_TARGET_SCHEMAS,a as IMPORT_RUN_STATUSES,o as ImportMapping,n as ImportRun,x as MAX_PREVIEW_ROWS,u as TransformRegistry,d as assertValidMapping,t as cleanupOrphanedImports,e as cleanupProcessedImports,g as countDataRows,_ as feedReaderConfigSchema,s as getTransformRegistry,i as importMappingValidation,C as previewFeed,p as readFeed,c as registerImportTransform,h as runImport,v as streamFeed};
|
|
2
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/preview-feed.ts"],"sourcesContent":["/**\n * `previewFeed` — read the first N data rows of a feed for the PR3 import wizard\n * (commerce-demo phase 02 §4). Reuses {@link readFeed} so the preview is parsed by\n * the EXACT same reader (csv-parse / exceljs), with the same delimiter / header /\n * sheet config, that the run will use — the operator maps against what they'll get,\n * not a re-implemented best-guess parser (R02: drift between preview + run is a\n * mapping foot-gun).\n *\n * Bounded: it consumes only `maxRows` rows then stops iterating, so on a delimited\n * feed it streams a tiny prefix rather than the whole file. (The caller materialises\n * the storage object to a local path first — see the commerce preview route — so for\n * a very large upload the materialise cost dominates; that's a demo-depth trade-off,\n * noted on the route.)\n */\n\nimport { type ReadFeedOptions, readFeed } from './read-feed.js'\n\n/** Hard ceiling on preview rows regardless of caller request — bounds work + payload. */\nexport const MAX_PREVIEW_ROWS = 100\n/** Default sample size when the caller doesn't specify. */\nexport const DEFAULT_PREVIEW_ROWS = 20\n\nexport interface PreviewFeedOptions extends ReadFeedOptions {\n /** Rows to sample. Defaults to {@link DEFAULT_PREVIEW_ROWS}; clamped to [1, {@link MAX_PREVIEW_ROWS}]. */\n maxRows?: number\n}\n\nexport interface FeedPreview {\n /**\n * Source column names in first-seen order. With a header these are the (trimmed)\n * header names; headerless feeds yield positional `\"0\"`, `\"1\"`, … keys. The union\n * across sampled rows covers feeds whose first row omits trailing empty cells.\n */\n columns: string[]\n /** Up to `maxRows` sampled data rows, each a `{ column → cell }` map (cells are strings). */\n rows: Array<Record<string, string>>\n}\n\n/**\n * Read up to `maxRows` data rows from a feed file and return the detected columns +\n * sample rows. Throws whatever the underlying reader throws (bad sheet index,\n * duplicate header, unreadable file) — the caller maps that to a 4xx.\n */\nexport async function previewFeed(options: PreviewFeedOptions): Promise<FeedPreview> {\n const { maxRows, ...feedOptions } = options\n // Coerce to a finite integer BEFORE clamping: `previewFeed` is public API, so a\n // caller passing `NaN`/`Infinity`/fractional must not slip through. `NaN` in\n // particular would make `rows.length >= limit` always false → read the whole file.\n const requested = maxRows ?? DEFAULT_PREVIEW_ROWS\n const safeRows = Number.isFinite(requested) ? Math.floor(requested) : DEFAULT_PREVIEW_ROWS\n const limit = Math.min(MAX_PREVIEW_ROWS, Math.max(1, safeRows))\n\n const rows: Array<Record<string, string>> = []\n // First-seen-order column set: a Map preserves insertion order and dedupes.\n const columns = new Map<string, true>()\n\n // Preview is EXPLORATORY — force lenient column counts (csv/tsv). With the wrong\n // delimiter a feed parses as a single odd column, OR a row's field count differs\n // from the header; strict parsing would abort the whole preview with a cryptic\n // \"Invalid Record Length\" instead of letting the operator SEE the bad parse and\n // fix the delimiter/format, then re-preview. The actual run keeps its own\n // (stricter) settings.\n for await (const { row } of readFeed({ ...feedOptions, relaxColumnCount: true })) {\n for (const key of Object.keys(row)) columns.set(key, true)\n rows.push(row)\n if (rows.length >= limit) break\n }\n\n return { columns: [...columns.keys()], rows }\n}\n"],"mappings":"2PAkBA,MAAa,EAAmB,IAEnB,EAAuB,GAuBpC,eAAsB,EAAY,EAAmD,CACnF,GAAM,CAAE,UAAS,GAAG,GAAgB,EAI9B,EAAY,GAAA,GAEZ,EAAQ,KAAK,IAAA,IAAsB,KAAK,IAAI,EADjC,OAAO,SAAS,CAAS,EAAI,KAAK,MAAM,CAAS,EAAA,EACL,CAAC,EAExD,EAAsC,CAAC,EAEvC,EAAU,IAAI,IAQpB,UAAW,GAAM,CAAE,SAAS,EAAS,CAAE,GAAG,EAAa,iBAAkB,EAAK,CAAC,EAAG,CAChF,IAAK,IAAM,KAAO,OAAO,KAAK,CAAG,EAAG,EAAQ,IAAI,EAAK,EAAI,EAEzD,GADA,EAAK,KAAK,CAAG,EACT,EAAK,QAAU,EAAO,KAC5B,CAEA,MAAO,CAAE,QAAS,CAAC,GAAG,EAAQ,KAAK,CAAC,EAAG,MAAK,CAC9C"}
|
package/dist/mapping/index.d.mts
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { r as RowTransform } from "../transform-dO4z0Ze6.mjs";
|
|
2
|
-
import { _ as
|
|
2
|
+
import { S as toMinorUnits, _ as applyColumnTransforms, a as MappedRow, b as parseEuropeanNumber, c as TargetSchema, d as textOrNull, f as COLUMN_TRANSFORM_OPS, g as ColumnTransformOpDescriptor, h as ColumnTransformOp, i as BuildRowContext, l as numberCell, m as ColumnTransform, n as partsParamsSchema, o as NumberOutcome, p as CellValue, r as partsSchema, s as TargetField, t as PartsParams, u as rowError, v as columnTransformSchema, x as parseNumber, y as NumberFormat } from "../parts-schema-B7Cbkl9T.mjs";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
|
+
//#region src/mapping/discount-table-schema.d.ts
|
|
6
|
+
/** One parsed discount-table row: a group token + the purchase discount % off list. */
|
|
7
|
+
interface DiscountTableRow {
|
|
8
|
+
discountGroup: string;
|
|
9
|
+
discountPct: number;
|
|
10
|
+
}
|
|
11
|
+
declare const discountTableSchema: TargetSchema<DiscountTableRow>;
|
|
12
|
+
//#endregion
|
|
5
13
|
//#region src/mapping/engine.d.ts
|
|
6
14
|
/** One column-map entry. `target` is checked against the schema whitelist separately. */
|
|
7
15
|
declare const mappingColumnSchema: z.ZodObject<{
|
|
@@ -113,8 +121,15 @@ declare const mappingColumnSchema: z.ZodObject<{
|
|
|
113
121
|
})[] | undefined;
|
|
114
122
|
}>;
|
|
115
123
|
type MappingColumn = z.infer<typeof mappingColumnSchema>;
|
|
116
|
-
/**
|
|
117
|
-
|
|
124
|
+
/**
|
|
125
|
+
* Decimal + thousands separators for the `number` transform.
|
|
126
|
+
*
|
|
127
|
+
* The two MUST differ: `parseNumber` strips every thousands separator FIRST, so a
|
|
128
|
+
* config where `decimal === thousands` (e.g. both `,`) would strip the decimal
|
|
129
|
+
* separator too — `326,88` → `32688`, a silent 100× error rather than a row error.
|
|
130
|
+
* Rejected at save (`assertValidMapping`) AND at run (`mappingConfigSchema`).
|
|
131
|
+
*/
|
|
132
|
+
declare const numberFormatSchema: z.ZodEffects<z.ZodObject<{
|
|
118
133
|
decimal: z.ZodEnum<[".", ","]>;
|
|
119
134
|
thousands: z.ZodEnum<["", ".", ",", " "]>;
|
|
120
135
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -123,6 +138,12 @@ declare const numberFormatSchema: z.ZodObject<{
|
|
|
123
138
|
}, {
|
|
124
139
|
decimal: "." | ",";
|
|
125
140
|
thousands: "" | "." | "," | " ";
|
|
141
|
+
}>, {
|
|
142
|
+
decimal: "." | ",";
|
|
143
|
+
thousands: "" | "." | "," | " ";
|
|
144
|
+
}, {
|
|
145
|
+
decimal: "." | ",";
|
|
146
|
+
thousands: "" | "." | "," | " ";
|
|
126
147
|
}>;
|
|
127
148
|
/**
|
|
128
149
|
* The portable mapping config — the subset of an `import_mapping` row the engine
|
|
@@ -132,7 +153,7 @@ declare const numberFormatSchema: z.ZodObject<{
|
|
|
132
153
|
*/
|
|
133
154
|
declare const mappingConfigSchema: z.ZodObject<{
|
|
134
155
|
targetSchema: z.ZodString;
|
|
135
|
-
numberFormat: z.ZodObject<{
|
|
156
|
+
numberFormat: z.ZodEffects<z.ZodObject<{
|
|
136
157
|
decimal: z.ZodEnum<[".", ","]>;
|
|
137
158
|
thousands: z.ZodEnum<["", ".", ",", " "]>;
|
|
138
159
|
}, "strip", z.ZodTypeAny, {
|
|
@@ -141,6 +162,12 @@ declare const mappingConfigSchema: z.ZodObject<{
|
|
|
141
162
|
}, {
|
|
142
163
|
decimal: "." | ",";
|
|
143
164
|
thousands: "" | "." | "," | " ";
|
|
165
|
+
}>, {
|
|
166
|
+
decimal: "." | ",";
|
|
167
|
+
thousands: "" | "." | "," | " ";
|
|
168
|
+
}, {
|
|
169
|
+
decimal: "." | ",";
|
|
170
|
+
thousands: "" | "." | "," | " ";
|
|
144
171
|
}>;
|
|
145
172
|
columnMap: z.ZodArray<z.ZodObject<{
|
|
146
173
|
/** Source header name (or positional index string for headerless feeds). */source: z.ZodString; /** Target field key — must be in the chosen schema's whitelist (enforced in `buildMappingTransform`). */
|
|
@@ -339,20 +366,6 @@ interface ResolvableMapping {
|
|
|
339
366
|
*/
|
|
340
367
|
declare function resolveMapping<M extends ResolvableMapping>(mappings: readonly M[], brandId: string | null): M | undefined;
|
|
341
368
|
//#endregion
|
|
342
|
-
//#region src/mapping/discount-table-schema.d.ts
|
|
343
|
-
/** One parsed discount-table row: a group token + the purchase discount % off list. */
|
|
344
|
-
interface DiscountTableRow {
|
|
345
|
-
discountGroup: string;
|
|
346
|
-
discountPct: number;
|
|
347
|
-
}
|
|
348
|
-
declare const discountTableSchema: TargetSchema<DiscountTableRow>;
|
|
349
|
-
//#endregion
|
|
350
|
-
//#region src/mapping/schema-registry.d.ts
|
|
351
|
-
/** Resolve a registered schema by name, or `undefined` if not registered. */
|
|
352
|
-
declare function getTargetSchema(name: string): TargetSchema<unknown> | undefined;
|
|
353
|
-
/** All registered schema names (for the wizard's schema picker, PR3). */
|
|
354
|
-
declare function listTargetSchemas(): string[];
|
|
355
|
-
//#endregion
|
|
356
369
|
//#region src/mapping/mapping-transform.d.ts
|
|
357
370
|
/** Stable name under which the mapping-driven transform is registered. */
|
|
358
371
|
declare const MAPPING_TRANSFORM_NAME = "commerce:mapping";
|
|
@@ -363,5 +376,23 @@ declare const MAPPING_TRANSFORM_NAME = "commerce:mapping";
|
|
|
363
376
|
*/
|
|
364
377
|
declare const mappingTransform: RowTransform<unknown>;
|
|
365
378
|
//#endregion
|
|
366
|
-
|
|
379
|
+
//#region src/mapping/schema-registry.d.ts
|
|
380
|
+
/** Resolve a registered schema by name, or `undefined` if not registered. */
|
|
381
|
+
declare function getTargetSchema(name: string): TargetSchema<unknown> | undefined;
|
|
382
|
+
/** All registered schema names (for the wizard's schema picker, PR3). */
|
|
383
|
+
declare function listTargetSchemas(): string[];
|
|
384
|
+
/** Operator-facing summary of a target schema: its name + the field whitelist (PR3 wizard). */
|
|
385
|
+
interface TargetSchemaSummary {
|
|
386
|
+
name: string;
|
|
387
|
+
fields: readonly TargetField[];
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Describe every registered target schema (name + field whitelist) for the PR3
|
|
391
|
+
* mapping-wizard UI. Returns only metadata (no `buildRow`), so the wizard's RSC page
|
|
392
|
+
* can ship it across the client boundary as plain JSON without dragging the engine
|
|
393
|
+
* into the client bundle.
|
|
394
|
+
*/
|
|
395
|
+
declare function describeTargetSchemas(): TargetSchemaSummary[];
|
|
396
|
+
//#endregion
|
|
397
|
+
export { type BuildRowContext, COLUMN_TRANSFORM_OPS, type CellValue, type ColumnTransform, type ColumnTransformOp, type ColumnTransformOpDescriptor, type DiscountTableRow, MAPPING_TRANSFORM_NAME, type MappedRow, type MappingColumn, type MappingConfig, type NumberFormat, type NumberOutcome, type PartsParams, type ResolvableMapping, type TargetField, type TargetSchema, type TargetSchemaSummary, applyColumnTransforms, buildMappingTransform, columnTransformSchema, describeTargetSchemas, discountTableSchema, getTargetSchema, listTargetSchemas, mappingColumnSchema, mappingConfigSchema, mappingTransform, numberCell, numberFormatSchema, parseEuropeanNumber, parseNumber, partsParamsSchema, partsSchema, resolveMapping, rowError, textOrNull, toMinorUnits };
|
|
367
398
|
//# sourceMappingURL=index.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/mapping/
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/mapping/discount-table-schema.ts","../../src/mapping/engine.ts","../../src/mapping/mapping-transform.ts","../../src/mapping/schema-registry.ts"],"mappings":";;;;;;UAqBiB,gBAAA;EACf,aAAA;EACA,WAAW;AAAA;AAAA,cAQA,mBAAA,EAAqB,YAAY,CAAC,gBAAA;;;;cCPlC,mBAAA,EAAmB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAQpB,aAAA,GAAgB,CAAA,CAAE,KAAK,QAAQ,mBAAA;;;;;;;;;cAU9B,kBAAA,EAAkB,CAAA,CAAA,UAAA,CAAA,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;cAgBlB,mBAAA,EAAmB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAKpB,aAAA,GAAgB,CAAA,CAAE,KAAK,QAAQ,mBAAA;;;;;;;;;;;;;iBAc3B,qBAAA,MAAA,CACd,MAAA,EAAQ,aAAA,EACR,MAAA,EAAQ,YAAA,CAAa,IAAA,IACpB,YAAA,CAAa,IAAA;;UA+BC,iBAAA;;EAEf,OAAA;EACA,MAAM;AAAA;;;;;;;iBASQ,cAAA,WAAyB,iBAAA,CAAA,CACvC,QAAA,WAAmB,CAAA,IACnB,OAAA,kBACC,CAAA;;;;cC/FU,sBAAA;;;;;;cA4BA,gBAAA,EAAkB,YAAY;;;;iBClC3B,eAAA,CAAgB,IAAA,WAAe,YAAY;AHM3D;AAAA,iBGDgB,iBAAA,CAAA;;UAKC,mBAAA;EACf,IAAA;EACA,MAAA,WAAiB,WAAW;AAAA;;AFb9B;;;;;iBEsBgB,qBAAA,CAAA,GAAyB,mBAAmB"}
|
package/dist/mapping/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{a as e,c as t,d as n,f as r,
|
|
1
|
+
import{a as e,c as t,d as n,f as r,g as i,h as a,i as o,l as s,m as c,n as l,o as u,p as d,r as f,s as p,t as m,u as h}from"../parts-schema-Cuaq2_At.mjs";import{i as g,n as _,r as v,t as y}from"../schema-registry-DtXbLhOg.mjs";import{z as b}from"zod";const x=`commerce:mapping`,S=b.object({mapping:t}),C=new WeakMap;function w(e){let t=C.get(e);if(t)return t;let{mapping:n}=S.parse(e),r=_(n.targetSchema);if(!r)throw Error(`Unknown target schema "${n.targetSchema}" (registered: ${v().join(`, `)})`);let i=u(n,r);return C.set(e,i),i}const T=async(e,t)=>w(t.params)(e,t);export{n as COLUMN_TRANSFORM_OPS,x as MAPPING_TRANSFORM_NAME,r as applyColumnTransforms,u as buildMappingTransform,d as columnTransformSchema,y as describeTargetSchemas,g as discountTableSchema,_ as getTargetSchema,v as listTargetSchemas,p as mappingColumnSchema,t as mappingConfigSchema,T as mappingTransform,f as numberCell,s as numberFormatSchema,c as parseEuropeanNumber,a as parseNumber,m as partsParamsSchema,l as partsSchema,h as resolveMapping,o as rowError,e as textOrNull,i as toMinorUnits};
|
|
2
2
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/mapping/
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/mapping/mapping-transform.ts"],"sourcesContent":["/**\n * `commerce:mapping` — the registered transform for runs driven by a saved\n * `import_mapping` (commerce-demo phase 02; the PR3 wizard enqueues these).\n *\n * The run's `params.mapping` carries a SNAPSHOT of the mapping config, so the run\n * is reproducible (it records the exact mapping used) and the transform stays a\n * pure function of `(row, ctx)` with no per-row DB I/O. This transform compiles\n * the snapshot once per `ctx.params` reference (memoized via a `WeakMap`, the same\n * shape as the carmaker path) and applies it.\n *\n * Output type is `unknown` because the target schema (parts vs discount_table)\n * isn't known until the snapshot is read at runtime. The parts schema additionally\n * reads brand/supplier identity from the SAME `params` object (alongside `mapping`).\n *\n * **Sink caveat.** The `runImport` runner bulk-writes whatever doc this produces\n * into the ES `parts` index. That's correct for a `parts` mapping but WRONG for a\n * `discount_table` mapping — its rows belong in Postgres (`supplier_discount_group`),\n * not ES. PR2 ships the discount_table SCHEMA (so the engine parses it) but no sink;\n * a `discount_table` run must NOT be enqueued through the parts-ES `runImport` path.\n * PR4 wires the Postgres sink + the route that enqueues discount runs.\n *\n * PR2 ships + tests this transform; the production registration (and the wizard\n * that creates the mappings + sets `params.feed` for the reader) land in PR3.\n */\n\nimport { z } from 'zod'\nimport type { RowTransform } from '../transform.js'\nimport { buildMappingTransform, mappingConfigSchema } from './engine.js'\nimport { getTargetSchema, listTargetSchemas } from './schema-registry.js'\n\n/** Stable name under which the mapping-driven transform is registered. */\nexport const MAPPING_TRANSFORM_NAME = 'commerce:mapping'\n\n/** The mapping snapshot lives under `params.mapping`; other params flow to the schema. */\nconst mappingParamsSchema = z.object({ mapping: mappingConfigSchema })\n\n/** Compiled-transform cache, keyed on the `ctx.params` object identity (per run). */\nconst compiledCache = new WeakMap<object, RowTransform<unknown>>()\n\nfunction compile(params: Record<string, unknown>): RowTransform<unknown> {\n const cached = compiledCache.get(params)\n if (cached) return cached\n const { mapping } = mappingParamsSchema.parse(params)\n const schema = getTargetSchema(mapping.targetSchema)\n if (!schema) {\n throw new Error(\n `Unknown target schema \"${mapping.targetSchema}\" (registered: ${listTargetSchemas().join(', ')})`,\n )\n }\n const transform = buildMappingTransform(mapping, schema)\n compiledCache.set(params, transform)\n return transform\n}\n\n/**\n * The registered transform. Resolves + compiles the mapping snapshot from\n * `ctx.params` (memoized), then delegates each row to it. A malformed snapshot or\n * unknown schema throws — surfaced by the runner as a row-level `transform_threw`.\n */\nexport const mappingTransform: RowTransform<unknown> = async (row, ctx) => {\n const transform = compile(ctx.params)\n return transform(row, ctx)\n}\n"],"mappings":"2PA+BA,MAAa,EAAyB,mBAGhC,EAAsB,EAAE,OAAO,CAAE,QAAS,CAAoB,CAAC,EAG/D,EAAgB,IAAI,QAE1B,SAAS,EAAQ,EAAwD,CACvE,IAAM,EAAS,EAAc,IAAI,CAAM,EACvC,GAAI,EAAQ,OAAO,EACnB,GAAM,CAAE,WAAY,EAAoB,MAAM,CAAM,EAC9C,EAAS,EAAgB,EAAQ,YAAY,EACnD,GAAI,CAAC,EACH,MAAU,MACR,0BAA0B,EAAQ,aAAa,iBAAiB,EAAkB,CAAC,CAAC,KAAK,IAAI,EAAE,EACjG,EAEF,IAAM,EAAY,EAAsB,EAAS,CAAM,EAEvD,OADA,EAAc,IAAI,EAAQ,CAAS,EAC5B,CACT,CAOA,MAAa,EAA0C,MAAO,EAAK,IAC/C,EAAQ,EAAI,MACf,CAAC,CAAC,EAAK,CAAG"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as RowResult } from "./transform-dO4z0Ze6.mjs";
|
|
2
|
-
import { PartsDocument } from "@murumets-ee/search-elasticsearch";
|
|
3
2
|
import { z } from "zod";
|
|
3
|
+
import { PartsDocument } from "@murumets-ee/search-elasticsearch";
|
|
4
4
|
|
|
5
5
|
//#region src/mapping/number-format.d.ts
|
|
6
6
|
/**
|
|
@@ -144,6 +144,21 @@ declare const columnTransformSchema: z.ZodDiscriminatedUnion<"op", [z.ZodObject<
|
|
|
144
144
|
}>]>;
|
|
145
145
|
/** One declarative cleaning operation. */
|
|
146
146
|
type ColumnTransform = z.infer<typeof columnTransformSchema>;
|
|
147
|
+
/** The discriminator literal of a {@link ColumnTransform}. */
|
|
148
|
+
type ColumnTransformOp = ColumnTransform['op'];
|
|
149
|
+
/** UI descriptor for one transform op: its key, a label, and whether it takes a `value` param. */
|
|
150
|
+
interface ColumnTransformOpDescriptor {
|
|
151
|
+
op: ColumnTransformOp;
|
|
152
|
+
label: string;
|
|
153
|
+
/** `true` → the op carries a `value` string param (`stripPrefix`/`stripSuffix`/`defaultIfEmpty`). */
|
|
154
|
+
hasValue: boolean;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Static catalogue of the available per-column transform ops, for the PR3 wizard's
|
|
158
|
+
* transform picker. The ORDER is the suggested UI order; `number`/`europeanNumber`
|
|
159
|
+
* are terminal (see {@link applyColumnTransforms}) and labelled accordingly.
|
|
160
|
+
*/
|
|
161
|
+
declare const COLUMN_TRANSFORM_OPS: readonly ColumnTransformOpDescriptor[];
|
|
147
162
|
/** The value a column carries as it moves through the transform pipeline. */
|
|
148
163
|
type CellValue = string | number | null;
|
|
149
164
|
/**
|
|
@@ -256,5 +271,5 @@ type PartsParams = z.infer<typeof partsParamsSchema>;
|
|
|
256
271
|
*/
|
|
257
272
|
declare const partsSchema: TargetSchema<PartsDocument>;
|
|
258
273
|
//#endregion
|
|
259
|
-
export {
|
|
260
|
-
//# sourceMappingURL=parts-schema-
|
|
274
|
+
export { toMinorUnits as S, applyColumnTransforms as _, MappedRow as a, parseEuropeanNumber as b, TargetSchema as c, textOrNull as d, COLUMN_TRANSFORM_OPS as f, ColumnTransformOpDescriptor as g, ColumnTransformOp as h, BuildRowContext as i, numberCell as l, ColumnTransform as m, partsParamsSchema as n, NumberOutcome as o, CellValue as p, partsSchema as r, TargetField as s, PartsParams as t, rowError as u, columnTransformSchema as v, parseNumber as x, NumberFormat as y };
|
|
275
|
+
//# sourceMappingURL=parts-schema-B7Cbkl9T.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parts-schema-B7Cbkl9T.d.mts","names":[],"sources":["../src/mapping/number-format.ts","../src/mapping/column-transforms.ts","../src/mapping/target-schema.ts","../src/mapping/parts-schema.ts"],"mappings":";;;;;;;;;;AAqBA;;;;AAIW;AAsBX;;;;;;;;AAA6D;AA+B7D;AAAA,UAzDiB,YAAA;;EAEf,OAAA;EAuDwC;EArDxC,SAAS;AAAA;;;AA+EkC;;;;AC9E7C;;;;;;;;;;iBDqBgB,WAAA,CAAY,GAAA,UAAa,MAAA,EAAQ,YAAY;;;;;;;;iBA+B7C,YAAA,CAAa,KAAa;;;;;;;;;;;;;;;;;;;;;;;iBA0B1B,mBAAA,CAAoB,CAAS;;;;;AAzDgB;AA+B7D;;cCpDa,qBAAA,EAAqB,CAAA,CAAA,qBAAA,QAAA,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAYtB,eAAA,GAAkB,CAAA,CAAE,KAAK,QAAQ,qBAAA;;KAGjC,iBAAA,GAAoB,eAAe;;UAG9B,2BAAA;EACf,EAAA,EAAI,iBAAiB;EACrB,KAAA;;EAEA,QAAA;AAAA;;;;;;cAQW,oBAAA,WAA+B,2BAA2B;;KAY3D,SAAA;;;;;;;;iBASI,qBAAA,CACd,GAAA,UACA,UAAA,WAAqB,eAAA,IACrB,MAAA,EAAQ,YAAA,GACP,SAAA;;;;UC/Dc,WAAA;EF6B4C;EE3B3D,GAAA;EF0Dc;EExDd,KAAA;;EAEA,QAAA;EFsDwC;EEpDxC,IAAA;AAAA;;KAIU,SAAA,GAAY,MAAM,SAAS,SAAA;AF0EM;AAAA,UEvE5B,eAAA;EACf,WAAA;EACA,SAAA;EDTW;;;;ECcX,MAAA,EAAQ,MAAM;AAAA;;UAIC,YAAA;;EAEf,IAAA;;EAEA,MAAA,WAAiB,WAAA;;EAEjB,QAAA,CAAS,MAAA,EAAQ,SAAA,EAAW,GAAA,EAAK,eAAA,GAAkB,SAAA,CAAU,IAAA;AAAA;;;;;;iBAY/C,UAAA,CAAW,CAAwB,EAArB,SAAS;;KAO3B,aAAA;EACN,IAAA;EAAe,KAAA;AAAA;EACf,IAAA;AAAA;EACA,IAAA;AAAA;;;;;;iBAOU,UAAA,CAAW,CAAA,EAAG,SAAA,eAAwB,aAAa;;iBASnD,QAAA,CAAS,SAAA,UAAmB,OAAA,UAAiB,KAAA;;;;;;;;;;AFVnB;AA0B1C;;;;AAA6C;AA1BH,cGtC7B,iBAAA,EAAiB,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;KAalB,WAAA,GAAc,CAAA,CAAE,KAAK,QAAQ,iBAAA;;;;;;cAmD5B,WAAA,EAAa,YAAY,CAAC,aAAA"}
|