Files
HiveGrid-Wireframes/hivegrid-cloud/apps/web/app/modules/knowledgeBase/db/kbArticles.db.server.ts
T

635 lines
16 KiB
TypeScript

import { KnowledgeBaseArticle, KnowledgeBaseRelatedArticle, Prisma } from "@toolctrl/db/prisma-browser";
import { PaginationDto } from "~/application/dtos/data/PaginationDto";
import { cachified, clearCacheKey } from "~/utils/cache.server";
import { db, dbRead } from "~/utils/db.server";
export type KnowledgeBaseArticleWithDetails = KnowledgeBaseArticle & {
knowledgeBase: { id: string; slug: string; title: string };
category: { slug: string; title: string } | null;
section: { order: number; title: string } | null;
relatedArticles: (KnowledgeBaseRelatedArticle & {
relatedArticle: { id: string; order: number; title: string; slug: string };
})[];
createdByUser: {
email: string;
firstName: string;
lastName: string;
avatar: string | null;
} | null;
_count: {
views: number;
upvotes: number;
downvotes: number;
};
};
export type KnowledgeBaseArticleSimple = {
id: string;
knowledgeBaseId: string;
title: string;
language: string;
category: { id: string; slug: string; title: string } | null;
section: { id: string; order: number; title: string } | null;
};
const include = {
knowledgeBase: {
select: { slug: true, title: true, id: true },
},
category: {
select: { slug: true, title: true },
},
section: {
select: { order: true, title: true },
},
relatedArticles: {
include: {
relatedArticle: {
select: { id: true, order: true, title: true, slug: true },
},
},
},
createdByUser: {
select: { email: true, firstName: true, lastName: true, avatar: true },
},
_count: {
select: { views: true, upvotes: true, downvotes: true },
},
};
export async function getAllKnowledgeBaseArticles({
knowledgeBaseSlug,
categoryId,
language,
}: {
knowledgeBaseSlug: string;
categoryId?: string;
language: string | undefined;
}): Promise<KnowledgeBaseArticleWithDetails[]> {
// Public KB listing — eventually-consistent read is fine.
return await dbRead.knowledgeBaseArticle.findMany({
where: {
knowledgeBase: { slug: knowledgeBaseSlug },
categoryId,
language,
},
include,
orderBy: [
{
category: { order: "asc" },
},
{
section: { order: "asc" },
},
{
order: "asc",
},
],
});
}
export async function getKnowledgeBaseArticlesForTemplateExport({
knowledgeBaseIds,
}: {
knowledgeBaseIds: string[];
}): Promise<KnowledgeBaseArticleWithDetails[]> {
if (knowledgeBaseIds.length === 0) {
return [];
}
return await dbRead.knowledgeBaseArticle.findMany({
where: {
knowledgeBaseId: { in: knowledgeBaseIds },
},
include,
orderBy: [
{
knowledgeBaseId: "asc",
},
{
category: { order: "asc" },
},
{
section: { order: "asc" },
},
{
order: "asc",
},
],
});
}
export async function getAllKnowledgeBaseArticlesSimple(): Promise<KnowledgeBaseArticleSimple[]> {
// Admin sidebar enumeration — read-only, replica-safe.
return await dbRead.knowledgeBaseArticle.findMany({
select: {
id: true,
knowledgeBaseId: true,
title: true,
language: true,
category: { select: { id: true, slug: true, title: true } },
section: { select: { id: true, order: true, title: true } },
},
orderBy: [
{
knowledgeBaseId: "asc",
},
{
category: { order: "asc" },
},
{
section: { order: "asc" },
},
{
order: "asc",
},
],
});
}
export async function getKnowledgeBaseArticlesSimple({
knowledgeBaseId,
language,
}: {
knowledgeBaseId: string;
language: string;
}): Promise<KnowledgeBaseArticleSimple[]> {
// Per-KB simple listing — read-only.
return await dbRead.knowledgeBaseArticle.findMany({
where: { knowledgeBaseId, language },
select: {
id: true,
knowledgeBaseId: true,
title: true,
language: true,
category: { select: { id: true, slug: true, title: true } },
section: { select: { id: true, order: true, title: true } },
},
orderBy: [{ category: { order: "asc" } }, { section: { order: "asc" } }, { order: "asc" }],
});
}
export async function getAllKnowledgeBaseArticlesWithPagination({
knowledgeBaseSlug,
language,
pagination,
filters,
}: {
knowledgeBaseSlug: string | undefined;
language: string | undefined;
pagination: {
page: number;
pageSize: number;
};
filters: {
title?: string;
description?: string;
categoryId?: string | null;
content?: string;
};
}): Promise<{
items: KnowledgeBaseArticleWithDetails[];
pagination: PaginationDto;
}> {
const where: Prisma.KnowledgeBaseArticleWhereInput = {
knowledgeBase: { slug: knowledgeBaseSlug },
language,
};
if (filters.title !== undefined) {
where.title = { contains: filters.title, mode: "insensitive" };
}
if (filters.description !== undefined) {
where.description = { contains: filters.description, mode: "insensitive" };
}
if (filters.categoryId !== undefined) {
where.categoryId = filters.categoryId;
}
if (filters.content !== undefined) {
where.contentPublishedAsText = { contains: filters.content, mode: "insensitive" };
}
// Paginated listing for admin grid — read-only, replica-safe.
const items = await dbRead.knowledgeBaseArticle.findMany({
take: pagination.pageSize,
skip: (pagination.page - 1) * pagination.pageSize,
where,
include,
orderBy: [{ updatedAt: "desc" }, { createdAt: "desc" }],
});
const totalItems = await dbRead.knowledgeBaseArticle.count({
where,
});
return {
items,
pagination: {
page: pagination.page,
pageSize: pagination.pageSize,
totalItems,
totalPages: Math.ceil(totalItems / pagination.pageSize),
},
};
}
export async function findKnowledgeBaseArticles({
knowledgeBaseSlug,
language,
query,
}: {
knowledgeBaseSlug: string;
language: string | undefined;
query: string;
}): Promise<KnowledgeBaseArticleWithDetails[]> {
// Public KB search — read-only, replica-safe.
return await dbRead.knowledgeBaseArticle.findMany({
where: {
knowledgeBase: { slug: knowledgeBaseSlug },
language,
publishedAt: { not: null },
OR: [
{ title: { contains: query, mode: "insensitive" } },
{ description: { contains: query, mode: "insensitive" } },
{
contentPublishedAsText: { contains: query, mode: "insensitive" },
},
],
},
include,
orderBy: [
{
category: { order: "asc" },
},
{
section: { order: "asc" },
},
{
order: "asc",
},
],
});
}
export async function findKnowledgeBaseArticlesByIds({
knowledgeBaseSlug,
language,
ids,
}: {
knowledgeBaseSlug: string;
language: string | undefined;
ids: string[];
}): Promise<KnowledgeBaseArticleWithDetails[]> {
if (ids.length === 0) return [];
// Public KB search hydration — read-only, replica-safe.
return await dbRead.knowledgeBaseArticle.findMany({
where: {
id: { in: ids },
knowledgeBase: { slug: knowledgeBaseSlug },
language,
publishedAt: { not: null },
},
include,
});
}
export async function getFeaturedKnowledgeBaseArticles({
knowledgeBaseId,
language,
}: {
knowledgeBaseId: string;
language: string;
}): Promise<KnowledgeBaseArticleWithDetails[]> {
// Featured-article enumeration for public KB landing — read-only.
return await dbRead.knowledgeBaseArticle.findMany({
where: {
knowledgeBaseId,
language,
featuredOrder: { not: null },
publishedAt: { not: null },
},
include,
orderBy: [{ featuredOrder: "asc" }],
});
}
export async function getKbArticleById(id: string): Promise<KnowledgeBaseArticleWithDetails | null> {
// Read-only lookup; admin edit views read this then save via a separate
// request through the write client.
return await dbRead.knowledgeBaseArticle.findUnique({
where: { id },
include,
});
}
/**
* Returns true only when the article exists AND its parent knowledge base is
* owned by the given tenant. Used to scope resource access (e.g. realtime-collab
* token issuance) so a token can never be minted for a global or other-tenant
* article id via a tenant context.
*/
export async function knowledgeBaseArticleBelongsToTenant(articleId: string, tenantId: string): Promise<boolean> {
const article = await dbRead.knowledgeBaseArticle.findUnique({
where: { id: articleId },
select: { knowledgeBase: { select: { tenantId: true } } },
});
return article?.knowledgeBase?.tenantId === tenantId;
}
export async function getKbArticleByIdInKnowledgeBase({
id,
knowledgeBaseId,
language,
}: {
id: string;
knowledgeBaseId: string;
language: string;
}): Promise<KnowledgeBaseArticleWithDetails | null> {
// Read-only lookup, replica-safe.
return await dbRead.knowledgeBaseArticle.findFirst({
where: { id, knowledgeBaseId, language },
include,
});
}
export async function getKbArticleBySlug({
knowledgeBaseId,
slug,
language,
}: {
knowledgeBaseId: string;
slug: string;
language: string;
}): Promise<KnowledgeBaseArticleWithDetails | null> {
return await cachified({
key: `kb-article:${knowledgeBaseId}:${slug}:${language}`,
ttl: 60 * 60 * 24,
getFreshValue: () =>
// Public KB article fetch (cached for 24h) — replica-safe; lag is
// well under the cache TTL.
dbRead.knowledgeBaseArticle
.findFirstOrThrow({
where: {
knowledgeBaseId,
slug,
language,
},
include: {
...include,
},
})
.catch(() => {
return null;
}),
});
}
export async function createKnowledgeBaseArticle(data: {
knowledgeBaseId: string;
categoryId: string | null;
sectionId: string | null;
slug: string;
title: string;
description: string;
order: number;
contentDraft: string;
contentPublished: string;
contentPublishedAsText: string;
contentType: string;
language: string;
featuredOrder: number | null;
createdByUserId: string | null;
seoImage: string;
publishedAt: Date | null;
}) {
const article = await db.knowledgeBaseArticle.create({
data: {
knowledgeBaseId: data.knowledgeBaseId,
categoryId: data.categoryId,
sectionId: data.sectionId,
slug: data.slug,
title: data.title,
description: data.description,
order: data.order,
contentDraft: data.contentDraft,
contentPublished: data.contentPublished,
contentPublishedAsText: data.contentPublishedAsText,
contentType: data.contentType,
language: data.language,
featuredOrder: data.featuredOrder,
createdByUserId: data.createdByUserId,
seoImage: data.seoImage,
publishedAt: data.publishedAt,
},
});
return article;
}
export async function updateKnowledgeBaseArticle(
id: string,
data: {
categoryId?: string | null;
sectionId?: string | null;
slug?: string;
title?: string;
description?: string;
order?: number;
contentDraft?: string;
contentPublished?: string;
contentPublishedAsText?: string;
contentType?: string;
language?: string;
featuredOrder?: number | null;
createdByUserId?: string | null;
seoImage?: string;
publishedAt?: Date | null;
relatedArticles?: string[];
}
) {
const update: Prisma.KnowledgeBaseArticleUncheckedUpdateInput = {
categoryId: data.categoryId,
sectionId: data.sectionId,
slug: data.slug,
title: data.title,
description: data.description,
order: data.order,
contentDraft: data.contentDraft,
contentPublished: data.contentPublished,
contentPublishedAsText: data.contentPublishedAsText,
contentType: data.contentType,
language: data.language,
featuredOrder: data.featuredOrder,
createdByUserId: data.createdByUserId,
seoImage: data.seoImage,
publishedAt: data.publishedAt,
};
const updated = await db.knowledgeBaseArticle
.update({
where: { id },
data: update,
})
.then((item) => {
clearCacheKey(`kb-article:${item.knowledgeBaseId}:${item.slug}:${item.language}`);
return item;
});
if (data.relatedArticles) {
await db.knowledgeBaseRelatedArticle.deleteMany({
where: {
articleId: id,
},
});
await db.knowledgeBaseRelatedArticle.createMany({
data: data.relatedArticles.map((f) => {
return {
articleId: id,
relatedArticleId: f,
};
}),
});
}
}
export async function updateKnowledgeBaseArticleInKnowledgeBase(
{
id,
knowledgeBaseId,
language,
}: {
id: string;
knowledgeBaseId: string;
language: string;
},
data: {
categoryId?: string | null;
sectionId?: string | null;
slug?: string;
title?: string;
description?: string;
order?: number;
contentDraft?: string;
contentPublished?: string;
contentPublishedAsText?: string;
contentType?: string;
language?: string;
featuredOrder?: number | null;
createdByUserId?: string | null;
seoImage?: string;
publishedAt?: Date | null;
relatedArticles?: string[];
}
) {
const update: Prisma.KnowledgeBaseArticleUncheckedUpdateInput = {
categoryId: data.categoryId,
sectionId: data.sectionId,
slug: data.slug,
title: data.title,
description: data.description,
order: data.order,
contentDraft: data.contentDraft,
contentPublished: data.contentPublished,
contentPublishedAsText: data.contentPublishedAsText,
contentType: data.contentType,
language: data.language,
featuredOrder: data.featuredOrder,
createdByUserId: data.createdByUserId,
seoImage: data.seoImage,
publishedAt: data.publishedAt,
};
const result = await db.knowledgeBaseArticle.updateMany({
where: { id, knowledgeBaseId, language },
data: update,
});
if (result.count !== 1) {
return result;
}
const item = await db.knowledgeBaseArticle.findUnique({ where: { id }, select: { knowledgeBaseId: true, slug: true, language: true } });
if (item) {
clearCacheKey(`kb-article:${item.knowledgeBaseId}:${item.slug}:${item.language}`);
}
if (data.relatedArticles) {
await db.knowledgeBaseRelatedArticle.deleteMany({
where: {
articleId: id,
article: { knowledgeBaseId, language },
},
});
await db.knowledgeBaseRelatedArticle.createMany({
data: data.relatedArticles.map((f) => {
return {
articleId: id,
relatedArticleId: f,
};
}),
});
}
return result;
}
export async function updateKnowledgeBaseArticleOrderInCategory({ id, categoryId, order }: { id: string; categoryId: string; order: number }) {
return await db.knowledgeBaseArticle.updateMany({
where: { id, categoryId },
data: { order },
});
}
export async function updateKnowledgeBaseArticleOrderInSection({ id, sectionId, order }: { id: string; sectionId: string; order: number }) {
return await db.knowledgeBaseArticle.updateMany({
where: { id, sectionId },
data: { order },
});
}
export async function updateKnowledgeBaseArticleOrderInKnowledgeBase({
id,
knowledgeBaseId,
language,
order,
}: {
id: string;
knowledgeBaseId: string;
language: string;
order: number;
}) {
return await db.knowledgeBaseArticle.updateMany({
where: { id, knowledgeBaseId, language },
data: { order },
});
}
export async function deleteKnowledgeBaseArticle(id: string) {
const result = await db.knowledgeBaseArticle
.delete({
where: { id },
})
.then((item) => {
clearCacheKey(`kb-article:${item.knowledgeBaseId}:${item.slug}:${item.language}`);
return item;
});
return result;
}
export async function deleteKnowledgeBaseArticleInKnowledgeBase({
id,
knowledgeBaseId,
language,
}: {
id: string;
knowledgeBaseId: string;
language: string;
}) {
const item = await db.knowledgeBaseArticle.findFirst({
where: { id, knowledgeBaseId, language },
select: { id: true, slug: true },
});
if (!item) {
return { count: 0 };
}
const result = await db.knowledgeBaseArticle.deleteMany({
where: { id, knowledgeBaseId, language },
});
if (result.count === 1) {
clearCacheKey(`kb-article:${knowledgeBaseId}:${item.slug}:${language}`);
}
return result;
}
export async function countKnowledgeBaseArticles() {
// Dashboard count — read-only, replica-safe.
return await dbRead.knowledgeBaseArticle.count();
}