Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions resources/ResourceInterface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,13 @@ export type Comparator =
| 'ends_with'
| 'eq'
| 'equals'
| 'gt'
| 'ge'
| 'lt'
| 'le'
| 'greater_than'
| 'greater_than_equal'
| 'in'
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not_X comparators missing from the Comparator union

not_in, not_starts_with, not_between, not_contains, and not_ends_with are valid runtime inputs to the structured query API — prepareConditions normalizes them, and the test suite has a test explicitly titled "accepts not_in directly without going through parser". They're not in the Comparator union, so TypeScript callers get a compile error when using these strings.

not_equal (the existing ne alias) is already in the type and sets the precedent. Either add the new not_X entries here, or clarify in the test description that these are internal aliases not intended as typed public inputs (in which case { comparator: 'in', negated: true } is the only typed path).

Suggested change
| 'in'
+ | 'in'

(Extend to add 'not_in' | 'not_starts_with' | 'not_between' | 'not_contains' | 'not_ends_with' after 'in' if these are intended as public API strings.)

| 'less_than'
| 'less_than_equal'
| 'ne'
Expand All @@ -149,8 +154,13 @@ interface TypedDirectCondition<Record extends object, Property extends keyof Rec
search_attribute?: keyof Record | Array<keyof Record> | string | string[];
comparator?: Comparator;
search_type?: Comparator;
value?: Record[Property];
search_value?: Record[Property];
value?: Record[Property] | Record[Property][];
search_value?: Record[Property] | Record[Property][];
/**
* If true, the condition is negated. Phase 1: filter-only — forces a
* full scan unless paired with another indexed condition.
*/
negated?: boolean;
}

interface ConditionGroup<Record extends object = any> {
Expand Down
45 changes: 39 additions & 6 deletions resources/Table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
flattenKey,
COERCIBLE_OPERATORS,
executeConditions,
resolveComparator,
} from './search.ts';
import { logger } from '../utility/logging/logger.ts';
import { Addition, assignTrackedAccessors, updateAndFreeze, hasChanges, GenericTrackedObject } from './tracked.ts';
Expand Down Expand Up @@ -2056,15 +2057,37 @@ export function makeTable(options) {
condition.conditions = prepareConditions(condition.conditions, condition.operator);
continue;
}
// Normalize `not_X` comparator forms passed in via structured queries.
// The REST parser already does this, but programmatic callers may
// pass `not_in`, `not_starts_with`, etc. directly.
if (condition.comparator) {
const resolved = resolveComparator(condition.comparator);
if (resolved.negated) {
condition.comparator = resolved.comparator;
condition.negated = true;
}
}
const attribute_name = condition[0] ?? condition.attribute;
const attribute = attribute_name == null ? primaryKeyAttribute : findAttribute(attributes, attribute_name);
let attribute = attribute_name == null ? primaryKeyAttribute : findAttribute(attributes, attribute_name);
if (!attribute && Array.isArray(attribute_name) && attribute_name.length > 1) {
// Plain JSON nested path: the leaf may not be declared in the
// schema. Fall back to the root attribute so we can validate
// existence without requiring the inner structure to be typed.
attribute = findAttribute(attributes, attribute_name[0]);
}
if (!attribute) {
if (attribute_name != null && !target.allowConditionsOnDynamicAttributes)
throw handleHDBError(new Error(), `${attribute_name} is not a defined attribute`, 404);
} else if (attribute.type || COERCIBLE_OPERATORS[condition.comparator]) {
// Do auto-coercion or coercion as required by the attribute type
if (condition[1] === undefined) condition.value = coerceTypedValues(condition.value, attribute);
else condition[1] = coerceTypedValues(condition[1], attribute);
// Do auto-coercion or coercion as required by the attribute type.
// Skipped for nested paths into plain JSON — the root attribute's
// type is not the leaf type, so coercion would be wrong.
const isNestedPathRoot =
Array.isArray(attribute_name) && attribute_name.length > 1 && !attribute.relationship;
if (!isNestedPathRoot) {
if (condition[1] === undefined) condition.value = coerceTypedValues(condition.value, attribute);
else condition[1] = coerceTypedValues(condition[1], attribute);
}
}
if (condition.chainedConditions) {
if (condition.chainedConditions.length === 1 && (!condition.operator || condition.operator == 'and')) {
Expand Down Expand Up @@ -2572,12 +2595,22 @@ export function makeTable(options) {
} else {
value = record[attribute_name];
if (value && typeof value === 'object' && attribute_name !== attribute) {
value = TableResource.transformEntryForSelect(
const subTransform = TableResource.transformEntryForSelect(
attribute.select || attribute,
context,
readTxn,
null
)({ value });
);
// Plain JSON nested values: arrays project per-element so that
// `select: [{ name: 'addresses', select: ['city'] }]` returns
// `addresses: [{ city }, { city }]` rather than a single object.
if (Array.isArray(value)) {
value = value.map((item) =>
item && typeof item === 'object' ? subTransform({ value: item }) : item
);
} else if (!(value instanceof Date)) {
value = subTransform({ value });
}
}
}
callback(value, attribute_name);
Expand Down
Loading
Loading