Skip to content

Commit c68869c

Browse files
fix(spec)!: every filter door refuses a $now anchor that carries a key beyond $now / addDays (#26592)
<!-- claude-projects-attribution: {"github_login":"os-support-ai"} --> _Requested by **Yibo Wang** · [project thread](https://claude.ai/code/project/chan_01BVUyyPie8gHwoPM18iABwB?thread=cmsg_01BVUyyPie8gHwoPM18iABwB4zZywfLfQ2M3x31mQ5qLJm)_ Fixes #26413 Clause-②: no (narrowing) — every filter door refuses a `$now` anchor carrying a key beyond `$now` / `addDays`, which it ran before. Nothing is widened. Metadata-protocol: narrowed (a filter saved through `FilterConditionSchema` may no longer carry a key beside `$now` and `addDays` on a now-anchored comparand) Skills-docs: none — `skills/objectstack-query/rules/filters.md` (the refusal list under "Now-Anchored Comparands") already teaches that "any extra key (there is no `subDays`)" is refused with `INVALID_FILTER` / 400, and eval `filters-pagination-search.json` already forbids `subDays`; this diff makes that sentence true at every door, so it needs no edit. ## 维护者速读 - **改了什么:** 所有过滤入口共用的 `$now` 锚点检查(`isWellFormedNowAnchor`)现在拒收 `$now` / `addDays` 之外的任何键,报 `INVALID_FILTER` / 400 并点名多余的键。 - **为什么改:** `NowAnchorSchema` 声明为封闭,但入口只看 `addDays`,`subDays: 7` 这类键被静默忽略,查询按 `addDays` 跑完、不报错(ADR-0049:声明即须执行)。 - **风险与回滚:** 收窄:之前带多余键能跑的查询和能保存的过滤现在被拒;合法锚点不受影响。回滚即还原 `filter-comparand-type.ts` 一处。 - **先看:** `packages/spec/src/data/filter-comparand-type.ts`(`isWellFormedNowAnchor` 与 `malformedNowAnchorRefusal` 新分支)、`packages/objectql/src/engine-now-anchor.test.ts`(五个入口的钉)、`.changeset/26413-now-anchor-extra-key-refused.md`。 - **两分钟验证:** `pnpm --filter @objectstack/spec exec vitest run src/data/filter-comparand-type.test.ts`,预期新的 `[#26413]` 组全绿。 - **需要你做的:** 无,席位落地。 ## What changes - `packages/spec/src/data/filter-comparand-type.ts`: `isWellFormedNowAnchor` now answers false for an anchor with any key other than `$now` / `addDays`, so the comparand-type walk that every filter door runs refuses it. `malformedNowAnchorRefusal` gains an undeclared-key arm, checked first: `it carries an undeclared key "subDays" — the anchor takes $now and addDays only (there is no subDays: a negative addDays subtracts)`. No new error code: the doors keep `INVALID_FILTER` / 400. The key set is restated locally (`filter.zod.ts` imports this module) and the test reconciles it against `NowAnchorSchema.shape`. - Doors this reaches, all through the one walk: `find()` / `count()` / `update()` in object and array form, `aggregate()`'s `where`, `aggregations[i].filter`, `having`, and `FilterConditionSchema` on save. A well-formed anchor and its resolved form (`$now` holding the pinned ISO instant) run as before. - `@objectstack/spec` changeset, `minor` (see Acceptance notes on the level). ## Tests - `packages/spec/src/data/filter-comparand-type.test.ts`: new `[#26413]` group: the door's key set equals `NowAnchorSchema.shape`; refusal with code, status and the named key at all six scalar comparisons for an integer offset, a `{ $field }` offset and the resolved form; under `$and` / `$or` / `$not` and through both `parseFilterAST` forms; every undeclared key named, before a malformed offset; well-formed anchors kept by reference. - `packages/spec/src/data/filter.test.ts`: the `FilterConditionSchema` save door refuses `subDays` and `note`, naming the key, beside the existing closed-schema pin; the clean anchor still parses. - `packages/objectql/src/engine-now-anchor.test.ts`: new `[#26413]` group, one answer at every door: `find()` object form (driver never called), `find()` array form, `aggregate()` `where`, `aggregations[i].filter`, `having`, each with code, status and the named key, and each with the clean anchor still running. - `packages/formula/src/matches-filter-now-anchor.test.ts`: **narrowed, not retired.** Measured: it does not contradict the refusal. The engine reaches this evaluator only through `having-filter.ts` (`having` and the `aggregations[i].filter` rows fallback), both behind the door, and the CEL lowering for the RLS `check` emits no anchor. The assertion stays; the title and a comment now say the evaluator sits behind the door and does not re-judge the keys, instead of presenting the tolerance as the anchor's contract. ## Acceptance notes - **Level vs. the order.** The dispatch said "a patch changeset" and "Metadata-protocol as measured". Measured: `FilterConditionSchema.safeParse({ opened_at: { $lt: { $now: true, addDays: -30, subDays: 7 } } })` was `success: true` on base and is refused now, so a value a metadata author could save is refused: `narrowed`. AGENTS.md's level table makes a declared kind at least `minor` on a moved package, so the changeset is `@objectstack/spec: minor`. `objectql` and `formula` change tests only and publish nothing. - **ADR-0087:** `not-required (no-migration-prescription)`: no key is added, removed or renamed (`NowAnchorSchema` declared exactly two keys since 18.2.0), and an extra key never had a meaning at any executor, so no rewrite keeps what the author meant. - Noted, not fixed (from the earlier report on this card, unchanged by this diff): the bare implicit-equality spelling `{ opened_at: { $now: true, addDays: -30, note: 'x' } }` is read by the walk as an operator bag, not a comparand, so it is outside this refusal. `having` already refuses the bare spelling; its answer at `where` against a real driver was not measured. ## Verification Readings at head `98b536ff`. Builds and tests ran under `os-verify-lock`. <details><summary>Package tests and typecheck</summary> - `pnpm --filter @objectstack/spec test`: exit 0, 729 files, 22055 passed, 1 todo. - `pnpm --filter @objectstack/formula test`: exit 0, 54 files, 1452 passed. - `pnpm --filter @objectstack/objectql test`: exit 0, 472 files, 8794 passed (after `pnpm --filter '@objectstack/objectql^...' build`). - `pnpm --filter @objectstack/{spec,formula,objectql} typecheck`: exit 0 each. - `pnpm exec eslint --no-inline-config` on the five changed source and test files: exit 0 (the repo-wide `pnpm lint` is CI's run). </details> <details><summary>Ablation (one, on the guard line)</summary> Committed fix first, then `node scripts/ablation-replace.mjs` replaced `if (undeclaredNowAnchorKeys(value).length > 0) return false;` with a branch that never fires (marker `ABLATED_26413`). Anchor 1 to 0, blob `a7e88059` to `64df538e`; spec rebuilt; `ablation-dist-preflight` found the marker in 2 built files. Expected direction red; observed red: spec 3 failed / 221 passed (the three new refusal pins), objectql 5 failed / 15 passed (all five new door pins). Restore: blob equals HEAD, `git diff HEAD` empty, spec rebuilt, preflight `--absent` clean over 580 built files, whole-tree `git status --porcelain` empty. </details> <details><summary>Derived gates</summary> `node scripts/pm/dispatch-gates.mjs --commands` derived 82 families; 80 exit 0. `check:dual-build-cjs-loads` and `check:lean-entry-closure` exit 3 (prerequisite: they read whole-repo built output; this worktree built the objectql closure only), NOT MEASURED locally, left to CI. `--ran` reconciliation: 82 derived, 80 run, 2 NOT-MEASURED, 0 UNRUN. `check-adr-0087-registration --base origin/main`: one declared-breaking changeset, disposition carried. Local-only: `check-skill-frame-freshness` exit 0; `check-dev-prereqs` exit 1 because the workspace is not fully built (37 packages lack `dist/`), an environment state, not a finding. The full dogfood battery reading goes in the os-dev report on the card. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind. - [x] 3. No formatter ran; `pnpm lint` passes. - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English. - [x] The *Before Submitting* list in CONTRIBUTING.md is done. --- _Generated by [Claude Code](https://claude.ai/code/session_01WGufzf89Y8Q23nPngB94Bo)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent f986702 commit c68869c

6 files changed

Lines changed: 186 additions & 10 deletions

File tree

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
fix(spec)!: every filter door refuses a `{ $now, addDays }` anchor that carries any other key, as `NowAnchorSchema` already declares, instead of running it on the `addDays` reading alone
6+
7+
`NowAnchorSchema` is closed: `{ $now: true, addDays: -30, subDays: 7 }` fails to parse on its own. But the comparand-type walk every filter door runs (`normalizeFilterComparandTypes`, through `FilterConditionSchema` and `parseFilterAST`) judged an anchor by its `addDays` alone, so the extra key was kept, every executor ignored it, and the query answered the `addDays` reading with no word. The walk now refuses an anchor with any key other than `$now` and `addDays`, with the existing `INVALID_FILTER` / 400 envelope, and the refusal names the key: `it carries an undeclared key "subDays" — the anchor takes $now and addDays only (there is no subDays: a negative addDays subtracts)`.
8+
9+
Clause-②: no (narrowing) — every filter door refuses a `$now` anchor carrying a key beyond `$now` / `addDays`, which it ran before. Nothing is widened.
10+
11+
Metadata-protocol: narrowed (a filter saved through `FilterConditionSchema` may no longer carry a key beside `$now` and `addDays` on a now-anchored comparand)
12+
13+
<!-- adr-0087: not-required (no-migration-prescription) No key of any schema is added, removed or renamed: NowAnchorSchema declared exactly $now and addDays since it shipped, and this aligns the walk with that declaration. An extra key never had a meaning at any executor, so there is no rewrite that keeps what the author meant; the author decides, and the refusal names the key. -->
14+
15+
**BREAKING** accept-set narrowing at the filter comparand-type walk (`@objectstack/spec`), served at every filter door.
16+
17+
- **Before.** `engine.find()` / `count()` / `update()` in object or array form, `aggregate()`'s `where`, `aggregations[i].filter`, `having`, and a filter saved through `FilterConditionSchema` all kept `{ $now: true, addDays: N, subDays: M }` or `{ …, note: 'x' }`. The query ran on `addDays` alone.
18+
- **Now.** Each of them answers `INVALID_FILTER` / 400 (a save fails its parse), naming the undeclared key.
19+
- **Unchanged.** A well-formed anchor, `{ $now: true, addDays: <integer | { $field }> }`, and its resolved form (`$now` holding the pinned ISO instant) run as before at every door. The refusals for a missing or malformed `addDays` keep their wording.
20+
- **Who sees it.** An author who wrote an extra key on an anchor. That key was ignored before, so the rows answered were the `addDays` rows.
21+
22+
**The remedy.** Drop the extra key. To move the instant back, give `addDays` a negative value; to read the offset off a column, use `addDays: { $field: 'numeric_column' }`.

‎packages/formula/src/matches-filter-now-anchor.test.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,13 @@ describe('[#25808] the own-clock arm and the fail-closed arms', () => {
164164
expect(ids({ completed_on: { $lte: anchored({ $field: 'id' }) } })).toEqual(['2', '3', '4', '6', '8', '9']);
165165
});
166166

167-
it('the anchor is read by SHAPE: an extra key does not disqualify it', () => {
167+
// [#26413] An extra key is refused at every filter door before a filter
168+
// reaches this evaluator (`normalizeFilterComparandTypes` in
169+
// `@objectstack/spec`: `INVALID_FILTER` / 400, the key named). The engine
170+
// calls this evaluator for `having` and the `aggregations[i].filter` rows
171+
// fallback, both behind that door; the CEL lowering for the RLS `check`
172+
// emits no anchor. So the evaluator does not judge the anchor's keys again.
173+
it('behind the door, the evaluator reads the anchor by SHAPE: it does not re-judge an extra key', () => {
168174
expect(ids({ completed_on: { $eq: { ...anchored(0), note: 'x' } } })).toEqual(['2']);
169175
});
170176
});

‎packages/objectql/src/engine-now-anchor.test.ts‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,4 +256,62 @@ describe('[#25808] the now-anchored comparand on the engine', () => {
256256
expect(err.message).toContain('now-anchored');
257257
});
258258
});
259+
260+
describe('[#26413] an undeclared key on the anchor gets one answer at every door', () => {
261+
const EXTRA = { $now: true, addDays: -30, subDays: 7 };
262+
const CLEAN = { $now: true, addDays: -30 };
263+
264+
const refusedWithKey = (err: Wire) => {
265+
expect(err.code).toBe('INVALID_FILTER');
266+
expect(err.status).toBe(400);
267+
expect(err.message).toContain('undeclared key "subDays"');
268+
expect(err.message).toContain('the anchor takes $now and addDays only');
269+
};
270+
271+
it('`find()` object-form `where`: refused before any driver runs; the clean anchor runs', async () => {
272+
seen.length = 0;
273+
refusedWithKey(await refusalOf(() => engine.find('ticket', { where: { opened_at: { $lt: EXTRA } } })));
274+
expect(seen).toHaveLength(0);
275+
await engine.find('ticket', { where: { opened_at: { $lt: CLEAN } } });
276+
expect(seen).toHaveLength(1);
277+
});
278+
279+
it('`find()` array-form `where`: refused the same way', async () => {
280+
seen.length = 0;
281+
refusedWithKey(await refusalOf(() => engine.find('ticket', { where: [['opened_at', '<', EXTRA]] } as never)));
282+
expect(seen).toHaveLength(0);
283+
await engine.find('ticket', { where: [['opened_at', '<', CLEAN]] } as never);
284+
expect(seen).toHaveLength(1);
285+
});
286+
287+
it('`aggregate()` `where`: refused the same way', async () => {
288+
refusedWithKey(await refusalOf(() => engine.aggregate('ticket', {
289+
where: { opened_at: { $lt: EXTRA } },
290+
aggregations: [{ function: 'count', alias: 'n' }],
291+
})));
292+
await expect(engine.aggregate('ticket', {
293+
where: { opened_at: { $lt: CLEAN } },
294+
aggregations: [{ function: 'count', alias: 'n' }],
295+
})).resolves.toBeDefined();
296+
});
297+
298+
it('`aggregations[i].filter`: refused the same way', async () => {
299+
refusedWithKey(await refusalOf(() => engine.aggregate('ticket', {
300+
aggregations: [{ function: 'count', alias: 'x', filter: { opened_at: { $lt: EXTRA } } }],
301+
})));
302+
await expect(engine.aggregate('ticket', {
303+
aggregations: [{ function: 'count', alias: 'x', filter: { opened_at: { $lt: CLEAN } } }],
304+
})).resolves.toBeDefined();
305+
});
306+
307+
it('`having`: refused the same way', async () => {
308+
const grouped = (anchor: unknown) => engine.aggregate('ticket', {
309+
groupBy: ['region'],
310+
aggregations: [{ function: 'min', field: 'opened_at', alias: 'first_opened' }],
311+
having: { first_opened: { $lt: anchor } } as never,
312+
});
313+
refusedWithKey(await refusalOf(() => grouped(EXTRA)));
314+
await expect(grouped(CLEAN)).resolves.toBeDefined();
315+
});
316+
});
259317
});

