From be5e52143956291c2f580398b8193731bb94d747 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:36:05 +0800 Subject: [PATCH 1/3] refactor(spec)!: retire three orphan operator vocabularies (objectui#2945) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each had no importer in this repo, objectui, or cloud, and each contradicted the vocabulary that is actually enforced. - AggregationFunctionEnum (shared/enums.zod.ts): claimed in its own doc comment to be "used across query, data-engine, analytics, field"; used by nothing. data/query.zod.ts's AggregationFunction is what the query engine, dataset compiler and native-SQL strategy gate on, and the two disagreed (percentile/median/stddev/variance vs array_agg/string_agg). It also exported a *type* named AggregationFunction against the other file's *value* of that name. - FilterOperator + EventFilterCondition + EventFilterSchema (api/websocket.zod.ts), and EventSubscriptionSchema.filters with them. No runtime evaluated an event filter — matchesSubscription matches on object name and event type only — and the live subscription shape is the separate, deliberately unvalidated `filters: z.unknown()` on api/realtime.zod.ts. A subscriber who set `filters` got every event. - ODataFilterOperatorSchema (api/odata.zod.ts): nothing parses $filter against it ($filter is an opaque string), and an enum mixing operators with parentheses describes tokens, not a grammar. Breaking only in that all three were public exports, so: major. Nothing is narrowed — no accepted value stops being accepted, so no stored metadata or in-flight payload changes meaning. That is what made this track safe; narrowing VALID_AST_OPERATORS or retiring a VIEW_FILTER_OPERATORS alias is not, and stays blocked on #3948. Full spec suite 6917 tests / 266 files green; tsc --noEmit clean. Refs objectstack-ai/objectui#2945, objectstack-ai/objectui#2901, #3948 Co-Authored-By: Claude Opus 5 --- ...tire-three-orphan-operator-vocabularies.md | 54 ++++++++ packages/spec/src/api/odata.test.ts | 33 ----- packages/spec/src/api/odata.zod.ts | 41 ++---- packages/spec/src/api/websocket.test.ts | 126 ------------------ packages/spec/src/api/websocket.zod.ts | 69 +++------- packages/spec/src/shared/enums.test.ts | 26 ---- packages/spec/src/shared/enums.zod.ts | 15 ++- 7 files changed, 92 insertions(+), 272 deletions(-) create mode 100644 .changeset/retire-three-orphan-operator-vocabularies.md diff --git a/.changeset/retire-three-orphan-operator-vocabularies.md b/.changeset/retire-three-orphan-operator-vocabularies.md new file mode 100644 index 0000000000..3f8bd25d97 --- /dev/null +++ b/.changeset/retire-three-orphan-operator-vocabularies.md @@ -0,0 +1,54 @@ +--- +"@objectstack/spec": major +--- + +refactor(spec)!: retire three orphan operator vocabularies (objectui#2945 Track A) + +An audit of every comparison/aggregation vocabulary the spec ships +(objectstack-ai/objectui#2901) found the operator vocabularies had multiplied +past what any code consults. Three had **no importer at all** — not in this +repo, not in objectui, not in cloud — and each contradicted the vocabulary that +is actually enforced. Removed rather than reconciled: a second name for one +concept is how they drifted apart in the first place. + +**`AggregationFunctionEnum`** (`shared/enums.zod.ts`). Its own doc comment +claimed it was *"used across query, data-engine, analytics, field"*. It was used +by nothing. `AggregationFunction` (`data/query.zod.ts`) is the vocabulary the +query engine, `service-analytics`' dataset compiler and the native-SQL strategy +all gate on — and the two disagreed: this one carried +`percentile`/`median`/`stddev`/`variance`, that one carries +`array_agg`/`string_agg`. It also exported a *type* named `AggregationFunction` +while `data/query.zod.ts` exports a *value* of that name, so the two occupied +the same identifier in different declaration spaces with different members. + +**`FilterOperator`** + `EventFilterCondition` + `EventFilterSchema` +(`api/websocket.zod.ts`), reached from `EventSubscriptionSchema.filters`. No +runtime ever evaluated an event filter — `matchesSubscription` matches on object +name and event type only (`contracts/realtime-service.ts`) — and the +subscription shape the transports actually carry is the separate, deliberately +unvalidated `filters: z.unknown()` on `SubscriptionEventSchema` +(`api/realtime.zod.ts`). So this was a *second* modelling of event filtering +that advertised a capability no code provided: a subscriber who set `filters` +would have received every event. The `filters` key is removed with it; the +surface that matters is the realtime contract, and it should grow one filter +vocabulary rather than inherit an orphan's. + +**`ODataFilterOperatorSchema`** (`api/odata.zod.ts`). Nothing parses an OData +`$filter` against it — `$filter` is carried as an opaque string on +`ODataQuerySchema` and as the `odata` adapter template in +`query-adapter.zod.ts` — and an enum mixing operators with `(`/`)` could not +validate an expression anyway, since it describes tokens, not a grammar. A real +implementation needs a parser, and that parser should lower onto +`AST_OPERATOR_MAP` like every other entry point. + +**Breaking, in the narrowest sense.** All three were reachable as public +exports (`@objectstack/spec/shared` and `@objectstack/spec/api`), so this is a +`major`. No consumer exists to break: verified by grep across framework +`packages/` + `apps/`, objectui, and cloud. Nothing is *narrowed* — no accepted +value stops being accepted, so no already-stored metadata or in-flight payload +changes meaning. That is what made this the one track of objectui#2945 that was +safe to start; narrowing `VALID_AST_OPERATORS` or retiring a +`VIEW_FILTER_OPERATORS` alias is not, and remains blocked on #3948. + +Verified: full `@objectstack/spec` suite **6917 tests across 266 files**, plus +`tsc --noEmit`, both clean. diff --git a/packages/spec/src/api/odata.test.ts b/packages/spec/src/api/odata.test.ts index 3241edb027..0b4fb06832 100644 --- a/packages/spec/src/api/odata.test.ts +++ b/packages/spec/src/api/odata.test.ts @@ -1,7 +1,6 @@ import { describe, it, expect } from 'vitest'; import { ODataQuerySchema, - ODataFilterOperatorSchema, ODataFilterFunctionSchema, ODataResponseSchema, ODataErrorSchema, @@ -269,38 +268,6 @@ describe('ODataQuerySchema', () => { }); }); -describe('ODataFilterOperatorSchema', () => { - it('should accept comparison operators', () => { - const operators = ['eq', 'ne', 'lt', 'le', 'gt', 'ge']; - - operators.forEach(op => { - expect(() => ODataFilterOperatorSchema.parse(op)).not.toThrow(); - }); - }); - - it('should accept logical operators', () => { - const operators = ['and', 'or', 'not']; - - operators.forEach(op => { - expect(() => ODataFilterOperatorSchema.parse(op)).not.toThrow(); - }); - }); - - it('should accept grouping operators', () => { - expect(() => ODataFilterOperatorSchema.parse('(')).not.toThrow(); - expect(() => ODataFilterOperatorSchema.parse(')')).not.toThrow(); - }); - - it('should accept other operators', () => { - expect(() => ODataFilterOperatorSchema.parse('in')).not.toThrow(); - expect(() => ODataFilterOperatorSchema.parse('has')).not.toThrow(); - }); - - it('should reject invalid operators', () => { - expect(() => ODataFilterOperatorSchema.parse('invalid')).toThrow(); - }); -}); - describe('ODataFilterFunctionSchema', () => { it('should accept string functions', () => { const functions = [ diff --git a/packages/spec/src/api/odata.zod.ts b/packages/spec/src/api/odata.zod.ts index ed82ebb5b5..b0c0274522 100644 --- a/packages/spec/src/api/odata.zod.ts +++ b/packages/spec/src/api/odata.zod.ts @@ -209,35 +209,18 @@ export const ODataQuerySchema = lazySchema(() => z.object({ export type ODataQuery = z.infer; -/** - * OData Filter Operator - * - * Standard comparison and logical operators in OData filter expressions. - */ -export const ODataFilterOperatorSchema = lazySchema(() => z.enum([ - // Comparison Operators - 'eq', // Equal to - 'ne', // Not equal to - 'lt', // Less than - 'le', // Less than or equal to - 'gt', // Greater than - 'ge', // Greater than or equal to - - // Logical Operators - 'and', // Logical AND - 'or', // Logical OR - 'not', // Logical NOT - - // Grouping - '(', // Left parenthesis - ')', // Right parenthesis - - // Other - 'in', // Value in list - 'has', // Has flag (for enum flags) -])); - -export type ODataFilterOperator = z.infer; +// `ODataFilterOperatorSchema` — one more spelling of comparison +// (eq/ne/lt/le/gt/ge, and/or/not, `(`/`)`, in/has) — lived here with no +// importer in this repo, objectui, or cloud. Nothing parses an OData `$filter` +// against it: `$filter` is carried as an opaque string +// (`ODataQuerySchema.$filter`, and the `odata` adapter template in +// `query-adapter.zod.ts`), and an enum that mixes operators with parentheses +// could not validate an expression anyway — it describes tokens, not a grammar. +// +// A real OData implementation needs a parser, and that parser should lower onto +// `AST_OPERATOR_MAP` (`data/filter.zod.ts`) like every other entry point. Kept +// as an enum, this was only a way for the count of filter vocabularies to grow. +// objectui#2945. /** * OData Filter Function diff --git a/packages/spec/src/api/websocket.test.ts b/packages/spec/src/api/websocket.test.ts index d59338d471..b1ce26b9d4 100644 --- a/packages/spec/src/api/websocket.test.ts +++ b/packages/spec/src/api/websocket.test.ts @@ -1,9 +1,6 @@ import { describe, it, expect } from 'vitest'; import { WebSocketMessageType, - FilterOperator, - EventFilterCondition, - EventFilterSchema, EventSubscriptionSchema, UnsubscribeRequestSchema, WebSocketPresenceStatus, @@ -50,115 +47,6 @@ describe('WebSocketMessageType', () => { }); }); -describe('FilterOperator', () => { - it('should accept valid filter operators', () => { - const operators = ['eq', 'ne', 'gt', 'gte', 'lt', 'lte', 'in', 'nin', 'contains', 'startsWith', 'endsWith', 'exists', 'regex']; - - operators.forEach(op => { - expect(() => FilterOperator.parse(op)).not.toThrow(); - }); - }); - - it('should reject invalid operators', () => { - expect(() => FilterOperator.parse('like')).toThrow(); - expect(() => FilterOperator.parse('between')).toThrow(); - }); -}); - -describe('EventFilterCondition', () => { - it('should accept valid filter condition', () => { - const condition = { - field: 'status', - operator: 'eq', - value: 'active', - }; - - expect(() => EventFilterCondition.parse(condition)).not.toThrow(); - }); - - it('should accept filter with dot notation field path', () => { - const condition = { - field: 'user.email', - operator: 'contains', - value: '@example.com', - }; - - const parsed = EventFilterCondition.parse(condition); - expect(parsed.field).toBe('user.email'); - }); - - it('should accept exists operator without value', () => { - const condition = { - field: 'optional_field', - operator: 'exists', - }; - - const parsed = EventFilterCondition.parse(condition); - expect(parsed.value).toBeUndefined(); - }); -}); - -describe('EventFilterSchema', () => { - it('should accept simple filter with conditions', () => { - const filter = { - conditions: [ - { field: 'status', operator: 'eq', value: 'active' }, - { field: 'amount', operator: 'gt', value: 1000 }, - ], - }; - - expect(() => EventFilterSchema.parse(filter)).not.toThrow(); - }); - - it('should accept AND logical combination', () => { - const filter = { - and: [ - { conditions: [{ field: 'status', operator: 'eq', value: 'active' }] }, - { conditions: [{ field: 'verified', operator: 'eq', value: true }] }, - ], - }; - - expect(() => EventFilterSchema.parse(filter)).not.toThrow(); - }); - - it('should accept OR logical combination', () => { - const filter = { - or: [ - { conditions: [{ field: 'type', operator: 'eq', value: 'urgent' }] }, - { conditions: [{ field: 'priority', operator: 'gte', value: 5 }] }, - ], - }; - - expect(() => EventFilterSchema.parse(filter)).not.toThrow(); - }); - - it('should accept NOT logical negation', () => { - const filter = { - not: { - conditions: [{ field: 'deleted', operator: 'eq', value: true }], - }, - }; - - expect(() => EventFilterSchema.parse(filter)).not.toThrow(); - }); - - it('should accept complex nested filters', () => { - const filter = { - and: [ - { conditions: [{ field: 'status', operator: 'eq', value: 'active' }] }, - { - or: [ - { conditions: [{ field: 'type', operator: 'eq', value: 'premium' }] }, - { conditions: [{ field: 'amount', operator: 'gte', value: 10000 }] }, - ], - }, - ], - }; - - expect(() => EventFilterSchema.parse(filter)).not.toThrow(); - }); -}); - describe('EventSubscriptionSchema', () => { it('should accept valid minimal subscription', () => { const subscription: EventSubscription = { @@ -189,20 +77,6 @@ describe('EventSubscriptionSchema', () => { expect(parsed.objects).toEqual(['account', 'contact']); }); - it('should accept subscription with advanced filters', () => { - const subscription = { - subscriptionId: '550e8400-e29b-41d4-a716-446655440000', - events: ['record.created'], - filters: { - conditions: [ - { field: 'amount', operator: 'gt', value: 5000 }, - ], - }, - }; - - expect(() => EventSubscriptionSchema.parse(subscription)).not.toThrow(); - }); - it('should accept subscription with channels', () => { const subscription = { subscriptionId: '550e8400-e29b-41d4-a716-446655440000', diff --git a/packages/spec/src/api/websocket.zod.ts b/packages/spec/src/api/websocket.zod.ts index 74a1f8fc4a..a79e887694 100644 --- a/packages/spec/src/api/websocket.zod.ts +++ b/packages/spec/src/api/websocket.zod.ts @@ -53,57 +53,23 @@ export type WebSocketMessageType = z.infer; // Event Subscription // ========================================== -/** - * Event Filter Operator Enum - * SQL-like filter operators for event filtering - */ -export const FilterOperator = z.enum([ - 'eq', // Equal - 'ne', // Not equal - 'gt', // Greater than - 'gte', // Greater than or equal - 'lt', // Less than - 'lte', // Less than or equal - 'in', // In array - 'nin', // Not in array - 'contains', // String contains - 'startsWith', // String starts with - 'endsWith', // String ends with - 'exists', // Field exists - 'regex', // Regex match -]); - -export type FilterOperator = z.infer; - -/** - * Event Filter Condition - * Defines a single filter condition for event filtering - */ -export const EventFilterCondition = z.object({ - field: z.string().describe('Field path to filter on (supports dot notation, e.g., "user.email")'), - operator: FilterOperator.describe('Comparison operator'), - value: z.unknown().optional().describe('Value to compare against (not needed for "exists" operator)'), -}); - -export type EventFilterCondition = z.infer; - -/** - * Event Filter Schema - * Logical combination of filter conditions - */ -export const EventFilterSchema: z.ZodType<{ - conditions?: EventFilterCondition[]; - and?: EventFilter[]; - or?: EventFilter[]; - not?: EventFilter; -}> = lazySchema(() => z.object({ - conditions: z.array(EventFilterCondition).optional().describe('Array of filter conditions'), - and: z.lazy(() => z.array(EventFilterSchema)).optional().describe('AND logical combination of filters'), - or: z.lazy(() => z.array(EventFilterSchema)).optional().describe('OR logical combination of filters'), - not: z.lazy(() => EventFilterSchema).optional().describe('NOT logical negation of filter'), -})); - -export type EventFilter = z.infer; +// A `FilterOperator` enum (eq/ne/gt/gte/lt/lte/in/nin/contains/startsWith/ +// endsWith/exists/regex) and the `EventFilterCondition` + `EventFilterSchema` +// pair it fed lived here, reached from `EventSubscriptionSchema.filters`. +// +// Nothing imported any of it — not this repo, not objectui, not cloud — and no +// runtime ever evaluated an event filter: `matchesSubscription` matches on +// object name and event type only (see `contracts/realtime-service.ts`), and the +// subscription shape the transports actually carry is the separate, deliberately +// unvalidated `filters: z.unknown()` on `SubscriptionEventSchema` +// (`api/realtime.zod.ts`). +// +// So this was a *second* spelling of event filtering, disagreeing with both the +// live one and with `VALID_AST_OPERATORS`, that advertised a capability no code +// provided — a subscriber setting `filters` would have received every event. +// Removed rather than wired up: the surface that matters is the realtime +// contract, and it should grow one filter vocabulary, not inherit an orphan's. +// objectui#2945. /** * Event Pattern Schema @@ -127,7 +93,6 @@ export const EventSubscriptionSchema = lazySchema(() => z.object({ subscriptionId: z.string().uuid().describe('Unique subscription identifier'), events: z.array(EventPatternSchema).describe('Event patterns to subscribe to (supports wildcards, e.g., "record.*", "user.created")'), objects: z.array(z.string()).optional().describe('Object names to filter events by (e.g., ["account", "contact"])'), - filters: EventFilterSchema.optional().describe('Advanced filter conditions for event payloads'), channels: z.array(z.string()).optional().describe('Channel names for scoped subscriptions'), })); diff --git a/packages/spec/src/shared/enums.test.ts b/packages/spec/src/shared/enums.test.ts index fa3861ff74..f227283f44 100644 --- a/packages/spec/src/shared/enums.test.ts +++ b/packages/spec/src/shared/enums.test.ts @@ -1,6 +1,5 @@ import { describe, it, expect } from 'vitest'; import { - AggregationFunctionEnum, SortDirectionEnum, SortItemSchema, MutationEventEnum, @@ -8,31 +7,6 @@ import { CacheStrategyEnum, } from './enums.zod'; -describe('AggregationFunctionEnum', () => { - it('should accept all valid aggregation functions', () => { - const valid = [ - 'count', 'sum', 'avg', 'min', 'max', - 'count_distinct', 'percentile', 'median', 'stddev', 'variance', - ]; - valid.forEach((v) => { - expect(() => AggregationFunctionEnum.parse(v)).not.toThrow(); - }); - }); - - it('should reject invalid values', () => { - const invalid = ['COUNT', 'SUM', 'average', 'total', '', 'unknown']; - invalid.forEach((v) => { - expect(() => AggregationFunctionEnum.parse(v)).toThrow(); - }); - }); - - it('should reject non-string types', () => { - expect(() => AggregationFunctionEnum.parse(123)).toThrow(); - expect(() => AggregationFunctionEnum.parse(null)).toThrow(); - expect(() => AggregationFunctionEnum.parse(undefined)).toThrow(); - }); -}); - describe('SortDirectionEnum', () => { it('should accept asc and desc', () => { expect(SortDirectionEnum.parse('asc')).toBe('asc'); diff --git a/packages/spec/src/shared/enums.zod.ts b/packages/spec/src/shared/enums.zod.ts index 728c221425..8974c7e973 100644 --- a/packages/spec/src/shared/enums.zod.ts +++ b/packages/spec/src/shared/enums.zod.ts @@ -6,13 +6,16 @@ import { z } from 'zod'; // Shared Enumerations // ============================================================================ -/** Aggregation functions used across query, data-engine, analytics, field */ import { lazySchema } from './lazy-schema'; -export const AggregationFunctionEnum = z.enum([ - 'count', 'sum', 'avg', 'min', 'max', - 'count_distinct', 'percentile', 'median', 'stddev', 'variance', -]).describe('Standard aggregation functions'); -export type AggregationFunction = z.infer; + +// `AggregationFunctionEnum` lived here, claiming in its own doc comment to be +// "used across query, data-engine, analytics, field". It was used by nothing — +// no importer in this repo, objectui, or cloud — while `AggregationFunction` +// (`data/query.zod.ts`) is the vocabulary the query engine, dataset compiler and +// native-SQL strategy all gate on. The two even disagreed: this one had +// percentile/median/stddev/variance, that one has array_agg/string_agg. Removed +// rather than reconciled, because a second name for one concept is how the +// vocabularies drifted apart in the first place. objectui#2945. /** Sort direction used across query, data-engine, analytics */ export const SortDirectionEnum = z.enum(['asc', 'desc']) From a021d68530f55fd92cf505ef3382a1322c00ba4e Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:18:09 +0800 Subject: [PATCH 2/3] fix(spec): move the generated artefacts with the deletions, and keep the `filters` key MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three ratchets and the reference docs track the spec's public surface; the deletions had to be recorded in each. - json-schema.manifest.json: drop the five schemas no longer emitted (api/EventFilter, api/EventFilterCondition, api/FilterOperator, api/ODataFilterOperator, shared/AggregationFunctionEnum). Left silent, gen:docs would delete their reference pages without a trace (#2978). - authorable-surface.json: drop the seven keys of the two deleted objects. - api-surface.json / api-surface-signatures.json: regenerated. - content/docs/references/{api/odata,api/websocket,shared/enums}.mdx: regenerated. EventSubscriptionSchema.filters is restored, now `z.unknown()` carrying the same NOT-YET-ENFORCED marker as SubscriptionEventSchema.filters. Retiring an object key needs a tombstone plus a conversion (ADR-0104) — the right rule, and the wrong trade for a shape nothing validated and no author writes: Track A is meant to carry no migration. The two subscription surfaces now describe event filtering identically, and neither implies enforcement that does not exist. check:docs, check:api-surface, check:authorable-surface and check:skill-docs all pass; spec suite still 6917 / 266. Co-Authored-By: Claude Opus 5 --- ...tire-three-orphan-operator-vocabularies.md | 24 ++++++-- content/docs/references/api/odata.mdx | 25 +------- content/docs/references/api/websocket.mdx | 58 ++----------------- content/docs/references/shared/enums.mdx | 28 ++------- packages/spec/api-surface.json | 8 --- packages/spec/authorable-surface.json | 7 --- packages/spec/json-schema.manifest.json | 5 -- packages/spec/src/api/websocket.zod.ts | 24 ++++++-- 8 files changed, 50 insertions(+), 129 deletions(-) diff --git a/.changeset/retire-three-orphan-operator-vocabularies.md b/.changeset/retire-three-orphan-operator-vocabularies.md index 3f8bd25d97..7e41c8faf9 100644 --- a/.changeset/retire-three-orphan-operator-vocabularies.md +++ b/.changeset/retire-three-orphan-operator-vocabularies.md @@ -28,10 +28,17 @@ name and event type only (`contracts/realtime-service.ts`) — and the subscription shape the transports actually carry is the separate, deliberately unvalidated `filters: z.unknown()` on `SubscriptionEventSchema` (`api/realtime.zod.ts`). So this was a *second* modelling of event filtering -that advertised a capability no code provided: a subscriber who set `filters` -would have received every event. The `filters` key is removed with it; the -surface that matters is the realtime contract, and it should grow one filter -vocabulary rather than inherit an orphan's. +that described a capability no code provided: a subscriber who set `filters` +received every event regardless. + +The `filters` **key stays**, now typed `z.unknown()` with the same +NOT-YET-ENFORCED marker as its `api/realtime.zod.ts` counterpart. Retiring an +object key requires a tombstone plus a conversion (ADR-0104), which is the right +rule and the wrong trade here — there is no author to migrate for a shape nothing +validated, and Track A is meant to carry no migration. The two subscription +surfaces now describe event filtering identically, and neither implies an +enforcement that does not exist. Whichever grows real filtering should lower onto +`AST_OPERATOR_MAP` rather than reintroduce a vocabulary of its own. **`ODataFilterOperatorSchema`** (`api/odata.zod.ts`). Nothing parses an OData `$filter` against it — `$filter` is carried as an opaque string on @@ -50,5 +57,12 @@ changes meaning. That is what made this the one track of objectui#2945 that was safe to start; narrowing `VALID_AST_OPERATORS` or retiring a `VIEW_FILTER_OPERATORS` alias is not, and remains blocked on #3948. +The generated artefacts move with the deletions, as the ratchets require: +`json-schema.manifest.json` drops the five unpublished schemas, +`authorable-surface.json` the seven keys of the two deleted objects, +`api-surface.json` the eight exports, and the three reference-doc pages are +regenerated. + Verified: full `@objectstack/spec` suite **6917 tests across 266 files**, plus -`tsc --noEmit`, both clean. +`tsc --noEmit`, `check:docs`, `check:api-surface`, `check:authorable-surface` and +`check:skill-docs`, all clean. diff --git a/content/docs/references/api/odata.mdx b/content/docs/references/api/odata.mdx index 6121bc5841..1ce9e0f576 100644 --- a/content/docs/references/api/odata.mdx +++ b/content/docs/references/api/odata.mdx @@ -112,8 +112,8 @@ count: true ## TypeScript Usage ```typescript -import { ODataConfig, ODataError, ODataFilterFunction, ODataFilterOperator, ODataMetadata, ODataQuery, ODataResponse } from '@objectstack/spec/api'; -import type { ODataConfig, ODataError, ODataFilterFunction, ODataFilterOperator, ODataMetadata, ODataQuery, ODataResponse } from '@objectstack/spec/api'; +import { ODataConfig, ODataError, ODataFilterFunction, ODataMetadata, ODataQuery, ODataResponse } from '@objectstack/spec/api'; +import type { ODataConfig, ODataError, ODataFilterFunction, ODataMetadata, ODataQuery, ODataResponse } from '@objectstack/spec/api'; // Validate data const result = ODataConfig.parse(data); @@ -179,27 +179,6 @@ const result = ODataConfig.parse(data); * `all` ---- - -## ODataFilterOperator - -### Allowed Values - -* `eq` -* `ne` -* `lt` -* `le` -* `gt` -* `ge` -* `and` -* `or` -* `not` -* `(` -* `)` -* `in` -* `has` - - --- ## ODataMetadata diff --git a/content/docs/references/api/websocket.mdx b/content/docs/references/api/websocket.mdx index 31addeaebb..dae07c1e3a 100644 --- a/content/docs/references/api/websocket.mdx +++ b/content/docs/references/api/websocket.mdx @@ -30,8 +30,8 @@ runtime today. ## TypeScript Usage ```typescript -import { AckMessage, CursorMessage, CursorPosition, DocumentState, EditMessage, EditOperation, EditOperationType, ErrorMessage, EventFilter, EventFilterCondition, EventMessage, EventPattern, EventSubscription, FilterOperator, PingMessage, PongMessage, PresenceMessage, PresenceState, PresenceUpdate, SimpleCursorPosition, SimplePresenceState, SubscribeMessage, UnsubscribeMessage, UnsubscribeRequest, WebSocketConfig, WebSocketEvent, WebSocketMessage, WebSocketMessageType, WebSocketPresenceStatus, WebSocketServerConfig } from '@objectstack/spec/api'; -import type { AckMessage, CursorMessage, CursorPosition, DocumentState, EditMessage, EditOperation, EditOperationType, ErrorMessage, EventFilter, EventFilterCondition, EventMessage, EventPattern, EventSubscription, FilterOperator, PingMessage, PongMessage, PresenceMessage, PresenceState, PresenceUpdate, SimpleCursorPosition, SimplePresenceState, SubscribeMessage, UnsubscribeMessage, UnsubscribeRequest, WebSocketConfig, WebSocketEvent, WebSocketMessage, WebSocketMessageType, WebSocketPresenceStatus, WebSocketServerConfig } from '@objectstack/spec/api'; +import { AckMessage, CursorMessage, CursorPosition, DocumentState, EditMessage, EditOperation, EditOperationType, ErrorMessage, EventMessage, EventPattern, EventSubscription, PingMessage, PongMessage, PresenceMessage, PresenceState, PresenceUpdate, SimpleCursorPosition, SimplePresenceState, SubscribeMessage, UnsubscribeMessage, UnsubscribeRequest, WebSocketConfig, WebSocketEvent, WebSocketMessage, WebSocketMessageType, WebSocketPresenceStatus, WebSocketServerConfig } from '@objectstack/spec/api'; +import type { AckMessage, CursorMessage, CursorPosition, DocumentState, EditMessage, EditOperation, EditOperationType, ErrorMessage, EventMessage, EventPattern, EventSubscription, PingMessage, PongMessage, PresenceMessage, PresenceState, PresenceUpdate, SimpleCursorPosition, SimplePresenceState, SubscribeMessage, UnsubscribeMessage, UnsubscribeRequest, WebSocketConfig, WebSocketEvent, WebSocketMessage, WebSocketMessageType, WebSocketPresenceStatus, WebSocketServerConfig } from '@objectstack/spec/api'; // Validate data const result = AckMessage.parse(data); @@ -163,33 +163,6 @@ const result = AckMessage.parse(data); | **details** | `any` | optional | Additional error details | ---- - -## EventFilter - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **conditions** | `{ field: string; operator: Enum<'eq' \| 'ne' \| 'gt' \| 'gte' \| 'lt' \| 'lte' \| 'in' \| 'nin' \| 'contains' \| 'startsWith' \| 'endsWith' \| 'exists' \| 'regex'>; value?: any }[]` | optional | Array of filter conditions | -| **and** | `[EventFilter](#eventfilter)[]` | optional | AND logical combination of filters | -| **or** | `[EventFilter](#eventfilter)[]` | optional | OR logical combination of filters | -| **not** | `[EventFilter](#eventfilter)` | optional | NOT logical negation of filter | - - ---- - -## EventFilterCondition - -### Properties - -| Property | Type | Required | Description | -| :--- | :--- | :--- | :--- | -| **field** | `string` | ✅ | Field path to filter on (supports dot notation, e.g., "user.email") | -| **operator** | `Enum<'eq' \| 'ne' \| 'gt' \| 'gte' \| 'lt' \| 'lte' \| 'in' \| 'nin' \| 'contains' \| 'startsWith' \| 'endsWith' \| 'exists' \| 'regex'>` | ✅ | Comparison operator | -| **value** | `any` | optional | Value to compare against (not needed for "exists" operator) | - - --- ## EventMessage @@ -222,31 +195,10 @@ const result = AckMessage.parse(data); | **subscriptionId** | `string` | ✅ | Unique subscription identifier | | **events** | `string[]` | ✅ | Event patterns to subscribe to (supports wildcards, e.g., "record.*", "user.created") | | **objects** | `string[]` | optional | Object names to filter events by (e.g., ["account", "contact"]) | -| **filters** | `{ conditions?: { field: string; operator: Enum<'eq' \| 'ne' \| 'gt' \| 'gte' \| 'lt' \| 'lte' \| 'in' \| 'nin' \| 'contains' \| 'startsWith' \| 'endsWith' \| 'exists' \| 'regex'>; value?: any }[]; and?: object[]; or?: object[]; not?: object }` | optional | Advanced filter conditions for event payloads | +| **filters** | `any` | optional | Filter conditions for event payloads (not yet enforced — the runtime filters by object name and event type only) | | **channels** | `string[]` | optional | Channel names for scoped subscriptions | ---- - -## FilterOperator - -### Allowed Values - -* `eq` -* `ne` -* `gt` -* `gte` -* `lt` -* `lte` -* `in` -* `nin` -* `contains` -* `startsWith` -* `endsWith` -* `exists` -* `regex` - - --- ## PingMessage @@ -361,7 +313,7 @@ const result = AckMessage.parse(data); | **messageId** | `string` | ✅ | Unique message identifier | | **type** | `'subscribe'` | ✅ | | | **timestamp** | `string` | ✅ | ISO 8601 datetime when message was sent | -| **subscription** | `{ subscriptionId: string; events: string[]; objects?: string[]; filters?: { conditions?: { field: string; operator: Enum<'eq' \| 'ne' \| 'gt' \| 'gte' \| 'lt' \| 'lte' \| 'in' \| 'nin' \| 'contains' \| 'startsWith' \| 'endsWith' \| 'exists' \| 'regex'>; value?: any }[]; and?: object[]; or?: object[]; not?: object }; … }` | ✅ | Subscription configuration | +| **subscription** | `{ subscriptionId: string; events: string[]; objects?: string[]; filters?: any; … }` | ✅ | Subscription configuration | --- @@ -440,7 +392,7 @@ This schema accepts one of the following structures: | **messageId** | `string` | ✅ | Unique message identifier | | **type** | `'subscribe'` | ✅ | | | **timestamp** | `string` | ✅ | ISO 8601 datetime when message was sent | -| **subscription** | `{ subscriptionId: string; events: string[]; objects?: string[]; filters?: { conditions?: { field: string; operator: Enum<'eq' \| 'ne' \| 'gt' \| 'gte' \| 'lt' \| 'lte' \| 'in' \| 'nin' \| 'contains' \| 'startsWith' \| 'endsWith' \| 'exists' \| 'regex'>; value?: any }[]; and?: object[]; or?: object[]; not?: object }; … }` | ✅ | Subscription configuration | +| **subscription** | `{ subscriptionId: string; events: string[]; objects?: string[]; filters?: any; … }` | ✅ | Subscription configuration | --- diff --git a/content/docs/references/shared/enums.mdx b/content/docs/references/shared/enums.mdx index 596bc11adf..146295df72 100644 --- a/content/docs/references/shared/enums.mdx +++ b/content/docs/references/shared/enums.mdx @@ -5,7 +5,7 @@ description: Enums protocol schemas {/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */} -Aggregation functions used across query, data-engine, analytics, field +Sort direction used across query, data-engine, analytics **Source:** `packages/spec/src/shared/enums.zod.ts` @@ -14,33 +14,13 @@ Aggregation functions used across query, data-engine, analytics, field ## TypeScript Usage ```typescript -import { AggregationFunctionEnum, CacheStrategyEnum, IsolationLevelEnum, MutationEventEnum, SortDirectionEnum, SortItem } from '@objectstack/spec/shared'; -import type { AggregationFunctionEnum, CacheStrategyEnum, IsolationLevelEnum, MutationEventEnum, SortDirectionEnum, SortItem } from '@objectstack/spec/shared'; +import { CacheStrategyEnum, IsolationLevelEnum, MutationEventEnum, SortDirectionEnum, SortItem } from '@objectstack/spec/shared'; +import type { CacheStrategyEnum, IsolationLevelEnum, MutationEventEnum, SortDirectionEnum, SortItem } from '@objectstack/spec/shared'; // Validate data -const result = AggregationFunctionEnum.parse(data); +const result = CacheStrategyEnum.parse(data); ``` ---- - -## AggregationFunctionEnum - -Standard aggregation functions - -### Allowed Values - -* `count` -* `sum` -* `avg` -* `min` -* `max` -* `count_distinct` -* `percentile` -* `median` -* `stddev` -* `variance` - - --- ## CacheStrategyEnum diff --git a/packages/spec/api-surface.json b/packages/spec/api-surface.json index c1841144d3..fbd5414b60 100644 --- a/packages/spec/api-surface.json +++ b/packages/spec/api-surface.json @@ -2494,9 +2494,6 @@ "ErrorMessageSchema (const)", "ErrorResponse (type)", "ErrorResponseSchema (const)", - "EventFilter (type)", - "EventFilterCondition (type)", - "EventFilterSchema (const)", "EventMessage (type)", "EventMessageSchema (const)", "EventPattern (type)", @@ -2525,7 +2522,6 @@ "FileTypeValidationSchema (const)", "FileUploadResponse (type)", "FileUploadResponseSchema (const)", - "FilterOperator (type)", "FindDataRequest (type)", "FindDataRequestSchema (const)", "FindDataResponse (type)", @@ -2793,8 +2789,6 @@ "ODataErrorSchema (const)", "ODataFilterFunction (type)", "ODataFilterFunctionSchema (const)", - "ODataFilterOperator (type)", - "ODataFilterOperatorSchema (const)", "ODataMetadata (type)", "ODataMetadataSchema (const)", "ODataQuery (type)", @@ -4392,8 +4386,6 @@ "positionForm (const)" ], "./shared": [ - "AggregationFunction (type)", - "AggregationFunctionEnum (const)", "AppName (type)", "AppNameSchema (const)", "ApplyProtectionContext (interface)", diff --git a/packages/spec/authorable-surface.json b/packages/spec/authorable-surface.json index 0caa8cc0d5..305de41637 100644 --- a/packages/spec/authorable-surface.json +++ b/packages/spec/authorable-surface.json @@ -999,13 +999,6 @@ "api/ErrorResponse:error", "api/ErrorResponse:meta", "api/ErrorResponse:success", - "api/EventFilter:and", - "api/EventFilter:conditions", - "api/EventFilter:not", - "api/EventFilter:or", - "api/EventFilterCondition:field", - "api/EventFilterCondition:operator", - "api/EventFilterCondition:value", "api/EventMessage:eventName", "api/EventMessage:messageId", "api/EventMessage:object", diff --git a/packages/spec/json-schema.manifest.json b/packages/spec/json-schema.manifest.json index 1faebb7fad..7a55adc4f4 100644 --- a/packages/spec/json-schema.manifest.json +++ b/packages/spec/json-schema.manifest.json @@ -208,8 +208,6 @@ "api/ErrorHandlingConfig", "api/ErrorMessage", "api/ErrorResponse", - "api/EventFilter", - "api/EventFilterCondition", "api/EventMessage", "api/EventPattern", "api/EventSubscription", @@ -225,7 +223,6 @@ "api/FileDownloadUrlResponse", "api/FileTypeValidation", "api/FileUploadResponse", - "api/FilterOperator", "api/FindDataRequest", "api/FindDataResponse", "api/FlowSummary", @@ -361,7 +358,6 @@ "api/ODataConfig", "api/ODataError", "api/ODataFilterFunction", - "api/ODataFilterOperator", "api/ODataMetadata", "api/ODataQuery", "api/ODataQueryAdapter", @@ -1199,7 +1195,6 @@ "security/SharingRule", "security/SharingRuleType", "security/TenancyPosture", - "shared/AggregationFunctionEnum", "shared/AppName", "shared/BaseMetadataRecord", "shared/CacheStrategyEnum", diff --git a/packages/spec/src/api/websocket.zod.ts b/packages/spec/src/api/websocket.zod.ts index a79e887694..eeda723da7 100644 --- a/packages/spec/src/api/websocket.zod.ts +++ b/packages/spec/src/api/websocket.zod.ts @@ -65,10 +65,16 @@ export type WebSocketMessageType = z.infer; // (`api/realtime.zod.ts`). // // So this was a *second* spelling of event filtering, disagreeing with both the -// live one and with `VALID_AST_OPERATORS`, that advertised a capability no code -// provided — a subscriber setting `filters` would have received every event. -// Removed rather than wired up: the surface that matters is the realtime -// contract, and it should grow one filter vocabulary, not inherit an orphan's. +// live one and with `VALID_AST_OPERATORS`, and it described a capability no code +// provided: a subscriber setting `filters` received every event regardless. +// +// The `filters` key itself stays — retiring an object key needs a tombstone and +// a conversion (ADR-0104), and there is no author to migrate for a shape nothing +// validated. It now carries the same `z.unknown()` type and NOT-YET-ENFORCED +// marker as `SubscriptionEventSchema.filters`, so the two subscription surfaces +// describe event filtering identically and neither implies enforcement that does +// not exist. Whichever grows real filtering should lower onto `AST_OPERATOR_MAP` +// (`data/filter.zod.ts`) rather than reintroduce a vocabulary of its own. // objectui#2945. /** @@ -93,6 +99,16 @@ export const EventSubscriptionSchema = lazySchema(() => z.object({ subscriptionId: z.string().uuid().describe('Unique subscription identifier'), events: z.array(EventPatternSchema).describe('Event patterns to subscribe to (supports wildcards, e.g., "record.*", "user.created")'), objects: z.array(z.string()).optional().describe('Object names to filter events by (e.g., ["account", "contact"])'), + /** + * ⚠️ NOT YET ENFORCED — no runtime evaluates a payload filter. + * `matchesSubscription` matches on object name and event type only + * (`contracts/realtime-service.ts`), so a subscription carrying `filters` + * receives every event its patterns match. Deliberately `unknown` for the + * same reason as `SubscriptionEventSchema.filters` (`api/realtime.zod.ts`): + * validating a shape nothing reads would imply an enforcement that does not + * exist. objectui#2945. + */ + filters: z.unknown().optional().describe('Filter conditions for event payloads (not yet enforced — the runtime filters by object name and event type only)'), channels: z.array(z.string()).optional().describe('Channel names for scoped subscriptions'), })); From ee5abc534cd855dba63f2068ca6062d58584fd29 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:23:52 +0800 Subject: [PATCH 3/3] fix(skills): regenerate the objectstack-ui reference index Fourth generated artefact tracking the spec surface: the skill's reference index lists each spec module's exports, and shared/enums.zod.ts no longer leads with AggregationFunctionEnum. Co-Authored-By: Claude Opus 5 --- skills/objectstack-ui/references/_index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skills/objectstack-ui/references/_index.md b/skills/objectstack-ui/references/_index.md index 0a1e014beb..6d6fbd9d3a 100644 --- a/skills/objectstack-ui/references/_index.md +++ b/skills/objectstack-ui/references/_index.md @@ -29,7 +29,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/data/hook-body.zod.ts` — Capability tokens a script body may request. - `node_modules/@objectstack/spec/src/data/query.zod.ts` — Sort Node - `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010) -- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: AggregationFunctionEnum, SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum +- `node_modules/@objectstack/spec/src/shared/enums.zod.ts` — Exports: SortDirectionEnum, SortItemSchema, MutationEventEnum, IsolationLevelEnum, CacheStrategyEnum - `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol - `node_modules/@objectstack/spec/src/shared/http.zod.ts` — Shared HTTP Schemas - `node_modules/@objectstack/spec/src/shared/identifiers.zod.ts` — System Identifier Schema