From 0f9d607b0ccb0a8e512e0eec8d44cdcfd16cd2ec Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:52:57 +0800 Subject: [PATCH] fix(platform-objects): allow import/export on sys_business_unit_member (#3025 / #3391 P0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Completes the #3025 point-fix. #3392 added import/export to sys_business_unit but left the sibling membership table on a CRUD-only whitelist, so the HRIS org-tree sync scenario (units + memberships imported together) still 405'd on the membership import/export path. #3391's P0 checklist pairs both tables; this is the half #3392 missed. - sys_business_unit_member enable.apiMethods gains 'import','export'. - Reconcile-safe: import/export are not in MANAGED_WRITE_VERB_AFFORDANCE, so reconcileManagedApiMethods (managedBy:'platform') never strips them — the declared whitelist reaches apiAccessDenialFromEnable intact. - Regression test locks import/export + CRUD, proven red-before-green. Transitional per #3391 P2 (derived import/export mapping reclaims both objects). Refs #3025, #3391. Co-Authored-By: Claude Opus 4.8 --- .../business-unit-member-import-export.md | 32 +++++++++++++++++++ .../sys-business-unit-member.object.ts | 9 +++++- .../src/platform-objects.test.ts | 22 +++++++++++++ 3 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 .changeset/business-unit-member-import-export.md diff --git a/.changeset/business-unit-member-import-export.md b/.changeset/business-unit-member-import-export.md new file mode 100644 index 0000000000..38b70f30c8 --- /dev/null +++ b/.changeset/business-unit-member-import-export.md @@ -0,0 +1,32 @@ +--- +"@objectstack/platform-objects": patch +--- + +fix(platform-objects): allow import/export on sys_business_unit_member (#3025 / #3391 P0) + +Completes the #3025 point-fix. #3392 unblocked `sys_business_unit`'s Import/Export +buttons (405 `OBJECT_API_METHOD_NOT_ALLOWED`) by adding `'import'`/`'export'` to its +`enable.apiMethods` whitelist, but the HRIS org-tree sync scenario imports **two +tables together** — the units *and* their memberships — and the sibling +`sys_business_unit_member` was left on the CRUD-only whitelist, so the membership +Import/Export path still 405'd. #3391's P0 checklist pairs both tables; this is the +half #3392 missed. + +- `packages/platform-objects/src/identity/sys-business-unit-member.object.ts`: + `apiMethods` gains `'import'`, `'export'`. Import reuses the object's + already-granted create/update affordances; export is a bulk read. +- Reconcile-safe: the object is `managedBy:'platform'`, but + `reconcileManagedApiMethods` only strips generic write verbs + (`create/update/upsert/delete/purge` — `MANAGED_WRITE_VERB_AFFORDANCE`). It never + touches `import`/`export`, so the declared whitelist reaches the REST gate intact + (no false-green: the static whitelist the regression test asserts IS what + `apiAccessDenialFromEnable` enforces at runtime). +- Regression test (`platform-objects.test.ts`) locks `import`/`export` presence and + CRUD retention. Proven red-before-green: reverting the object edit fails with + `expected [...] to include 'import'`. + +Transitional: #3391 P2 replaces per-object `import`/`export` declarations with a +single derived mapping (import ⊆ create/update, export ⊆ list) and reclaims the +explicit entries on both business-unit objects together. + +Refs #3025, #3391. diff --git a/packages/platform-objects/src/identity/sys-business-unit-member.object.ts b/packages/platform-objects/src/identity/sys-business-unit-member.object.ts index 8e0066eae8..4ee73d2e2f 100644 --- a/packages/platform-objects/src/identity/sys-business-unit-member.object.ts +++ b/packages/platform-objects/src/identity/sys-business-unit-member.object.ts @@ -101,7 +101,14 @@ export const SysBusinessUnitMember = ObjectSchema.create({ trackHistory: true, searchable: true, apiEnabled: true, - apiMethods: ['get', 'list', 'create', 'update', 'delete'], + // import/export complete the HRIS org-tree sync scenario: the units + // (sys_business_unit, #3025/#3392) and their memberships are imported + // together as one bulk operation. Import reuses the already-granted + // create/update affordances; export is a bulk read. Transitional — #3391 + // P2 derives these from create/update|list and reclaims the explicit + // entries. Reconcile-safe: import/export are not generic write verbs, so + // reconcileManagedApiMethods (managedBy:'platform') never strips them. + 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 25c385fa8f..b2246d971e 100644 --- a/packages/platform-objects/src/platform-objects.test.ts +++ b/packages/platform-objects/src/platform-objects.test.ts @@ -5,6 +5,7 @@ import { SysAccount, SysApiKey, SysBusinessUnit, + SysBusinessUnitMember, SysDeviceCode, SysInvitation, SysJwks, @@ -246,6 +247,27 @@ describe('@objectstack/platform-objects', () => { expect(SysBusinessUnit.enable?.apiMethods).toContain(verb); } }); + + it('sys_business_unit_member allows import/export and keeps CRUD (#3391 P0)', () => { + // HRIS org-tree sync imports TWO tables together — the units (above) AND + // their memberships. The sibling membership table carries the same kind + // of restrictive whitelist, so it needs import/export too or the + // membership import path 405s (OBJECT_API_METHOD_NOT_ALLOWED). #3391's P0 + // checklist pairs both tables; this is the half #3392 did not cover. + const methods = SysBusinessUnitMember.enable?.apiMethods ?? []; + expect(methods).toContain('import'); + expect(methods).toContain('export'); + // CRUD must remain — import writes reuse create/update; the membership + // grid depends on the rest. + for (const verb of ['get', 'list', 'create', 'update', 'delete'] as const) { + expect(methods).toContain(verb); + } + // Transitional: #3391 P2 derives import/export (import ⊆ create/update, + // export ⊆ list) and reclaims both objects' explicit entries together. + // Reconcile-safe: import/export are not generic write verbs, so + // reconcileManagedApiMethods (managedBy:'platform') never strips them — + // this static whitelist IS what apiAccessDenialFromEnable enforces. + }); }); describe('SETUP_APP (ADR-0029 D7 shell)', () => {