fix: one field-resolution law across aggregation hooks, source.where, removeMany, and find() spellings
Four fixes from a consumer conformance report, one root disease — two field-resolution regimes where there must be one: - The delete/update aggregation hooks fed the engine a partial entity view (type/service/data/metadata only), so a reserved-field groupBy (subtype, visibility, ...) resolved to a nonexistent group on the way down: counts drifted upward forever after deletes, and updates moving an entity between reserved-field groups double-counted. The hooks now pass the full-fidelity view via entityForAggFromRawRecord (every reserved field top-level, mirroring the add path); the update sites pass the full get() view instead of a hand-rolled subset. - Aggregation source.where resolved fields only against the custom metadata bag, so where on a reserved field silently matched nothing. The matcher now resolves each filtered field through resolveEntityField — the same single source of truth groupBy uses. - removeMany() with no usable selector (bare array passed positionally, empty params, ids: []) resolved successfully having deleted nothing. All three now throw; the two legacy tests that pinned the silent no-op as 'graceful' now pin the refusal. - find() where keys accept both spellings: a metadata.-prefixed key falls back to its flattened spelling when the prefixed one is not indexed (metadata is flattened at index time). A literal nested custom key named metadata still wins when indexed as spelled. Five regression pins in aggregate-reserved-fields.test.ts (4 of 5 vary red on the unfixed code).
This commit is contained in:
parent
42037d0cd0
commit
945d92d29e
7 changed files with 303 additions and 45 deletions
|
|
@ -1867,9 +1867,26 @@ export class MetadataIndexManager implements MetadataIndexProvider {
|
|||
// not once per AND-clause inside it.
|
||||
const unindexedFields: string[] = []
|
||||
|
||||
for (const [field, condition] of Object.entries(filter)) {
|
||||
for (const [rawField, condition] of Object.entries(filter)) {
|
||||
// Skip logical operators
|
||||
if (field === 'allOf' || field === 'anyOf' || field === 'not') continue
|
||||
if (rawField === 'allOf' || rawField === 'anyOf' || rawField === 'not') continue
|
||||
|
||||
// Metadata is FLATTENED at index time (metadata.entry.title indexes as
|
||||
// entry.title), so a `metadata.`-prefixed where key is almost always
|
||||
// the caller spelling the STORAGE shape rather than the index shape.
|
||||
// Accept both spellings: when the key as spelled is unindexed but its
|
||||
// stripped spelling is, query the stripped one. A literal nested
|
||||
// custom key named `metadata` still wins when indexed as spelled
|
||||
// (checked first), so that rare shape keeps working.
|
||||
let field = rawField
|
||||
if (
|
||||
rawField.startsWith('metadata.') &&
|
||||
this.columnStore &&
|
||||
!this.columnStore.hasField(rawField) &&
|
||||
this.columnStore.hasField(rawField.slice('metadata.'.length))
|
||||
) {
|
||||
field = rawField.slice('metadata.'.length)
|
||||
}
|
||||
|
||||
let fieldResults: string[] = []
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue