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
60 changes: 60 additions & 0 deletions .changeset/data-objectstack-chatbot-list-spec-symbol-burn-down.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
---
"@object-ui/data-objectstack": major
"@object-ui/plugin-chatbot": major
"@object-ui/plugin-list": major
---

Stop declaring 12 `@object-ui/data-objectstack` / `@object-ui/plugin-chatbot` /
`@object-ui/plugin-list` symbols under names `@objectstack/spec` owns
(objectui#3160, objectstack#4115 batch 6). All three packages leave the ledger.

**Breaking for importers of `@object-ui/data-objectstack`** — four exported
names changed, because the spec exports the same name for a *different* thing:

| was | now | what the spec's same-named export actually is |
|:--|:--|:--|
| `CacheStats` | `MetadataCacheStats` | the platform `ICacheService` counters (`keyCount`, `memoryUsage`) |
| `MetadataSaveOptions` | `MetadataClientSaveOptions` | options for writing a metadata item to a **file** (`format`, `path`, `indent`, `atomic`) |
| `SecurityPolicy` | `SecurityManagerPolicy` | the package supply-chain policy (`autoScan`, licences, code signing, sandbox) |
| `ValidationError` | `DataApiValidationError` | a plain `{ field, message, code? }` entry in a validation report |

Each pair is disjoint or nearly so — `MetadataSaveOptions` and `SecurityPolicy`
share not one key with the spec type whose name they wore — so none of them was
a dialect to reconcile; they were four unrelated concepts squatting on spec
names. `DataApiValidationError` follows the `<what was validated>Validation<Error|Result>`
convention registered on objectstack#4115 (`@object-ui/core` took
`SchemaNodeValidationError` in batch 4). Its **runtime** `name` deliberately
stays `'ValidationError'`: `normaliseClientError` and `@object-ui/react`'s
error-message helper both sniff `err.name`, so that string is a wire contract,
not a symbol.

**Breaking for importers of `@object-ui/plugin-chatbot`** — `PendingActionRow`
and `PendingActionStatus` are now re-exported from `@objectstack/spec/contracts`
instead of hand-transcribed, which narrows them. The copies had drifted three
ways, and each drift had **disabled a compile-time check** rather than merely
differed from one:

- `status: PendingActionStatus | string` — a union with `string` absorbs the
literals, so that annotation carried no information at all;
- `[key: string]: unknown` — the objectstack#4075 mechanism: an index signature
makes every structural comparison against the spec answer "identical", however
far the copy has drifted;
- `created_at` / `updated_at`, which the service contract does not carry and no
consumer in this repo reads.

**Breaking for importers of `@object-ui/plugin-list`** — `ViewTab` is derived from the spec's `ViewTabSchema`
— from its **input** side, because `pinned` / `isDefault` / `visible` carry
`.default()`s and this component is handed authored metadata, not parsed output.
That removes a renderer-side tolerance the copy carried: `visible` accepted
`string | boolean` and the tab bar compared it against the literal `'false'`, a
spelling no producer emits. `label` also stops being required (the spec makes it
optional; `name` is the identifier) and `filter` stops being `any`.

