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

+ 0 - 2
apps/app/src/interfaces/page.ts

@@ -40,9 +40,7 @@ export type IOptionsForUpdate = {
 };
 
 export type IOptionsForCreate = {
-  // format?: string,
   grant?: PageGrant,
   grantUserGroupIds?: IGrantedGroup[],
   overwriteScopesOfDescendants?: boolean,
-  isSynchronously?: boolean,
 };

+ 5 - 11
apps/app/src/server/models/page.ts

@@ -5,10 +5,8 @@ import nodePath from 'path';
 
 import {
   type IPage,
-  type IGrantedGroup,
   GroupType, type HasObjectId,
 } from '@growi/core';
-import type { ITag } from '@growi/core/dist/interfaces';
 import { isPopulated } from '@growi/core/dist/interfaces';
 import { isTopPage, hasSlash, collectAncestorPaths } from '@growi/core/dist/utils/page-path-utils';
 import { addTrailingSlash, normalizePath } from '@growi/core/dist/utils/path-utils';
@@ -21,6 +19,7 @@ import mongoosePaginate from 'mongoose-paginate-v2';
 import uniqueValidator from 'mongoose-unique-validator';
 
 import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
+import type { IOptionsForCreate } from '~/interfaces/page';
 import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
 
 import loggerFactory from '../../utils/logger';
@@ -61,7 +60,8 @@ type PaginatedPages = {
   offset: number
 }
 
