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
2 changes: 1 addition & 1 deletion content/docs/references/api/contract.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ const result = ApiError.parse(data);
| **cursor** | `Record<string, any>` | optional | Cursor for keyset pagination |
| **joins** | `Object[]` | optional | Explicit Table Joins |
| **aggregations** | `Object[]` | optional | Aggregation functions |
| **groupBy** | `string \| Object[]` | optional | GROUP BY targets (strings or ``{field, dateGranularity?}`` objects for date bucketing) |
| **groupBy** | `string \| Object[]` | optional | GROUP BY targets (strings or `{field, dateGranularity?}` objects for date bucketing) |
| **having** | `[__schema2](./__schema2)` | optional | HAVING clause for aggregation filtering |
| **windowFunctions** | `Object[]` | optional | Window functions with OVER clause |
| **distinct** | `boolean` | optional | SELECT DISTINCT flag |
Expand Down
4 changes: 2 additions & 2 deletions content/docs/references/api/metadata.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ Metadata query with filtering, sorting, and pagination

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **types** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
| **types** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
| **namespaces** | `string[]` | optional | Filter by namespaces |
| **packageId** | `string` | optional | Filter by owning package |
| **search** | `string` | optional | Full-text search query |
Expand Down Expand Up @@ -349,7 +349,7 @@ Metadata query with filtering, sorting, and pagination

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **type** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>` | ✅ | Metadata type |
| **type** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>` | ✅ | Metadata type |
| **name** | `string` | ✅ | Item name (snake_case) |
| **data** | `Record<string, any>` | ✅ | Metadata payload |
| **namespace** | `string` | optional | Optional namespace |
Expand Down
38 changes: 36 additions & 2 deletions content/docs/references/data/datasource.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ Can be a built-in driver or a plugin-contributed driver (e.g., "com.vendor.snowf
## TypeScript Usage

```typescript
import { Datasource, DatasourceCapabilities, DriverDefinition, DriverType } from '@objectstack/spec/data';
import type { Datasource, DatasourceCapabilities, DriverDefinition, DriverType } from '@objectstack/spec/data';
import { Datasource, DatasourceCapabilities, DriverDefinition, DriverType, ExternalDatasourceSettings, ModeSchema } from '@objectstack/spec/data';
import type { Datasource, DatasourceCapabilities, DriverDefinition, DriverType, ExternalDatasourceSettings, Mode } from '@objectstack/spec/data';

// Validate data
const result = Datasource.parse(data);
Expand All @@ -43,6 +43,8 @@ const result = Datasource.parse(data);
| **retryPolicy** | `Object` | optional | Connection retry policy for transient failures |
| **description** | `string` | optional | Internal description |
| **active** | `boolean` | ✅ | Is datasource enabled |
| **schemaMode** | `Enum<'managed' \| 'external' \| 'validate-only'>` | ✅ | Schema ownership mode |
| **external** | `Object` | optional | External datasource federation settings (schemaMode != "managed") |


---
Expand Down Expand Up @@ -87,3 +89,35 @@ const result = Datasource.parse(data);

---

## ExternalDatasourceSettings

External datasource federation settings (schemaMode != "managed")

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **label** | `string` | optional | Display label, e.g. "Snowflake — ANALYTICS / PROD" |
| **allowedSchemas** | `string[]` | optional | Whitelist of remote schemas/databases that may be exposed. |
| **allowWrites** | `boolean` | ✅ | Global write gate. Individual objects must also opt in via object.external.writable. |
| **validation** | `Object` | ✅ | Boot/drift validation policy |
| **credentialsRef** | `string` | optional | Reference into the secrets store; never inline credentials. |
| **queryTimeoutMs** | `number` | ✅ | Hard cap on per-query execution time. |
| **requirePermission** | `string` | optional | Optional convenience: gate the entire datasource behind a single role. |


---

## ModeSchema

Schema ownership mode

### Allowed Values

* `managed`
* `external`
* `validate-only`


---

82 changes: 82 additions & 0 deletions content/docs/references/data/external-catalog.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
---
title: External Catalog
description: External Catalog protocol schemas
---

{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs go in content/docs/guides/. */}

ExternalCatalog — cached remote-schema snapshot for a federated datasource

(ADR-0015 §4.3).

Introspecting a mature warehouse on every boot is expensive, so the

`IExternalDatasourceService.refreshCatalog` persists a snapshot of the

remote tables/columns as an `external_catalog` metadata record. The

boot-validation gate (Gate 2) and Studio's schema browser read from it;

drift is detected by diffing a fresh introspection against the snapshot.

<Callout type="info">
**Source:** `packages/spec/src/data/external-catalog.zod.ts`
</Callout>

## TypeScript Usage

```typescript
import { ExternalCatalog, ExternalColumn, ExternalTable } from '@objectstack/spec/data';
import type { ExternalCatalog, ExternalColumn, ExternalTable } from '@objectstack/spec/data';

// Validate data
const result = ExternalCatalog.parse(data);
```

---

## ExternalCatalog

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **name** | `string` | ✅ | Catalog id, conventionally `<datasource>_catalog`. |
| **datasource** | `string` | ✅ | Datasource.name this catalog snapshots. |
| **snapshotAt** | `string` | ✅ | When the snapshot was taken (ISO 8601). |
| **dialect** | `string` | optional | Remote SQL dialect, when known. |
| **tables** | `Object[]` | ✅ | Snapshotted remote tables. |


---

## ExternalColumn

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **name** | `string` | ✅ | Remote column name |
| **sqlType** | `string` | ✅ | Raw remote SQL type (e.g. "numeric(10,2)") |
| **nullable** | `boolean` | ✅ | Whether the remote column is nullable |
| **primaryKey** | `boolean` | ✅ | Part of the remote primary key |
| **suggestedFieldType** | `string` | optional | ObjectStack field type suggested by the type-compat matrix |


---

## ExternalTable

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **remoteSchema** | `string` | optional | Remote schema/database qualifier |
| **remoteName** | `string` | ✅ | Remote table/view name |
| **columns** | `Object[]` | ✅ | Remote columns |
| **indexes** | `Object[]` | optional | Remote indexes, when introspectable |
| **rowCountEstimate** | `number` | optional | Approximate row count |


---

1 change: 1 addition & 0 deletions content/docs/references/data/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ This section contains all protocol schemas for the data layer of ObjectStack.
<Card href="/docs/references/data/driver" title="Driver" description="Source: packages/spec/src/data/driver.zod.ts" />
<Card href="/docs/references/data/driver-nosql" title="Driver Nosql" description="Source: packages/spec/src/data/driver-nosql.zod.ts" />
<Card href="/docs/references/data/driver-sql" title="Driver Sql" description="Source: packages/spec/src/data/driver-sql.zod.ts" />
<Card href="/docs/references/data/external-catalog" title="External Catalog" description="Source: packages/spec/src/data/external-catalog.zod.ts" />
<Card href="/docs/references/data/external-lookup" title="External Lookup" description="Source: packages/spec/src/data/external-lookup.zod.ts" />
<Card href="/docs/references/data/feed" title="Feed" description="Source: packages/spec/src/data/feed.zod.ts" />
<Card href="/docs/references/data/field" title="Field" description="Source: packages/spec/src/data/field.zod.ts" />
Expand Down
1 change: 1 addition & 0 deletions content/docs/references/data/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"driver",
"driver-nosql",
"driver-sql",
"external-catalog",
"external-lookup",
"feed",
"field",
Expand Down
22 changes: 20 additions & 2 deletions content/docs/references/data/object.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ API Operations Enum
## TypeScript Usage

```typescript
import { ApiMethod, CDCConfig, Index, ObjectCapabilities, ObjectOwnershipEnum, PartitioningConfig, SoftDeleteConfig, TenancyConfig, VersioningConfig } from '@objectstack/spec/data';
import type { ApiMethod, CDCConfig, Index, ObjectCapabilities, ObjectOwnershipEnum, PartitioningConfig, SoftDeleteConfig, TenancyConfig, VersioningConfig } from '@objectstack/spec/data';
import { ApiMethod, CDCConfig, Index, ObjectCapabilities, ObjectExternalBinding, ObjectOwnershipEnum, PartitioningConfig, SoftDeleteConfig, TenancyConfig, VersioningConfig } from '@objectstack/spec/data';
import type { ApiMethod, CDCConfig, Index, ObjectCapabilities, ObjectExternalBinding, ObjectOwnershipEnum, PartitioningConfig, SoftDeleteConfig, TenancyConfig, VersioningConfig } from '@objectstack/spec/data';

// Validate data
const result = ApiMethod.parse(data);
Expand Down Expand Up @@ -91,6 +91,24 @@ const result = ApiMethod.parse(data);
| **clone** | `boolean` | ✅ | Allow record deep cloning |


---

## ObjectExternalBinding

External datasource binding (ADR-0015)

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **remoteName** | `string` | optional | Remote table/view name. Defaults to object.name. |
| **remoteSchema** | `string` | optional | Remote schema/database qualifier. |
| **writable** | `boolean` | ✅ | Per-object write opt-in (also requires datasource.external.allowWrites). |
| **columnMap** | `Record<string, string>` | optional | Remote column name → local field name. |
| **introspectedAt** | `string` | optional | Set by `os datasource introspect`; informational. |
| **ignoreColumns** | `string[]` | optional | Remote columns to skip during validation (dev convenience). |


---

## ObjectOwnershipEnum
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/data/query.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ Type: `string`
| **cursor** | `Record<string, any>` | optional | Cursor for keyset pagination |
| **joins** | `Object[]` | optional | Explicit Table Joins |
| **aggregations** | `Object[]` | optional | Aggregation functions |
| **groupBy** | `string \| Object[]` | optional | GROUP BY targets (strings or ``{field, dateGranularity?}`` objects for date bucketing) |
| **groupBy** | `string \| Object[]` | optional | GROUP BY targets (strings or `{field, dateGranularity?}` objects for date bucketing) |
| **having** | `[__schema1](./__schema1)` | optional | HAVING clause for aggregation filtering |
| **windowFunctions** | `Object[]` | optional | Window functions with OVER clause |
| **distinct** | `boolean` | optional | SELECT DISTINCT flag |
Expand Down
7 changes: 4 additions & 3 deletions content/docs/references/kernel/metadata-plugin.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const result = MetadataBulkRegisterRequest.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **event** | `Enum<'metadata.registered' \| 'metadata.updated' \| 'metadata.unregistered' \| 'metadata.validated' \| 'metadata.deployed' \| 'metadata.overlay.applied' \| 'metadata.overlay.removed' \| 'metadata.imported' \| 'metadata.exported'>` | ✅ | Event type |
| **metadataType** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>` | ✅ | Metadata type |
| **metadataType** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>` | ✅ | Metadata type |
| **name** | `string` | ✅ | Metadata item name |
| **namespace** | `string` | optional | Namespace |
| **packageId** | `string` | optional | Owning package ID |
Expand Down Expand Up @@ -183,7 +183,7 @@ const result = MetadataBulkRegisterRequest.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **types** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
| **types** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>[]` | optional | Filter by metadata types |
| **namespaces** | `string[]` | optional | Filter by namespaces |
| **packageId** | `string` | optional | Filter by owning package |
| **search** | `string` | optional | Full-text search query |
Expand Down Expand Up @@ -232,6 +232,7 @@ const result = MetadataBulkRegisterRequest.parse(data);
* `approval`
* `job`
* `datasource`
* `external_catalog`
* `translation`
* `router`
* `function`
Expand All @@ -253,7 +254,7 @@ const result = MetadataBulkRegisterRequest.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **type** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>` | ✅ | Metadata type identifier |
| **type** | `Enum<'object' \| 'field' \| 'trigger' \| 'validation' \| 'hook' \| 'view' \| 'page' \| 'dashboard' \| 'app' \| 'action' \| 'report' \| 'flow' \| 'workflow' \| 'approval' \| 'job' \| 'datasource' \| 'external_catalog' \| 'translation' \| 'router' \| 'function' \| 'service' \| 'email_template' \| 'permission' \| 'profile' \| 'role' \| 'agent' \| 'tool' \| 'skill'>` | ✅ | Metadata type identifier |
| **label** | `string` | ✅ | Display label for the metadata type |
| **description** | `string` | optional | Description of the metadata type |
| **filePatterns** | `string[]` | ✅ | Glob patterns to discover files of this type |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/ui/app.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const result = AppBranding.parse(data);

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **id** | `string` | ✅ | Selector id; selected value is exposed as the nav template var `{`<id>`}` |
| **id** | `string` | ✅ | Selector id; selected value is exposed as the nav template var `{<id>}` |
| **label** | `string` | ✅ | Dropdown label |
| **icon** | `string` | optional | Icon name |
| **optionsSource** | `Object` | ✅ | Option data source |
Expand Down
15 changes: 13 additions & 2 deletions packages/cli/test/serve-defaults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,23 @@ import { describe, expect, it } from 'vitest';
import Serve from '../src/commands/serve.js';

describe('serve: ALWAYS_ON_CAPABILITIES default slate', () => {
it('exposes the six foundational capabilities in stable order', () => {
expect(Serve.ALWAYS_ON_CAPABILITIES).toEqual([
// ALWAYS_ON_CAPABILITIES is the fail-closed allowlist of platform services
// that are injected into every app's `requires` at runtime. The six
// foundational capabilities must always lead the slate in this precedence
// order; the list may grow beyond them (e.g. `sharing`) without churning
// this assertion, so we pin the prefix rather than the whole array.
it('leads with the six foundational capabilities in stable order', () => {
expect(Serve.ALWAYS_ON_CAPABILITIES.slice(0, 6)).toEqual([
'queue', 'job', 'cache', 'settings', 'email', 'storage',
]);
});

it('contains no duplicates', () => {
expect(Serve.ALWAYS_ON_CAPABILITIES).toHaveLength(
new Set(Serve.ALWAYS_ON_CAPABILITIES).size,
);
});

it('is frozen so accidental mutation throws', () => {
expect(Object.isFrozen(Serve.ALWAYS_ON_CAPABILITIES)).toBe(true);
});
Expand Down
7 changes: 6 additions & 1 deletion packages/objectql/src/protocol-meta.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -878,7 +878,12 @@ describe('ObjectStackProtocolImplementation - Metadata Persistence', () => {

expect(result.loaded).toBe(2);
expect(registry.getItem('app', 'test_app')).toEqual(sampleApp);
expect(registry.getItem('object', 'task')).toEqual(objDef);
// Object schemas pass through registerObject -> applyProtection (ADR-0010 §3.7),
// which stamps the internal `_packageId`/`_provenance` envelope markers used by
// listItems() filtering, the HTTP dispatcher and the runtime. Those are an
// intentional, system-wide concern — assert the author-supplied shape is preserved
// rather than demanding strict equality against them.
expect(registry.getItem('object', 'task')).toMatchObject(objDef);
});
});

Expand Down
40 changes: 32 additions & 8 deletions packages/spec/scripts/build-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,38 @@ function generateMarkdown(schemaName: string, schema: any, category: string, zod
// Add schema heading
md += `## ${schemaName}\n\n`;

// Escape MDX-unsafe characters in description text. MDX parses `<` as JSX,
// so any raw `<title>` / `<meta>` / etc. inside a Zod `.describe()` string
// breaks the docs build. Wrap inline tag-like fragments in backticks so they
// render as code rather than being parsed as JSX.
const escapeMdxDescription = (raw: string): string =>
raw
.replace(/\{[^}]*\}/g, (m: string) => `\`${m}\``)
.replace(/<[^`>][^>]*>/g, (m: string) => `\`${m}\``);
// Escape MDX-unsafe characters in description text. MDX parses `{` as a JS
// expression and `<` as JSX, so any raw `{token}` / `<title>` inside a Zod
// `.describe()` string breaks the docs build. Wrap such fragments in inline
// code so they render literally.
//
// Single pass with backtick tracking: fragments already inside an inline-code
// span are left untouched. A naive two-pass replace double-wraps nested cases
// like `{<id>}` into `` `{`<id>`}` `` — the inner backticks close the span
// early and leak `<id>` as raw JSX (MDX: "Expected a closing tag for `<id>`").
const escapeMdxDescription = (raw: string): string => {
let out = '';
let inCode = false;
for (let i = 0; i < raw.length; i++) {
const ch = raw[i];
if (ch === '`') {
inCode = !inCode;
out += ch;
continue;
}
if (!inCode && (ch === '{' || ch === '<')) {
const close = ch === '{' ? '}' : '>';
const end = raw.indexOf(close, i + 1);
if (end !== -1) {
out += '`' + raw.slice(i, end + 1) + '`';
i = end;
continue;
}
}
out += ch;
}
return out;
};

// Add description with better formatting
if (mainDef.description) {
Expand Down