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
56 changes: 56 additions & 0 deletions .changeset/http-method-defkey-collision.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
---
"@objectstack/spec": major
---

BREAKING(spec): `@objectstack/spec/shared` 与 `/ui` 改名 `HttpMethodSchema` → `HttpMethodSubsetSchema`、`HttpMethodType` → `HttpMethodSubset`;裸名 `HttpMethod(.json)` 现在全包唯一地指 7 值的路由契约,`HEAD`/`OPTIONS` 回到发布出去的 JSON Schema 与参考页 (#5832)

`packages/spec/src/shared/http.zod.ts` 在同一个文件里声明了两个**内容不同**的枚举,
而它们经 `schemaNameFromExportKey()` 剥掉 `Schema` 后缀后同名:

| 导出名 | 值域 | 剥后的发布名 |
|:---|:---|:---|
| `HttpMethod` | **7 值**(含 `HEAD`/`OPTIONS`) | `HttpMethod` |
| `HttpMethodSchema` | 5 值(view 数据源子集) | `HttpMethod` |

`build-schemas.ts` 对 `generatedSchemas.set(defKey, …)` 是无条件覆盖,后写覆盖前写,
5 值那份按导出枚举顺序排在后面。实测结果:`json-schema/shared/HttpMethod.json`、
bundled `objectstack.json` 的 `$defs['shared/HttpMethod']`、以及
`content/docs/references/shared/http#httpmethod` **只描述 5 值那份** —— 而 7 值那份才是
`api/discovery`、`api/endpoint`、`api/plugin-rest-api`、`api/rest-server`、`api/router`
声明 `method` 字段用的线上契约。任何按发布出去的 JSON Schema 做校验的下游(IDE 自动补全、
codegen、AI 元数据作者)拿到的都是被截断的那一份,会以为 `HEAD`/`OPTIONS` 非法。
属于 AGENTS.md「Machine-readable surfaces must not lie」。

## 发布面变化

- `shared/HttpMethod.json` 的 `enum` 从 5 值 **修正为** 7 值
(`GET`/`POST`/`PUT`/`DELETE`/`PATCH`/`HEAD`/`OPTIONS`)。这是**修复性契约变化**:
7 值那份一直是源码里 `api/*` 实际使用的那一个,只是从未被发布出去。
- 新增发布名 `shared/HttpMethodSubset`(5 值)。
- `ui/HttpMethod`(5 值)**改名**为 `ui/HttpMethodSubset`,登记在
`scripts/lib/renamed-defs.ts`。

## 迁移

```ts
// 5 值子集(view 数据源;`HttpRequestSchema.method` 校验用的就是它)
- import { HttpMethodSchema } from '@objectstack/spec/shared'; // 或 '/ui'
+ import { HttpMethodSubsetSchema } from '@objectstack/spec/shared'; // 或 '/ui'
- import type { HttpMethodType } from '@objectstack/spec/ui';
+ import type { HttpMethodSubset } from '@objectstack/spec/ui';
```

⚠️ **不要把 `HttpMethodSchema` 直接换成 `HttpMethod`。** `shared/HttpMethod` 是 7 值的那一个,
换过去会把类型悄悄放宽两个值,而 `HttpRequestSchema.method` 运行时仍只接受 5 值 ——
`method: 'HEAD'` 会通过编译、在 `.parse()` 抛错。这正是 #4691 当初拒绝合并两个名字的理由,
本次只是把当年留下的 `HttpMethodType` 这个「因为 `HttpMethod` 被占用才起的名字」换成了
说明其含义的名字,让发布名、schema const、类型别名三者按本包
`<Name>Schema` / `<Name>` 的惯例对齐(ADR-0112 D9:一个名字只指一件事;改名走 #4684
`RateLimitConfig` 的先例)。运行时值域**一字未动**。

## 守卫

`build-schemas.ts` 补上「同一个 def key 被两个**不同** schema 写第二次 = 硬报错」
(`scripts/lib/def-key-collisions.ts`),在两个 ratchet 之前跑 —— 它们都以 def key 计量,
碰撞只产生一个 key,谁也看不见。`export const X = XSchema` 这种自别名(本包 `api`/`system`/`ui`
共 14 处)不算碰撞:两次写的是同一个对象,不可能改变发布出去的内容。
4 changes: 3 additions & 1 deletion content/docs/references/api/router.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ const result = ConflictResolutionStrategy.parse(data);

## HttpMethod

HTTP method — the full routing vocabulary (`api/*` endpoints, router and REST-server routes). The narrower `HttpMethodSubset` is what view data sources may request.

### Allowed Values

* `GET`
Expand Down Expand Up @@ -72,7 +74,7 @@ const result = ConflictResolutionStrategy.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>` | ✅ | |
| **method** | `Enum<'GET' \| 'POST' \| 'PUT' \| 'DELETE' \| 'PATCH' \| 'HEAD' \| 'OPTIONS'>` | ✅ | HTTP method — the full routing vocabulary (`api/*` endpoints, router and REST-server routes). The narrower `HttpMethodSubset` is what view data sources may request. |
| **path** | `string` | ✅ | URL Path pattern |
| **category** | `Enum<'system' \| 'api' \| 'auth' \| 'static' \| 'webhook' \| 'plugin'>` | ✅ | |
| **handler** | `string` | ✅ | Unique handler identifier |
Expand Down
12 changes: 6 additions & 6 deletions content/docs/references/index.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Protocol Reference
description: Every schema published by @objectstack/spec — 1609 schemas across 14 protocol modules
description: Every schema published by @objectstack/spec — 1610 schemas across 14 protocol modules
---

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
Expand Down Expand Up @@ -29,11 +29,11 @@ counts are sums of the rows they head. Regenerate with
| [Kernel Protocol](/docs/references/kernel) | 31 | 187 | Plugin lifecycle and manifests, capabilities and security, metadata loading, service registry. |
| [Qa Protocol](/docs/references/qa) | 1 | 8 | Declarative test suites — scenarios, steps, actions and assertions. |
| [Security Protocol](/docs/references/security) | 5 | 27 | Permission sets, row-level security, sharing rules, tenancy posture. |
| [Shared Protocol](/docs/references/shared) | 8 | 31 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. |
| [Shared Protocol](/docs/references/shared) | 8 | 32 | Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums. |
| [Studio Protocol](/docs/references/studio) | 3 | 35 | Studio designer metadata — the authoring surfaces for the protocols above. |
| [System Protocol](/docs/references/system) | 37 | 295 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
| [UI Protocol](/docs/references/ui) | 17 | 155 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
| **Total** | **201** | **1609** | 14 protocol modules |
| **Total** | **201** | **1610** | 14 protocol modules |

---

Expand Down Expand Up @@ -285,7 +285,7 @@ Permission sets, row-level security, sharing rules, tenancy posture.

## Shared Protocol

**Source:** `packages/spec/src/shared/` · **Import:** `@objectstack/spec/shared` · **8 pages, 31 schemas**
**Source:** `packages/spec/src/shared/` · **Import:** `@objectstack/spec/shared` · **8 pages, 32 schemas**

Primitives used across every protocol — identifiers, HTTP, expressions, error maps, enums.

Expand All @@ -294,7 +294,7 @@ Primitives used across every protocol — identifiers, HTTP, expressions, error
| [`branded-types.zod.ts`](/docs/references/shared/branded-types) | `AppName`, `FieldName`, `FlowName`, `ObjectName`, `RoleName`, `ViewName` |
| [`enums.zod.ts`](/docs/references/shared/enums) | `IsolationLevelEnum`, `MutationEventEnum`, `SortDirectionEnum`, `SortItem` |
| [`expression.zod.ts`](/docs/references/shared/expression) | `CronExpressionInput`, `Expression`, `ExpressionDialect`, `ExpressionInput`, `ExpressionMeta`, `Predicate`, `PredicateInput`, `TemplateExpressionInput` |
| [`http.zod.ts`](/docs/references/shared/http) | `CorsConfig`, `HttpMethod`, `HttpRequest`, `RateLimitConfig`, `StaticMount` |
| [`http.zod.ts`](/docs/references/shared/http) | `CorsConfig`, `HttpMethod`, `HttpMethodSubset`, `HttpRequest`, `RateLimitConfig`, `StaticMount` |
| [`identifiers.zod.ts`](/docs/references/shared/identifiers) | `EventName`, `SnakeCaseIdentifier`, `SystemIdentifier` |
| [`mapping.zod.ts`](/docs/references/shared/mapping) | `FieldMapping`, `FieldMappingTransform` |
| [`metadata-types.zod.ts`](/docs/references/shared/metadata-types) | `BaseMetadataRecord`, `MetadataFormat` |
Expand Down Expand Up @@ -387,7 +387,7 @@ Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI lay
| [`responsive.zod.ts`](/docs/references/ui/responsive) | `BreakpointColumnMap`, `BreakpointName`, `BreakpointOrderMap`, `ResponsiveConfig`, `ResponsiveStyles`, `StyleMap` |
| [`sharing.zod.ts`](/docs/references/ui/sharing) | `SharingConfig` |
| [`theme.zod.ts`](/docs/references/ui/theme) | `BorderRadius`, `ColorPalette`, `Shadow`, `Theme`, `ThemeMode`, `Typography` |
| [`view.zod.ts`](/docs/references/ui/view) | `AddRecordConfig`, `AppearanceConfig`, `CalendarConfig`, `ColumnPrefix`, `ColumnSummary`, `ColumnSummaryConfig`, `FormButtonConfig`, `FormField`, `FormSection`, `FormView`, `GalleryConfig`, `GanttConfig`, `GanttQuickFilter`, `GroupingConfig`, `GroupingField`, `HttpMethod`, `HttpRequest`, `KanbanConfig`, `ListChartConfig`, `ListColumn`, `ListView`, `NavigationConfig`, `NavigationMode`, `ObjectListView`, `ObjectUserFilters`, `PaginationConfig`, `RowColorConfig`, `RowHeight`, `SelectionConfig`, `TimelineConfig`, `TreeConfig`, `UserActionsConfig`, `UserFilterField`, `UserFilters`, `View`, `ViewData`, `ViewFilterRule`, `ViewItem`, `ViewItemName`, `ViewItemWire`, `ViewKind`, `ViewScope`, `ViewSharing`, `ViewTab`, `VisualizationType` |
| [`view.zod.ts`](/docs/references/ui/view) | `AddRecordConfig`, `AppearanceConfig`, `CalendarConfig`, `ColumnPrefix`, `ColumnSummary`, `ColumnSummaryConfig`, `FormButtonConfig`, `FormField`, `FormSection`, `FormView`, `GalleryConfig`, `GanttConfig`, `GanttQuickFilter`, `GroupingConfig`, `GroupingField`, `HttpMethodSubset`, `HttpRequest`, `KanbanConfig`, `ListChartConfig`, `ListColumn`, `ListView`, `NavigationConfig`, `NavigationMode`, `ObjectListView`, `ObjectUserFilters`, `PaginationConfig`, `RowColorConfig`, `RowHeight`, `SelectionConfig`, `TimelineConfig`, `TreeConfig`, `UserActionsConfig`, `UserFilterField`, `UserFilters`, `View`, `ViewData`, `ViewFilterRule`, `ViewItem`, `ViewItemName`, `ViewItemWire`, `ViewKind`, `ViewScope`, `ViewSharing`, `ViewTab`, `VisualizationType` |
| [`widget.zod.ts`](/docs/references/ui/widget) | `WidgetEvent`, `WidgetLifecycle`, `WidgetManifest`, `WidgetProperty`, `WidgetSource` |

---
Expand Down
23 changes: 21 additions & 2 deletions content/docs/references/shared/http.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ These schemas ensure consistency across different parts of the stack.
## TypeScript Usage

```typescript
import { CorsConfigSchema, HttpMethodSchema, HttpRequestSchema, RateLimitConfigSchema, StaticMountSchema } from '@objectstack/spec/shared';
import type { CorsConfig, HttpMethod, HttpRequest, RateLimitConfig, StaticMount } from '@objectstack/spec/shared';
import { CorsConfigSchema, HttpMethod, HttpMethodSubsetSchema, HttpRequestSchema, RateLimitConfigSchema, StaticMountSchema } from '@objectstack/spec/shared';
import type { CorsConfig, HttpMethod, HttpMethodSubset, HttpRequest, RateLimitConfig, StaticMount } from '@objectstack/spec/shared';

// Validate data
const result = CorsConfigSchema.parse(data);
Expand All @@ -44,6 +44,25 @@ const result = CorsConfigSchema.parse(data);

## HttpMethod

HTTP method — the full routing vocabulary (`api/*` endpoints, router and REST-server routes). The narrower `HttpMethodSubset` is what view data sources may request.

### Allowed Values

* `GET`
* `POST`
* `PUT`
* `DELETE`
* `PATCH`
* `HEAD`
* `OPTIONS`


---

## HttpMethodSubset

HTTP methods a view data source may request — the subset of `HttpMethod` without `HEAD`/`OPTIONS`.

### Allowed Values

* `GET`
Expand Down
8 changes: 5 additions & 3 deletions content/docs/references/ui/view.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ Migrated to [shared/http.zod.ts](/docs/references/shared/http). Re-exported here
## TypeScript Usage

```typescript
import { AddRecordConfigSchema, AppearanceConfigSchema, CalendarConfigSchema, ColumnPrefixSchema, ColumnSummarySchema, ColumnSummaryConfigSchema, FormButtonConfigSchema, FormFieldSchema, FormSectionSchema, FormViewSchema, GalleryConfigSchema, GanttConfigSchema, GanttQuickFilterSchema, GroupingConfigSchema, GroupingFieldSchema, HttpMethodSchema, HttpRequestSchema, KanbanConfigSchema, ListChartConfigSchema, ListColumnSchema, ListViewSchema, NavigationConfigSchema, NavigationModeSchema, ObjectListViewSchema, ObjectUserFiltersSchema, PaginationConfigSchema, RowColorConfigSchema, RowHeightSchema, SelectionConfigSchema, TimelineConfigSchema, TreeConfigSchema, UserActionsConfigSchema, UserFilterFieldSchema, UserFiltersSchema, ViewSchema, ViewDataSchema, ViewFilterRuleSchema, ViewItemSchema, ViewItemNameSchema, ViewItemWireSchema, ViewKindSchema, ViewScopeSchema, ViewSharingSchema, ViewTabSchema, VisualizationTypeSchema } from '@objectstack/spec/ui';
import type { AddRecordConfig, AppearanceConfig, ColumnPrefix, ColumnSummary, ColumnSummaryConfig, FormButtonConfig, FormField, FormSection, FormView, GalleryConfig, GroupingConfig, HttpRequest, ListChartConfig, ListColumn, ListView, NavigationConfig, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, UserActionsConfig, UserFilterField, UserFilters, View, ViewData, ViewFilterRule, ViewItem, ViewItemWire, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui';
import { AddRecordConfigSchema, AppearanceConfigSchema, CalendarConfigSchema, ColumnPrefixSchema, ColumnSummarySchema, ColumnSummaryConfigSchema, FormButtonConfigSchema, FormFieldSchema, FormSectionSchema, FormViewSchema, GalleryConfigSchema, GanttConfigSchema, GanttQuickFilterSchema, GroupingConfigSchema, GroupingFieldSchema, HttpMethodSubsetSchema, HttpRequestSchema, KanbanConfigSchema, ListChartConfigSchema, ListColumnSchema, ListViewSchema, NavigationConfigSchema, NavigationModeSchema, ObjectListViewSchema, ObjectUserFiltersSchema, PaginationConfigSchema, RowColorConfigSchema, RowHeightSchema, SelectionConfigSchema, TimelineConfigSchema, TreeConfigSchema, UserActionsConfigSchema, UserFilterFieldSchema, UserFiltersSchema, ViewSchema, ViewDataSchema, ViewFilterRuleSchema, ViewItemSchema, ViewItemNameSchema, ViewItemWireSchema, ViewKindSchema, ViewScopeSchema, ViewSharingSchema, ViewTabSchema, VisualizationTypeSchema } from '@objectstack/spec/ui';
import type { AddRecordConfig, AppearanceConfig, ColumnPrefix, ColumnSummary, ColumnSummaryConfig, FormButtonConfig, FormField, FormSection, FormView, GalleryConfig, GroupingConfig, HttpMethodSubset, HttpRequest, ListChartConfig, ListColumn, ListView, NavigationConfig, PaginationConfig, RowColorConfig, RowHeight, SelectionConfig, TimelineConfig, UserActionsConfig, UserFilterField, UserFilters, View, ViewData, ViewFilterRule, ViewItem, ViewItemWire, ViewKind, ViewScope, ViewSharing, ViewTab, VisualizationType } from '@objectstack/spec/ui';

// Validate data
const result = AddRecordConfigSchema.parse(data);
Expand Down Expand Up @@ -307,7 +307,9 @@ Record grouping configuration

---

## HttpMethod
## HttpMethodSubset

HTTP methods a view data source may request — the subset of `HttpMethod` without `HEAD`/`OPTIONS`.

### Allowed Values

Expand Down
8 changes: 4 additions & 4 deletions packages/spec/api-surface.json
Original file line number Diff line number Diff line change
Expand Up @@ -3314,8 +3314,8 @@
"GroupingConfig (type)",
"GroupingConfigSchema (const)",
"GroupingFieldSchema (const)",
"HttpMethodSchema (const)",
"HttpMethodType (type)",
"HttpMethodSubset (type)",
"HttpMethodSubsetSchema (const)",
"HttpRequest (type)",
"HttpRequestSchema (const)",
"I18nLabel (type)",
Expand Down Expand Up @@ -4346,8 +4346,8 @@
"FlowName (type)",
"FlowNameSchema (const)",
"HttpMethod (type)",
"HttpMethodSchema (const)",
"HttpMethodType (type)",
"HttpMethodSubset (type)",
"HttpMethodSubsetSchema (const)",
"HttpRequest (type)",
"HttpRequestSchema (const)",
"IsolationLevel (type)",
Expand Down
1 change: 0 additions & 1 deletion packages/spec/docs-import-surface.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@
"ui/GanttConfig \u2014 no type export",
"ui/GanttQuickFilter \u2014 no type export",
"ui/GroupingField \u2014 no type export",
"ui/HttpMethod \u2014 no type export",
"ui/KanbanConfig \u2014 no type export",
"ui/NavigationMode \u2014 no type export",
"ui/ObjectListView \u2014 no type export",
Expand Down
3 changes: 2 additions & 1 deletion packages/spec/json-schema.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1108,6 +1108,7 @@
"shared/FieldName",
"shared/FlowName",
"shared/HttpMethod",
"shared/HttpMethodSubset",
"shared/HttpRequest",
"shared/IsolationLevelEnum",
"shared/MetadataFormat",
Expand Down Expand Up @@ -1526,7 +1527,7 @@
"ui/GroupNavItem",
"ui/GroupingConfig",
"ui/GroupingField",
"ui/HttpMethod",
"ui/HttpMethodSubset",
"ui/HttpRequest",
"ui/I18nLabel",
"ui/I18nObject",
Expand Down
27 changes: 27 additions & 0 deletions packages/spec/scripts/build-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ import path from 'path';
import { spawnSync } from 'child_process';
import { z } from 'zod';
import { schemaNameFromExportKey } from './lib/schema-name';
import {
findDefKeyCollisions,
formatDefKeyCollisions,
type EmittedDef,
} from './lib/def-key-collisions';
import { RENAMED_DEFS, carryAuthorableKey, checkRenameTable } from './lib/renamed-defs';
import { CONVERSIONS_BY_MAJOR } from '../src/conversions/registry';
import { MIGRATIONS_BY_MAJOR, RETIRED_KEYS_BY_MAJOR } from '../src/migrations/registry';
Expand Down Expand Up @@ -296,6 +301,12 @@ const generatedSchemas = new Map<string, Record<string, unknown>>();
// roots instead of approximating reachability from names or imports.
const zodByDefKey = new Map<string, z.ZodType>();

// Every export this run published, in encounter order, so the def-key collision
// guard below can see the writes `generatedSchemas` collapses. That map is
// keyed by def key and `set()` is unconditional, so by the time a duplicate is
// in it the loser is already gone — the record has to be kept alongside (#5832).
const emittedDefs: EmittedDef[] = [];

// Error messages for schema types that inherently cannot be represented in JSON Schema.
// These are expected warnings, not build-breaking errors.
const KNOWN_UNSUPPORTED_PATTERNS = [
Expand Down Expand Up @@ -373,6 +384,7 @@ for (const [namespaceName, namespaceExports] of Object.entries(Protocol)) {
writeFileWithRetry(filePath, JSON.stringify(jsonSchema, null, 2));
generatedSchemas.set(`${categorySlug}/${schemaName}`, jsonSchema);
zodByDefKey.set(`${categorySlug}/${schemaName}`, value);
emittedDefs.push({ category: categorySlug, exportKey: key, schemaName, schema: value });
console.log(` ✓ ${namespaceName.toLowerCase()}/${fileName}${io === 'input' ? ' (input shape)' : ''}`);
count++;
if (io === 'input') inputModeCount++;
Expand Down Expand Up @@ -407,6 +419,21 @@ if (errorCount > 0) {
process.exit(1);
}

// ─── Guard: one def key, one schema (#5832) ──────────────────────────
// `generatedSchemas.set()` above is an unconditional overwrite, so two exports
// of one namespace that strip to the same schema name publish ONE file and the
// loser vanishes without a word — which is how `shared/HttpMethod` shipped the
// five-value view subset while `api/*` routes were declared with the seven-value
// enum of the same name. Runs BEFORE both ratchets: neither can see this (they
// measure def keys, and a collision produces exactly one), and neither should
// adjudicate a build whose output already depends on export iteration order.
// See lib/def-key-collisions.ts for why a self-alias is exempt.
const defKeyCollisions = findDefKeyCollisions(emittedDefs);
if (defKeyCollisions.length > 0) {
console.error(`\n❌ ${formatDefKeyCollisions(defKeyCollisions)}`);
process.exit(1);
}

// ─── Ratchet: a published schema must never silently disappear ────────
// json-schema/ is a public contract surface (IDE validation, gen:docs input,
// $id URLs under schema.objectstack.io). The manifest is the committed record
Expand Down
Loading
Loading