Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,163 @@
|
||||
import { db, dbRead } from "~/utils/db.server";
|
||||
|
||||
export async function createKnowledgeBaseView({ userAnalyticsId, knowledgeBaseId }: { userAnalyticsId: string | null; knowledgeBaseId: string }) {
|
||||
if (!userAnalyticsId) {
|
||||
return;
|
||||
}
|
||||
const existing = await db.knowledgeBaseViews.findUnique({
|
||||
where: {
|
||||
knowledgeBaseId_userAnalyticsId: {
|
||||
userAnalyticsId,
|
||||
knowledgeBaseId,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!existing) {
|
||||
await db.knowledgeBaseViews.create({
|
||||
data: {
|
||||
userAnalyticsId,
|
||||
knowledgeBaseId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function createKnowledgeBaseArticleView({ userAnalyticsId, articleId }: { userAnalyticsId: string | null; articleId: string }) {
|
||||
if (!userAnalyticsId) {
|
||||
return;
|
||||
}
|
||||
const existing = await db.knowledgeBaseArticleViews.findUnique({
|
||||
where: {
|
||||
knowledgeBaseArticleId_userAnalyticsId: {
|
||||
userAnalyticsId,
|
||||
knowledgeBaseArticleId: articleId,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!existing) {
|
||||
await db.knowledgeBaseArticleViews.create({
|
||||
data: {
|
||||
userAnalyticsId,
|
||||
knowledgeBaseArticleId: articleId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function voteArticle({ userAnalyticsId, articleId, type }: { userAnalyticsId: string | null; articleId: string; type: "up" | "down" }) {
|
||||
if (!userAnalyticsId) {
|
||||
return;
|
||||
}
|
||||
if (type === "up") {
|
||||
await db.knowledgeBaseArticleDownvotes.deleteMany({
|
||||
where: {
|
||||
userAnalyticsId,
|
||||
knowledgeBaseArticleId: articleId,
|
||||
},
|
||||
});
|
||||
const existing = await db.knowledgeBaseArticleUpvotes.findUnique({
|
||||
where: {
|
||||
knowledgeBaseArticleId_userAnalyticsId: {
|
||||
userAnalyticsId,
|
||||
knowledgeBaseArticleId: articleId,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!existing) {
|
||||
await db.knowledgeBaseArticleUpvotes.create({
|
||||
data: {
|
||||
userAnalyticsId,
|
||||
knowledgeBaseArticleId: articleId,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await db.knowledgeBaseArticleUpvotes.delete({
|
||||
where: {
|
||||
knowledgeBaseArticleId_userAnalyticsId: {
|
||||
userAnalyticsId,
|
||||
knowledgeBaseArticleId: articleId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await db.knowledgeBaseArticleUpvotes.deleteMany({
|
||||
where: {
|
||||
userAnalyticsId,
|
||||
knowledgeBaseArticleId: articleId,
|
||||
},
|
||||
});
|
||||
const existing = await db.knowledgeBaseArticleDownvotes.findUnique({
|
||||
where: {
|
||||
knowledgeBaseArticleId_userAnalyticsId: {
|
||||
userAnalyticsId,
|
||||
knowledgeBaseArticleId: articleId,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!existing) {
|
||||
await db.knowledgeBaseArticleDownvotes.create({
|
||||
data: {
|
||||
userAnalyticsId,
|
||||
knowledgeBaseArticleId: articleId,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
await db.knowledgeBaseArticleDownvotes.delete({
|
||||
where: {
|
||||
knowledgeBaseArticleId_userAnalyticsId: {
|
||||
userAnalyticsId,
|
||||
knowledgeBaseArticleId: articleId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function getArticleStateByUserAnalyticsId({ userAnalyticsId, articleId }: { userAnalyticsId: string | null; articleId: string }) {
|
||||
if (!userAnalyticsId) {
|
||||
return {
|
||||
hasThumbsUp: false,
|
||||
hasThumbsDown: false,
|
||||
};
|
||||
}
|
||||
return {
|
||||
hasThumbsUp: !!(await db.knowledgeBaseArticleUpvotes.findUnique({
|
||||
where: {
|
||||
knowledgeBaseArticleId_userAnalyticsId: {
|
||||
userAnalyticsId,
|
||||
knowledgeBaseArticleId: articleId,
|
||||
},
|
||||
},
|
||||
})),
|
||||
hasThumbsDown: !!(await db.knowledgeBaseArticleDownvotes.findUnique({
|
||||
where: {
|
||||
knowledgeBaseArticleId_userAnalyticsId: {
|
||||
userAnalyticsId,
|
||||
knowledgeBaseArticleId: articleId,
|
||||
},
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
export async function countAllKbsViews() {
|
||||
// Dashboard count — read-only, replica-safe.
|
||||
return await dbRead.knowledgeBaseViews.count();
|
||||
}
|
||||
|
||||
export async function countAllKbsArticleViews() {
|
||||
// Dashboard count — read-only, replica-safe.
|
||||
return await dbRead.knowledgeBaseArticleViews.count();
|
||||
}
|
||||
|
||||
export async function countAllKbsArticleUpvotes() {
|
||||
// Dashboard count — read-only, replica-safe.
|
||||
return await dbRead.knowledgeBaseArticleUpvotes.count();
|
||||
}
|
||||
|
||||
export async function countAllKbsArticleDownvotes() {
|
||||
// Dashboard count — read-only, replica-safe.
|
||||
return await dbRead.knowledgeBaseArticleDownvotes.count();
|
||||
}
|
||||
@@ -0,0 +1,634 @@
|
||||
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();
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import { db, dbRead } from "~/utils/db.server";
|
||||
import { KnowledgeBaseCategoryWithDetails } from "../helpers/KbCategoryModelHelper";
|
||||
import { Prisma } from "@toolctrl/db/prisma-browser";
|
||||
|
||||
export type KnowledgeBaseCategorySimple = {
|
||||
id: string;
|
||||
knowledgeBaseId: string;
|
||||
title: string;
|
||||
language: string;
|
||||
sections: { id: string; order: number; title: string }[];
|
||||
};
|
||||
|
||||
const include = {
|
||||
articles: {
|
||||
select: { id: true, order: true, title: true, description: true, slug: true, language: true, sectionId: true, publishedAt: true },
|
||||
},
|
||||
sections: {
|
||||
select: { id: true, order: true, title: true, description: true },
|
||||
},
|
||||
};
|
||||
|
||||
export async function getAllKnowledgeBaseCategories({
|
||||
knowledgeBaseSlug,
|
||||
language,
|
||||
}: {
|
||||
knowledgeBaseSlug: string | undefined;
|
||||
language: string | undefined;
|
||||
}): Promise<KnowledgeBaseCategoryWithDetails[]> {
|
||||
// Public KB category list — read-only, replica-safe.
|
||||
return await dbRead.knowledgeBaseCategory.findMany({
|
||||
where: {
|
||||
knowledgeBase: { slug: knowledgeBaseSlug },
|
||||
language,
|
||||
},
|
||||
include,
|
||||
orderBy: { order: "asc" },
|
||||
});
|
||||
}
|
||||
|
||||
export async function getKnowledgeBaseCategoriesForTemplateExport({
|
||||
knowledgeBaseIds,
|
||||
}: {
|
||||
knowledgeBaseIds: string[];
|
||||
}): Promise<KnowledgeBaseCategoryWithDetails[]> {
|
||||
if (knowledgeBaseIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
return await dbRead.knowledgeBaseCategory.findMany({
|
||||
where: {
|
||||
knowledgeBaseId: { in: knowledgeBaseIds },
|
||||
},
|
||||
include,
|
||||
orderBy: [{ knowledgeBaseId: "asc" }, { order: "asc" }],
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAllKnowledgeBaseCategoriesSimple(): Promise<KnowledgeBaseCategorySimple[]> {
|
||||
// Admin sidebar enumeration — read-only.
|
||||
return await dbRead.knowledgeBaseCategory.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
knowledgeBaseId: true,
|
||||
title: true,
|
||||
language: true,
|
||||
sections: { select: { id: true, order: true, title: true } },
|
||||
},
|
||||
orderBy: [{ knowledgeBaseId: "asc" }, { order: "asc" }],
|
||||
});
|
||||
}
|
||||
|
||||
export async function getKbCategoryById(id: string): Promise<KnowledgeBaseCategoryWithDetails | null> {
|
||||
// Read-only lookup.
|
||||
return await dbRead.knowledgeBaseCategory.findUnique({
|
||||
where: { id },
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getKbCategoryByIdInKnowledgeBase({
|
||||
id,
|
||||
knowledgeBaseId,
|
||||
language,
|
||||
}: {
|
||||
id: string;
|
||||
knowledgeBaseId: string;
|
||||
language: string;
|
||||
}): Promise<KnowledgeBaseCategoryWithDetails | null> {
|
||||
// Read-only lookup.
|
||||
return await dbRead.knowledgeBaseCategory.findFirst({
|
||||
where: { id, knowledgeBaseId, language },
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getKbCategoryBySlug({
|
||||
knowledgeBaseId,
|
||||
slug,
|
||||
language,
|
||||
}: {
|
||||
knowledgeBaseId: string;
|
||||
slug: string;
|
||||
language: string | undefined;
|
||||
}): Promise<KnowledgeBaseCategoryWithDetails | null> {
|
||||
const where: Prisma.KnowledgeBaseCategoryWhereInput = {
|
||||
knowledgeBaseId,
|
||||
slug,
|
||||
language,
|
||||
};
|
||||
// Public route lookup, read-only.
|
||||
return await dbRead.knowledgeBaseCategory.findFirst({
|
||||
where,
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createKnowledgeBaseCategory(data: {
|
||||
knowledgeBaseId: string;
|
||||
slug: string;
|
||||
order: number;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
language: string;
|
||||
seoImage: string;
|
||||
}): Promise<KnowledgeBaseCategoryWithDetails> {
|
||||
return await db.knowledgeBaseCategory.create({
|
||||
data: {
|
||||
knowledgeBaseId: data.knowledgeBaseId,
|
||||
slug: data.slug,
|
||||
order: data.order,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
icon: data.icon,
|
||||
language: data.language,
|
||||
seoImage: data.seoImage,
|
||||
},
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateKnowledgeBaseCategory(
|
||||
id: string,
|
||||
data: {
|
||||
slug?: string;
|
||||
title?: string;
|
||||
order?: number;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
language?: string;
|
||||
seoImage?: string;
|
||||
}
|
||||
) {
|
||||
return await db.knowledgeBaseCategory.update({
|
||||
where: { id },
|
||||
data: {
|
||||
slug: data.slug,
|
||||
order: data.order,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
icon: data.icon,
|
||||
language: data.language,
|
||||
seoImage: data.seoImage,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateKnowledgeBaseCategoryInKnowledgeBase(
|
||||
{
|
||||
id,
|
||||
knowledgeBaseId,
|
||||
language,
|
||||
}: {
|
||||
id: string;
|
||||
knowledgeBaseId: string;
|
||||
language: string;
|
||||
},
|
||||
data: {
|
||||
slug?: string;
|
||||
title?: string;
|
||||
order?: number;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
language?: string;
|
||||
seoImage?: string;
|
||||
}
|
||||
) {
|
||||
return await db.knowledgeBaseCategory.updateMany({
|
||||
where: { id, knowledgeBaseId, language },
|
||||
data: {
|
||||
slug: data.slug,
|
||||
order: data.order,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
icon: data.icon,
|
||||
language: data.language,
|
||||
seoImage: data.seoImage,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateKnowledgeBaseCategoryOrderInKnowledgeBase({
|
||||
id,
|
||||
knowledgeBaseId,
|
||||
language,
|
||||
order,
|
||||
}: {
|
||||
id: string;
|
||||
knowledgeBaseId: string;
|
||||
language: string;
|
||||
order: number;
|
||||
}) {
|
||||
return await db.knowledgeBaseCategory.updateMany({
|
||||
where: { id, knowledgeBaseId, language },
|
||||
data: { order },
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteKnowledgeBaseCategory(id: string) {
|
||||
return await db.knowledgeBaseCategory.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteKnowledgeBaseCategoryInKnowledgeBase({
|
||||
id,
|
||||
knowledgeBaseId,
|
||||
language,
|
||||
}: {
|
||||
id: string;
|
||||
knowledgeBaseId: string;
|
||||
language: string;
|
||||
}) {
|
||||
return await db.knowledgeBaseCategory.deleteMany({
|
||||
where: { id, knowledgeBaseId, language },
|
||||
});
|
||||
}
|
||||
|
||||
export async function countKnowledgeBaseCategories() {
|
||||
// Dashboard count — read-only.
|
||||
return await dbRead.knowledgeBaseCategory.count();
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { KnowledgeBaseCategorySection } from "@toolctrl/db/prisma-browser";
|
||||
import { db, dbRead } from "~/utils/db.server";
|
||||
|
||||
export type KnowledgeBaseCategorySectionWithDetails = KnowledgeBaseCategorySection & {
|
||||
articles: {
|
||||
id: string;
|
||||
order: number;
|
||||
title: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
export async function getAllKnowledgeBaseCategorySections(): Promise<KnowledgeBaseCategorySectionWithDetails[]> {
|
||||
// Read-only listing used by public KB rendering — replica is fine.
|
||||
return await dbRead.knowledgeBaseCategorySection.findMany({
|
||||
include: {
|
||||
articles: { select: { id: true, order: true, title: true } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getKbCategorySectionById(id: string): Promise<KnowledgeBaseCategorySectionWithDetails | null> {
|
||||
// Read-only lookup; called from both admin edit views and public rendering.
|
||||
return await dbRead.knowledgeBaseCategorySection.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
articles: { select: { id: true, order: true, title: true } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getKbCategorySectionByIdInCategory({
|
||||
id,
|
||||
categoryId,
|
||||
}: {
|
||||
id: string;
|
||||
categoryId: string;
|
||||
}): Promise<KnowledgeBaseCategorySectionWithDetails | null> {
|
||||
// Read-only lookup, replica-safe.
|
||||
return await dbRead.knowledgeBaseCategorySection.findFirst({
|
||||
where: { id, categoryId },
|
||||
include: {
|
||||
articles: { select: { id: true, order: true, title: true } },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function createKnowledgeBaseCategorySection(data: { categoryId: string; order: number; title: string; description: string }) {
|
||||
return await db.knowledgeBaseCategorySection.create({
|
||||
data: {
|
||||
categoryId: data.categoryId,
|
||||
order: data.order,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateKnowledgeBaseCategorySection(
|
||||
id: string,
|
||||
data: {
|
||||
order?: number;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
) {
|
||||
return await db.knowledgeBaseCategorySection.update({
|
||||
where: { id },
|
||||
data: {
|
||||
order: data.order,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateKnowledgeBaseCategorySectionInCategory(
|
||||
{ id, categoryId }: { id: string; categoryId: string },
|
||||
data: {
|
||||
order?: number;
|
||||
title?: string;
|
||||
description?: string;
|
||||
}
|
||||
) {
|
||||
return await db.knowledgeBaseCategorySection.updateMany({
|
||||
where: { id, categoryId },
|
||||
data: {
|
||||
order: data.order,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateKnowledgeBaseCategorySectionOrderInKnowledgeBase({
|
||||
id,
|
||||
knowledgeBaseId,
|
||||
language,
|
||||
order,
|
||||
}: {
|
||||
id: string;
|
||||
knowledgeBaseId: string;
|
||||
language: string;
|
||||
order: number;
|
||||
}) {
|
||||
return await db.knowledgeBaseCategorySection.updateMany({
|
||||
where: { id, category: { knowledgeBaseId, language } },
|
||||
data: { order },
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteKnowledgeBaseCategorySection(id: string) {
|
||||
return await db.knowledgeBaseCategorySection.delete({
|
||||
where: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteKnowledgeBaseCategorySectionInCategory({ id, categoryId }: { id: string; categoryId: string }) {
|
||||
return await db.knowledgeBaseCategorySection.deleteMany({
|
||||
where: { id, categoryId },
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { KnowledgeBase } from "@toolctrl/db/prisma-browser";
|
||||
import { clearCacheKey } from "~/utils/cache.server";
|
||||
import { db, dbRead } from "~/utils/db.server";
|
||||
|
||||
export type KnowledgeBaseWithDetails = KnowledgeBase & {
|
||||
_count: {
|
||||
categories: number;
|
||||
articles: number;
|
||||
views: number;
|
||||
};
|
||||
};
|
||||
|
||||
const include = {
|
||||
_count: {
|
||||
select: { categories: true, articles: true, views: true },
|
||||
},
|
||||
};
|
||||
|
||||
function knowledgeBaseOwnerKey(tenantId?: string | null) {
|
||||
return tenantId ?? "global";
|
||||
}
|
||||
|
||||
export async function getAllKnowledgeBases({ enabled, tenantId }: { enabled?: boolean; tenantId?: string | null } = {}): Promise<KnowledgeBaseWithDetails[]> {
|
||||
// Listing for KB management UI and public KB discovery — read-only.
|
||||
return await dbRead.knowledgeBase.findMany({
|
||||
where: { enabled, ownerKey: knowledgeBaseOwnerKey(tenantId) },
|
||||
include,
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAllKnowledgeBaseNames({ tenantId }: { tenantId?: string | null } = {}): Promise<{ id: string; title: string }[]> {
|
||||
// Simple enumeration — read-only.
|
||||
return await dbRead.knowledgeBase.findMany({
|
||||
where: { ownerKey: knowledgeBaseOwnerKey(tenantId) },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "asc",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function getKnowledgeBaseById(id: string): Promise<KnowledgeBaseWithDetails | null> {
|
||||
// Read-only lookup.
|
||||
return await dbRead.knowledgeBase.findUnique({
|
||||
where: { id },
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getKnowledgeBaseBySlug(slug: string, { tenantId }: { tenantId?: string | null } = {}): Promise<KnowledgeBaseWithDetails | null> {
|
||||
// Public route lookup, read-only.
|
||||
return await dbRead.knowledgeBase.findUnique({
|
||||
where: { ownerKey_slug: { ownerKey: knowledgeBaseOwnerKey(tenantId), slug } },
|
||||
include,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createKnowledgeBase(data: {
|
||||
basePath: string;
|
||||
slug: string;
|
||||
tenantId?: string | null;
|
||||
title: string;
|
||||
description: string;
|
||||
defaultLanguage: string;
|
||||
layout: string;
|
||||
color: number;
|
||||
enabled: boolean;
|
||||
languages: string;
|
||||
links: string;
|
||||
logo: string;
|
||||
seoImage: string;
|
||||
}) {
|
||||
return await db.knowledgeBase.create({
|
||||
data: {
|
||||
basePath: data.basePath,
|
||||
slug: data.slug,
|
||||
tenantId: data.tenantId ?? null,
|
||||
ownerKey: knowledgeBaseOwnerKey(data.tenantId),
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
defaultLanguage: data.defaultLanguage,
|
||||
layout: data.layout,
|
||||
color: data.color,
|
||||
enabled: data.enabled,
|
||||
languages: data.languages,
|
||||
links: data.links,
|
||||
logo: data.logo,
|
||||
seoImage: data.seoImage,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function updateKnowledgeBase(
|
||||
id: string,
|
||||
data: {
|
||||
basePath?: string;
|
||||
slug?: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
defaultLanguage?: string;
|
||||
layout?: string;
|
||||
color?: number;
|
||||
enabled?: boolean;
|
||||
languages?: string;
|
||||
links?: string;
|
||||
logo?: string;
|
||||
seoImage?: string;
|
||||
}
|
||||
) {
|
||||
return await db.knowledgeBase
|
||||
.update({
|
||||
where: { id },
|
||||
data: {
|
||||
basePath: data.basePath,
|
||||
slug: data.slug,
|
||||
title: data.title,
|
||||
description: data.description,
|
||||
defaultLanguage: data.defaultLanguage,
|
||||
layout: data.layout,
|
||||
color: data.color,
|
||||
enabled: data.enabled,
|
||||
languages: data.languages,
|
||||
links: data.links,
|
||||
logo: data.logo,
|
||||
seoImage: data.seoImage,
|
||||
},
|
||||
})
|
||||
.then((item) => {
|
||||
clearCacheKey(`kb:${item.slug}`);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteKnowledgeBase(id: string) {
|
||||
return await db.knowledgeBase
|
||||
.delete({
|
||||
where: { id },
|
||||
})
|
||||
.then((item) => {
|
||||
clearCacheKey(`kb:${item.slug}`);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
export async function countKnowledgeBases() {
|
||||
// Dashboard count — read-only.
|
||||
return await dbRead.knowledgeBase.count();
|
||||
}
|
||||
Reference in new issue
Block a user