Skip to content

Commit 490ab51

Browse files
committed
Merge origin/main into claude/issue-6145-module-header-doc-blocks
2 parents c9d64fc + 53068c1 commit 490ab51

282 files changed

Lines changed: 3340 additions & 2238 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
---
2+
"@objectstack/spec": major
3+
---
4+
5+
feat(spec)!: the bare type name is now the AUTHOR state — 1384 aliases flipped, 102 `XInput` synonyms retired (ADR-0122 phase 2, #6083)
6+
7+
A Zod schema denotes two types: `z.input` (what an author writes — defaulted keys
8+
optional, pre-transform) and `z.infer` (what `.parse()` returns). Until protocol 17 the
9+
bare name `X` meant the second one in 1384 places and the first one in 86, with nothing
10+
recorded about which was which.
11+
12+
**[ADR-0122](https://github.com/objectstack-ai/objectstack/blob/main/docs/adr/0122-schema-type-alias-naming-convention.md)
13+
settles it: the bare name `X` is the AUTHOR state, `XParsed` is the PARSED state.**
14+
Phase 1 (16.x, additive) gave every schema with two distinct shapes its `XParsed` name so
15+
nothing would be stranded. **This release is phase 2: it flips the bare names.** It is the
16+
breaking half, and it is the reason `@objectstack/spec` goes to 17.0.0.
17+
18+
```ts
19+
// before (16.x) // after (17.0.0)
20+
export type Connector = z.infer<…>; export type Connector = z.input<…>;
21+
export type ConnectorParsed = z.infer<…>; export type ConnectorParsed = z.infer<…>;
22+
export type ConnectorInput = z.input<…>; // ConnectorInput: RETIRED
23+
```
24+
25+
## FROM → TO
26+
27+
There are exactly two migrations, and each has a mechanical test.
28+
29+
### 1. `XInput``X` (102 names removed)
30+
31+
The flip made `XInput` a character-for-character synonym of the bare name, and ADR-0122
32+
D3 forbids a permanent synonym. Every retired name has the same fix: **drop the `Input`
33+
suffix.**
34+
35+
```diff
36+
- import type { ConnectorInput } from '@objectstack/spec/integration';
37+
- const c: ConnectorInput = { name: 'acme', label: 'Acme', type: 'saas' };
38+
+ import type { Connector } from '@objectstack/spec/integration';
39+
+ const c: Connector = { name: 'acme', label: 'Acme', type: 'saas' };
40+
```
41+
42+
Find them: `rg '\b\w+Input\b' --type ts` over your own code, then keep only the hits that
43+
resolve to `@objectstack/spec`. Every one of them is a compile error on upgrade — there is
44+
no silent failure in this direction, because the name is gone.
45+
46+
The 102 retired names, by module:
47+
48+
| module | retired |
49+
|:---|:---|
50+
| `api/auth` | `SessionUserInput`, `LoginRequestInput` |
51+
| `api/dispatcher` | `DispatcherRouteInput`, `DispatcherConfigInput` |
52+
| `api/endpoint` | `ApiEndpointInput` |
53+
| `api/plugin-rest-api` | `RequestValidationConfigInput`, `ResponseEnvelopeConfigInput`, `ErrorHandlingConfigInput`, `OpenApiGenerationConfigInput`, `RestApiPluginConfigInput` |
54+
| `api/protocol` | `NotificationPreferencesInput`, `NotificationInput` |
55+
| `api/query-adapter` | `RestQueryAdapterInput`, `ODataQueryAdapterInput`, `QueryAdapterConfigInput` |
56+
| `api/rest-server` | `RestApiConfigInput`, `CrudEndpointsConfigInput`, `MetadataEndpointsConfigInput`, `BatchEndpointsConfigInput`, `RouteGenerationConfigInput`, `RestServerConfigInput` |
57+
| `api/versioning` | `VersioningConfigInput` |
58+
| `automation` | `FlowFunctionDeclarationInput`, `ActionDescriptorInput`, `TimeRelativeTriggerInput`, `WebhookInput` |
59+
| `data/analytics` | `CubeInput`, `AnalyticsQueryInput` |
60+
| `data/datasource` | `DatasourceInput` |
61+
| `data/field` | `FieldParseInput` (→ `Field`), `CurrencyConfigInput` |
62+
| `data/mapping` | `MappingInput` |
63+
| `data/object` | `ObjectFieldGroupInput`, `RowCrudActionOverrideInput`, `ServiceObjectInput`, `ObjectExtensionInput` |
64+
| `data/seed`, `data/seed-loader` | `SeedInput`, `SeedLoaderConfigInput`, `SeedLoaderRequestInput` |
65+
| `identity` | `EvalUserInput`, `PositionInput` |
66+
| `integration/connector` | `ConnectorInput` |
67+
| `kernel` | `ClusterCapabilityConfigInput`, `ExecutionContextInput`, `ObjectStackManifestInput`, `PackageArtifactInput`, `PluginVendorInput`, `PluginQualityMetricsInput`, `PluginStatisticsInput`, `PluginRegistryEntryInput`, `PluginSearchFiltersInput`, `PluginInstallConfigInput`, `ServiceRegistryConfigInput`, `StartupOptionsInput` |
68+
| `security` | `ExplainRequestInput`, `AdminScopeInput`, `PermissionSetInput`, `SharingRuleInput` |
69+
| `system` | `CacheTierInput`, `CacheConfigInput`, `DistributedCacheConfigInput`, `BackupConfigInput`, `FailoverConfigInput`, `DisasterRecoveryPlanInput`, `EmailTemplateDefinitionInput`, `KeyRotationPolicyInput`, `EncryptionConfigInput`, `FieldEncryptionInput`, `EnvironmentArtifactInput`, `RouteHandlerMetadataInput`, `MiddlewareConfigInput`, `ServerCapabilitiesInput`, `JobInput`, `FeatureInput`, `PlanInput`, `SecurityContextConfigInput`, `StackServerConfigInput`, `RowLevelIsolationStrategyInput`, `SchemaLevelIsolationStrategyInput`, `DatabaseLevelIsolationStrategyInput`, `TenantSecurityPolicyInput`, `TranslationBundleInput`, `TaskRetryPolicyInput`, `TaskInput`, `QueueConfigInput`, `BatchTaskInput`, `BatchProgressInput`, `WorkerConfigInput` |
70+
| `ui` | `ActionInput`, `InlineActionInput`, `NavigationContributionInput`, `AppInput`, `DashboardInput`, `DatasetDimensionInput`, `DatasetMeasureInput`, `DatasetInput`, `PageInput`, `JoinedReportBlockInput`, `ReportInput`, `ReportChartInput`, `ReportSortInput`, `ThemeInput` |
71+
72+
**Nine `*Input` names are NOT retired** and need no change: `ExpressionInput`,
73+
`CronExpressionInput`, `TemplateExpressionInput` and `PredicateInput` are the bare aliases
74+
of their own `…InputSchema`, and `FormFieldInput`, `QueryInput`, `FieldInput`,
75+
`ObjectStackDefinitionInput` and `NavigationItemInput` are composed types (recursive or
76+
`Partial`-shaped) that no bare alias denotes.
77+
78+
### 2. `X``XParsed` **only where you hold a parse result**
79+
80+
If you annotate a value you *wrote*, do nothing — the bare name is now correct, and this
81+
is the whole point of the change:
82+
83+
```ts
84+
// This did not compile in 16.x unless you knew to write `ConnectorInput`.
85+
// In 17.0.0 it is simply right, in every domain.
86+
const c: Connector = { name: 'acme_erp', label: 'Acme ERP', type: 'saas' };
87+
```
88+
89+
If you annotate a value that came *out of* `.parse()` (or out of a `defineX()` factory, or
90+
off the wire after the engine parsed it) and you read a defaulted key from it, move that
91+
annotation to `XParsed`:
92+
93+
```diff
94+
- const parsed: Connector = ConnectorSchema.parse(raw);
95+
+ const parsed: ConnectorParsed = ConnectorSchema.parse(raw);
96+
if (parsed.enabled) { … } // `enabled` is `boolean` here, `boolean | undefined` on `Connector`
97+
```
98+
99+
**The grep that finds these:** `rg 'Schema\.parse\(' -A2` and `rg ': *\w+ *= *await'` in
100+
your own code, then check each annotation. **The reliable finder is the compiler**: every
101+
site that reads a defaulted key off an author-state value is a `TS18048` /
102+
`TS2532` ("possibly undefined") or a `TS2345`. Upgrade, run `tsc`, and fix what it names. In
103+
this repo — 1127 files annotate a value with a spec type — that came to **40 files outside
104+
`packages/spec`**, and every one of them was a compile error first, never a silent change.
105+
106+
**The one case tsc cannot name for you:** a *function's declared return type*. A parse
107+
result is structurally assignable to the author state, so
108+
109+
```ts
110+
function loadConnector(): Connector { return ConnectorSchema.parse(raw); } // still compiles!
111+
```
112+
113+
keeps compiling while quietly promising callers less than it delivers. If you have
114+
factories or loaders that return a parsed value, re-declare them as `XParsed` by hand.
115+
`@objectstack/spec`'s own 24 `defineX` factories were migrated exactly this way —
116+
`defineApp(...)` now returns `AppParsed`, `defineConnector(...)` returns `ConnectorParsed`,
117+
and so on for every factory whose schema has two shapes.
118+
119+
## What did NOT change
120+
121+
- **No runtime behaviour.** Not one `.parse()` call, `.default()`, `.transform()` or schema
122+
shape moved. This release changes which type name describes which value, nothing else.
123+
- **`json-schema/` and `authorable-surface/` are byte-identical.** Those generators read
124+
runtime `z.ZodType` exports, never type aliases.
125+
- **Your metadata files.** `*.object.ts`, `*.view.ts`, connector and flow definitions
126+
authored with `defineX(...)` are untouched. Bare-literal metadata files typed with
127+
`XInput` need the suffix dropped and nothing else.
128+
129+
## Also in this release
130+
131+
- **`check:spec-parsed-alias` is inverted.** It used to require every bare `z.infer` alias
132+
to be paired or pinned; the flip empties that population, so it now refuses a bare name
133+
that reads `z.infer` (the flip, enforced), refuses an `XInput` synonym of a bare name
134+
(the retirement, enforced), and keeps the paired-or-pinned and stale-pin arms on the
135+
flipped form.
136+
- **57 previously ungoverned aliases were audited.** Inverting the gate widened it to the
137+
86 aliases that already read `z.input`, which phase 1 never examined. 22 gained an
138+
`XParsed`; 35 were proved isomorphic and pinned, adding 35 to the pin registry (716 → 751
139+
on the merged tree, after #5055's four retirements and #5775's one addition). This closes
140+
#5507's remaining scope.
141+
- **`@objectstack/spec` public surface: 106 export names removed, 24 added.** The removals
142+
are the 102 `XInput` aliases (plus re-exports); the additions are the 22 new `XParsed`
143+
names (plus re-exports). All type-only — no runtime code, no bundle-size change.
144+
145+
<!-- adr-0087: registered spec-type-alias-input-suffix-retired -->
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
"@objectstack/service-analytics": patch
3+
---
4+
5+
fix(analytics): read scope 里非布尔的 `$null` / `$exists` 比较数改为拒收,不再按真值性编成相反的谓词 (#6387)
6+
7+
**⚠️ 行为变更。** `compileScopedFilterToSql` 遇到 `$null` / `$exists` 上的非布尔比较数,从「按 JS 真值性归入两个声明答案之一、静默编出合法 SQL」改为 `READ_SCOPE_COMPILE_FAILED` / **500** 拒收。今天靠这个静默翻转在跑的 read scope,从此会响亮地失败。
8+
9+
## 实测到的毛病
10+
11+
发射器读的是 `val ? … : …` —— **真值性**,不是 `@objectstack/spec` `FieldOperatorsSchema` 声明的 `z.boolean()`。在 `5faa23ca3` 上直接调 `compileScopedFilterToSql`,alias `t`
12+
13+
| read scope | 编译结果 | |
14+
|---|---|---|
15+
| `{ owner_id: { $null: "false" } }` | `"t"."owner_id" IS NULL` | ⛔ 与作者写的意思**相反** |
16+
| `{ owner_id: { $null: "true" } }` | `"t"."owner_id" IS NULL` | |
17+
| `{ owner_id: { $null: 0 } }` | `"t"."owner_id" IS NOT NULL` | |
18+
| `{ owner_id: { $null: null } }` | `"t"."owner_id" IS NOT NULL` | |
19+
| `{ owner_id: { $null: undefined } }` | `"t"."owner_id" IS NOT NULL` | |
20+
| `{ owner_id: { $exists: "false" } }` | `"t"."owner_id" IS NOT NULL` | ⛔ 与作者写的意思**相反** |
21+
| `{ owner_id: { $exists: 0 } }` | `"t"."owner_id" IS NULL` | |
22+
| `{ owner_id: { $exists: "no" } }` | `"t"."owner_id" IS NOT NULL` | |
23+
24+
两行 ⛔ 是要害:字符串 `"false"`**真值**,于是它落在它被写下来所要表达的 `false`**对面** —— `{ $exists: "false" }` 写来表示「没有 owner 的行」,编出来是「**** owner 的行」。这与 #6125 那一格方向相反:那边是 fail-**closed**(匹配零行、只是安静),这边是**加宽** —— admit 了策略要排除的行,出现在一个自述「A read-scope predicate must never be silently dropped、fail-closed」的模块里。
25+
26+
## 修法
27+
28+
#5347`$null`)/ #5369`$exists`)在 `driver-sql` 面确立的先例,理由逐字适用:非布尔比较数**按声明拒收**,不做强转。闸落在 `compileField`,紧挨 #6125`undefined` 闸 —— 两道闸的作用域互不相交(那一道按名字跳过这两个算子),所以谁也盖不住谁的措辞。
29+
30+
两个算子**共用一条措辞**#5240「一个条件一种措辞」),只有算子名与 `path` 不同:`driver-sql` 给孪生实现两条措辞,是因为各自要指名**自己**发射器默认倒向哪边;本模块只有一条规则(真值性)同时管着两个算子,两者失败方式完全一样,所以一条措辞才是诚实的写法。测试里有一条断言把「只有这两处不同」钉死。
31+
32+
信封沿用本模块自述的那一个(`READ_SCOPE_COMPILE_FAILED` / 500),不是 #5347`INVALID_FILTER` / 400:read scope 由平台自己从 CEL 与库存 metadata 编出来,报 400 等于让调用方去修一个他既没写、也改不动的东西。继承的是**处置**(拒收),不是信封。
33+
34+
极性表**同 PR 一起改**`nullValueSatisfiesOperator``$null` / `$exists` 两臂从真值性(`Boolean(value)` / `!value`)改为恒等(`value === true` / `value === false`)。每张极性表钉的是它**自己**发射器的拼写(#5146 / #5298),只改发射器不改表,不变量会安静地断在定义处。这条差异消失后,本编译器与 `driver-sql` 的同名表第一次逐臂一致。
35+
36+
## ⚠️ 触达性:实测结论是**库存 metadata 走不通**
37+
38+
定级依据是测量,不是立单时的措辞。`{ $null: <非布尔> }` **无法**从库存 metadata 走到本编译器,三道闸各自独立关死:`RowLevelSecurityPolicySchema``using` / `check` 声明为 `z.string()`(CEL 谓词,不是 FilterCondition),存对象直接被拒;CEL 下降只在两处发射 `$null` 且比较数是**硬编码布尔**`== null``{$null: true}``!= null``{$null: false}`),`$exists` 一次都不发射;绕开 schema 塞裸对象会在 `sqlPredicateToCel` 里抛错,被 `getReadFilter` 的 catch 变成 `RLS_DENY_FILTER`。其余 read scope 生产者(Layer 0 租户过滤、`plugin-sharing``buildReadFilter`、controlled-by-parent、deny 哨兵)压根不含这两个算子。
39+
40+
**仍然开着的那条**`getReadScope``AnalyticsPluginOptions` 上有文档的公开扩展点,宿主自带的 read scope(来自 JSON 配置或没走类型检查的 JS)与本编译器之间没有任何闸 —— 本单也确认了 `plugin-security` 全路径无 `FilterConditionSchema` / `safeParse`。所以:今天不从库存 metadata 触达,但没有任何结构性的东西挡住下一个生产者。在编译器处拒收,才让「声明为布尔」等于「强制为布尔」,与谁写这条 scope 无关。
41+
42+
## ⛔ 一字未动的邻居
43+
44+
- **合法布尔**`$null: true/false``$exists: true/false` 的 SQL 逐字节不变(`IS NULL` 下降正是 RLS 用来圈无主行的写法,也是 CEL 唯一能产出的四种形状)。有自己的对照组回归 pin。
45+
- **比较数位置上的 `null`**`{ d: null }``{ $eq: null }``{ $ne: null }``$in: [null]`#6125`NULL_CONTROL` 全部保持绿。
46+
- `driver-sql` / `driver-turso`#5347 / #5369 已落地)、`packages/spec`(声明已是 `z.boolean()`)、以及本包的 `where``strategies/filter-normalizer.ts` 均未触碰。
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): the `systemFields.owner` rescue no longer tells authors that `ownership: 'org'` picks a different principal (#6365)
6+
7+
`systemFields` has never declared an `owner` key, and the field doc above the
8+
block names one — so an author (or an AI writing metadata) who follows that
9+
prose lands on the block's `guidance.owner` prescription. That prescription
10+
said:
11+
12+
> `owner_id` injection is governed by the object-level `ownership` property
13+
> (`ownership: 'none'` skips it; `'user'`/`'org'` choose the principal).
14+
15+
The second half was false. `'org'` does not choose a different principal — it
16+
injects **no** `owner_id` at all. The authority `applySystemFields` consumes,
17+
`resolveInjectedSystemColumns` (`packages/spec/src/data/injected-system-columns.ts`),
18+
admits exactly two spellings:
19+
20+
```ts
21+
const owner = ownershipEligible && (ownership === undefined || ownership === 'user');
22+
```
23+
24+
and the `ownership` property's own JSDoc, ~90 lines above the guidance, already
25+
said so correctly (`org` / `none` — no per-record owner; `owner_id` is NOT
26+
injected). The guidance was the wrong side of that contradiction.
27+
28+
Why it was worth fixing rather than leaving as prose drift: this is the text an
29+
author is handed at the exact moment they are already confused about where owner
30+
injection is configured, and it sent them to `ownership: 'org'` expecting an
31+
org-keyed owner column. Nothing rejects `ownership: 'org'`, so the mistake
32+
ships silently and every owner-keyed feature quietly does nothing —
33+
owner-scoped RLS, "My" views, owner reports, the first-admin bootstrap handoff.
34+
That is the failure mode the `guidance` machinery exists to prevent, inverted:
35+
a wrong-key rescue handing out a second wrong answer.
36+
37+
The rescue now states the injection rule as the authority implements it —
38+
`'user'` (or omitted) injects `owner_id`; `'org'` and `'none'` **both** skip it
39+
and no `owner_id` is injected at all — while keeping the two skipping values
40+
visibly distinct in intent (`'org'` for an org-wide catalog, `'none'` for a
41+
junction/link table), since that distinction is the reason the enum carries
42+
both.
43+
44+
The sibling `guidance.ownership` message is widened in the same pass. It was not
45+
wrong, only out of date: since #5677 / ADR-0117 D1 the `ownership` property
46+
governs **both** record-ownership anchors, so the message now says it decides
47+
whether `owner_id` **and** `owning_business_unit_id` are injected, rather than
48+
naming only the first.
49+
50+
Text only — no acceptance change. Every value `ObjectSchema` accepted before is
51+
accepted now, every value it rejected is still rejected, and the injection
52+
behaviour is untouched. The new pin tests assert the message's substance against
53+
`resolveInjectedSystemColumns` rather than echoing the sentence, so the
54+
prescription can only stay green while it still describes what the injection
55+
pass really does.

content/docs/getting-started/quick-reference.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,10 +298,10 @@ is gated at publish and, once it passes, serves real traffic. Full contract:
298298

299299
{/* os:check */}
300300
```typescript
301-
import type { ApiEndpointInput } from '@objectstack/spec/api';
301+
import type { ApiEndpoint } from '@objectstack/spec/api';
302302

303303
// With `manifest: { namespace: 'acme', … }` on the same stack.
304-
export const leadFeed: ApiEndpointInput = {
304+
export const leadFeed: ApiEndpoint = {
305305
name: 'acme_lead_feed',
306306
path: '/api/v1/apps/acme/leads', // /api/v1/apps/<namespace>/<subpath>
307307
method: 'GET',

0 commit comments

Comments
 (0)