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.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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<string, any>).
|
||||
@@ -12,5 +13,6 @@ declare module "payload" {
|
||||
interface CollectionCustom {
|
||||
retention?: CollectionRetention;
|
||||
authPii?: Record<string, FieldPii | null>;
|
||||
subject?: CollectionSubject | CollectionSubject[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)", () => {
|
||||
|
||||
@@ -61,4 +61,6 @@ export const PAYLOAD_AUTH_PII_DEFAULTS: Record<string, FieldPii | null> = {
|
||||
lockUntil: null,
|
||||
apiKey: null,
|
||||
apiKeyIndex: null,
|
||||
processingRestrictedAt: null,
|
||||
consentState: null,
|
||||
};
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
10
packages/core-shared/src/payload/subject-linkage-types.ts
Normal file
10
packages/core-shared/src/payload/subject-linkage-types.ts
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user