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
48 changes: 48 additions & 0 deletions .changeset/metadata-item-name-grammar-enforced.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
---
"@objectstack/spec": minor
"@objectstack/metadata-protocol": minor
---

feat(spec,metadata-protocol): declare the metadata item-name grammar and refuse it loudly at the publish door (#12194, #12176 stage 1)

**BREAKING** accept-set narrowing at the metadata write door, shipped as
`minor` under the repo's launch-window convention for breaking changes.

Metadata item names — the `name` half of the `type`/`name` pair that keys
`sys_metadata` and the `/api/v1/meta` URL space — were entirely unconstrained:
the empty string, `//`, `'Views/All Leads'` and slash-compound spellings
(`views/all_leads`) were all accepted and stored, and a slash in the name
bypassed the unrecognised-metadata-type refusal entirely (`type=fieldz
name='a/b'` was accepted and stored while `type=fieldz name='a'` was 400).
Maintainer ruling 2026-08-25 (#12176): item names must not contain `/`.

The grammar is now **declared in spec** (`MetadataItemNameSchema` /
`METADATA_ITEM_NAME_PATTERN`, `@objectstack/spec/shared`): lowercase
snake_case segments, optionally dot-qualified — the family
`^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)*$` — sourced from the existing
`ViewItemNameSchema` dotted declaration (same segment source, one grammar; the
view-item identity keeps requiring its dot). And it is **enforced at the
publish door** (`saveMetaItem` and `publishMetaItem` in
`@objectstack/metadata-protocol`): an off-grammar name is refused
`400 INVALID_REQUEST` with the grammar and the dotted prescription in the
message, and nothing is persisted. The slash bypass of
`refuseUnmintableMetaType` closes as a consequence.

**What an author writes instead.** A flat snake_case name (`crm_lead`) and a
dotted qualified name (`crm_lead.pipeline`) both work exactly as before. A
name that spelled a sub-resource with a slash (`views/all_leads`) is
re-authored with a dot qualifier (`crm_lead.pipeline` — the qualified identity
whose prefix recovers the owner) or flattened with an underscore
(`views_all_leads`); containment is expressed by structure, never by a
separator inside the identity string. A translation item conventionally named
after its locale is named in snake_case (`zh_cn`) with the BCP-47 spelling in
its required `locale` field (`"zh-CN"`), which has been the item's real
identity key all along.

Reads and `deleteMetaItem` deliberately stay open, so any pre-grammar residue
row remains listable and clearable. The in-repo stored corpus was measured at
**zero** slash-bearing item names (#12176 census, re-asserted at land time);
out-of-repo stored slash rows, if any exist, are reported by their deployment's
migrate run rather than rewritten silently.

<!-- adr-0087: registered metadata-item-name-grammar-enforced -->
4 changes: 2 additions & 2 deletions content/docs/references/api/protocol.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -1271,7 +1271,7 @@ List packages response
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **type** | `string` | ✅ | Metadata type name |
| **name** | `string` | ✅ | Item name |
| **name** | `string` | ✅ | Item name — lowercase snake_case segments, optionally dot-qualified (`crm_lead`, `crm_lead.pipeline`). The promotion door enforces the same grammar as `saveMetaItem` (#12194). |
| **organizationId** | `string` | optional | Organization (tenant) scope for the promotion. The implementation resolves the draft through the org partition (ADR-0005, #8805), so a draft authored org-scoped must be published under the same scope or the lookup answers 404 `[no_draft]`. Absent = environment-wide. |
| **actor** | `string` | optional | Identity recorded on the `op='publish'` history event. On the REST door this is the request's authenticated identity (one producer, #7749) — never a caller-supplied header. |
| **message** | `string` | optional | Optional human-readable note recorded with the publish history event. |
Expand DownExpand Up@@ -1477,7 +1477,7 @@ List packages response
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **type** | `string` | ✅ | Metadata type name |
| **name** | `string` | ✅ | Item name |
| **name** | `string` | ✅ | Item name — lowercase snake_case segments, optionally dot-qualified (`crm_lead`, `crm_lead.pipeline`). Slash-compound names are refused at the publish door (#12176). |
| **item** | `any` | ✅ | Metadata item definition |


Expand Down
10 changes: 5 additions & 5 deletions content/docs/references/index.mdx
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
---
title: Protocol Reference
description: Every schema published by @objectstack/spec — 1588 schemas across 14 protocol modules
description: Every schema published by @objectstack/spec — 1589 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 DownExpand Up@@ -29,11 +29,11 @@ counts are sums of the rows they head. Regenerate with
| [Kernel Protocol](/docs/references/kernel) | 31 | 176 | 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) | 36 | 288 | The runtime environment — logging, jobs, cache, metrics, notifications, i18n and compliance. |
| [UI Protocol](/docs/references/ui) | 16 | 152 | Apps, pages, views, dashboards, reports, actions and themes — the ObjectUI layer. |
| **Total** | **199** | **1588** | 14 protocol modules |
| **Total** | **199** | **1589** | 14 protocol modules |

---

Expand DownExpand 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@@ -295,7 +295,7 @@ Primitives used across every protocol — identifiers, HTTP, expressions, error
| [`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`, `HttpMethodSubset`, `HttpRequest`, `RateLimitConfig`, `StaticMount` |
| [`identifiers.zod.ts`](/docs/references/shared/identifiers) | `EventName`, `SnakeCaseIdentifier`, `SystemIdentifier` |
| [`identifiers.zod.ts`](/docs/references/shared/identifiers) | `EventName`, `MetadataItemName`, `SnakeCaseIdentifier`, `SystemIdentifier` |
| [`mapping.zod.ts`](/docs/references/shared/mapping) | `FieldMapping` |
| [`metadata-types.zod.ts`](/docs/references/shared/metadata-types) | `BaseMetadataRecord`, `MetadataFormat` |
| [`protection.zod.ts`](/docs/references/shared/protection) | `Protection` |
Expand Down
13 changes: 11 additions & 2 deletions content/docs/references/shared/identifiers.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -54,8 +54,8 @@ Enforces lowercase with underscores or dots to ensure:
## TypeScript Usage

```typescript
import { EventNameSchema, SnakeCaseIdentifierSchema, SystemIdentifierSchema } from '@objectstack/spec/shared';
import type { EventName, SnakeCaseIdentifier, SystemIdentifier } from '@objectstack/spec/shared';
import { EventNameSchema, MetadataItemNameSchema, SnakeCaseIdentifierSchema, SystemIdentifierSchema } from '@objectstack/spec/shared';
import type { EventName, MetadataItemName, SnakeCaseIdentifier, SystemIdentifier } from '@objectstack/spec/shared';

// Validate data
const result = EventNameSchema.parse(data);
Expand All@@ -70,6 +70,15 @@ Event name (lowercase with dot notation for namespacing)
**Type:** `string`


---

## MetadataItemName

Metadata item name (lowercase snake_case segments, optionally dot-qualified)

**Type:** `string`


---

## SnakeCaseIdentifier
Expand Down
Loading
Loading