@docupace/ihub-config 1.1.23 → 1.1.24-admin.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,8 +11,194 @@ import { builtInProviders } from './providers/index.js';
11
11
  import { deepMergeResolvers, } from './resolvers/index.js';
12
12
  import { dump, load } from 'js-yaml';
13
13
  import { buildQuery } from '@docupace/ihub-core/query';
14
+ import jp from 'jsonpath';
14
15
  const FULL_OBJECT_EXPRESSION_PATTERN = /^\${(.*?)}$/;
15
16
  const INLINE_OBJECT_EXPRESSION_PATTERN = /\${(.*?)}/g;
17
+ function isNamedObjectArray(value) {
18
+ return (Array.isArray(value) &&
19
+ value.every((item) => item !== null &&
20
+ typeof item === 'object' &&
21
+ typeof item.name === 'string'));
22
+ }
23
+ // An override element counts as "self-contained" when it carries enough of
24
+ // its own definition to render on its own — a real attribute path (or paths)
25
+ // or a computed expression — as opposed to a thin `{ name, visible }`-style
26
+ // reference that only makes sense resolved against a base/predefined
27
+ // counterpart of the same name. `paths` (plural) is `Column`'s shape;
28
+ // `path` (singular) is `TableCriteria`'s — both must count, or a resolved
29
+ // filter-criterion `$ref` (which only ever has a singular `path`) is wrongly
30
+ // treated as a dangling reference and silently dropped, even though it just
31
+ // resolved successfully.
32
+ function isSelfContainedOverrideElement(item) {
33
+ const paths = item.paths;
34
+ return ((Array.isArray(paths) && paths.length > 0) ||
35
+ (typeof item.path === 'string' && item.path.length > 0) ||
36
+ (typeof item.expression === 'string' && item.expression.length > 0));
37
+ }
38
+ /**
39
+ * Merge semantics for a config sub-path override against the value already
40
+ * at that JSONPath. Generic — used for any typename/sub-path the
41
+ * configPath/subPath mechanism resolves, not just `Table.columns`.
42
+ *
43
+ * When both the current value and the override value are arrays of objects
44
+ * each carrying a string `name`, override elements are merged onto their
45
+ * base counterpart by name (override wins per-attribute, everything else
46
+ * inherited) instead of the override replacing the array wholesale. This is
47
+ * what makes a saved override non-destructive:
48
+ * - Reordering falls out of the override array's own element order.
49
+ * - "Hiding" an element is an attribute override (e.g. `visible: false`) on
50
+ * an element that's still present, not a deletion.
51
+ * - A base element the override never mentions is appended unchanged, so a
52
+ * base element added later still surfaces for someone with an existing
53
+ * override, instead of being permanently absent from their frozen array.
54
+ * - An override element naming something not found in the base is kept
55
+ * as-is when it's self-contained (has its own path(s)/expression — e.g. a
56
+ * hand-authored column), or silently dropped when it's a thin reference
57
+ * whose target (e.g. a predefined column) no longer exists — this never
58
+ * throws; the stale reference just doesn't round-trip into the next save.
59
+ *
60
+ * Any other shape (scalars, plain objects, arrays without a `name` key) is
61
+ * returned untouched, so the caller's plain whole-value replace applies
62
+ * exactly as it did before this merge existed.
63
+ */
64
+ function mergeSubPathOverrideValue(base, override) {
65
+ if (!isNamedObjectArray(override) || !isNamedObjectArray(base)) {
66
+ return override;
67
+ }
68
+ const baseByName = new Map(base.map((item) => [item.name, item]));
69
+ const merged = [];
70
+ const seenNames = new Set();
71
+ for (const overrideItem of override) {
72
+ // Guard against a corrupted/hand-edited override array carrying two
73
+ // elements with the same name — only the first is kept, so the merged
74
+ // result never has duplicate names.
75
+ if (seenNames.has(overrideItem.name)) {
76
+ continue;
77
+ }
78
+ seenNames.add(overrideItem.name);
79
+ const baseItem = baseByName.get(overrideItem.name);
80
+ if (baseItem) {
81
+ merged.push({ ...baseItem, ...overrideItem });
82
+ }
83
+ else if (isSelfContainedOverrideElement(overrideItem)) {
84
+ merged.push(overrideItem);
85
+ }
86
+ // else: a thin reference to a name that no longer resolves — drop it.
87
+ }
88
+ const mergedNames = new Set(merged.map((item) => item.name));
89
+ const untouchedBaseItems = base.filter((item) => !mergedNames.has(item.name));
90
+ return [...merged, ...untouchedBaseItems];
91
+ }
92
+ // Marks each element of a fully-folded named-object array with whether it
93
+ // would still be present (by name) without the narrowest override layer —
94
+ // i.e. whether hiding it (rather than deleting it outright) is required to
95
+ // suppress it, since deleting it would just have the next resolution fold
96
+ // bring it back from a broader layer. `withoutInnermost` is the same
97
+ // broadest-to-narrowest fold as `mergedValue`, minus its last (narrowest)
98
+ // layer — see `applyConfigPathOverride`, where both folds are computed.
99
+ // Only stamps when both values are named-object arrays; any other shape
100
+ // (scalar, plain object, unnamed array) is returned unchanged, mirroring
101
+ // `mergeSubPathOverrideValue`'s own shape guard.
102
+ function stampInherited(mergedValue, withoutInnermost) {
103
+ if (!isNamedObjectArray(mergedValue) ||
104
+ !isNamedObjectArray(withoutInnermost)) {
105
+ return mergedValue;
106
+ }
107
+ const inheritedNames = new Set(withoutInnermost.map((item) => item.name));
108
+ return mergedValue.map((item) => ({
109
+ ...item,
110
+ inherited: inheritedNames.has(item.name),
111
+ }));
112
+ }
113
+ function isRefElement(item) {
114
+ return (item !== null &&
115
+ typeof item === 'object' &&
116
+ typeof item.$ref === 'string');
117
+ }
118
+ // Resolves a config-path override array element's fragment-only `$ref`
119
+ // (e.g. `"#$.predefinedColumns[?(@.name=='email')]"`) against `refRoot` —
120
+ // the config-path's own sibling fields, not another file. This is
121
+ // deliberately narrower than `template`'s YAML `$ref` (processed by
122
+ // `processReferencedObject`/`getRefConfig`, which loads another config
123
+ // file): an override-array `$ref` only ever supports the `#<jsonpath>`
124
+ // self-reference form, resolved with the already-vendored `jsonpath`
125
+ // package (the same one `applyConfigPathOverride` uses for `subPath`
126
+ // strings). A ref that doesn't start with `#`, resolves to nothing, or
127
+ // resolves to a non-object is treated the same as any other unresolved
128
+ // override reference elsewhere in this module — silently dropped rather
129
+ // than thrown, so a stale reference just doesn't round-trip into the next
130
+ // save. Multiple matches take the first, in JSONPath document order.
131
+ //
132
+ // The resolved object keeps the original fragment string under `ref` (not
133
+ // `$ref` — GraphQL field names can't start with `$`) rather than dropping
134
+ // it once resolved. Without this, a client re-saving the array unchanged
135
+ // (e.g. reordering a different column) would have no way to tell this
136
+ // element apart from a plain inline column and would flatten it back into
137
+ // a frozen copy on that save — exactly the staleness problem `$ref` exists
138
+ // to avoid. `Column.ref` (the GraphQL field) surfaces it back to the client
139
+ // so `docupace-ui-main` can keep re-emitting the same `$ref` indefinitely.
140
+ function resolveOverrideElementRef(item, refRoot) {
141
+ const ref = item.$ref;
142
+ if (!ref.startsWith('#')) {
143
+ return undefined;
144
+ }
145
+ const [match] = jp.query(refRoot, ref.slice(1));
146
+ if (match === null || typeof match !== 'object') {
147
+ return undefined;
148
+ }
149
+ const { $ref: _unusedRef, ...localOverrides } = item;
150
+ return {
151
+ ...match,
152
+ ...localOverrides,
153
+ ref,
154
+ };
155
+ }
156
+ // Expands any `$ref` elements of a config-path override array into the
157
+ // full object they point at (merged with whatever local attributes the
158
+ // element carries alongside `$ref`, e.g. `visible`) before the array is
159
+ // handed to `mergeSubPathOverrideValue`. Runs first so that a `{ $ref,
160
+ // visible }` stub — which has no `name` of its own — always arrives at the
161
+ // name-keyed merge as a proper named object; `mergeSubPathOverrideValue`
162
+ // itself needs no `$ref` awareness.
163
+ function expandRefOverrideElements(value, refRoot) {
164
+ if (!Array.isArray(value)) {
165
+ return value;
166
+ }
167
+ return value
168
+ .map((item) => isRefElement(item) ? resolveOverrideElementRef(item, refRoot) : item)
169
+ .filter((item) => item !== undefined);
170
+ }
171
+ // Builds the object a config-path override's `$ref` fragments resolve
172
+ // against: the config being overridden, overlaid with the *raw* (not yet
173
+ // merged) override value of every other simple top-level subPath in this
174
+ // same patch (e.g. `$.predefinedColumns`). Using the raw override value
175
+ // rather than depending on loop order elsewhere in
176
+ // `applyConfigPathOverride` means a `$.columns` override's `$ref` into
177
+ // `$.predefinedColumns` resolves correctly regardless of which subPath
178
+ // happens to be processed first. Only single-segment paths (`$.foo`) are
179
+ // overlaid — nested subPaths (`$.filter.criteria`) fall back to whatever
180
+ // value is already on `config`, which is fine since nothing currently
181
+ // needs to `$ref` into a nested subPath.
182
+ //
183
+ // `subPathPatch` now carries an *ordered* array of layers per jsonPath
184
+ // (broadest to narrowest — see `ConfigurationOverrideResult.subPaths`), not
185
+ // a single already-resolved value. The overlay uses only the narrowest
186
+ // (last) layer, same approximation as before cascading existed: a catalog
187
+ // like `$.predefinedColumns`/`$.predefinedFilters` is always saved as a full
188
+ // replacement at a single scope in practice (never a partial patch spread
189
+ // across tiers), so the narrowest present layer already *is* effectively
190
+ // the whole value — exactly what this overlay used before this function
191
+ // took an array at all.
192
+ function buildRefResolutionRoot(config, subPathPatch) {
193
+ const overlay = {};
194
+ for (const [jsonPath, layers] of Object.entries(subPathPatch)) {
195
+ const topLevelMatch = /^\$\.([^.[]+)$/.exec(jsonPath);
196
+ if (topLevelMatch && layers.length) {
197
+ overlay[topLevelMatch[1]] = layers[layers.length - 1];
198
+ }
199
+ }
200
+ return { ...config, ...overlay };
201
+ }
16
202
  export class ConfigFileStore {
17
203
  localPath;
18
204
  remotePath;
@@ -155,6 +341,7 @@ class ConfigService {
155
341
  configInjectorResolvers;
156
342
  providers;
157
343
  resolvers;
344
+ configurationOverrideProvider;
158
345
  constructor(model, configStores, extensionOptions = {}) {
159
346
  this.configStores = configStores;
160
347
  this.model = model;
@@ -167,6 +354,8 @@ class ConfigService {
167
354
  this.resolvers = extensionOptions.resolvers
168
355
  ? { ...extensionOptions.resolvers }
169
356
  : {};
357
+ this.configurationOverrideProvider =
358
+ extensionOptions.configurationOverrideProvider;
170
359
  if (this.model) {
171
360
  this.generateResolvers();
172
361
  }
@@ -232,12 +421,34 @@ class ConfigService {
232
421
  }
233
422
  return processedData;
234
423
  }
424
+ applyReferenceExtensions(processedData, referenceConfig) {
425
+ if (typeof referenceConfig?.extend !== 'object') {
426
+ return processedData;
427
+ }
428
+ const { extend } = referenceConfig;
429
+ for (const key in extend) {
430
+ const extensionValue = extend[key];
431
+ if (!Array.isArray(extensionValue)) {
432
+ throw new GraphQLError(`$ref.extend.${key} must be an array; received ${typeof extensionValue}`);
433
+ }
434
+ const baseValue = processedData[key];
435
+ if (baseValue === undefined) {
436
+ processedData[key] = extensionValue;
437
+ continue;
438
+ }
439
+ if (!Array.isArray(baseValue)) {
440
+ throw new GraphQLError(`$ref.extend.${key} can only extend an array property; "${key}" resolved to ${typeof baseValue}`);
441
+ }
442
+ processedData[key] = [...baseValue, ...extensionValue];
443
+ }
444
+ return processedData;
445
+ }
235
446
  async processReferencedObject(configPath, obj, context, values) {
236
447
  const referenceConfig = obj.$ref;
237
448
  const refConfigPath = typeof referenceConfig === 'object' && referenceConfig.refPath
238
449
  ? referenceConfig.refPath
239
450
  : referenceConfig;
240
- const imageData = await this.processImageReference(refConfigPath);
451
+ const imageData = await this.processImageReference(refConfigPath, context);
241
452
  if (imageData) {
242
453
  return imageData;
243
454
  }
@@ -246,6 +457,7 @@ class ConfigService {
246
457
  ? this.updatePathValues(referencedData, referenceConfig.path)
247
458
  : referencedData;
248
459
  processedData = this.applyReferenceOverrides(processedData, referenceConfig);
460
+ processedData = this.applyReferenceExtensions(processedData, referenceConfig);
249
461
  processedData = await this.processObjectExpressions(processedData, values, context);
250
462
  return this.processObject(configPath, processedData, context, values);
251
463
  }
@@ -262,7 +474,16 @@ class ConfigService {
262
474
  const builtInQueryResolvers = {
263
475
  pages: () => ({
264
476
  getPageConfig: async (obj, context) => {
265
- const pageConfig = await this.getPageConfig(obj.path, context, true, obj.context);
477
+ // `impersonateRoleId`/`impersonateGeneral` are a plain
478
+ // pass-through onto context here — this engine never inspects
479
+ // identity itself; the host's resolveCurrentIdentity is what
480
+ // interprets them and enforces the admin-only restriction.
481
+ const impersonation = obj.impersonateRoleId
482
+ ? { impersonateRoleId: obj.impersonateRoleId }
483
+ : obj.impersonateGeneral
484
+ ? { impersonateGeneral: true }
485
+ : null;
486
+ const pageConfig = await this.getPageConfig(obj.path, impersonation ? { ...context, ...impersonation } : context, true, obj.context);
266
487
  return pageConfig;
267
488
  },
268
489
  getAIConfig: async (obj, context) => {
@@ -346,29 +567,37 @@ class ConfigService {
346
567
  return deepMergeResolvers(builtInMutationResolvers, hostMutationResolvers);
347
568
  };
348
569
  }
349
- async processConfig(config, context, processProviders) {
570
+ // Applies any stored config-path sub-path overrides across the whole
571
+ // config tree before processConfig's own recursive walk runs, so that
572
+ // overridden values (which may introduce new sub-config) still go
573
+ // through provider injection/__typename dispatch below.
574
+ //
575
+ // rootSubPathOverrides lets a caller that already resolved overrides for
576
+ // the root config (e.g. getConfigFromStore, which fetches the whole-object
577
+ // and sub-path overrides together in one round trip) hand them in directly
578
+ // instead of having the root node re-resolved a second time below; nested
579
+ // nodes discovered deeper in the tree are still resolved individually,
580
+ // since their configPaths aren't known until the tree has been loaded.
581
+ async processConfig(config, context, processProviders, rootSubPathOverrides) {
582
+ const overriddenConfig = await this.applyConfigPathOverride(config, context, processProviders, rootSubPathOverrides);
583
+ return this.processConfigTree(overriddenConfig, context, processProviders);
584
+ }
585
+ async processConfigTree(config, context, processProviders) {
350
586
  if (!config) {
351
587
  return config;
352
588
  }
353
589
  // Handle arrays
354
590
  if (Array.isArray(config)) {
355
- return Promise.all(config.map((item) => this.processConfig(item, context, processProviders)));
591
+ return Promise.all(config.map((item) => this.processConfigTree(item, context, processProviders)));
356
592
  }
357
593
  // Handle objects
358
594
  if (typeof config === 'object') {
359
595
  // Recursively process all properties of config
360
596
  let processedConfig = { ...config };
361
597
  for (const key in processedConfig) {
362
- processedConfig[key] = await this.processConfig(processedConfig[key], context, processProviders);
363
- }
364
- // First process the current object if it has __typename
365
- if (processedConfig?.__typename && processProviders) {
366
- const providerClass = this.providers[processedConfig.__typename];
367
- if (providerClass) {
368
- const provider = new providerClass();
369
- processedConfig = await provider.processConfig(processedConfig, this, context);
370
- }
598
+ processedConfig[key] = await this.processConfigTree(processedConfig[key], context, processProviders);
371
599
  }
600
+ processedConfig = await this.applyTypenameProvider(processedConfig, context, processProviders);
372
601
  return processedConfig;
373
602
  }
374
603
  if (processProviders) {
@@ -376,6 +605,97 @@ class ConfigService {
376
605
  }
377
606
  return config;
378
607
  }
608
+ // Walks the whole config tree up front (pre-order) and, for every object
609
+ // that declares a configPath, deep-sets any stored sub-path overrides
610
+ // (JSONPath-addressed, e.g. "$.columns" or "$.filter.criteria").
611
+ //
612
+ // preFetchedSubPaths is only honored for the root node of the walk (the
613
+ // one being called with a defined third argument); nested calls always
614
+ // omit it so their own configPath gets resolved fresh.
615
+ async applyConfigPathOverride(config, context, processProviders, preFetchedSubPaths) {
616
+ if (!config) {
617
+ return config;
618
+ }
619
+ if (Array.isArray(config)) {
620
+ return Promise.all(config.map((item) => this.applyConfigPathOverride(item, context, processProviders)));
621
+ }
622
+ if (typeof config !== 'object') {
623
+ return config;
624
+ }
625
+ let result = config;
626
+ if (processProviders) {
627
+ const subPathPatch = preFetchedSubPaths !== undefined
628
+ ? preFetchedSubPaths
629
+ : result.configPath && this.configurationOverrideProvider
630
+ ? (await this.configurationOverrideProvider.resolve(result.configPath, context)).subPaths
631
+ : null;
632
+ if (subPathPatch) {
633
+ const refRoot = buildRefResolutionRoot(result, subPathPatch);
634
+ // Deep-clone once, up front — `jp.value` mutates its target in
635
+ // place, and a shallow `{ ...result }` per jsonPath only protects
636
+ // the top level. A nested subPath (e.g. `$.filter.criteria`) would
637
+ // otherwise still mutate `result.filter`, which is the *same*
638
+ // object reference as the original, potentially-shared config
639
+ // (e.g. reused across tables via `extend`/fragment composition).
640
+ // Config values are always plain JSON (from YAML/domain storage),
641
+ // so a JSON round-trip clone is sufficient and avoids
642
+ // `structuredClone`'s cross-realm `instanceof` pitfalls (its output
643
+ // fails `instanceof Object` checks — e.g. inside the `jsonpath`
644
+ // package — when the calling code runs in a different VM
645
+ // realm/context, as test sandboxes commonly do).
646
+ result = JSON.parse(JSON.stringify(result));
647
+ for (const [jsonPath, layers] of Object.entries(subPathPatch)) {
648
+ // A single malformed override (an unparseable `$ref` JSONPath, or
649
+ // a jsonPath that doesn't resolve to a settable location) must not
650
+ // abort resolution of the rest of this config tree — that would
651
+ // take down the page for every user over one bad admin-authored
652
+ // row. Log and skip just this jsonPath instead.
653
+ try {
654
+ const currentValue = jp.value(result, jsonPath);
655
+ const expandedLayers = layers.map((layer) => expandRefOverrideElements(layer, refRoot));
656
+ // `layers` is ordered broadest to narrowest (general, tenant,
657
+ // role, user — see `ConfigurationOverrideResult.subPaths`), so
658
+ // folding them in order onto `currentValue` cascades correctly:
659
+ // each layer only patches what it mentions, inheriting
660
+ // everything else from the fold so far. Folding every layer
661
+ // except the last gives "what this identity would see without
662
+ // its own narrowest override" — the last layer is guaranteed to
663
+ // be whichever tier actually has a row for this identity (the
664
+ // same tier `pickWinner` would have picked pre-cascading), so
665
+ // this is exactly the reference `inherited` needs.
666
+ const mergedValue = expandedLayers.reduce(mergeSubPathOverrideValue, currentValue);
667
+ const withoutInnermost = expandedLayers
668
+ .slice(0, -1)
669
+ .reduce(mergeSubPathOverrideValue, currentValue);
670
+ const finalValue = stampInherited(mergedValue, withoutInnermost);
671
+ const applied = jp.value(result, jsonPath, finalValue);
672
+ if (applied === undefined) {
673
+ throw new Error(`does not resolve to a settable location: "${jsonPath}"`);
674
+ }
675
+ }
676
+ catch (error) {
677
+ log.warn(`Skipping configuration override for "${result.configPath}" at "${jsonPath}": ${error.message}`);
678
+ }
679
+ }
680
+ }
681
+ }
682
+ const overriddenEntries = await Promise.all(Object.entries(result).map(async ([key, value]) => [
683
+ key,
684
+ await this.applyConfigPathOverride(value, context, processProviders),
685
+ ]));
686
+ return Object.fromEntries(overriddenEntries);
687
+ }
688
+ async applyTypenameProvider(processedConfig, context, processProviders) {
689
+ if (!processedConfig?.__typename || !processProviders) {
690
+ return processedConfig;
691
+ }
692
+ const providerClass = this.providers[processedConfig.__typename];
693
+ if (!providerClass) {
694
+ return processedConfig;
695
+ }
696
+ const provider = new providerClass();
697
+ return await provider.processConfig(processedConfig, this, context);
698
+ }
379
699
  async getRefConfig(configPath, refConfigPath, context, values) {
380
700
  let fullPath;
381
701
  if (refConfigPath.startsWith('.')) {
@@ -462,13 +782,21 @@ class ConfigService {
462
782
  };
463
783
  return await processValue(object);
464
784
  }
465
- async processImageReference(refConfigPath) {
785
+ async processImageReference(refConfigPath, context) {
466
786
  const SUPPORTED_IMAGE_FORMATS = ['.jpg', '.jpeg', '.png', '.svg'];
467
787
  // Check if the reference is an image file
468
788
  const ext = path.extname(refConfigPath).toLowerCase();
469
789
  if (!SUPPORTED_IMAGE_FORMATS.includes(ext)) {
470
790
  return null;
471
791
  }
792
+ const override = await this.configurationOverrideProvider?.resolve(refConfigPath, context);
793
+ if (override?.whole?.contentType) {
794
+ return {
795
+ data: override.whole.value,
796
+ mimeType: override.whole.contentType,
797
+ hash: createHash('sha256').update(override.whole.value).digest('hex'),
798
+ };
799
+ }
472
800
  let standardStore;
473
801
  for (const store of this.configStores) {
474
802
  if (store instanceof ConfigFileStore) {
@@ -520,19 +848,31 @@ class ConfigService {
520
848
  return 'application/octet-stream';
521
849
  }
522
850
  }
523
- async getConfigFromStore(path) {
851
+ // Resolves the whole-object override and any sub-path overrides for
852
+ // configPath together, in one provider round trip, instead of fetching
853
+ // them separately at different points of the processing pipeline. The
854
+ // sub-path overrides are handed back alongside the config so callers can
855
+ // thread them into processConfig rather than re-resolving them later.
856
+ async getConfigFromStore(configPath, context) {
857
+ const override = await this.configurationOverrideProvider?.resolve(configPath, context);
858
+ const subPathOverrides = override?.subPaths ?? null;
859
+ if (override?.whole && !override.whole.contentType) {
860
+ return { config: load(override.whole.value), subPathOverrides };
861
+ }
524
862
  const exceptions = [];
525
863
  for (const [index, configStore] of this.configStores.entries()) {
526
864
  try {
527
- return await configStore.getConfig(path);
865
+ const config = await configStore.getConfig(configPath);
866
+ return { config, subPathOverrides };
528
867
  }
529
868
  catch (e) {
530
869
  exceptions.push(e);
531
870
  if (index == this.configStores.length - 1) {
532
- throw new GraphQLError(`Config not found in path in any store: ${path}`, { originalError: exceptions });
871
+ throw new GraphQLError(`Config not found in path in any store: ${configPath}`, { originalError: exceptions });
533
872
  }
534
873
  }
535
874
  }
875
+ return { config: undefined, subPathOverrides };
536
876
  }
537
877
  async hasConfig(configPath) {
538
878
  return (await Promise.all(this.configStores.map((configStore) => configStore.hasConfig(configPath)))).some(Boolean);
@@ -542,9 +882,9 @@ class ConfigService {
542
882
  return this.getDirectConfig(globalPath, context, values, processProviders);
543
883
  }
544
884
  async getDirectConfig(configPath, context, values, processProviders) {
545
- const obj = await this.getConfigFromStore(configPath);
885
+ const { config: obj, subPathOverrides } = await this.getConfigFromStore(configPath, context);
546
886
  const fullObj = await this.processObject(configPath, obj, context, values);
547
- const processedObj = await this.processConfig(fullObj, context, processProviders);
887
+ const processedObj = await this.processConfig(fullObj, context, processProviders, subPathOverrides);
548
888
  return processedObj;
549
889
  }
550
890
  async listDirectConfigs(configPath) {
@@ -895,21 +1235,22 @@ class ConfigService {
895
1235
  }
896
1236
  async getPageConfig(pagePath, context, processConfig, rulesContext) {
897
1237
  const globalPath = path.join('global', 'pages', pagePath);
898
- const obj = await this.getConfigFromStore(globalPath);
1238
+ const { config: obj, subPathOverrides } = await this.getConfigFromStore(globalPath, context);
899
1239
  const fullObj = await this.processObject(globalPath, obj, context, null);
900
1240
  const matchingRuleGroups = await this.getRuleGroups(rulesContext, context);
901
1241
  const configToProcess = this.applyFieldRuleOverrides(fullObj, matchingRuleGroups);
902
- return await this.processConfig(configToProcess, context, processConfig);
1242
+ return await this.processConfig(configToProcess, context, processConfig, subPathOverrides);
903
1243
  }
904
1244
  async init() {
905
1245
  return Promise.all(this.configStores.map((configStore) => configStore.init()));
906
1246
  }
907
1247
  }
908
- export const defaultConfigService = ({ model, configStores, configInjectorResolvers, providers, resolvers, }) => {
1248
+ export const defaultConfigService = ({ model, configStores, configInjectorResolvers, providers, resolvers, configurationOverrideProvider, }) => {
909
1249
  const extensionOptions = {
910
1250
  configInjectorResolvers,
911
1251
  providers,
912
1252
  resolvers,
1253
+ configurationOverrideProvider,
913
1254
  };
914
1255
  return new ConfigService(model, configStores.map((configStore) => new ConfigFileStore(configStore)), extensionOptions);
915
1256
  };