Yuki Takei 2 лет назад
Родитель
Сommit
b23d71e5ac

+ 1 - 0
apps/app/src/interfaces/page-tag-relation.ts

@@ -3,4 +3,5 @@ import type { IPage, ITag } from '@growi/core';
 export type IPageTagRelation = {
   relatedPage: IPage,
   relatedTag: ITag,
+  isPageTrashed: boolean,
 }

+ 1 - 0
apps/app/src/server/models/index.js → apps/app/src/server/models/index.ts

@@ -20,6 +20,7 @@ export * as PageRedirect from './page-redirect';
 export * as ShareLink from './share-link';
 export * as Tag from './tag';
 export * as UserGroup from './user-group';
+export * as PageTagRelation from './page-tag-relation';
 
 export * from './serializers';
 

+ 147 - 125
apps/app/src/server/models/page-tag-relation.ts

@@ -1,20 +1,54 @@
+import type { ITag } from '@growi/core';
+import type { Document, Model } from 'mongoose';
+import mongoose from 'mongoose';
+import mongoosePaginate from 'mongoose-paginate-v2';
+import uniqueValidator from 'mongoose-unique-validator';
+
+import type { IPageTagRelation } from '~/interfaces/page-tag-relation';
+
+import { getOrCreateModel } from '../util/mongoose-utils';
+
 import Tag from './tag';
 
+
+const ObjectId = mongoose.Schema.Types.ObjectId;
+
+
 // 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');
 
-const ObjectId = mongoose.Schema.Types.ObjectId;
+
+export interface PageTagRelationDocument extends IPageTagRelation, Document {
+}
+
+type CreateTagListWithCountOpts = {
+  sortOpt?: any,
+  offset?: number,
+  limit?: number,
+}
+type CreateTagListWithCountResult = {
+  data: ITag[],
+  totalCount: number
+}
+type CreateTagListWithCount = (opts?: CreateTagListWithCountOpts) => Promise<CreateTagListWithCountResult>;
+
+type UpdatePageTags = (pageId: string, tags: string[]) => Promise<void>
+
+export interface PageTagRelationModel extends Model<PageTagRelationDocument> {
+  createTagListWithCount: CreateTagListWithCount
+  findByPageId(pageId: string, options?: { nullable?: boolean }): Promise<PageTagRelationDocument[]>
+  listTagNamesByPage(pageId: string): Promise<PageTagRelationDocument[]>
+  getIdToTagNamesMap(): any
+  updatePageTags: UpdatePageTags
+}
 
 
 /*
  * define schema
  */
