diff --git a/.changeset/business-unit-import-export.md b/.changeset/business-unit-import-export.md new file mode 100644 index 0000000000..7e0e0bbd66 --- /dev/null +++ b/.changeset/business-unit-import-export.md @@ -0,0 +1,17 @@ +--- +"@objectstack/platform-objects": patch +--- + +fix(platform-objects): allow import/export on sys_business_unit (#3025) + +The Business Units list (Setup → Business Units) surfaces Import/Export buttons, +but `sys_business_unit` declared a restrictive `enable.apiMethods` whitelist of +only the five CRUD verbs. The REST data plane gates import/export on that +whitelist (ADR-0049), so both buttons returned `405 OBJECT_API_METHOD_NOT_ALLOWED`. + +This was an unintentional gap, not a deliberate restriction: the object's fields +(`external_ref`, `effective_from/to`) are designed for HRIS batch sync, and the +org tree is expected to support bulk import. Added `'import'` and `'export'` to +the whitelist. Import reuses the `create`/`update` affordances the object already +grants, and the managed-object reconciliation backstop leaves import/export +untouched (it only strips write verbs). Regression test added. diff --git a/packages/platform-objects/src/identity/sys-business-unit.object.ts b/packages/platform-objects/src/identity/sys-business-unit.object.ts index 94b94b75d3..bc9cbc1292 100644 --- a/packages/platform-objects/src/identity/sys-business-unit.object.ts +++ b/packages/platform-objects/src/identity/sys-business-unit.object.ts @@ -216,7 +216,10 @@ export const SysBusinessUnit = ObjectSchema.create({ trackHistory: true, searchable: true, apiEnabled: true, - apiMethods: ['get', 'list', 'create', 'update', 'delete'], + // Data portability is expected for the org tree: fields like `external_ref` + // and `effective_from/to` are designed for HRIS batch sync (#3025). Import + // reuses the create/update affordances this object already grants. + apiMethods: ['get', 'list', 'create', 'update', 'delete', 'import', 'export'], trash: true, mru: false, }, diff --git a/packages/platform-objects/src/platform-objects.test.ts b/packages/platform-objects/src/platform-objects.test.ts index 7af69ef9b6..25c385fa8f 100644 --- a/packages/platform-objects/src/platform-objects.test.ts +++ b/packages/platform-objects/src/platform-objects.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it } from 'vitest'; import { SysAccount, SysApiKey, + SysBusinessUnit, SysDeviceCode, SysInvitation, SysJwks, @@ -230,6 +231,23 @@ describe('@objectstack/platform-objects', () => { }); }); + describe('data portability whitelist (#3025)', () => { + it('sys_business_unit allows import/export so the org tree can be batch-synced', () => { + // The Business Units list exposes Import/Export buttons and the object's + // schema (external_ref, effective_from/to) is designed for HRIS batch + // sync. The REST data plane gates these routes on `enable.apiMethods` + // (ADR-0049), so both verbs must be present or the buttons 405 with + // OBJECT_API_METHOD_NOT_ALLOWED. Regression guard for #3025. + expect(SysBusinessUnit.enable?.apiMethods).toContain('import'); + expect(SysBusinessUnit.enable?.apiMethods).toContain('export'); + // The five CRUD verbs it already granted must remain — import writes + // reuse the create/update affordances. + for (const verb of ['get', 'list', 'create', 'update', 'delete'] as const) { + expect(SysBusinessUnit.enable?.apiMethods).toContain(verb); + } + }); + }); + describe('SETUP_APP (ADR-0029 D7 shell)', () => { it('parses cleanly through AppSchema', () => { expect(() => AppSchema.parse(SETUP_APP)).not.toThrow();