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
80 changes: 80 additions & 0 deletions .changeset/dual-source-contracts-convergence.md
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
---
"@objectstack/spec": major
"@objectstack/service-analytics": major
"@objectstack/metadata": patch
---

feat(spec)!: converge the 11 contracts-vs-domain dual-source type names (#4538)

`packages/spec/src/contracts/` hand-wrote parameter/result interfaces whose
names collided with same-named zod-derived types in the domains — the #4411
trap, tracked as 11 rows of `dual-source-exports.baseline.json`. Each name was
judged individually against a three-repo import-level scan (framework, cloud,
objectui): which declaration actually flows at runtime decides the direction.
All 11 rows are deleted from the baseline; no name below is exported twice
anymore.

**Converged — `./contracts` now re-exports the domain zod type (same
declaration on both entries, imports keep compiling from either):**

- `NotificationChannel` → `system/notification.zod`'s
`z.infer<NotificationChannelSchema>` (member sets were identical).
- `ValidationResult` → `kernel/plugin-validator.zod` (shapes were identical).
- `HealthStatus` → `kernel/startup-orchestrator.zod` (`details` narrows
`Record<string, any>` → `Record<string, unknown>`).
- `PluginStartupResult` → `kernel/startup-orchestrator.zod`. FROM `plugin:
Plugin` (live object) and `error?: Error` TO the serializable projection
(`plugin: { name, version? }`-passthrough, `error?: { name, message,
stack?, code? }`). Neither side had any consumer outside spec; the
zod-validatable shape wins.
- `StartupOptions` → `kernel/startup-orchestrator.zod` — the PARSED tier
(defaults applied). `IStartupOrchestrator.orchestrateStartup` now takes
`StartupOptionsInput` (the caller-authored all-optional tier, also
re-exported from `./contracts`). Fix for callers typed to the old
all-optional `StartupOptions`: rename to `StartupOptionsInput`.
- `JobExecution` → `system/job.zod`. The system schema's `duration` field is
RENAMED `durationMs` — that is what every job adapter produces and what the
`sys_job_run.duration_ms` column round-trips; the schema described records
nothing ever wrote. Fix: `duration` → `durationMs` when parsing
`JobExecutionSchema` payloads.
- `AnalyticsQuery` → `data/analytics.zod`. The domain schema aligned to the
contract's semantics first: `timezone` LOST its `.default('UTC')` — absence
is meaningful (the engine resolves org timezone, #1982/#2018; the
`/analytics` entry always refused to apply that default). The schema is now
transform-free, so `AnalyticsQuery` ≡ `AnalyticsQueryInput` (both kept
exported). Fix for code that relied on `.parse()` injecting `timezone:
'UTC'`: pass the timezone explicitly or resolve it via the engine chain
(`selection.timezone ?? context.timezone ?? 'UTC'`).

**Renamed — two genuinely different concepts were sharing one name (both
flow at runtime):**

- `./contracts` `DriverCapabilities` → **`AnalyticsDriverCapabilities`**
(`{ nativeSql, objectqlAggregate, inMemory }`, the analytics strategy-chain
execution-path probe). The `DriverCapabilities` name now belongs solely to
the data domain's driver feature-flag record (`DriverCapabilitiesSchema`,
what `IDataDriver.supports` declares). Fix: importers of the trio from
`@objectstack/spec/contracts` (or `@objectstack/service-analytics`, whose
re-export is renamed in lockstep) rename the import; importers who meant
the driver flags import `DriverCapabilities` from `@objectstack/spec/data`.

