dicom-curate 0.34.0 → 0.35.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/README.md CHANGED
@@ -287,6 +287,64 @@ export function sampleBatchCurationSpecification(): TCurationSpecification {
287
287
  }
288
288
  ```
289
289
 
290
+ ## Excluding files with preExclude and postExclude
291
+
292
+ The curation specification supports two optional exclusion functions that let you skip files at different stages of processing. Both return **`true` to exclude** the file; returning `false` (or omitting the function entirely) lets the file through.
293
+
294
+ ### preExclude — skip before mapping
295
+
296
+ `preExclude` receives a `parser` with access to the **original, unmapped DICOM tags**. Return `true` to skip the file entirely — it will not be mapped, written, or uploaded.
297
+
298
+ ```ts
299
+ export function myCurationSpec(): TCurationSpecification {
300
+ return {
301
+ // ... other fields ...
302
+
303
+ // Exclude files whose PatientID doesn't match the expected study format.
304
+ preExclude(parser) {
305
+ return !/^AB\d{2}-\d{3}$/.test(parser.getDicom('PatientID'))
306
+ },
307
+ }
308
+ }
309
+ ```
310
+
311
+ ### postExclude — skip after mapping
312
+
313
+ `postExclude` receives a `parser` whose `getDicom()` returns **de-identified tag values** (PS315E de-identification has already run at this point), and exposes the computed output path as `parser.outputFilePath`. Return `true` to skip writing or uploading the mapped file.
314
+
315
+ Note: `parser.getFilePathComp()` still returns **input** path components inside `postExclude`, the same as in `preExclude`. Only `parser.outputFilePath` reflects the post-mapping location, as a full string.
316
+
317
+ ```ts
318
+ export function myCurationSpec(): TCurationSpecification {
319
+ return {
320
+ // ... other fields ...
321
+
322
+ // Exclude structured reports and files routed to an 'exclude' output folder.
323
+ postExclude(parser) {
324
+ if (parser.getDicom('Modality') === 'SR') return true
325
+ if (parser.outputFilePath.includes('/exclude/')) return true
326
+ return false
327
+ },
328
+ }
329
+ }
330
+ ```
331
+
332
+ ### Behaviour notes
333
+
334
+ - **Exclusions are re-evaluated on every run.** When a `preExclude` or `postExclude` is configured, the "unchanged source bytes" short-circuit is disabled so an exclusion added in a later run takes effect even if the file itself didn't change.
335
+ - **Composition across multiple specs is OR.** When `composeSpecs` merges specs that each define `preExclude` / `postExclude`, the composed function excludes a file if **any** spec's function returns `true`. Evaluation short-circuits on the first `true`.
336
+ - **Exceptions are fail-safe.** If an exclusion function throws, the file is treated as **included** and the error message is appended to `mapResults.errors`.
337
+
338
+ ### Result shape
339
+
340
+ When a file is excluded, `curateOne` / `curateMany` still returns a result object for it. The `excluded` field indicates which function rejected it:
341
+
342
+ ```ts
343
+ // 'pre' — excluded by preExclude (file was never mapped)
344
+ // 'post' — excluded by postExclude (file was mapped but not written)
345
+ result.excluded // => 'pre' | 'post' | undefined
346
+ ```
347
+
290
348
  ## DICOM Conformance Notes
291
349
 
292
350
  dicom-curate
@@ -67499,38 +67499,6 @@ var require_acorn_globals = __commonJS({
67499
67499
  // src/curateOne.ts
67500
67500
  var dcmjs7 = __toESM(require_dcmjs(), 1);
67501
67501
 
67502
- // src/createNestedDirectories.ts
67503
- async function createNestedDirectories(topLevelDirectoryHandle, path) {
67504
- const pathSegments = path.split("/").filter((segment) => segment !== "");
67505
- let currentDirectoryHandle = topLevelDirectoryHandle;
67506
- for (const segment of pathSegments) {
67507
- try {
67508
- const entry = await currentDirectoryHandle.getDirectoryHandle(segment, {
67509
- create: false
67510
- });
67511
- currentDirectoryHandle = entry;
67512
- } catch (error2) {
67513
- if (error2.name === "NotFoundError") {
67514
- const entry = await currentDirectoryHandle.getDirectoryHandle(segment, {
67515
- create: true
67516
- });
67517
- currentDirectoryHandle = entry;
67518
- } else {
67519
- return false;
67520
- }
67521
- }
67522
- }
67523
- return currentDirectoryHandle;
67524
- }
67525
-
67526
- // src/curateDict.ts
67527
- var dcmjs6 = __toESM(require_dcmjs(), 1);
67528
- var import_lodash4 = __toESM(require_lodash(), 1);
67529
-
67530
- // src/collectMappings.ts
67531
- var dcmjs4 = __toESM(require_dcmjs(), 1);
67532
- var import_lodash3 = __toESM(require_lodash(), 1);
67533
-
67534
67502
  // src/config/specVersion.ts
67535
67503
  var specVersion = "3.0";
67536
67504
 
@@ -79352,11 +79320,53 @@ function composeSpecs(specOrComposedSpec) {
79352
79320
  return [...prev(p4), ...next(p4)];
79353
79321
  };
79354
79322
  }
79323
+ if (spec.preExclude) {
79324
+ const prev = final.preExclude;
79325
+ const next = spec.preExclude;
79326
+ final.preExclude = prev ? (p4) => prev(p4) || next(p4) : next;
79327
+ }
79328
+ if (spec.postExclude) {
79329
+ const prev = final.postExclude;
79330
+ const next = spec.postExclude;
79331
+ final.postExclude = prev ? (p4) => prev(p4) || next(p4) : next;
79332
+ }
79355
79333
  }
79356
79334
  final.dicomPS315EOptions = mergePs315(ps315Chain);
79357
79335
  return final;
79358
79336
  }
79359
79337
 
79338
+ // src/createNestedDirectories.ts
79339
+ async function createNestedDirectories(topLevelDirectoryHandle, path) {
79340
+ const pathSegments = path.split("/").filter((segment) => segment !== "");
79341
+ let currentDirectoryHandle = topLevelDirectoryHandle;
79342
+ for (const segment of pathSegments) {
79343
+ try {
79344
+ const entry = await currentDirectoryHandle.getDirectoryHandle(segment, {
79345
+ create: false
79346
+ });
79347
+ currentDirectoryHandle = entry;
79348
+ } catch (error2) {
79349
+ if (error2.name === "NotFoundError") {
79350
+ const entry = await currentDirectoryHandle.getDirectoryHandle(segment, {
79351
+ create: true
79352
+ });
79353
+ currentDirectoryHandle = entry;
79354
+ } else {
79355
+ return false;
79356
+ }
79357
+ }
79358
+ }
79359
+ return currentDirectoryHandle;
79360
+ }
79361
+
79362
+ // src/curateDict.ts
79363
+ var dcmjs6 = __toESM(require_dcmjs(), 1);
79364
+ var import_lodash4 = __toESM(require_lodash(), 1);
79365
+
79366
+ // src/collectMappings.ts
79367
+ var dcmjs4 = __toESM(require_dcmjs(), 1);
79368
+ var import_lodash3 = __toESM(require_lodash(), 1);
79369
+
79360
79370
  // src/getParser.ts
79361
79371
  var dcmjs3 = __toESM(require_dcmjs(), 1);
79362
79372
  var import_lodash2 = __toESM(require_lodash(), 1);
@@ -79484,9 +79494,21 @@ function collectMappings(inputFilePath, dicomData, mappingOptions) {
79484
79494
  mappingOptions.columnMappings,
79485
79495
  finalSpec.additionalData
79486
79496
  );
79497
+ let preExcludeError;
79498
+ try {
79499
+ if (finalSpec.preExclude?.(parser)) {
79500
+ mapResults.excluded = "pre";
79501
+ return [naturalData, mapResults];
79502
+ }
79503
+ } catch (e4) {
79504
+ preExcludeError = `preExclude threw an error: ${e4 instanceof Error ? e4.message : String(e4)} \u2014 treating file as included (fail-safe)`;
79505
+ }
79487
79506
  if (!mappingOptions.skipValidation) {
79488
79507
  mapResults.errors = finalSpec.errors(parser).filter(([, failure]) => failure).map(([message]) => message);
79489
79508
  }
79509
+ if (preExcludeError) {
79510
+ mapResults.errors.push(preExcludeError);
79511
+ }
79490
79512
  if (finalSpec.additionalData?.type === "listing") {
79491
79513
  const { lookups, info, collect } = finalSpec.additionalData.collect(parser);
79492
79514
  const collectByValue = collect.map((item) => {
@@ -79538,6 +79560,19 @@ function collectMappings(inputFilePath, dicomData, mappingOptions) {
79538
79560
  mapResults.outputFilePath = parts.join("/");
79539
79561
  }
79540
79562
  }
79563
+ try {
79564
+ if (finalSpec.postExclude?.({
79565
+ ...parser,
79566
+ outputFilePath: mapResults.outputFilePath
79567
+ })) {
79568
+ mapResults.excluded = "post";
79569
+ return [naturalData, mapResults];
79570
+ }
79571
+ } catch (e4) {
79572
+ mapResults.errors.push(
79573
+ `postExclude threw an error: ${e4 instanceof Error ? e4.message : String(e4)} \u2014 treating file as included (fail-safe)`
79574
+ );
79575
+ }
79541
79576
  if (!mappingOptions.skipModifications) {
79542
79577
  const dicomMap = finalSpec.modifyDicomHeader(parser);
79543
79578
  for (const attrPath in dicomMap) {
@@ -79804,6 +79839,12 @@ async function loadS3Client() {
79804
79839
  }
79805
79840
 
79806
79841
  // src/curateOne.ts
79842
+ function specHasFilter(mappingOptions) {
79843
+ if (mappingOptions.curationSpec === "none")
79844
+ return false;
79845
+ const composed = composeSpecs(mappingOptions.curationSpec());
79846
+ return !!(composed.preExclude ?? composed.postExclude);
79847
+ }
79807
79848
  async function curateOne({
79808
79849
  fileInfo,
79809
79850
  outputTarget,
@@ -79950,7 +79991,8 @@ async function curateOne({
79950
79991
  };
79951
79992
  return retval;
79952
79993
  };
79953
- if (canSkip && previousSourceFileInfo) {
79994
+ const hasFilter = specHasFilter(mappingOptions);
79995
+ if (canSkip && previousSourceFileInfo && !hasFilter) {
79954
79996
  return noMapResult();
79955
79997
  }
79956
79998
  let mappedDicomData;
@@ -79997,6 +80039,18 @@ async function curateOne({
79997
80039
  write: () => fileArrayBuffer
79998
80040
  };
79999
80041
  }
80042
+ if (clonedMapResults.excluded) {
80043
+ clonedMapResults.mappingRequired = false;
80044
+ clonedMapResults.fileInfo = {
80045
+ name: fileInfo.name,
80046
+ size: fileInfo.size,
80047
+ path: fileInfo.path,
80048
+ mtime,
80049
+ preMappedHash
80050
+ };
80051
+ clonedMapResults.curationTime = performance.now() - startTime;
80052
+ return clonedMapResults;
80053
+ }
80000
80054
  clonedMapResults.mappingRequired = true;
80001
80055
  } else {
80002
80056
  mappedDicomData = {
@@ -35345,6 +35345,16 @@ function composeSpecs(specOrComposedSpec) {
35345
35345
  return [...prev(p), ...next(p)];
35346
35346
  };
35347
35347
  }
35348
+ if (spec.preExclude) {
35349
+ const prev = final.preExclude;
35350
+ const next = spec.preExclude;
35351
+ final.preExclude = prev ? (p) => prev(p) || next(p) : next;
35352
+ }
35353
+ if (spec.postExclude) {
35354
+ const prev = final.postExclude;
35355
+ const next = spec.postExclude;
35356
+ final.postExclude = prev ? (p) => prev(p) || next(p) : next;
35357
+ }
35348
35358
  }
35349
35359
  final.dicomPS315EOptions = mergePs315(ps315Chain);
35350
35360
  return final;
@@ -35477,9 +35487,21 @@ function collectMappings(inputFilePath, dicomData, mappingOptions) {
35477
35487
  mappingOptions.columnMappings,
35478
35488
  finalSpec.additionalData
35479
35489
  );
35490
+ let preExcludeError;
35491
+ try {
35492
+ if (finalSpec.preExclude?.(parser)) {
35493
+ mapResults.excluded = "pre";
35494
+ return [naturalData, mapResults];
35495
+ }
35496
+ } catch (e) {
35497
+ preExcludeError = `preExclude threw an error: ${e instanceof Error ? e.message : String(e)} \u2014 treating file as included (fail-safe)`;
35498
+ }
35480
35499
  if (!mappingOptions.skipValidation) {
35481
35500
  mapResults.errors = finalSpec.errors(parser).filter(([, failure]) => failure).map(([message]) => message);
35482
35501
  }
35502
+ if (preExcludeError) {
35503
+ mapResults.errors.push(preExcludeError);
35504
+ }
35483
35505
  if (finalSpec.additionalData?.type === "listing") {
35484
35506
  const { lookups, info, collect } = finalSpec.additionalData.collect(parser);
35485
35507
  const collectByValue = collect.map((item) => {
@@ -35531,6 +35553,19 @@ function collectMappings(inputFilePath, dicomData, mappingOptions) {
35531
35553
  mapResults.outputFilePath = parts.join("/");
35532
35554
  }
35533
35555
  }
35556
+ try {
35557
+ if (finalSpec.postExclude?.({
35558
+ ...parser,
35559
+ outputFilePath: mapResults.outputFilePath
35560
+ })) {
35561
+ mapResults.excluded = "post";
35562
+ return [naturalData, mapResults];
35563
+ }
35564
+ } catch (e) {
35565
+ mapResults.errors.push(
35566
+ `postExclude threw an error: ${e instanceof Error ? e.message : String(e)} \u2014 treating file as included (fail-safe)`
35567
+ );
35568
+ }
35534
35569
  if (!mappingOptions.skipModifications) {
35535
35570
  const dicomMap = finalSpec.modifyDicomHeader(parser);
35536
35571
  for (const attrPath in dicomMap) {
@@ -33805,6 +33805,16 @@ function composeSpecs(specOrComposedSpec) {
33805
33805
  return [...prev(p), ...next(p)];
33806
33806
  };
33807
33807
  }
33808
+ if (spec.preExclude) {
33809
+ const prev = final.preExclude;
33810
+ const next = spec.preExclude;
33811
+ final.preExclude = prev ? (p) => prev(p) || next(p) : next;
33812
+ }
33813
+ if (spec.postExclude) {
33814
+ const prev = final.postExclude;
33815
+ const next = spec.postExclude;
33816
+ final.postExclude = prev ? (p) => prev(p) || next(p) : next;
33817
+ }
33808
33818
  }
33809
33819
  final.dicomPS315EOptions = mergePs315(ps315Chain);
33810
33820
  return final;
@@ -35363,6 +35363,16 @@ function composeSpecs(specOrComposedSpec) {
35363
35363
  return [...prev(p), ...next(p)];
35364
35364
  };
35365
35365
  }
35366
+ if (spec.preExclude) {
35367
+ const prev = final.preExclude;
35368
+ const next = spec.preExclude;
35369
+ final.preExclude = prev ? (p) => prev(p) || next(p) : next;
35370
+ }
35371
+ if (spec.postExclude) {
35372
+ const prev = final.postExclude;
35373
+ const next = spec.postExclude;
35374
+ final.postExclude = prev ? (p) => prev(p) || next(p) : next;
35375
+ }
35366
35376
  }
35367
35377
  final.dicomPS315EOptions = mergePs315(ps315Chain);
35368
35378
  return final;
@@ -35495,9 +35505,21 @@ function collectMappings(inputFilePath, dicomData, mappingOptions) {
35495
35505
  mappingOptions.columnMappings,
35496
35506
  finalSpec.additionalData
35497
35507
  );
35508
+ let preExcludeError;
35509
+ try {
35510
+ if (finalSpec.preExclude?.(parser)) {
35511
+ mapResults.excluded = "pre";
35512
+ return [naturalData, mapResults];
35513
+ }
35514
+ } catch (e) {
35515
+ preExcludeError = `preExclude threw an error: ${e instanceof Error ? e.message : String(e)} \u2014 treating file as included (fail-safe)`;
35516
+ }
35498
35517
  if (!mappingOptions.skipValidation) {
35499
35518
  mapResults.errors = finalSpec.errors(parser).filter(([, failure]) => failure).map(([message]) => message);
35500
35519
  }
35520
+ if (preExcludeError) {
35521
+ mapResults.errors.push(preExcludeError);
35522
+ }
35501
35523
  if (finalSpec.additionalData?.type === "listing") {
35502
35524
  const { lookups, info, collect } = finalSpec.additionalData.collect(parser);
35503
35525
  const collectByValue = collect.map((item) => {
@@ -35549,6 +35571,19 @@ function collectMappings(inputFilePath, dicomData, mappingOptions) {
35549
35571
  mapResults.outputFilePath = parts.join("/");
35550
35572
  }
35551
35573
  }
35574
+ try {
35575
+ if (finalSpec.postExclude?.({
35576
+ ...parser,
35577
+ outputFilePath: mapResults.outputFilePath
35578
+ })) {
35579
+ mapResults.excluded = "post";
35580
+ return [naturalData, mapResults];
35581
+ }
35582
+ } catch (e) {
35583
+ mapResults.errors.push(
35584
+ `postExclude threw an error: ${e instanceof Error ? e.message : String(e)} \u2014 treating file as included (fail-safe)`
35585
+ );
35586
+ }
35552
35587
  if (!mappingOptions.skipModifications) {
35553
35588
  const dicomMap = finalSpec.modifyDicomHeader(parser);
35554
35589
  for (const attrPath in dicomMap) {
@@ -61208,38 +61208,6 @@ var require_dist_cjs71 = __commonJS({
61208
61208
  // src/curateOne.ts
61209
61209
  var dcmjs7 = __toESM(require_dcmjs(), 1);
61210
61210
 
61211
- // src/createNestedDirectories.ts
61212
- async function createNestedDirectories(topLevelDirectoryHandle, path) {
61213
- const pathSegments = path.split("/").filter((segment) => segment !== "");
61214
- let currentDirectoryHandle = topLevelDirectoryHandle;
61215
- for (const segment of pathSegments) {
61216
- try {
61217
- const entry = await currentDirectoryHandle.getDirectoryHandle(segment, {
61218
- create: false
61219
- });
61220
- currentDirectoryHandle = entry;
61221
- } catch (error2) {
61222
- if (error2.name === "NotFoundError") {
61223
- const entry = await currentDirectoryHandle.getDirectoryHandle(segment, {
61224
- create: true
61225
- });
61226
- currentDirectoryHandle = entry;
61227
- } else {
61228
- return false;
61229
- }
61230
- }
61231
- }
61232
- return currentDirectoryHandle;
61233
- }
61234
-
61235
- // src/curateDict.ts
61236
- var dcmjs6 = __toESM(require_dcmjs(), 1);
61237
- var import_lodash4 = __toESM(require_lodash(), 1);
61238
-
61239
- // src/collectMappings.ts
61240
- var dcmjs4 = __toESM(require_dcmjs(), 1);
61241
- var import_lodash3 = __toESM(require_lodash(), 1);
61242
-
61243
61211
  // src/config/specVersion.ts
61244
61212
  var specVersion = "3.0";
61245
61213
 
@@ -73061,11 +73029,53 @@ function composeSpecs(specOrComposedSpec) {
73061
73029
  return [...prev(p4), ...next(p4)];
73062
73030
  };
73063
73031
  }
73032
+ if (spec.preExclude) {
73033
+ const prev = final.preExclude;
73034
+ const next = spec.preExclude;
73035
+ final.preExclude = prev ? (p4) => prev(p4) || next(p4) : next;
73036
+ }
73037
+ if (spec.postExclude) {
73038
+ const prev = final.postExclude;
73039
+ const next = spec.postExclude;
73040
+ final.postExclude = prev ? (p4) => prev(p4) || next(p4) : next;
73041
+ }
73064
73042
  }
73065
73043
  final.dicomPS315EOptions = mergePs315(ps315Chain);
73066
73044
  return final;
73067
73045
  }
73068
73046
 
73047
+ // src/createNestedDirectories.ts
73048
+ async function createNestedDirectories(topLevelDirectoryHandle, path) {
73049
+ const pathSegments = path.split("/").filter((segment) => segment !== "");
73050
+ let currentDirectoryHandle = topLevelDirectoryHandle;
73051
+ for (const segment of pathSegments) {
73052
+ try {
73053
+ const entry = await currentDirectoryHandle.getDirectoryHandle(segment, {
73054
+ create: false
73055
+ });
73056
+ currentDirectoryHandle = entry;
73057
+ } catch (error2) {
73058
+ if (error2.name === "NotFoundError") {
73059
+ const entry = await currentDirectoryHandle.getDirectoryHandle(segment, {
73060
+ create: true
73061
+ });
73062
+ currentDirectoryHandle = entry;
73063
+ } else {
73064
+ return false;
73065
+ }
73066
+ }
73067
+ }
73068
+ return currentDirectoryHandle;
73069
+ }
73070
+
73071
+ // src/curateDict.ts
73072
+ var dcmjs6 = __toESM(require_dcmjs(), 1);
73073
+ var import_lodash4 = __toESM(require_lodash(), 1);
73074
+
73075
+ // src/collectMappings.ts
73076
+ var dcmjs4 = __toESM(require_dcmjs(), 1);
73077
+ var import_lodash3 = __toESM(require_lodash(), 1);
73078
+
73069
73079
  // src/getParser.ts
73070
73080
  var dcmjs3 = __toESM(require_dcmjs(), 1);
73071
73081
  var import_lodash2 = __toESM(require_lodash(), 1);
@@ -73193,9 +73203,21 @@ function collectMappings(inputFilePath, dicomData, mappingOptions) {
73193
73203
  mappingOptions.columnMappings,
73194
73204
  finalSpec.additionalData
73195
73205
  );
73206
+ let preExcludeError;
73207
+ try {
73208
+ if (finalSpec.preExclude?.(parser)) {
73209
+ mapResults.excluded = "pre";
73210
+ return [naturalData, mapResults];
73211
+ }
73212
+ } catch (e4) {
73213
+ preExcludeError = `preExclude threw an error: ${e4 instanceof Error ? e4.message : String(e4)} \u2014 treating file as included (fail-safe)`;
73214
+ }
73196
73215
  if (!mappingOptions.skipValidation) {
73197
73216
  mapResults.errors = finalSpec.errors(parser).filter(([, failure]) => failure).map(([message]) => message);
73198
73217
  }
73218
+ if (preExcludeError) {
73219
+ mapResults.errors.push(preExcludeError);
73220
+ }
73199
73221
  if (finalSpec.additionalData?.type === "listing") {
73200
73222
  const { lookups, info, collect } = finalSpec.additionalData.collect(parser);
73201
73223
  const collectByValue = collect.map((item) => {
@@ -73247,6 +73269,19 @@ function collectMappings(inputFilePath, dicomData, mappingOptions) {
73247
73269
  mapResults.outputFilePath = parts.join("/");
73248
73270
  }
73249
73271
  }
73272
+ try {
73273
+ if (finalSpec.postExclude?.({
73274
+ ...parser,
73275
+ outputFilePath: mapResults.outputFilePath
73276
+ })) {
73277
+ mapResults.excluded = "post";
73278
+ return [naturalData, mapResults];
73279
+ }
73280
+ } catch (e4) {
73281
+ mapResults.errors.push(
73282
+ `postExclude threw an error: ${e4 instanceof Error ? e4.message : String(e4)} \u2014 treating file as included (fail-safe)`
73283
+ );
73284
+ }
73250
73285
  if (!mappingOptions.skipModifications) {
73251
73286
  const dicomMap = finalSpec.modifyDicomHeader(parser);
73252
73287
  for (const attrPath in dicomMap) {
@@ -73513,6 +73548,12 @@ async function loadS3Client() {
73513
73548
  }
73514
73549
 
73515
73550
  // src/curateOne.ts
73551
+ function specHasFilter(mappingOptions) {
73552
+ if (mappingOptions.curationSpec === "none")
73553
+ return false;
73554
+ const composed = composeSpecs(mappingOptions.curationSpec());
73555
+ return !!(composed.preExclude ?? composed.postExclude);
73556
+ }
73516
73557
  async function curateOne({
73517
73558
  fileInfo,
73518
73559
  outputTarget,
@@ -73659,7 +73700,8 @@ async function curateOne({
73659
73700
  };
73660
73701
  return retval;
73661
73702
  };
73662
- if (canSkip && previousSourceFileInfo) {
73703
+ const hasFilter = specHasFilter(mappingOptions);
73704
+ if (canSkip && previousSourceFileInfo && !hasFilter) {
73663
73705
  return noMapResult();
73664
73706
  }
73665
73707
  let mappedDicomData;
@@ -73706,6 +73748,18 @@ async function curateOne({
73706
73748
  write: () => fileArrayBuffer
73707
73749
  };
73708
73750
  }
73751
+ if (clonedMapResults.excluded) {
73752
+ clonedMapResults.mappingRequired = false;
73753
+ clonedMapResults.fileInfo = {
73754
+ name: fileInfo.name,
73755
+ size: fileInfo.size,
73756
+ path: fileInfo.path,
73757
+ mtime,
73758
+ preMappedHash
73759
+ };
73760
+ clonedMapResults.curationTime = performance.now() - startTime;
73761
+ return clonedMapResults;
73762
+ }
73709
73763
  clonedMapResults.mappingRequired = true;
73710
73764
  } else {
73711
73765
  mappedDicomData = {
package/dist/esm/index.js CHANGED
@@ -80855,6 +80855,16 @@ function composeSpecs(specOrComposedSpec) {
80855
80855
  return [...prev(p4), ...next(p4)];
80856
80856
  };
80857
80857
  }
80858
+ if (spec.preExclude) {
80859
+ const prev = final.preExclude;
80860
+ const next = spec.preExclude;
80861
+ final.preExclude = prev ? (p4) => prev(p4) || next(p4) : next;
80862
+ }
80863
+ if (spec.postExclude) {
80864
+ const prev = final.postExclude;
80865
+ const next = spec.postExclude;
80866
+ final.postExclude = prev ? (p4) => prev(p4) || next(p4) : next;
80867
+ }
80858
80868
  }
80859
80869
  final.dicomPS315EOptions = mergePs315(ps315Chain);
80860
80870
  return final;
@@ -81047,9 +81057,21 @@ function collectMappings(inputFilePath, dicomData, mappingOptions) {
81047
81057
  mappingOptions.columnMappings,
81048
81058
  finalSpec.additionalData
81049
81059
  );
81060
+ let preExcludeError;
81061
+ try {
81062
+ if (finalSpec.preExclude?.(parser)) {
81063
+ mapResults.excluded = "pre";
81064
+ return [naturalData, mapResults];
81065
+ }
81066
+ } catch (e4) {
81067
+ preExcludeError = `preExclude threw an error: ${e4 instanceof Error ? e4.message : String(e4)} \u2014 treating file as included (fail-safe)`;
81068
+ }
81050
81069
  if (!mappingOptions.skipValidation) {
81051
81070
  mapResults.errors = finalSpec.errors(parser).filter(([, failure]) => failure).map(([message]) => message);
81052
81071
  }
81072
+ if (preExcludeError) {
81073
+ mapResults.errors.push(preExcludeError);
81074
+ }
81053
81075
  if (finalSpec.additionalData?.type === "listing") {
81054
81076
  const { lookups, info, collect } = finalSpec.additionalData.collect(parser);
81055
81077
  const collectByValue = collect.map((item) => {
@@ -81101,6 +81123,19 @@ function collectMappings(inputFilePath, dicomData, mappingOptions) {
81101
81123
  mapResults.outputFilePath = parts.join("/");
81102
81124
  }
81103
81125
  }
81126
+ try {
81127
+ if (finalSpec.postExclude?.({
81128
+ ...parser,
81129
+ outputFilePath: mapResults.outputFilePath
81130
+ })) {
81131
+ mapResults.excluded = "post";
81132
+ return [naturalData, mapResults];
81133
+ }
81134
+ } catch (e4) {
81135
+ mapResults.errors.push(
81136
+ `postExclude threw an error: ${e4 instanceof Error ? e4.message : String(e4)} \u2014 treating file as included (fail-safe)`
81137
+ );
81138
+ }
81104
81139
  if (!mappingOptions.skipModifications) {
81105
81140
  const dicomMap = finalSpec.modifyDicomHeader(parser);
81106
81141
  for (const attrPath in dicomMap) {
@@ -81367,6 +81402,12 @@ async function loadS3Client() {
81367
81402
  }
81368
81403
 
81369
81404
  // src/curateOne.ts
81405
+ function specHasFilter(mappingOptions) {
81406
+ if (mappingOptions.curationSpec === "none")
81407
+ return false;
81408
+ const composed = composeSpecs(mappingOptions.curationSpec());
81409
+ return !!(composed.preExclude ?? composed.postExclude);
81410
+ }
81370
81411
  async function curateOne({
81371
81412
  fileInfo,
81372
81413
  outputTarget,
@@ -81513,7 +81554,8 @@ async function curateOne({
81513
81554
  };
81514
81555
  return retval;
81515
81556
  };
81516
- if (canSkip && previousSourceFileInfo) {
81557
+ const hasFilter = specHasFilter(mappingOptions);
81558
+ if (canSkip && previousSourceFileInfo && !hasFilter) {
81517
81559
  return noMapResult();
81518
81560
  }
81519
81561
  let mappedDicomData;
@@ -81560,6 +81602,18 @@ async function curateOne({
81560
81602
  write: () => fileArrayBuffer
81561
81603
  };
81562
81604
  }
81605
+ if (clonedMapResults.excluded) {
81606
+ clonedMapResults.mappingRequired = false;
81607
+ clonedMapResults.fileInfo = {
81608
+ name: fileInfo.name,
81609
+ size: fileInfo.size,
81610
+ path: fileInfo.path,
81611
+ mtime,
81612
+ preMappedHash
81613
+ };
81614
+ clonedMapResults.curationTime = performance.now() - startTime;
81615
+ return clonedMapResults;
81616
+ }
81563
81617
  clonedMapResults.mappingRequired = true;
81564
81618
  } else {
81565
81619
  mappedDicomData = {
@@ -148,6 +148,7 @@ export type TMapResults = {
148
148
  etag?: string;
149
149
  };
150
150
  mappingRequired?: boolean;
151
+ excluded?: 'pre' | 'post';
151
152
  curationTime?: number;
152
153
  };
153
154
  export type TPs315EElement = {
@@ -180,6 +181,9 @@ export type TParser = {
180
181
  FILENAME: symbol;
181
182
  FILEBASENAME: symbol;
182
183
  };
184
+ export type TPostExcludeParser = TParser & {
185
+ outputFilePath: string;
186
+ };
183
187
  type TMappingInputDirect = {
184
188
  type: 'load';
185
189
  collect: Record<string, RegExp | string[]>;
@@ -220,6 +224,8 @@ export type TCurationSpecification<THost extends HostProps = HostProps> = {
220
224
  mapping: TMappedValues;
221
225
  } & (TMappingInputDirect | TMappingInputTwoPass);
222
226
  excludedFiletypes?: string[];
227
+ preExclude?: (parser: TParser) => boolean;
228
+ postExclude?: (parser: TPostExcludeParser) => boolean;
223
229
  };
224
230
  type TProgressMessageBase = {
225
231
  totalFiles?: number;