Просмотр исходного кода

Merge pull request #5778 from weseek/support/93924-typescriptize-tag-model

support: Typescriptize tag model
cao 3 лет назад
Родитель
Сommit
04f6d91a00

+ 2 - 2
packages/app/src/components/Page/TagsInput.tsx

@@ -5,7 +5,7 @@ import { AsyncTypeahead } from 'react-bootstrap-typeahead';
 
 import { apiGet } from '~/client/util/apiv1-client';
 import { toastError } from '~/client/util/apiNotification';
-import { ITagsSearchApiv1Result } from '~/interfaces/tag';
+import { IResTagsSearchApiv1 } from '~/interfaces/tag';
 
 type TypeaheadInstance = {
   _handleMenuItemSelect: (activeItem: string, event: React.KeyboardEvent) => void,
@@ -36,7 +36,7 @@ const TagsInput: FC<Props> = (props: Props) => {
     setLoading(true);
     try {
       // TODO: 91698 SWRize
-      const res = await apiGet('/tags.search', { q: query }) as ITagsSearchApiv1Result;
+      const res = await apiGet('/tags.search', { q: query }) as IResTagsSearchApiv1;
       res.tags.unshift(query);
       setResultTags(Array.from(new Set(res.tags)));
     }

+ 2 - 2
packages/app/src/components/Sidebar/Tag.tsx

@@ -2,7 +2,7 @@ import React, { FC, useState, useCallback } from 'react';
 
 import { useTranslation } from 'react-i18next';
 
-import { ITagCountHasId } from '~/interfaces/tag';
+import { IDataTagCount } from '~/interfaces/tag';
 import { useSWRxTagsList } from '~/stores/tag';
 
 import TagCloudBox from '../TagCloudBox';
@@ -16,7 +16,7 @@ const Tag: FC = () => {
   const [offset, setOffset] = useState<number>(0);
 
   const { data: tagDataList, mutate: mutateTagDataList, error } = useSWRxTagsList(PAGING_LIMIT, offset);
-  const tagData: ITagCountHasId[] = tagDataList?.data || [];
+  const tagData: IDataTagCount[] = tagDataList?.data || [];
   const totalCount: number = tagDataList?.totalCount || 0;
   const isLoading = tagDataList === undefined && error == null;
 

+ 5 - 3
packages/app/src/components/TagCloudBox.tsx

@@ -1,9 +1,11 @@
 import React, { FC, memo } from 'react';
+
 import { TagCloud } from 'react-tagcloud';
-import { ITagCountHasId } from '~/interfaces/tag';
+
+import { IDataTagCount } from '~/interfaces/tag';
 
 type Props = {
-  tags:ITagCountHasId[],
+  tags:IDataTagCount[],
   minSize?: number,
   maxSize?: number,
   maxTagTextLength?: number,
@@ -29,7 +31,7 @@ const TagCloudBox: FC<Props> = memo((props:(Props & typeof defaultProps)) => {
       <TagCloud
         minSize={minSize ?? MIN_FONT_SIZE}
         maxSize={maxSize ?? MAX_FONT_SIZE}
-        tags={tags.map((tag:ITagCountHasId) => {
+        tags={tags.map((tag:IDataTagCount) => {
           return {
             // text truncation
             value: (tag.name).length > maxTagTextLength ? `${(tag.name).slice(0, maxTagTextLength)}...` : tag.name,

+ 3 - 3
packages/app/src/components/TagList.tsx

@@ -4,12 +4,12 @@ import React, {
 
 import { useTranslation } from 'react-i18next';
 
-import { ITagCountHasId } from '~/interfaces/tag';
+import { IDataTagCount } from '~/interfaces/tag';
 
 import PaginationWrapper from './PaginationWrapper';
 
 type TagListProps = {
-  tagData: ITagCountHasId[],
+  tagData: IDataTagCount[],
   totalTags: number,
   activePage: number,
   onChangePage?: (selectedPageNumber: number) => void,
@@ -29,7 +29,7 @@ const TagList: FC<TagListProps> = (props:(TagListProps & typeof defaultProps)) =
   const { t } = useTranslation('');
 
   const generateTagList = useCallback((tagData) => {
-    return tagData.map((tag:ITagCountHasId, index:number) => {
+    return tagData.map((tag:IDataTagCount, index:number) => {
       const tagListClasses: string = index === 0 ? 'list-group-item d-flex' : 'list-group-item d-flex border-top-0';
 
       return (

+ 2 - 2
packages/app/src/components/TagPage.tsx

@@ -2,7 +2,7 @@ import React, { FC, useState, useCallback } from 'react';
 
 import { useTranslation } from 'react-i18next';
 
-import { ITagCountHasId } from '~/interfaces/tag';
+import { IDataTagCount } from '~/interfaces/tag';
 import { useSWRxTagsList } from '~/stores/tag';
 
 import TagCloudBox from './TagCloudBox';
@@ -15,7 +15,7 @@ const TagPage: FC = () => {
   const [offset, setOffset] = useState<number>(0);
 
   const { data: tagDataList, error } = useSWRxTagsList(PAGING_LIMIT, offset);
-  const tagData: ITagCountHasId[] = tagDataList?.data || [];
+  const tagData: IDataTagCount[] = tagDataList?.data || [];
   const totalCount: number = tagDataList?.totalCount || 0;
   const isLoading = tagDataList === undefined && error == null;
 

+ 3 - 3
packages/app/src/interfaces/page.ts

@@ -1,9 +1,9 @@
 import { Ref, Nullable } from './common';
-import { IUser } from './user';
-import { IRevision, HasRevisionShortbody } from './revision';
-import { ITag } from './tag';
 import { HasObjectId } from './has-object-id';
+import { IRevision, HasRevisionShortbody } from './revision';
 import { SubscriptionStatusType } from './subscription';
+import { ITag } from './tag';
+import { IUser } from './user';
 
 
 export interface IPage {

+ 6 - 9
packages/app/src/interfaces/tag.ts

@@ -1,21 +1,18 @@
-import { HasObjectId } from './has-object-id';
-
-export type ITag = {
+export type ITag<ID = string> = {
+  _id: ID
   name: string,
-  createdAt: Date;
 }
 
-export type ITagCount = Omit<ITag, 'createdAt'> & {count: number}
+export type IDataTagCount = ITag & {count: number}
 
-export type ITagCountHasId = ITagCount & HasObjectId
 
-export type ITagsSearchApiv1Result = {
+export type IResTagsSearchApiv1 = {
   ok: boolean,
   tags: string[]
 }
 
-export type ITagsListApiv1Result = {
+export type IResTagsListApiv1 = {
   ok: boolean,
-  data: ITagCountHasId[],
+  data: IDataTagCount[],
   totalCount: number,
 }

+ 13 - 12
packages/app/src/server/crowi/index.js

@@ -1,12 +1,13 @@
 /* eslint-disable @typescript-eslint/no-this-alias */
 
-import path from 'path';
 import http from 'http';
-import mongoose from 'mongoose';
+import path from 'path';
 
 import { createTerminus } from '@godaddy/terminus';
-
 import { initMongooseGlobalSettings, getMongoUri, mongoOptions } from '@growi/core';
+import mongoose from 'mongoose';
+
+
 import pkg from '^/package.json';
 
 import CdnResourcesService from '~/services/cdn-resources-service';
@@ -15,26 +16,25 @@ import Xss from '~/services/xss';
 import loggerFactory from '~/utils/logger';
 import { projectRoot } from '~/utils/project-dir-utils';
 
-import ConfigManager from '../service/config-manager';
-import AppService from '../service/app';
+import Activity from '../models/activity';
+import PageRedirect from '../models/page-redirect';
+import Tag from '../models/tag';
+import UserGroup from '../models/user-group';
 import AclService from '../service/acl';
-import SearchService from '../service/search';
+import AppService from '../service/app';
 import AttachmentService from '../service/attachment';
+import ConfigManager from '../service/config-manager';
+import { InstallerService } from '../service/installer';
 import PageService from '../service/page';
 import PageGrantService from '../service/page-grant';
 import PageOperationService from '../service/page-operation';
+import SearchService from '../service/search';
 import { SlackIntegrationService } from '../service/slack-integration';
 import { UserNotificationService } from '../service/user-notification';
-import { InstallerService } from '../service/installer';
-import Activity from '../models/activity';
-import UserGroup from '../models/user-group';
-import PageRedirect from '../models/page-redirect';
 
 const logger = loggerFactory('growi:crowi');
 const httpErrorHandler = require('../middlewares/http-error-handler');
-
 const models = require('../models');
-
 const PluginService = require('../plugins/plugin.service');
 
 const sep = path.sep;
@@ -281,6 +281,7 @@ Crowi.prototype.setupModels = async function() {
 
   // include models that independent from crowi
   allModels.Activity = Activity;
+  allModels.Tag = Tag;
   allModels.UserGroup = UserGroup;
   allModels.PageRedirect = PageRedirect;
 

+ 3 - 5
packages/app/src/server/models/page-tag-relation.js

@@ -1,8 +1,9 @@
+import Tag from './tag';
+
 // disable no-return-await for model functions
 /* eslint-disable no-return-await */
 
 const flatMap = require('array.prototype.flatmap');
-
 const mongoose = require('mongoose');
 const mongoosePaginate = require('mongoose-paginate-v2');
 const uniqueValidator = require('mongoose-unique-validator');
@@ -110,8 +111,7 @@ class PageTagRelation {
       .flatMap(result => result.tagIds); // map + flatten
     const distinctTagIds = Array.from(new Set(allTagIds));
 
-    // retrieve tag documents
-    const Tag = mongoose.model('Tag');
+    // TODO: set IdToNameMap type by 93933
     const tagIdToNameMap = await Tag.getIdToNameMap(distinctTagIds);
 
     // convert to map
@@ -136,8 +136,6 @@ class PageTagRelation {
     // eslint-disable-next-line no-param-reassign
     tags = tags.filter((tag) => { return tag !== '' });
 
-    const Tag = mongoose.model('Tag');
-
     // get relations for this page
     const relations = await this.findByPageId(pageId, { nullable: true });
 

+ 0 - 69
packages/app/src/server/models/tag.js

@@ -1,69 +0,0 @@
-// disable no-return-await for model functions
-/* eslint-disable no-return-await */
-
-const mongoose = require('mongoose');
-const mongoosePaginate = require('mongoose-paginate-v2');
-const uniqueValidator = require('mongoose-unique-validator');
-
-/*
- * define schema
- */
-const schema = new mongoose.Schema({
-  name: {
-    type: String,
-    required: true,
-    unique: true,
-  },
-});
-schema.plugin(mongoosePaginate);
-schema.plugin(uniqueValidator);
-
-/**
- * Tag Class
- *
- * @class Tag
- */
-class Tag {
-
-  static async getIdToNameMap(tagIds) {
-    const tags = await this.find({ _id: { $in: tagIds } });
-
-    const idToNameMap = {};
-    tags.forEach((tag) => {
-      idToNameMap[tag._id.toString()] = tag.name;
-    });
-
-    return idToNameMap;
-  }
-
-  static async findOrCreate(tagName) {
-    const tag = await this.findOne({ name: tagName });
-    if (!tag) {
-      return this.create({ name: tagName });
-    }
-    return tag;
-  }
-
-  static async findOrCreateMany(tagNames) {
-    const existTags = await this.find({ name: { $in: tagNames } });
-    const existTagNames = existTags.map((tag) => { return tag.name });
-
-    // bulk insert
-    const tagsToCreate = tagNames.filter((tagName) => { return !existTagNames.includes(tagName) });
-    await this.insertMany(
-      tagsToCreate.map((tag) => {
-        return { name: tag };
-      }),
-    );
-
-    return this.find({ name: { $in: tagNames } });
-  }
-
-}
-
-module.exports = function(crowi) {
-  Tag.crowi = crowi;
-  schema.loadClass(Tag);
-  const model = mongoose.model('Tag', schema);
-  return model;
-};

+ 63 - 0
packages/app/src/server/models/tag.ts

@@ -0,0 +1,63 @@
+import { getOrCreateModel } from '@growi/core';
+import {
+  Types, Model, Schema,
+} from 'mongoose';
+
+import { ObjectIdLike } from '../interfaces/mongoose-utils';
+
+const mongoosePaginate = require('mongoose-paginate-v2');
+const uniqueValidator = require('mongoose-unique-validator');
+
+
+export interface TagDocument {
+  _id: Types.ObjectId;
+  name: string;
+}
+
+export type IdToNameMap = {[key: string] : string }
+
+export interface TagModel extends Model<TagDocument>{
+  getIdToNameMap(tagIds: ObjectIdLike[]): IdToNameMap
+  findOrCreateMany(tagNames: string[]): Promise<TagDocument[]>
+}
+
+
+const tagSchema = new Schema<TagDocument, TagModel>({
+  name: {
+    type: String,
+    require: true,
+    unique: true,
+  },
+});
+tagSchema.plugin(mongoosePaginate);
+tagSchema.plugin(uniqueValidator);
+
+
+tagSchema.statics.getIdToNameMap = async function(tagIds: ObjectIdLike[]): Promise<IdToNameMap> {
+  const tags = await this.find({ _id: { $in: tagIds } });
+
+  const idToNameMap = {};
+  tags.forEach((tag) => {
+    idToNameMap[tag._id.toString()] = tag.name;
+  });
+
+  return idToNameMap;
+};
+
+tagSchema.statics.findOrCreateMany = async function(tagNames: string[]): Promise<TagDocument[]> {
+  const existTags = await this.find({ name: { $in: tagNames } });
+  const existTagNames = existTags.map((tag) => { return tag.name });
+
+  // bulk insert
+  const tagsToCreate = tagNames.filter((tagName) => { return !existTagNames.includes(tagName) });
+  await this.insertMany(
+    tagsToCreate.map((tag) => {
+      return { name: tag };
+    }),
+  );
+
+  return this.find({ name: { $in: tagNames } });
+};
+
+
+export default getOrCreateModel<TagDocument, TagModel>('Tag', tagSchema);

+ 2 - 1
packages/app/src/server/routes/tag.js

@@ -1,3 +1,5 @@
+import Tag from '~/server/models/tag';
+
 /**
  * @swagger
  *
@@ -29,7 +31,6 @@
  */
 module.exports = function(crowi, app) {
 
-  const Tag = crowi.model('Tag');
   const PageTagRelation = crowi.model('PageTagRelation');
   const ApiResponse = require('../util/apiResponse');
   const actions = {};

+ 3 - 3
packages/app/src/stores/tag.tsx

@@ -2,11 +2,11 @@ import { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
 import { apiGet } from '~/client/util/apiv1-client';
-import { ITagsListApiv1Result } from '~/interfaces/tag';
+import { IResTagsListApiv1 } from '~/interfaces/tag';
 
-export const useSWRxTagsList = (limit?: number, offset?: number): SWRResponse<ITagsListApiv1Result, Error> => {
+export const useSWRxTagsList = (limit?: number, offset?: number): SWRResponse<IResTagsListApiv1, Error> => {
   return useSWRImmutable(
     ['/tags.list', limit, offset],
-    (endpoint, limit, offset) => apiGet(endpoint, { limit, offset }).then((result: ITagsListApiv1Result) => result),
+    (endpoint, limit, offset) => apiGet(endpoint, { limit, offset }).then((result: IResTagsListApiv1) => result),
   );
 };

+ 1 - 2
packages/app/test/integration/models/v5.page.test.js

@@ -1,5 +1,6 @@
 import mongoose from 'mongoose';
 
+
 import { getInstance } from '../setup-crowi';
 
 describe('Page', () => {
@@ -7,7 +8,6 @@ describe('Page', () => {
   let Page;
   let Revision;
   let User;
-  let Tag;
   let PageTagRelation;
   let Bookmark;
   let Comment;
@@ -35,7 +35,6 @@ describe('Page', () => {
     User = mongoose.model('User');
     Page = mongoose.model('Page');
     Revision = mongoose.model('Revision');
-    Tag = mongoose.model('Tag');
     PageTagRelation = mongoose.model('PageTagRelation');
     Bookmark = mongoose.model('Bookmark');
     Comment = mongoose.model('Comment');

+ 3 - 2
packages/app/test/integration/service/page.test.js

@@ -1,8 +1,11 @@
 /* eslint-disable no-unused-vars */
 import { advanceTo } from 'jest-date-mock';
 
+import Tag from '~/server/models/tag';
+
 const mongoose = require('mongoose');
 
+
 const { getInstance } = require('../setup-crowi');
 
 let testUser1;
@@ -50,7 +53,6 @@ describe('PageService', () => {
   let Page;
   let Revision;
   let User;
-  let Tag;
   let PageTagRelation;
   let Bookmark;
   let Comment;
@@ -64,7 +66,6 @@ describe('PageService', () => {
     User = mongoose.model('User');
     Page = mongoose.model('Page');
     Revision = mongoose.model('Revision');
-    Tag = mongoose.model('Tag');
     PageTagRelation = mongoose.model('PageTagRelation');
     Bookmark = mongoose.model('Bookmark');
     Comment = mongoose.model('Comment');

+ 5 - 7
packages/app/test/integration/service/v5.non-public-page.test.ts

@@ -1,8 +1,8 @@
 /* eslint-disable no-unused-vars */
 import { advanceTo } from 'jest-date-mock';
-
 import mongoose from 'mongoose';
 
+import Tag from '../../../src/server/models/tag';
 import { getInstance } from '../setup-crowi';
 
 describe('PageService page operations with non-public pages', () => {
@@ -22,7 +22,6 @@ describe('PageService page operations with non-public pages', () => {
   let User;
   let UserGroup;
   let UserGroupRelation;
-  let Tag;
   let PageTagRelation;
   let Bookmark;
   let Comment;
@@ -93,7 +92,6 @@ describe('PageService page operations with non-public pages', () => {
     UserGroupRelation = mongoose.model('UserGroupRelation');
     Page = mongoose.model('Page');
     Revision = mongoose.model('Revision');
-    Tag = mongoose.model('Tag');
     PageTagRelation = mongoose.model('PageTagRelation');
     Bookmark = mongoose.model('Bookmark');
     Comment = mongoose.model('Comment');
@@ -1044,7 +1042,7 @@ describe('PageService page operations with non-public pages', () => {
       const trashedPage = await Page.findOne({ path: '/trash/np_revert1', status: Page.STATUS_DELETED, grant: Page.GRANT_RESTRICTED });
       const revision = await Revision.findOne({ pageId: trashedPage._id });
       const tag = await Tag.findOne({ name: 'np_revertTag1' });
-      const deletedPageTagRelation = await PageTagRelation.findOne({ relatedPage: trashedPage._id, relatedTag: tag._id, isPageTrashed: true });
+      const deletedPageTagRelation = await PageTagRelation.findOne({ relatedPage: trashedPage._id, relatedTag: tag?._id, isPageTrashed: true });
       expect(trashedPage).toBeTruthy();
       expect(revision).toBeTruthy();
       expect(tag).toBeTruthy();
@@ -1054,7 +1052,7 @@ describe('PageService page operations with non-public pages', () => {
 
       const revertedPage = await Page.findOne({ path: '/np_revert1' });
       const deltedPageBeforeRevert = await Page.findOne({ path: '/trash/np_revert1' });
-      const pageTagRelation = await PageTagRelation.findOne({ relatedPage: revertedPage._id, relatedTag: tag._id });
+      const pageTagRelation = await PageTagRelation.findOne({ relatedPage: revertedPage._id, relatedTag: tag?._id });
       expect(revertedPage).toBeTruthy();
       expect(pageTagRelation).toBeTruthy();
       expect(deltedPageBeforeRevert).toBeNull();
@@ -1071,7 +1069,7 @@ describe('PageService page operations with non-public pages', () => {
       const trashedPage = await Page.findOne({ path: beforeRevertPath, status: Page.STATUS_DELETED, grant: Page.GRANT_USER_GROUP });
       const revision = await Revision.findOne({ pageId: trashedPage._id });
       const tag = await Tag.findOne({ name: 'np_revertTag2' });
-      const deletedPageTagRelation = await PageTagRelation.findOne({ relatedPage: trashedPage._id, relatedTag: tag._id, isPageTrashed: true });
+      const deletedPageTagRelation = await PageTagRelation.findOne({ relatedPage: trashedPage._id, relatedTag: tag?._id, isPageTrashed: true });
       expect(trashedPage).toBeTruthy();
       expect(revision).toBeTruthy();
       expect(tag).toBeTruthy();
@@ -1081,7 +1079,7 @@ describe('PageService page operations with non-public pages', () => {
 
       const revertedPage = await Page.findOne({ path: '/np_revert2' });
       const trashedPageBR = await Page.findOne({ path: beforeRevertPath });
-      const pageTagRelation = await PageTagRelation.findOne({ relatedPage: revertedPage._id, relatedTag: tag._id });
+      const pageTagRelation = await PageTagRelation.findOne({ relatedPage: revertedPage._id, relatedTag: tag?._id });
       expect(revertedPage).toBeTruthy();
       expect(pageTagRelation).toBeTruthy();
       expect(trashedPageBR).toBeNull();

+ 8 - 10
packages/app/test/integration/service/v5.public-page.test.ts

@@ -1,8 +1,8 @@
 /* eslint-disable no-unused-vars */
 import { advanceTo } from 'jest-date-mock';
-
 import mongoose from 'mongoose';
 
+import Tag from '../../../src/server/models/tag';
 import { getInstance } from '../setup-crowi';
 
 describe('PageService page operations with only public pages', () => {
@@ -14,7 +14,6 @@ describe('PageService page operations with only public pages', () => {
   let Page;
   let Revision;
   let User;
-  let Tag;
   let PageTagRelation;
   let Bookmark;
   let Comment;
@@ -31,7 +30,6 @@ describe('PageService page operations with only public pages', () => {
     User = mongoose.model('User');
     Page = mongoose.model('Page');
     Revision = mongoose.model('Revision');
-    Tag = mongoose.model('Tag');
     PageTagRelation = mongoose.model('PageTagRelation');
     Bookmark = mongoose.model('Bookmark');
     Comment = mongoose.model('Comment');
@@ -1304,8 +1302,8 @@ describe('PageService page operations with only public pages', () => {
       const basePage = await Page.findOne({ path: '/v5_PageForDuplicate5' });
       const tag1 = await Tag.findOne({ name: 'duplicate_Tag1' });
       const tag2 = await Tag.findOne({ name:  'duplicate_Tag2' });
-      const basePageTagRelation1 = await PageTagRelation.findOne({ relatedTag: tag1._id });
-      const basePageTagRelation2 = await PageTagRelation.findOne({ relatedTag: tag2._id });
+      const basePageTagRelation1 = await PageTagRelation.findOne({ relatedTag: tag1?._id });
+      const basePageTagRelation2 = await PageTagRelation.findOne({ relatedTag: tag2?._id });
       expect(basePage).toBeTruthy();
       expect(tag1).toBeTruthy();
       expect(tag2).toBeTruthy();
@@ -1463,8 +1461,8 @@ describe('PageService page operations with only public pages', () => {
       const pageToDelete = await Page.findOne({ path: '/v5_PageForDelete6' });
       const tag1 = await Tag.findOne({ name: 'TagForDelete1' });
       const tag2 = await Tag.findOne({ name: 'TagForDelete2' });
-      const pageRelation1 = await PageTagRelation.findOne({ relatedTag: tag1._id });
-      const pageRelation2 = await PageTagRelation.findOne({ relatedTag: tag2._id });
+      const pageRelation1 = await PageTagRelation.findOne({ relatedTag: tag1?._id });
+      const pageRelation2 = await PageTagRelation.findOne({ relatedTag: tag2?._id });
       expect(pageToDelete).toBeTruthy();
       expect(tag1).toBeTruthy();
       expect(tag2).toBeTruthy();
@@ -1549,7 +1547,7 @@ describe('PageService page operations with only public pages', () => {
       await deleteCompletely(parentPage, dummyUser1, {}, true);
       const deletedPages = await Page.find({ _id: { $in: [parentPage._id, childPage._id, grandchildPage._id] } });
       const deletedRevisions = await Revision.find({ pageId: { $in: [parentPage._id, grandchildPage._id] } });
-      const tags = await Tag.find({ _id: { $in: [tag1._id, tag2._id] } });
+      const tags = await Tag.find({ _id: { $in: [tag1?._id, tag2?._id] } });
       const deletedPageTagRelations = await PageTagRelation.find({ _id: { $in: [pageTagRelation1._id, pageTagRelation2._id] } });
       const deletedBookmarks = await Bookmark.find({ _id: bookmark._id });
       const deletedComments = await Comment.find({ _id: comment._id });
@@ -1632,14 +1630,14 @@ describe('PageService page operations with only public pages', () => {
       const deletedPage = await Page.findOne({ path: '/trash/v5_revert1', status: Page.STATUS_DELETED });
       const revision = await Revision.findOne({ pageId: deletedPage._id });
       const tag = await Tag.findOne({ name: 'revertTag1' });
-      const deletedPageTagRelation = await PageTagRelation.findOne({ relatedPage: deletedPage._id, relatedTag: tag._id, isPageTrashed: true });
+      const deletedPageTagRelation = await PageTagRelation.findOne({ relatedPage: deletedPage._id, relatedTag: tag?._id, isPageTrashed: true });
       expect(deletedPage).toBeTruthy();
       expect(revision).toBeTruthy();
       expect(tag).toBeTruthy();
       expect(deletedPageTagRelation).toBeTruthy();
 
       const revertedPage = await revertDeletedPage(deletedPage, dummyUser1, {}, false);
-      const pageTagRelation = await PageTagRelation.findOne({ relatedPage: deletedPage._id, relatedTag: tag._id });
+      const pageTagRelation = await PageTagRelation.findOne({ relatedPage: deletedPage._id, relatedTag: tag?._id });
 
       expect(revertedPage.parent).toStrictEqual(rootPage._id);
       expect(revertedPage.path).toBe('/v5_revert1');