hoàn thành tích hợp FSRS

This commit is contained in:
2026-04-26 19:31:29 +07:00
parent a573acedd0
commit 8459801ca9
33 changed files with 997 additions and 185 deletions
@@ -880,5 +880,20 @@
"Try a different search term.": "Try a different search term.",
"Try again": "Try again",
"Untitled chat": "Untitled chat",
"What can I help you with?": "What can I help you with?"
"What can I help you with?": "What can I help you with?",
"Need to Review": "Need to Review",
"How well do you know this page?": "How well do you know this page?",
"Due": "Due",
"All caught up!": "All caught up!",
"No pages need review today. Good job!": "No pages need review today. Good job!",
"Review updated": "Review updated",
"Failed to update review": "Failed to update review",
"Hard": "Hard",
"Good": "Good",
"Easy": "Easy",
"Again": "Again",
"Plan": "Plan",
"Next review": "Next review",
"No learning plan yet": "No learning plan yet",
"Try adding some cloze deletions to your notes to start learning.": "Try adding some cloze deletions to your notes to start learning."
}
@@ -10,6 +10,8 @@ import {
IconUnderline,
IconMessage,
IconSparkles,
IconEyeOff,
IconCards,
} from "@tabler/icons-react";
import clsx from "clsx";
import classes from "./bubble-menu.module.css";
@@ -78,6 +80,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
isStrike: ctx.editor.isActive("strike"),
isCode: ctx.editor.isActive("code"),
isComment: ctx.editor.isActive("comment"),
isCloze: ctx.editor.isActive("cloze"),
};
},
});
@@ -113,6 +116,51 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
command: () => props.editor.chain().focus().toggleCode().run(),
icon: IconCode,
},
{
name: "Cloze Deletion",
isActive: () => editorState?.isCloze,
command: () => props.editor.chain().focus().toggleCloze().run(),
icon: IconEyeOff,
},
{
name: "Send to Anki",
isActive: () => false,
command: async () => {
const { from, to } = props.editor.state.selection;
const text = props.editor.state.doc.textBetween(from, to, " ");
const title = document.title || "Docmost Page";
try {
const response = await fetch("http://localhost:8765", {
method: "POST",
body: JSON.stringify({
action: "addNote",
version: 6,
params: {
note: {
deckName: "Docmost",
modelName: "Basic",
fields: {
Front: title,
Back: text,
},
tags: ["docmost"],
},
},
}),
});
const result = await response.json();
if (result.error) {
alert(`AnkiConnect Error: ${result.error}`);
} else {
alert("Successfully sent to Anki!");
}
} catch (error) {
alert("Failed to connect to AnkiConnect. Make sure Anki is running with AnkiConnect plugin installed.");
}
},
icon: IconCards,
},
];
const commentItem: BubbleMenuItem = {
@@ -52,6 +52,7 @@ import {
Columns,
Column,
Status,
Cloze,
} from "@docmost/editor-ext";
import {
randomElement,
@@ -375,6 +376,7 @@ export const mainExtensions = [
}).configure(),
Columns,
Column,
Cloze,
AutoJoiner.configure({
elementsToJoin: [],
}),
@@ -0,0 +1,23 @@
.docmost-cloze {
background-color: var(--mantine-color-black);
color: var(--mantine-color-black);
border-radius: 4px;
padding: 0 2px;
transition: all 0.2s ease;
cursor: help;
}
.docmost-cloze:hover {
background-color: rgba(0, 0, 0, 0.1);
color: inherit;
}
[data-theme='dark'] .docmost-cloze {
background-color: var(--mantine-color-white);
color: var(--mantine-color-white);
}
[data-theme='dark'] .docmost-cloze:hover {
background-color: rgba(255, 255, 255, 0.2);
color: inherit;
}
@@ -15,3 +15,4 @@
@import "./highlight.css";
@import "./columns.css";
@import "./status.css";
@import "./cloze.css";
@@ -1,8 +1,9 @@
import { Text, Tabs, Space } from "@mantine/core";
import { IconClockHour3, IconStar, IconUser } from "@tabler/icons-react";
import { IconClockHour3, IconStar, IconUser, IconSchool, IconCalendarStats } from "@tabler/icons-react";
import RecentChanges from "@/components/common/recent-changes";
import FavoritesPages from "./favorites-pages";
import CreatedByMe from "./created-by-me";
import NeedToReview from "./need-to-review";
import { useTranslation } from "react-i18next";
import { useAtom } from "jotai";
import { homeTabAtom } from "@/features/home/atoms/home-tab-atom";
@@ -35,6 +36,16 @@ export default function HomeTabs() {
{t("Created by me")}
</Text>
</Tabs.Tab>
<Tabs.Tab value="review" leftSection={<IconSchool size={18} />}>
<Text size="sm" fw={500}>
{t("Need to Review")}
</Text>
</Tabs.Tab>
<Tabs.Tab value="plan" leftSection={<IconCalendarStats size={18} />}>
<Text size="sm" fw={500}>
{t("Plan")}
</Text>
</Tabs.Tab>
</Tabs.List>
<Space my="md" />
@@ -48,6 +59,12 @@ export default function HomeTabs() {
<Tabs.Panel value="created">
<CreatedByMe />
</Tabs.Panel>
<Tabs.Panel value="review">
<NeedToReview />
</Tabs.Panel>
<Tabs.Panel value="plan">
<NeedToReview onlyDue={false} />
</Tabs.Panel>
</Tabs>
);
}
@@ -0,0 +1,122 @@
import {
Text,
Group,
UnstyledButton,
Badge,
Table,
ActionIcon,
Button,
} from "@mantine/core";
import { Link } from "react-router-dom";
import PageListSkeleton from "@/components/ui/page-list-skeleton";
import { buildPageUrl } from "@/features/page/page.utils";
import { formattedDate } from "@/lib/time";
import { useReviewListQuery } from "@/features/page/queries/page-query";
import { IconFileDescription, IconSchool } from "@tabler/icons-react";
import { EmptyState } from "@/components/ui/empty-state";
import { getSpaceUrl } from "@/lib/config";
import { useTranslation } from "react-i18next";
import { getInitialsColor } from "@/lib/get-initials-color";
export default function NeedToReview({ spaceId, onlyDue = true }: { spaceId?: string; onlyDue?: boolean }) {
const { t } = useTranslation();
const {
data,
isLoading,
isError,
hasNextPage,
fetchNextPage,
isFetchingNextPage,
} = useReviewListQuery(spaceId, onlyDue);
const pages = data?.pages.flatMap((p) => p.items) ?? [];
if (isLoading) {
return <PageListSkeleton />;
}
if (isError) {
return <Text>{t("Failed to fetch pages")}</Text>;
}
return pages.length > 0 ? (
<>
<Table.ScrollContainer minWidth={500}>
<Table highlightOnHover verticalSpacing="sm">
<Table.Tbody>
{pages.map((page) => (
<Table.Tr key={page.id}>
<Table.Td>
<UnstyledButton
component={Link}
to={buildPageUrl(
page?.space?.slug,
page.slugId,
page.title,
)}
>
<Group wrap="nowrap">
{page.icon || (
<ActionIcon
variant="transparent"
color="gray"
size={18}
>
<IconFileDescription size={18} />
</ActionIcon>
)}
<Text fw={500} size="md" lineClamp={1}>
{page.title || t("Untitled")}
</Text>
</Group>
</UnstyledButton>
</Table.Td>
{!spaceId && (
<Table.Td>
<Badge
color={getInitialsColor(page?.space?.name || "default")}
variant="light"
component={Link}
to={getSpaceUrl(page?.space?.slug || "")}
style={{ cursor: "pointer" }}
>
{page?.space?.name || t("Unknown")}
</Badge>
</Table.Td>
)}
<Table.Td>
<Text
c="dimmed"
style={{ whiteSpace: "nowrap" }}
size="xs"
fw={500}
>
{onlyDue ? t("Due") : t("Next review")}: {formattedDate(page.nextReviewDate)}
</Text>
</Table.Td>
</Table.Tr>
))}
</Table.Tbody>
</Table>
</Table.ScrollContainer>
{hasNextPage && (
<Button
variant="subtle"
fullWidth
mt="sm"
mb="xl"
onClick={() => fetchNextPage()}
loading={isFetchingNextPage}
>
{t("Load more")}
</Button>
)}
</>
) : (
<EmptyState
icon={IconSchool}
title={onlyDue ? t("All caught up!") : t("No learning plan yet")}
description={onlyDue ? t("No pages need review today. Good job!") : t("Try adding some cloze deletions to your notes to start learning.")}
/>
);
}
@@ -0,0 +1,68 @@
import { Button, Group, Paper, Text, Stack } from "@mantine/core";
import { useReviewMutation } from "@/features/page/queries/page-query";
import { useTranslation } from "react-i18next";
import { IconBrain } from "@tabler/icons-react";
type Props = {
pageId: string;
};
export default function ReviewButtons({ pageId }: Props) {
const { t } = useTranslation();
const { mutate, isPending } = useReviewMutation();
const handleReview = (quality: number) => {
mutate({ pageId, quality });
};
return (
<Paper withBorder p="md" mb="xl" radius="md" bg="gray.0">
<Stack gap="xs">
<Group gap="xs">
<IconBrain size={20} color="var(--mantine-color-blue-filled)" />
<Text fw={600} size="sm">
{t("How well do you know this page?")}
</Text>
</Group>
<Group gap="xs">
<Button
variant="light"
color="red"
size="compact-sm"
onClick={() => handleReview(1)}
disabled={isPending}
>
{t("Again")}
</Button>
<Button
variant="light"
color="orange"
size="compact-sm"
onClick={() => handleReview(2)}
disabled={isPending}
>
{t("Hard")}
</Button>
<Button
variant="light"
color="blue"
size="compact-sm"
onClick={() => handleReview(3)}
disabled={isPending}
>
{t("Good")}
</Button>
<Button
variant="light"
color="green"
size="compact-sm"
onClick={() => handleReview(4)}
disabled={isPending}
>
{t("Easy")}
</Button>
</Group>
</Stack>
</Paper>
);
}
@@ -21,6 +21,8 @@ import {
getAllSidebarPages,
getDeletedPages,
restorePage,
reviewPage,
getPagesToReview,
} from "@/features/page/services/page-service";
import {
IMovePage,
@@ -607,7 +609,33 @@ export function invalidateOnDeletePage(pageId: string) {
});
//update recent changes
queryClient.invalidateQueries({
queryClient.invalidateQueries({
queryKey: ["recent-changes"],
});
}
export function useReviewListQuery(spaceId?: string, onlyDue?: boolean) {
return useInfiniteQuery({
queryKey: ["review-list", spaceId, onlyDue],
queryFn: ({ pageParam }) =>
getPagesToReview({ spaceId, onlyDue, cursor: pageParam, limit: 15 }),
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) =>
lastPage.meta.hasNextPage ? lastPage.meta.nextCursor : undefined,
refetchOnMount: true,
});
}
export function useReviewMutation() {
const { t } = useTranslation();
return useMutation<IPage, Error, { pageId: string; quality: number }>({
mutationFn: ({ pageId, quality }) => reviewPage(pageId, quality),
onSuccess: () => {
notifications.show({ message: t("Review updated") });
queryClient.invalidateQueries({ queryKey: ["review-list"] });
},
onError: () => {
notifications.show({ message: t("Failed to update review"), color: "red" });
},
});
}
@@ -194,3 +194,13 @@ export async function uploadFile(
return req as unknown as IAttachment;
}
export async function reviewPage(pageId: string, quality: number): Promise<IPage> {
const req = await api.post<IPage>("/pages/review", { pageId, quality });
return req.data;
}
export async function getPagesToReview(params?: QueryParams & { spaceId?: string; onlyDue?: boolean }): Promise<IPagination<IPage>> {
const req = await api.post("/pages/review-list", params);
return req.data;
}
@@ -28,6 +28,15 @@ export interface IPage {
canEdit: boolean;
hasRestriction: boolean;
};
nextReviewDate?: Date;
easeFactor?: number;
repetitionCount?: number;
reviewInterval?: number;
stability?: number;
difficulty?: number;
lapses?: number;
state?: number;
lastReview?: Date;
}
export interface IContributor {
@@ -1,8 +1,9 @@
import { Text, Tabs, Space } from "@mantine/core";
import { IconClockHour3, IconStar, IconUser } from "@tabler/icons-react";
import { IconClockHour3, IconSchool, IconStar, IconUser, IconCalendarStats } from "@tabler/icons-react";
import RecentChanges from "@/components/common/recent-changes";
import FavoritesPages from "@/features/home/components/favorites-pages";
import CreatedByMe from "@/features/home/components/created-by-me";
import NeedToReview from "@/features/home/components/need-to-review";
import { useParams } from "react-router-dom";
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query";
import { useTranslation } from "react-i18next";
@@ -39,6 +40,16 @@ export default function SpaceHomeTabs() {
{t("Created by me")}
</Text>
</Tabs.Tab>
<Tabs.Tab value="review" leftSection={<IconSchool size={18} />}>
<Text size="sm" fw={500}>
{t("Need to Review")}
</Text>
</Tabs.Tab>
<Tabs.Tab value="plan" leftSection={<IconCalendarStats size={18} />}>
<Text size="sm" fw={500}>
{t("Plan")}
</Text>
</Tabs.Tab>
</Tabs.List>
<Space my="md" />
@@ -52,6 +63,12 @@ export default function SpaceHomeTabs() {
<Tabs.Panel value="created">
{space?.id && <CreatedByMe spaceId={space.id} />}
</Tabs.Panel>
<Tabs.Panel value="review">
{space?.id && <NeedToReview spaceId={space.id} />}
</Tabs.Panel>
<Tabs.Panel value="plan">
{space?.id && <NeedToReview spaceId={space.id} onlyDue={false} />}
</Tabs.Panel>
</Tabs>
);
}
+6 -1
View File
@@ -10,8 +10,9 @@ import { useTranslation } from "react-i18next";
import React from "react";
import { EmptyState } from "@/components/ui/empty-state.tsx";
import { IconAlertTriangle, IconFileOff } from "@tabler/icons-react";
import { Button } from "@mantine/core";
import { Button, Container } from "@mantine/core";
import { Link } from "react-router-dom";
import ReviewButtons from "@/features/page/components/review-buttons";
import { ErrorBoundary } from "react-error-boundary";
const MemoizedFullEditor = React.memo(FullEditor);
const MemoizedPageHeader = React.memo(PageHeader);
@@ -99,6 +100,10 @@ function PageContent({ pageSlug }: { pageSlug: string | undefined }) {
<MemoizedPageHeader readOnly={!canEdit} />
<Container size="900" px="xs" pt="50">
<ReviewButtons pageId={page.id} />
</Container>
<MemoizedFullEditor
key={page.id}
pageId={page.id}
@@ -0,0 +1,18 @@
import { IsEnum, IsUUID } from 'class-validator';
export enum ReviewQuality {
BLACKOUT = 0,
WRONG = 1,
HARD = 2,
GOOD = 3,
EASY = 4,
PERFECT = 5,
}
export class PageReviewDto {
@IsUUID()
pageId: string;
@IsEnum(ReviewQuality)
quality: ReviewQuality;
}
@@ -21,6 +21,7 @@ import {
PageIdDto,
PageInfoDto,
} from './dto/page.dto';
import { PageReviewDto } from './dto/page-review.dto';
import { PageHistoryService } from './services/page-history.service';
import { AuthUser } from '../../common/decorators/auth-user.decorator';
import { AuthWorkspace } from '../../common/decorators/auth-workspace.decorator';
@@ -647,4 +648,38 @@ export class PageController {
return this.pageService.getPageBreadCrumbs(page.id);
}
@HttpCode(HttpStatus.OK)
@Post('review')
async review(@Body() dto: PageReviewDto, @AuthUser() user: User) {
const page = await this.pageRepo.findById(dto.pageId);
if (!page) {
throw new NotFoundException('Page not found');
}
await this.pageAccessService.validateCanView(page, user);
return this.pageService.review(dto.pageId, dto.quality);
}
@HttpCode(HttpStatus.OK)
@Post('review-list')
async reviewList(
@Body() dto: { spaceId?: string; onlyDue?: boolean },
@Body() pagination: PaginationOptions,
@AuthUser() user: User,
) {
if (dto.spaceId) {
const ability = await this.spaceAbility.createForUser(user, dto.spaceId);
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) {
throw new ForbiddenException();
}
}
return this.pageService.getPagesToReview(
user.id,
pagination,
dto.spaceId,
dto.onlyDue ?? true,
);
}
}
@@ -9,6 +9,7 @@ import { ContentOperation, UpdatePageDto } from '../dto/update-page.dto';
import { PageRepo } from '@docmost/db/repos/page/page.repo';
import { PagePermissionRepo } from '@docmost/db/repos/page/page-permission.repo';
import { InsertablePage, Page, User } from '@docmost/db/types/entity.types';
import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import {
CursorPaginationResult,
@@ -71,7 +72,8 @@ export class PageService {
private eventEmitter: EventEmitter2,
private collaborationGateway: CollaborationGateway,
private readonly watcherService: WatcherService,
) {}
private readonly spaceMemberRepo: SpaceMemberRepo,
) { }
async findById(
pageId: string,
@@ -1066,4 +1068,133 @@ export class PageService {
return pages.filter((p) => includedIds.has(p.id));
}
async review(pageId: string, quality: number): Promise<Page> {
console.log('FSRS REVIEW:', { pageId, quality });
const page = await this.pageRepo.findById(pageId);
if (!page) {
throw new NotFoundException('Page not found');
}
// FSRS v4.5 Weights
const w = [
0.4025, 1.4612, 3.3458, 15.6922, 5.8483, 1.2081, 1.2946, 0.0014, 1.5478,
0.133, 1.0124, 2.4467, 0.0641, 0.5044, 1.242, 0.2836, 2.8536,
];
let { stability: rawStability, difficulty: rawDifficulty, lapses, state, repetitionCount, lastReview } = page;
// Default values if null
const stability = Number(rawStability) || 0;
const difficulty = Number(rawDifficulty) || 0;
lapses = lapses || 0;
state = state || 0; // 0: New, 1: Learning, 2: Review, 3: Relearning
repetitionCount = repetitionCount || 0;
const now = new Date();
const elapsedDays = lastReview ? Math.max(0, Math.floor((now.getTime() - new Date(lastReview).getTime()) / (1000 * 60 * 60 * 24))) : 0;
let nextStability = 0;
let nextDifficulty = 0;
let nextState = state;
if (state === 0 || !lastReview) {
// First review
nextStability = w[quality - 1];
nextDifficulty = w[4] - w[5] * (quality - 3);
nextState = quality === 1 ? 1 : 2; // If Again, move to Learning, else Review
} else {
const retrievability = Math.pow(0.9, elapsedDays / stability);
if (quality === 1) {
// Failed
lapses++;
nextDifficulty = Math.min(Math.max(difficulty + w[6], 1), 10);
nextStability = w[10] * Math.pow(nextDifficulty, -w[11]) * (Math.pow(stability + 1, w[12]) - 1) * Math.exp(w[13] * (1 - retrievability));
nextState = 3; // Relearning
} else {
// Success
nextDifficulty = Math.min(Math.max(difficulty - w[6] * (quality - 3), 1), 10);
const hardPenalty = quality === 2 ? w[14] : 1;
const easyBonus = quality === 4 ? w[15] : 1;
nextStability = stability * (1 + Math.exp(w[7]) * (11 - nextDifficulty) * Math.pow(stability, -w[8]) * (Math.exp(w[9] * (1 - retrievability)) - 1) * hardPenalty * easyBonus);
nextState = 2; // Review
}
}
// Constraints
nextStability = Math.min(Math.max(nextStability, 0.1), 36500);
// FSRS interval formula: I = S * ln(target_recall) / ln(0.9)
// For default target_recall = 0.9, I = S
const exactReviewInterval = nextStability; // Keep as float for precision
const nextReviewDate = new Date();
// Convert days (stability) to minutes for precision
nextReviewDate.setMinutes(nextReviewDate.getMinutes() + Math.round(exactReviewInterval * 24 * 60));
await this.pageRepo.updatePage(
{
nextReviewDate,
lastReview: now,
stability: nextStability,
difficulty: nextDifficulty,
lapses,
state: nextState,
repetitionCount: repetitionCount + 1,
reviewInterval: Math.round(exactReviewInterval), // DB column is int4, must be integer
},
pageId,
);
return this.pageRepo.findById(pageId);
}
async getPagesToReview(userId: string, pagination: PaginationOptions, spaceId?: string, onlyDue = true) {
let query = this.db
.selectFrom('pages')
.innerJoin('spaces', 'spaces.id', 'pages.spaceId')
.select([
'pages.id',
'pages.slugId',
'pages.title',
'pages.icon',
'pages.spaceId',
'pages.nextReviewDate',
'pages.easeFactor',
'pages.repetitionCount',
'pages.reviewInterval',
'pages.createdAt',
'pages.updatedAt',
])
.select(
sql<{ name: string; slug: string }>`json_build_object('name', spaces.name, 'slug', spaces.slug)`.as('space'),
)
.where('pages.nextReviewDate', 'is not', null)
.where('pages.deletedAt', 'is', null)
.orderBy('pages.nextReviewDate', 'asc');
if (onlyDue) {
query = query.where('pages.nextReviewDate', '<=', new Date());
}
if (spaceId) {
query = query.where('pages.spaceId', '=', spaceId);
} else {
query = query.where('pages.spaceId', 'in', this.spaceMemberRepo.getUserSpaceIdsQuery(userId));
}
return executeWithCursorPagination(query, {
perPage: pagination.limit,
cursor: pagination.cursor,
fields: [
{ expression: 'pages.nextReviewDate', direction: 'asc' },
{ expression: 'pages.id', direction: 'asc' },
],
parseCursor: (cursor: any) => ({
nextReviewDate: new Date(cursor.nextReviewDate),
id: cursor.id,
}),
});
}
}
@@ -32,7 +32,9 @@ export class TrashCleanupService {
for (const workspace of workspaces) {
const retentionDays =
workspace.trashRetentionDays ?? DEFAULT_RETENTION_DAYS;
workspace.trashRetentionDays != null
? Number(workspace.trashRetentionDays)
: DEFAULT_RETENTION_DAYS;
const retentionDate = new Date();
retentionDate.setDate(retentionDate.getDate() - retentionDays);
@@ -366,9 +366,9 @@ export class WorkspaceService {
if (
typeof updateWorkspaceDto.trashRetentionDays !== 'undefined' &&
updateWorkspaceDto.trashRetentionDays !== ws.trashRetentionDays
updateWorkspaceDto.trashRetentionDays !== Number(ws.trashRetentionDays)
) {
before.trashRetentionDays = ws.trashRetentionDays;
before.trashRetentionDays = ws.trashRetentionDays != null ? Number(ws.trashRetentionDays) : null;
after.trashRetentionDays = updateWorkspaceDto.trashRetentionDays;
}
}
@@ -0,0 +1,21 @@
import { Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.alterTable('pages')
.addColumn('next_review_date', 'timestamptz')
.addColumn('ease_factor', 'float8', (col) => col.defaultTo(2.5).notNull())
.addColumn('repetition_count', 'int4', (col) => col.defaultTo(0).notNull())
.addColumn('review_interval', 'int4', (col) => col.defaultTo(0).notNull())
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema
.alterTable('pages')
.dropColumn('next_review_date')
.dropColumn('ease_factor')
.dropColumn('repetition_count')
.dropColumn('review_interval')
.execute();
}
@@ -0,0 +1,23 @@
import { Kysely } from 'kysely';
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.alterTable('pages')
.addColumn('stability', 'decimal', (col) => col.defaultTo(0))
.addColumn('difficulty', 'decimal', (col) => col.defaultTo(0))
.addColumn('lapses', 'integer', (col) => col.defaultTo(0))
.addColumn('state', 'integer', (col) => col.defaultTo(0))
.addColumn('last_review', 'timestamptz')
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema
.alterTable('pages')
.dropColumn('stability')
.dropColumn('difficulty')
.dropColumn('lapses')
.dropColumn('state')
.dropColumn('last_review')
.execute();
}
@@ -42,6 +42,15 @@ export class PageRepo {
'updatedAt',
'deletedAt',
'contributorIds',
'nextReviewDate',
'easeFactor',
'repetitionCount',
'reviewInterval',
'stability',
'difficulty',
'lapses',
'state',
'lastReview',
];
async findById(
+172 -166
View File
@@ -3,18 +3,13 @@
* Please do not edit it manually.
*/
import type { ColumnType } from 'kysely';
import type { ColumnType } from "kysely";
export type Generated<T> =
T extends ColumnType<infer S, infer I, infer U>
? ColumnType<S, I | undefined, U>
: ColumnType<T, T | undefined, T>;
export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
? ColumnType<S, I | undefined, U>
: ColumnType<T, T | undefined, T>;
export type Int8 = ColumnType<
string,
bigint | number | string,
bigint | number | string
>;
export type Int8 = ColumnType<string, bigint | number | string, bigint | number | string>;
export type Json = JsonValue;
@@ -28,17 +23,44 @@ export type JsonPrimitive = boolean | number | string | null;
export type JsonValue = JsonArray | JsonObject | JsonPrimitive;
export type Numeric = ColumnType<string, number | string, number | string>;
export type Timestamp = ColumnType<Date, Date | string, Date | string>;
export interface AiChatMessages {
chatId: string;
content: string | null;
createdAt: Generated<Timestamp>;
deletedAt: Timestamp | null;
id: Generated<string>;
metadata: Json | null;
role: string;
toolCalls: Json | null;
tsv: string | null;
updatedAt: Generated<Timestamp>;
userId: string | null;
workspaceId: string;
}
export interface AiChats {
createdAt: Generated<Timestamp>;
creatorId: string;
deletedAt: Timestamp | null;
id: Generated<string>;
title: string | null;
updatedAt: Generated<Timestamp>;
workspaceId: string;
}
export interface ApiKeys {
createdAt: Generated<Timestamp>;
creatorId: string;
deletedAt: Timestamp | null;
expiresAt: Timestamp | null;
id: Generated<string>;
lastUsedAt: Timestamp | null;
name: string | null;
updatedAt: Generated<Timestamp>;
creatorId: string;
workspaceId: string;
}
@@ -93,25 +115,25 @@ export interface AuthProviders {
createdAt: Generated<Timestamp>;
creatorId: string | null;
deletedAt: Timestamp | null;
groupSync: Generated<boolean>;
id: Generated<string>;
isEnabled: Generated<boolean>;
groupSync: Generated<boolean>;
ldapBaseDn: string | null;
ldapBindDn: string | null;
ldapBindPassword: string | null;
ldapConfig: Generated<Json | null>;
ldapTlsCaCert: string | null;
ldapTlsEnabled: Generated<boolean | null>;
ldapUrl: string | null;
ldapUserAttributes: Json | null;
ldapUserAttributes: Generated<Json | null>;
ldapUserSearchFilter: string | null;
ldapConfig: Json | null;
settings: Json | null;
name: string;
oidcClientId: string | null;
oidcClientSecret: string | null;
oidcIssuer: string | null;
samlCertificate: string | null;
samlUrl: string | null;
settings: Generated<Json | null>;
type: string;
updatedAt: Generated<Timestamp>;
workspaceId: string;
@@ -176,14 +198,14 @@ export interface Comments {
}
export interface Favorites {
createdAt: Generated<Timestamp>;
id: Generated<string>;
userId: string;
pageId: string | null;
spaceId: string | null;
templateId: string | null;
type: string;
userId: string;
workspaceId: string;
createdAt: Generated<Timestamp>;
}
export interface FileTasks {
@@ -226,6 +248,34 @@ export interface GroupUsers {
userId: string;
}
export interface Notifications {
actorId: string | null;
archivedAt: Timestamp | null;
commentId: string | null;
createdAt: Generated<Timestamp>;
data: Json | null;
emailedAt: Timestamp | null;
id: Generated<string>;
pageId: string | null;
pageVerificationId: string | null;
readAt: Timestamp | null;
spaceId: string | null;
type: string;
userId: string;
workspaceId: string;
}
export interface PageAccess {
accessLevel: string;
createdAt: Generated<Timestamp>;
creatorId: string | null;
id: Generated<string>;
pageId: string;
spaceId: string;
updatedAt: Generated<Timestamp>;
workspaceId: string;
}
export interface PageHistory {
content: Json | null;
contributorIds: Generated<string[] | null>;
@@ -244,6 +294,17 @@ export interface PageHistory {
workspaceId: string;
}
export interface PagePermissions {
addedById: string | null;
createdAt: Generated<Timestamp>;
groupId: string | null;
id: Generated<string>;
pageAccessId: string;
role: string;
updatedAt: Generated<Timestamp>;
userId: string | null;
}
export interface Pages {
content: Json | null;
contributorIds: Generated<string[] | null>;
@@ -252,14 +313,23 @@ export interface Pages {
creatorId: string | null;
deletedAt: Timestamp | null;
deletedById: string | null;
difficulty: Generated<Numeric | null>;
easeFactor: Generated<number>;
icon: string | null;
id: Generated<string>;
isLocked: Generated<boolean>;
lapses: Generated<number | null>;
lastReview: Timestamp | null;
lastUpdatedById: string | null;
nextReviewDate: Timestamp | null;
parentPageId: string | null;
position: string | null;
repetitionCount: Generated<number>;
reviewInterval: Generated<number>;
slugId: string;
spaceId: string;
stability: Generated<Numeric | null>;
state: Generated<number | null>;
textContent: string | null;
title: string | null;
tsv: string | null;
@@ -268,6 +338,39 @@ export interface Pages {
ydoc: Buffer | null;
}
export interface PageVerifications {
createdAt: Generated<Timestamp>;
creatorId: string | null;
data: Json | null;
expiresAt: Timestamp | null;
id: Generated<string>;
mode: string | null;
pageId: string;
periodAmount: number | null;
periodUnit: string | null;
rejectedAt: Timestamp | null;
rejectedById: string | null;
rejectionComment: string | null;
requestedAt: Timestamp | null;
requestedById: string | null;
spaceId: string;
status: string | null;
type: Generated<string>;
updatedAt: Generated<Timestamp>;
verifiedAt: Timestamp | null;
verifiedById: string | null;
workspaceId: string;
}
export interface PageVerifiers {
addedById: string | null;
createdAt: Generated<Timestamp>;
id: Generated<string>;
isPrimary: Generated<boolean>;
pageVerificationId: string;
userId: string;
}
export interface Shares {
createdAt: Generated<Timestamp>;
creatorId: string | null;
@@ -310,6 +413,25 @@ export interface Spaces {
workspaceId: string;
}
export interface Templates {
collaboratorIds: string[] | null;
content: Json | null;
createdAt: Generated<Timestamp>;
creatorId: string | null;
deletedAt: Timestamp | null;
description: string | null;
icon: string | null;
id: Generated<string>;
lastUpdatedById: string | null;
spaceId: string | null;
textContent: string | null;
title: string | null;
tsv: string | null;
updatedAt: Generated<Timestamp>;
workspaceId: string;
ydoc: Buffer | null;
}
export interface UserMfa {
backupCodes: string[] | null;
createdAt: Generated<Timestamp>;
@@ -329,12 +451,12 @@ export interface Users {
deletedAt: Timestamp | null;
email: string;
emailVerifiedAt: Timestamp | null;
hasGeneratedPassword: Generated<boolean>;
id: Generated<string>;
invitedById: string | null;
lastActiveAt: Timestamp | null;
lastLoginAt: Timestamp | null;
locale: string | null;
hasGeneratedPassword: Generated<boolean | null>;
name: string | null;
password: string | null;
role: string | null;
@@ -344,6 +466,21 @@ export interface Users {
workspaceId: string | null;
}
export interface UserSessions {
createdAt: Generated<Timestamp>;
deviceName: string | null;
expiresAt: Timestamp;
geoLocation: string | null;
id: Generated<string>;
ipAddress: string | null;
lastActiveAt: Generated<Timestamp>;
metadata: Json | null;
revokedAt: Timestamp | null;
userAgent: string | null;
userId: string;
workspaceId: string;
}
export interface UserTokens {
createdAt: Generated<Timestamp>;
expiresAt: Timestamp | null;
@@ -355,6 +492,18 @@ export interface UserTokens {
workspaceId: string | null;
}
export interface Watchers {
addedById: string | null;
createdAt: Generated<Timestamp>;
id: Generated<string>;
mutedAt: Timestamp | null;
pageId: string | null;
spaceId: string;
type: string;
userId: string;
workspaceId: string;
}
export interface WorkspaceInvitations {
createdAt: Generated<Timestamp>;
email: string | null;
@@ -368,8 +517,7 @@ export interface WorkspaceInvitations {
}
export interface Workspaces {
auditRetentionDays: Generated<number>;
trashRetentionDays: Generated<number>;
auditRetentionDays: Int8 | null;
billingEmail: string | null;
createdAt: Generated<Timestamp>;
customDomain: string | null;
@@ -389,156 +537,14 @@ export interface Workspaces {
settings: Json | null;
status: string | null;
stripeCustomerId: string | null;
trashRetentionDays: Int8 | null;
trialEndAt: Timestamp | null;
updatedAt: Generated<Timestamp>;
}
export interface Notifications {
id: Generated<string>;
userId: string;
workspaceId: string;
type: string;
actorId: string | null;
pageId: string | null;
spaceId: string | null;
commentId: string | null;
pageVerificationId: string | null;
data: Json | null;
readAt: Timestamp | null;
emailedAt: Timestamp | null;
archivedAt: Timestamp | null;
createdAt: Generated<Timestamp>;
}
export interface Watchers {
id: Generated<string>;
userId: string;
pageId: string | null;
spaceId: string;
workspaceId: string;
type: string;
addedById: string | null;
mutedAt: Timestamp | null;
createdAt: Generated<Timestamp>;
}
export interface PageAccess {
id: Generated<string>;
pageId: string;
workspaceId: string;
spaceId: string;
accessLevel: string;
creatorId: string | null;
createdAt: Generated<Timestamp>;
updatedAt: Generated<Timestamp>;
}
export interface PagePermissions {
id: Generated<string>;
pageAccessId: string;
userId: string | null;
groupId: string | null;
role: string;
addedById: string | null;
createdAt: Generated<Timestamp>;
updatedAt: Generated<Timestamp>;
}
export interface PageVerifications {
id: Generated<string>;
pageId: string;
workspaceId: string;
spaceId: string;
type: Generated<string>;
status: string | null;
mode: string | null;
periodAmount: number | null;
periodUnit: string | null;
verifiedAt: Timestamp | null;
verifiedById: string | null;
expiresAt: Timestamp | null;
requestedAt: Timestamp | null;
requestedById: string | null;
rejectedAt: Timestamp | null;
rejectedById: string | null;
rejectionComment: string | null;
data: Json | null;
creatorId: string | null;
createdAt: Generated<Timestamp>;
updatedAt: Generated<Timestamp>;
}
export interface PageVerifiers {
id: Generated<string>;
pageVerificationId: string;
userId: string;
isPrimary: Generated<boolean>;
addedById: string | null;
createdAt: Generated<Timestamp>;
}
export interface Templates {
id: Generated<string>;
title: string | null;
description: string | null;
content: Json | null;
ydoc: Buffer | null;
icon: string | null;
spaceId: string | null;
workspaceId: string;
creatorId: string | null;
lastUpdatedById: string | null;
collaboratorIds: string[] | null;
textContent: string | null;
tsv: string | null;
createdAt: Generated<Timestamp>;
updatedAt: Generated<Timestamp>;
deletedAt: Timestamp | null;
}
export interface AiChats {
id: Generated<string>;
workspaceId: string;
creatorId: string;
title: string | null;
createdAt: Generated<Timestamp>;
updatedAt: Generated<Timestamp>;
deletedAt: Timestamp | null;
}
export interface AiChatMessages {
id: Generated<string>;
chatId: string;
workspaceId: string;
userId: string | null;
role: string;
content: string | null;
toolCalls: Json | null;
metadata: Json | null;
tsv: string | null;
createdAt: Generated<Timestamp>;
updatedAt: Generated<Timestamp>;
deletedAt: Timestamp | null;
}
export interface UserSessions {
id: Generated<string>;
userId: string;
workspaceId: string;
deviceName: string | null;
userAgent: string | null;
ipAddress: string | null;
geoLocation: string | null;
metadata: Json | null;
lastActiveAt: Generated<Timestamp>;
expiresAt: Timestamp;
revokedAt: Timestamp | null;
createdAt: Generated<Timestamp>;
}
export interface DB {
aiChats: AiChats;
aiChatMessages: AiChatMessages;
aiChats: AiChats;
apiKeys: ApiKeys;
attachments: Attachments;
audit: Audit;
@@ -553,11 +559,11 @@ export interface DB {
groupUsers: GroupUsers;
notifications: Notifications;
pageAccess: PageAccess;
pagePermissions: PagePermissions;
pageHistory: PageHistory;
pagePermissions: PagePermissions;
pages: Pages;
pageVerifications: PageVerifications;
pageVerifiers: PageVerifiers;
pages: Pages;
shares: Shares;
spaceMembers: SpaceMembers;
spaces: Spaces;