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
24 changes: 24 additions & 0 deletions .changeset/adr-0029-k2b-approvals-ownership.md
Original file line number Diff line number Diff line change
@@ -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).
6 changes: 2 additions & 4 deletions packages/platform-objects/scripts/i18n-extract.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
22 changes: 8 additions & 14 deletions packages/platform-objects/src/apps/setup-nav.contributions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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',
Expand Down
3 changes: 1 addition & 2 deletions packages/platform-objects/src/audit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
12 changes: 7 additions & 5 deletions packages/platform-objects/src/platform-objects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
});
});
});
20 changes: 16 additions & 4 deletions packages/plugins/plugin-approvals/src/approvals-plugin.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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');
}
Expand Down
6 changes: 2 additions & 4 deletions packages/plugins/plugin-approvals/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
46 changes: 46 additions & 0 deletions packages/plugins/plugin-approvals/src/nav-contribution.test.ts
Original file line number Diff line number Diff line change
@@ -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);
}
});
});