@theunwalked/cardigantime 0.0.8 → 0.0.9

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
@@ -233,6 +233,60 @@ debug: true
233
233
  ./myapp --debug
234
234
  ```
235
235
 
236
+ ### Advanced Usage Examples
237
+
238
+ #### Basic Configuration with Path Resolution
239
+
240
+ ```typescript
241
+ import { create } from '@theunwalked/cardigantime';
242
+ import { z } from 'zod';
243
+
244
+ const MyConfigSchema = z.object({
245
+ apiKey: z.string().min(1),
246
+ timeout: z.number().default(5000),
247
+ debug: z.boolean().default(false),
248
+ contextDirectories: z.array(z.string()).optional(),
249
+ });
250
+
251
+ const cardigantime = create({
252
+ defaults: {
253
+ configDirectory: './config',
254
+ configFile: 'myapp.yaml',
255
+ // Resolve relative paths in contextDirectories relative to config file location
256
+ pathResolution: {
257
+ pathFields: ['contextDirectories'],
258
+ resolvePathArray: ['contextDirectories']
259
+ }
260
+ },
261
+ configShape: MyConfigSchema.shape,
262
+ });
263
+ ```
264
+
265
+ #### Hierarchical Configuration with Custom Array Overlap
266
+
267
+ ```typescript
268
+ const cardigantime = create({
269
+ defaults: {
270
+ configDirectory: '.myapp',
271
+ configFile: 'config.yaml',
272
+ fieldOverlaps: {
273
+ 'features': 'append', // Accumulate features from all levels
274
+ 'excludePatterns': 'prepend', // Higher precedence patterns come first
275
+ 'api.endpoints': 'append', // Nested field configuration
276
+ 'security.allowedOrigins': 'append' // Security settings accumulate
277
+ }
278
+ },
279
+ configShape: MyConfigSchema.shape,
280
+ features: ['config', 'hierarchical'], // Enable hierarchical discovery
281
+ });
282
+ ```
283
+
284
+ This configuration enables powerful composition scenarios where:
285
+ - **Features** from all configuration levels are combined (e.g., base features + project features + local features)
286
+ - **Exclude patterns** are layered with local patterns taking precedence
287
+ - **API endpoints** can be extended at each level
288
+ - **Security settings** accumulate for maximum flexibility
289
+
236
290
  ## Core Concepts
237
291
 
238
292
  ### 1. Configuration Sources & Precedence
@@ -297,6 +351,9 @@ database:
297
351
  ssl: false
298
352
  logging:
299
353
  level: info
354
+ features:
355
+ - auth
356
+ - basic-logging
300
357
 
301
358
  # /home/user/projects/myproject/.kodrdriv/config.yaml (Level 1)
302
359
  database:
@@ -304,14 +361,19 @@ database:
304
361
  ssl: true
305
362
  api:
306
363
  timeout: 5000
364
+ features:
365
+ - advanced-logging
366
+ - metrics
307
367
 
308
368
  # /home/user/projects/myproject/submodule/.kodrdriv/config.yaml (Level 0)
309
369
  database:
310
370
  host: dev.example.com
311
371
  logging:
312
372
  level: debug
373
+ features:
374
+ - debug-mode
313
375
 
314
- # Final merged configuration:
376
+ # Final merged configuration (with default array behavior):
315
377
  database:
316
378
  host: dev.example.com # From Level 0 (highest precedence)
317
379
  port: 5433 # From Level 1
@@ -320,6 +382,79 @@ api:
320
382
  timeout: 5000 # From Level 1
321
383
  logging:
322
384
  level: debug # From Level 0 (highest precedence)
385
+ features:
386
+ - debug-mode # From Level 0 (arrays override by default)
387
+ ```
388
+
389
+ #### Configurable Array Overlap Behavior
390
+
391
+ By default, arrays in hierarchical configurations follow the **override** behavior - arrays from higher precedence levels completely replace arrays from lower precedence levels. However, you can configure custom overlap behavior for array fields:
392
+
393
+ ```typescript
394
+ const cardigantime = create({
395
+ defaults: {
396
+ configDirectory: '.kodrdriv',
397
+ configFile: 'config.yaml',
398
+ fieldOverlaps: {
399
+ 'features': 'append', // Combine features by appending
400
+ 'excludePatterns': 'prepend', // Combine exclude patterns by prepending
401
+ 'middlewares': 'override' // Override middlewares (default behavior)
402
+ }
403
+ },
404
+ configShape: MyConfigSchema.shape,
405
+ features: ['config', 'hierarchical'],
406
+ });
407
+ ```
408
+
409
+ **Available Overlap Modes:**
410
+
411
+ - **`override`** (default): Higher precedence arrays completely replace lower precedence arrays
412
+ - **`append`**: Higher precedence array elements are appended to lower precedence arrays
413
+ - **`prepend`**: Higher precedence array elements are prepended to lower precedence arrays
414
+
415
+ **Example with Custom Array Overlap:**
416
+
417
+ ```yaml
418
+ # /home/user/projects/.kodrdriv/config.yaml (Level 2)
419
+ features: ['auth', 'basic-logging']
420
+ excludePatterns: ['*.tmp', '*.cache']
421
+
422
+ # /home/user/projects/myproject/.kodrdriv/config.yaml (Level 1)
423
+ features: ['advanced-logging', 'metrics']
424
+ excludePatterns: ['*.log']
425
+
426
+ # /home/user/projects/myproject/submodule/.kodrdriv/config.yaml (Level 0)
427
+ features: ['debug-mode']
428
+ excludePatterns: ['*.debug']
429
+ ```
430
+
431
+ With the configuration above (`features: 'append'`, `excludePatterns: 'prepend'`):
432
+
433
+ ```yaml
434
+ # Final merged configuration:
435
+ features:
436
+ - auth # From Level 2
437
+ - basic-logging # From Level 2
438
+ - advanced-logging # From Level 1
439
+ - metrics # From Level 1
440
+ - debug-mode # From Level 0 (appended)
441
+ excludePatterns:
442
+ - "*.debug" # From Level 0 (prepended first)
443
+ - "*.log" # From Level 1 (prepended second)
444
+ - "*.tmp" # From Level 2
445
+ - "*.cache" # From Level 2
446
+ ```
447
+
448
+ **Nested Field Paths:**
449
+
450
+ You can configure overlap behavior for nested array fields using dot notation:
451
+
452
+ ```typescript
453
+ fieldOverlaps: {
454
+ 'api.endpoints': 'append',
455
+ 'database.migrations': 'prepend',
456
+ 'config.features.experimental': 'override'
457
+ }
323
458
  ```
