Skip to content
Merged
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
10 changes: 10 additions & 0 deletions .changeset/29611-driver-sql-list-comparand-predicate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/driver-sql': patch
---

refactor(driver-sql): the comparand bind test reads the list operators from the spec

Clause-②: no
Metadata-protocol: none

The SQL driver bind-tests each member of a `$in`, `$nin` or `$between` list as a comparand in its own right. It now asks `isListComparandOperator` from `@objectstack/spec/data` which operators take a list, instead of keeping its own copy of `$in` / `$nin` / `$between`. The set is the same, so no filter compiles or is refused differently. A later change to the spec's list operators now reaches this driver without a second edit.
10 changes: 10 additions & 0 deletions .changeset/29611-plugin-security-list-comparand-predicate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
'@objectstack/plugin-security': patch
---

refactor(plugin-security): the RLS stored-form check reads the list operators from the spec

Clause-②: no
Metadata-protocol: none

The RLS check puts a value comparand into its column's storage form, and a `$in`, `$nin` or `$between` list member by member. It now asks `isListComparandOperator` from `@objectstack/spec/data` which operators take a list, instead of keeping its own copy of `$in` / `$nin` / `$between`. The set is the same, so no policy filter is judged differently. A later change to the spec's list operators now reaches this check without a second edit.
27 changes: 12 additions & 15 deletions packages/drivers/driver-sql/src/sql-driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ import {
isAcceptedFilterComparand,
ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE,
} from '@objectstack/spec/data';
// [#29611] The operator split's list half (`$in` / `$nin` / `$between`): the
// operators whose comparand is a LIST, so each member is bind-tested on its own
// (see {@link assertCompilableComparand}). Asked of the spec, the one source the
// comparand faces read, so this driver keeps no copy to drift from it.
import { isListComparandOperator } from '@objectstack/spec/data';
// [#7536] `$like`/`$ilike`'s pattern language, defined once in the spec: the
// dangling-escape gate every face refuses on, and the LIKE→GLOB translation the
// SQLite dialects need because GLOB is the only case-exact pattern operator
Expand Down Expand Up @@ -2487,7 +2492,7 @@ const CROSS_FIELD_COMPARISON_OPERATORS: ReadonlySet<string> = new Set([
* their own `whereIn` / `whereBetween` arms. Only their MEMBERS are inspected —
* for `$field` (#5041) and, since #5234, for bindability — never their arity:
* the existing descriptive `$between` refusal stays the one that answers a
* malformed range. See {@link LIST_COMPARAND_OPERATORS}.
* malformed range. See {@link isListComparandOperator}.
*/
const SCALAR_COMPARAND_OPERATORS: ReadonlySet<string> = new Set([
'$eq', '$ne', '$gt', '$gte', '$lt', '$lte',
Expand Down Expand Up @@ -2569,17 +2574,6 @@ const TEXT_OPERATORS: ReadonlySet<string> = new Set([
...LIKE_PATTERN_OPERATORS,
]);

/**
* [#5234] Operators for which an ARRAY is the legitimate comparand, so it is
* each MEMBER that must be individually compilable.
*
* Scoping the member scan to these three is what keeps a scalar operator that
* received an array answering with its own message ("requires a single
* comparable value") instead of reporting the first bad member of a list it
* should never have been given.
*/
const LIST_COMPARAND_OPERATORS: ReadonlySet<string> = new Set(['$in', '$nin', '$between']);

/**
* [#5234] Does this value have a faithful rendering as the text of a LIKE
* pattern?
Expand Down Expand Up @@ -2687,10 +2681,13 @@ function assertCompilableComparand(
if (nowAnchorOf(member) !== null) throw nowAnchorPositionError(field, op, member, index);
// [#5234] Every member of a list operator's array is a comparand in its
// own right and gets the same bind test the whole comparand gets. Scoped
// to the operators for which an array is legitimate, so a scalar operator
// handed an array keeps answering with its own message below.
// to the operators for which an array is legitimate (the spec's list
// half, {@link isListComparandOperator}), so a scalar operator handed an
// array keeps answering with its own message ("requires a single
// comparable value") instead of reporting the first bad member of a list
// it should never have been given.
// [#20039] The member when it is an object, else the list that holds it.
if (LIST_COMPARAND_OPERATORS.has(op) && !isBindableComparand(member)) {
if (isListComparandOperator(op) && !isBindableComparand(member)) {
throw unbindableListMemberError(field, op, member, index, refusalSubtree(member, value));
}
}
Expand Down
10 changes: 5 additions & 5 deletions packages/plugins/plugin-security/src/rls-check-stored-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ import {
INSTANT_TYPES,
STRUCTURED_JSON_TYPES,
filterSubtreeProvenanceOf,
isListComparandOperator,
isMultiValueField,
markFilterSubtreeProvenance,
} from '@objectstack/spec/data';
Expand All @@ -179,15 +180,14 @@ const NO_MULTI_VALUE_COLUMNS: DeclaredMultiValueColumns = new Set();
/**
* The operators whose comparand is a VALUE of the column, compared with the
* stored value. The drivers put these comparands into the column's storage
* form; every other operator's operand is left as written.
* form; every other operator's operand is left as written. Which of them take
* a LIST of values is the spec's answer ({@link isListComparandOperator}), not
* a copy kept here.
*/
const VALUE_COMPARISON_OPERATORS: ReadonlySet<string> = new Set([
'$eq', '$ne', '$gt', '$gte', '$lt', '$lte', '$in', '$nin', '$between',
]);

/** The value-comparison operators whose comparand is a LIST of values. */
const LIST_COMPARAND_OPERATORS: ReadonlySet<string> = new Set(['$in', '$nin', '$between']);

/** The storage rule a declared type takes, or `undefined` for a non-temporal type. */
function temporalKindOfDeclaredType(type: string): TemporalComparandKind | undefined {
if (CALENDAR_DATE_TYPES.has(type)) return 'date';
Expand Down Expand Up @@ -412,7 +412,7 @@ function storedComparand(value: unknown, kind: TemporalComparandKind): unknown {

/** A value-comparison operand in its column's storage form, a list member by member. */
function storedOperand(op: string, value: unknown, kind: TemporalComparandKind): unknown {
if (!LIST_COMPARAND_OPERATORS.has(op) || !Array.isArray(value)) return storedComparand(value, kind);
if (!isListComparandOperator(op) || !Array.isArray(value)) return storedComparand(value, kind);
let copy: unknown[] | undefined;
value.forEach((member, index) => {
const stored = storedComparand(member, kind);
Expand Down
Loading