-const schema = new mongoose.Schema({
+const schema = new mongoose.Schema<PageTagRelationDocument, PageTagRelationModel>({
   relatedPage: {
     type: ObjectId,
     ref: 'Page',
@@ -39,142 +73,130 @@ schema.index({ relatedPage: 1, relatedTag: 1 }, { unique: true });
 schema.plugin(mongoosePaginate);
 schema.plugin(uniqueValidator);
 
-/**
- * PageTagRelation Class
- *
- * @class PageTagRelation
- */
-class PageTagRelation {
-
-  static async createTagListWithCount(option) {
-    const opt = option || {};
-    const sortOpt = opt.sortOpt || {};
-    const offset = opt.offset;
-    const limit = opt.limit;
-
-    const tags = await this.aggregate()
-      .match({ isPageTrashed: false })
-      .lookup({
-        from: 'tags',
-        localField: 'relatedTag',
-        foreignField: '_id',
-        as: 'tag',
-      })
-      .unwind('$tag')
-      .group({ _id: '$relatedTag', count: { $sum: 1 }, name: { $first: '$tag.name' } })
-      .sort(sortOpt)
-      .skip(offset)
-      .limit(limit);
-
-    const totalCount = (await this.find({ isPageTrashed: false }).distinct('relatedTag')).length;
-
-    return { data: tags, totalCount };
-  }
+const createTagListWithCount: CreateTagListWithCount = async function(opts) {
+  const sortOpt = opts?.sortOpt || {};
+  const offset = opts?.offset;
+  const limit = opts?.limit;
+
+  const tags = await this.aggregate()
+    .match({ isPageTrashed: false })
+    .lookup({
+      from: 'tags',
+      localField: 'relatedTag',
+      foreignField: '_id',
+      as: 'tag',
+    })
+    .unwind('$tag')
+    .group({ _id: '$relatedTag', count: { $sum: 1 }, name: { $first: '$tag.name' } })
+    .sort(sortOpt)
+    .skip(offset)
+    .limit(limit);
+
+  const totalCount = (await this.find({ isPageTrashed: false }).distinct('relatedTag')).length;
+
+  return { data: tags, totalCount };
+};
+schema.statics.createTagListWithCount = createTagListWithCount;
 
-  static async findByPageId(pageId, options = {}) {
-    const isAcceptRelatedTagNull = options.nullable || null;
-    const relations = await this.find({ relatedPage: pageId }).populate('relatedTag').select('relatedTag');
-    return isAcceptRelatedTagNull ? relations : relations.filter((relation) => { return relation.relatedTag !== null });
-  }
+schema.statics.findByPageId = async function(pageId, options = {}) {
+  const isAcceptRelatedTagNull = options.nullable || null;
+  const relations = await this.find({ relatedPage: pageId }).populate('relatedTag').select('relatedTag');
+  return isAcceptRelatedTagNull ? relations : relations.filter((relation) => { return relation.relatedTag !== null });
+};
 
-  static async listTagNamesByPage(pageId) {
-    const relations = await this.findByPageId(pageId);
-    return relations.map((relation) => { return relation.relatedTag.name });
-  }
+schema.statics.listTagNamesByPage = async function(pageId) {
+  const relations = await this.findByPageId(pageId);
+  return relations.map((relation) => { return relation.relatedTag.name });
+};
 
+/**
+ * @return {object} key: Page._id, value: array of tag names
+ */
+schema.statics.getIdToTagNamesMap = async function(pageIds) {
   /**
-   * @return {object} key: Page._id, value: array of tag names
+   * @see https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pivot-data
+   *
+   * results will be:
+   * [
+   *   { _id: 58dca7b2c435b3480098dbbc, tagIds: [ 5da630f71a677515601e36d7, 5da77163ec786e4fe43e0e3e ]},
+   *   { _id: 58dca7b2c435b3480098dbbd, tagIds: [ ... ]},
+   *   ...
+   * ]
    */
-  static async getIdToTagNamesMap(pageIds) {
-    /**
-     * @see https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pivot-data
-     *
-     * results will be:
-     * [
-     *   { _id: 58dca7b2c435b3480098dbbc, tagIds: [ 5da630f71a677515601e36d7, 5da77163ec786e4fe43e0e3e ]},
-     *   { _id: 58dca7b2c435b3480098dbbd, tagIds: [ ... ]},
-     *   ...
-     * ]
-     */
-    const results = await this.aggregate()
-      .match({ relatedPage: { $in: pageIds } })
-      .group({ _id: '$relatedPage', tagIds: { $push: '$relatedTag' } });
-
-    if (results.length === 0) {
-      return {};
-    }
+  const results = await this.aggregate()
+    .match({ relatedPage: { $in: pageIds } })
+    .group({ _id: '$relatedPage', tagIds: { $push: '$relatedTag' } });
 
-    results.flatMap = flatMap.shim(); // TODO: remove after upgrading to node v12
+  if (results.length === 0) {
+    return {};
+  }
 
-    // extract distinct tag ids
-    const allTagIds = results
-      .flatMap(result => result.tagIds); // map + flatten
-    const distinctTagIds = Array.from(new Set(allTagIds));
+  results.flatMap = flatMap.shim(); // TODO: remove after upgrading to node v12
 
-    // TODO: set IdToNameMap type by 93933
-    const tagIdToNameMap = await Tag.getIdToNameMap(distinctTagIds);
+  // extract distinct tag ids
+  const allTagIds = results
+    .flatMap(result => result.tagIds); // map + flatten
+  const distinctTagIds = Array.from(new Set(allTagIds));
 
-    // convert to map
-    const idToTagNamesMap = {};
-    results.forEach((result) => {
-      const tagNames = result.tagIds
-        .map(tagId => tagIdToNameMap[tagId])
-        .filter(tagName => tagName != null); // filter null object
+  // TODO: set IdToNameMap type by 93933
+  const tagIdToNameMap = await Tag.getIdToNameMap(distinctTagIds);
 
-      idToTagNamesMap[result._id] = tagNames;
-    });
+  // convert to map
+  const idToTagNamesMap = {};
+  results.forEach((result) => {
+    const tagNames = result.tagIds
+      .map(tagId => tagIdToNameMap[tagId])
+      .filter(tagName => tagName != null); // filter null object
 
-    return idToTagNamesMap;
-  }
+    idToTagNamesMap[result._id] = tagNames;
+  });
 
-  static async updatePageTags(pageId, tags) {
-    if (pageId == null || tags == null) {
-      throw new Error('args \'pageId\' and \'tags\' are required.');
-    }
+  return idToTagNamesMap;
+};
 
-    // filter empty string
-    // eslint-disable-next-line no-param-reassign
-    tags = tags.filter((tag) => { return tag !== '' });
+const updatePageTags: UpdatePageTags = async function(pageId, tags) {
+  if (pageId == null || tags == null) {
+    throw new Error('args \'pageId\' and \'tags\' are required.');
+  }
+
+  // filter empty string
+  // eslint-disable-next-line no-param-reassign
+  tags = tags.filter((tag) => { return tag !== '' });
 
-    // get relations for this page
-    const relations = await this.findByPageId(pageId, { nullable: true });
+  // get relations for this page
+  const relations = await this.findByPageId(pageId, { nullable: true });
 
-    const unlinkTagRelationIds = [];
-    const relatedTagNames = [];
+  const unlinkTagRelationIds: string[] = [];
+  const relatedTagNames: string[] = [];
 
-    relations.forEach((relation) => {
-      if (relation.relatedTag == null) {
+  relations.forEach((relation) => {
+    if (relation.relatedTag == null) {
+      unlinkTagRelationIds.push(relation._id);
+    }
+    else {
+      relatedTagNames.push(relation.relatedTag.name);
+      if (!tags.includes(relation.relatedTag.name)) {
         unlinkTagRelationIds.push(relation._id);
       }
-      else {
-        relatedTagNames.push(relation.relatedTag.name);
-        if (!tags.includes(relation.relatedTag.name)) {
-          unlinkTagRelationIds.push(relation._id);
-        }
-      }
-    });
-    const bulkDeletePromise = this.deleteMany({ _id: { $in: unlinkTagRelationIds } });
-    // find or create tags
-    const tagsToCreate = tags.filter((tag) => { return !relatedTagNames.includes(tag) });
-    const tagEntities = await Tag.findOrCreateMany(tagsToCreate);
-
-    // create relations
-    const bulkCreatePromise = this.insertMany(
-      tagEntities.map((relatedTag) => {
-        return {
-          relatedPage: pageId,
-          relatedTag,
-        };
-      }),
-    );
-
-    return Promise.all([bulkDeletePromise, bulkCreatePromise]);
-  }
-
-}
-
-module.exports = function() {
-  schema.loadClass(PageTagRelation);
-  const model = mongoose.model('PageTagRelation', schema);
-  return model;
+    }
+  });
+  const bulkDeletePromise = this.deleteMany({ _id: { $in: unlinkTagRelationIds } });
+  // find or create tags
+  const tagsToCreate = tags.filter((tag) => { return !relatedTagNames.includes(tag) });
+  const tagEntities = await Tag.findOrCreateMany(tagsToCreate);
+
+  // create relations
+  const bulkCreatePromise = this.insertMany(
+    tagEntities.map((relatedTag) => {
+      return {
+        relatedPage: pageId,
+        relatedTag,
+      };
+    }),
+  );
+
+  await Promise.all([bulkDeletePromise, bulkCreatePromise]);
 };
+schema.statics.updatePageTags = updatePageTags;
+
+export default getOrCreateModel<PageTagRelationDocument, PageTagRelationModel>('PageTagRelation', schema);

+ 1 - 1
apps/app/src/server/models/page.ts

@@ -77,7 +77,7 @@ export interface PageModel extends Model<PageDocument> {
   removeLeafEmptyPagesRecursively(pageId: ObjectIdLike): Promise<void>
   findTemplate(path: string): Promise<{
     templateBody?: string,
-    templateTags?: ITag[],
+    templateTags?: string[],
   }>
 
   PageQueryBuilder: typeof PageQueryBuilder

+ 3 - 2
apps/app/src/server/routes/tag.js

@@ -1,6 +1,9 @@
 import { SupportedAction } from '~/interfaces/activity';
 import Tag from '~/server/models/tag';
 
+import PageTagRelation from '../models/page-tag-relation';
+import ApiResponse from '../util/apiResponse';
+
 /**
  * @swagger
  *
@@ -32,9 +35,7 @@ import Tag from '~/server/models/tag';
  */
 module.exports = function(crowi, app) {
 
-  const PageTagRelation = crowi.model('PageTagRelation');
   const activityEvent = crowi.event('activity');
-  const ApiResponse = require('../util/apiResponse');
   const actions = {};
   const api = {};