324
459
 
325
460
  #### Enabling Hierarchical Discovery
@@ -590,24 +590,26 @@ const create$1 = (params)=>{
590
590
  }
591
591
  }
592
592
  /**
593
- * Deep merges multiple configuration objects with proper precedence.
593
+ * Deep merges multiple configuration objects with proper precedence and configurable array overlap behavior.
594
594
  *
595
595
  * Objects are merged from lowest precedence to highest precedence,
596
596
  * meaning that properties in later objects override properties in earlier objects.
597
- * Arrays are replaced entirely (not merged).
597
+ * Arrays can be merged using different strategies based on the fieldOverlaps configuration.
598
598
  *
599
599
  * @param configs Array of configuration objects, ordered from lowest to highest precedence
600
+ * @param fieldOverlaps Configuration for how array fields should be merged (optional)
600
601
  * @returns Merged configuration object
601
602
  *
602
603
  * @example
603
604
  * ```typescript
604
605
  * const merged = deepMergeConfigs([
605
- * { api: { timeout: 5000 }, debug: true }, // Lower precedence
606
- * { api: { retries: 3 }, features: ['auth'] }, // Higher precedence
607
- * ]);
608
- * // Result: { api: { timeout: 5000, retries: 3 }, debug: true, features: ['auth'] }
606
+ * { api: { timeout: 5000 }, features: ['auth'] }, // Lower precedence
607
+ * { api: { retries: 3 }, features: ['analytics'] }, // Higher precedence
608
+ * ], {
609
+ * 'features': 'append' // Results in features: ['auth', 'analytics']
610
+ * });
609
611
  * ```
610
- */ function deepMergeConfigs(configs) {
612
+ */ function deepMergeConfigs(configs, fieldOverlaps) {
611
613
  if (configs.length === 0) {
612
614
  return {};
613
615
  }
@@ -617,16 +619,18 @@ const create$1 = (params)=>{
617
619
  };
618
620
  }
619
621
  return configs.reduce((merged, current)=>{
620
- return deepMergeTwo(merged, current);
622
+ return deepMergeTwo(merged, current, fieldOverlaps);
621
623
  }, {});
622
624
  }