-export type CreateMethod = (path: string, body: string, user, options: PageCreateOptions) => Promise<PageDocument & { _id: any }>
+export type CreateMethod = (path: string, body: string, user, options: IOptionsForCreate) => Promise<PageDocument & { _id: any }>
+
 export interface PageModel extends Model<PageDocument> {
   [x: string]: any; // for obsolete static methods
   findByIdsAndViewer(pageIds: ObjectIdLike[], user, userGroups?, includeEmpty?: boolean, includeAnyoneWithTheLink?: boolean): Promise<PageDocument[]>
@@ -74,6 +74,8 @@ export interface PageModel extends Model<PageDocument> {
   generateGrantCondition(
     user, userGroups, includeAnyoneWithTheLink?: boolean, showPagesRestrictedByOwner?: boolean, showPagesRestrictedByGroup?: boolean,
   ): { $or: any[] }
+  findNonEmptyClosestAncestor(path: string): Promise<PageDocument | undefined>
+  findNotEmptyParentByPathRecursively(path: string): Promise<PageDocument | undefined>
   removeLeafEmptyPagesRecursively(pageId: ObjectIdLike): Promise<void>
   findTemplate(path: string): Promise<{
     templateBody?: string,
@@ -92,7 +94,6 @@ export interface PageModel extends Model<PageDocument> {
   STATUS_DELETED
 }
 
-type IObjectId = mongoose.Types.ObjectId;
 const ObjectId = mongoose.Schema.Types.ObjectId;
 
 const schema = new Schema<PageDocument, PageModel>({
@@ -1050,13 +1051,6 @@ schema.methods.calculateAndUpdateLatestRevisionBodyLength = async function(this:
   await this.save();
 };
 
-export type PageCreateOptions = {
-  format?: string
-  grantUserGroupIds?: IGrantedGroup[],
-  grant?: number
-  overwriteScopesOfDescendants?: boolean
-}
-
 /*
  * Merge obsolete page model methods and define new methods which depend on crowi instance
  */

+ 40 - 48
apps/app/src/server/service/installer.ts

@@ -1,7 +1,8 @@
 import path from 'path';
 
-import { Lang } from '@growi/core';
-import type { IPage, IUser } from '@growi/core';
+import type {
+  Lang, IPage, IUser,
+} from '@growi/core';
 import { addSeconds } from 'date-fns';
 import ExtensibleCustomError from 'extensible-custom-error';
 import fs from 'graceful-fs';
@@ -9,10 +10,10 @@ import mongoose from 'mongoose';
 
 import loggerFactory from '~/utils/logger';
 
+import type Crowi from '../crowi';
 import { generateConfigsForInstalling } from '../models/config';
 
-import type { ConfigManager } from './config-manager';
-import SearchService from './search';
+import { configManager } from './config-manager';
 
 const logger = loggerFactory('growi:service:installer');
 
@@ -26,17 +27,16 @@ export type AutoInstallOptions = {
 
 export class InstallerService {
 
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  crowi: any;
+  crowi: Crowi;
 
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
-  constructor(crowi: any) {
+  constructor(crowi: Crowi) {
     this.crowi = crowi;
   }
 
   private async initSearchIndex() {
-    const searchService: SearchService = this.crowi.searchService;
-    if (!searchService.isReachable) {
+    const { searchService } = this.crowi;
+
+    if (searchService == null || !searchService.isReachable) {
       return;
     }
 
@@ -48,18 +48,19 @@ export class InstallerService {
     }
   }
 
-  private async createPage(filePath, pagePath, owner): Promise<IPage|undefined> {
+  private async createPage(filePath, pagePath): Promise<IPage|undefined> {
+    const { pageService } = this.crowi;
+
     try {
       const markdown = fs.readFileSync(filePath);
-      return this.crowi.pageService.create(pagePath, markdown, owner, { isSynchronously: true }) as IPage;
+      return pageService.forceCreateBySystem(pagePath, markdown.toString(), {});
     }
     catch (err) {
       logger.error(`Failed to create ${pagePath}`, err);
     }
   }
 
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  private async createInitialPages(owner, lang: Lang, initialPagesCreatedAt?: Date): Promise<any> {
+  private async createInitialPages(lang: Lang, initialPagesCreatedAt?: Date): Promise<any> {
     const { localeDir } = this.crowi;
     // create /Sandbox/*
     /*
@@ -67,10 +68,10 @@ export class InstallerService {
      *   1. avoid creating the same pages
      *   2. avoid difference for order in VRT
      */
-    await this.createPage(path.join(localeDir, lang, 'sandbox.md'), '/Sandbox', owner);
-    await this.createPage(path.join(localeDir, lang, 'sandbox-bootstrap4.md'), '/Sandbox/Bootstrap4', owner);
-    await this.createPage(path.join(localeDir, lang, 'sandbox-diagrams.md'), '/Sandbox/Diagrams', owner);
-    await this.createPage(path.join(localeDir, lang, 'sandbox-math.md'), '/Sandbox/Math', owner);
+    await this.createPage(path.join(localeDir, lang, 'sandbox.md'), '/Sandbox');
+    await this.createPage(path.join(localeDir, lang, 'sandbox-bootstrap4.md'), '/Sandbox/Bootstrap4');
+    await this.createPage(path.join(localeDir, lang, 'sandbox-diagrams.md'), '/Sandbox/Diagrams');
+    await this.createPage(path.join(localeDir, lang, 'sandbox-math.md'), '/Sandbox/Math');
 
     // update createdAt and updatedAt fields of all pages
     if (initialPagesCreatedAt != null) {
@@ -110,8 +111,6 @@ export class InstallerService {
    * Execute only once for installing application
    */
   private async initDB(globalLang: Lang, options?: AutoInstallOptions): Promise<void> {
-    const configManager: ConfigManager = this.crowi.configManager;
-
     const initialConfig = generateConfigsForInstalling();
     initialConfig['app:globalLang'] = globalLang;
 
@@ -125,45 +124,38 @@ export class InstallerService {
   async install(firstAdminUserToSave: Pick<IUser, 'name' | 'username' | 'email' | 'password'>, globalLang: Lang, options?: AutoInstallOptions): Promise<IUser> {
     await this.initDB(globalLang, options);
 
-    // TODO typescriptize models/user.js and remove eslint-disable-next-line
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    const User = mongoose.model('User') as any;
-    const Page = mongoose.model('Page') as any;
+    const User = mongoose.model<IUser, { createUser }>('User');
 
     // create portal page for '/' before creating admin user
-    await this.createPage(
-      path.join(this.crowi.localeDir, globalLang, 'welcome.md'),
-      '/',
-      { _id: '000000000000000000000000' }, // use 0 as a mock user id
-    );
-
-    // create first admin user
-    // TODO: with transaction
-    let adminUser;
     try {
+      await this.createPage(
+        path.join(this.crowi.localeDir, globalLang, 'welcome.md'),
+        '/',
+      );
+    }
+    catch (err) {
+      logger.error(err);
+      throw err;
+    }
+
+    try {
+      // create first admin user
       const {
         name, username, email, password,
       } = firstAdminUserToSave;
-      adminUser = await User.createUser(name, username, email, password, globalLang);
-      await adminUser.asyncGrantAdmin();
+      const adminUser = await User.createUser(name, username, email, password, globalLang);
+      await (adminUser as any).asyncGrantAdmin();
+
+      // create initial pages
+      await this.createInitialPages(globalLang, options?.serverDate);
+
+      return adminUser;
     }
     catch (err) {
+      logger.error(err);
       throw new FailedToCreateAdminUserError(err);
     }
 
-    // add owner after creating admin user
-    const Revision = this.crowi.model('Revision');
-    const rootPage = await Page.findOne({ path: '/' });
-    const rootRevision = await Revision.findOne({ path: '/' });
-    rootPage.creator = adminUser._id;
-    rootPage.lastUpdateUser = adminUser._id;
-    rootRevision.author = adminUser._id;
-    await Promise.all([rootPage.save(), rootRevision.save()]);
-
-    // create initial pages
-    await this.createInitialPages(adminUser, globalLang, options?.serverDate);
-
-    return adminUser;
   }
 
 }

+ 23 - 37
apps/app/src/server/service/page/index.ts

@@ -31,8 +31,9 @@ import {
   type IPageOperationProcessInfo, type IPageOperationProcessData, PageActionStage, PageActionType,
 } from '~/interfaces/page-operation';
 import { SocketEventName, type PageMigrationErrorData, type UpdateDescCountRawData } from '~/interfaces/websocket';
+import type { CreateMethod } from '~/server/models/page';
 import {
-  type CreateMethod, type PageCreateOptions, type PageModel, type PageDocument, pushRevision, PageQueryBuilder,
+  type PageModel, type PageDocument, pushRevision, PageQueryBuilder,
 } from '~/server/models/page';
 import type { PageTagRelationDocument } from '~/server/models/page-tag-relation';
 import PageTagRelation from '~/server/models/page-tag-relation';
@@ -420,14 +421,6 @@ class PageService implements IPageService {
 
     const subscription = await Subscription.findByUserIdAndTargetId(user._id, pageId);
 
-    let creatorId = page.creator;
-    if (page.isEmpty) {
-      // Need non-empty ancestor page to get its creatorId because empty page does NOT have it.
-      // Use creatorId of ancestor page to determine whether the empty page is deletable
-      const notEmptyClosestAncestor = await Page.findNonEmptyClosestAncestor(page.path);
-      creatorId = notEmptyClosestAncestor.creator;
-    }
-
     const userRelatedGroups = await this.pageGrantService.getUserRelatedGroups(user);
 
     const isDeletable = this.canDelete(page, user, false);
@@ -1124,7 +1117,7 @@ class PageService implements IPageService {
     const copyPage = { ...page };
 
     // 3. Duplicate target
-    const options: PageCreateOptions = {
+    const options: IOptionsForCreate = {
       grant,
       grantUserGroupIds: grantedGroupIds,
     };
@@ -2626,7 +2619,7 @@ class PageService implements IPageService {
   }
 
   async normalizeParentByPath(path: string, user): Promise<void> {
-    const Page = mongoose.model('Page') as unknown as PageModel;
+    const Page = mongoose.model<PageDocument, PageModel>('Page');
     const { PageQueryBuilder } = Page;
 
     // This validation is not 100% correct since it ignores user to count
@@ -2664,16 +2657,14 @@ class PageService implements IPageService {
     if (shouldCreateNewPage) {
       const notEmptyParent = await Page.findNotEmptyParentByPathRecursively(path);
 
-      const options: PageCreateOptions & { grantedUsers?: ObjectIdLike[] | undefined } = {
-        grant: notEmptyParent.grant,
-        grantUserGroupIds: notEmptyParent.grantedGroups,
-        grantedUsers: notEmptyParent.grantedUsers,
-      };
-
       systematicallyCreatedPage = await this.forceCreateBySystem(
         path,
         '',
-        options,
+        {
+          grant: notEmptyParent?.grant,
+          grantUserIds: notEmptyParent?.grantedUsers.map(u => getIdForRef(u)),
+          grantUserGroupIds: notEmptyParent?.grantedGroups,
+        },
       );
       page = systematicallyCreatedPage;
     }
@@ -3680,12 +3671,12 @@ class PageService implements IPageService {
       path: string,
       grantData: {
         grant?: PageGrant,
-        grantedUserIds?: ObjectIdLike[],
+        grantUserIds?: ObjectIdLike[],
         grantUserGroupIds?: IGrantedGroup[],
       },
       shouldValidateGrant: boolean,
       user?,
-      options?: Partial<PageCreateOptions>,
+      options?: IOptionsForCreate,
   ): Promise<boolean> {
     const Page = mongoose.model('Page') as unknown as PageModel;
 
@@ -3704,7 +3695,7 @@ class PageService implements IPageService {
     }
 
     // UserGroup & Owner validation
-    const { grant, grantedUserIds, grantUserGroupIds } = grantData;
+    const { grant, grantUserIds, grantUserGroupIds } = grantData;
     if (shouldValidateGrant) {
       if (user == null) {
         throw Error('user is required to validate grant');
@@ -3716,7 +3707,7 @@ class PageService implements IPageService {
         const isEmptyPageAlreadyExist = await Page.count({ path, isEmpty: true }) > 0;
         const shouldCheckDescendants = isEmptyPageAlreadyExist && !options?.overwriteScopesOfDescendants;
 
-        isGrantNormalized = await this.pageGrantService.isGrantNormalized(user, path, grant, grantedUserIds, grantUserGroupIds, shouldCheckDescendants);
+        isGrantNormalized = await this.pageGrantService.isGrantNormalized(user, path, grant, grantUserIds, grantUserGroupIds, shouldCheckDescendants);
       }
       catch (err) {
         logger.error(`Failed to validate grant of page at "${path}" of grant ${grant}:`, err);
@@ -3743,7 +3734,7 @@ class PageService implements IPageService {
    * Create a page
    * Set options.isSynchronously to true to await all process when you want to run this method multiple times at short intervals.
    */
-  async create(_path: string, body: string, user, options: IOptionsForCreate = {}): Promise<PageDocument> {
+  async create(_path: string, body: string, user: HasObjectId, options: IOptionsForCreate = {}): Promise<PageDocument> {
     // Switch method
     const isV5Compatible = this.crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
     if (!isV5Compatible) {
@@ -3828,12 +3819,7 @@ class PageService implements IPageService {
       throw err;
     }
 
-    if (options.isSynchronously) {
-      await this.createSubOperation(savedPage, user, options, pageOp._id);
-    }
-    else {
-      this.createSubOperation(savedPage, user, options, pageOp._id);
-    }
+    this.createSubOperation(savedPage, user, options, pageOp._id);
 
     return savedPage;
   }
@@ -3919,7 +3905,7 @@ class PageService implements IPageService {
       path: string,
       grantData: {
         grant: PageGrant,
-        grantedUserIds?: ObjectIdLike[],
+        grantUserIds?: ObjectIdLike[],
         grantUserGroupId?: ObjectIdLike,
       },
   ): Promise<boolean> {
@@ -3928,13 +3914,13 @@ class PageService implements IPageService {
 
   /**
    * @private
-   * This method receives the same arguments as the PageService.create method does except for the added type '{ grantedUsers?: ObjectIdLike[] }'.
+   * This method receives the same arguments as the PageService.create method does except for the added type '{ grantUserIds?: ObjectIdLike[] }'.
    * This additional value is used to determine the grantedUser of the page to be created by system.
    * This method must not run isGrantNormalized method to validate grant. **If necessary, run it before use this method.**
    * -- Reason 1: This is because it is not expected to use this method when the grant validation is required.
    * -- Reason 2: This is because it is not expected to use this method when the program cannot determine the operator.
    */
-  private async forceCreateBySystem(path: string, body: string, options: PageCreateOptions & { grantedUsers?: ObjectIdLike[] }): Promise<PageDocument> {
+  async forceCreateBySystem(path: string, body: string, options: IOptionsForCreate & { grantUserIds?: ObjectIdLike[] }): Promise<PageDocument> {
     const Page = mongoose.model('Page') as unknown as PageModel;
 
     const isV5Compatible = this.crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
@@ -3947,7 +3933,7 @@ class PageService implements IPageService {
     path = this.crowi.xss.process(path); // sanitize path
 
     const {
-      format = 'markdown', grantUserGroupIds, grantedUsers,
+      grantUserGroupIds, grantUserIds,
     } = options;
     const grant = isTopPage(path) ? Page.GRANT_PUBLIC : options.grant;
 
@@ -3956,12 +3942,12 @@ class PageService implements IPageService {
 
     const grantData = {
       grant,
-      grantedUserIds: isGrantOwner ? grantedUsers : undefined,
+      grantUserIds: isGrantOwner ? grantUserIds : undefined,
       grantUserGroupIds,
     };
 
     // Validate
-    if (isGrantOwner && grantedUsers?.length !== 1) {
+    if (isGrantOwner && grantUserIds?.length !== 1) {
       throw Error('grantedUser must exist when grant is GRANT_OWNER');
     }
     const canProcessForceCreateBySystem = await this.canProcessForceCreateBySystem(path, grantData);
@@ -3977,7 +3963,7 @@ class PageService implements IPageService {
     this.setFieldExceptForGrantRevisionParent(page, path);
 
     // Apply scope
-    page.applyScope({ _id: grantedUsers?.[0] }, grant, grantUserGroupIds);
+    page.applyScope({ _id: grantUserIds?.[0] }, grant, grantUserGroupIds);
 
     // Set parent
     if (isTopPage(path) || isGrantRestricted) { // set parent to null when GRANT_RESTRICTED
@@ -3994,7 +3980,7 @@ class PageService implements IPageService {
     // Create revision
     const Revision = mongoose.model('Revision') as any; // TODO: Typescriptize model
     const dummyUser = { _id: new mongoose.Types.ObjectId() };
-    const newRevision = Revision.prepareRevision(savedPage, body, null, dummyUser, { format });
+    const newRevision = Revision.prepareRevision(savedPage, body, null, dummyUser);
     savedPage = await pushRevision(savedPage, newRevision, dummyUser);
 
     // Update descendantCount

+ 3 - 1
apps/app/src/server/service/page/page-service.ts

@@ -1,6 +1,7 @@
 import type EventEmitter from 'events';
 
 import type {
+  HasObjectId,
   IPageInfo, IPageInfoForEntity, IUser,
 } from '@growi/core';
 import type { ObjectId } from 'mongoose';
@@ -11,7 +12,8 @@ import type { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
 import type { PageDocument } from '~/server/models/page';
 
 export interface IPageService {
-  create(path: string, body: string, user: IUser, options: IOptionsForCreate): Promise<PageDocument>,
+  create(path: string, body: string, user: HasObjectId, options: IOptionsForCreate): Promise<PageDocument>,
+  forceCreateBySystem(path: string, body: string, options: IOptionsForCreate): Promise<PageDocument>,
   updatePage(pageData: PageDocument, body: string | null, previousBody: string | null, user: IUser, options: IOptionsForUpdate,): Promise<PageDocument>,
   updateDescendantCountOfAncestors: (pageId: ObjectIdLike, inc: number, shouldIncludeTarget: boolean) => Promise<void>,
   deleteCompletelyOperation: (pageIds: string[], pagePaths: string[]) => Promise<void>,