**Removed — the domain-side declaration was dead (zero import-level consumers
in framework/cloud/objectui; the #4411 family's last survivors):**

- `system` `MetadataExportOptionsSchema` / `MetadataExportOptions` and
`MetadataImportOptionsSchema` / `MetadataImportOptions` (the
`output`/`source`-directory bags). The names now have ONE declaration each:
the `IMetadataService.exportMetadata` / `importMetadata` parameter
interfaces on `./contracts` (`types`/`namespaces`/`format` and
`conflictResolution`/`validate`/`dryRun`), which `MetadataManager`
implements. No tombstone/D2 conversion, deliberately — these are runtime
option-bag types, not authorable metadata (same reasoning as #4458).
`@objectstack/metadata` re-exports the two names from `./contracts` now
(it previously re-exported the dead system-side shapes its own manager
did not accept).
- `system` `JobSchedule` (the `= Schedule` back-compat alias). The name's one
declaration is the `IJobService.schedule` boundary shape on `./contracts`
(plain-string cron `expression`); the authored metadata type keeps its real
name `Schedule`. Fix: `import type { JobSchedule } from
'@objectstack/spec/system'` → `Schedule` (authoring tier) or the
`./contracts` `JobSchedule` (service boundary), whichever you meant.
2 changes: 1 addition & 1 deletion content/docs/references/api/analytics.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,7 +66,7 @@ const result = AnalyticsEndpoint.parse(data);
| **order** | `Record<string, Enum<'asc' \| 'desc'>>` | optional | |
| **limit** | `number` | optional | |
| **offset** | `number` | optional | |
| **timezone** | `string` | | |
| **timezone** | `string` | optional | |
| **query** | `any` | optional | [REMOVED] `query` was removed from AnalyticsQueryRequest in @objectstack/spec 17.0.0 (#3878). The `{ cube, query: {...}` } envelope was the dialect of the retired degraded analytics shim (#3891) — the real engine never understood it. Move the query.* fields to the body top level: `{ cube, measures, dimensions?, where?, timeDimensions?, order?, limit?, offset?, timezone? }`. |
| **format** | `any` | optional | [REMOVED] `format` was removed from AnalyticsQueryRequest in @objectstack/spec 17.0.0 (#3878). It was never implemented — every response is the JSON envelope. Delete the key; for CSV/XLSX use the export surface instead. |

Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/data/analytics.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ const result = AggregationMetricType.parse(data);
| **order** | `Record<string, Enum<'asc' \| 'desc'>>` | optional | |
| **limit** | `number` | optional | |
| **offset** | `number` | optional | |
| **timezone** | `string` | | |
| **timezone** | `string` | optional | |


---
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/system/job.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -87,7 +87,7 @@ const result = CronSchedule.parse(data);
| **completedAt** | `string` | optional | ISO 8601 datetime when execution completed |
| **status** | `Enum<'running' \| 'success' \| 'failed' \| 'timeout'>` | ✅ | Execution status |
| **error** | `string` | optional | Error message if failed |
| **duration** | `integer` | optional | Execution duration in milliseconds |
| **durationMs** | `integer` | optional | Execution duration in milliseconds |


---
Expand Down
31 changes: 2 additions & 29 deletions content/docs/references/system/metadata-persistence.mdx
Original file line numberDiff line numberDiff line change
Expand Up@@ -16,8 +16,8 @@ Defines the lifecycle and mutability of a metadata item.
## TypeScript Usage

```typescript
import { MetadataCollectionInfo, MetadataDiffResult, MetadataExportOptions, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataImportOptions, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataRecord, MetadataSaveOptions, MetadataSaveResult, MetadataScope, MetadataSource, MetadataState, MetadataStats, MetadataWatchEvent, PackagePublishResult } from '@objectstack/spec/system';
import type { MetadataCollectionInfo, MetadataDiffResult, MetadataExportOptions, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataImportOptions, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataRecord, MetadataSaveOptions, MetadataSaveResult, MetadataScope, MetadataSource, MetadataState, MetadataStats, MetadataWatchEvent, PackagePublishResult } from '@objectstack/spec/system';
import { MetadataCollectionInfo, MetadataDiffResult, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataRecord, MetadataSaveOptions, MetadataSaveResult, MetadataScope, MetadataSource, MetadataState, MetadataStats, MetadataWatchEvent, PackagePublishResult } from '@objectstack/spec/system';
import type { MetadataCollectionInfo, MetadataDiffResult, MetadataHistoryQueryOptions, MetadataHistoryQueryResult, MetadataHistoryRecord, MetadataHistoryRetentionPolicy, MetadataLoadOptions, MetadataLoadResult, MetadataLoaderContract, MetadataRecord, MetadataSaveOptions, MetadataSaveResult, MetadataScope, MetadataSource, MetadataState, MetadataStats, MetadataWatchEvent, PackagePublishResult } from '@objectstack/spec/system';

// Validate data
const result = MetadataCollectionInfo.parse(data);
Expand DownExpand Up@@ -55,20 +55,6 @@ const result = MetadataCollectionInfo.parse(data);
| **summary** | `string` | optional | Human-readable summary of changes |


---

## MetadataExportOptions

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **types** | `string[]` | optional | |
| **namespaces** | `string[]` | optional | |
| **output** | `string` | ✅ | Output directory or file |
| **format** | `Enum<'yaml' \| 'json' \| 'typescript' \| 'javascript'>` | ✅ | Metadata file format |


---

## MetadataHistoryQueryOptions
Expand DownExpand Up@@ -135,19 +121,6 @@ const result = MetadataCollectionInfo.parse(data);
| **cleanupIntervalHours** | `integer` | ✅ | How often to run cleanup (in hours) |


---

## MetadataImportOptions

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **source** | `string` | ✅ | Input directory or file |
| **strategy** | `Enum<'merge' \| 'replace' \| 'skip'>` | ✅ | |
| **validate** | `boolean` | ✅ | |


---

## MetadataLoadOptions
Expand Down
11 changes: 8 additions & 3 deletions packages/metadata/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -45,8 +45,6 @@ export type {
MetadataStats,
MetadataLoadOptions,
MetadataSaveOptions,
MetadataExportOptions,
MetadataImportOptions,
MetadataLoadResult,
MetadataSaveResult,
MetadataWatchEvent,
Expand All@@ -60,11 +58,18 @@ export type {
MetadataHistoryRetentionPolicy,
} from '@objectstack/spec/system';

// Re-export IMetadataService contract
// Re-export IMetadataService contract.
// [#4538] `MetadataExportOptions` / `MetadataImportOptions` moved into this
// block: this package used to re-export the same-named system-entry bags
// (`output`/`source`-flavored, removed with #4538) while `MetadataManager`
// implements the contracts shapes — the public re-export was pointing at the
// wrong declaration.
export type {
IMetadataService,
MetadataWatchCallback,
MetadataWatchHandle,
MetadataExportOptions,
MetadataImportOptions,
MetadataTypeInfo,
MetadataImportResult,
} from '@objectstack/spec/contracts';
Expand Down
16 changes: 8 additions & 8 deletions packages/plugins/driver-memory/src/memory-analytics.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,15 +7,15 @@ import { AnalyticsQuerySchema, defineCube } from '@objectstack/spec/data';
import type { AnalyticsQuery, AnalyticsQueryInput, Cube } from '@objectstack/spec/data';

/**
* Author-tier literal → the parsed `AnalyticsQuery` the service contract takes.
* Validate a literal through the schema before handing it to the service —
* the same route a real request body takes (the REST layer validates against
* the schema and forwards).
*
* `timezone` is `.default('UTC')` on the schema, so it is optional to write and
* required on the parsed type — the two tiers are genuinely different types. A
* real query reaches `query()` through the schema (the REST layer parses the
* request body), so these tests take the same route rather than hand-writing
* the filled-in default: the parse IS the proof that the default lands. Until
* #4311 no tsc read this file, so 19 author-tier literals sat unnoticed in a
* parameter that had required `timezone` all along.
* [#4538] The two tiers collapsed: `AnalyticsQuerySchema` no longer carries
* any `.default()`/`.transform()` (`timezone` is genuinely optional — absence
* means the engine resolves org timezone, #1982/#2018), so `AnalyticsQuery`
* and `AnalyticsQueryInput` are the same shape and the parse is validation
* only. The helper stays so every test query is proven schema-valid.
*/
const asQuery = (input: AnalyticsQueryInput): AnalyticsQuery => AnalyticsQuerySchema.parse(input);

Expand Down
9 changes: 7 additions & 2 deletions packages/runtime/src/domains/analytics.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -38,8 +38,13 @@ import type { DomainHandlerDeps, DomainRoute } from '../domain-handler-registry.
* "unrecognized key" — gets a bespoke hint at the contract field `where`.
*
* Validation only — the ORIGINAL body is forwarded to the service untouched.
* (Parsing would inject the schema's `timezone: 'UTC'` default and silently
* override the engine's org-timezone resolution, #1982/#2018.)
* (Historically load-bearing: the schema carried a `timezone: 'UTC'` default
* that parsing would have injected, silently overriding the engine's
* org-timezone resolution, #1982/#2018. #4538 removed that default from
* `AnalyticsQuerySchema` itself — the schema is transform-free now, so
* validated body ≡ parsed output by construction — but forwarding the
* original body stays the rule: it keeps this entry immune to any future
* default someone adds to the schema without re-reading this file.)
*/
function assertAnalyticsQueryBody(body: unknown): void {
if (body && typeof body === 'object' && !Array.isArray(body)) {
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,7 +7,7 @@ import { AnalyticsService } from '../analytics-service.js';
import { CubeRegistry } from '../cube-registry.js';
import { NativeSQLStrategy } from '../strategies/native-sql-strategy.js';
import { ObjectQLStrategy } from '../strategies/objectql-strategy.js';
import type { DriverCapabilities } from '../strategies/types.js';
import type { AnalyticsDriverCapabilities } from '../strategies/types.js';

// ─────────────────────────────────────────────────────────────────
// Test fixtures
Expand Down
6 changes: 3 additions & 3 deletions packages/services/service-analytics/src/analytics-service.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -13,7 +13,7 @@ import type { Dataset } from '@objectstack/spec/ui';
import type { Logger } from '@objectstack/spec/contracts';
import { createLogger, bucketKeyToCalendarRange, zonedDateStartToUtcMs } from '@objectstack/core';
import { CubeRegistry } from './cube-registry.js';
import type { AnalyticsStrategy, DriverCapabilities, StrategyContext } from './strategies/types.js';
import type { AnalyticsStrategy, AnalyticsDriverCapabilities, StrategyContext } from './strategies/types.js';
import { NativeSQLStrategy } from './strategies/native-sql-strategy.js';
import { ObjectQLStrategy } from './strategies/objectql-strategy.js';
import { compileDataset, type CompiledDataset, type RelationshipResolver } from './dataset-compiler.js';
Expand DownExpand Up@@ -112,7 +112,7 @@ export interface AnalyticsServiceConfig {
* Probe driver capabilities for the object that backs a cube.
* The service calls this function to decide which strategy can handle a query.
*/
queryCapabilities?: (cubeName: string) => DriverCapabilities;
queryCapabilities?: (cubeName: string) => AnalyticsDriverCapabilities;
/**
* Execute raw SQL on the driver for a given object.
* Required for NativeSQLStrategy.
Expand DownExpand Up@@ -287,7 +287,7 @@ export interface AnalyticsServiceConfig {
/**
* Default capabilities when probing is not configured — assumes in-memory only.
*/
const DEFAULT_CAPABILITIES: DriverCapabilities = {
const DEFAULT_CAPABILITIES: AnalyticsDriverCapabilities = {
nativeSql: false,
objectqlAggregate: false,
inMemory: true,
Expand Down
2 changes: 1 addition & 1 deletion packages/services/service-analytics/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,6 @@ export { compileScopedFilterToSql } from './read-scope-sql.js';
// Strategies
export { NativeSQLStrategy } from './strategies/native-sql-strategy.js';
export { ObjectQLStrategy } from './strategies/objectql-strategy.js';
export type { AnalyticsStrategy, StrategyContext, DriverCapabilities } from './strategies/types.js';
export type { AnalyticsStrategy, StrategyContext, AnalyticsDriverCapabilities } from './strategies/types.js';

// Note: InMemoryStrategy is exported from @objectstack/driver-memory
4 changes: 2 additions & 2 deletions packages/services/service-analytics/src/plugin.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,7 +6,7 @@ import type { ExecutionContext } from '@objectstack/spec/kernel';
import type { IAnalyticsService, IDataDriver } from '@objectstack/spec/contracts';
import { AnalyticsService } from './analytics-service.js';
import type { AnalyticsServiceConfig } from './analytics-service.js';
import type { DriverCapabilities } from './strategies/types.js';
import type { AnalyticsDriverCapabilities } from './strategies/types.js';
import { pickDisplayField, type DimensionLabelDeps } from './dimension-labels.js';

/**
Expand DownExpand Up@@ -77,7 +77,7 @@ export interface AnalyticsServicePluginOptions {
* Probe driver capabilities for a given cube.
* When omitted, defaults to in-memory only.
*/
queryCapabilities?: (cubeName: string) => DriverCapabilities;
queryCapabilities?: (cubeName: string) => AnalyticsDriverCapabilities;
/**
* Execute raw SQL on a driver. Enables NativeSQLStrategy.
*/
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,9 +3,14 @@
/**
* Strategy pattern types — re-exported from @objectstack/spec/contracts
* for convenience. The canonical definitions live in the spec package.
*
* [#4538] `DriverCapabilities` → `AnalyticsDriverCapabilities`: the old name
* belonged to the data domain's driver feature-flag record
* (`DriverCapabilitiesSchema` — every `IDataDriver.supports`); the analytics
* execution-path trio was renamed with its spec declaration.
*/
export type {
AnalyticsStrategy,
StrategyContext,
DriverCapabilities,
AnalyticsDriverCapabilities,
} from '@objectstack/spec/contracts';
Loading
Loading