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:
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"generatedAt": "2026-05-18T20:21:48.917Z",
|
"generatedAt": "2026-05-19T10:10:24.367Z",
|
||||||
"commit": "188625b",
|
"commit": "5abf7fe",
|
||||||
"repo": {
|
"repo": {
|
||||||
"statements": 96.47,
|
"statements": 96.48,
|
||||||
"branches": 91.71,
|
"branches": 91.72,
|
||||||
"functions": 96.81,
|
"functions": 96.83,
|
||||||
"lines": 96.47,
|
"lines": 96.48,
|
||||||
"counts": {
|
"counts": {
|
||||||
"lf": 4254,
|
"lf": 4264,
|
||||||
"lh": 4104,
|
"lh": 4114,
|
||||||
"brf": 808,
|
"brf": 809,
|
||||||
"brh": 741,
|
"brh": 742,
|
||||||
"fnf": 251,
|
"fnf": 252,
|
||||||
"fnh": 243
|
"fnh": 244
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"byPackage": {
|
"byPackage": {
|
||||||
@@ -59,17 +59,17 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@repo/core-shared": {
|
"@repo/core-shared": {
|
||||||
"statements": 97.98,
|
"statements": 98,
|
||||||
"branches": 95.79,
|
"branches": 95.81,
|
||||||
"functions": 91.92,
|
"functions": 92,
|
||||||
"lines": 97.98,
|
"lines": 98,
|
||||||
"counts": {
|
"counts": {
|
||||||
"lf": 1040,
|
"lf": 1050,
|
||||||
"lh": 1019,
|
"lh": 1029,
|
||||||
"brf": 309,
|
"brf": 310,
|
||||||
"brh": 296,
|
"brh": 297,
|
||||||
"fnf": 99,
|
"fnf": 100,
|
||||||
"fnh": 91
|
"fnh": 92
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@repo/marketing-pages": {
|
"@repo/marketing-pages": {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export const users: CollectionConfig = {
|
|||||||
action: "hard-delete",
|
action: "hard-delete",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
subject: { kind: "self", field: "id" },
|
||||||
},
|
},
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -14,6 +14,11 @@ export type {
|
|||||||
} from "./pii-types";
|
} from "./pii-types";
|
||||||
export { PAYLOAD_AUTH_PII_DEFAULTS } from "./pii-types";
|
export { PAYLOAD_AUTH_PII_DEFAULTS } from "./pii-types";
|
||||||
export type { PurgeSchedule, CollectionRetention } from "./retention-types";
|
export type { PurgeSchedule, CollectionRetention } from "./retention-types";
|
||||||
|
export type {
|
||||||
|
SubjectLinkKind,
|
||||||
|
SubjectLink,
|
||||||
|
CollectionSubject,
|
||||||
|
} from "./subject-linkage-types";
|
||||||
export {
|
export {
|
||||||
parseDurationMs,
|
parseDurationMs,
|
||||||
scheduleDelayMs,
|
scheduleDelayMs,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { FieldPii } from "./pii-types";
|
import type { FieldPii } from "./pii-types";
|
||||||
import type { CollectionRetention } from "./retention-types";
|
import type { CollectionRetention } from "./retention-types";
|
||||||
|
import type { CollectionSubject } from "./subject-linkage-types";
|
||||||
|
|
||||||
declare module "payload" {
|
declare module "payload" {
|
||||||
// FieldBase.custom is typed as FieldCustom (interface extending Record<string, any>).
|
// FieldBase.custom is typed as FieldCustom (interface extending Record<string, any>).
|
||||||
@@ -12,5 +13,6 @@ declare module "payload" {
|
|||||||
interface CollectionCustom {
|
interface CollectionCustom {
|
||||||
retention?: CollectionRetention;
|
retention?: CollectionRetention;
|
||||||
authPii?: Record<string, FieldPii | null>;
|
authPii?: Record<string, FieldPii | null>;
|
||||||
|
subject?: CollectionSubject | CollectionSubject[];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ const CREDENTIAL_FIELDS = [
|
|||||||
"apiKeyIndex",
|
"apiKeyIndex",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
const DSR_MANAGED_FIELDS = ["processingRestrictedAt", "consentState"] as const;
|
||||||
|
|
||||||
describe("FieldPii type safety", () => {
|
describe("FieldPii type safety", () => {
|
||||||
it("rejects FieldPii missing required fields at compile time", () => {
|
it("rejects FieldPii missing required fields at compile time", () => {
|
||||||
// @ts-expect-error — 'purpose', 'exportable', 'restrictable' are required
|
// @ts-expect-error — 'purpose', 'exportable', 'restrictable' are required
|
||||||
@@ -88,8 +90,14 @@ describe("PAYLOAD_AUTH_PII_DEFAULTS", () => {
|
|||||||
expect(emailPii?.restrictable).toBe(true);
|
expect(emailPii?.restrictable).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("has exactly 10 keys: email plus 9 credential fields", () => {
|
it("has exactly 12 keys: email, 9 credential fields, and 2 DSR-managed fields", () => {
|
||||||
expect(Object.keys(PAYLOAD_AUTH_PII_DEFAULTS)).toHaveLength(10);
|
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)", () => {
|
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,
|
lockUntil: null,
|
||||||
apiKey: null,
|
apiKey: null,
|
||||||
apiKeyIndex: 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