@dcupl/common 2.0.0-beta.2 → 2.0.0-beta.21

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.
Files changed (38) hide show
  1. package/dist/esm/analytics.controller.d.ts +1 -1
  2. package/dist/esm/changeDetection.d.ts +2 -2
  3. package/dist/esm/computeSuggestions.d.ts +2 -2
  4. package/dist/esm/deep-query.service.d.ts +1 -1
  5. package/dist/esm/getAggregation.d.ts +1 -1
  6. package/dist/esm/getFacets.d.ts +26 -3
  7. package/dist/esm/helper.d.ts +11 -1
  8. package/dist/esm/index.d.ts +25 -22
  9. package/dist/esm/index.js +1004 -690
  10. package/dist/esm/index.js.map +1 -1
  11. package/dist/esm/indices.controller.d.ts +7 -1
  12. package/dist/esm/logger/index.d.ts +2 -2
  13. package/dist/esm/pivot.d.ts +2 -2
  14. package/dist/esm/property-parser.d.ts +27 -2
  15. package/dist/esm/query-builder.d.ts +15 -5
  16. package/dist/esm/query.helper.d.ts +1 -1
  17. package/dist/esm/queryData.d.ts +53 -3
  18. package/dist/esm/redact-url.d.ts +9 -0
  19. package/dist/esm/resource-id.d.ts +43 -0
  20. package/dist/esm/scheduling.d.ts +17 -0
  21. package/dist/esm/types/app-loader.types.d.ts +94 -4
  22. package/dist/esm/types/data.types.d.ts +7 -3
  23. package/dist/esm/types/dcupl.types.d.ts +26 -12
  24. package/dist/esm/types/facets.types.d.ts +14 -1
  25. package/dist/esm/types/filter.types.d.ts +3 -3
  26. package/dist/esm/types/group-by.types.d.ts +2 -2
  27. package/dist/esm/types/index.d.ts +17 -16
  28. package/dist/esm/types/internal.types.d.ts +1 -1
  29. package/dist/esm/types/key-property.types.d.ts +11 -0
  30. package/dist/esm/types/list.types.d.ts +25 -4
  31. package/dist/esm/types/model.types.d.ts +54 -5
  32. package/dist/esm/types/quality.types.d.ts +87 -1
  33. package/dist/esm/types/query.types.d.ts +115 -20
  34. package/dist/esm/types/section.types.d.ts +2 -2
  35. package/dist/esm/types/suggestion.types.d.ts +6 -1
  36. package/dist/node/index.cjs +2 -2
  37. package/dist/node/index.cjs.map +1 -1
  38. package/package.json +2 -4
@@ -1,4 +1,4 @@
1
- import { InverseIndexValuesMap, InverseIndexDataValuesMap, ListItem } from './types';
1
+ import { InverseIndexValuesMap, InverseIndexDataValuesMap, ListItem } from './types/index.js';
2
2
  /**
3
3
  * Statistics about indices for monitoring
4
4
  */
@@ -15,6 +15,12 @@ export declare class IndicesController {
15
15
  private indexMetadata;
16
16
  constructor();
17
17
  reset(attributeKey?: string): void;
18
+ /**
19
+ * Removes every index the predicate selects. Used to drop indices that
20
+ * were built from now-stale data; the next read lazily rebuilds them via
21
+ * {@link getOrCreateIndex}.
22
+ */
23
+ resetIndicesWhere(predicate: (indexKey: string) => boolean): void;
18
24
  fillDataMap(attributeKey: string, attributeValue: Array<string | undefined | ListItem>): void;
19
25
  fillInverseIndex(attributeKey: string, originKey: string, foreignKeys: Array<string | undefined | ListItem>): void;
20
26
  getIndex(attributeKey: string, context?: any): InverseIndexValuesMap | undefined;
@@ -1,2 +1,2 @@
1
- export { Logger, logger } from './logger';
2
- export type { LogLevel, LoggerConfig, LogEntry, TrackedErrorEntry } from './logger';
1
+ export { Logger, logger } from './logger.js';
2
+ export type { LogLevel, LoggerConfig, LogEntry, TrackedErrorEntry } from './logger.js';
@@ -1,5 +1,5 @@
1
- import { IndicesController } from './indices.controller';
2
- import { ListItem, Aggregation, AggregationOptions } from './types';
1
+ import { IndicesController } from './indices.controller.js';
2
+ import { ListItem, Aggregation, AggregationOptions } from './types/index.js';
3
3
  type PivotRowOption = {
4
4
  attribute: string;
5
5
  calculateTotals?: boolean;
@@ -1,5 +1,30 @@
1
- import { Property, PropertyType } from './types';
2
- export declare const getPropertiesForData: (data: any[], deep: boolean) => Property[];
1
+ import { Property, PropertyType } from './types/index.js';
2
+ export declare const coerceStringType: (value: string) => PropertyType;
3
+ export declare const looksDateLike: (value: string) => boolean;
4
+ export type GetPropertiesForDataOptions = {
5
+ deep?: boolean;
6
+ sampleSize?: number;
7
+ /** @deprecated No longer honored — empty values are always skipped for type
8
+ * inference and counted toward ColumnDiagnostic.emptyRate. */
9
+ ignoreEmpty?: boolean;
10
+ };
11
+ export type ColumnDiagnostic = {
12
+ column: string;
13
+ inferredType: PropertyType;
14
+ observedTypes: PropertyType[];
15
+ emptyRate: number;
16
+ conflict: boolean;
17
+ hint?: string;
18
+ };
19
+ export type InferModelOptions = GetPropertiesForDataOptions & {
20
+ coerceStrings?: boolean;
21
+ };
22
+ export type InferModelResult = {
23
+ properties: Property[];
24
+ diagnostics: ColumnDiagnostic[];
25
+ };
26
+ export declare const inferModel: (data: any[], options?: InferModelOptions) => InferModelResult;
27
+ export declare const getPropertiesForData: (data: any[], deepOrOptions?: boolean | GetPropertiesForDataOptions) => Property[];
3
28
  export declare const isInt: (n: any) => boolean;
4
29
  export declare const isFloat: (n: any) => boolean;
5
30
  export declare const getDcuplPropertyTypeForNativeType: (value: any) => PropertyType;
@@ -1,5 +1,5 @@
1
- import { DcuplGlobalQueryOptions, DcuplItemsOptions, DcuplQuery, DcuplQueryGroup } from './types';
2
- import { DeepQueryService } from './deep-query.service';
1
+ import { DcuplGlobalQueryOptions, DcuplItemsOptions, DcuplQuery, DcuplQueryGroup } from './types/index.js';
2
+ import { DeepQueryService } from './deep-query.service.js';
3
3
  export type QueryAttributeEntry = {
4
4
  attribute: string;
5
5
  reference?: string;
@@ -69,7 +69,8 @@ export declare class DcuplQueryBuilder {
69
69
  * - {@link replaceQuery} - Replace complete query
70
70
  *
71
71
  * @param queryToApply - Query, condition, or array to apply
72
- * @param options - Mode: 'add' (default) or 'set' (replace)
72
+ * @param options - Mode: 'add' (default) or 'set' (replace). In 'add' mode a group
73
+ * without `groupKey` is always appended as a new group, never merged into another.
73
74
  * @param relevantQuery - Target query (defaults to current)
74
75
  * @returns Updated query
75
76
  */
@@ -187,7 +188,10 @@ export declare class DcuplQueryBuilder {
187
188
  /**
188
189
  * Add query condition(s) to the current query.
189
190
  * Accepts single condition or array of conditions.
190
- * Each condition is added to its respective attribute group.
191
+ * Each condition is added to its respective attribute group (the group whose
192
+ * groupKey equals the condition's attribute; created as an `'or'` group if missing).
193
+ * To combine conditions in an unkeyed group, use {@link addGroup} — unkeyed groups
194
+ * are always appended as new groups and never merged.
191
195
  *
192
196
  * Clearer alternative to `applyQuery(condition, { mode: 'add' })`.
193
197
  *
@@ -209,7 +213,13 @@ export declare class DcuplQueryBuilder {
209
213
  addCondition(condition: DcuplQuery | DcuplQuery[]): DcuplGlobalQueryOptions<any>;
210
214
  /**
211
215
  * Add a query group to the current query.
212
- * If a group with the same groupKey exists, extends it; otherwise creates new group.
216
+ * - Keyed group (`groupKey` set): if a group with the same groupKey exists, its queries
217
+ * are extended and its groupType is replaced by the given one (if set); otherwise a new
218
+ * group is created.
219
+ * - Unkeyed group (no `groupKey`): always appended as a new group; it is never merged
220
+ * into another group, so each unkeyed group keeps its own groupType.
221
+ *
222
+ * A newly created group without `groupType` is stored as `'or'`.
213
223
  *
214
224
  * @param group - Query group to add
215
225
  * @returns Updated query
@@ -1,4 +1,4 @@
1
- import { BooleanFilterItem, BooleanFilterOptions, DateFilterItem, DateFilterOptions, DcuplQueryGroup, DcuplQueryStatement, NumberFilterItem, NumberFilterOptions, ReferenceFilterItem, ReferenceFilterOptions, TextFilterItem, TextFilterOptions } from './types';
1
+ import { BooleanFilterItem, BooleanFilterOptions, DateFilterItem, DateFilterOptions, DcuplQueryGroup, DcuplQueryStatement, NumberFilterItem, NumberFilterOptions, ReferenceFilterItem, ReferenceFilterOptions, TextFilterItem, TextFilterOptions } from './types/index.js';
2
2
  export declare const generateTextQuery: (options: TextFilterOptions, filterItem?: TextFilterItem) => DcuplQueryGroup;
3
3
  export declare const generateBooleanQuery: (options: BooleanFilterOptions, filterItem?: BooleanFilterItem) => DcuplQueryGroup;
4
4
  export declare const generateNumberAndDateQuery: (options: NumberFilterOptions | DateFilterOptions, filterItem?: NumberFilterItem | DateFilterItem) => DcuplQueryGroup;
@@ -1,5 +1,5 @@
1
- import { ListItem, InverseIndexReferenceMap, DcuplQueryTransformOptions, CustomOperatorFn, DcuplGlobalQueryOptions } from './types';
2
- import { ScriptController } from './script.controller';
1
+ import { ListItem, InverseIndexReferenceMap, DcuplQueryTransformOptions, CustomOperatorFn, DcuplGlobalQueryOptions } from './types/index.js';
2
+ import { ScriptController } from './script.controller.js';
3
3
  import { ModelParser } from '@dcupl/core/model-parser';
4
4
  import { ItemService } from '@dcupl/core/catalog';
5
5
  export declare class QueryManager {
@@ -12,9 +12,59 @@ export declare class QueryManager {
12
12
  registerCustomOperator(operator: string, fn: CustomOperatorFn): void;
13
13
  transformValue(value: any, transformers: DcuplQueryTransformOptions[]): any;
14
14
  private evaluateQuery;
15
+ /** A missing value never compares — in JS `null >= 0` is true. */
16
+ private compareValue;
17
+ /**
18
+ * The value a condition compares against. A dotted path that runs through a
19
+ * list (a multi-valued reference, e.g. `memberships.role`) fans out into the
20
+ * list of leaf values, the same values the attribute's inverse index holds —
21
+ * `lodash.get` cannot step into an array and would answer `undefined`. An
22
+ * index (`tags.0`) or `length` still reads the list itself, as with `get`.
23
+ */
24
+ private getAttributeValue;
25
+ /**
26
+ * Applies `predicate` to a single value, or to each value of a list per
27
+ * `arrayValueHandling`. `every` needs at least one value: an empty list
28
+ * matches nothing (and so matches under `invert`).
29
+ */
30
+ private matchValues;
31
+ /**
32
+ * `eq` against a list compares each value, as the inverse index does; a
33
+ * query value that is itself a list still compares the whole list.
34
+ */
35
+ private queryEq;
36
+ /**
37
+ * `in`: a value matches if it equals (as `eq` compares) any listed value; a
38
+ * list of values matches per `arrayValueHandling`. An empty list matches nothing.
39
+ */
40
+ private queryIn;
41
+ /**
42
+ * The inverse-index key a query value looks up: a string, or the key of a
43
+ * `{ key }` object. Anything else has no key and is compared by a scan.
44
+ */
45
+ private getIndexKey;
46
+ /**
47
+ * Equality the way the inverse index keys values: a string (or `{ key }`)
48
+ * query value matches a reference by its key and a date by its ISO string.
49
+ */
50
+ private equalsQueryValue;
51
+ /**
52
+ * An inverse index answers `eq` as "some value equals", untransformed — only
53
+ * use it when that is the question.
54
+ */
55
+ private canUseIndex;
15
56
  private evaluateEqOperator;
57
+ /**
58
+ * `in` from the inverse index when it can answer: the union of the index
59
+ * sets of the listed values. A value the index cannot look up (not a string
60
+ * or `{ key }`, or not in the index) is answered by a scan of the rest, so
61
+ * the result equals the scan's.
62
+ */
63
+ private evaluateInOperator;
64
+ /** Evaluates `query` on each item, resolving a deep path's references first. */
65
+ private scanDataset;
16
66
  private equalRegexFn;
17
- queryFind(entryValue: any | any[], queryValue: any, arrayValueHandling: 'some' | 'every'): boolean;
67
+ queryFind(entryValue: any | any[], queryValue: any, arrayValueHandling: 'some' | 'every', regexFlags?: string): boolean;
18
68
  private validateQuery;
19
69
  private getEvaluatedQueryDataset;
20
70
  private handleArrayStarQuery;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * `url` with the values of secret query parameters replaced by `<redacted>`.
3
+ *
4
+ * For error messages and log lines only — the request itself keeps the real values. The app
5
+ * loader appends the project `api-key` to every dcupl CDN request, and a failed fetch used to
6
+ * carry that URL verbatim into the log output and the quality error. Works on the raw string so
7
+ * nothing else is re-encoded or reordered. Same parameter set as console-api's `redactUrl`.
8
+ */
9
+ export declare function redactUrl(url: string): string;
@@ -0,0 +1,43 @@
1
+ /** Authored `resource.key` values are namespaced so they can never collide with a derived hash. */
2
+ export declare const AUTHORED_RESOURCE_ID_PREFIX = "k:";
3
+ export declare const DERIVED_RESOURCE_ID_PREFIX = "h:";
4
+ /**
5
+ * The fields a resource identity is derived from.
6
+ *
7
+ * Structural rather than `AppLoaderConfiguration.Resource` so the same function serves a raw config
8
+ * entry and a runtime progress item — `model` only exists on data resources, `rawUrl` only at runtime.
9
+ */
10
+ export type ResourceIdentityInput = {
11
+ key?: string;
12
+ type: string;
13
+ url: string;
14
+ rawUrl?: string;
15
+ model?: string;
16
+ };
17
+ /**
18
+ * A stable id for one resource.
19
+ *
20
+ * An authored `key` wins and is used verbatim — that id survives a url edit, which is the reason to
21
+ * author keys at all. Otherwise the id is a hash of the RAW url plus type/model. Deriving from the
22
+ * raw url (never the resolved one) is what makes the id identical on both sides of the config↔runtime
23
+ * boundary, since `${variable}` substitution happens only on the runtime side.
24
+ *
25
+ * A derived id therefore changes when the url, type or model changes. That is the accepted limit:
26
+ * it is stable across runs, not across edits.
27
+ *
28
+ * `occurrence` disambiguates config entries that are identical in type/url/model. Prefer
29
+ * `resolveResourceIds` for a list — it assigns occurrences in document order, so reordering the
30
+ * config does not churn ids.
31
+ */
32
+ export declare function resolveResourceId(resource: ResourceIdentityInput, occurrence?: number): string;
33
+ /**
34
+ * Ids for a resource list, positionally aligned with the input, guaranteed unique.
35
+ *
36
+ * Uniqueness is enforced on the OUTPUT, not just on the (type, url, model) basis, because two other
37
+ * things can collide: two entries authored with the same `key` (nothing in the config validates key
38
+ * uniqueness — `loader.resources.get()` simply returns the first match), and a hash collision, which
39
+ * `generateObjectHash` admits by construction since it is a 32-bit rolling hash. Random collisions
40
+ * are vanishingly rare at realistic config sizes, but a duplicate id silently merges two nodes in a
41
+ * consumer's graph, so it is worth ruling out rather than reasoning about.
42
+ */
43
+ export declare function resolveResourceIds(resources: readonly ResourceIdentityInput[]): string[];
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Yields control back to the event loop as a macrotask WITHOUT going through `setTimeout`.
3
+ *
4
+ * Browsers throttle DOM timers on hidden/background tabs: Chrome aligns chained timers to
5
+ * ~1 s wake-ups (and to 1 min after five minutes hidden). A loop that awaited
6
+ * `setTimeout(resolve, 0)` per iteration therefore stalled ~1 s per iteration whenever the
7
+ * tab was not in the foreground, turning a sub-second `dcupl.init()` into minutes.
8
+ * `scheduler.yield()`, `setImmediate` and `MessageChannel` tasks are not throttled that way,
9
+ * while still letting rendering and input events run between iterations.
10
+ *
11
+ * Resolution order:
12
+ * 1. `scheduler.yield()` — browsers with the Prioritized Task Scheduling API (Chrome 129+)
13
+ * 2. `setImmediate` — Node.js
14
+ * 3. `MessageChannel` — every other browser
15
+ * 4. `setTimeout(0)` — last resort
16
+ */
17
+ export declare function yieldToEventLoop(): Promise<void>;
@@ -1,4 +1,5 @@
1
- import { DataContainerType } from './';
1
+ import { AutoGeneratePropertiesOption, DataContainerType } from './index.js';
2
+ import { KeyPropertySpec } from './key-property.types.js';
2
3
  export declare namespace AppLoaderConfiguration {
3
4
  /**
4
5
  * The base structure for configuration file contains all resources, evnironments and headers
@@ -55,7 +56,7 @@ export declare namespace AppLoaderConfiguration {
55
56
  defaultDataContainerUpdateType?: DataContainerType;
56
57
  csvParserOptions?: CsvParserOptions;
57
58
  missingModelHandling?: {
58
- autoGenerateProperties?: boolean;
59
+ autoGenerateProperties?: AutoGeneratePropertiesOption;
59
60
  };
60
61
  };
61
62
  export type CsvParserOptions = {
@@ -181,9 +182,25 @@ export declare namespace AppLoaderConfiguration {
181
182
  };
182
183
  export type DataResourceOptions = {
183
184
  updateType?: DataContainerType;
184
- keyProperty?: string;
185
+ /**
186
+ * Property name(s) on each row that uniquely identify it.
187
+ *
188
+ * Pass a single string for a simple key (`keyProperty: 'ID'`) or a
189
+ * non-empty array for a composite key (`keyProperty: ['ID', 'Language']`).
190
+ * Composite parts are joined into the internal `.key` using
191
+ * `keyPropertySeparator` (default `::`). Order is preserved literally.
192
+ *
193
+ * Each part is stringified via `String(value)`, so `null`, `undefined`,
194
+ * and `''` produce distinct keys (`'null'`, `'undefined'`, `''`).
195
+ */
196
+ keyProperty?: KeyPropertySpec;
197
+ /**
198
+ * Separator joining composite `keyProperty` parts. Default `'::'`.
199
+ * Ignored when `keyProperty` is a string or a single-element array.
200
+ */
201
+ keyPropertySeparator?: string;
185
202
  autoGenerateKey?: boolean;
186
- autoGenerateProperties?: boolean;
203
+ autoGenerateProperties?: AutoGeneratePropertiesOption;
187
204
  csvParserOptions?: CsvParserOptions;
188
205
  };
189
206
  export type Resource = ModelResource | DataResource | TransformerResource | CustomOperatorResource | CustomScriptResource;
@@ -191,6 +208,45 @@ export declare namespace AppLoaderConfiguration {
191
208
  key: string;
192
209
  value: string;
193
210
  };
211
+ /** Why a declared resource was not scheduled for a run. */
212
+ export type SkipReason =
213
+ /** The run filters by resourceTags, and the resource carries no tags at all. */
214
+ 'no-tags'
215
+ /** The run filters by resourceTags, and none of them matched the resource's tags. */
216
+ | 'tag-mismatch';
217
+ export type SkippedResource = {
218
+ resource: Resource;
219
+ reason: SkipReason;
220
+ /**
221
+ * The same identity a scheduled item would carry — see `AppLoaderProgress.ResourceItem.id`.
222
+ *
223
+ * Assigned here rather than left to the consumer because ids are allotted over the FULL declared
224
+ * list: recomputing one in isolation cannot know its occurrence index, so duplicate entries would
225
+ * come out with a different id than the SDK gave them.
226
+ */
227
+ id?: string;
228
+ };
229
+ /**
230
+ * The outcome of resolving a configuration against process options, WITHOUT fetching anything.
231
+ *
232
+ * `scheduled` is what a `process()` call would actually load (tag-filtered, urls resolved);
233
+ * `skipped` is every declared resource that dropped out, with the reason. Together they cover
234
+ * `config.resources` exactly, which is what lets a consumer show "declared but not in this app"
235
+ * without re-implementing the filter.
236
+ */
237
+ export type ResourcePlan = {
238
+ scheduled: AppLoaderProgress.ResourceItem[];
239
+ skipped: SkippedResource[];
240
+ /** The variables the scheduled urls were resolved with. */
241
+ variables: EnvironmentVariableAsObject;
242
+ /** The process options the plan was built for. */
243
+ run: RunContext;
244
+ };
245
+ export type RunContext = {
246
+ applicationKey?: string;
247
+ resourceTags?: StringOrStringArray;
248
+ environmentKeys?: string[];
249
+ };
194
250
  export {};
195
251
  }
196
252
  export type FilesApiStatusResponse = {
@@ -205,6 +261,30 @@ export declare namespace AppLoaderProgress {
205
261
  type ResourceItem = AppLoaderConfiguration.Resource & {
206
262
  success?: boolean;
207
263
  status?: number;
264
+ /**
265
+ * Stable identity for this resource, derived once at plan time.
266
+ *
267
+ * Authored `resource.key` wins (`k:<key>`); otherwise a deterministic hash of the resource's
268
+ * RAW url plus type/model (`h:<hash>`). Because it is derived from the raw url it is the same
269
+ * value a consumer computes from the config file, which is what makes declared↔runtime joins
270
+ * possible without url matching. See `resolveResourceId`.
271
+ */
272
+ id?: string;
273
+ /**
274
+ * The url as authored in the config, BEFORE `${variable}` substitution.
275
+ *
276
+ * `url` is overwritten with the resolved value at plan time; without this the template — and
277
+ * therefore the link back to the config entry — would be unrecoverable at runtime.
278
+ */
279
+ rawUrl?: string;
280
+ /**
281
+ * Variables referenced by `rawUrl` that had no value at resolve time.
282
+ *
283
+ * `evaluateTemplate` substitutes an unknown variable with an empty string, so an unset
284
+ * `${baseUrl}` silently yields a mangled url and a download failure that looks like a missing
285
+ * file. A non-empty array here is the difference between "file is gone" and "config is wrong".
286
+ */
287
+ unresolvedVariables?: string[];
208
288
  };
209
289
  type ProgressItem = {
210
290
  total: number;
@@ -213,11 +293,21 @@ export declare namespace AppLoaderProgress {
213
293
  progress: number;
214
294
  context: string;
215
295
  currentItem: ResourceItem;
296
+ /** The resources SCHEDULED for this run — already tag-filtered and url-resolved. */
216
297
  totalItems: ResourceItem[];
217
298
  processedItems: ResourceItem[];
218
299
  downloadedItems: ResourceItem[];
219
300
  failedItems: ResourceItem[];
220
301
  remainingItems: ResourceItem[];
302
+ /**
303
+ * Declared resources that this run never scheduled, with the reason.
304
+ *
305
+ * `totalItems` covers only what the run's resourceTags selected, so on its own it cannot
306
+ * distinguish "loaded everything" from "loaded everything it was allowed to see".
307
+ */
308
+ skippedItems?: AppLoaderConfiguration.SkippedResource[];
309
+ /** The application/tags/environments this run resolved against. */
310
+ run?: AppLoaderConfiguration.RunContext;
221
311
  };
222
312
  type CallbackFunction = (item: ProgressItem) => void;
223
313
  }
@@ -1,3 +1,5 @@
1
+ import { AutoGeneratePropertiesOption } from './model.types.js';
2
+ import { KeyPropertySpec } from './key-property.types.js';
1
3
  export type RawItem = any;
2
4
  export type ListItem = {
3
5
  key: string;
@@ -7,15 +9,17 @@ export type DataContainer<ListType extends RawItem | ListItem = ListItem, ModelN
7
9
  model: ModelNames;
8
10
  type?: DataContainerType;
9
11
  data: ListType[];
10
- keyProperty?: string;
12
+ keyProperty?: KeyPropertySpec;
13
+ keyPropertySeparator?: string;
11
14
  autoGenerateKey?: boolean;
12
- autoGenerateProperties?: boolean;
15
+ autoGenerateProperties?: AutoGeneratePropertiesOption;
13
16
  placeholderUid?: string;
14
17
  };
15
18
  export type DataContainerType = 'upsert' | 'update' | 'set' | 'remove';
16
19
  export type ReferenceDataTypes<ListType extends ListItem = ListItem> = string | string[] | ListType | ListType[];
17
20
  export type DataOptions<ModelNames extends string = string> = {
18
21
  model: ModelNames;
19
- keyProperty?: string;
22
+ keyProperty?: KeyPropertySpec;
23
+ keyPropertySeparator?: string;
20
24
  autoGenerateKey?: boolean;
21
25
  };
@@ -1,12 +1,12 @@
1
- import { DcuplUpdateMessage } from '../changeDetection';
2
- import { PivotOptions } from '../pivot';
3
- import { AggregationOptions } from './aggregation.types';
4
- import { DcuplFacetOptions } from './facets.types';
5
- import { DcuplGroupByOptions } from './group-by.types';
6
- import { DcuplItemsOptions } from './list.types';
7
- import { DcuplGlobalQueryOptions } from './query.types';
8
- import { SuggestionOptions } from './suggestion.types';
9
- import { LogLevel, LogEntry } from '../logger';
1
+ import { DcuplUpdateMessage } from '../changeDetection.js';
2
+ import { PivotOptions } from '../pivot.js';
3
+ import { AggregationOptions } from './aggregation.types.js';
4
+ import { DcuplFacetOptions } from './facets.types.js';
5
+ import { DcuplGroupByOptions } from './group-by.types.js';
6
+ import { DcuplItemsOptions } from './list.types.js';
7
+ import { DcuplGlobalQueryOptions } from './query.types.js';
8
+ import { SuggestionOptions } from './suggestion.types.js';
9
+ import { LogLevel, LogEntry } from '../logger/index.js';
10
10
  export type DcuplInitOptions = {
11
11
  config?: DcuplInitConfig;
12
12
  /**
@@ -85,9 +85,9 @@ export type DcuplPartialUpdateConfig = {
85
85
  */
86
86
  itemRatioThreshold?: number;
87
87
  /**
88
- * Threshold for cache invalidation (0-1). If more than this percentage of models are affected,
89
- * the entire cache will be cleared instead of selective invalidation.
90
- * Default: 0.7 (70%)
88
+ * @deprecated No effect since 2.0.0-beta.14 and ignored. It gated a global
89
+ * query cache that was never populated; query/facet caches live per list and
90
+ * are invalidated lazily on every update (#245). Will be removed in 2.0.0.
91
91
  */
92
92
  cacheInvalidationThreshold?: number;
93
93
  /**
@@ -98,6 +98,20 @@ export type DcuplPartialUpdateConfig = {
98
98
  };
99
99
  export type DcuplInitQualityConfig = {
100
100
  enabled: boolean;
101
+ /**
102
+ * Maximum number of stored error objects per (model, property, errorType)
103
+ * group. Quality errors are still counted exactly beyond this limit and are
104
+ * reachable via `dcupl.quality.errors.summary()` or the
105
+ * `$DcuplErrorTrackingSummary` model; only the retained error objects (and
106
+ * the `$DcuplErrorTrackingErrors` model) are bounded. Retained records of a
107
+ * capped group carry `truncated: true` and `groupTotal`. Default: 1000.
108
+ *
109
+ * Note: for error groups whose true count exceeds `maxErrorsPerGroup`,
110
+ * partial-update cleanup may cause the totals to slightly
111
+ * over-report, since over-cap occurrences are counted but never stored as
112
+ * error objects and therefore cannot be decremented on removal.
113
+ */
114
+ maxErrorsPerGroup?: number;
101
115
  };
102
116
  export type DcuplInitErrorTrackingConfig = {
103
117
  enabled: boolean;
@@ -1,4 +1,4 @@
1
- import { ListItem } from './data.types';
1
+ import { ListItem } from './data.types.js';
2
2
  export type DcuplFacetOptions = {
3
3
  attribute: string;
4
4
  count?: number;
@@ -6,6 +6,19 @@ export type DcuplFacetOptions = {
6
6
  excludeUndefineds?: boolean;
7
7
  excludeUnresolved?: boolean;
8
8
  calculateResults?: boolean;
9
+ /**
10
+ * Opt-in deterministic ordering. See `DcuplFilterOptions.sort` for full semantics.
11
+ * When omitted the fast (insertion-order) path is preserved.
12
+ */
13
+ sort?: 'size-desc' | 'size-asc' | 'value-asc' | 'value-desc';
14
+ /**
15
+ * Key of the query group to leave out when counting (disjunctive facets):
16
+ * each facet value is counted under every other group of the current query,
17
+ * so selecting one value does not collapse its siblings.
18
+ * Defaults to the group keyed by `attribute`. A key that matches no group
19
+ * leaves nothing out. `selected` reflects this group's conditions on `attribute`.
20
+ */
21
+ excludeGroup?: string;
9
22
  };
10
23
  export type DcuplFacet = {
11
24
  key: string;
@@ -1,6 +1,6 @@
1
- import { DcuplQueryGroup, ListItem } from '.';
2
- import { CatalogApiOptions } from './list.types';
3
- import { FilterComparisonType } from './model.types';
1
+ import { DcuplQueryGroup, ListItem } from './index.js';
2
+ import { CatalogApiOptions } from './list.types.js';
3
+ import { FilterComparisonType } from './model.types.js';
4
4
  export type NumberConstraintOperators = {
5
5
  gt?: number;
6
6
  lt?: number;
@@ -1,5 +1,5 @@
1
- import { AggregationOptions } from './aggregation.types';
2
- import { DcuplItemsOptions } from './list.types';
1
+ import { AggregationOptions } from './aggregation.types.js';
2
+ import { DcuplItemsOptions } from './list.types.js';
3
3
  export type DcuplSectionItemOption<T> = DcuplItemsOptions<T> & {
4
4
  key: string;
5
5
  };
@@ -1,16 +1,17 @@
1
- export * from './aggregation.types';
2
- export * from './app-loader.types';
3
- export * from './data.types';
4
- export * from './dcupl.types';
5
- export * from './facets.types';
6
- export * from './filter.types';
7
- export * from './group-by.types';
8
- export * from './internal.types';
9
- export * from './list.types';
10
- export * from './model.types';
11
- export * from './projection.types';
12
- export * from './quality.types';
13
- export * from './query.types';
14
- export * from './section.types';
15
- export * from './suggestion.types';
16
- export * from './testing.types';
1
+ export * from './aggregation.types.js';
2
+ export * from './app-loader.types.js';
3
+ export * from './key-property.types.js';
4
+ export * from './data.types.js';
5
+ export * from './dcupl.types.js';
6
+ export * from './facets.types.js';
7
+ export * from './filter.types.js';
8
+ export * from './group-by.types.js';
9
+ export * from './internal.types.js';
10
+ export * from './list.types.js';
11
+ export * from './model.types.js';
12
+ export * from './projection.types.js';
13
+ export * from './quality.types.js';
14
+ export * from './query.types.js';
15
+ export * from './section.types.js';
16
+ export * from './suggestion.types.js';
17
+ export * from './testing.types.js';
@@ -1,4 +1,4 @@
1
- import { ListItem } from './data.types';
1
+ import { ListItem } from './data.types.js';
2
2
  export type InverseIndexReferenceMap = Map<string, InverseIndexValuesMap>;
3
3
  export type InverseIndexValuesMap = Map<string, Set<string>>;
4
4
  export type InverseIndexDataMap = Map<string, InverseIndexDataValuesMap>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Specifies the property (or properties) on a row that uniquely identifies it.
3
+ *
4
+ * - A single string names one property: `keyProperty: 'id'`.
5
+ * - A non-empty array of strings produces a composite key by joining the
6
+ * stringified values of each property with the configured separator
7
+ * (default `::`): `keyProperty: ['ID', 'Language']` → `.key === 'A1::en'`.
8
+ *
9
+ * The order of array entries is significant and preserved verbatim.
10
+ */
11
+ export type KeyPropertySpec = string | readonly string[];
@@ -1,7 +1,7 @@
1
- import { DcuplGlobalQueryOptions } from './query.types';
2
- import { ViewDefinition, ModelAttribute } from './model.types';
3
- import { Projection, SortingProjection } from './projection.types';
4
- import { ListItem } from './data.types';
1
+ import { DcuplGlobalQueryOptions } from './query.types.js';
2
+ import { ViewDefinition, ModelAttribute } from './model.types.js';
3
+ import { Projection, SortingProjection } from './projection.types.js';
4
+ import { ListItem } from './data.types.js';
5
5
  export type DcuplItemsOptions<T> = {
6
6
  start?: number;
7
7
  count?: number;
@@ -22,6 +22,27 @@ export type DcuplFilterOptions = CatalogApiOptions & {
22
22
  excludeUndefineds?: boolean;
23
23
  excludeUnresolved?: boolean;
24
24
  count?: number;
25
+ /**
26
+ * Opt-in deterministic ordering for facet results.
27
+ *
28
+ * When omitted (default), results are returned in inverse-index iteration
29
+ * order and `count` truncation is applied mid-iteration. This is the
30
+ * fast path and matches all existing call sites.
31
+ *
32
+ * When set, `getFacets` walks all facet keys, computes real sizes
33
+ * (overriding `calculateFacets: false`), applies the chosen sort with
34
+ * deterministic tie-break, and truncates to `count` after sorting.
35
+ * This is the slow but correct path — opt in when ordering matters.
36
+ *
37
+ * Tie-break for size sorts is always `value` ascending with `undefined`
38
+ * last. `value-desc` keeps `undefined` last as well.
39
+ */
40
+ sort?: 'size-desc' | 'size-asc' | 'value-asc' | 'value-desc';
41
+ /**
42
+ * Key of the query group left out when counting facet entries.
43
+ * Defaults to the group keyed by the faceted attribute. See `DcuplFacetOptions.excludeGroup`.
44
+ */
45
+ excludeGroup?: string;
25
46
  };
26
47
  export type DcuplFiltersOptions = DcuplFilterOptions & {
27
48
  filterKeys?: string[];