‎packages/spec/src/data/filter-comparand-type.test.ts‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
normalizeFilterComparandTypes,
2222
parseFilterAST,
2323
FieldOperatorsSchema,
24+
NowAnchorSchema,
2425
} from './index';
2526
import { StandardErrorCode } from '../api/errors.zod';
2627

@@ -295,3 +296,57 @@ describe('parseFilterAST is the compile face that runs the door', () => {
295296
expect(parseFilterAST(null)).toBeUndefined();
296297
});
297298
});
299+
300+
describe('[#26413] the now-anchor is closed at the door, as NowAnchorSchema declares', () => {
301+
const EXTRA_KEY_ANCHORS: Array<[string, Record<string, unknown>, string]> = [
302+
['a subDays beside an integer offset', { $now: true, addDays: -30, subDays: 7 }, 'subDays'],
303+
['a note beside a { $field } offset', { $now: true, addDays: { $field: 'sla_days' }, note: 'x' }, 'note'],
304+
['the resolved form the engine hands the executors', { $now: '2026-03-03T12:00:00.000Z', addDays: 0, note: 'x' }, 'note'],
305+
];
306+
307+
it('the door\'s key set is the schema\'s shape: $now and addDays, nothing else', () => {
308+
expect(Object.keys(NowAnchorSchema.shape).sort()).toEqual(['$now', 'addDays']);
309+
});
310+
311+
it('refuses an undeclared key at every scalar comparison, naming the key', () => {
312+
for (const [label, anchor, key] of EXTRA_KEY_ANCHORS) {
313+
for (const op of ['$eq', '$ne', '$gt', '$gte', '$lt', '$lte']) {
314+
const err = refusalOf(() => normalizeFilterComparandTypes({ opened_at: { [op]: anchor } }));
315+
expect(err?.code, `${label} ${op}`).toBe('INVALID_FILTER');
316+
expect(err?.status, `${label} ${op}`).toBe(400);
317+
expect(err?.message, `${label} ${op}`).toContain(`undeclared key "${key}"`);
318+
expect(err?.message, `${label} ${op}`).toContain('the anchor takes $now and addDays only');
319+
}
320+
}
321+
});
322+
323+
it('refuses it under $and / $or / $not and through parseFilterAST\'s two forms', () => {
324+
const anchor = { $now: true, addDays: 1, subDays: 1 };
325+
for (const filter of [
326+
{ $and: [{ status: 'open' }, { opened_at: { $lt: anchor } }] },
327+
{ $or: [{ opened_at: { $gte: anchor } }] },
328+
{ $not: { opened_at: { $lt: anchor } } },
329+
]) {
330+
expect(refusalOf(() => normalizeFilterComparandTypes(filter))?.code, JSON.stringify(filter)).toBe('INVALID_FILTER');
331+
}
332+
expect(refusalOf(() => parseFilterAST({ opened_at: { $lt: anchor } }))?.code).toBe('INVALID_FILTER');
333+
expect(refusalOf(() => parseFilterAST(['opened_at', '<', anchor]))?.code).toBe('INVALID_FILTER');
334+
});
335+
336+
it('names every undeclared key, and names it before a malformed offset', () => {
337+
const err = refusalOf(() => normalizeFilterComparandTypes({ opened_at: { $lt: { $now: true, subDays: 7, note: 'x' } } }));
338+
expect(err?.code).toBe('INVALID_FILTER');
339+
expect(err?.message).toContain('undeclared key "subDays", "note"');
340+
});
341+
342+
it('keeps a well-formed anchor, on both offset arms and in its resolved form, by reference', () => {
343+
for (const anchor of [
344+
{ $now: true, addDays: -30 },
345+
{ $now: true, addDays: { $field: 'sla_days' } },
346+
{ $now: '2026-03-03T12:00:00.000Z', addDays: 0 },
347+
]) {
348+
const filter = { opened_at: { $lt: anchor } };
349+
expect(normalizeFilterComparandTypes(filter)).toBe(filter);
350+
}
351+
});
352+
});

‎packages/spec/src/data/filter-comparand-type.ts‎

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -224,25 +224,48 @@ function isNowAnchor(value: unknown): boolean {
224224
return isFilterNode(value) && (value.$now === true || typeof value.$now === 'string');
225225
}
226226

227+
/**
228+
* [#26413] The keys a now-anchor carries: `NowAnchorSchema` is closed over
229+
* exactly these two, and `filter-comparand-type.test.ts` reconciles this set
230+
* against the schema's own shape. Restated rather than imported because
231+
* `filter.zod.ts` imports this module.
232+
*/
233+
const NOW_ANCHOR_KEYS: ReadonlySet<string> = new Set(['$now', 'addDays']);
234+
235+
function undeclaredNowAnchorKeys(value: Record<string, unknown>): string[] {
236+
return Object.keys(value).filter((key) => !NOW_ANCHOR_KEYS.has(key));
237+
}
238+
227239
/**
228240
* [#25808] Is this anchor WELL-FORMED — `addDays` present, an integer or a
229-
* `{ $field: string }`? The door steps around a well-formed anchor as it steps
230-
* around a reference; a malformed one (`{ $now: true }` with no offset, a
231-
* fractional or string offset) is refused HERE in the anchor's own words,
232-
* because the `FilterCondition` door runs this walk and not the operator
233-
* schemas, and an exemption by `$now` alone would have let the malformed shape
234-
* through to the executors as a plain object they each refuse differently.
241+
* `{ $field: string }`, and no other key? The door steps around a well-formed
242+
* anchor as it steps around a reference; a malformed one (`{ $now: true }`
243+
* with no offset, a fractional or string offset) is refused HERE in the
244+
* anchor's own words, because the `FilterCondition` door runs this walk and
245+
* not the operator schemas, and an exemption by `$now` alone would have let
246+
* the malformed shape through to the executors as a plain object they each
247+
* refuse differently.
248+
*
249+
* [#26413] An undeclared key (`subDays`, `note`) makes the anchor malformed
250+
* too, as `NowAnchorSchema` declares: every executor reads `$now` and
251+
* `addDays` only, so a kept extra key would run the query on the `addDays`
252+
* reading alone and say nothing.
235253
*/
236254
function isWellFormedNowAnchor(value: Record<string, unknown>): boolean {
255+
if (undeclaredNowAnchorKeys(value).length > 0) return false;
237256
const offset = value.addDays;
238257
if (typeof offset === 'number') return Number.isInteger(offset);
239258
return isFilterNode(offset) && typeof offset.$field === 'string';
240259
}
241260

242261
function malformedNowAnchorRefusal(context: string | undefined, path: string, value: Record<string, unknown>): Error {
243-
const detail = value.addDays === undefined
244-
? 'it carries no addDays — for the unshifted instant write the "{now}" placeholder'
245-
: `its addDays (${preview(value.addDays)}) is neither an integer nor a { "$field": "numeric_column" } reference`;
262+
const undeclared = undeclaredNowAnchorKeys(value);
263+
const detail = undeclared.length > 0
264+
? `it carries an undeclared key ${undeclared.map((key) => preview(key)).join(', ')} — the anchor takes `
265+
+ '$now and addDays only (there is no subDays: a negative addDays subtracts)'
266+
: value.addDays === undefined
267+
? 'it carries no addDays — for the unshifted instant write the "{now}" placeholder'
268+
: `its addDays (${preview(value.addDays)}) is neither an integer nor a { "$field": "numeric_column" } reference`;
246269
return invalidComparandError(
247270
context,
248271
`Filter comparand at ${path} is a malformed now-anchored comparand (${preview(value)}): ${detail}. `

‎packages/spec/src/data/filter.test.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,18 @@ describe('[#25808] NowAnchorSchema — the now-anchored comparand', () => {
265265
expect(NowAnchorSchema.safeParse({ $now: true, addDays: -1, note: 'x' }).success).toBe(false);
266266
});
267267

268+
it('[#26413] …and the `FilterCondition` save door refuses the extra key too, naming it', () => {
269+
for (const [anchor, key] of [
270+
[{ $now: true, addDays: -30, subDays: 7 }, 'subDays'],
271+
[{ $now: true, addDays: GRACE, note: 'x' }, 'note'],
272+
] as const) {
273+
const result = FilterConditionSchema.safeParse({ opened_at: { $lt: anchor } });
274+
expect(result.success, key).toBe(false);
275+
expect(JSON.stringify(result.error?.issues), key).toContain(`undeclared key \\"${key}\\"`);
276+
}
277+
expect(FilterConditionSchema.safeParse({ opened_at: { $lt: { $now: true, addDays: GRACE } } }).success).toBe(true);
278+
});
279+
268280
it('is legal exactly where a { $field } reference is: the six scalar comparisons', () => {
269281
for (const op of ['$eq', '$ne', '$gt', '$gte', '$lt', '$lte']) {
270282
const result = FilterConditionSchema.safeParse({ opened_at: { [op]: { $now: true, addDays: GRACE } } });

0 commit comments

Comments
 (0)