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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -7,6 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added — Runtime `services.*` API reference chapter

Documentation now includes a dedicated `Runtime Services` guides chapter
(`content/docs/guides/runtime-services/`) covering:

- `services.data`
- `services.sharing`
- `services.audit`
- `services.queue`
- `services.email`
- `services.settings`
- `services.storage`

The chapter includes per-service method signatures, parameter/return
notes, error semantics, stability labels (`stable` / `experimental`),
and practical examples for flows, hooks, and plugin event
subscriptions.

### Fixed — `objectstack init` produces a working project

`npx @objectstack/cli init my-app` used to scaffold a project that
Expand Down
1 change: 1 addition & 0 deletions content/docs/guides/meta.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,6 +27,7 @@
"project-scoping",
"driver-configuration",
"kernel-services",
"runtime-services",
"data-flow",
"---Operations---",
"deployment-vercel",
Expand Down
36 changes: 36 additions & 0 deletions content/docs/guides/runtime-services/audit-service.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
---
title: services.audit
description: Audit write bridge currently used by settings/runtime integrations.
---

# `services.audit`

- **Stability:** `experimental`
- **Canonical source:** `packages/services/service-settings/src/settings-service.types.ts`

`services.audit` is currently a lightweight sink abstraction used by runtime services (for example `service-settings`) to emit append-only audit entries.

## Method

```ts
services.audit.record(entry: {
namespace: string;
key: string;
scope: SpecifierScope;
userId?: string;
actor?: string;
action: 'set' | 'reset';
valueDigest: string;
encrypted: boolean;
requestId?: string;
}): Promise<void> | void
```

## Notes

- This API is intentionally write-only.
- For querying audit history, use `services.data` against `sys_audit_log`/`sys_activity` objects.

## Typical Errors

Implementations should avoid throwing into user paths. Callers should treat audit writes as best-effort unless strict compliance mode is enabled by host code.
50 changes: 50 additions & 0 deletions content/docs/guides/runtime-services/data-service.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
---
title: services.data
description: CRUD runtime helper API for records (`get`, `list`, `create`, `update`, `delete`).
---

# `services.data`

- **Stability:** `stable`
- **Canonical source:** `packages/client/src/index.ts`

## Methods

```ts
services.data.get<T = any>(object: string, id: string): Promise<GetDataResult<T>>
services.data.list<T = any>(object: string, options?: ListDataOptions): Promise<PaginatedResult<T>>
services.data.create<T = any>(object: string, data: Partial<T>): Promise<CreateDataResult<T>>
services.data.update<T = any>(object: string, id: string, data: Partial<T>): Promise<UpdateDataResult<T>>
services.data.delete(object: string, id: string): Promise<DeleteDataResult>
```

## Parameters

- `object`: short object name (for example `task`, `account`)
- `id`: record ID for single-record operations
- `data`: partial payload for create/update
- `options` (`list`): filters, sorting, pagination (`top`, `skip`, `filter`, `sort`, `select`)

## Returns

- `get`: single record payload
- `list`: list payload + pagination metadata
- `create`/`update`: mutated record payload
- `delete`: success marker / deleted ID payload

## Typical Errors

- `RECORD_NOT_FOUND`
- `VALIDATION_ERROR`
- `PERMISSION_DENIED`

## Example

```ts
const contact = await services.data.get('contact', ctx.input.contact_id);
const orders = await services.data.list('sales_order', {
filter: { contact_id: contact.id },
sort: [{ field: 'created_at', order: 'desc' }],
top: 20,
});
```
33 changes: 33 additions & 0 deletions content/docs/guides/runtime-services/email-service.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
title: services.email
description: Outbound email delivery and template rendering APIs.
---

# `services.email`

- **Stability:** `stable`
- **Canonical source:** `packages/spec/src/contracts/email-service.ts`

## Methods

```ts
services.email.send(input: SendEmailInput): Promise<SendEmailResult>
services.email.sendTemplate(input: SendTemplateInput): Promise<SendEmailResult>
```

## Returns

`SendEmailResult`:

- `id: string`
- `status: 'queued' | 'sent' | 'failed'`
- `messageId?: string`
- `error?: string`

## Typed Error Codes

`sendTemplate` may return/throw implementation errors including:

- `TEMPLATE_NOT_FOUND`
- `TEMPLATE_INACTIVE`
- `MISSING_VARIABLES`
59 changes: 59 additions & 0 deletions content/docs/guides/runtime-services/examples.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
---
title: Runtime Service Examples
description: Practical examples for flow nodes, hooks, and plugin event subscriptions.
---

# Runtime Service Examples

## 1) Flow custom node: read related records

```ts
export async function run(ctx: any) {
const order = await ctx.services?.data?.get('sales_order', ctx.input.orderId);
const lines = await ctx.services?.data?.list('sales_order_line', {
filter: { sales_order_id: order.id },
sort: [{ field: 'line_no', order: 'asc' }],
top: 200,
});

return {
order,
lines: lines.data ?? lines.items ?? [],
};
}
```

## 2) Hook: check sharing permission before mutation

```ts
export async function beforeUpdate(ctx: any) {
const ok = await ctx.services?.sharing?.canEdit('contract', ctx.input.id, {
userId: ctx.session?.userId,
tenantId: ctx.session?.tenantId,
roles: ctx.session?.roles,
});

if (!ok) {
throw new Error('PERMISSION_DENIED');
}
}
```

## 3) Plugin: subscribe to kernel lifecycle events

```ts
import type { Plugin } from '@objectstack/core';

export const ExamplePlugin: Plugin = {
name: 'example-plugin',
async onEnable(ctx: any) {
ctx.events.on('kernel:ready', async () => {
ctx.logger.info('Kernel ready, initializing subscriptions');
});

ctx.events.on('service:registered', async (serviceName: string) => {
ctx.logger.debug('Service registered', { serviceName });
});
},
};
```
35 changes: 35 additions & 0 deletions content/docs/guides/runtime-services/index.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
---
title: Runtime Service APIs
description: Reference entry for runtime `services.*` APIs used by flow nodes, hooks, and plugins.
---

# Runtime Service APIs

This chapter documents the runtime `services.*` APIs used in hook/action/flow/plugin code:

- `services.data`
- `services.sharing`
- `services.audit`
- `services.queue`
- `services.email`
- `services.settings`
- `services.storage`

## Stability Legend

| Level | Meaning |
|:--|:--|
| `stable` | Backward-compatible within a major version |
| `experimental` | API is available but may change in minor releases |

## Source of Truth

Each page links the canonical TypeScript source used to derive signatures.

- Data: `packages/client/src/index.ts`
- Sharing: `packages/spec/src/contracts/sharing-service.ts`
- Queue: `packages/spec/src/contracts/queue-service.ts`
- Email: `packages/spec/src/contracts/email-service.ts`
- Storage: `packages/spec/src/contracts/storage-service.ts`
- Settings: `packages/services/service-settings/src/settings-service.ts`
- Audit bridge: `packages/services/service-settings/src/settings-service.types.ts`
15 changes: 15 additions & 0 deletions content/docs/guides/runtime-services/meta.json
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
{
"title": "Runtime Services",
"pages": [
"index",
"data-service",
"sharing-service",
"audit-service",
"queue-service",
"email-service",
"settings-service",
"storage-service",
"examples",
"versioning"
]
}
28 changes: 28 additions & 0 deletions content/docs/guides/runtime-services/queue-service.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
---
title: services.queue
description: Async queue publish/subscribe and DLQ operations.
---

# `services.queue`

- **Stability:** `stable`
- **Canonical source:** `packages/spec/src/contracts/queue-service.ts`

## Methods

