Add consent records tab to cookie banner config
Exposes the cookie consent record audit trail through a new "Consent Records" tab on the cookie banner configuration page. The full stack includes: extended coredata filter (visitor ID, banner version), GraphQL schema/types/resolvers, and a React page with SortableTable (size 50) and three compliance filters (action, visitor ID, banner version). Signed-off-by: Émile Ré <emile@getprobo.com>
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
// PERFORMANCE OF THIS SOFTWARE.
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { ClipboardTextIcon } from "@phosphor-icons/react";
|
||||||
import { formatError, type GraphQLError } from "@probo/helpers";
|
import { formatError, type GraphQLError } from "@probo/helpers";
|
||||||
import { useTranslate } from "@probo/i18n";
|
import { useTranslate } from "@probo/i18n";
|
||||||
import {
|
import {
|
||||||
@@ -251,6 +252,10 @@ export default function CookieBannerConfigLayout({ queryRef }: CookieBannerConfi
|
|||||||
<IconListStack size={20} />
|
<IconListStack size={20} />
|
||||||
{__("Cookies")}
|
{__("Cookies")}
|
||||||
</TabLink>
|
</TabLink>
|
||||||
|
<TabLink to={`/organizations/${organizationId}/cookie-banners/${cookieBannerId}/consent-records`}>
|
||||||
|
<ClipboardTextIcon size={20} />
|
||||||
|
{__("Consent Records")}
|
||||||
|
</TabLink>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
@@ -0,0 +1,230 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Input,
|
||||||
|
Option,
|
||||||
|
Select,
|
||||||
|
Tbody,
|
||||||
|
Th,
|
||||||
|
Thead,
|
||||||
|
Tr,
|
||||||
|
} from "@probo/ui";
|
||||||
|
import { type ComponentProps, useState, useTransition } from "react";
|
||||||
|
import {
|
||||||
|
graphql,
|
||||||
|
type PreloadedQuery,
|
||||||
|
usePaginationFragment,
|
||||||
|
usePreloadedQuery,
|
||||||
|
} from "react-relay";
|
||||||
|
|
||||||
|
import type { CookieBannerConsentRecordsPageFragment$key } from "#/__generated__/core/CookieBannerConsentRecordsPageFragment.graphql";
|
||||||
|
import type { CookieBannerConsentRecordsPageQuery } from "#/__generated__/core/CookieBannerConsentRecordsPageQuery.graphql";
|
||||||
|
import type {
|
||||||
|
CookieBannerConsentRecordsPageRefetchQuery,
|
||||||
|
CookieConsentAction,
|
||||||
|
CookieConsentRecordOrderField,
|
||||||
|
} from "#/__generated__/core/CookieBannerConsentRecordsPageRefetchQuery.graphql";
|
||||||
|
import { SortableTable, SortableTh } from "#/components/SortableTable";
|
||||||
|
|
||||||
|
import { ConsentRecordRow } from "./_components/ConsentRecordRow";
|
||||||
|
|
||||||
|
export const cookieBannerConsentRecordsPageQuery = graphql`
|
||||||
|
query CookieBannerConsentRecordsPageQuery($cookieBannerId: ID!) {
|
||||||
|
node(id: $cookieBannerId) @required(action: THROW) {
|
||||||
|
__typename
|
||||||
|
... on CookieBanner {
|
||||||
|
...CookieBannerConsentRecordsPageFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const consentRecordsFragment = graphql`
|
||||||
|
fragment CookieBannerConsentRecordsPageFragment on CookieBanner
|
||||||
|
@refetchable(queryName: "CookieBannerConsentRecordsPageRefetchQuery")
|
||||||
|
@argumentDefinitions(
|
||||||
|
first: { type: "Int", defaultValue: 50 }
|
||||||
|
order: { type: "CookieConsentRecordOrder", defaultValue: null }
|
||||||
|
after: { type: "CursorKey", defaultValue: null }
|
||||||
|
before: { type: "CursorKey", defaultValue: null }
|
||||||
|
last: { type: "Int", defaultValue: null }
|
||||||
|
action: { type: "CookieConsentAction", defaultValue: null }
|
||||||
|
visitorId: { type: "String", defaultValue: null }
|
||||||
|
cookieBannerVersionId: { type: "ID", defaultValue: null }
|
||||||
|
) {
|
||||||
|
consentRecords(
|
||||||
|
first: $first
|
||||||
|
after: $after
|
||||||
|
last: $last
|
||||||
|
before: $before
|
||||||
|
orderBy: $order
|
||||||
|
filter: {
|
||||||
|
action: $action
|
||||||
|
visitorId: $visitorId
|
||||||
|
cookieBannerVersionId: $cookieBannerVersionId
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@connection(
|
||||||
|
key: "CookieBannerConsentRecordsPage_consentRecords"
|
||||||
|
filters: ["filter"]
|
||||||
|
) @required(action: THROW) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
id
|
||||||
|
...ConsentRecordRowFragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface CookieBannerConsentRecordsPageProps {
|
||||||
|
queryRef: PreloadedQuery<CookieBannerConsentRecordsPageQuery>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function CookieBannerConsentRecordsPage({
|
||||||
|
queryRef,
|
||||||
|
}: CookieBannerConsentRecordsPageProps) {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
const data = usePreloadedQuery(cookieBannerConsentRecordsPageQuery, queryRef);
|
||||||
|
|
||||||
|
if (data.node.__typename !== "CookieBanner") {
|
||||||
|
throw new Error("invalid type for node");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [isPending, startTransition] = useTransition();
|
||||||
|
const [actionFilter, setActionFilter] = useState<CookieConsentAction | null>(null);
|
||||||
|
const [visitorIdFilter, setVisitorIdFilter] = useState<string>("");
|
||||||
|
const [versionIdFilter, setVersionIdFilter] = useState<string>("");
|
||||||
|
|
||||||
|
const { data: fragmentData, ...pagination } = usePaginationFragment<
|
||||||
|
CookieBannerConsentRecordsPageRefetchQuery,
|
||||||
|
CookieBannerConsentRecordsPageFragment$key
|
||||||
|
>(consentRecordsFragment, data.node);
|
||||||
|
|
||||||
|
const records = fragmentData.consentRecords.edges.map(edge => edge.node) ?? [];
|
||||||
|
|
||||||
|
const refetchFilters = (overrides: Record<string, unknown> = {}) => {
|
||||||
|
startTransition(() => {
|
||||||
|
pagination.refetch(
|
||||||
|
{
|
||||||
|
action: actionFilter,
|
||||||
|
visitorId: visitorIdFilter || null,
|
||||||
|
cookieBannerVersionId: versionIdFilter || null,
|
||||||
|
...overrides,
|
||||||
|
},
|
||||||
|
{ fetchPolicy: "network-only" },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleActionFilterChange = (value: string) => {
|
||||||
|
const newAction = value === "ALL" ? null : (value as CookieConsentAction);
|
||||||
|
setActionFilter(newAction);
|
||||||
|
refetchFilters({ action: newAction });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVisitorIdSubmit = () => {
|
||||||
|
refetchFilters({ visitorId: visitorIdFilter || null });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleVersionIdSubmit = () => {
|
||||||
|
refetchFilters({ cookieBannerVersionId: versionIdFilter || null });
|
||||||
|
};
|
||||||
|
|
||||||
|
const refetchWithFilters: ComponentProps<typeof SortableTable>["refetch"] = ({ order }) => {
|
||||||
|
pagination.refetch({
|
||||||
|
order: { direction: order.direction, field: order.field as CookieConsentRecordOrderField },
|
||||||
|
action: actionFilter,
|
||||||
|
visitorId: visitorIdFilter || null,
|
||||||
|
cookieBannerVersionId: versionIdFilter || null,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Select
|
||||||
|
value={actionFilter ?? "ALL"}
|
||||||
|
onValueChange={handleActionFilterChange}
|
||||||
|
>
|
||||||
|
<Option value="ALL">{__("All actions")}</Option>
|
||||||
|
<Option value="ACCEPT_ALL">{__("Accept All")}</Option>
|
||||||
|
<Option value="REJECT_ALL">{__("Reject All")}</Option>
|
||||||
|
<Option value="CUSTOMIZE">{__("Customize")}</Option>
|
||||||
|
<Option value="GPC">{__("GPC")}</Option>
|
||||||
|
</Select>
|
||||||
|
<Input
|
||||||
|
placeholder={__("Visitor ID")}
|
||||||
|
value={visitorIdFilter}
|
||||||
|
onChange={e => setVisitorIdFilter(e.target.value)}
|
||||||
|
onKeyDown={e => e.key === "Enter" && handleVisitorIdSubmit()}
|
||||||
|
onBlur={handleVisitorIdSubmit}
|
||||||
|
className="w-48"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder={__("Version ID")}
|
||||||
|
value={versionIdFilter}
|
||||||
|
onChange={e => setVersionIdFilter(e.target.value)}
|
||||||
|
onKeyDown={e => e.key === "Enter" && handleVersionIdSubmit()}
|
||||||
|
onBlur={handleVersionIdSubmit}
|
||||||
|
className="w-48"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={isPending ? "opacity-50 pointer-events-none transition-opacity" : ""}>
|
||||||
|
{records.length > 0
|
||||||
|
? (
|
||||||
|
<SortableTable
|
||||||
|
{...pagination}
|
||||||
|
refetch={refetchWithFilters}
|
||||||
|
pageSize={50}
|
||||||
|
>
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th>{__("Visitor ID")}</Th>
|
||||||
|
<Th>{__("Action")}</Th>
|
||||||
|
<Th>{__("Version")}</Th>
|
||||||
|
<Th>{__("IP Address")}</Th>
|
||||||
|
<Th>{__("SDK Version")}</Th>
|
||||||
|
<Th>{__("Consent Data")}</Th>
|
||||||
|
<SortableTh field="CREATED_AT">{__("Date")}</SortableTh>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{records.map(record => (
|
||||||
|
<ConsentRecordRow key={record.id} recordKey={record} />
|
||||||
|
))}
|
||||||
|
</Tbody>
|
||||||
|
</SortableTable>
|
||||||
|
)
|
||||||
|
: (
|
||||||
|
<Card padded>
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<h3 className="text-lg font-semibold mb-2">
|
||||||
|
{__("No consent records")}
|
||||||
|
</h3>
|
||||||
|
<p className="text-txt-tertiary">
|
||||||
|
{__("Consent records will appear here once visitors interact with your cookie banner.")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { Suspense, useEffect } from "react";
|
||||||
|
import { useQueryLoader } from "react-relay";
|
||||||
|
import { useParams } from "react-router";
|
||||||
|
|
||||||
|
import type { CookieBannerConsentRecordsPageQuery } from "#/__generated__/core/CookieBannerConsentRecordsPageQuery.graphql";
|
||||||
|
|
||||||
|
import CookieBannerConsentRecordsPage, { cookieBannerConsentRecordsPageQuery } from "./CookieBannerConsentRecordsPage";
|
||||||
|
import { CookieBannerConsentRecordsPageSkeleton } from "./CookieBannerConsentRecordsPageSkeleton";
|
||||||
|
|
||||||
|
export default function CookieBannerConsentRecordsPageLoader() {
|
||||||
|
const { cookieBannerId } = useParams<{ cookieBannerId: string }>();
|
||||||
|
if (typeof cookieBannerId !== "string") {
|
||||||
|
throw new Error("Missing cookieBannerId parameter");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [queryRef, loadQuery] = useQueryLoader<CookieBannerConsentRecordsPageQuery>(
|
||||||
|
cookieBannerConsentRecordsPageQuery,
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadQuery({ cookieBannerId });
|
||||||
|
}, [loadQuery, cookieBannerId]);
|
||||||
|
|
||||||
|
if (!queryRef) {
|
||||||
|
return <CookieBannerConsentRecordsPageSkeleton />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<CookieBannerConsentRecordsPageSkeleton />}>
|
||||||
|
<CookieBannerConsentRecordsPage queryRef={queryRef} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
export function CookieBannerConsentRecordsPageSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="space-y-4 animate-pulse">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="h-9 w-36 rounded bg-bg-subtle" />
|
||||||
|
<div className="h-9 w-48 rounded bg-bg-subtle" />
|
||||||
|
<div className="h-9 w-40 rounded bg-bg-subtle" />
|
||||||
|
</div>
|
||||||
|
<div className="rounded-lg border border-border-low">
|
||||||
|
<div className="h-10 border-b border-border-low bg-bg-subtle" />
|
||||||
|
{Array.from({ length: 5 }).map((_, i) => (
|
||||||
|
<div key={i} className="h-12 border-b border-border-low last:border-b-0" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
import { formatDate } from "@probo/helpers";
|
||||||
|
import { useTranslate } from "@probo/i18n";
|
||||||
|
import { Badge, Td, Tr } from "@probo/ui";
|
||||||
|
import { graphql, useFragment } from "react-relay";
|
||||||
|
|
||||||
|
import type { ConsentRecordRowFragment$key } from "#/__generated__/core/ConsentRecordRowFragment.graphql";
|
||||||
|
|
||||||
|
const consentRecordFragment = graphql`
|
||||||
|
fragment ConsentRecordRowFragment on CookieConsentRecord {
|
||||||
|
visitorId
|
||||||
|
action
|
||||||
|
cookieBannerVersion {
|
||||||
|
id
|
||||||
|
version
|
||||||
|
}
|
||||||
|
ipAddress
|
||||||
|
sdkVersion
|
||||||
|
consentData
|
||||||
|
createdAt
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function getActionLabel(action: string, __: (s: string) => string): string {
|
||||||
|
switch (action) {
|
||||||
|
case "ACCEPT_ALL":
|
||||||
|
return __("Accept All");
|
||||||
|
case "REJECT_ALL":
|
||||||
|
return __("Reject All");
|
||||||
|
case "CUSTOMIZE":
|
||||||
|
return __("Customize");
|
||||||
|
case "GPC":
|
||||||
|
return __("GPC");
|
||||||
|
default:
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getActionVariant(action: string): "success" | "danger" | "warning" | "neutral" {
|
||||||
|
switch (action) {
|
||||||
|
case "ACCEPT_ALL":
|
||||||
|
return "success";
|
||||||
|
case "REJECT_ALL":
|
||||||
|
return "danger";
|
||||||
|
case "CUSTOMIZE":
|
||||||
|
return "warning";
|
||||||
|
case "GPC":
|
||||||
|
return "neutral";
|
||||||
|
default:
|
||||||
|
return "neutral";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ConsentRecordRowProps {
|
||||||
|
recordKey: ConsentRecordRowFragment$key;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConsentRecordRow({ recordKey }: ConsentRecordRowProps) {
|
||||||
|
const { __ } = useTranslate();
|
||||||
|
const record = useFragment(consentRecordFragment, recordKey);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<span className="font-mono text-sm">{record.visitorId}</span>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<Badge variant={getActionVariant(record.action)}>
|
||||||
|
{getActionLabel(record.action, __)}
|
||||||
|
</Badge>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
{record.cookieBannerVersion
|
||||||
|
? (
|
||||||
|
<span className="font-mono text-sm">
|
||||||
|
v
|
||||||
|
{record.cookieBannerVersion.version}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
: <span className="text-txt-tertiary">-</span>}
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<span className="font-mono text-sm">{record.ipAddress ?? "-"}</span>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<span className="font-mono text-sm">{record.sdkVersion}</span>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<span className="font-mono text-xs max-w-48 truncate block" title={record.consentData}>
|
||||||
|
{record.consentData}
|
||||||
|
</span>
|
||||||
|
</Td>
|
||||||
|
<Td>
|
||||||
|
<time dateTime={record.createdAt}>
|
||||||
|
{formatDate(record.createdAt)}
|
||||||
|
</time>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -64,6 +64,11 @@ export const cookieBannerRoutes = [
|
|||||||
Fallback: LinkCardSkeleton,
|
Fallback: LinkCardSkeleton,
|
||||||
Component: lazy(() => import("#/pages/organizations/cookie-banners/configuration/display/CookieBannerDisplayPageLoader")),
|
Component: lazy(() => import("#/pages/organizations/cookie-banners/configuration/display/CookieBannerDisplayPageLoader")),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "consent-records",
|
||||||
|
Fallback: LinkCardSkeleton,
|
||||||
|
Component: lazy(() => import("#/pages/organizations/cookie-banners/configuration/consent-records/CookieBannerConsentRecordsPageLoader")),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
] satisfies AppRoute[];
|
] satisfies AppRoute[];
|
||||||
|
|||||||
@@ -7,10 +7,14 @@ Schema-first GraphQL using [gqlgen](https://gqlgen.com/). The schema is hand-wri
|
|||||||
Each API's schema lives in `pkg/server/api/{api}/v1/graphql/` as multiple `.graphql` files, one per coredata model:
|
Each API's schema lives in `pkg/server/api/{api}/v1/graphql/` as multiple `.graphql` files, one per coredata model:
|
||||||
|
|
||||||
- `base.graphql` — directives, scalars, Node interface, PageInfo, OrderDirection, root Query/Mutation/Organization types
|
- `base.graphql` — directives, scalars, Node interface, PageInfo, OrderDirection, root Query/Mutation/Organization types
|
||||||
- Entity files (e.g., `vendor.graphql`, `control.graphql`) — use `extend type Organization`, `extend type Mutation`, etc.
|
- Entity files (e.g., `vendor.graphql`, `control.graphql`) — use `extend type Mutation` to add their mutations.
|
||||||
|
|
||||||
gqlgen's `follow-schema` layout generates one resolver file per schema file (e.g., `vendor.resolvers.go`). Types that get extended across files (Organization, Mutation, Viewer, TrustCenter) must be defined in `base.graphql`.
|
gqlgen's `follow-schema` layout generates one resolver file per schema file (e.g., `vendor.resolvers.go`). Types that get extended across files (Organization, Mutation, Viewer, TrustCenter) must be defined in `base.graphql`.
|
||||||
|
|
||||||
|
### `extend type` restrictions
|
||||||
|
|
||||||
|
**The only permitted use of `extend type` is `extend type Mutation`.** Never use `extend type` on any other type — not on entity types, not on `Organization`, not on `Query`. If `CookieBanner` needs a `consentRecords` connection, add the field directly to the `CookieBanner` type definition in `cookie_banner.graphql` — do not write `extend type CookieBanner` in another file. This keeps each entity's full field set visible in one place and avoids resolver mis-routing across generated files.
|
||||||
|
|
||||||
## Connection types and `@goModel`
|
## Connection types and `@goModel`
|
||||||
|
|
||||||
**Always define a custom Go type for connection types** using the `@goModel` directive. The model path points to the `types` package for the relevant API. The `totalCount` field must use `@goField(forceResolver: true)`. Edge types do not need `@goModel`.
|
**Always define a custom Go type for connection types** using the `@goModel` directive. The model path points to the `types` package for the relevant API. The `totalCount` field must use `@goField(forceResolver: true)`. Edge types do not need `@goModel`.
|
||||||
|
|||||||
@@ -14,15 +14,27 @@
|
|||||||
|
|
||||||
package coredata
|
package coredata
|
||||||
|
|
||||||
import "github.com/jackc/pgx/v5"
|
import (
|
||||||
|
"github.com/jackc/pgx/v5"
|
||||||
|
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
)
|
||||||
|
|
||||||
type CookieConsentRecordFilter struct {
|
type CookieConsentRecordFilter struct {
|
||||||
action *CookieConsentAction
|
action *CookieConsentAction
|
||||||
|
visitorID *string
|
||||||
|
cookieBannerVersionID *gid.GID
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCookieConsentRecordFilter(action *CookieConsentAction) *CookieConsentRecordFilter {
|
func NewCookieConsentRecordFilter(
|
||||||
|
action *CookieConsentAction,
|
||||||
|
visitorID *string,
|
||||||
|
cookieBannerVersionID *gid.GID,
|
||||||
|
) *CookieConsentRecordFilter {
|
||||||
return &CookieConsentRecordFilter{
|
return &CookieConsentRecordFilter{
|
||||||
action: action,
|
action: action,
|
||||||
|
visitorID: visitorID,
|
||||||
|
cookieBannerVersionID: cookieBannerVersionID,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +43,23 @@ func (f *CookieConsentRecordFilter) SQLFragment() string {
|
|||||||
(
|
(
|
||||||
CASE
|
CASE
|
||||||
WHEN @filter_action::text IS NOT NULL THEN
|
WHEN @filter_action::text IS NOT NULL THEN
|
||||||
action = @filter_action::consent_action
|
action = @filter_action::cookie_consent_action
|
||||||
|
ELSE TRUE
|
||||||
|
END
|
||||||
|
)
|
||||||
|
AND
|
||||||
|
(
|
||||||
|
CASE
|
||||||
|
WHEN @filter_visitor_id::text IS NOT NULL THEN
|
||||||
|
visitor_id = @filter_visitor_id
|
||||||
|
ELSE TRUE
|
||||||
|
END
|
||||||
|
)
|
||||||
|
AND
|
||||||
|
(
|
||||||
|
CASE
|
||||||
|
WHEN @filter_cookie_banner_version_id::text IS NOT NULL THEN
|
||||||
|
cookie_banner_version_id = @filter_cookie_banner_version_id
|
||||||
ELSE TRUE
|
ELSE TRUE
|
||||||
END
|
END
|
||||||
)`
|
)`
|
||||||
@@ -40,11 +68,21 @@ func (f *CookieConsentRecordFilter) SQLFragment() string {
|
|||||||
func (f *CookieConsentRecordFilter) SQLArguments() pgx.StrictNamedArgs {
|
func (f *CookieConsentRecordFilter) SQLArguments() pgx.StrictNamedArgs {
|
||||||
args := pgx.StrictNamedArgs{
|
args := pgx.StrictNamedArgs{
|
||||||
"filter_action": nil,
|
"filter_action": nil,
|
||||||
|
"filter_visitor_id": nil,
|
||||||
|
"filter_cookie_banner_version_id": nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.action != nil {
|
if f.action != nil {
|
||||||
args["filter_action"] = string(*f.action)
|
args["filter_action"] = string(*f.action)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if f.visitorID != nil {
|
||||||
|
args["filter_visitor_id"] = *f.visitorID
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.cookieBannerVersionID != nil {
|
||||||
|
args["filter_cookie_banner_version_id"] = f.cookieBannerVersionID.String()
|
||||||
|
}
|
||||||
|
|
||||||
return args
|
return args
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -408,4 +408,7 @@ const (
|
|||||||
ActionCookieCreate = "core:cookie:create"
|
ActionCookieCreate = "core:cookie:create"
|
||||||
ActionCookieUpdate = "core:cookie:update"
|
ActionCookieUpdate = "core:cookie:update"
|
||||||
ActionCookieDelete = "core:cookie:delete"
|
ActionCookieDelete = "core:cookie:delete"
|
||||||
|
|
||||||
|
// CookieConsentRecord actions
|
||||||
|
ActionCookieConsentRecordList = "core:cookie-consent-record:list"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ var ViewerPolicy = policy.NewPolicy(
|
|||||||
ActionCookieBannerVersionGet, ActionCookieBannerVersionList,
|
ActionCookieBannerVersionGet, ActionCookieBannerVersionList,
|
||||||
ActionCookieCategoryGet, ActionCookieCategoryList,
|
ActionCookieCategoryGet, ActionCookieCategoryList,
|
||||||
ActionCookieGet, ActionCookieList,
|
ActionCookieGet, ActionCookieList,
|
||||||
|
ActionCookieConsentRecordList,
|
||||||
).WithSID("entity-read-access").When(organizationCondition),
|
).WithSID("entity-read-access").When(organizationCondition),
|
||||||
|
|
||||||
policy.Allow(
|
policy.Allow(
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"go.gearno.de/kit/log"
|
"go.gearno.de/kit/log"
|
||||||
"go.probo.inc/probo/pkg/cookiebanner"
|
"go.probo.inc/probo/pkg/cookiebanner"
|
||||||
"go.probo.inc/probo/pkg/coredata"
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
"go.probo.inc/probo/pkg/page"
|
"go.probo.inc/probo/pkg/page"
|
||||||
"go.probo.inc/probo/pkg/probo"
|
"go.probo.inc/probo/pkg/probo"
|
||||||
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
|
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
|
||||||
@@ -122,6 +123,50 @@ func (r *cookieBannerResolver) LatestVersion(ctx context.Context, obj *types.Coo
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ConsentRecords is the resolver for the consentRecords field.
|
||||||
|
func (r *cookieBannerResolver) ConsentRecords(ctx context.Context, obj *types.CookieBanner, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.CookieConsentRecordOrderBy, filter *types.CookieConsentRecordFilter) (*types.CookieConsentRecordConnection, error) {
|
||||||
|
if err := r.authorize(ctx, obj.ID, probo.ActionCookieConsentRecordList); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pageOrderBy := page.OrderBy[coredata.CookieConsentRecordOrderField]{
|
||||||
|
Field: coredata.CookieConsentRecordOrderFieldCreatedAt,
|
||||||
|
Direction: page.OrderDirectionDesc,
|
||||||
|
}
|
||||||
|
if orderBy != nil {
|
||||||
|
pageOrderBy = page.OrderBy[coredata.CookieConsentRecordOrderField]{
|
||||||
|
Field: orderBy.Field,
|
||||||
|
Direction: orderBy.Direction,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||||
|
scope := coredata.NewScopeFromObjectID(obj.ID)
|
||||||
|
|
||||||
|
var (
|
||||||
|
action *coredata.CookieConsentAction
|
||||||
|
visitorID *string
|
||||||
|
cookieBannerVersionID *gid.GID
|
||||||
|
)
|
||||||
|
if filter != nil {
|
||||||
|
action = filter.Action
|
||||||
|
visitorID = filter.VisitorID
|
||||||
|
cookieBannerVersionID = filter.CookieBannerVersionID
|
||||||
|
}
|
||||||
|
|
||||||
|
coredataFilter := coredata.NewCookieConsentRecordFilter(action, visitorID, cookieBannerVersionID)
|
||||||
|
|
||||||
|
records, err := r.cookieBanner.ListCookieConsentRecordsForBanner(ctx, scope, obj.ID, cursor, coredataFilter)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot list consent records", log.Error(err))
|
||||||
|
return nil, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
p := page.NewPage(records, cursor)
|
||||||
|
|
||||||
|
return types.NewCookieConsentRecordConnection(p, r, obj.ID, coredataFilter), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Permission is the resolver for the permission field.
|
// Permission is the resolver for the permission field.
|
||||||
func (r *cookieBannerResolver) Permission(ctx context.Context, obj *types.CookieBanner, action string) (bool, error) {
|
func (r *cookieBannerResolver) Permission(ctx context.Context, obj *types.CookieBanner, action string) (bool, error) {
|
||||||
return r.Resolver.Permission(ctx, obj, action)
|
return r.Resolver.Permission(ctx, obj, action)
|
||||||
|
|||||||
57
pkg/server/api/console/v1/cookie_consent_record_resolvers.go
Normal file
57
pkg/server/api/console/v1/cookie_consent_record_resolvers.go
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package console_v1
|
||||||
|
|
||||||
|
// This file will be automatically regenerated based on the schema, any resolver
|
||||||
|
// implementations
|
||||||
|
// will be copied through when generating and any unknown code will be moved to the end.
|
||||||
|
// Code generated by github.com/99designs/gqlgen version v0.17.87
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"go.gearno.de/kit/log"
|
||||||
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
"go.probo.inc/probo/pkg/probo"
|
||||||
|
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
|
||||||
|
"go.probo.inc/probo/pkg/server/api/console/v1/types"
|
||||||
|
"go.probo.inc/probo/pkg/server/gqlutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CookieBanner is the resolver for the cookieBanner field.
|
||||||
|
func (r *cookieConsentRecordResolver) CookieBanner(ctx context.Context, obj *types.CookieConsentRecord) (*types.CookieBanner, error) {
|
||||||
|
return obj.CookieBanner, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CookieBannerVersion is the resolver for the cookieBannerVersion field.
|
||||||
|
func (r *cookieConsentRecordResolver) CookieBannerVersion(ctx context.Context, obj *types.CookieConsentRecord) (*types.CookieBannerVersion, error) {
|
||||||
|
return obj.CookieBannerVersion, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TotalCount is the resolver for the totalCount field.
|
||||||
|
func (r *cookieConsentRecordConnectionResolver) TotalCount(ctx context.Context, obj *types.CookieConsentRecordConnection) (int, error) {
|
||||||
|
if err := r.authorize(ctx, obj.ParentID, probo.ActionCookieConsentRecordList); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
scope := coredata.NewScopeFromObjectID(obj.ParentID)
|
||||||
|
|
||||||
|
count, err := r.cookieBanner.CountCookieConsentRecordsForBanner(ctx, scope, obj.ParentID, obj.Filter)
|
||||||
|
if err != nil {
|
||||||
|
r.logger.ErrorCtx(ctx, "cannot count consent records", log.Error(err))
|
||||||
|
return 0, gqlutils.Internal(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CookieConsentRecord returns schema.CookieConsentRecordResolver implementation.
|
||||||
|
func (r *Resolver) CookieConsentRecord() schema.CookieConsentRecordResolver {
|
||||||
|
return &cookieConsentRecordResolver{r}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CookieConsentRecordConnection returns schema.CookieConsentRecordConnectionResolver implementation.
|
||||||
|
func (r *Resolver) CookieConsentRecordConnection() schema.CookieConsentRecordConnectionResolver {
|
||||||
|
return &cookieConsentRecordConnectionResolver{r}
|
||||||
|
}
|
||||||
|
|
||||||
|
type cookieConsentRecordResolver struct{ *Resolver }
|
||||||
|
type cookieConsentRecordConnectionResolver struct{ *Resolver }
|
||||||
@@ -102,6 +102,15 @@ type CookieBanner implements Node {
|
|||||||
|
|
||||||
latestVersion: CookieBannerVersion @goField(forceResolver: true)
|
latestVersion: CookieBannerVersion @goField(forceResolver: true)
|
||||||
|
|
||||||
|
consentRecords(
|
||||||
|
first: Int
|
||||||
|
after: CursorKey
|
||||||
|
last: Int
|
||||||
|
before: CursorKey
|
||||||
|
orderBy: CookieConsentRecordOrder
|
||||||
|
filter: CookieConsentRecordFilter
|
||||||
|
): CookieConsentRecordConnection @goField(forceResolver: true)
|
||||||
|
|
||||||
createdAt: Datetime!
|
createdAt: Datetime!
|
||||||
updatedAt: Datetime!
|
updatedAt: Datetime!
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
# Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
#
|
||||||
|
# Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
# purpose with or without fee is hereby granted, provided that the above
|
||||||
|
# copyright notice and this permission notice appear in all copies.
|
||||||
|
#
|
||||||
|
# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
# PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
enum CookieConsentAction
|
||||||
|
@goModel(model: "go.probo.inc/probo/pkg/coredata.CookieConsentAction") {
|
||||||
|
ACCEPT_ALL
|
||||||
|
@goEnum(
|
||||||
|
value: "go.probo.inc/probo/pkg/coredata.CookieConsentActionAcceptAll"
|
||||||
|
)
|
||||||
|
REJECT_ALL
|
||||||
|
@goEnum(
|
||||||
|
value: "go.probo.inc/probo/pkg/coredata.CookieConsentActionRejectAll"
|
||||||
|
)
|
||||||
|
CUSTOMIZE
|
||||||
|
@goEnum(
|
||||||
|
value: "go.probo.inc/probo/pkg/coredata.CookieConsentActionCustomize"
|
||||||
|
)
|
||||||
|
GPC
|
||||||
|
@goEnum(
|
||||||
|
value: "go.probo.inc/probo/pkg/coredata.CookieConsentActionGPC"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum CookieConsentRecordOrderField
|
||||||
|
@goModel(
|
||||||
|
model: "go.probo.inc/probo/pkg/coredata.CookieConsentRecordOrderField"
|
||||||
|
) {
|
||||||
|
CREATED_AT
|
||||||
|
@goEnum(
|
||||||
|
value: "go.probo.inc/probo/pkg/coredata.CookieConsentRecordOrderFieldCreatedAt"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
input CookieConsentRecordOrder
|
||||||
|
@goModel(
|
||||||
|
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.CookieConsentRecordOrderBy"
|
||||||
|
) {
|
||||||
|
direction: OrderDirection!
|
||||||
|
field: CookieConsentRecordOrderField!
|
||||||
|
}
|
||||||
|
|
||||||
|
input CookieConsentRecordFilter {
|
||||||
|
action: CookieConsentAction
|
||||||
|
visitorId: String
|
||||||
|
cookieBannerVersionId: ID
|
||||||
|
}
|
||||||
|
|
||||||
|
type CookieConsentRecord implements Node {
|
||||||
|
id: ID!
|
||||||
|
cookieBanner: CookieBanner @goField(forceResolver: true)
|
||||||
|
cookieBannerVersion: CookieBannerVersion @goField(forceResolver: true)
|
||||||
|
visitorId: String!
|
||||||
|
ipAddress: String
|
||||||
|
userAgent: String
|
||||||
|
consentData: String!
|
||||||
|
action: CookieConsentAction!
|
||||||
|
sdkVersion: String!
|
||||||
|
createdAt: Datetime!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CookieConsentRecordConnection
|
||||||
|
@goModel(
|
||||||
|
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.CookieConsentRecordConnection"
|
||||||
|
) {
|
||||||
|
totalCount: Int! @goField(forceResolver: true)
|
||||||
|
edges: [CookieConsentRecordEdge!]!
|
||||||
|
pageInfo: PageInfo!
|
||||||
|
}
|
||||||
|
|
||||||
|
type CookieConsentRecordEdge {
|
||||||
|
cursor: CursorKey!
|
||||||
|
node: CookieConsentRecord!
|
||||||
|
}
|
||||||
86
pkg/server/api/console/v1/types/cookie_consent_record.go
Normal file
86
pkg/server/api/console/v1/types/cookie_consent_record.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
|
||||||
|
//
|
||||||
|
// Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
// purpose with or without fee is hereby granted, provided that the above
|
||||||
|
// copyright notice and this permission notice appear in all copies.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
// PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
|
||||||
|
package types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.probo.inc/probo/pkg/coredata"
|
||||||
|
"go.probo.inc/probo/pkg/gid"
|
||||||
|
"go.probo.inc/probo/pkg/page"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
CookieConsentRecordOrderBy OrderBy[coredata.CookieConsentRecordOrderField]
|
||||||
|
|
||||||
|
CookieConsentRecordConnection struct {
|
||||||
|
TotalCount int
|
||||||
|
Edges []*CookieConsentRecordEdge
|
||||||
|
PageInfo PageInfo
|
||||||
|
|
||||||
|
Resolver any
|
||||||
|
ParentID gid.GID
|
||||||
|
Filter *coredata.CookieConsentRecordFilter
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewCookieConsentRecordConnection(
|
||||||
|
p *page.Page[*coredata.CookieConsentRecord, coredata.CookieConsentRecordOrderField],
|
||||||
|
parentType any,
|
||||||
|
parentID gid.GID,
|
||||||
|
filter *coredata.CookieConsentRecordFilter,
|
||||||
|
) *CookieConsentRecordConnection {
|
||||||
|
edges := make([]*CookieConsentRecordEdge, len(p.Data))
|
||||||
|
|
||||||
|
for i := range edges {
|
||||||
|
edges[i] = NewCookieConsentRecordEdge(p.Data[i], p.Cursor.OrderBy.Field)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CookieConsentRecordConnection{
|
||||||
|
Edges: edges,
|
||||||
|
PageInfo: *NewPageInfo(p),
|
||||||
|
|
||||||
|
Resolver: parentType,
|
||||||
|
ParentID: parentID,
|
||||||
|
Filter: filter,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCookieConsentRecordEdge(
|
||||||
|
r *coredata.CookieConsentRecord,
|
||||||
|
orderBy coredata.CookieConsentRecordOrderField,
|
||||||
|
) *CookieConsentRecordEdge {
|
||||||
|
return &CookieConsentRecordEdge{
|
||||||
|
Cursor: r.CursorKey(orderBy),
|
||||||
|
Node: NewCookieConsentRecord(r),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCookieConsentRecord(r *coredata.CookieConsentRecord) *CookieConsentRecord {
|
||||||
|
return &CookieConsentRecord{
|
||||||
|
ID: r.ID,
|
||||||
|
CookieBanner: &CookieBanner{
|
||||||
|
ID: r.CookieBannerID,
|
||||||
|
},
|
||||||
|
CookieBannerVersion: &CookieBannerVersion{
|
||||||
|
ID: r.CookieBannerVersionID,
|
||||||
|
},
|
||||||
|
VisitorID: r.VisitorID,
|
||||||
|
IPAddress: r.IPAddress,
|
||||||
|
UserAgent: r.UserAgent,
|
||||||
|
ConsentData: string(r.ConsentData),
|
||||||
|
Action: r.Action,
|
||||||
|
SdkVersion: r.SdkVersion,
|
||||||
|
CreatedAt: r.CreatedAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user