From f8bb2f40940a67752f0f5f83d173d1e7ac6b2c33 Mon Sep 17 00:00:00 2001 From: Danijel Martinek Date: Tue, 19 May 2026 10:11:09 +0000 Subject: [PATCH] feat(core-shared): add subject-linkage types and extend PII defaults Introduces SubjectLinkKind, SubjectLink, and CollectionSubject types to packages/core-shared/src/payload/subject-linkage-types.ts, establishes the ambient CollectionCustom.subject declaration (parallel to custom.pii / custom.retention from Epic A), and extends PAYLOAD_AUTH_PII_DEFAULTS with processingRestrictedAt and consentState as DSR-managed excluded fields. Applies the first canonical usage of custom.subject = { kind: "self", field: "id" } on the auth users collection. --- coverage/summary.json | 44 +++++------ .../src/integrations/cms/collections/users.ts | 1 + packages/core-shared/src/payload/index.ts | 5 ++ .../src/payload/payload-custom-ambient.d.ts | 2 + .../core-shared/src/payload/pii-types.test.ts | 12 ++- packages/core-shared/src/payload/pii-types.ts | 2 + .../src/payload/subject-linkage-types.test.ts | 73 +++++++++++++++++++ .../src/payload/subject-linkage-types.ts | 10 +++ 8 files changed, 125 insertions(+), 24 deletions(-) create mode 100644 packages/core-shared/src/payload/subject-linkage-types.test.ts create mode 100644 packages/core-shared/src/payload/subject-linkage-types.ts diff --git a/coverage/summary.json b/coverage/summary.json index f2ad0e7..bab4d10 100644 --- a/coverage/summary.json +++ b/coverage/summary.json @@ -1,18 +1,18 @@ { - "generatedAt": "2026-05-18T20:21:48.917Z", - "commit": "188625b", + "generatedAt": "2026-05-19T10:10:24.367Z", + "commit": "5abf7fe", "repo": { - "statements": 96.47, - "branches": 91.71, - "functions": 96.81, - "lines": 96.47, + "statements": 96.48, + "branches": 91.72, + "functions": 96.83, + "lines": 96.48, "counts": { - "lf": 4254, - "lh": 4104, - "brf": 808, - "brh": 741, - "fnf": 251, - "fnh": 243 + "lf": 4264, + "lh": 4114, + "brf": 809, + "brh": 742, + "fnf": 252, + "fnh": 244 } }, "byPackage": { @@ -59,17 +59,17 @@ } }, "@repo/core-shared": { - "statements": 97.98, - "branches": 95.79, - "functions": 91.92, - "lines": 97.98, + "statements": 98, + "branches": 95.81, + "functions": 92, + "lines": 98, "counts": { - "lf": 1040, - "lh": 1019, - "brf": 309, - "brh": 296, - "fnf": 99, - "fnh": 91 + "lf": 1050, + "lh": 1029, + "brf": 310, + "brh": 297, + "fnf": 100, + "fnh": 92 } }, "@repo/marketing-pages": { diff --git a/packages/auth/src/integrations/cms/collections/users.ts b/packages/auth/src/integrations/cms/collections/users.ts index 87ed51e..cabb76c 100644 --- a/packages/auth/src/integrations/cms/collections/users.ts +++ b/packages/auth/src/integrations/cms/collections/users.ts @@ -15,6 +15,7 @@ export const users: CollectionConfig = { action: "hard-delete", }, }, + subject: { kind: "self", field: "id" }, }, fields: [ { diff --git a/packages/core-shared/src/payload/index.ts b/packages/core-shared/src/payload/index.ts index 070c6d5..900c133 100644 --- a/packages/core-shared/src/payload/index.ts +++ b/packages/core-shared/src/payload/index.ts @@ -14,6 +14,11 @@ export type { } from "./pii-types"; export { PAYLOAD_AUTH_PII_DEFAULTS } from "./pii-types"; export type { PurgeSchedule, CollectionRetention } from "./retention-types"; +export type { + SubjectLinkKind, + SubjectLink, + CollectionSubject, +} from "./subject-linkage-types"; export { parseDurationMs, scheduleDelayMs, diff --git a/packages/core-shared/src/payload/payload-custom-ambient.d.ts b/packages/core-shared/src/payload/payload-custom-ambient.d.ts index 1279c63..390b9fe 100644 --- a/packages/core-shared/src/payload/payload-custom-ambient.d.ts +++ b/packages/core-shared/src/payload/payload-custom-ambient.d.ts @@ -1,5 +1,6 @@ import type { FieldPii } from "./pii-types"; import type { CollectionRetention } from "./retention-types"; +import type { CollectionSubject } from "./subject-linkage-types"; declare module "payload" { // FieldBase.custom is typed as FieldCustom (interface extending Record). @@ -12,5 +13,6 @@ declare module "payload" { interface CollectionCustom { retention?: CollectionRetention; authPii?: Record; + subject?: CollectionSubject | CollectionSubject[]; } } diff --git a/packages/core-shared/src/payload/pii-types.test.ts b/packages/core-shared/src/payload/pii-types.test.ts index 9b7a398..f8c67fb 100644 --- a/packages/core-shared/src/payload/pii-types.test.ts +++ b/packages/core-shared/src/payload/pii-types.test.ts @@ -13,6 +13,8 @@ const CREDENTIAL_FIELDS = [ "apiKeyIndex", ] as const; +const DSR_MANAGED_FIELDS = ["processingRestrictedAt", "consentState"] as const; + describe("FieldPii type safety", () => { it("rejects FieldPii missing required fields at compile time", () => { // @ts-expect-error — 'purpose', 'exportable', 'restrictable' are required @@ -88,8 +90,14 @@ describe("PAYLOAD_AUTH_PII_DEFAULTS", () => { expect(emailPii?.restrictable).toBe(true); }); - it("has exactly 10 keys: email plus 9 credential fields", () => { - expect(Object.keys(PAYLOAD_AUTH_PII_DEFAULTS)).toHaveLength(10); + it("has exactly 12 keys: email, 9 credential fields, and 2 DSR-managed fields", () => { + expect(Object.keys(PAYLOAD_AUTH_PII_DEFAULTS)).toHaveLength(12); + }); + + it("sets DSR-managed fields to null", () => { + for (const field of DSR_MANAGED_FIELDS) { + expect(PAYLOAD_AUTH_PII_DEFAULTS[field]).toBeNull(); + } }); it("email has no retention override (falls back to collection-level)", () => { diff --git a/packages/core-shared/src/payload/pii-types.ts b/packages/core-shared/src/payload/pii-types.ts index eb9616e..9e0899a 100644 --- a/packages/core-shared/src/payload/pii-types.ts +++ b/packages/core-shared/src/payload/pii-types.ts @@ -61,4 +61,6 @@ export const PAYLOAD_AUTH_PII_DEFAULTS: Record = { lockUntil: null, apiKey: null, apiKeyIndex: null, + processingRestrictedAt: null, + consentState: null, }; diff --git a/packages/core-shared/src/payload/subject-linkage-types.test.ts b/packages/core-shared/src/payload/subject-linkage-types.test.ts new file mode 100644 index 0000000..5c8e9bd --- /dev/null +++ b/packages/core-shared/src/payload/subject-linkage-types.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from "vitest"; +import type { + CollectionSubject, + SubjectLink, + SubjectLinkKind, +} from "./subject-linkage-types"; + +describe("SubjectLinkKind", () => { + it("accepts all valid kinds", () => { + const kinds: SubjectLinkKind[] = ["self", "owner", "reference"]; + expect(kinds).toHaveLength(3); + }); +}); + +describe("SubjectLink type safety", () => { + it("accepts a minimal self-link", () => { + const link: SubjectLink = { field: "id", kind: "self" }; + expect(link.field).toBe("id"); + expect(link.kind).toBe("self"); + expect(link.target).toBeUndefined(); + expect(link.role).toBeUndefined(); + }); + + it("accepts a reference link with target and role", () => { + const link: SubjectLink = { + field: "createdBy", + kind: "reference", + target: "users", + role: "author", + }; + expect(link.target).toBe("users"); + expect(link.role).toBe("author"); + }); + + it("accepts an owner link with target", () => { + const link: SubjectLink = { + field: "userId", + kind: "owner", + target: "users", + }; + expect(link.kind).toBe("owner"); + expect(link.target).toBe("users"); + }); + + it("rejects a SubjectLink missing required field at compile time", () => { + // @ts-expect-error — 'field' is required + const _missing: SubjectLink = { kind: "self" }; + void _missing; + }); + + it("rejects a SubjectLink missing required kind at compile time", () => { + // @ts-expect-error — 'kind' is required + const _missing: SubjectLink = { field: "id" }; + void _missing; + }); +}); + +describe("CollectionSubject", () => { + it("is assignable from a SubjectLink", () => { + const subject: CollectionSubject = { field: "id", kind: "self" }; + expect(subject.kind).toBe("self"); + }); + + it("accepts an array of CollectionSubject entries for multi-linkage", () => { + const subjects: CollectionSubject[] = [ + { field: "id", kind: "self" }, + { field: "authorId", kind: "owner", target: "users" }, + ]; + expect(subjects).toHaveLength(2); + expect(subjects[0]?.kind).toBe("self"); + expect(subjects[1]?.kind).toBe("owner"); + }); +}); diff --git a/packages/core-shared/src/payload/subject-linkage-types.ts b/packages/core-shared/src/payload/subject-linkage-types.ts new file mode 100644 index 0000000..d8e39ea --- /dev/null +++ b/packages/core-shared/src/payload/subject-linkage-types.ts @@ -0,0 +1,10 @@ +export type SubjectLinkKind = "self" | "owner" | "reference"; + +export type SubjectLink = { + field: string; + kind: SubjectLinkKind; + target?: string; + role?: string; +}; + +export type CollectionSubject = SubjectLink;