diff --git a/libs/common/src/platform/services/sdk/client-managed-state.spec.ts b/libs/common/src/platform/services/sdk/client-managed-state.spec.ts index 574628290f11..ff82adfc644a 100644 --- a/libs/common/src/platform/services/sdk/client-managed-state.spec.ts +++ b/libs/common/src/platform/services/sdk/client-managed-state.spec.ts @@ -91,6 +91,63 @@ describe("RepositoryRecord", () => { }); }); + describe("shouldInclude filtering", () => { + // `excluded:`-prefixed values stand in for records the SDK cannot represent. + function createFilteringMapper(): SdkRecordMapper { + return { + ...createMapper(), + shouldInclude: (value: ClientType) => !value.startsWith("excluded:"), + }; + } + + beforeEach(() => { + repo = new RepositoryRecord(userId, stateProvider, createFilteringMapper()); + }); + + it("get returns the mapped value for an included element", async () => { + await setState({ "id-1": "value-1" }); + + const result = await repo.get("id-1"); + + expect(result).toEqual({ value: "value-1" }); + }); + + it("get returns null for a stored but excluded element", async () => { + await setState({ "id-1": "excluded:value-1" }); + + const result = await repo.get("id-1"); + + expect(result).toBeNull(); + }); + + it("list omits excluded elements and keeps the rest", async () => { + await setState({ "id-1": "value-1", "id-2": "excluded:value-2", "id-3": "value-3" }); + + const result = await repo.list(); + + expect(result).toEqual([{ value: "value-1" }, { value: "value-3" }]); + }); + + it("list returns everything when the mapper has no shouldInclude", async () => { + repo = new RepositoryRecord(userId, stateProvider, createMapper()); + await setState({ "id-1": "value-1", "id-2": "excluded:value-2" }); + + const result = await repo.list(); + + expect(result).toEqual([{ value: "value-1" }, { value: "excluded:value-2" }]); + }); + + it("set still writes an excluded element to state", async () => { + await setState({}); + + await repo.set("id-1", { value: "excluded:value-1" }); + + // Excluding a record hides it from the SDK; it must still round-trip through client state. + expect(await getState()).toEqual({ "id-1": "excluded:value-1" }); + expect(await repo.get("id-1")).toBeNull(); + }); + }); + describe("set", () => { it("adds new item to empty state", async () => { await setState({}); diff --git a/libs/common/src/platform/services/sdk/client-managed-state.ts b/libs/common/src/platform/services/sdk/client-managed-state.ts index aadd9ab91b1c..e6ecb35c1eeb 100644 --- a/libs/common/src/platform/services/sdk/client-managed-state.ts +++ b/libs/common/src/platform/services/sdk/client-managed-state.ts @@ -38,6 +38,15 @@ export interface SdkRecordMapper { userKeyDefinition(): UserKeyDefinition>; toSdk(value: ClientType): SdkType; fromSdk(value: SdkType): ClientType; + /** + * Optional predicate to exclude entries from the SDK-visible view of state. + * Returning `false` makes {@link RepositoryRecord.get} return null and excludes + * the entry from {@link RepositoryRecord.list}. Use for records the SDK cannot + * represent, so it never has to parse them. Writes are unaffected — the entry + * still lives in client state and can be read through the client's own services. + * Defaults to including everything. + */ + shouldInclude?(value: ClientType): boolean; } /** @@ -67,12 +76,20 @@ export class RepositoryRecord implements Repository { const record = await this.getRecord(); const element = record[id]; - return element ? this.mapper.toSdk(element) : null; + if (!element || this.mapper.shouldInclude?.(element) === false) { + return null; + } + return this.mapper.toSdk(element); } async list(): Promise { const record = await this.getRecord(); - return Object.values(record).map((element) => this.mapper.toSdk(element)); + const elements = Object.values(record); + const included = + this.mapper.shouldInclude != null + ? elements.filter((element) => this.mapper.shouldInclude!(element)) + : elements; + return included.map((element) => this.mapper.toSdk(element)); } async set(id: string, value: SdkType): Promise {