diff --git a/.changeset/adr-0029-k2b-approvals-ownership.md b/.changeset/adr-0029-k2b-approvals-ownership.md new file mode 100644 index 000000000..f94139e5c --- /dev/null +++ b/.changeset/adr-0029-k2b-approvals-ownership.md @@ -0,0 +1,24 @@ +--- +"@objectstack/platform-objects": minor +"@objectstack/plugin-approvals": minor +--- + +ADR-0029 K2.b — approvals domain ownership + Setup nav contribution. + +Moves `sys_approval_request` / `sys_approval_action` out of the +`@objectstack/platform-objects` monolith into `@objectstack/plugin-approvals`, +which already registers and operates them — so the plugin now owns its data +model, behavior, and admin menu as one unit. + +- The object definitions move to `plugin-approvals`; `platform-objects` no + longer exports them from `/audit`. Runtime is unchanged (the plugin already + registered them at runtime). +- **D7 navigation** — the Setup app's `group_approvals` entries (`Requests`, + `Action History`) move out of `platform-objects`' `SETUP_NAV_CONTRIBUTIONS` + into `plugin-approvals`' `navigationContributions`. The plugin fills the slot + it owns; when the plugin is absent the slot stays empty. +- **i18n (D8)** — the objects are removed from the `platform-objects` i18n + extract config; their existing generated translation bundles keep working at + runtime (object-name keyed). Migrating the i18n extraction/bundles to the + plugin remains the tracked cross-cutting follow-up (best done with the + `os i18n extract` tooling, not hand-edited generated files). diff --git a/packages/platform-objects/scripts/i18n-extract.config.ts b/packages/platform-objects/scripts/i18n-extract.config.ts index 3bc5b0465..574d27623 100644 --- a/packages/platform-objects/scripts/i18n-extract.config.ts +++ b/packages/platform-objects/scripts/i18n-extract.config.ts @@ -73,8 +73,7 @@ import { SysEmailTemplate, SysSavedReport, SysReportSchedule, - SysApprovalRequest, - SysApprovalAction, + // sys_approval_* moved to @objectstack/plugin-approvals (ADR-0029 K2.b / D8). SysJob, SysJobRun, SysJobQueue, @@ -167,8 +166,7 @@ export default defineStack({ SysEmailTemplate, SysSavedReport, SysReportSchedule, - SysApprovalRequest, - SysApprovalAction, + // sys_approval_* moved to @objectstack/plugin-approvals (ADR-0029 K2.b / D8). SysJob, SysJobRun, SysJobQueue, diff --git a/packages/platform-objects/src/apps/setup-nav.contributions.ts b/packages/platform-objects/src/apps/setup-nav.contributions.ts index f4e1b5c19..8693ab87f 100644 --- a/packages/platform-objects/src/apps/setup-nav.contributions.ts +++ b/packages/platform-objects/src/apps/setup-nav.contributions.ts @@ -9,11 +9,12 @@ * in `@objectstack/platform-objects`. They are registered alongside * `SETUP_APP` (via `plugin-auth`'s `manifest.register`). * - * `group_integrations` is intentionally **absent** here — its Webhooks / - * Webhook Deliveries entries are contributed by `@objectstack/plugin-webhooks`, - * which owns `sys_webhook` / `sys_webhook_delivery` (ADR-0029 K2.a). As each - * remaining domain moves to its capability plugin (K2.b+), its entries move - * out of this file into that plugin the same way. + * Some groups are intentionally **absent** here because a capability plugin + * owns them and contributes their entries: + * - `group_integrations` → `@objectstack/plugin-webhooks` (ADR-0029 K2.a) + * - `group_approvals` → `@objectstack/plugin-approvals` (ADR-0029 K2.b) + * As each remaining domain moves to its capability plugin, its entries move out + * of this file into that plugin the same way. * * Priority 100 keeps platform-objects base entries ahead of later * contributions in the same group (mirrors object owner priority). @@ -65,15 +66,8 @@ export const SETUP_NAV_CONTRIBUTIONS: NavigationContribution[] = [ { id: 'nav_api_keys', type: 'object', label: 'API Keys', objectName: 'sys_api_key', icon: 'key', requiredPermissions: ['manage_platform_settings'] }, ], }, - { - app: 'setup', - group: 'group_approvals', - priority: BASE_PRIORITY, - items: [ - { id: 'nav_approval_requests', type: 'object', label: 'Requests', objectName: 'sys_approval_request', icon: 'inbox', requiresObject: 'sys_approval_request' }, - { id: 'nav_approval_actions', type: 'object', label: 'Action History', objectName: 'sys_approval_action', icon: 'history', requiresObject: 'sys_approval_action' }, - ], - }, + // group_approvals is contributed by @objectstack/plugin-approvals, which owns + // sys_approval_request / sys_approval_action (ADR-0029 K2.b). { app: 'setup', group: 'group_configuration', diff --git a/packages/platform-objects/src/audit/index.ts b/packages/platform-objects/src/audit/index.ts index 032116acc..d794cfb50 100644 --- a/packages/platform-objects/src/audit/index.ts +++ b/packages/platform-objects/src/audit/index.ts @@ -14,8 +14,7 @@ export { SysEmail } from './sys-email.object.js'; export { SysEmailTemplate } from './sys-email-template.object.js'; export { SysSavedReport } from './sys-saved-report.object.js'; export { SysReportSchedule } from './sys-report-schedule.object.js'; -export { SysApprovalRequest } from './sys-approval-request.object.js'; -export { SysApprovalAction } from './sys-approval-action.object.js'; +// sys_approval_request / sys_approval_action moved to @objectstack/plugin-approvals (ADR-0029 K2.b). export { SysJob } from './sys-job.object.js'; export { SysJobRun } from './sys-job-run.object.js'; export { SysJobQueue } from './sys-job-queue.object.js'; diff --git a/packages/platform-objects/src/platform-objects.test.ts b/packages/platform-objects/src/platform-objects.test.ts index fc56b5619..1a440d3a9 100644 --- a/packages/platform-objects/src/platform-objects.test.ts +++ b/packages/platform-objects/src/platform-objects.test.ts @@ -286,11 +286,13 @@ describe('@objectstack/platform-objects', () => { } }); - it('does not contribute the plugin-owned integrations slot', () => { - // group_integrations belongs to @objectstack/plugin-webhooks, not the - // platform-objects base contributions. - const integrations = SETUP_NAV_CONTRIBUTIONS.find((c) => c.group === 'group_integrations'); - expect(integrations).toBeUndefined(); + it('does not contribute slots owned by capability plugins', () => { + // group_integrations → @objectstack/plugin-webhooks (K2.a) + // group_approvals → @objectstack/plugin-approvals (K2.b) + for (const ownedSlot of ['group_integrations', 'group_approvals']) { + const contrib = SETUP_NAV_CONTRIBUTIONS.find((c) => c.group === ownedSlot); + expect(contrib).toBeUndefined(); + } }); }); }); diff --git a/packages/plugins/plugin-approvals/src/approvals-plugin.ts b/packages/plugins/plugin-approvals/src/approvals-plugin.ts index 8027612a7..a500d3b53 100644 --- a/packages/plugins/plugin-approvals/src/approvals-plugin.ts +++ b/packages/plugins/plugin-approvals/src/approvals-plugin.ts @@ -1,10 +1,8 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. import type { Plugin, PluginContext } from '@objectstack/core'; -import { - SysApprovalRequest, - SysApprovalAction, -} from '@objectstack/platform-objects/audit'; +import { SysApprovalRequest } from './sys-approval-request.object.js'; +import { SysApprovalAction } from './sys-approval-action.object.js'; import { ApprovalService, type ApprovalEngine } from './approval-service.js'; import { bindApprovalLockHook, unbindAllHooks } from './lifecycle-hooks.js'; import { registerApprovalNode, type ApprovalAutomationSurface } from './approval-node.js'; @@ -53,6 +51,20 @@ export class ApprovalsServicePlugin implements Plugin { defaultDatasource: 'cloud', namespace: 'sys', objects: [SysApprovalRequest, SysApprovalAction], + // ADR-0029 D7 — contribute the Approvals entries into the Setup app's + // `group_approvals` slot. This plugin owns these objects (K2.b), so it + // ships their menu too; when the plugin isn't installed the slot is empty. + navigationContributions: [ + { + app: 'setup', + group: 'group_approvals', + priority: 100, + items: [ + { id: 'nav_approval_requests', type: 'object', label: 'Requests', objectName: 'sys_approval_request', icon: 'inbox', requiresObject: 'sys_approval_request' }, + { id: 'nav_approval_actions', type: 'object', label: 'Action History', objectName: 'sys_approval_action', icon: 'history', requiresObject: 'sys_approval_action' }, + ], + }, + ], }); ctx.logger.info('ApprovalsServicePlugin: schemas registered'); } diff --git a/packages/plugins/plugin-approvals/src/index.ts b/packages/plugins/plugin-approvals/src/index.ts index 8bccde322..355ad6e9b 100644 --- a/packages/plugins/plugin-approvals/src/index.ts +++ b/packages/plugins/plugin-approvals/src/index.ts @@ -10,10 +10,8 @@ * the `approval` node. */ -export { - SysApprovalRequest, - SysApprovalAction, -} from '@objectstack/platform-objects/audit'; +export { SysApprovalRequest } from './sys-approval-request.object.js'; +export { SysApprovalAction } from './sys-approval-action.object.js'; export { ApprovalService, type ApprovalEngine, diff --git a/packages/plugins/plugin-approvals/src/nav-contribution.test.ts b/packages/plugins/plugin-approvals/src/nav-contribution.test.ts new file mode 100644 index 000000000..f789168af --- /dev/null +++ b/packages/plugins/plugin-approvals/src/nav-contribution.test.ts @@ -0,0 +1,46 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +import { describe, it, expect } from 'vitest'; +import { ApprovalsServicePlugin } from './approvals-plugin.js'; + +/** + * ADR-0029 K2.b / D7 — the approvals plugin owns sys_approval_request / + * sys_approval_action and ships their Setup-app menu as a navigation + * contribution (rather than the entries living statically in the + * platform-objects Setup shell). + */ +describe('ApprovalsServicePlugin schema + nav contribution (ADR-0029 K2.b)', () => { + it('registers the approval objects and contributes the group_approvals slot', async () => { + const registered: any[] = []; + const ctx: any = { + getService: (name: string) => + name === 'manifest' ? { register: (m: any) => registered.push(m) } : undefined, + logger: { info: () => {}, warn: () => {} }, + }; + + const plugin = new ApprovalsServicePlugin({ disableService: true }); + await plugin.init(ctx); + + expect(registered).toHaveLength(1); + const manifest = registered[0]; + + // Owns both approval objects (moved out of platform-objects). + expect(manifest.objects.map((o: any) => o.name).sort()).toEqual([ + 'sys_approval_action', + 'sys_approval_request', + ]); + + // Contributes its menu into the Setup app's approvals slot. + expect(manifest.navigationContributions).toHaveLength(1); + const contribution = manifest.navigationContributions[0]; + expect(contribution).toMatchObject({ app: 'setup', group: 'group_approvals' }); + expect(contribution.items.map((i: any) => i.objectName).sort()).toEqual([ + 'sys_approval_action', + 'sys_approval_request', + ]); + // Each entry is gated so the slot stays empty when the plugin is absent. + for (const item of contribution.items) { + expect(item.requiresObject).toBe(item.objectName); + } + }); +}); diff --git a/packages/platform-objects/src/audit/sys-approval-action.object.ts b/packages/plugins/plugin-approvals/src/sys-approval-action.object.ts similarity index 100% rename from packages/platform-objects/src/audit/sys-approval-action.object.ts rename to packages/plugins/plugin-approvals/src/sys-approval-action.object.ts diff --git a/packages/platform-objects/src/audit/sys-approval-request.object.ts b/packages/plugins/plugin-approvals/src/sys-approval-request.object.ts similarity index 100% rename from packages/platform-objects/src/audit/sys-approval-request.object.ts rename to packages/plugins/plugin-approvals/src/sys-approval-request.object.ts