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
20 changes: 20 additions & 0 deletions .changeset/adr-0062-analytics-external.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@objectstack/spec": minor
"@objectstack/service-analytics": minor
---

feat(analytics): correct analytics over federated objects (ADR-0062 Phase 3, D6)

Analytics over an external (federated) object now aggregates against the
**correct** remote table instead of silently querying the wrong one. The
`NativeSQLStrategy` hand-compiles `FROM "<object>"` and bare column references,
which bypass the driver's physical-table resolution (`external.remoteName` /
`remoteSchema` / `columnMap`). It now **declines** any query whose base or joined
object is federated, routing it to the `ObjectQLStrategy` — whose
`engine.aggregate()` goes through the driver's `getBuilder` and already honours
`remoteName`/`remoteSchema` (#2138/#2149). This "reuses the driver's resolution"
(D6) rather than re-implementing it.

Adds an optional `StrategyContext.isExternalObject(objectName)` hook (reported by
the analytics plugin from the object's `external` block). Purely additive — with
no hook, behavior is unchanged for managed objects.
20 changes: 20 additions & 0 deletions .changeset/adr-0062-credentials-fail-closed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@objectstack/service-datasource": minor
---

feat(datasource): fail-closed credential resolution at connect (ADR-0062 Phase 2, D3)

`DatasourceConnectionService` now treats a declared `external.credentialsRef` as
**fail-closed**: the credential must resolve to a cleartext secret (via the
host's `SecretBinder` over `ICryptoProvider`) *before* the driver is built. An
absent secret store, or a ref that cannot be resolved/decrypted (missing
`sys_secret` row, rotated key, or a throwing resolver), leaves the datasource
**unconnected with a clear message** — never a silent build-without-secret that
would connect with no/wrong auth or fail later with a confusing driver error.

The same policy as connect failures applies: a code-defined `external` datasource
with `validation.onMismatch: 'fail'` auto-connected at boot fails fast (bricks
boot); runtime-admin create/update + boot rehydration degrade-with-warning. Code-
and runtime-origin secrets converge on the one connection path (the same
`SecretBinder` is threaded through the shared service). New `failed-credentials`
connect status.
38 changes: 38 additions & 0 deletions .changeset/adr-0062-datasource-connection-service.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
"@objectstack/service-datasource": minor
"@objectstack/runtime": minor
"@objectstack/spec": minor
---

feat(datasource): auto-connect declared external datasources (ADR-0062 Phase 1, D1/D2/D5)

A declared external datasource is now connected to a live ObjectQL driver and its
federated objects are queryable **with zero app code** — no `onEnable` driver
wiring. Implements ADR-0062 Phase 1.

- **D1 — one connect path.** New `DatasourceConnectionService` in
`@objectstack/service-datasource` owns the single "definition → live driver"
path: build via the injected driver factory → resolve `external.credentialsRef`
via the `SecretBinder` → connect → `engine.registerDriver` under the datasource
name → register the datasource def → sync each bound federated object's read
metadata (DDL-free). Both origins converge on it: the runtime-admin
`registerPool` now delegates here, and `AppPlugin` auto-connects code-defined
datasources. Exposed as the `'datasource-connection'` kernel service.
- **D2 — opt-in-safe gate.** A declared datasource auto-connects only when it is
`external`, an object **explicitly** binds to it via `object.datasource`, or it
sets the new `autoConnect: true` flag. A managed datasource that nothing
explicitly binds (incl. ones referenced only by a `datasourceMapping` rule, e.g.
`examples/app-crm`'s `:memory:` datasources) stays metadata-only — existing apps
are byte-for-byte unchanged. See the ADR-0062 D2 implementation note.
- **D5 — lifecycle, ordering & policy.** Connect happens in `AppPlugin.start()`
(before the `kernel:ready` validation gate, relying on the kernel's
init-all-then-start-all ordering). Fail-fast for a declared `external` datasource
with `validation.onMismatch: 'fail'`; degrade-with-warning otherwise (and always
for runtime-admin/rehydrate, so a UI action or replica blip never bricks the
server). Adds a host-injectable `DatasourceConnectPolicy` (open-core default
allows; a multi-tenant host binds a stricter fail-closed policy for egress
isolation) consulted before every connect — one connect path, no cloud fork.

Adds `datasource.autoConnect` to the spec. The legacy `onEnable` +
`ctx.drivers.register` bridge remains supported as an escape hatch (idempotent vs.
auto-connect). No behavior change for managed apps.
22 changes: 22 additions & 0 deletions .changeset/adr-0062-reconcile-cleanup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
"@objectstack/cli": minor
"@objectstack/verify": minor
---

feat(datasource): reject field.columnName on external objects + drop showcase onEnable bridge (ADR-0062 Phase 4, D7/D8)

**D7 — reconcile column mapping.** `os compile`/`build` (`validateStackExpressions`)
now rejects `field.columnName` on a federated (external) object with a corrective
message: the driver's query pipeline ignores `field.columnName` for external
objects, so `external.columnMap` is the single authoritative mechanism. Managed
objects are untouched.

**D8 — drop the canonical example's driver bridge.** `examples/app-showcase`
declares its external datasource with **no** `onEnable` driver registration — the
declared datasource auto-connects at boot (ADR-0062 D1). `onEnable` now only
provisions the "remote" fixture tables. To cover this end-to-end, the
`@objectstack/verify` harness wires the datasource-admin plugin (registering the
`'datasource-connection'` service) when an app declares datasources, so it mirrors
`objectstack dev`/serve; a new dogfood test reads the federated objects through the
real REST stack (incl. the `remoteName` remap). `onEnable` + `ctx.drivers.register`
remains supported as an escape hatch for drivers built dynamically at runtime.
1 change: 1 addition & 0 deletions content/docs/references/ai/agent.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ const result = AIKnowledge.parse(data);
| **instructions** | `string` | ✅ | System Prompt / Prime Directives |
| **model** | `Object` | optional | |
| **lifecycle** | `Object` | optional | State machine defining the agent conversation follow and constraints |
| **surface** | `Enum<'ask' \| 'build'>` | ✅ | Product surface this agent binds ('ask' | 'build') — ADR-0063 §1 |
| **skills** | `string[]` | optional | Skill names to attach (Agent→Skill→Tool architecture) |
| **tools** | `Object[]` | optional | Direct tool references (legacy fallback) |
| **knowledge** | `Object` | optional | RAG access |
Expand Down
1 change: 1 addition & 0 deletions content/docs/references/ai/skill.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const result = Skill.parse(data);
| **name** | `string` | ✅ | Skill unique identifier (snake_case) |
| **label** | `string` | ✅ | Skill display name |
| **description** | `string` | optional | Skill description |
| **surface** | `Enum<'ask' \| 'build' \| 'both'>` | ✅ | Agent surface this skill binds to ('ask' | 'build' | 'both') — ADR-0063 §3 |
| **instructions** | `string` | optional | LLM instructions when skill is active |
| **tools** | `string[]` | ✅ | Tool names belonging to this skill (supports trailing wildcard, e.g. `action_*`) |
| **triggerPhrases** | `string[]` | optional | Phrases that activate this skill |
Expand Down
2 changes: 1 addition & 1 deletion content/docs/references/api/contract.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ const result = ApiError.parse(data);
| **having** | `[__schema2](./__schema2)` | optional | HAVING clause for aggregation filtering |
| **windowFunctions** | `Object[]` | optional | Window functions with OVER clause |
| **distinct** | `boolean` | optional | SELECT DISTINCT flag |
| **expand** | `Record<string, [__schema0](./__schema0)>` | optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select, filter, sort, and further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3. |
| **expand** | `Record<string, [__schema0](./__schema0)>` | optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (`fields`) and filter (`where`, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent `limit`/`offset`/`orderBy` are NOT applied on this path. |


---
Expand Down
1 change: 1 addition & 0 deletions content/docs/references/data/data-engine.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ QueryAST-aligned options for DataEngine.aggregate operations
| **where** | `Record<string, any> \| [__schema0](./__schema0)` | optional | |
| **groupBy** | `string[]` | optional | |
| **aggregations** | `Object[]` | optional | |
| **timezone** | `string` | optional | |


---
Expand Down
1 change: 1 addition & 0 deletions content/docs/references/data/datasource.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const result = Datasource.parse(data);
| **retryPolicy** | `Object` | optional | Connection retry policy for transient failures |
| **description** | `string` | optional | Internal description |
| **active** | `boolean` | ✅ | Is datasource enabled |
| **autoConnect** | `boolean` | ✅ | Force a live driver connection at boot even when managed + unrouted (ADR-0062 D2). |
| **schemaMode** | `Enum<'managed' \| 'external' \| 'validate-only'>` | ✅ | Schema ownership mode |
| **external** | `Object` | optional | External datasource federation settings (schemaMode != "managed") |
| **origin** | `Enum<'code' \| 'runtime'>` | ✅ | Datasource provenance (server-managed, read-only) |
Expand Down
1 change: 1 addition & 0 deletions content/docs/references/data/driver.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ const result = DriverCapabilities.parse(data);
| **skipCache** | `boolean` | optional | Bypass cache |
| **traceContext** | `Record<string, string>` | optional | OpenTelemetry context or request ID |
| **tenantId** | `string` | optional | Tenant Isolation identifier |
| **timezone** | `string` | optional | Business reference timezone (IANA) for date-dependent generation, e.g. autonumber date tokens |


---
Expand Down
13 changes: 11 additions & 2 deletions content/docs/references/data/hook-body.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ a capability it did not declare, the call throws at invocation time.

- `api.write` — `ctx.api.object(...).insert / update / delete`

- `api.transaction` — `ctx.api.transaction(async () => \{ … \})` — runs the

callback's `ctx.api` writes/reads inside one driver transaction, committed

on return and rolled back if the callback throws. Requires `api.write`

alongside it to be useful (the transaction body still needs write access).

- `crypto.uuid` — `ctx.crypto.randomUUID()`

- `crypto.hash` — `ctx.crypto.hash(algo, data)`
Expand Down Expand Up @@ -86,7 +94,7 @@ L2 sandboxed JS body — runs inside an isolated VM with declared capabilities
| :--- | :--- | :--- | :--- |
| **language** | `string` | ✅ | |
| **source** | `string` | ✅ | Function body source |
| **capabilities** | `Enum<'api.read' \| 'api.write' \| 'crypto.uuid' \| 'crypto.hash' \| 'log'>[]` | ✅ | Granted capability tokens |
| **capabilities** | `Enum<'api.read' \| 'api.write' \| 'api.transaction' \| 'crypto.uuid' \| 'crypto.hash' \| 'log'>[]` | ✅ | Granted capability tokens |
| **timeoutMs** | `integer` | optional | Per-invocation timeout (ms) |
| **memoryMb** | `integer` | optional | Per-invocation memory cap (MB) |

Expand All @@ -101,6 +109,7 @@ L2 sandboxed JS body — runs inside an isolated VM with declared capabilities

* `api.read`
* `api.write`
* `api.transaction`
* `crypto.uuid`
* `crypto.hash`
* `log`
Expand All @@ -118,7 +127,7 @@ L2 sandboxed JS body — runs inside an isolated VM with declared capabilities
| :--- | :--- | :--- | :--- |
| **language** | `string` | ✅ | |
| **source** | `string` | ✅ | Function body source |
| **capabilities** | `Enum<'api.read' \| 'api.write' \| 'crypto.uuid' \| 'crypto.hash' \| 'log'>[]` | ✅ | Granted capability tokens |
| **capabilities** | `Enum<'api.read' \| 'api.write' \| 'api.transaction' \| 'crypto.uuid' \| 'crypto.hash' \| 'log'>[]` | ✅ | Granted capability tokens |
| **timeoutMs** | `integer` | optional | Per-invocation timeout (ms) |
| **memoryMb** | `integer` | optional | Per-invocation memory cap (MB) |

Expand Down
17 changes: 2 additions & 15 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, 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';
import { ApiMethod, Index, ObjectCapabilities, ObjectExternalBinding, ObjectOwnershipEnum, PartitioningConfig, SoftDeleteConfig, TenancyConfig, VersioningConfig } from '@objectstack/spec/data';
import type { ApiMethod, Index, ObjectCapabilities, ObjectExternalBinding, ObjectOwnershipEnum, PartitioningConfig, SoftDeleteConfig, TenancyConfig, VersioningConfig } from '@objectstack/spec/data';

// Validate data
const result = ApiMethod.parse(data);
Expand Down Expand Up @@ -43,19 +43,6 @@ const result = ApiMethod.parse(data);
* `export`


---

## CDCConfig

### Properties

| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **enabled** | `boolean` | ✅ | Enable Change Data Capture |
| **events** | `Enum<'insert' \| 'update' \| 'delete'>[]` | ✅ | Event types to capture |
| **destination** | `string` | ✅ | Destination endpoint (e.g., "kafka://topic", "webhook://url") |


---

## Index
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 @@ -202,7 +202,7 @@ Type: `string`
| **having** | `[__schema1](./__schema1)` | optional | HAVING clause for aggregation filtering |
| **windowFunctions** | `Object[]` | optional | Window functions with OVER clause |
| **distinct** | `boolean` | optional | SELECT DISTINCT flag |
| **expand** | `Record<string, [#](./#)>` | optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select, filter, sort, and further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3. |
| **expand** | `Record<string, [#](./#)>` | optional | Recursive relation loading map. Keys are lookup/master_detail field names; values are nested QueryAST objects that control select (`fields`) and filter (`where`, AND-merged with the batch $in), plus further expansion on the related object. The engine resolves expand via batch $in queries (driver-agnostic) with a default max depth of 3; per-parent `limit`/`offset`/`orderBy` are NOT applied on this path. |


---
Expand Down
6 changes: 6 additions & 0 deletions content/docs/references/kernel/execution-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,18 @@ const result = ExecutionContext.parse(data);
| Property | Type | Required | Description |
| :--- | :--- | :--- | :--- |
| **userId** | `string` | optional | |
| **actor** | `string` | optional | |
| **email** | `string` | optional | |
| **tenantId** | `string` | optional | |
| **timezone** | `string` | optional | |
| **locale** | `string` | optional | |
| **currency** | `string` | optional | |
| **roles** | `string[]` | ✅ | |
| **permissions** | `string[]` | ✅ | |
| **systemPermissions** | `string[]` | optional | |
| **tabPermissions** | `Record<string, Enum<'visible' \| 'hidden' \| 'default_on' \| 'default_off'>>` | optional | |
| **org_user_ids** | `string[]` | optional | |
| **rlsMembership** | `Record<string, string[]>` | optional | |
| **isSystem** | `boolean` | ✅ | |
| **accessToken** | `string` | optional | |
| **transaction** | `any` | optional | |
Expand Down
20 changes: 18 additions & 2 deletions content/docs/references/security/permission.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ Refined with enterprise data lifecycle controls:
## TypeScript Usage

```typescript
import { FieldPermission, ObjectPermission, PermissionSet } from '@objectstack/spec/security';
import type { FieldPermission, ObjectPermission, PermissionSet } from '@objectstack/spec/security';
import { FieldPermission, ObjectAccessScope, ObjectPermission, PermissionSet } from '@objectstack/spec/security';
import type { FieldPermission, ObjectAccessScope, ObjectPermission, PermissionSet } from '@objectstack/spec/security';

// Validate data
const result = FieldPermission.parse(data);
Expand All @@ -43,6 +43,19 @@ const result = FieldPermission.parse(data);
| **editable** | `boolean` | ✅ | Field edit access |


---

## ObjectAccessScope

### Allowed Values

* `own`
* `own_and_reports`
* `unit`
* `unit_and_below`
* `org`


---

## ObjectPermission
Expand All @@ -60,6 +73,8 @@ const result = FieldPermission.parse(data);
| **allowPurge** | `boolean` | ✅ | [EXPERIMENTAL — not enforced] Permanently delete (Hard Delete/GDPR) |
| **viewAllRecords** | `boolean` | ✅ | View All Data (Bypass Sharing) |
| **modifyAllRecords** | `boolean` | ✅ | Modify All Data (Bypass Sharing) |
| **readScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Read depth: own|unit|unit_and_below|org |
| **writeScope** | `Enum<'own' \| 'own_and_reports' \| 'unit' \| 'unit_and_below' \| 'org'>` | optional | [ADR-0057 D1] Write depth: own|unit|unit_and_below|org |


---
Expand All @@ -73,6 +88,7 @@ const result = FieldPermission.parse(data);
| **name** | `string` | ✅ | Permission set unique name (lowercase snake_case) |
| **label** | `string` | optional | Display label |
| **isProfile** | `boolean` | ✅ | Whether this is a user profile |
| **isDefault** | `boolean` | ✅ | [ADR-0056 D7] When true, this profile is the FALLBACK assigned to authenticated users who have no explicit grants — an app declares its default access posture here instead of relying on the built-in member_default. Foundation for SSO/JIT provisioning. |
| **objects** | `Record<string, Object>` | ✅ | Entity permissions |
| **fields** | `Record<string, Object>` | optional | Field level security |
| **systemPermissions** | `string[]` | optional | System level capabilities |
Expand Down
Loading