feat(housekeeping): define typed poll query contract
This commit is contained in:
1 parent
e69633b753
commit
cbc3075e5a
4 files changed
+432
-8
No files matched your search
@@ -92,5 +92,8 @@ export function parseContentListInput(
|
|||||||
status: text(searchParams.status, 32).toLocaleLowerCase(),
|
status: text(searchParams.status, 32).toLocaleLowerCase(),
|
||||||
pageSize: bounded(searchParams.pageSize, 25, 1, 100),
|
pageSize: bounded(searchParams.pageSize, 25, 1, 100),
|
||||||
offset: bounded(searchParams.offset, 0, 0, 100_000),
|
offset: bounded(searchParams.offset, 0, 0, 100_000),
|
||||||
|
responseQuestionId: text(searchParams.responseQuestionId, 32),
|
||||||
|
responsePageSize: bounded(searchParams.responsePageSize, 25, 1, 50),
|
||||||
|
responseOffset: bounded(searchParams.responseOffset, 0, 0, 100_000),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -482,6 +482,9 @@ describe("Content actionable form wiring", () => {
|
|||||||
status: "published",
|
status: "published",
|
||||||
pageSize: 25,
|
pageSize: 25,
|
||||||
offset: 25,
|
offset: 25,
|
||||||
|
responseQuestionId: "",
|
||||||
|
responsePageSize: 25,
|
||||||
|
responseOffset: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
|
|||||||
@@ -57,6 +57,35 @@ const ready: ContentQueryData = {
|
|||||||
partialDependencies: [],
|
partialDependencies: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const pollDetail = {
|
||||||
|
kind: "poll-detail" as const,
|
||||||
|
description: "Complete poll",
|
||||||
|
showResults: true,
|
||||||
|
multipleChoice: false,
|
||||||
|
startsAt: "2026-09-02T18:00:00.000Z",
|
||||||
|
endsAt: null,
|
||||||
|
questionCount: 1,
|
||||||
|
voterCount: 2,
|
||||||
|
answerCount: 2,
|
||||||
|
questions: [
|
||||||
|
{
|
||||||
|
id: "21",
|
||||||
|
question: "Favourite colour?",
|
||||||
|
type: "single" as const,
|
||||||
|
sortOrder: 0,
|
||||||
|
options: ["Red", "Blue"],
|
||||||
|
answerCount: 2,
|
||||||
|
choiceResults: [{ option: "Red", count: 2 }],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
textResponses: {
|
||||||
|
questionId: null,
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
pageSize: 25,
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
describe("Content query", () => {
|
describe("Content query", () => {
|
||||||
beforeEach(() => queryMocks.execute.mockReset());
|
beforeEach(() => queryMocks.execute.mockReset());
|
||||||
|
|
||||||
@@ -355,7 +384,14 @@ describe("Content query", () => {
|
|||||||
expect(load).toHaveBeenCalledWith({
|
expect(load).toHaveBeenCalledWith({
|
||||||
routeId: "content.editorial.articles",
|
routeId: "content.editorial.articles",
|
||||||
params: {},
|
params: {},
|
||||||
list: { search: "launch", pageSize: 100, offset: 100_000 },
|
list: {
|
||||||
|
search: "launch",
|
||||||
|
pageSize: 100,
|
||||||
|
offset: 100_000,
|
||||||
|
responseQuestionId: "",
|
||||||
|
responsePageSize: 25,
|
||||||
|
responseOffset: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
ok: true,
|
ok: true,
|
||||||
@@ -367,6 +403,146 @@ describe("Content query", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("normalizes bounded poll response-page input before invoking the adapter", async () => {
|
||||||
|
const load = vi.fn(async () => ({
|
||||||
|
kind: "engagement" as const,
|
||||||
|
items: [],
|
||||||
|
total: 0,
|
||||||
|
partialDependencies: [],
|
||||||
|
}));
|
||||||
|
const query = createContentQuery({ load });
|
||||||
|
|
||||||
|
await query.run(context([PERMS.POLLS_VIEW, PERMS.POLLS_EDIT]), {
|
||||||
|
routeId: "content.engagement.poll-detail",
|
||||||
|
params: { id: "7" },
|
||||||
|
list: {
|
||||||
|
responseQuestionId: " 22 ",
|
||||||
|
responsePageSize: 50,
|
||||||
|
responseOffset: 100_000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(load).toHaveBeenCalledWith({
|
||||||
|
routeId: "content.engagement.poll-detail",
|
||||||
|
params: { id: "7" },
|
||||||
|
list: {
|
||||||
|
search: "",
|
||||||
|
pageSize: 25,
|
||||||
|
offset: 0,
|
||||||
|
responseQuestionId: "22",
|
||||||
|
responsePageSize: 50,
|
||||||
|
responseOffset: 100_000,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["mismatched id", "8", pollDetail],
|
||||||
|
[
|
||||||
|
"too many questions",
|
||||||
|
"7",
|
||||||
|
{ ...pollDetail, questions: Array(101).fill(pollDetail.questions[0]) },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"invalid response user",
|
||||||
|
"7",
|
||||||
|
{
|
||||||
|
...pollDetail,
|
||||||
|
textResponses: {
|
||||||
|
questionId: "22",
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "1",
|
||||||
|
questionId: "22",
|
||||||
|
userId: "0",
|
||||||
|
username: null,
|
||||||
|
answer: "Text",
|
||||||
|
createdAt: "2026-09-02T18:00:00.000Z",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 1,
|
||||||
|
pageSize: 25,
|
||||||
|
offset: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
] as const)(
|
||||||
|
"fails closed for poll detail with %s",
|
||||||
|
async (_label, itemId, payload) => {
|
||||||
|
const query = createContentQuery({
|
||||||
|
load: async () => ({
|
||||||
|
kind: "engagement" as const,
|
||||||
|
items: [{ id: itemId, title: "Poll", privatePayload: payload }],
|
||||||
|
total: 1,
|
||||||
|
partialDependencies: [],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const result = await query.run(
|
||||||
|
context([PERMS.POLLS_VIEW, PERMS.POLLS_EDIT]),
|
||||||
|
{
|
||||||
|
routeId: "content.engagement.poll-detail",
|
||||||
|
params: { id: "7" },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: { code: "DEPENDENCY_UNAVAILABLE" },
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it("fails closed when poll creation does not return the exact create payload", async () => {
|
||||||
|
const query = createContentQuery({
|
||||||
|
load: async () => ({
|
||||||
|
kind: "engagement" as const,
|
||||||
|
items: [
|
||||||
|
{ id: "7", title: "Poll", privatePayload: { kind: "poll-create" } },
|
||||||
|
],
|
||||||
|
total: 1,
|
||||||
|
partialDependencies: [],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
await query.run(context([PERMS.POLLS_EDIT]), {
|
||||||
|
routeId: "content.engagement.poll-create",
|
||||||
|
}),
|
||||||
|
).toMatchObject({
|
||||||
|
ok: false,
|
||||||
|
error: { code: "DEPENDENCY_UNAVAILABLE" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts an empty poll response page past its total", async () => {
|
||||||
|
const query = createContentQuery({
|
||||||
|
load: async () => ({
|
||||||
|
kind: "engagement" as const,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: "7",
|
||||||
|
title: "Poll",
|
||||||
|
privatePayload: {
|
||||||
|
...pollDetail,
|
||||||
|
textResponses: {
|
||||||
|
questionId: null,
|
||||||
|
items: [],
|
||||||
|
total: 1,
|
||||||
|
pageSize: 25,
|
||||||
|
offset: 100_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
total: 1,
|
||||||
|
partialDependencies: [],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(
|
||||||
|
await query.run(context([PERMS.POLLS_VIEW, PERMS.POLLS_EDIT]), {
|
||||||
|
routeId: "content.engagement.poll-detail",
|
||||||
|
params: { id: "7" },
|
||||||
|
}),
|
||||||
|
).toMatchObject({ ok: true });
|
||||||
|
});
|
||||||
it("fails closed when an article detail payload is incomplete", async () => {
|
it("fails closed when an article detail payload is incomplete", async () => {
|
||||||
const query = createContentQuery({
|
const query = createContentQuery({
|
||||||
load: async () => ({
|
load: async () => ({
|
||||||
@@ -417,6 +593,9 @@ describe("Content query", () => {
|
|||||||
status: `published${"x".repeat(23)}`,
|
status: `published${"x".repeat(23)}`,
|
||||||
pageSize: 25,
|
pageSize: 25,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
|
responseQuestionId: "",
|
||||||
|
responsePageSize: 25,
|
||||||
|
responseOffset: 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -488,19 +667,31 @@ describe("Content query", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("has an authorized production query path for every Content route", async () => {
|
it("has an authorized production query path for every Content route", async () => {
|
||||||
const load = vi.fn(async (input: ContentQueryInput) => ({
|
const load = vi.fn(async (input: ContentQueryInput) => {
|
||||||
kind: input.routeId.split(".")[1] as ContentQueryData["kind"],
|
const isPollCreate = input.routeId === "content.engagement.poll-create";
|
||||||
items: [],
|
return {
|
||||||
total: 0,
|
kind: input.routeId.split(".")[1] as ContentQueryData["kind"],
|
||||||
partialDependencies: [],
|
items: isPollCreate
|
||||||
}));
|
? [
|
||||||
|
{
|
||||||
|
id: "create",
|
||||||
|
title: "Create poll",
|
||||||
|
privatePayload: { kind: "poll-create" },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [],
|
||||||
|
total: isPollCreate ? 1 : 0,
|
||||||
|
partialDependencies: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
const query = createContentQuery({ load });
|
const query = createContentQuery({ load });
|
||||||
const all = context(Object.values(PERMS));
|
const all = context(Object.values(PERMS));
|
||||||
|
|
||||||
for (const routeId of CONTENT_ROUTE_IDS) {
|
for (const routeId of CONTENT_ROUTE_IDS) {
|
||||||
const result = await query.run(all, {
|
const result = await query.run(all, {
|
||||||
routeId,
|
routeId,
|
||||||
...(routeId === "content.engagement.event-detail"
|
...(routeId === "content.engagement.event-detail" ||
|
||||||
|
routeId === "content.engagement.poll-detail"
|
||||||
? { params: { id: "1" } }
|
? { params: { id: "1" } }
|
||||||
: {}),
|
: {}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
POLL_QUESTION_TYPES,
|
||||||
|
type PollQuestionType,
|
||||||
|
} from "@/lib/polls/poll-semantics";
|
||||||
import { authorizeHousekeeping } from "../../../foundation/authorization";
|
import { authorizeHousekeeping } from "../../../foundation/authorization";
|
||||||
import {
|
import {
|
||||||
fail,
|
fail,
|
||||||
@@ -17,6 +21,9 @@ export interface ContentQueryListInput {
|
|||||||
readonly status?: string;
|
readonly status?: string;
|
||||||
readonly pageSize?: number;
|
readonly pageSize?: number;
|
||||||
readonly offset?: number;
|
readonly offset?: number;
|
||||||
|
readonly responseQuestionId?: string;
|
||||||
|
readonly responsePageSize?: number;
|
||||||
|
readonly responseOffset?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ContentQueryInput {
|
export interface ContentQueryInput {
|
||||||
@@ -248,6 +255,183 @@ export function isContentEventDetailPayload(
|
|||||||
payload.registrations.every(isContentEventRegistrationPayload)
|
payload.registrations.every(isContentEventRegistrationPayload)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export type ContentPollQuestionType = PollQuestionType;
|
||||||
|
|
||||||
|
export interface ContentPollCreatePayload {
|
||||||
|
readonly kind: "poll-create";
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentPollSummaryPayload {
|
||||||
|
readonly kind: "poll-summary";
|
||||||
|
readonly showResults: boolean;
|
||||||
|
readonly multipleChoice: boolean;
|
||||||
|
readonly startsAt: string | null;
|
||||||
|
readonly endsAt: string | null;
|
||||||
|
readonly questionCount: number;
|
||||||
|
readonly voterCount: number;
|
||||||
|
readonly answerCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentPollChoiceResultPayload {
|
||||||
|
readonly option: string;
|
||||||
|
readonly count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentPollQuestionPayload {
|
||||||
|
readonly id: string;
|
||||||
|
readonly question: string;
|
||||||
|
readonly type: ContentPollQuestionType;
|
||||||
|
readonly sortOrder: number;
|
||||||
|
readonly options: readonly string[];
|
||||||
|
readonly answerCount: number;
|
||||||
|
readonly choiceResults: readonly ContentPollChoiceResultPayload[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentPollTextResponsePayload {
|
||||||
|
readonly id: string;
|
||||||
|
readonly questionId: string;
|
||||||
|
readonly userId: string;
|
||||||
|
readonly username: string | null;
|
||||||
|
readonly answer: string;
|
||||||
|
readonly createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentPollTextResponsePagePayload {
|
||||||
|
readonly questionId: string | null;
|
||||||
|
readonly items: readonly ContentPollTextResponsePayload[];
|
||||||
|
readonly total: number;
|
||||||
|
readonly pageSize: number;
|
||||||
|
readonly offset: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContentPollDetailPayload {
|
||||||
|
readonly kind: "poll-detail";
|
||||||
|
readonly description: string;
|
||||||
|
readonly showResults: boolean;
|
||||||
|
readonly multipleChoice: boolean;
|
||||||
|
readonly startsAt: string | null;
|
||||||
|
readonly endsAt: string | null;
|
||||||
|
readonly questionCount: number;
|
||||||
|
readonly voterCount: number;
|
||||||
|
readonly answerCount: number;
|
||||||
|
readonly questions: readonly ContentPollQuestionPayload[];
|
||||||
|
readonly textResponses: ContentPollTextResponsePagePayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isContentPollQuestionPayload(
|
||||||
|
value: unknown,
|
||||||
|
): value is ContentPollQuestionPayload {
|
||||||
|
const payload = object(value);
|
||||||
|
const options = payload?.options;
|
||||||
|
const choiceResults = payload?.choiceResults;
|
||||||
|
return (
|
||||||
|
positiveIdentifier(payload?.id) &&
|
||||||
|
typeof payload.question === "string" &&
|
||||||
|
typeof payload.type === "string" &&
|
||||||
|
POLL_QUESTION_TYPES.includes(payload.type as ContentPollQuestionType) &&
|
||||||
|
safeCount(payload.sortOrder) &&
|
||||||
|
Array.isArray(options) &&
|
||||||
|
options.length <= 100 &&
|
||||||
|
options.every((option) => typeof option === "string") &&
|
||||||
|
safeCount(payload.answerCount) &&
|
||||||
|
Array.isArray(choiceResults) &&
|
||||||
|
choiceResults.length <= 100 &&
|
||||||
|
choiceResults.every((choice) => {
|
||||||
|
const result = object(choice);
|
||||||
|
return (
|
||||||
|
typeof result?.option === "string" &&
|
||||||
|
options.includes(result.option) &&
|
||||||
|
safeCount(result.count)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isContentPollTextResponsePayload(
|
||||||
|
value: unknown,
|
||||||
|
): value is ContentPollTextResponsePayload {
|
||||||
|
const payload = object(value);
|
||||||
|
return (
|
||||||
|
positiveIdentifier(payload?.id) &&
|
||||||
|
positiveIdentifier(payload.questionId) &&
|
||||||
|
positiveIdentifier(payload.userId) &&
|
||||||
|
stringOrNull(payload.username) &&
|
||||||
|
typeof payload.answer === "string" &&
|
||||||
|
canonicalDate(payload.createdAt)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isContentPollCreatePayload(
|
||||||
|
value: unknown,
|
||||||
|
): value is ContentPollCreatePayload {
|
||||||
|
return object(value)?.kind === "poll-create";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isContentPollSummaryPayload(
|
||||||
|
value: unknown,
|
||||||
|
): value is ContentPollSummaryPayload {
|
||||||
|
const payload = object(value);
|
||||||
|
return (
|
||||||
|
payload?.kind === "poll-summary" &&
|
||||||
|
typeof payload.showResults === "boolean" &&
|
||||||
|
typeof payload.multipleChoice === "boolean" &&
|
||||||
|
dateOrNull(payload.startsAt) &&
|
||||||
|
dateOrNull(payload.endsAt) &&
|
||||||
|
safeCount(payload.questionCount) &&
|
||||||
|
safeCount(payload.voterCount) &&
|
||||||
|
safeCount(payload.answerCount)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isContentPollDetailPayload(
|
||||||
|
value: unknown,
|
||||||
|
): value is ContentPollDetailPayload {
|
||||||
|
const payload = object(value);
|
||||||
|
if (
|
||||||
|
payload?.kind !== "poll-detail" ||
|
||||||
|
typeof payload.description !== "string" ||
|
||||||
|
typeof payload.showResults !== "boolean" ||
|
||||||
|
typeof payload.multipleChoice !== "boolean" ||
|
||||||
|
!dateOrNull(payload.startsAt) ||
|
||||||
|
!dateOrNull(payload.endsAt) ||
|
||||||
|
!safeCount(payload.questionCount) ||
|
||||||
|
!safeCount(payload.voterCount) ||
|
||||||
|
!safeCount(payload.answerCount) ||
|
||||||
|
!Array.isArray(payload.questions) ||
|
||||||
|
payload.questions.length > 100 ||
|
||||||
|
!payload.questions.every(isContentPollQuestionPayload) ||
|
||||||
|
payload.questionCount !== payload.questions.length
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
const responses = object(payload.textResponses);
|
||||||
|
if (
|
||||||
|
!responses ||
|
||||||
|
(responses.questionId !== null &&
|
||||||
|
!positiveIdentifier(responses.questionId)) ||
|
||||||
|
!Array.isArray(responses.items) ||
|
||||||
|
responses.items.length > 50 ||
|
||||||
|
!safeCount(responses.total) ||
|
||||||
|
!positiveCount(responses.pageSize) ||
|
||||||
|
responses.pageSize > 50 ||
|
||||||
|
!safeCount(responses.offset) ||
|
||||||
|
responses.items.length > responses.pageSize ||
|
||||||
|
responses.total < responses.items.length ||
|
||||||
|
!responses.items.every(isContentPollTextResponsePayload)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (responses.questionId === null) return responses.items.length === 0;
|
||||||
|
return (
|
||||||
|
payload.questions.some(
|
||||||
|
(question) =>
|
||||||
|
question.id === responses.questionId && question.type === "text",
|
||||||
|
) &&
|
||||||
|
responses.items.every(
|
||||||
|
(response) => response.questionId === responses.questionId,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export interface ContentQueryItem {
|
export interface ContentQueryItem {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
@@ -274,6 +458,9 @@ export interface NormalizedContentQueryInput {
|
|||||||
status?: string;
|
status?: string;
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
offset: number;
|
offset: number;
|
||||||
|
responseQuestionId?: string;
|
||||||
|
responsePageSize?: number;
|
||||||
|
responseOffset?: number;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,6 +501,12 @@ function normalizeInput(input: ContentQueryInput): NormalizedContentQueryInput {
|
|||||||
...(status ? { status } : {}),
|
...(status ? { status } : {}),
|
||||||
pageSize: boundedInteger(input.list?.pageSize, 25, 1, 100),
|
pageSize: boundedInteger(input.list?.pageSize, 25, 1, 100),
|
||||||
offset: boundedInteger(input.list?.offset, 0, 0, 100_000),
|
offset: boundedInteger(input.list?.offset, 0, 0, 100_000),
|
||||||
|
responseQuestionId: String(input.list?.responseQuestionId ?? "")
|
||||||
|
.normalize("NFC")
|
||||||
|
.trim()
|
||||||
|
.slice(0, 32),
|
||||||
|
responsePageSize: boundedInteger(input.list?.responsePageSize, 25, 1, 50),
|
||||||
|
responseOffset: boundedInteger(input.list?.responseOffset, 0, 0, 100_000),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -390,6 +583,40 @@ function isValidData(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (input.routeId === "content.engagement.polls") {
|
||||||
|
return (
|
||||||
|
data.items.length <= input.list.pageSize &&
|
||||||
|
data.items.length <= data.total &&
|
||||||
|
data.items.every(
|
||||||
|
(item) =>
|
||||||
|
positiveIdentifier(item.id) &&
|
||||||
|
isContentPollSummaryPayload(item.privatePayload),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (input.routeId === "content.engagement.poll-create") {
|
||||||
|
return (
|
||||||
|
data.total === 1 &&
|
||||||
|
data.items.length === 1 &&
|
||||||
|
data.items[0]?.id === "create" &&
|
||||||
|
isContentPollCreatePayload(data.items[0]?.privatePayload)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (input.routeId === "content.engagement.poll-detail") {
|
||||||
|
const requestedId = input.params.id;
|
||||||
|
return (
|
||||||
|
positiveIdentifier(requestedId) &&
|
||||||
|
data.items.length <= 1 &&
|
||||||
|
data.total === data.items.length &&
|
||||||
|
data.items.every(
|
||||||
|
(item) =>
|
||||||
|
item.id === requestedId &&
|
||||||
|
positiveIdentifier(item.id) &&
|
||||||
|
isContentPollDetailPayload(item.privatePayload),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user