623
625
  /**
624
- * Deep merges two objects with proper precedence.
626
+ * Deep merges two objects with proper precedence and configurable array overlap behavior.
625
627
  *
626
628
  * @param target Target object (lower precedence)
627
629
  * @param source Source object (higher precedence)
630
+ * @param fieldOverlaps Configuration for how array fields should be merged (optional)
631
+ * @param currentPath Current field path for nested merging (used internally)
628
632
  * @returns Merged object
629
- */ function deepMergeTwo(target, source) {
633
+ */ function deepMergeTwo(target, source, fieldOverlaps, currentPath = '') {
630
634
  // Handle null/undefined
631
635
  if (source == null) return target;
632
636
  if (target == null) return source;
@@ -634,11 +638,17 @@ const create$1 = (params)=>{
634
638
  if (typeof source !== 'object' || typeof target !== 'object') {
635
639
  return source; // Source takes precedence
636
640
  }
637
- // Handle arrays - replace entirely, don't merge
641
+ // Handle arrays with configurable overlap behavior
638
642
  if (Array.isArray(source)) {
639
- return [
640
- ...source
641
- ];
643
+ if (Array.isArray(target) && fieldOverlaps) {
644
+ const overlapMode = getOverlapModeForPath(currentPath, fieldOverlaps);
645
+ return mergeArrays(target, source, overlapMode);
646
+ } else {
647
+ // Default behavior: replace entirely
648
+ return [
649
+ ...source
650
+ ];
651
+ }
642
652
  }
643
653
  if (Array.isArray(target)) {
644
654
  return source; // Source object replaces target array
@@ -649,17 +659,72 @@ const create$1 = (params)=>{
649
659
  };
650
660
  for(const key in source){
651
661
  if (Object.prototype.hasOwnProperty.call(source, key)) {
662
+ const fieldPath = currentPath ? `${currentPath}.${key}` : key;
652
663
  if (Object.prototype.hasOwnProperty.call(result, key) && typeof result[key] === 'object' && typeof source[key] === 'object' && !Array.isArray(source[key]) && !Array.isArray(result[key])) {
653
664
  // Recursively merge nested objects
654
- result[key] = deepMergeTwo(result[key], source[key]);
665
+ result[key] = deepMergeTwo(result[key], source[key], fieldOverlaps, fieldPath);
655
666
  } else {
656
- // Replace with source value (higher precedence)
657
- result[key] = source[key];
667
+ // Handle arrays and primitives with overlap consideration
668
+ if (Array.isArray(source[key]) && Array.isArray(result[key]) && fieldOverlaps) {
669
+ const overlapMode = getOverlapModeForPath(fieldPath, fieldOverlaps);
670
+ result[key] = mergeArrays(result[key], source[key], overlapMode);
671
+ } else {
672
+ // Replace with source value (higher precedence)
673
+ result[key] = source[key];
674
+ }
658
675
  }
659
676
  }
660
677
  }
661
678
  return result;
662
679
  }
680
+ /**
681
+ * Determines the overlap mode for a given field path.
682
+ *
683
+ * @param fieldPath The current field path (dot notation)
684
+ * @param fieldOverlaps Configuration mapping field paths to overlap modes
685
+ * @returns The overlap mode to use for this field path
686
+ */ function getOverlapModeForPath(fieldPath, fieldOverlaps) {
687
+ // Check for exact match first
688
+ if (fieldPath in fieldOverlaps) {
689
+ return fieldOverlaps[fieldPath];
690
+ }
691
+ // Check for any parent path matches (for nested configurations)
692
+ const pathParts = fieldPath.split('.');
693
+ for(let i = pathParts.length - 1; i > 0; i--){
694
+ const parentPath = pathParts.slice(0, i).join('.');
695
+ if (parentPath in fieldOverlaps) {
696
+ return fieldOverlaps[parentPath];
697
+ }
698
+ }
699
+ // Default to override if no specific configuration found
700
+ return 'override';
701
+ }
702
+ /**
703
+ * Merges two arrays based on the specified overlap mode.
704
+ *
705
+ * @param targetArray The lower precedence array
706
+ * @param sourceArray The higher precedence array
707
+ * @param mode The overlap mode to use
708
+ * @returns The merged array
709
+ */ function mergeArrays(targetArray, sourceArray, mode) {
710
+ switch(mode){
711
+ case 'append':
712
+ return [
713
+ ...targetArray,
714
+ ...sourceArray
715
+ ];
716
+ case 'prepend':
717
+ return [
718
+ ...sourceArray,
719
+ ...targetArray
720
+ ];
721
+ case 'override':
722
+ default:
723
+ return [
724
+ ...sourceArray
725
+ ];
726
+ }
727
+ }
663
728
  /**
664
729
  * Loads configurations from multiple directories and merges them with proper precedence.
665
730
  *
@@ -678,15 +743,19 @@ const create$1 = (params)=>{
678
743
  * configDirName: '.kodrdriv',
679
744
  * configFileName: 'config.yaml',
680
745
  * startingDir: '/project/subdir',
681
- * maxLevels: 5
746
+ * maxLevels: 5,
747
+ * fieldOverlaps: {
748
+ * 'features': 'append',
749
+ * 'excludePatterns': 'prepend'
750
+ * }
682
751
  * });
683
752
  *
684
- * // result.config contains merged configuration
753
+ * // result.config contains merged configuration with custom array merging
685
754
  * // result.discoveredDirs shows where configs were found
686
755
  * // result.errors contains any non-fatal errors
687
756
  * ```
688
757
  */ async function loadHierarchicalConfig(options) {
689
- const { configFileName, encoding = 'utf8', logger, pathFields, resolvePathArray } = options;
758
+ const { configFileName, encoding = 'utf8', logger, pathFields, resolvePathArray, fieldOverlaps } = options;
690
759
  logger === null || logger === void 0 ? void 0 : logger.verbose('Starting hierarchical configuration loading');
691
760
  // Discover all configuration directories
692
761
  const discoveredDirs = await discoverConfigDirectories(options);
@@ -720,8 +789,8 @@ const create$1 = (params)=>{
720
789
  logger === null || logger === void 0 ? void 0 : logger.debug(errorMsg);
721
790
  }
722
791
  }
723
- // Merge all configurations with proper precedence
724
- const mergedConfig = deepMergeConfigs(configs);
792
+ // Merge all configurations with proper precedence and configurable array overlap
793
+ const mergedConfig = deepMergeConfigs(configs, fieldOverlaps);
725
794
  logger === null || logger === void 0 ? void 0 : logger.verbose(`Hierarchical loading complete. Merged ${configs.length} configurations`);
726
795
  return {
727
796
  config: mergedConfig,
@@ -909,7 +978,8 @@ const create$1 = (params)=>{
909
978
  encoding: options.defaults.encoding,
910
979
  logger,
911
980
  pathFields: (_options_defaults_pathResolution1 = options.defaults.pathResolution) === null || _options_defaults_pathResolution1 === void 0 ? void 0 : _options_defaults_pathResolution1.pathFields,
912
- resolvePathArray: (_options_defaults_pathResolution2 = options.defaults.pathResolution) === null || _options_defaults_pathResolution2 === void 0 ? void 0 : _options_defaults_pathResolution2.resolvePathArray
981
+ resolvePathArray: (_options_defaults_pathResolution2 = options.defaults.pathResolution) === null || _options_defaults_pathResolution2 === void 0 ? void 0 : _options_defaults_pathResolution2.resolvePathArray,
982
+ fieldOverlaps: options.defaults.fieldOverlaps
913
983
  });
914
984
  rawFileConfig = hierarchicalResult.config;
915
985
  if (hierarchicalResult.discoveredDirs.length > 0) {
@@ -1335,9 +1405,15 @@ class ConfigurationError extends Error {
1335
1405
  * pathResolution: {
1336
1406
  * pathFields: ['contextDirectories'],
1337
1407
  * resolvePathArray: ['contextDirectories']
1408
+ * },
1409
+ * // Configure how array fields are merged in hierarchical mode
1410
+ * fieldOverlaps: {
1411
+ * 'features': 'append', // Accumulate features from all levels
1412
+ * 'excludePatterns': 'prepend' // Higher precedence patterns come first
1338
1413
  * }
1339
1414
  * },
1340
1415
  * configShape: MyConfigSchema.shape,
1416
+ * features: ['config', 'hierarchical'], // Enable hierarchical discovery
1341
1417
  * });
1342
1418
  *
1343
1419
  * // If config file is at ../config/myapp.yaml and contains: