Skip to content

Commit 2af1988

Browse files
os-zhuangclaude
andauthored
fix(formula,spec,core): the RLS write-side check evaluator honours calendar-day upper bounds (ADR-0053 D-D) (#4076)
* fix(formula,spec,core): the RLS write-side check evaluator honours calendar-day upper bounds matchesFilterCondition — the evaluator behind RLS write-side `check` policies (ADR-0058 D4) — compared a bare YYYY-MM-DD $lte bound literally. On a datetime post-image a policy of the shape { signed_on: { $lte: '{today}' } } therefore DENIED every write made after 00:00: the write-side twin of the read-side loss #3777 fixed, and the last of the platform's filter backends that disagreed about what a bare day denotes as a bound. The failure direction is what makes it worth its own change — a rejected write, not a missing row. $lte and a $between max now evaluate half-open against the next calendar day, matching the SQL compiler, the memory and mongo drivers and the analytics preview evaluator. Unchanged per the same semantics table: full-ISO bounds keep exact-instant semantics, $gte/$gt/$lt keep their midnight anchoring, a plain YYYY-MM-DD value compares identically (string ordering makes the forms equivalent), and the evaluator stays fail-closed on a null bound. Where the rule lives: nextUtcCalendarDay moves from @objectstack/core to @objectstack/spec/data, beside date-macros.zod.ts whose vocabulary it interprets — the macros define which bare days an author can NAME, this defines what such a day DENOTES as a bound, and both are protocol. Same species as the pure helpers spec/data already owns, so PD#2 holds. Chosen over @objectstack/types because it adds no dependency edge: every consumer already depends on spec, while core does not depend on types. core re-exports the symbol, so the published surface the drivers and analytics strategies import from is unchanged. Rejected: a private copy in formula — the precedent it would cite (formula's own today()) is debt, not a pattern, and a second copy of this rule is exactly the divergence #3777 catalogued. Six backends now share one definition. Tests: 10 alignment cases in formula (whole-day admit, next-day stop, plain-date equivalence, full-ISO not widened, gte/gt/lt invariance, $between min still bounded, impossible day not rolled over, null bound fail-closed); the primitive's thorough coverage moves to spec, with core keeping a re-export gate. spec 6943 / core 414 / formula 273 / driver-sql 508 / driver-memory 194 / driver-mongodb 107 / service-analytics 329, all green. ADR-0053 gains D-D2 and its D-E4 open item is closed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkL3RGJrzjLEGURsLxfS2f * docs(queries): a bare calendar day as an upper bound means the whole day The comparand table said a bare YYYY-MM-DD on a datetime field means "midnight UTC" full stop. That is true for $gte/$gt/$lt and wrong for $lte and a $between max, where the day denotes the whole day and compiles half-open (ADR-0053 D-D). Documents the operator split, when to pass a full ISO instant instead, and that the rule holds on every backend including the RLS write-side check evaluator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkL3RGJrzjLEGURsLxfS2f * chore(spec): refresh the public API-surface snapshot for nextUtcCalendarDay The API-surface gate diffs spec's built exports against a committed snapshot; adding `nextUtcCalendarDay` to ./data made it stale and turned the TypeScript Type Check job red. Regenerated with the gate's own `gen:api-surface`, so the diff is exactly the one addition — 0 breaking, 1 added, which is what a new export should read as. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EkL3RGJrzjLEGURsLxfS2f --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 575dd6c commit 2af1988

11 files changed

Lines changed: 310 additions & 56 deletions

File tree

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/formula": patch
4+
"@objectstack/core": patch
5+
---
6+
7+
fix(formula,spec,core): the RLS write-side `check` evaluator honours calendar-day upper bounds (ADR-0053 D-D)
8+
9+
`@objectstack/formula`'s `matchesFilterCondition` — the evaluator behind RLS
10+
write-side `check` policies (ADR-0058 D4) — compared a bare `YYYY-MM-DD` `$lte`
11+
bound literally. On a `datetime` post-image that meant a policy of the shape
12+
`{ signed_on: { $lte: '{today}' } }` **denied every write made after 00:00**:
13+
the write-side twin of the read-side data loss #3777 fixed, and the last of the
14+
platform's filter backends that disagreed about what a bare day means as a
15+
bound.
16+
17+
`$lte` and a `$between` max now evaluate half-open against the next calendar
18+
day, matching the SQL compiler, the memory and mongo drivers, and the analytics
19+
preview evaluator. Unchanged, per the same semantics table: full-ISO bounds keep
20+
exact-instant semantics, `$gte`/`$gt`/`$lt` keep their midnight anchoring, and a
21+
plain `YYYY-MM-DD` value compares identically (string ordering makes the two
22+
forms equivalent). The evaluator stays fail-closed on a null bound.
23+
24+
**Where the rule now lives.** `nextUtcCalendarDay` moved from
25+
`@objectstack/core` to `@objectstack/spec/data` — beside `date-macros.zod.ts`,
26+
whose vocabulary it interprets. `formula` cannot depend on `core`, and a second
27+
copy of the rule is exactly the divergence #3777 catalogued; `spec` is the one
28+
package all six consumers already depend on, so this adds no dependency edge.
29+
30+
No import changes are required: `@objectstack/core` re-exports the symbol, so
31+
existing `import { nextUtcCalendarDay } from '@objectstack/core'` keeps working.
32+
New code should prefer `@objectstack/spec/data`.

content/docs/data-modeling/queries.mdx

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ disagree about shape:
8383
| Field type | Canonical comparand | Semantics |
8484
|:---|:---|:---|
8585
| `date` | `YYYY-MM-DD` | Timezone-naive calendar day. A `Date` collapses to its UTC day; a longer ISO string is truncated to its leading date. |
86-
| `datetime` | `YYYY-MM-DDTHH:MM:SS.sssZ` | A UTC instant. A `Date`, an epoch-millisecond number, a bare `YYYY-MM-DD` (→ midnight UTC) and a zone-naive `YYYY-MM-DD HH:MM:SS` (→ read as UTC) all fold to this one form. |
86+
| `datetime` | `YYYY-MM-DDTHH:MM:SS.sssZ` | A UTC instant. A `Date`, an epoch-millisecond number, a bare `YYYY-MM-DD` (→ midnight UTC, but see the whole-day rule below) and a zone-naive `YYYY-MM-DD HH:MM:SS` (→ read as UTC) all fold to this one form. |
8787
| `time` | `HH:MM:SS`, with `.fff` **only** when the milliseconds are non-zero | Timezone-naive wall clock. `'14:30'` and `'14:30:00'` canonicalise identically, so they are the same filter. |
8888

8989
```typescript
@@ -97,6 +97,32 @@ disagree about shape:
9797
}
9898
```
9999

100+
### A bare calendar day as an UPPER bound means the whole day
101+
102+
Canonicalisation settles a comparand's *shape*; which **instant** a bare
103+
`YYYY-MM-DD` denotes additionally depends on the side of the comparison it sits on:
104+
105+
| Operator | A bare `YYYY-MM-DD` means |
106+
|:---|:---|
107+
| `$gte` / `$gt` / `$lt` | that day's `00:00:00.000` |
108+
| `$lte`, and the max of a `$between` | the **whole** day — compiled half-open, as `< next day` |
109+
110+
So `{ created_at: { $gte: '2026-01-01', $lte: '2026-03-31' } }` includes
111+
everything recorded *on* March 31st, not just the instant it began. Without
112+
that rule a dashboard window ending "today" silently dropped every row created
113+
after midnight (#3777). Pass a full ISO timestamp when you want an exact
114+
instant — only the day-granular *string* carries whole-day intent:
115+
116+
```typescript
117+
{ created_at: { $lte: '2026-03-31' } } // through all of March 31st
118+
{ created_at: { $lte: '2026-03-31T12:00:00.000Z' } } // up to noon exactly
119+
```
120+
121+
On a `date` field the two forms are equivalent (`< next day` and `<= day` order
122+
identically over `YYYY-MM-DD` text), so the rule needs no special handling
123+
there. It applies uniformly across the SQL drivers, the in-memory and MongoDB
124+
drivers, analytics windows, and the RLS write-side `check` evaluator.
125+
100126
<Callout type="info">
101127
Canonicalisation runs on **every SQL dialect**, not just SQLite — a zone-naive string bound
102128
into a Postgres `timestamptz` would otherwise be read in the *server's* timezone. MySQL

docs/adr/0053-date-and-datetime-semantics.md

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -782,8 +782,36 @@ cover lives in `mongodb-datetime-storage.test.ts` (against a real MongoDB via
782782
`mongodb-memory-server`) and `memory-datetime-storage.test.ts`. Still open
783783
under D-A3: the relative-token × live-driver × timezone combinations.
784784

785-
One evaluator remains unaligned on both the D-D and D-E rules:
785+
~~One evaluator remains unaligned on both the D-D and D-E rules:
786786
`@objectstack/formula`'s `matchesFilterCondition` (the RLS write-side `check`
787787
path), which cannot depend on `@objectstack/core` for the shared primitive.
788788
Deciding its home — a private copy, as `formula` already keeps for `today()`,
789-
or lowering the utility into `spec`/`types` — is the remaining piece.
789+
or lowering the utility into `spec`/`types` — is the remaining piece.~~
790+
Closed 2026-07-30 — see D-D2 below.
791+
792+
### D-D2 — The primitive lives in `spec`, beside the vocabulary it interprets
793+
794+
`nextUtcCalendarDay` moved from `@objectstack/core` to
795+
`@objectstack/spec/data` (`calendar-day.ts`), next to `date-macros.zod.ts`.
796+
The two are halves of one contract: the macros define which bare days an author
797+
can *name*, D-D defines what such a day *denotes* as a bound. That is protocol,
798+
not business logic — the same species as the pure helpers `spec/data` already
799+
owns (`parseDateMacroParam`, `canonicalAstOperator`, `parseAutonumberFormat`),
800+
so Prime Directive #2 is satisfied.
801+
802+
Chosen over `@objectstack/types` because **it adds no dependency edge**: every
803+
consumer already depends on `spec`, whereas `core` does not depend on `types`
804+
and would have needed a new one. `core` re-exports the symbol, so the published
805+
`@objectstack/core` surface — which the drivers and analytics strategies import
806+
from — is unchanged.
807+
808+
Rejected: a private copy in `formula` (the precedent it would cite, `formula`'s
809+
own `today()`, is debt rather than a pattern). Six backends now share one
810+
definition: the SQL compiler, the memory and mongo drivers, the analytics
811+
raw-SQL strategy, the dataset preview evaluator, and `matchesFilterCondition`.
812+
813+
The write-side gap this closed was not cosmetic. A `check` policy of the shape
814+
`{ signed_on: { $lte: '{today}' } }` evaluated against a `datetime` post-image
815+
**denied every write made after 00:00** — the write-side twin of the read-side
816+
loss #3777 fixed, on the one surface where the failure direction is a rejected
817+
write rather than a missing row.

packages/core/src/utils/datetime.test.ts

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -61,28 +61,14 @@ describe('zonedDateStartToUtcMs — round-trips to the day boundary in the zone'
6161
}
6262
});
6363

64-
describe('nextUtcCalendarDay — the exclusive upper bound of a bare calendar day (#3777)', () => {
65-
it('advances one day, rolling month, year and leap boundaries', () => {
64+
describe('nextUtcCalendarDay — re-exported from @objectstack/spec (ADR-0053 D-D)', () => {
65+
it('is still reachable from this package, with the same semantics', () => {
66+
// The rule itself now lives in spec (six backends share it, and
67+
// `@objectstack/formula` cannot depend on core) — its thorough coverage is
68+
// `packages/spec/src/data/calendar-day.test.ts`. What this asserts is the
69+
// re-export the drivers and analytics strategies import from HERE, so
70+
// dropping it would break them loudly rather than at their call sites.
6671
expect(nextUtcCalendarDay('2026-07-28')).toBe('2026-07-29');
67-
expect(nextUtcCalendarDay('2026-07-31')).toBe('2026-08-01');
68-
expect(nextUtcCalendarDay('2026-12-31')).toBe('2027-01-01');
69-
expect(nextUtcCalendarDay('2024-02-28')).toBe('2024-02-29'); // leap year
70-
expect(nextUtcCalendarDay('2025-02-28')).toBe('2025-03-01');
71-
expect(nextUtcCalendarDay(' 2026-07-28 ')).toBe('2026-07-29'); // trimmed
72-
});
73-
74-
it('returns null for anything that is not a valid bare calendar day', () => {
75-
// Instants keep instant semantics — never widened.
7672
expect(nextUtcCalendarDay('2026-07-28T12:00:00Z')).toBeNull();
77-
expect(nextUtcCalendarDay(new Date('2026-07-28T00:00:00Z'))).toBeNull();
78-
// Impossible days are rejected, not rolled into an invented bound.
79-
expect(nextUtcCalendarDay('2026-02-30')).toBeNull();
80-
expect(nextUtcCalendarDay('2026-13-01')).toBeNull();
81-
// Non-strings / junk.
82-
expect(nextUtcCalendarDay(1753660800000)).toBeNull();
83-
expect(nextUtcCalendarDay(null)).toBeNull();
84-
expect(nextUtcCalendarDay(undefined)).toBeNull();
85-
expect(nextUtcCalendarDay('7/28/2026')).toBeNull();
86-
expect(nextUtcCalendarDay('2026-7-28')).toBeNull(); // not zero-padded → not the canonical shape
8773
});
8874
});

packages/core/src/utils/datetime.ts

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -103,37 +103,16 @@ export function zonedDateStartToUtcMs(ymd: string, tz?: string): number {
103103
}
104104

105105
/**
106-
* The calendar day after a bare `YYYY-MM-DD` string — the exclusive upper
107-
* bound of that day, for compiling "through day X" into the half-open
108-
* `[X, X+1)` a `datetime` column needs (#3777).
106+
* Calendar-day bound semantics (ADR-0053 D-D) now live in `@objectstack/spec`,
107+
* beside the date-macro vocabulary they give meaning to — the fifth consumer
108+
* (`@objectstack/formula`'s RLS write-side `check` evaluator) cannot depend on
109+
* this package, and a second copy of the rule is exactly the divergence #3777
110+
* catalogued.
109111
*
110-
* A bare calendar day used as an upper bound (`$lte`, a `dateRange` end, a
111-
* `{current_month_end}` token) always carries whole-day intent — the author
112-
* means "including everything that happened on X", never "up to the stroke of
113-
* midnight that begins X". On a `date` column `<= X` already says that; on a
114-
* `datetime` column it silently drops every instant after 00:00. The correct
115-
* translation is the half-open pair `>= start AND < nextUtcCalendarDay(end)`
116-
* — the same shape the analytics drill ranges already emit — rather than an
117-
* inclusive `23:59:59.999` constant, which re-opens the gap at whatever
118-
* precision the dialect stores beyond milliseconds.
119-
*
120-
* `< nextUtcCalendarDay(X)` is also *equivalent* to `<= X` for a `date` column
121-
* (plain `YYYY-MM-DD` text ordering), so emitters that cannot see the column
122-
* type (raw-SQL strategies, the dataset preview evaluator) can apply it
123-
* unconditionally to a bare-day bound and be right on both column types.
124-
*
125-
* Returns `null` for anything that is not a valid bare calendar day — full
126-
* ISO timestamps keep instant semantics and must NOT be widened, and an
127-
* impossible day (`2026-02-30`) is rejected the same way
128-
* {@link bucketKeyToCalendarRange} rejects it, so a caller falls back to the
129-
* untranslated comparand instead of inventing a bound.
112+
* Re-exported here so the published `@objectstack/core` surface is unchanged
113+
* for the drivers and analytics strategies that already import it from here.
130114
*/
131-
export function nextUtcCalendarDay(value: unknown): string | null {
132-
if (typeof value !== 'string') return null;
133-
const day = value.trim();
134-
if (!/^\d{4}-\d{2}-\d{2}$/.test(day)) return null;
135-
return bucketKeyToCalendarRange(day, 'day')?.end ?? null;
136-
}
115+
export { nextUtcCalendarDay } from '@objectstack/spec/data';
137116

138117
/**
139118
* Granularity of a canonical date-bucket key. Mirrors `@objectstack/spec`'s

packages/formula/src/matches-filter.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,55 @@ describe('matchesFilterCondition — FAIL CLOSED', () => {
108108
expect(m(rec, 'nope' as never)).toBe(false);
109109
});
110110
});
111+
112+
describe('matchesFilterCondition — calendar-day upper bounds (ADR-0053 D-D, #3777)', () => {
113+
// A `check` policy of this shape on a datetime post-image used to DENY every
114+
// write made after 00:00 of the bound day — the write-side twin of the
115+
// read-side data loss #3777 fixed. The four other filter backends already
116+
// share this rule; this evaluator was the last one that disagreed.
117+
const at = (created_at: string) => ({ created_at });
118+
119+
it('a bare-day $lte admits the whole day', () => {
120+
expect(m(at('2026-07-28T00:00:00.000Z'), { created_at: { $lte: '2026-07-28' } })).toBe(true);
121+
expect(m(at('2026-07-28T09:15:00.000Z'), { created_at: { $lte: '2026-07-28' } })).toBe(true);
122+
expect(m(at('2026-07-28T23:59:59.999Z'), { created_at: { $lte: '2026-07-28' } })).toBe(true);
123+
});
124+
125+
it('…and stops at the next day', () => {
126+
expect(m(at('2026-07-29T00:00:00.000Z'), { created_at: { $lte: '2026-07-28' } })).toBe(false);
127+
});
128+
129+
it('a plain calendar-day value is unchanged (string ordering is equivalent)', () => {
130+
expect(m({ signed_on: '2026-07-28' }, { signed_on: { $lte: '2026-07-28' } })).toBe(true);
131+
expect(m({ signed_on: '2026-07-29' }, { signed_on: { $lte: '2026-07-28' } })).toBe(false);
132+
});
133+
134+
it('a full-ISO bound keeps exact-instant semantics — never widened', () => {
135+
expect(m(at('2026-07-28T09:15:00.000Z'), { created_at: { $lte: '2026-07-28T12:00:00.000Z' } })).toBe(true);
136+
expect(m(at('2026-07-28T21:40:00.000Z'), { created_at: { $lte: '2026-07-28T12:00:00.000Z' } })).toBe(false);
137+
});
138+
139+
it('$gte / $gt / $lt keep their midnight anchoring', () => {
140+
expect(m(at('2026-07-28T09:15:00.000Z'), { created_at: { $gte: '2026-07-28' } })).toBe(true);
141+
expect(m(at('2026-07-27T23:59:59.999Z'), { created_at: { $gte: '2026-07-28' } })).toBe(false);
142+
expect(m(at('2026-07-28T09:15:00.000Z'), { created_at: { $lt: '2026-07-28' } })).toBe(false);
143+
});
144+
145+
it('$between inherits the rule on its max, and still bounds the min', () => {
146+
expect(m(at('2026-07-28T21:40:00.000Z'), { created_at: { $between: ['2026-04-29', '2026-07-28'] } })).toBe(true);
147+
expect(m(at('2026-07-29T00:00:00.000Z'), { created_at: { $between: ['2026-04-29', '2026-07-28'] } })).toBe(false);
148+
expect(m(at('2026-04-28T23:00:00.000Z'), { created_at: { $between: ['2026-04-29', '2026-07-28'] } })).toBe(false);
149+
});
150+
151+
it('an impossible day is not rolled over — the bound stays as written', () => {
152+
// `2026-02-30` is rejected by the primitive, so the comparison falls back
153+
// to the literal `<=` rather than silently querying March 2nd.
154+
expect(m({ signed_on: '2026-02-30' }, { signed_on: { $lte: '2026-02-30' } })).toBe(true);
155+
expect(m({ signed_on: '2026-03-01' }, { signed_on: { $lte: '2026-02-30' } })).toBe(false);
156+
});
157+
158+
it('stays fail-closed on a null bound', () => {
159+
expect(m(at('2026-07-28T09:15:00.000Z'), { created_at: { $lte: null } as never })).toBe(false);
160+
expect(m(at('2026-07-28T09:15:00.000Z'), { created_at: { $between: ['2026-04-29', null] } as never })).toBe(false);
161+
});
162+
});

packages/formula/src/matches-filter.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
*/
1818

1919
import type { FilterCondition } from '@objectstack/spec/data';
20+
import { nextUtcCalendarDay } from '@objectstack/spec/data';
2021

2122
/** True iff `record` satisfies `filter`. A null/empty filter matches everything. */
2223
export function matchesFilterCondition(record: Record<string, unknown>, filter: FilterCondition | null | undefined): boolean {
@@ -72,12 +73,12 @@ function evalOp(actual: unknown, op: string, raw: unknown, record: Record<string
7273
case '$gt': return actual != null && v != null && (actual as never) > (v as never);
7374
case '$gte': return actual != null && v != null && (actual as never) >= (v as never);
7475
case '$lt': return actual != null && v != null && (actual as never) < (v as never);
75-
case '$lte': return actual != null && v != null && (actual as never) <= (v as never);
76+
case '$lte': return actual != null && v != null && lteBound(actual, v);
7677
case '$in': return Array.isArray(v) && v.some((x) => looseEq(actual, x));
7778
case '$nin': return Array.isArray(v) && !v.some((x) => looseEq(actual, x));
7879
case '$between':
79-
return Array.isArray(v) && v.length === 2 && actual != null
80-
&& (actual as never) >= (v[0] as never) && (actual as never) <= (v[1] as never);
80+
return Array.isArray(v) && v.length === 2 && actual != null && v[0] != null && v[1] != null
81+
&& (actual as never) >= (v[0] as never) && lteBound(actual, v[1]);
8182
case '$contains': return typeof actual === 'string' && typeof v === 'string' && actual.includes(v);
8283
case '$notContains': return !(typeof actual === 'string' && typeof v === 'string' && actual.includes(v));
8384
case '$startsWith': return typeof actual === 'string' && typeof v === 'string' && actual.startsWith(v);
@@ -88,6 +89,31 @@ function evalOp(actual: unknown, op: string, raw: unknown, record: Record<string
8889
}
8990
}
9091

92+
/**
93+
* The inclusive-upper-bound comparison, with the calendar-day rule the rest of
94+
* the platform applies (ADR-0053 D-D, #3777): a bare `YYYY-MM-DD` bound means
95+
* "through that whole day", so it is evaluated half-open against the next day
96+
* rather than against that day's midnight.
97+
*
98+
* Without this, a `check` policy of the shape `{ signed_on: { $lte: '{today}' } }`
99+
* evaluated on a `datetime` post-image **denied every write made after 00:00** —
100+
* the write-side twin of the read-side data loss #3777 fixed, and the reason
101+
* this evaluator had to stop being the one backend that disagreed. The four
102+
* other backends (SQL compiler, memory, mongo, the analytics preview) already
103+
* share this rule via the same primitive.
104+
*
105+
* String ordering makes `< nextDay` equivalent to `<= day` for a plain
106+
* `YYYY-MM-DD` value, so no field-type lookup is needed — which matters here,
107+
* because this evaluator sees a bare record and has no schema to consult.
108+
* A full-ISO or non-string bound keeps exact-instant semantics.
109+
*/
110+
function lteBound(actual: unknown, bound: unknown): boolean {
111+
if (bound == null) return false;
112+
const nextDay = nextUtcCalendarDay(bound);
113+
if (nextDay != null) return (actual as never) < (nextDay as never);
114+
return (actual as never) <= (bound as never);
115+
}
116+
91117
/** Resolve a `{ $field: 'path' }` reference against the record; else passthrough. */
92118
function resolveValue(raw: unknown, record: Record<string, unknown>): unknown {
93119
if (raw && typeof raw === 'object' && !Array.isArray(raw) && '$field' in (raw as Record<string, unknown>)) {

packages/spec/api-surface.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,7 @@
582582
"isTitleEligible (function)",
583583
"isUniqueDeclared (function)",
584584
"missingFieldValues (function)",
585+
"nextUtcCalendarDay (function)",
585586
"objectForm (const)",
586587
"objectTitleCompleteness (function)",
587588
"parseAutonumberFormat (function)",

0 commit comments

Comments
 (0)