```ts
services.queue.publish<T = unknown>(queue: string, data: T, options?: QueuePublishOptions): Promise<string>
services.queue.subscribe<T = unknown>(queue: string, handler: QueueHandler<T>): Promise<void>
services.queue.unsubscribe(queue: string): Promise<void>
services.queue.getQueueSize?(queue: string): Promise<number>
services.queue.purge?(queue: string): Promise<void>
services.queue.listFailed?(queue?: string, options?: { limit?: number; offset?: number }): Promise<QueueMessageRecord[]>
services.queue.replay?(messageId: string): Promise<void>
services.queue.purgeFailed?(messageId: string): Promise<void>
```

## Typical Errors

- `QUEUE_NOT_FOUND`
- `QUEUE_HANDLER_ERROR`
- `QUEUE_MESSAGE_NOT_DLQ` (for replay/purgeFailed)
33 changes: 33 additions & 0 deletions content/docs/guides/runtime-services/settings-service.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
---
title: services.settings
description: Namespace-based configuration service with env/global/tenant/user/default cascade.
---

# `services.settings`

- **Stability:** `stable`
- **Canonical source:** `packages/services/service-settings/src/settings-service.ts`

## Key Methods

```ts
services.settings.get<T = unknown>(namespace: string, key: string, ctx?: SettingsContext): Promise<ResolvedSettingValue<T>>
services.settings.getNamespace(namespace: string, ctx?: SettingsContext): Promise<SettingsNamespacePayload>
services.settings.set(namespace: string, key: string, value: unknown, ctx?: SettingsContext): Promise<ResolvedSettingValue>
services.settings.setMany(namespace: string, patch: Record<string, unknown>, ctx?: SettingsContext): Promise<Record<string, ResolvedSettingValue>>
services.settings.runAction(namespace: string, actionId: string, payload: unknown, ctx?: SettingsContext): Promise<SettingsActionResult>
services.settings.registerManifest(manifest: SettingsManifest): void
services.settings.listManifests(ctx?: SettingsContext): SettingsManifest[]
services.settings.subscribe(namespace: string | undefined, handler: SettingsChangeHandler): SettingsUnsubscribe
```

## Error Types

- `SETTINGS_LOCKED` (`SettingsLockedError`)
- `SETTINGS_UNKNOWN_NAMESPACE` (`UnknownNamespaceError`)
- `SETTINGS_UNKNOWN_KEY` (`UnknownKeyError`)

## Notes

- Effective resolution order: **Env > Global > Tenant > User > Default**
- Encrypted keys are supported through crypto adapters/providers.
41 changes: 41 additions & 0 deletions content/docs/guides/runtime-services/sharing-service.mdx
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
---
title: services.sharing
description: Record-level sharing and editability checks.
---

# `services.sharing`

- **Stability:** `stable`
- **Canonical source:** `packages/spec/src/contracts/sharing-service.ts`

## Methods

```ts
services.sharing.buildReadFilter(object: string, context: SharingExecutionContext): Promise<unknown | null>
services.sharing.canEdit(object: string, recordId: string, context: SharingExecutionContext): Promise<boolean>
services.sharing.grant(input: GrantShareInput, context: SharingExecutionContext): Promise<RecordShare>
services.sharing.revoke(shareId: string, context: SharingExecutionContext): Promise<void>
services.sharing.listShares(object: string, recordId: string, context: SharingExecutionContext): Promise<RecordShare[]>
```

## Returns

- `buildReadFilter`: `null` means unrestricted read; otherwise returns an engine filter
- `canEdit`: boolean decision
- `grant`/`listShares`: normalized `RecordShare` rows

## Typical Errors

- `PERMISSION_DENIED`
- `SHARE_NOT_FOUND` (implementation-defined)

## Example

```ts
const allowed = await services.sharing.canEdit('contract', ctx.input.id, {
userId: ctx.session?.userId,
tenantId: ctx.session?.tenantId,
roles: ctx.session?.roles,
});
if (!allowed) throw new Error('PERMISSION_DENIED');
```
Loading