diff --git a/CHANGELOG.md b/CHANGELOG.md index 3760277956..1084c7cf63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/content/docs/guides/meta.json b/content/docs/guides/meta.json index 04fb592823..3f21377979 100644 --- a/content/docs/guides/meta.json +++ b/content/docs/guides/meta.json @@ -27,6 +27,7 @@ "project-scoping", "driver-configuration", "kernel-services", + "runtime-services", "data-flow", "---Operations---", "deployment-vercel", diff --git a/content/docs/guides/runtime-services/audit-service.mdx b/content/docs/guides/runtime-services/audit-service.mdx new file mode 100644 index 0000000000..6ea8dc7cef --- /dev/null +++ b/content/docs/guides/runtime-services/audit-service.mdx @@ -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 +``` + +## 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. diff --git a/content/docs/guides/runtime-services/data-service.mdx b/content/docs/guides/runtime-services/data-service.mdx new file mode 100644 index 0000000000..88ebe8060c --- /dev/null +++ b/content/docs/guides/runtime-services/data-service.mdx @@ -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(object: string, id: string): Promise> +services.data.list(object: string, options?: ListDataOptions): Promise> +services.data.create(object: string, data: Partial): Promise> +services.data.update(object: string, id: string, data: Partial): Promise> +services.data.delete(object: string, id: string): Promise +``` + +## 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, +}); +``` diff --git a/content/docs/guides/runtime-services/email-service.mdx b/content/docs/guides/runtime-services/email-service.mdx new file mode 100644 index 0000000000..111994c822 --- /dev/null +++ b/content/docs/guides/runtime-services/email-service.mdx @@ -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 +services.email.sendTemplate(input: SendTemplateInput): Promise +``` + +## 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` diff --git a/content/docs/guides/runtime-services/examples.mdx b/content/docs/guides/runtime-services/examples.mdx new file mode 100644 index 0000000000..6b9a3fe85f --- /dev/null +++ b/content/docs/guides/runtime-services/examples.mdx @@ -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 }); + }); + }, +}; +``` diff --git a/content/docs/guides/runtime-services/index.mdx b/content/docs/guides/runtime-services/index.mdx new file mode 100644 index 0000000000..4f6135040c --- /dev/null +++ b/content/docs/guides/runtime-services/index.mdx @@ -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` diff --git a/content/docs/guides/runtime-services/meta.json b/content/docs/guides/runtime-services/meta.json new file mode 100644 index 0000000000..ddff2f6049 --- /dev/null +++ b/content/docs/guides/runtime-services/meta.json @@ -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" + ] +} diff --git a/content/docs/guides/runtime-services/queue-service.mdx b/content/docs/guides/runtime-services/queue-service.mdx new file mode 100644 index 0000000000..d3ad033d41 --- /dev/null +++ b/content/docs/guides/runtime-services/queue-service.mdx @@ -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(queue: string, data: T, options?: QueuePublishOptions): Promise +services.queue.subscribe(queue: string, handler: QueueHandler): Promise +services.queue.unsubscribe(queue: string): Promise +services.queue.getQueueSize?(queue: string): Promise +services.queue.purge?(queue: string): Promise +services.queue.listFailed?(queue?: string, options?: { limit?: number; offset?: number }): Promise +services.queue.replay?(messageId: string): Promise +services.queue.purgeFailed?(messageId: string): Promise +``` + +## Typical Errors + +- `QUEUE_NOT_FOUND` +- `QUEUE_HANDLER_ERROR` +- `QUEUE_MESSAGE_NOT_DLQ` (for replay/purgeFailed) diff --git a/content/docs/guides/runtime-services/settings-service.mdx b/content/docs/guides/runtime-services/settings-service.mdx new file mode 100644 index 0000000000..919bdbbb79 --- /dev/null +++ b/content/docs/guides/runtime-services/settings-service.mdx @@ -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(namespace: string, key: string, ctx?: SettingsContext): Promise> +services.settings.getNamespace(namespace: string, ctx?: SettingsContext): Promise +services.settings.set(namespace: string, key: string, value: unknown, ctx?: SettingsContext): Promise +services.settings.setMany(namespace: string, patch: Record, ctx?: SettingsContext): Promise> +services.settings.runAction(namespace: string, actionId: string, payload: unknown, ctx?: SettingsContext): Promise +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. diff --git a/content/docs/guides/runtime-services/sharing-service.mdx b/content/docs/guides/runtime-services/sharing-service.mdx new file mode 100644 index 0000000000..01612dccdd --- /dev/null +++ b/content/docs/guides/runtime-services/sharing-service.mdx @@ -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 +services.sharing.canEdit(object: string, recordId: string, context: SharingExecutionContext): Promise +services.sharing.grant(input: GrantShareInput, context: SharingExecutionContext): Promise +services.sharing.revoke(shareId: string, context: SharingExecutionContext): Promise +services.sharing.listShares(object: string, recordId: string, context: SharingExecutionContext): Promise +``` + +## 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'); +``` diff --git a/content/docs/guides/runtime-services/storage-service.mdx b/content/docs/guides/runtime-services/storage-service.mdx new file mode 100644 index 0000000000..78e2737793 --- /dev/null +++ b/content/docs/guides/runtime-services/storage-service.mdx @@ -0,0 +1,39 @@ +--- +title: services.storage +description: File/object storage contract for upload/download and presigned URL workflows. +--- + +# `services.storage` + +- **Stability:** `stable` +- **Canonical source:** `packages/spec/src/contracts/storage-service.ts` + +## Core Methods + +```ts +services.storage.upload(key: string, data: Buffer | ReadableStream, options?: StorageUploadOptions): Promise +services.storage.download(key: string): Promise +services.storage.delete(key: string): Promise +services.storage.exists(key: string): Promise +services.storage.getInfo(key: string): Promise +services.storage.list?(prefix: string): Promise +services.storage.getSignedUrl?(key: string, expiresIn: number): Promise +``` + +## Presigned / Chunked (optional) + +```ts +services.storage.getPresignedUpload?(key: string, expiresIn: number, options?: StorageUploadOptions): Promise +services.storage.getPresignedDownload?(key: string, expiresIn: number): Promise +services.storage.initiateChunkedUpload?(key: string, options?: StorageUploadOptions): Promise +services.storage.uploadChunk?(uploadId: string, partNumber: number, data: Buffer): Promise +services.storage.completeChunkedUpload?(uploadId: string, parts: Array<{ partNumber: number; eTag: string }>): Promise +services.storage.abortChunkedUpload?(uploadId: string): Promise +``` + +## Typical Errors + +- `FILE_NOT_FOUND` +- `PERMISSION_DENIED` +- `INVALID_CONTENT_TYPE` +- `UPLOAD_SESSION_NOT_FOUND` diff --git a/content/docs/guides/runtime-services/versioning.mdx b/content/docs/guides/runtime-services/versioning.mdx new file mode 100644 index 0000000000..344ba63fd9 --- /dev/null +++ b/content/docs/guides/runtime-services/versioning.mdx @@ -0,0 +1,31 @@ +--- +title: Runtime Service API Versioning +description: Stability labels and how to track runtime service API breaking changes. +--- + +# Runtime Service API Versioning + +## Stability Labels + +- `stable`: backward-compatible within the current major version. +- `experimental`: can change in minor versions; verify release notes before upgrades. + +## Breaking-Change Tracking + +When a runtime service API changes in a breaking way: + +1. Update the relevant page in this chapter. +2. Add a breaking-change note to root `CHANGELOG.md`. +3. Include migration guidance and affected methods. + +## Current Matrix + +| Service | Stability | +|:--|:--| +| `services.data` | `stable` | +| `services.sharing` | `stable` | +| `services.audit` | `experimental` | +| `services.queue` | `stable` | +| `services.email` | `stable` | +| `services.settings` | `stable` | +| `services.storage` | `stable` | diff --git a/docs/PLATFORM_GAPS_FROM_TEMPLATES.md b/docs/PLATFORM_GAPS_FROM_TEMPLATES.md index c62be73dc3..53fe4f872c 100644 --- a/docs/PLATFORM_GAPS_FROM_TEMPLATES.md +++ b/docs/PLATFORM_GAPS_FROM_TEMPLATES.md @@ -32,7 +32,7 @@ | 20 | 🟢 P3 | 企业 | 多步审批、委派、不在岗代理缺失 | 同上 | | 21 | 🟢 P3 | 企业 | 外部审计员 / 只读访客门户缺失 | 合规类应用刚需 | | 22 | 🟢 P3 | 移动 | 移动端响应式未验证 | 审批走手机是基本盘 | -| 23 | 🟢 P3 | 文档 | `services.data.{get,update}` 等 API 未文档化 | 靠读源码 | +| 23 | 🟢 P3 | 文档 | ~~`services.data.{get,update}` 等 API 未文档化~~ | Fixed in Unreleased: 新增 `content/docs/guides/runtime-services/*` | | 24 | 🟢 P3 | 发布 | 模板包发布/版本升级流程不明 | 模板生态化卡点 | | 25 | 🟢 P3 | 审计 | 平台内置审计日志的可见性 / 配置入口不清 | 合规模板无法引用 | @@ -287,9 +287,10 @@ 审批/提醒类应用在手机上完成是基本盘。当前 UI 在 < 768px 表现未测试。 -### 23. `services.data.{get,update}` 等 API 未文档化 +### 23. ~~`services.data.{get,update}` 等 API 未文档化~~ -模板代码中调用 `services?.data?.get(object, id)` / `services?.data?.update(object, id, values)`,靠读源码发现,签名/错误语义无文档。建议补 SDK 参考。 +模板代码中调用 `services?.data?.get(object, id)` / `services?.data?.update(object, id, values)`,靠读源码发现,签名/错误语义无文档。建议补 SDK 参考。 +**Fixed in Unreleased:** 已新增独立章节 `content/docs/guides/runtime-services/`,覆盖 `services.data/sharing/audit/queue/email/settings/storage` 方法签名、参数、返回、错误语义、示例与稳定性标记。 ### 24. 模板包发布 / 版本升级流程不明 @@ -358,4 +359,3 @@ - **P0 新增**:#26(内联撰写器)、#27(外部门户)、#28(附件 UI 端到端) - **原 P0 复确认**:#1(通知投递)—— 客服模板再次确认是阻塞性 - **P1 新增**:#32(Canned Response)、#34(快捷键)、#35(条件计时器)、#36(跨对象公式) -