`ListView` and `UserFilters` keep their names as declared dialects: both are the
React **renderers** of the spec types whose names they share, and each takes that
spec type as a prop (`ListViewProps.schema`, `UserFiltersProps.config`) rather
than restating its shape. `Tool` and `MessageContent` in `plugin-chatbot` are
vendored Vercel AI Elements / Shadcn primitives — upstream's component API, not
objectui's authored surface — so the guard now skips that directory the same way
it already skips `components/src/ui/`, with a test that fails if any file there
stops carrying its vendor banner.
7 changes: 6 additions & 1 deletion packages/data-objectstack/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,12 @@ import {
BulkOperationError, // Bulk operation failures with partial results
ConnectionError, // Network/connection errors (503/504)
AuthenticationError, // Authentication failures (401/403)
ValidationError, // Data validation errors (400)
DataApiValidationError, // Data validation errors (400). Its runtime `name` is
// still 'ValidationError' — that string is the wire
// discriminator shared with @objectstack/client. The
// SYMBOL is prefixed because @objectstack/spec/kernel
// owns `ValidationError` for a { field, message, code? }
// record (objectui#3160).
} from '@object-ui/data-objectstack';
```

Expand Down
4 changes: 2 additions & 2 deletions packages/data-objectstack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@
"dependencies": {
"@object-ui/core": "workspace:*",
"@object-ui/types": "workspace:*",
"@objectstack/client": "^17.0.0-rc.1"
"@objectstack/client": "^17.0.0-rc.1",
"@objectstack/spec": "^17.0.0-rc.1"
},
"devDependencies": {
"@objectstack/spec": "^17.0.0-rc.1",
"tsup": "^8.5.1",
"typescript": "^6.0.3",
"vitest": "^4.1.10"
Expand Down
15 changes: 12 additions & 3 deletions packages/data-objectstack/src/cache/MetadataCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,18 @@ interface CachedSchema {
}

/**
* Cache statistics for monitoring
* Statistics reported by {@link MetadataCache.getStats} for monitoring.
*
* NOT the spec's `CacheStats` (`@objectstack/spec/contracts`), whose name this
* interface wore until objectui#3160 (objectstack#4115 ledger batch 6). That one
* describes the platform's `ICacheService` — a server-side KV cache measured by
* `keyCount` and `memoryUsage`. This one describes the browser-side LRU in front
* of `/api/v1/meta/*`: it is bounded (`size`/`maxSize`), it evicts, it coalesces
* concurrent fetches onto one in-flight promise, and it reports a `hitRate`.
* Neither type has a key the other has, so this is a name collision, not a
* dialect — deriving would have replaced every field.
*/
export interface CacheStats {
export interface MetadataCacheStats {
size: number;
maxSize: number;
hits: number;
Expand Down Expand Up @@ -218,7 +227,7 @@ export class MetadataCache {
*
* @returns Cache statistics including hit rate
*/
getStats(): CacheStats {
getStats(): MetadataCacheStats {
const total = this.stats.hits + this.stats.misses;
const hitRate = total > 0 ? this.stats.hits / total : 0;

Expand Down
24 changes: 12 additions & 12 deletions packages/data-objectstack/src/errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
BulkOperationError,
ConnectionError,
AuthenticationError,
ValidationError,
DataApiValidationError,
createErrorFromResponse,
isObjectStackError,
isErrorType,
Expand Down Expand Up @@ -189,20 +189,20 @@ describe('Error Classes', () => {
});
});

describe('ValidationError', () => {
describe('DataApiValidationError', () => {
it('should create validation error', () => {
const error = new ValidationError('Invalid input');
const error = new DataApiValidationError('Invalid input');

expect(error.message).toBe('Invalid input');
expect(error.code).toBe('VALIDATION_ERROR');
expect(error.statusCode).toBe(400);
expect(error.name).toBe('ValidationError');
expect(error).toBeInstanceOf(ObjectStackError);
expect(error).toBeInstanceOf(ValidationError);
expect(error).toBeInstanceOf(DataApiValidationError);
});

it('should include field information', () => {
const error = new ValidationError('Email is invalid', 'email');
const error = new DataApiValidationError('Email is invalid', 'email');

expect(error.field).toBe('email');
expect(error.details).toHaveProperty('field', 'email');
Expand All @@ -214,7 +214,7 @@ describe('Error Classes', () => {
{ field: 'age', message: 'Must be a positive number' },
];

const error = new ValidationError(
const error = new DataApiValidationError(
'Validation failed',
undefined,
validationErrors
Expand All @@ -225,7 +225,7 @@ describe('Error Classes', () => {
});

it('should return empty array when no validation errors', () => {
const error = new ValidationError('Validation failed');
const error = new DataApiValidationError('Validation failed');

expect(error.getValidationErrors()).toEqual([]);
});
Expand Down Expand Up @@ -286,7 +286,7 @@ describe('Error Helpers', () => {
expect(error.code).toBe('NOT_FOUND');
});

it('should create ValidationError for 400 status', () => {
it('should create DataApiValidationError for 400 status', () => {
const response = {
status: 400,
message: 'Bad request',
Expand All @@ -299,9 +299,9 @@ describe('Error Helpers', () => {

const error = createErrorFromResponse(response);

expect(error).toBeInstanceOf(ValidationError);
expect(error).toBeInstanceOf(DataApiValidationError);
expect(error.statusCode).toBe(400);
expect((error as ValidationError).validationErrors).toEqual([
expect((error as DataApiValidationError).validationErrors).toEqual([
{ field: 'email', message: 'Invalid email' },
]);
});
Expand Down Expand Up @@ -379,7 +379,7 @@ describe('Error Helpers', () => {
const bulkError = new BulkOperationError('create', 0, 1, []);
const connError = new ConnectionError('timeout');
const authError = new AuthenticationError();
const validError = new ValidationError('invalid');
const validError = new DataApiValidationError('invalid');

expect(isObjectStackError(metadataError)).toBe(true);
expect(isObjectStackError(bulkError)).toBe(true);
Expand Down Expand Up @@ -409,7 +409,7 @@ describe('Error Helpers', () => {

it('should return false for non-matching error type', () => {
const error = new MetadataNotFoundError('users');
expect(isErrorType(error, ValidationError)).toBe(false);
expect(isErrorType(error, DataApiValidationError)).toBe(false);
});

it('should return true for base class check', () => {
Expand Down
28 changes: 23 additions & 5 deletions packages/data-objectstack/src/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,12 +159,24 @@ export class AuthenticationError extends ObjectStackError {
}

/**
* Error thrown when data validation fails
* Error thrown when the ObjectStack data API rejects a write as invalid.
*
* NOT the spec's `ValidationError` (`@objectstack/spec/kernel`), whose name this
* class wore until objectui#3160 (objectstack#4115 ledger batch 6). That one is
* a plain DATA SHAPE — `{ field, message, code? }`, one entry in a plugin
* manifest's validation report — and `@object-ui/types` re-exports it under that
* name. This is a runtime `Error` subclass carrying an HTTP status plus a list
* of such entries, so the two are not even the same KIND of thing.
*
* The name follows the convention registered on objectstack#4115 for this
* family — `<what was validated>Validation<Error|Result>`. `@object-ui/core`
* took `SchemaNodeValidationError` for its SDUI-tree walk; this one belongs to
* the data API.
*/
export class ValidationError extends ObjectStackError {
export class DataApiValidationError extends ObjectStackError {
/**
* Create a new ValidationError
*
* Create a new DataApiValidationError
*
* @param message - Human-readable error message
* @param field - The field that failed validation (optional)
* @param validationErrors - Array of validation error details
Expand All @@ -186,6 +198,12 @@ export class ValidationError extends ObjectStackError {
...details,
}
);
// The RUNTIME name stays `'ValidationError'` on purpose — it is the wire
// discriminator this adapter shares with `@objectstack/client` and with
// consumers that sniff `err.name` rather than `instanceof`
// (`normaliseClientError` here, `@object-ui/react`'s `error-message`).
// Renaming the TypeScript symbol is a source-level rename; renaming this
// string would be a behaviour change nobody asked for.
this.name = 'ValidationError';
}

Expand Down Expand Up @@ -232,7 +250,7 @@ export function createErrorFromResponse(response: Record<string, unknown>, conte
return new ObjectStackError(message, 'NOT_FOUND', 404, details);

case 400:
return new ValidationError(message, undefined, (response?.data as Record<string, unknown>)?.errors as Array<{ field: string; message: string }>, details);
return new DataApiValidationError(message, undefined, (response?.data as Record<string, unknown>)?.errors as Array<{ field: string; message: string }>, details);

case 503:
return new ConnectionError(message, (response?.config as Record<string, unknown>)?.url as string, details, 503);
Expand Down
38 changes: 22 additions & 16 deletions packages/data-objectstack/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { ObjectStackClient, type QueryOptions as ObjectStackQueryOptions } from '@objectstack/client';
import type { DroppedFieldsEvent } from '@objectstack/spec/data';
import type {
DataSource,
BatchTransactionOperation,
Expand Down Expand Up @@ -39,7 +40,7 @@ import {
MetadataNotFoundError,
BulkOperationError,
ConnectionError,
ValidationError,
DataApiValidationError,
createErrorFromResponse,
} from './errors';

Expand Down Expand Up @@ -588,7 +589,7 @@ export function isConcurrentUpdateError(error: unknown): error is ConcurrentUpda
*
* Two shapes are recognised:
* - `409` + `CONCURRENT_UPDATE` → {@link ConcurrentUpdateError};
* - `400` + `VALIDATION_FAILED` → {@link ValidationError}, carrying the
* - `400` + `VALIDATION_FAILED` → {@link DataApiValidationError}, carrying the
* server's per-field entries so a form can mark the offending inputs
* instead of showing one undirected toast.
*/
Expand Down Expand Up @@ -621,7 +622,7 @@ export function normaliseClientError(error: unknown): unknown {
})
.filter((x): x is { field: string; message: string } => x !== null);

return new ValidationError(
return new DataApiValidationError(
typeof e.message === 'string' ? e.message : 'Validation failed',
validationErrors[0]?.field,
validationErrors,
Expand Down Expand Up @@ -721,16 +722,21 @@ export type BatchProgressListener = (event: BatchProgressEvent) => void;
/**
* One server-reported write-strip: caller-supplied fields the backend LEGALLY
* removed from a write before persisting (a non-system caller cannot seed a
* `readonly` field, a `readonlyWhen` predicate locked it, etc.). Mirrors the
* framework `DroppedFieldsEvent` (spec `DroppedFieldsEventSchema`) structurally
* so we don't pin a client type version — `reason` is kept as a widened string
* for forward-compatibility with reasons added server-side.
* `readonly` field, a `readonlyWhen` predicate locked it, etc.).
*
* THE spec type, re-exported (objectui#3160, objectstack#4115 ledger batch 6).
* Until then this was a hand copy whose comment said it "mirrors the framework
* `DroppedFieldsEvent` (spec `DroppedFieldsEventSchema`) structurally so we
* don't pin a client type version", with `reason` widened from the spec's
* `'readonly' | 'readonly_when'` to bare `string` "for forward-compatibility
* with reasons added server-side". Both halves of that reasoning are the
* failure mode this ledger exists to remove: the spec IS the client type
* version, and a consumer-side widening of a producer's enum is precisely the
* lenient fallback AGENTS.md #12 bans — it deletes the only compile-time signal
* that would tell `AdapterProvider`'s toast wording (which branches on
* `readonly_when`) that a new reason had appeared.
*/
export interface DroppedFieldsEvent {
object: string;
fields: string[];
reason: string;
}
export type { DroppedFieldsEvent };

/**
* Emitted after a create/update whose response carried `droppedFields`
Expand Down Expand Up @@ -3511,14 +3517,14 @@ export {
BulkOperationError,
ConnectionError,
AuthenticationError,
ValidationError,
DataApiValidationError,
createErrorFromResponse,
isObjectStackError,
isErrorType,
} from './errors';

// Export cache types
export type { CacheStats } from './cache/MetadataCache';
export type { MetadataCacheStats } from './cache/MetadataCache';

// v3.0.0 Deep Integration modules
export { CloudOperations } from './cloud';
Expand Down Expand Up @@ -3546,7 +3552,7 @@ export type {
MetadataClientConfig,
MetadataListOptions,
MetadataDraftHeader,
MetadataSaveOptions,
MetadataClientSaveOptions,
MetadataGetOptions,
MetadataDeleteOptions,
MetadataHistoryOptions,
Expand All @@ -3563,7 +3569,7 @@ export type {
} from './metadata-client';

export { SecurityManager } from './security';
export type { SecurityPolicy, CSPConfig, AuditLogConfig, AuditEventType, DataMaskingConfig, DataMaskingRule, AuditLogEntry } from './security';
export type { SecurityManagerPolicy, CSPConfig, AuditLogConfig, AuditEventType, DataMaskingConfig, DataMaskingRule, AuditLogEntry } from './security';

export { createDefaultCanvasConfig, snapToGrid, calculateAutoLayout } from './studio';
export type { StudioCanvasConfig, StudioPropertyEditor, StudioThemeBuilderConfig, StudioColorPalette, StudioTypographyPreset, StudioShadowPreset } from './studio';
Loading
Loading