♻️ Moving re-index commands to a class based system (#438)

This commit is contained in:
Eric Doughty-Papassideris
2024-03-26 12:06:22 +01:00
committed by ericlinagora
parent ed7c836889
commit f786d464ff
2 changed files with 107 additions and 75 deletions
@@ -14,10 +14,10 @@ import Repository, {
import { EntityTarget, SearchServiceAPI } from "../../../core/platform/services/search/api"; import { EntityTarget, SearchServiceAPI } from "../../../core/platform/services/search/api";
import CompanyUser, { TYPE as CompanyUserTYPE } from "../../../services/user/entities/company_user"; import CompanyUser, { TYPE as CompanyUserTYPE } from "../../../services/user/entities/company_user";
import runWithPlatform from "../../lib/run_with_platform"; import runWithPlatform from "../../lib/run_with_platform";
import SearchRepository from "src/core/platform/services/search/repository";
type Options = { type Options = {
spinner: ora.Ora; spinner: ora.Ora;
repositoryName?: string;
repairEntities: boolean; repairEntities: boolean;
}; };
@@ -40,90 +40,125 @@ async function iterateOverRepoPages<Entity>(
} while (page.page_token); } while (page.page_token);
} }
type RepositoryConstructor<Entity> = (database: DatabaseServiceAPI) => Promise<Repository<Entity>>; abstract class ReindexerCLICommand<Entity> {
protected readonly database: DatabaseServiceAPI;
const makeRepoConstructor = protected readonly search: SearchServiceAPI;
<Entity>(table: string, entity: EntityTarget<Entity>): RepositoryConstructor<Entity> => constructor(
database => protected readonly platform: TdrivePlatform,
database.getRepository<Entity>(table, entity); private readonly options: Options,
private readonly table: string,
/** The commandline name for the user repository. It has specific code for the repairEntities so is defined here. */ private readonly entity: EntityTarget<Entity>,
const UsersRepoName = "users"; ) {
class SearchIndexAll {
database: DatabaseServiceAPI;
search: SearchServiceAPI;
constructor(readonly platform: TdrivePlatform) {
this.database = this.platform.getProvider<DatabaseServiceAPI>("database"); this.database = this.platform.getProvider<DatabaseServiceAPI>("database");
this.search = this.platform.getProvider<SearchServiceAPI>("search"); this.search = this.platform.getProvider<SearchServiceAPI>("search");
} }
private _dbRepo: Repository<Entity>;
protected async dbRepository() {
return (this._dbRepo =
this._dbRepo || (await this.database.getRepository<Entity>(this.table, this.entity)));
}
private _searchRepo: SearchRepository<Entity>;
protected async searchRepository() {
return (this._searchRepo =
this._searchRepo || this.search.getRepository<Entity>(this.table, this.entity));
}
private static readonly supportedRepos = new Map<string, RepositoryConstructor<any>>([ protected statusStart(info: string) {
[UsersRepoName, makeRepoConstructor(UserTYPE, User)], this.options.spinner.start(`${this.table} > ${info}`);
["documents", makeRepoConstructor(DriveFileTYPE, DriveFile)], }
]); protected statusSucceed(info: string) {
public static isRepoSupported = (repositoryName: string) => this.options.spinner.succeed(`${this.table} > ${info}`);
this.supportedRepos.has(repositoryName); }
public static getSupportedRepoNames = () => [...this.supportedRepos.keys()]; protected statusWarn(info: string) {
private getRepository = (repositoryName: string) => this.options.spinner.warn(`${this.table} > ${info}`);
SearchIndexAll.supportedRepos.get(repositoryName)(this.database); }
protected statusFail(info: string) {
this.options.spinner.fail(`${this.table} > ${info}`);
}
protected statusInfo(info: string) {
this.options.spinner.info(`${this.table} > ${info}`);
}
private async repairEntitiesInUsers( protected async repairEntities(): Promise<void> {
options: Options, this.statusWarn(`repairEntities > No repair action for ${this.table}`);
repository: Repository<User>, }
): Promise<void> {
// Complete user with companies in cache protected async mapEntitiesToReIndexFromDBToSearch(entities: Entity[]): Promise<Entity[]> {
options.spinner.start("Adding companies to cache of user"); return entities;
}
protected async reindexFromDBToSearch(): Promise<void> {
const repository = await this.dbRepository();
this.statusStart("Start indexing...");
let count = 0;
await iterateOverRepoPages(repository, async entities => {
entities = await this.mapEntitiesToReIndexFromDBToSearch(entities);
await this.search.upsert(entities);
count += entities.length;
this.statusStart(`Indexed ${count} ${this.table}...`);
});
if (count === 0) this.statusWarn(`Index ${this.table} finished; but 0 items included`);
else this.statusSucceed(`${count} ${this.table} indexed`);
const giveFlushAChanceDurationMS = 10000;
this.statusStart(`Emptying flush (${giveFlushAChanceDurationMS / 1000}s)...`);
await waitTimeoutMS(giveFlushAChanceDurationMS);
this.statusSucceed("Done!");
}
public async run(): Promise<void> {
if (this.options.repairEntities) await this.repairEntities();
await this.reindexFromDBToSearch();
}
}
/** Serves as an index of classes for the repos to reindex; to specialise bits of behaviour and what not */
const RepositoryNameToCTOR = new Map<
string,
(platform: TdrivePlatform, options: Options) => ReindexerCLICommand<any>
>();
class UserReindexerCLICommand extends ReindexerCLICommand<User> {
constructor(platform: TdrivePlatform, options: Options) {
super(platform, options, UserTYPE, User);
}
protected override async repairEntities(): Promise<void> {
this.statusStart("repairEntities > Adding companies to cache of user");
const companiesUsersRepository = await this.database.getRepository( const companiesUsersRepository = await this.database.getRepository(
CompanyUserTYPE, CompanyUserTYPE,
CompanyUser, CompanyUser,
); );
let count = 0; let count = 0;
const repository = await this.dbRepository();
await iterateOverRepoPages(repository, async entities => { await iterateOverRepoPages(repository, async entities => {
for (const user of entities) { for (const user of entities) {
const companies = await companiesUsersRepository.find({ user_id: user.id }, {}, undefined); const companies = await companiesUsersRepository.find({ user_id: user.id }, {}, undefined);
const prevCache = JSON.stringify(user.cache);
user.cache ||= { companies: [] }; user.cache ||= { companies: [] };
user.cache.companies = companies.getEntities().map(company => company.group_id); user.cache.companies = companies.getEntities().map(company => company.group_id);
await repository.save(user, undefined); const newCache = JSON.stringify(user.cache);
if (prevCache != newCache) await repository.save(user, undefined);
} }
count += entities.length; count += entities.length;
options.spinner.start(`Adding companies to cache of ${count} users...`); this.statusStart(`repairEntities > Adding companies to cache of ${count} users...`);
}); });
options.spinner.succeed(`Added companies to cache of ${count} users`); this.statusSucceed(`repairEntities > Added companies to cache of ${count} users`);
}
private repairEntities(options: Options, repository: Repository<any>): Promise<void> {
switch (options.repositoryName) {
case UsersRepoName:
return this.repairEntitiesInUsers(options, repository);
default:
options.spinner.warn(`No repair action for repository ${options.repositoryName}`);
break;
}
}
public async run(options: Options): Promise<void> {
const repository = await this.getRepository(options.repositoryName);
if (options.repairEntities) await this.repairEntities(options, repository);
options.spinner.start(`Start indexing ${options.repositoryName}...`);
let count = 0;
await iterateOverRepoPages(repository, async entities => {
await this.search.upsert(entities);
count += entities.length;
options.spinner.start(`Indexed ${count} ${options.repositoryName}...`);
});
if (count === 0)
options.spinner.warn(`Index ${options.repositoryName} finished; but 0 items included`);
else options.spinner.succeed(`${count} ${options.repositoryName} indexed`);
const giveFlushAChanceDurationMS = 10000;
options.spinner.start(`Emptying flush (${giveFlushAChanceDurationMS / 1000}s)...`);
await waitTimeoutMS(giveFlushAChanceDurationMS);
options.spinner.succeed("Done!");
} }
} }
RepositoryNameToCTOR.set(
"users",
(platform, options) => new UserReindexerCLICommand(platform, options),
);
class DocumentsReindexerCLICommand extends ReindexerCLICommand<DriveFile> {
constructor(platform: TdrivePlatform, options: Options) {
super(platform, options, DriveFileTYPE, DriveFile);
}
}
RepositoryNameToCTOR.set(
"documents",
(platform, options) => new DocumentsReindexerCLICommand(platform, options),
);
const reindexingArgumentGroupTitle = "Re-indexing options"; const reindexingArgumentGroupTitle = "Re-indexing options";
const repositoryArgumentName = "repository"; const repositoryArgumentName = "repository";
const command: yargs.CommandModule<unknown, unknown> = { const command: yargs.CommandModule<unknown, unknown> = {
@@ -133,7 +168,7 @@ const command: yargs.CommandModule<unknown, unknown> = {
[repositoryArgumentName]: { [repositoryArgumentName]: {
type: "string", type: "string",
description: "Repository to re-index.", description: "Repository to re-index.",
choices: SearchIndexAll.getSupportedRepoNames(), choices: [...RepositoryNameToCTOR.keys()],
demandOption: true, demandOption: true,
group: reindexingArgumentGroupTitle, group: reindexingArgumentGroupTitle,
}, },
@@ -158,18 +193,15 @@ const command: yargs.CommandModule<unknown, unknown> = {
} }
} }
runWithPlatform("Re-index " + argv.repository, async ({ spinner, platform }) => { runWithPlatform("Re-index", async ({ spinner, platform }) => {
try { try {
const migrator = new SearchIndexAll(platform); for (const repositoryName of eachOnlyOnce(repositories))
for (const repositoryName of eachOnlyOnce(repositories)) { await RepositoryNameToCTOR.get(repositoryName)(platform, {
await migrator.run({
repositoryName,
spinner, spinner,
repairEntities: !!argv.repairEntities, repairEntities: !!argv.repairEntities,
}); }).run();
}
} catch (err) { } catch (err) {
spinner.fail(`Error indexing: ${err.stack}`); spinner.fail(err.stack || err);
return 1; return 1;
} }
}); });
@@ -22,7 +22,7 @@ export default async function runWithPlatform(
platform: TdrivePlatform; platform: TdrivePlatform;
}) => Promise<number | undefined> | Promise<void>, }) => Promise<number | undefined> | Promise<void>,
) { ) {
const spinner = ora({ prefixText: prefix + " -" }); const spinner = ora({ prefixText: prefix + " >" });
spinner.start("Platform: starting..."); spinner.start("Platform: starting...");
const platform = await tdrive.run(config.get("services")); const platform = await tdrive.run(config.get("services"));
await gr.doInit(platform); await gr.doInit(platform);