feat: init
This commit is contained in:
@@ -0,0 +1,14 @@
|
||||
import { CommandModule } from "yargs";
|
||||
|
||||
const command: CommandModule = {
|
||||
describe: "Manage Twake Applications",
|
||||
command: "applications <command>",
|
||||
builder: yargs =>
|
||||
yargs.commandDir("applications_cmds", {
|
||||
visit: commandModule => commandModule.default,
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
handler: () => {},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,54 @@
|
||||
import yargs from "yargs";
|
||||
import ora from "ora";
|
||||
import twake from "../../../twake";
|
||||
import Table from "cli-table";
|
||||
import * as process from "process";
|
||||
import gr from "../../../services/global-resolver";
|
||||
|
||||
/**
|
||||
* Merge command parameters. Check the builder definition below for more details.
|
||||
*/
|
||||
type CLIArgs = Record<string, unknown>;
|
||||
|
||||
const services = [
|
||||
"storage",
|
||||
"counter",
|
||||
"message-queue",
|
||||
"platform-services",
|
||||
"applications",
|
||||
"auth",
|
||||
"realtime",
|
||||
"websocket",
|
||||
// "user",
|
||||
"search",
|
||||
"database",
|
||||
"webserver",
|
||||
// "statistics",
|
||||
];
|
||||
|
||||
const command: yargs.CommandModule<unknown, CLIArgs> = {
|
||||
command: "ls",
|
||||
describe: "command that allow you to list applications (unpublished only)",
|
||||
|
||||
handler: async _argv => {
|
||||
const spinner = ora({ text: "Retrieving applications" }).start();
|
||||
const platform = await twake.run(services);
|
||||
await gr.doInit(platform);
|
||||
//
|
||||
const unpublished = await gr.services.applications.marketplaceApps.listUnpublished(undefined);
|
||||
//
|
||||
const table = new Table({
|
||||
head: ["ID", "Name", "Description"],
|
||||
colWidths: [40, 20, 40],
|
||||
});
|
||||
unpublished.forEach((app: any) => {
|
||||
table.push([app.id, app.identity.name, app.identity.description]);
|
||||
});
|
||||
spinner.stop();
|
||||
console.log(table.toString());
|
||||
await platform.stop();
|
||||
process.exit(0);
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,89 @@
|
||||
import yargs from "yargs";
|
||||
import ora from "ora";
|
||||
import twake from "../../../twake";
|
||||
import Table from "cli-table";
|
||||
import * as process from "process";
|
||||
import gr from "../../../services/global-resolver";
|
||||
/**
|
||||
* Merge command parameters. Check the builder definition below for more details.
|
||||
*/
|
||||
type CLIArgs = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const services = [
|
||||
"storage",
|
||||
"counter",
|
||||
"message-queue",
|
||||
"platform-services",
|
||||
"applications",
|
||||
"auth",
|
||||
"realtime",
|
||||
"websocket",
|
||||
"user",
|
||||
"search",
|
||||
"database",
|
||||
"webserver",
|
||||
"statistics",
|
||||
];
|
||||
|
||||
const command: yargs.CommandModule<unknown, CLIArgs> = {
|
||||
command: "publish <id>",
|
||||
describe:
|
||||
"command that allow you to validate an application and make it available on the marketplace",
|
||||
builder: {
|
||||
force: {
|
||||
default: false,
|
||||
type: "boolean",
|
||||
description: "Force update unrequested application",
|
||||
},
|
||||
},
|
||||
handler: async argv => {
|
||||
if (argv.id) {
|
||||
let spinner = ora({ text: "Retrieving application" }).start();
|
||||
const platform = await twake.run(services);
|
||||
await gr.doInit(platform);
|
||||
let app = await gr.services.applications.marketplaceApps.get({ id: argv.id }, undefined);
|
||||
spinner.stop();
|
||||
if (!app) {
|
||||
console.error(`Application ${argv.id} not found`);
|
||||
process.exit(1);
|
||||
}
|
||||
let table = new Table();
|
||||
table.push(app);
|
||||
table.push({ name: app.identity.name });
|
||||
table.push({ description: app.identity.description });
|
||||
table.push({ website: app.identity.website });
|
||||
table.push({ requested: app.publication.requested });
|
||||
table.push({ published: app.publication.published });
|
||||
console.log(table.toString());
|
||||
|
||||
if (app.publication.published) {
|
||||
console.error("Application already published");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!app.publication.requested && !argv.force) {
|
||||
console.error(
|
||||
"Application is not requested to be published. Use --force if you want to publish it anyway",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
spinner = ora({ text: "Publishing application" }).start();
|
||||
await gr.services.applications.marketplaceApps.publish({ id: argv.id }, undefined);
|
||||
app = await gr.services.applications.marketplaceApps.get({ id: argv.id }, undefined);
|
||||
|
||||
spinner.stop();
|
||||
console.log("Application published");
|
||||
|
||||
table = new Table();
|
||||
table.push(app);
|
||||
table.push({ published: app.publication.published });
|
||||
console.log(table.toString());
|
||||
}
|
||||
process.exit(0);
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,73 @@
|
||||
import yargs from "yargs";
|
||||
import ora from "ora";
|
||||
import twake from "../../../twake";
|
||||
import Table from "cli-table";
|
||||
import * as process from "process";
|
||||
import gr from "../../../services/global-resolver";
|
||||
/**
|
||||
* Merge command parameters. Check the builder definition below for more details.
|
||||
*/
|
||||
type CLIArgs = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const services = [
|
||||
"storage",
|
||||
"counter",
|
||||
"message-queue",
|
||||
"platform-services",
|
||||
"applications",
|
||||
"auth",
|
||||
"realtime",
|
||||
"websocket",
|
||||
"user",
|
||||
"search",
|
||||
"database",
|
||||
"webserver",
|
||||
"statistics",
|
||||
];
|
||||
|
||||
const command: yargs.CommandModule<unknown, CLIArgs> = {
|
||||
command: "unpublish <id>",
|
||||
describe: "command that allow you to make application unavailable on the marketplace",
|
||||
handler: async argv => {
|
||||
if (argv.id) {
|
||||
let spinner = ora({ text: "Retrieving application" }).start();
|
||||
const platform = await twake.run(services);
|
||||
await gr.doInit(platform);
|
||||
let app = await gr.services.applications.marketplaceApps.get({ id: argv.id }, undefined);
|
||||
spinner.stop();
|
||||
if (!app) {
|
||||
console.error(`Application ${argv.id} not found`);
|
||||
process.exit(1);
|
||||
}
|
||||
let table = new Table();
|
||||
table.push(app);
|
||||
table.push({ name: app.identity.name });
|
||||
table.push({ description: app.identity.description });
|
||||
table.push({ website: app.identity.website });
|
||||
table.push({ requested: app.publication.requested });
|
||||
table.push({ published: app.publication.published });
|
||||
console.log(table.toString());
|
||||
|
||||
if (!app.publication.published) {
|
||||
console.error("Application is not published");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
spinner = ora({ text: "Unpublishing application" }).start();
|
||||
await gr.services.applications.marketplaceApps.unpublish({ id: argv.id }, undefined);
|
||||
app = await gr.services.applications.marketplaceApps.get({ id: argv.id }, undefined);
|
||||
spinner.stop();
|
||||
console.log("Application unpublished");
|
||||
|
||||
table = new Table();
|
||||
table.push(app);
|
||||
table.push({ published: app.publication.published });
|
||||
console.log(table.toString());
|
||||
}
|
||||
process.exit(0);
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { CommandModule } from "yargs";
|
||||
|
||||
const command: CommandModule = {
|
||||
describe: "Manage Twake Console",
|
||||
command: "console <command>",
|
||||
builder: yargs =>
|
||||
yargs.commandDir("console_cmds", {
|
||||
visit: commandModule => commandModule.default,
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
handler: () => {},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,257 @@
|
||||
import yargs from "yargs";
|
||||
import ora from "ora";
|
||||
import twake from "../../../twake";
|
||||
import { CompanyReport, UserReport } from "../../../services/console/types";
|
||||
import Company from "../../../services/user/entities/company";
|
||||
import gr from "../../../services/global-resolver";
|
||||
import { ConsoleServiceImpl } from "../../../services/console/service";
|
||||
|
||||
/**
|
||||
* Merge command parameters. Check the builder definition below for more details.
|
||||
*/
|
||||
type MergeParams = {
|
||||
url: string;
|
||||
concurrent: number;
|
||||
dry: boolean;
|
||||
console: string;
|
||||
link: boolean;
|
||||
csv: boolean;
|
||||
client: string;
|
||||
secret: string;
|
||||
};
|
||||
|
||||
const services = [
|
||||
"platform-services",
|
||||
"user",
|
||||
"channels",
|
||||
"notifications",
|
||||
"database",
|
||||
"webserver",
|
||||
"message-queue",
|
||||
"console",
|
||||
];
|
||||
|
||||
const command: yargs.CommandModule<MergeParams, MergeParams> = {
|
||||
command: "merge",
|
||||
describe: "Merge Twake Chat users in the Twake Console",
|
||||
builder: {
|
||||
url: {
|
||||
default: "http://localhost:8080",
|
||||
type: "string",
|
||||
description: "URL of the Twake console",
|
||||
},
|
||||
concurrent: {
|
||||
default: 1,
|
||||
type: "number",
|
||||
description: "Number of concurrent imports",
|
||||
},
|
||||
dry: {
|
||||
default: false,
|
||||
type: "boolean",
|
||||
description: "Make a dry run without creating anything on the Twake console",
|
||||
},
|
||||
csv: {
|
||||
default: false,
|
||||
type: "boolean",
|
||||
description: "Generate result as CSV",
|
||||
},
|
||||
console: {
|
||||
default: "console",
|
||||
type: "string",
|
||||
description: "The console service identifier to user to link user and companies",
|
||||
},
|
||||
link: {
|
||||
default: true,
|
||||
type: "boolean",
|
||||
description:
|
||||
"Link the companies/users to external companies/user. Works with the --console parameter",
|
||||
implies: "console",
|
||||
},
|
||||
client: {
|
||||
default: "twake",
|
||||
type: "string",
|
||||
description: "Client identifier to be used to authenticate calls to the Console",
|
||||
},
|
||||
secret: {
|
||||
default: "secret",
|
||||
type: "string",
|
||||
description: "Client secret to be used to authenticate calls to the Console",
|
||||
},
|
||||
},
|
||||
handler: async argv => {
|
||||
const spinner = ora({ text: `Importing Twake data on ${argv.url}` }).start();
|
||||
const platform = await twake.run(services);
|
||||
await gr.doInit(platform);
|
||||
const consoleService = platform.getProvider<ConsoleServiceImpl>("console");
|
||||
const merge = consoleService.merge(
|
||||
argv.url,
|
||||
argv.concurrent,
|
||||
argv.dry,
|
||||
argv.console,
|
||||
argv.link,
|
||||
argv.client,
|
||||
argv.secret,
|
||||
);
|
||||
const start = Date.now();
|
||||
let stop: number = start;
|
||||
const userReports: UserReport[] = [];
|
||||
const companyReports: CompanyReport[] = [];
|
||||
const ownerReports: UserReport[] = [];
|
||||
let companiesWithoutAdmin: Company[] = [];
|
||||
|
||||
const process = merge.subscribe({
|
||||
next: report => {
|
||||
if (report.type === "company:created") {
|
||||
const companyReport = report.data as CompanyReport;
|
||||
companyReports.push(companyReport);
|
||||
|
||||
if (companyReport.status === "success") {
|
||||
spinner.succeed(
|
||||
`Company created: ${companyReport.company.source.id} ${companyReport.company.source.displayName}`,
|
||||
);
|
||||
} else {
|
||||
spinner.fail(
|
||||
`Creation error for company ${companyReport.company.source.id} (${companyReport.company.source.displayName}): ${companyReport?.error?.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (report.type === "user:created") {
|
||||
const userReport = report.data as UserReport;
|
||||
userReports.push(userReport);
|
||||
|
||||
if (userReport.status === "success") {
|
||||
spinner.succeed(
|
||||
`Company ${userReport.user.source.company.id}: User created ${userReport.user.destination.id}`,
|
||||
);
|
||||
} else {
|
||||
spinner.fail(
|
||||
`Company ${userReport.user.source.company.id}: User creation error ${userReport.user.destination.id}: ${userReport?.error?.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (report.type === "user:updated") {
|
||||
const userReport = report.data as UserReport;
|
||||
ownerReports.push(userReport);
|
||||
if (userReport.status === "success") {
|
||||
spinner.succeed(
|
||||
`Company ${userReport.destinationCompanyCode} owner updated to user ${userReport.destinationId}`,
|
||||
);
|
||||
} else {
|
||||
spinner.fail(
|
||||
`Company ${userReport.destinationCompanyCode}: Owner update error for user ${userReport.destinationId}: ${userReport?.error?.message}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (report.type === "processing:owner") {
|
||||
spinner.start("Computing company owners, please wait...");
|
||||
}
|
||||
|
||||
if (report.type === "user:updating") {
|
||||
//spinner.succeed(`Updating owner for company ${report?.company?.sourceId}`);
|
||||
}
|
||||
|
||||
if (report.type === "log") {
|
||||
report.message && (spinner.text = report.message);
|
||||
}
|
||||
|
||||
if (report.type === "company:withoutadmin") {
|
||||
report.message && (spinner.text = report.message);
|
||||
const companies = (report.data || []) as Company[];
|
||||
|
||||
if (companies.length) {
|
||||
companiesWithoutAdmin = companies;
|
||||
}
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
spinner.fail("Fatal error");
|
||||
},
|
||||
complete: async () => {
|
||||
stop = Date.now();
|
||||
spinner.succeed("Merge is complete");
|
||||
displayStats();
|
||||
await tearDown();
|
||||
},
|
||||
});
|
||||
|
||||
async function tearDown() {
|
||||
await platform.stop();
|
||||
process.unsubscribe();
|
||||
}
|
||||
|
||||
function reportAsCSV() {
|
||||
const users = userReports.map(
|
||||
user =>
|
||||
`${user.sourceId},${user.destinationId},${user.destinationCompanyCode},${user.status}`,
|
||||
);
|
||||
const userCSV = [...["sourceId,destinationId,destinationCompanyCode,status"], ...users].join(
|
||||
"\n",
|
||||
);
|
||||
|
||||
const companies = companyReports.map(
|
||||
company => `${company.sourceId},${company.destinationCode},${company.status}`,
|
||||
);
|
||||
const companyCSV = [...["sourceId,destinationCode,status"], ...companies].join("\n");
|
||||
|
||||
console.log(userCSV);
|
||||
console.log(companyCSV);
|
||||
}
|
||||
|
||||
function displayStats() {
|
||||
const userFailures = userReports.filter(user => user.error);
|
||||
const companyFailures = companyReports.filter(company => company.error);
|
||||
const ownerFailures = ownerReports.filter(report => report.error);
|
||||
|
||||
console.log("# Import report");
|
||||
console.log(`Data imported in ${(stop - start) / 1000} seconds`);
|
||||
console.log("## Company");
|
||||
console.log("- Companies success:", companyReports.filter(company => !company.error).length);
|
||||
console.log("- Companies failure:", companyFailures.length);
|
||||
if (companyFailures.length) {
|
||||
console.log("### Failures");
|
||||
companyFailures.forEach(failure =>
|
||||
console.log(
|
||||
`- Company ${failure.company.source.id} error: ${failure.company.error?.message}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
console.log("## User");
|
||||
console.log("- Users success:", userReports.filter(user => !user.error).length);
|
||||
console.log("- Users failure:", userFailures.length);
|
||||
if (userFailures.length) {
|
||||
console.log("### Failures");
|
||||
userFailures.forEach(failure =>
|
||||
console.log(
|
||||
`- User ${failure.user.source.user.user_id} error: ${failure.user.error?.message}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
console.log("## Owner");
|
||||
console.log("- Owners success:", ownerReports.filter(report => !report.error).length);
|
||||
console.log("- Owners failure:", ownerFailures.length);
|
||||
if (ownerFailures.length) {
|
||||
console.log("### Failures");
|
||||
ownerFailures.forEach(report =>
|
||||
console.log(`- User ${report.destinationId} error: ${report?.error?.message}`),
|
||||
);
|
||||
}
|
||||
|
||||
console.log("## Warnings");
|
||||
console.log("### Companies witout admins");
|
||||
companiesWithoutAdmin.forEach(company => {
|
||||
console.log(`- ${company.id} - ${company.displayName}`);
|
||||
});
|
||||
|
||||
if (argv.csv) {
|
||||
reportAsCSV();
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { CommandModule } from "yargs";
|
||||
|
||||
const command: CommandModule = {
|
||||
describe: "Export Twake data",
|
||||
command: "export <command>",
|
||||
builder: yargs =>
|
||||
yargs.commandDir("export_cmds", {
|
||||
visit: commandModule => commandModule.default,
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
handler: () => {},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,266 @@
|
||||
import yargs from "yargs";
|
||||
import twake from "../../../twake";
|
||||
import { mkdirSync, writeFileSync } from "fs";
|
||||
import { Pagination } from "../../../core/platform/framework/api/crud-service";
|
||||
import WorkspaceUser from "../../../services/workspaces/entities/workspace_user";
|
||||
import { ChannelVisibility } from "../../../services/channels/types";
|
||||
import { Channel, ChannelMember } from "../../../services/channels/entities";
|
||||
import { Message } from "../../../services/messages/entities/messages";
|
||||
import { MessageWithReplies } from "../../../services/messages/types";
|
||||
import { formatCompany } from "../../../services/user/utils";
|
||||
import gr from "../../../services/global-resolver";
|
||||
import { formatUser } from "../../../utils/users";
|
||||
|
||||
/**
|
||||
* Merge command parameters. Check the builder definition below for more details.
|
||||
*/
|
||||
type CLIArgs = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const services = [
|
||||
"auth",
|
||||
"storage",
|
||||
"counter",
|
||||
"message-queue",
|
||||
"user",
|
||||
"files",
|
||||
"messages",
|
||||
"workspaces",
|
||||
"platform-services",
|
||||
"console",
|
||||
"applications",
|
||||
"search",
|
||||
"database",
|
||||
"webserver",
|
||||
"channels",
|
||||
"statistics",
|
||||
];
|
||||
|
||||
const command: yargs.CommandModule<unknown, CLIArgs> = {
|
||||
command: "company",
|
||||
describe:
|
||||
"command to export everything inside a company (publicly data only available to a new member)",
|
||||
builder: {
|
||||
id: {
|
||||
default: "",
|
||||
type: "string",
|
||||
description: "Company ID",
|
||||
},
|
||||
output: {
|
||||
default: "",
|
||||
type: "string",
|
||||
description: "Folder containing the exported data",
|
||||
},
|
||||
},
|
||||
handler: async argv => {
|
||||
const platform = await twake.run(services);
|
||||
await gr.doInit(platform);
|
||||
|
||||
const company = await gr.services.companies.getCompany({ id: argv.id });
|
||||
|
||||
if (!company) {
|
||||
return "No such company";
|
||||
}
|
||||
|
||||
console.log(`Start export for company ${company.id}`);
|
||||
|
||||
const output = (argv.output as string) || `export-${company.id}`;
|
||||
mkdirSync(output, { recursive: true });
|
||||
|
||||
//Company
|
||||
console.log("- Create company json file");
|
||||
writeFileSync(`${output}/company.json`, JSON.stringify(formatCompany(company)));
|
||||
|
||||
//Workspaces
|
||||
console.log("- Create workspaces json file");
|
||||
const workspaces = await gr.services.workspaces.getAllForCompany(company.id);
|
||||
writeFileSync(`${output}/workspaces.json`, JSON.stringify(workspaces));
|
||||
|
||||
//Users
|
||||
console.log("- Create users json file");
|
||||
const users = [];
|
||||
for (const workspace of workspaces) {
|
||||
const workspace_users = [];
|
||||
let workspaceUsers: WorkspaceUser[] = [];
|
||||
let pagination = new Pagination();
|
||||
do {
|
||||
const res = await gr.services.workspaces.getUsers(
|
||||
{ workspaceId: workspace.id },
|
||||
pagination,
|
||||
);
|
||||
workspaceUsers = [...workspaceUsers, ...res.getEntities()];
|
||||
pagination = res.nextPage as Pagination;
|
||||
} while (pagination.page_token);
|
||||
for (const workspaceUser of workspaceUsers) {
|
||||
const user = await gr.services.users.get({ id: workspaceUser.userId });
|
||||
if (user) {
|
||||
users.push(await formatUser(user));
|
||||
workspace_users.push({ ...workspaceUser, user });
|
||||
}
|
||||
}
|
||||
mkdirSync(`${output}/workspaces/${workspace.id}`, { recursive: true });
|
||||
writeFileSync(
|
||||
`${output}/workspaces/${workspace.id}/users.json`,
|
||||
JSON.stringify(workspace_users),
|
||||
);
|
||||
}
|
||||
writeFileSync(`${output}/users.json`, JSON.stringify(users));
|
||||
|
||||
//Applications
|
||||
console.log("- Create applications json file");
|
||||
const applications = await gr.services.applications.companyApps.list(
|
||||
new Pagination(),
|
||||
{},
|
||||
{ company: { id: company.id }, user: { id: "", server_request: true } },
|
||||
);
|
||||
writeFileSync(`${output}/applications.json`, JSON.stringify(applications));
|
||||
|
||||
//Channels
|
||||
console.log("- Create channels json file");
|
||||
const directChannels: Channel[] = [];
|
||||
let allPublicChannels: Channel[] = [];
|
||||
|
||||
let pagination = new Pagination();
|
||||
do {
|
||||
const page = await gr.services.channels.channels.getDirectChannelsInCompany(
|
||||
pagination,
|
||||
company.id,
|
||||
undefined,
|
||||
);
|
||||
for (const channel of page.getEntities()) {
|
||||
const channelDetail = await gr.services.channels.channels.get(
|
||||
{
|
||||
company_id: channel.company_id,
|
||||
workspace_id: "direct",
|
||||
id: channel.id,
|
||||
},
|
||||
undefined,
|
||||
);
|
||||
directChannels.push(channelDetail);
|
||||
}
|
||||
pagination = page.nextPage as Pagination;
|
||||
} while (pagination.page_token);
|
||||
|
||||
for (const workspace of workspaces) {
|
||||
let pagination = new Pagination();
|
||||
|
||||
let publicChannels: Channel[] = [];
|
||||
pagination = new Pagination();
|
||||
do {
|
||||
const page = await gr.services.channels.channels.list(
|
||||
pagination,
|
||||
{},
|
||||
{
|
||||
user: { id: "", server_request: true },
|
||||
workspace: { workspace_id: workspace.id, company_id: company.id },
|
||||
},
|
||||
);
|
||||
const chans = page.getEntities().filter(c => c.visibility == ChannelVisibility.PUBLIC);
|
||||
allPublicChannels = [...allPublicChannels, ...chans];
|
||||
publicChannels = [...publicChannels, ...chans];
|
||||
pagination = page.nextPage as Pagination;
|
||||
} while (pagination.page_token);
|
||||
|
||||
mkdirSync(`${output}/workspaces/${workspace.id}`, { recursive: true });
|
||||
writeFileSync(
|
||||
`${output}/workspaces/${workspace.id}/channels.json`,
|
||||
JSON.stringify(publicChannels),
|
||||
);
|
||||
}
|
||||
writeFileSync(`${output}/direct_channels.json`, JSON.stringify(directChannels));
|
||||
|
||||
//Channels users
|
||||
console.log("- Create channels users json file");
|
||||
for (const channel of [...allPublicChannels /*, ...directChannels*/]) {
|
||||
let members: ChannelMember[] = [];
|
||||
let pagination = new Pagination();
|
||||
do {
|
||||
const page = await gr.services.channels.members.list(
|
||||
pagination,
|
||||
{},
|
||||
{
|
||||
user: { id: "", server_request: true },
|
||||
channel: {
|
||||
company_id: channel.company_id,
|
||||
workspace_id: channel.workspace_id,
|
||||
id: channel.id,
|
||||
},
|
||||
},
|
||||
);
|
||||
members = [...members, ...page.getEntities()] as ChannelMember[];
|
||||
pagination = page.nextPage as Pagination;
|
||||
} while (pagination.page_token);
|
||||
|
||||
mkdirSync(`${output}/workspaces/${channel.workspace_id}/channels/${channel.id}`, {
|
||||
recursive: true,
|
||||
});
|
||||
writeFileSync(
|
||||
`${output}/workspaces/${channel.workspace_id}/channels/${channel.id}/members.json`,
|
||||
JSON.stringify(members),
|
||||
);
|
||||
}
|
||||
|
||||
//Messages
|
||||
console.log("- Create messages json file");
|
||||
//Note: direct channels content is private and not needed for R&D
|
||||
for (const channel of [...allPublicChannels /*, ...directChannels*/]) {
|
||||
let threads: MessageWithReplies[] = [];
|
||||
let messages: Message[] = [];
|
||||
let pagination = new Pagination();
|
||||
try {
|
||||
do {
|
||||
const page = await gr.services.messages.views.listChannel(
|
||||
pagination,
|
||||
{
|
||||
include_users: false,
|
||||
replies_per_thread: 10000,
|
||||
emojis: false,
|
||||
},
|
||||
{
|
||||
user: { id: "", server_request: true },
|
||||
channel: {
|
||||
company_id: channel.company_id,
|
||||
workspace_id: channel.workspace_id,
|
||||
id: channel.id,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
for (const thread of page.getEntities()) {
|
||||
messages = [...messages, ...thread.last_replies];
|
||||
}
|
||||
|
||||
threads = [
|
||||
...threads,
|
||||
...page.getEntities().map(thread => {
|
||||
thread.last_replies = undefined;
|
||||
return thread;
|
||||
}),
|
||||
];
|
||||
|
||||
pagination = page.nextPage as Pagination;
|
||||
} while (pagination.page_token);
|
||||
} catch (err) {
|
||||
console.log(`-- Error on the channel ${channel.id}`);
|
||||
console.log(err);
|
||||
}
|
||||
|
||||
mkdirSync(`${output}/workspaces/${channel.workspace_id}/channels/${channel.id}`, {
|
||||
recursive: true,
|
||||
});
|
||||
writeFileSync(
|
||||
`${output}/workspaces/${channel.workspace_id}/channels/${channel.id}/threads.json`,
|
||||
JSON.stringify(threads),
|
||||
);
|
||||
writeFileSync(
|
||||
`${output}/workspaces/${channel.workspace_id}/channels/${channel.id}/messages.json`,
|
||||
JSON.stringify(messages),
|
||||
);
|
||||
}
|
||||
|
||||
await platform.stop();
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { CommandModule } from "yargs";
|
||||
|
||||
const command: CommandModule = {
|
||||
describe: "Generate Twake data",
|
||||
command: "generate <command>",
|
||||
builder: yargs =>
|
||||
yargs.commandDir("generate_cmds", {
|
||||
visit: commandModule => commandModule.default,
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
handler: () => {},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,147 @@
|
||||
import passwordGenerator from "generate-password";
|
||||
import { from, pipe } from "rxjs";
|
||||
import { bufferCount, first, map, mergeMap, switchMap, tap } from "rxjs/operators";
|
||||
import { v1 as uuid } from "uuid";
|
||||
import yargs from "yargs";
|
||||
|
||||
import Company, {
|
||||
getInstance as getCompanyInstance,
|
||||
} from "../../../services/user/entities/company";
|
||||
import CompanyUser from "../../../services/user/entities/company_user";
|
||||
import twake from "../../../twake";
|
||||
import User, { getInstance as getUserInstance } from "../../../services/user/entities/user";
|
||||
import gr from "../../../services/global-resolver";
|
||||
|
||||
type CLIArgs = {
|
||||
company: number;
|
||||
user: number;
|
||||
concurrent: number;
|
||||
};
|
||||
|
||||
const services = [
|
||||
"storage",
|
||||
"counter",
|
||||
"applications",
|
||||
"statistics",
|
||||
"auth",
|
||||
"realtime",
|
||||
"push",
|
||||
"platform-services",
|
||||
"user",
|
||||
"search",
|
||||
"channels",
|
||||
"notifications",
|
||||
"database",
|
||||
"webserver",
|
||||
"message-queue",
|
||||
];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
const command: yargs.CommandModule<{}, CLIArgs> = {
|
||||
command: "users",
|
||||
describe: "Generate users and companies",
|
||||
builder: {
|
||||
company: {
|
||||
alias: "c",
|
||||
default: 3,
|
||||
type: "number",
|
||||
description: "Number of companies to generate",
|
||||
},
|
||||
user: {
|
||||
alias: "u",
|
||||
default: 5,
|
||||
type: "number",
|
||||
description: "Number of users to generate in the company",
|
||||
},
|
||||
concurrent: {
|
||||
default: 1,
|
||||
type: "number",
|
||||
description: "Number of concurrent creation tasks",
|
||||
},
|
||||
},
|
||||
handler: async argv => {
|
||||
const concurrentTasks = argv.concurrent;
|
||||
const nbUsersPerCompany = argv.user;
|
||||
const nbCompanies = argv.company;
|
||||
const platform = await twake.run(services);
|
||||
await gr.doInit(platform);
|
||||
const companies = getCompanies(nbCompanies);
|
||||
const createUser = async (userInCompany: {
|
||||
user: User;
|
||||
company: Company;
|
||||
}): Promise<CompanyUser> => {
|
||||
console.log("Creating user", userInCompany);
|
||||
const created = await gr.services.users.create(getUserInstance(userInCompany.user));
|
||||
|
||||
return (await gr.services.companies.setUserRole(userInCompany.company.id, created.entity.id))
|
||||
?.entity;
|
||||
};
|
||||
const createCompany = (company: Company) => {
|
||||
console.log("Creating company", company);
|
||||
return gr.services.companies.createCompany(company);
|
||||
};
|
||||
|
||||
const obsv$ = from(companies).pipe(
|
||||
// Create companies sequentially
|
||||
mergeMap(company => createCompany(company), concurrentTasks),
|
||||
// until we create enough companies
|
||||
bufferCount(companies.length),
|
||||
tap(companies => console.log("Created companies", companies.length)),
|
||||
// for each created company
|
||||
switchMap(companies => from(companies)),
|
||||
// generate a set of user for each company
|
||||
map(company => getUsersForCompany(company, nbUsersPerCompany)),
|
||||
// Create users sequentially
|
||||
pipe(
|
||||
switchMap(userCompanies => from(userCompanies)),
|
||||
mergeMap(userCompany => createUser(userCompany), concurrentTasks),
|
||||
// until we reach the number of users in the company
|
||||
bufferCount(nbUsersPerCompany),
|
||||
),
|
||||
// until we reach the number of companies
|
||||
bufferCount(companies.length),
|
||||
first(),
|
||||
);
|
||||
|
||||
return obsv$
|
||||
.toPromise()
|
||||
.then(() => platform.stop())
|
||||
.finally(() => console.log("✅ Company users are now created"));
|
||||
},
|
||||
};
|
||||
|
||||
const getCompanies = (size: number = 10): Company[] => {
|
||||
return [...Array(size).keys()].map(i =>
|
||||
getCompanyInstance({
|
||||
id: uuid(),
|
||||
name: `twake${i}.app`,
|
||||
displayName: `My Twake Company #${i}`,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const getUsersForCompany = (company: Company, size: number = 10) => {
|
||||
return getUsers(company, size).map(user => ({
|
||||
user,
|
||||
company,
|
||||
}));
|
||||
};
|
||||
|
||||
const getUsers = (company: Company, size: number = 100): User[] => {
|
||||
return [...Array(size).keys()].map(i => getUser(company, i));
|
||||
};
|
||||
|
||||
const getUser = (company: Company, id: number): User => {
|
||||
return getUserInstance({
|
||||
id: uuid(),
|
||||
first_name: "John",
|
||||
last_name: `Doe${id}`,
|
||||
email_canonical: `user${id}@${company.name}`,
|
||||
password: passwordGenerator.generate({
|
||||
length: 10,
|
||||
numbers: true,
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { CommandModule } from "yargs";
|
||||
|
||||
const command: CommandModule = {
|
||||
describe: "Migrate your php message to node",
|
||||
command: "migration <command>",
|
||||
builder: yargs =>
|
||||
yargs.commandDir("migration_cmds", {
|
||||
visit: commandModule => commandModule.default,
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
handler: () => {},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,264 @@
|
||||
import yargs from "yargs";
|
||||
import twake from "../../../twake";
|
||||
import ora from "ora";
|
||||
import { TwakePlatform } from "../../../core/platform/platform";
|
||||
import { DatabaseServiceAPI } from "../../../core/platform/services/database/api";
|
||||
import PhpApplication, {
|
||||
DepreciatedDisplayConfiguration,
|
||||
TYPE as phpTYPE,
|
||||
} from "./php-application/php-application-entity";
|
||||
import { Pagination } from "../../../core/platform/framework/api/crud-service";
|
||||
import Application, { TYPE } from "../../../services/applications/entities/application";
|
||||
import gr from "../../../services/global-resolver";
|
||||
|
||||
type Options = {
|
||||
onlyApplication?: string;
|
||||
replaceExisting?: boolean;
|
||||
};
|
||||
|
||||
class ApplicationMigrator {
|
||||
database: DatabaseServiceAPI;
|
||||
|
||||
constructor(readonly platform: TwakePlatform) {
|
||||
this.database = this.platform.getProvider<DatabaseServiceAPI>("database");
|
||||
}
|
||||
|
||||
public async run(options: Options = {}): Promise<void> {
|
||||
const phpRepository = await this.database.getRepository(phpTYPE, PhpApplication);
|
||||
const repository = await this.database.getRepository(TYPE, Application);
|
||||
|
||||
let page: Pagination = { limitStr: "100" };
|
||||
do {
|
||||
const applicationListResult = await phpRepository.find({}, { pagination: page }, undefined);
|
||||
page = applicationListResult.nextPage as Pagination;
|
||||
|
||||
for (const application of applicationListResult.getEntities()) {
|
||||
if (
|
||||
!(await repository.findOne(
|
||||
{
|
||||
id: application.id,
|
||||
},
|
||||
{},
|
||||
undefined,
|
||||
)) ||
|
||||
options.replaceExisting
|
||||
) {
|
||||
const newApplication = importDepreciatedFields(application);
|
||||
await repository.save(newApplication, undefined);
|
||||
}
|
||||
}
|
||||
} while (page.page_token);
|
||||
}
|
||||
}
|
||||
|
||||
const services = [
|
||||
"storage",
|
||||
"counter",
|
||||
"platform-services",
|
||||
"user",
|
||||
"search",
|
||||
"channels",
|
||||
"database",
|
||||
"webserver",
|
||||
"message-queue",
|
||||
"applications",
|
||||
"console",
|
||||
"auth",
|
||||
"statistics",
|
||||
"realtime",
|
||||
];
|
||||
|
||||
const command: yargs.CommandModule<unknown, unknown> = {
|
||||
command: "application",
|
||||
describe: "command that allow you to migrate php applications to node",
|
||||
builder: {
|
||||
onlyApplication: {
|
||||
default: null,
|
||||
type: "string",
|
||||
description: "Migrate only this application ID",
|
||||
},
|
||||
replaceExisting: {
|
||||
default: false,
|
||||
type: "boolean",
|
||||
description: "Replace already migrated applications",
|
||||
},
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
handler: async argv => {
|
||||
const spinner = ora({ text: "Migrating php applications - " }).start();
|
||||
const platform = await twake.run(services);
|
||||
await gr.doInit(platform);
|
||||
const migrator = new ApplicationMigrator(platform);
|
||||
|
||||
const onlyApplication = argv.onlyApplication as string | null;
|
||||
const replaceExisting = (argv.replaceExisting || false) as boolean;
|
||||
|
||||
await migrator.run({
|
||||
onlyApplication,
|
||||
replaceExisting,
|
||||
});
|
||||
|
||||
return spinner.stop();
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
|
||||
export const importDepreciatedFields = (application: PhpApplication): Application => {
|
||||
const newApplication = new Application();
|
||||
|
||||
newApplication.id = application.id;
|
||||
newApplication.company_id = application.group_id;
|
||||
newApplication.is_default = application.is_default;
|
||||
|
||||
if (!newApplication.identity?.name) {
|
||||
newApplication.identity = {
|
||||
code:
|
||||
application.depreciated_simple_name ||
|
||||
(application.depreciated_name || "").toLocaleLowerCase(),
|
||||
name: application.depreciated_name,
|
||||
icon: application.depreciated_icon_url,
|
||||
description: application.depreciated_description,
|
||||
website: "http://twake.app/",
|
||||
categories: [],
|
||||
compatibility: ["twake"],
|
||||
};
|
||||
}
|
||||
|
||||
if (newApplication.publication?.published === undefined) {
|
||||
newApplication.publication = newApplication.publication || {
|
||||
published: false,
|
||||
requested: false,
|
||||
};
|
||||
newApplication.publication.published = application.depreciated_is_available_to_public;
|
||||
newApplication.publication.requested =
|
||||
application.depreciated_public && !application.depreciated_twake_team_validation;
|
||||
}
|
||||
|
||||
if (!newApplication.stats?.version) {
|
||||
newApplication.stats = newApplication.stats || {
|
||||
created_at: null,
|
||||
updated_at: null,
|
||||
version: null,
|
||||
};
|
||||
newApplication.stats.version = 1;
|
||||
newApplication.stats.created_at = Date.now();
|
||||
newApplication.stats.updated_at = Date.now();
|
||||
}
|
||||
|
||||
if (!newApplication.api?.private_key) {
|
||||
newApplication.api = newApplication.api || {
|
||||
hooks_url: null,
|
||||
allowed_ips: null,
|
||||
private_key: null,
|
||||
};
|
||||
newApplication.api.hooks_url = application.depreciated_api_events_url;
|
||||
newApplication.api.allowed_ips = application.depreciated_api_allowed_ip;
|
||||
newApplication.api.private_key = application.depreciated_api_private_key;
|
||||
}
|
||||
|
||||
if (newApplication.access?.write === undefined) {
|
||||
newApplication.access = newApplication.access || {
|
||||
read: null,
|
||||
write: null,
|
||||
delete: null,
|
||||
hooks: null,
|
||||
};
|
||||
try {
|
||||
newApplication.access.write = JSON.parse(application.depreciated_capabilities || "[]") || [];
|
||||
newApplication.access.delete = JSON.parse(application.depreciated_capabilities || "[]") || [];
|
||||
} catch (e) {
|
||||
newApplication.access.write = [];
|
||||
newApplication.access.delete = [];
|
||||
}
|
||||
try {
|
||||
newApplication.access.read = JSON.parse(application.depreciated_privileges || "[]") || [];
|
||||
} catch (e) {
|
||||
newApplication.access.read = [];
|
||||
}
|
||||
try {
|
||||
newApplication.access.hooks = JSON.parse(application.depreciated_hooks || "[]") || [];
|
||||
} catch (e) {
|
||||
newApplication.access.hooks = [];
|
||||
}
|
||||
}
|
||||
|
||||
newApplication.display = importDepreciatedDisplayFields(
|
||||
newApplication,
|
||||
JSON.parse(application.depreciated_display_configuration),
|
||||
);
|
||||
|
||||
return newApplication;
|
||||
};
|
||||
|
||||
export const importDepreciatedDisplayFields = (
|
||||
application: Application,
|
||||
depreciatedDisplay: DepreciatedDisplayConfiguration,
|
||||
): Application["display"] => {
|
||||
let display = application.display;
|
||||
|
||||
if (!display?.twake) {
|
||||
display = display || { twake: {} };
|
||||
display.twake = display.twake || {};
|
||||
}
|
||||
|
||||
display.twake.tab = depreciatedDisplay?.channel_tab
|
||||
? { url: depreciatedDisplay?.channel_tab?.iframe } || true
|
||||
: undefined;
|
||||
|
||||
display.twake.standalone = depreciatedDisplay?.app
|
||||
? { url: depreciatedDisplay?.app?.iframe } || true
|
||||
: undefined;
|
||||
|
||||
display.twake.configuration = [];
|
||||
if (depreciatedDisplay?.configuration?.can_configure_in_workspace)
|
||||
display.twake.configuration.push("global");
|
||||
if (depreciatedDisplay?.configuration?.can_configure_in_channel)
|
||||
display.twake.configuration.push("channel");
|
||||
|
||||
display.twake.direct = depreciatedDisplay?.member_app
|
||||
? { name: application.identity.name, icon: application.identity.icon } || true
|
||||
: undefined;
|
||||
|
||||
if (depreciatedDisplay?.drive_module) {
|
||||
display.twake.files = {
|
||||
editor: undefined,
|
||||
actions: [],
|
||||
};
|
||||
|
||||
display.twake.files.editor = {
|
||||
preview_url: depreciatedDisplay?.drive_module?.can_open_files?.preview_url,
|
||||
edition_url: depreciatedDisplay?.drive_module?.can_open_files?.url,
|
||||
extensions: [
|
||||
...(depreciatedDisplay?.drive_module?.can_open_files?.main_ext || []),
|
||||
...(depreciatedDisplay?.drive_module?.can_open_files?.other_ext || []),
|
||||
],
|
||||
empty_files: (depreciatedDisplay?.drive_module?.can_create_files as any) || [],
|
||||
};
|
||||
}
|
||||
|
||||
if (depreciatedDisplay?.messages_module) {
|
||||
display.twake.chat = {
|
||||
input:
|
||||
depreciatedDisplay?.messages_module?.in_plus ||
|
||||
depreciatedDisplay?.messages_module?.right_icon
|
||||
? {
|
||||
icon: application.identity.icon,
|
||||
}
|
||||
: undefined,
|
||||
commands:
|
||||
(depreciatedDisplay?.messages_module
|
||||
?.commands as Application["display"]["twake"]["chat"]["commands"]) || undefined,
|
||||
actions: depreciatedDisplay?.messages_module?.action
|
||||
? [
|
||||
{
|
||||
name: depreciatedDisplay?.messages_module?.action.description,
|
||||
id: "default",
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
return display;
|
||||
};
|
||||
@@ -0,0 +1,173 @@
|
||||
import { DatabaseServiceAPI } from "../../../core/platform/services/database/api";
|
||||
import { TwakePlatform } from "../../../core/platform/platform";
|
||||
import Repository from "../../../core/platform/services/database/services/orm/repository/repository";
|
||||
import { ChannelMember, ChannelMemberReadCursors } from "../../../services/channels/entities";
|
||||
import {
|
||||
ExecutionContext,
|
||||
Paginable,
|
||||
Pagination,
|
||||
} from "../../../core/platform/framework/api/crud-service";
|
||||
import gr from "../../../services/global-resolver";
|
||||
import yargs from "yargs";
|
||||
import ora from "ora";
|
||||
import twake from "../../../twake";
|
||||
|
||||
class ChannelMemberReadCursorsMigrator {
|
||||
readSectionRepository: Repository<ChannelMemberReadCursors>;
|
||||
database: DatabaseServiceAPI;
|
||||
|
||||
constructor(readonly _platform: TwakePlatform) {
|
||||
this.database = this._platform.getProvider<DatabaseServiceAPI>("database");
|
||||
}
|
||||
|
||||
public async run(_options = {}, context?: ExecutionContext): Promise<void> {
|
||||
const companyPagination: Paginable = new Pagination(null, "100");
|
||||
const companies = await gr.services.companies.getCompanies(companyPagination);
|
||||
|
||||
context.user.server_request = true;
|
||||
|
||||
for (const company of companies.getEntities()) {
|
||||
const { id: CompanyId } = company;
|
||||
|
||||
const workspaces = await gr.services.workspaces.getAllForCompany(CompanyId);
|
||||
const workspaceIds = [...workspaces.map(({ id }) => id), "direct"];
|
||||
|
||||
for (const workspaceId of workspaceIds) {
|
||||
const channels = await gr.services.channels.channels.getAllChannelsInWorkspace(
|
||||
CompanyId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
for (const channel of channels) {
|
||||
const { id: channelId } = channel;
|
||||
const membersPagination: Pagination = new Pagination(null, "100");
|
||||
const threadsPagination: Pagination = new Pagination(null, "1");
|
||||
let members: ChannelMember[];
|
||||
|
||||
try {
|
||||
const membersList = await gr.services.channels.members.list(
|
||||
membersPagination,
|
||||
{},
|
||||
{
|
||||
...context,
|
||||
channel: {
|
||||
company_id: CompanyId,
|
||||
id: channelId,
|
||||
workspace_id: workspaceId,
|
||||
},
|
||||
},
|
||||
);
|
||||
members = membersList.getEntities();
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let lastMessage = await gr.services.messages.views.listChannel(
|
||||
threadsPagination,
|
||||
{},
|
||||
{
|
||||
...context,
|
||||
channel: {
|
||||
company_id: CompanyId,
|
||||
id: channelId,
|
||||
workspace_id: workspaceId,
|
||||
},
|
||||
user: {
|
||||
id: null,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const firstMessage = await gr.services.messages.views.listChannel(
|
||||
{
|
||||
...threadsPagination,
|
||||
reversed: true,
|
||||
},
|
||||
{},
|
||||
{
|
||||
...context,
|
||||
channel: {
|
||||
company_id: CompanyId,
|
||||
id: channelId,
|
||||
workspace_id: workspaceId,
|
||||
},
|
||||
user: {
|
||||
id: null,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!firstMessage.getEntities().length) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!lastMessage.getEntities().length) {
|
||||
lastMessage = firstMessage;
|
||||
}
|
||||
|
||||
const firstMessageId = firstMessage.getEntities()[0].id;
|
||||
const lastMessageId = lastMessage.getEntities()[0].id;
|
||||
|
||||
if (!firstMessageId) {
|
||||
continue;
|
||||
} else {
|
||||
for (const member of members) {
|
||||
await gr.services.channels.members.setChannelMemberReadSections(
|
||||
{
|
||||
start: firstMessageId,
|
||||
end: lastMessageId,
|
||||
},
|
||||
{
|
||||
...context,
|
||||
channel_id: channelId,
|
||||
workspace_id: workspaceId,
|
||||
company: {
|
||||
id: CompanyId,
|
||||
},
|
||||
user: {
|
||||
id: member.user_id,
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const services = [
|
||||
"search",
|
||||
"database",
|
||||
"webserver",
|
||||
"auth",
|
||||
"counter",
|
||||
"cron",
|
||||
"message-queue",
|
||||
"push",
|
||||
"realtime",
|
||||
"storage",
|
||||
"tracker",
|
||||
"websocket",
|
||||
"email-pusher",
|
||||
];
|
||||
|
||||
const command: yargs.CommandModule<unknown, unknown> = {
|
||||
command: "channel-member-read-cursor-repair",
|
||||
describe: "fixes the channel members read cursors for old messages",
|
||||
builder: {},
|
||||
|
||||
handler: async () => {
|
||||
const spinner = ora({ text: "Fixing channel members read cursors" }).start();
|
||||
const platform = await twake.run(services);
|
||||
await gr.doInit(platform);
|
||||
const migrator = new ChannelMemberReadCursorsMigrator(platform);
|
||||
|
||||
await migrator.run({});
|
||||
|
||||
return spinner.stop();
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,103 @@
|
||||
import yargs from "yargs";
|
||||
import twake from "../../../twake";
|
||||
import ora from "ora";
|
||||
import { TwakePlatform } from "../../../core/platform/platform";
|
||||
import { DatabaseServiceAPI } from "../../../core/platform/services/database/api";
|
||||
import { PhpDevice, TYPE as phpTYPE } from "./php-device/php-device-entity";
|
||||
import { Pagination } from "../../../core/platform/framework/api/crud-service";
|
||||
import Device, { TYPE } from "../../../services/user/entities/device";
|
||||
import User, { TYPE as userTYPE } from "../../../services/user/entities/user";
|
||||
import _ from "lodash";
|
||||
import gr from "../../../services/global-resolver";
|
||||
|
||||
type Options = {
|
||||
replaceExisting?: boolean;
|
||||
};
|
||||
|
||||
class DeviceMigrator {
|
||||
database: DatabaseServiceAPI;
|
||||
|
||||
constructor(readonly platform: TwakePlatform) {
|
||||
this.database = this.platform.getProvider<DatabaseServiceAPI>("database");
|
||||
}
|
||||
|
||||
public async run(options: Options = {}): Promise<void> {
|
||||
const phpRepository = await this.database.getRepository(phpTYPE, PhpDevice);
|
||||
const userRepository = await this.database.getRepository(userTYPE, User);
|
||||
const repository = await this.database.getRepository(TYPE, Device);
|
||||
|
||||
// Get all companies
|
||||
let page: Pagination = { limitStr: "100" };
|
||||
// For each devices
|
||||
do {
|
||||
const deviceListResult = await phpRepository.find({}, { pagination: page }, undefined);
|
||||
page = deviceListResult.nextPage as Pagination;
|
||||
|
||||
for (const device of deviceListResult.getEntities()) {
|
||||
if (
|
||||
!(await repository.findOne({ id: device.value }, {}, undefined)) ||
|
||||
options.replaceExisting
|
||||
) {
|
||||
if (device.type === "FCM" || device.type === "fcm") {
|
||||
const newDevice = new Device();
|
||||
newDevice.id = device.value;
|
||||
newDevice.type = "FCM";
|
||||
newDevice.user_id = device.user_id;
|
||||
newDevice.version = device.version;
|
||||
await repository.save(newDevice, undefined);
|
||||
|
||||
const user = await userRepository.findOne({ id: device.user_id }, {}, undefined);
|
||||
if (user) {
|
||||
user.devices = _.uniq([...(user.devices || []), newDevice.id]);
|
||||
await userRepository.save(user, undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (page.page_token);
|
||||
}
|
||||
}
|
||||
|
||||
const services = [
|
||||
"storage",
|
||||
"counter",
|
||||
"platform-services",
|
||||
"user",
|
||||
"search",
|
||||
"channels",
|
||||
"database",
|
||||
"webserver",
|
||||
"message-queue",
|
||||
"workspaces",
|
||||
"console",
|
||||
"auth",
|
||||
];
|
||||
|
||||
const command: yargs.CommandModule<unknown, unknown> = {
|
||||
command: "device",
|
||||
describe: "command that allow you to migrate php devices to node",
|
||||
builder: {
|
||||
replaceExisting: {
|
||||
default: false,
|
||||
type: "boolean",
|
||||
description: "Replace already migrated devices",
|
||||
},
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
handler: async argv => {
|
||||
const spinner = ora({ text: "Migrating php devices - " }).start();
|
||||
const platform = await twake.run(services);
|
||||
await gr.doInit(platform);
|
||||
const migrator = new DeviceMigrator(platform);
|
||||
|
||||
const replaceExisting = (argv.replaceExisting || false) as boolean;
|
||||
|
||||
await migrator.run({
|
||||
replaceExisting,
|
||||
});
|
||||
|
||||
return spinner.stop();
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,40 @@
|
||||
import ora from "ora";
|
||||
import globalResolver from "../../../services/global-resolver";
|
||||
import twake from "../../../twake";
|
||||
import yargs from "yargs";
|
||||
import DriveMigrator from "./php-drive-file/drive-migrator-service";
|
||||
|
||||
const services = [
|
||||
"search",
|
||||
"database",
|
||||
"webserver",
|
||||
"auth",
|
||||
"counter",
|
||||
"cron",
|
||||
"message-queue",
|
||||
"push",
|
||||
"realtime",
|
||||
"storage",
|
||||
"tracker",
|
||||
"websocket",
|
||||
"email-pusher",
|
||||
];
|
||||
|
||||
const command: yargs.CommandModule<unknown, unknown> = {
|
||||
command: "drive",
|
||||
describe: "migrate php drive items to node",
|
||||
builder: {},
|
||||
handler: async _argv => {
|
||||
console.log("test");
|
||||
const spinner = ora({ text: "Migrating php drive - " }).start();
|
||||
const platform = await twake.run(services);
|
||||
await globalResolver.doInit(platform);
|
||||
const migrator = new DriveMigrator(platform);
|
||||
|
||||
await migrator.run();
|
||||
|
||||
return spinner.stop();
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,96 @@
|
||||
import yargs from "yargs";
|
||||
import twake from "../../../twake";
|
||||
import ora from "ora";
|
||||
import { DatabaseServiceAPI } from "../../../core/platform/services/database/api";
|
||||
import { TwakePlatform } from "../../../core/platform/platform";
|
||||
import gr from "../../../services/global-resolver";
|
||||
|
||||
type Options = {
|
||||
from?: string;
|
||||
onlyCompany?: string;
|
||||
onlyWorkspace?: string;
|
||||
onlyChannel?: string;
|
||||
dryRun?: boolean;
|
||||
};
|
||||
|
||||
class MessageReferenceRepair {
|
||||
database: DatabaseServiceAPI;
|
||||
|
||||
constructor(readonly platform: TwakePlatform) {
|
||||
this.database = this.platform.getProvider<DatabaseServiceAPI>("database");
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async run(_options: Options = {}): Promise<void> {
|
||||
//TODO repair messages
|
||||
}
|
||||
}
|
||||
|
||||
const services = [
|
||||
"storage",
|
||||
"counter",
|
||||
"platform-services",
|
||||
"user",
|
||||
"search",
|
||||
"channels",
|
||||
"database",
|
||||
"webserver",
|
||||
"message-queue",
|
||||
"messages",
|
||||
];
|
||||
|
||||
const command: yargs.CommandModule<unknown, unknown> = {
|
||||
command: "message-channel-repair",
|
||||
describe: "command that allow you to repair messages references in channels",
|
||||
builder: {
|
||||
from: {
|
||||
default: null,
|
||||
type: "string",
|
||||
description: "Start migration from this company ID",
|
||||
},
|
||||
onlyCompany: {
|
||||
default: null,
|
||||
type: "string",
|
||||
description: "Migrate only this company ID",
|
||||
},
|
||||
onlyWorkspace: {
|
||||
default: null,
|
||||
type: "string",
|
||||
description: "Migrate only this workspace ID",
|
||||
},
|
||||
onlyChannel: {
|
||||
default: null,
|
||||
type: "string",
|
||||
description: "Migrate only this channel ID",
|
||||
},
|
||||
dryRun: {
|
||||
default: false,
|
||||
type: "boolean",
|
||||
description: "Do not save anything and show missing references",
|
||||
},
|
||||
},
|
||||
handler: async argv => {
|
||||
const spinner = ora({ text: "Fixing messages references - " }).start();
|
||||
const platform = await twake.run(services);
|
||||
await gr.doInit(platform);
|
||||
const migrator = new MessageReferenceRepair(platform);
|
||||
|
||||
const from = argv.from as string | null;
|
||||
const onlyCompany = argv.onlyCompany as string | null;
|
||||
const onlyWorkspace = argv.onlyWorkspace as string | null;
|
||||
const onlyChannel = argv.onlyChannel as string | null;
|
||||
const dryRun = (argv.dryRun || false) as boolean;
|
||||
|
||||
await migrator.run({
|
||||
from,
|
||||
onlyCompany,
|
||||
onlyWorkspace,
|
||||
onlyChannel,
|
||||
dryRun,
|
||||
});
|
||||
|
||||
return spinner.stop();
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,190 @@
|
||||
import yargs from "yargs";
|
||||
import twake from "../../../twake";
|
||||
import ora from "ora";
|
||||
import { TwakePlatform } from "../../../core/platform/platform";
|
||||
import { DatabaseServiceAPI } from "../../../core/platform/services/database/api";
|
||||
import {
|
||||
ExecutionContext,
|
||||
Paginable,
|
||||
Pagination,
|
||||
} from "../../../core/platform/framework/api/crud-service";
|
||||
import gr from "../../../services/global-resolver";
|
||||
import { getInstance, MessageFileRef } from "../../../services/messages/entities/message-file-refs";
|
||||
import Repository from "../../../core/platform/services/database/services/orm/repository/repository";
|
||||
import uuid from "node-uuid";
|
||||
import { MessageFile } from "../../../services/messages/entities/message-files";
|
||||
import { ThreadExecutionContext } from "../../../services/messages/types";
|
||||
|
||||
type Options = Record<string, unknown>;
|
||||
|
||||
class MessageFilesCacheMigrator {
|
||||
database: DatabaseServiceAPI;
|
||||
repository: Repository<MessageFileRef>;
|
||||
messageFileRepository: Repository<MessageFile>;
|
||||
|
||||
constructor(readonly platform: TwakePlatform) {
|
||||
this.database = this.platform.getProvider<DatabaseServiceAPI>("database");
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async run(_options: Options = {}, context?: ExecutionContext): Promise<void> {
|
||||
this.repository = await gr.database.getRepository<MessageFileRef>(
|
||||
"message_file_refs",
|
||||
MessageFileRef,
|
||||
);
|
||||
this.messageFileRepository = await gr.database.getRepository<MessageFile>(
|
||||
"message_files",
|
||||
MessageFile,
|
||||
);
|
||||
|
||||
let count = 0;
|
||||
|
||||
let companyPagination: Paginable = new Pagination(null, "100");
|
||||
do {
|
||||
const companyList = await gr.services.companies.getCompanies(companyPagination);
|
||||
companyPagination = companyList.nextPage;
|
||||
|
||||
for (const company of companyList.getEntities()) {
|
||||
const companyId: string = company.id;
|
||||
const workspaceList = await gr.services.workspaces.getAllForCompany(companyId);
|
||||
|
||||
for (const workspaceId of [...workspaceList.map(w => w.id), "direct"]) {
|
||||
const channelsList = await gr.services.channels.channels.getAllChannelsInWorkspace(
|
||||
companyId,
|
||||
workspaceId,
|
||||
context,
|
||||
);
|
||||
|
||||
for (const channel of channelsList) {
|
||||
const channelId = channel.id;
|
||||
|
||||
let threadPagination: Paginable = new Pagination(null, "100");
|
||||
do {
|
||||
const threadList = await gr.services.messages.views.listChannel(
|
||||
threadPagination,
|
||||
{},
|
||||
{
|
||||
channel: {
|
||||
company_id: companyId,
|
||||
workspace_id: workspaceId,
|
||||
id: channelId,
|
||||
},
|
||||
user: { id: null },
|
||||
},
|
||||
);
|
||||
|
||||
for (const thread of threadList.getEntities()) {
|
||||
let messagesPagination: Paginable = new Pagination(null, "100");
|
||||
do {
|
||||
const messagesList = await gr.services.messages.messages.list(
|
||||
messagesPagination,
|
||||
{},
|
||||
{
|
||||
thread: { id: thread.id },
|
||||
company: { id: companyId },
|
||||
workspace: { id: workspaceId },
|
||||
channel: { id: channelId },
|
||||
user: { id: null },
|
||||
} as ThreadExecutionContext,
|
||||
);
|
||||
messagesPagination = messagesList.nextPage;
|
||||
messagesPagination.page_token = messagesList.getEntities()[0]?.id;
|
||||
|
||||
for (const message of messagesList.getEntities()) {
|
||||
if (message.files && message.files.length > 0) {
|
||||
for (const _messageFile of message.files) {
|
||||
count++;
|
||||
try {
|
||||
const messageFile = await this.messageFileRepository.findOne(
|
||||
{
|
||||
message_id: message.id,
|
||||
id: _messageFile.id,
|
||||
},
|
||||
{},
|
||||
undefined,
|
||||
);
|
||||
|
||||
if (messageFile) {
|
||||
console.log(messageFile.metadata.name);
|
||||
|
||||
//Update user uploads
|
||||
const fileRef = getInstance({
|
||||
target_type: "user_upload",
|
||||
target_id: message.user_id,
|
||||
id: uuid.v1({ msecs: message.created_at }),
|
||||
created_at: message.created_at,
|
||||
company_id: companyId,
|
||||
workspace_id: workspaceId,
|
||||
channel_id: channelId,
|
||||
thread_id: message.thread_id,
|
||||
message_id: message.id,
|
||||
message_file_id: messageFile.id,
|
||||
file_id: messageFile.metadata.external_id,
|
||||
});
|
||||
await this.repository.save(fileRef, undefined);
|
||||
|
||||
//Update messageFileRepository
|
||||
|
||||
messageFile.cache = {
|
||||
company_id: companyId,
|
||||
workspace_id: workspaceId,
|
||||
channel_id: channelId,
|
||||
user_id: message.user_id,
|
||||
};
|
||||
messageFile.thread_id = message.thread_id;
|
||||
|
||||
await this.messageFileRepository.save(messageFile, undefined);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (messagesPagination.page_token);
|
||||
}
|
||||
|
||||
threadPagination = threadList.nextPage;
|
||||
threadPagination.page_token =
|
||||
threadPagination.page_token &&
|
||||
threadList.getEntities()?.[threadList.getEntities().length - 1]?.thread_id;
|
||||
} while (false); // && threadPagination.page_token && threadList.getEntities().length > 50);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log("updated messages: ", count);
|
||||
} while (companyPagination.page_token);
|
||||
}
|
||||
}
|
||||
|
||||
const services = [
|
||||
"search",
|
||||
"database",
|
||||
"webserver",
|
||||
"auth",
|
||||
"counter",
|
||||
"cron",
|
||||
"message-queue",
|
||||
"push",
|
||||
"realtime",
|
||||
"storage",
|
||||
"tracker",
|
||||
"websocket",
|
||||
];
|
||||
|
||||
const command: yargs.CommandModule<unknown, unknown> = {
|
||||
command: "message-files-cache",
|
||||
describe: "command that allow you to fix cache for each message-file refs",
|
||||
builder: {},
|
||||
|
||||
handler: async () => {
|
||||
const spinner = ora({ text: "Migrating messages - " }).start();
|
||||
const platform = await twake.run(services);
|
||||
await gr.doInit(platform);
|
||||
const migrator = new MessageFilesCacheMigrator(platform);
|
||||
|
||||
await migrator.run({});
|
||||
|
||||
return spinner.stop();
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,91 @@
|
||||
import yargs from "yargs";
|
||||
import twake from "../../../twake";
|
||||
import ora from "ora";
|
||||
import MessageMigrator from "./php-message/message-migrator-service";
|
||||
import gr from "../../../services/global-resolver";
|
||||
|
||||
const services = [
|
||||
"storage",
|
||||
"counter",
|
||||
"platform-services",
|
||||
"user",
|
||||
"search",
|
||||
"channels",
|
||||
"database",
|
||||
"webserver",
|
||||
"message-queue",
|
||||
"messages",
|
||||
"statistics",
|
||||
];
|
||||
|
||||
const command: yargs.CommandModule<unknown, unknown> = {
|
||||
command: "message",
|
||||
describe: "command that allow you to migrate php messages to node",
|
||||
builder: {
|
||||
from: {
|
||||
default: null,
|
||||
type: "string",
|
||||
description: "Start migration from this company ID",
|
||||
},
|
||||
onlyCompany: {
|
||||
default: null,
|
||||
type: "string",
|
||||
description: "Migrate only this company ID",
|
||||
},
|
||||
onlyWorkspace: {
|
||||
default: null,
|
||||
type: "string",
|
||||
description: "Migrate only this workspace ID",
|
||||
},
|
||||
onlyChannel: {
|
||||
default: null,
|
||||
type: "string",
|
||||
description: "Migrate only this channel ID",
|
||||
},
|
||||
ignoreExisting: {
|
||||
default: false,
|
||||
type: "boolean",
|
||||
description: "Skip existing message ids",
|
||||
},
|
||||
backToPhp: {
|
||||
default: false,
|
||||
type: "boolean",
|
||||
description:
|
||||
"Run the migration back, put node messages into php table (only the one that are not already here)",
|
||||
},
|
||||
dryRun: {
|
||||
default: false,
|
||||
type: "boolean",
|
||||
description: "Do not save anything",
|
||||
},
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
handler: async argv => {
|
||||
const spinner = ora({ text: "Migrating php messages - " }).start();
|
||||
const platform = await twake.run(services);
|
||||
await gr.doInit(platform);
|
||||
const migrator = new MessageMigrator(platform);
|
||||
|
||||
const from = argv.from as string | null;
|
||||
const onlyCompany = argv.onlyCompany as string | null;
|
||||
const onlyWorkspace = argv.onlyWorkspace as string | null;
|
||||
const onlyChannel = argv.onlyChannel as string | null;
|
||||
const ignoreExisting = (argv.ignoreExisting || false) as boolean;
|
||||
const backToPhp = (argv.backToPhp || false) as boolean;
|
||||
const dryRun = (argv.dryRun || false) as boolean;
|
||||
|
||||
await migrator.run({
|
||||
from,
|
||||
onlyCompany,
|
||||
onlyWorkspace,
|
||||
onlyChannel,
|
||||
ignoreExisting,
|
||||
backToPhp,
|
||||
dryRun,
|
||||
});
|
||||
|
||||
return spinner.stop();
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,125 @@
|
||||
import yargs from "yargs";
|
||||
import twake from "../../../twake";
|
||||
import ora from "ora";
|
||||
import { DatabaseServiceAPI } from "../../../core/platform/services/database/api";
|
||||
import { TwakePlatform } from "../../../core/platform/platform";
|
||||
import gr from "../../../services/global-resolver";
|
||||
import { MessageFileRef } from "../../../services/messages/entities/message-file-refs";
|
||||
import { Paginable } from "../../../core/platform/framework/api/crud-service";
|
||||
import { Pagination } from "../../../core/platform/framework/api/crud-service";
|
||||
import { fileIsMedia } from "../../../services/files/utils";
|
||||
import { MessageFile } from "../../../services/messages/entities/message-files";
|
||||
import _ from "lodash";
|
||||
|
||||
type Options = Record<string, unknown>;
|
||||
|
||||
class MessageReferenceRepair {
|
||||
database: DatabaseServiceAPI;
|
||||
|
||||
constructor(readonly platform: TwakePlatform) {
|
||||
this.database = this.platform.getProvider<DatabaseServiceAPI>("database");
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public async run(_options: Options = {}): Promise<void> {
|
||||
const repository = await gr.database.getRepository<MessageFileRef>(
|
||||
"message_file_refs",
|
||||
MessageFileRef,
|
||||
);
|
||||
const repositoryMessageFile = await gr.database.getRepository<MessageFile>(
|
||||
"message_files",
|
||||
MessageFile,
|
||||
);
|
||||
|
||||
let count = 0;
|
||||
|
||||
let companyPagination: Paginable = new Pagination(null, "100");
|
||||
do {
|
||||
const companyList = await gr.services.companies.getCompanies(companyPagination);
|
||||
companyPagination = companyList.nextPage;
|
||||
|
||||
for (const company of companyList.getEntities()) {
|
||||
const companyId: string = company.id;
|
||||
const workspaceList = await gr.services.workspaces.getAllForCompany(companyId);
|
||||
|
||||
for (const workspaceId of [...workspaceList.map(w => w.id), "direct"]) {
|
||||
const channelsList = await gr.services.channels.channels.getAllChannelsInWorkspace(
|
||||
companyId,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
for (const channel of channelsList) {
|
||||
const channelId = channel.id;
|
||||
let filePagination: Pagination = new Pagination(null, "100");
|
||||
|
||||
do {
|
||||
const items = await repository.find(
|
||||
{
|
||||
target_type: "channel",
|
||||
target_id: channelId,
|
||||
company_id: companyId,
|
||||
},
|
||||
{ pagination: filePagination },
|
||||
);
|
||||
|
||||
for (const item of items.getEntities()) {
|
||||
try {
|
||||
const msgFile = await repositoryMessageFile.findOne({
|
||||
message_id: item.message_id,
|
||||
id: item.message_file_id,
|
||||
});
|
||||
if (msgFile) {
|
||||
count++;
|
||||
const isMedia = fileIsMedia(msgFile);
|
||||
const ref = _.cloneDeep(item);
|
||||
ref.target_type = isMedia ? "channel_media" : "channel_file";
|
||||
await repository.save(ref);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Error", e);
|
||||
}
|
||||
}
|
||||
|
||||
filePagination = new Pagination(items.nextPage.page_token, "100");
|
||||
} while (filePagination.page_token);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("updated files refs: ", count);
|
||||
} while (companyPagination.page_token);
|
||||
}
|
||||
}
|
||||
|
||||
const services = [
|
||||
"search",
|
||||
"database",
|
||||
"webserver",
|
||||
"auth",
|
||||
"counter",
|
||||
"cron",
|
||||
"message-queue",
|
||||
"push",
|
||||
"realtime",
|
||||
"storage",
|
||||
"tracker",
|
||||
"websocket",
|
||||
];
|
||||
|
||||
const command: yargs.CommandModule<unknown, unknown> = {
|
||||
command: "messages-files-medias-separation",
|
||||
describe: "command to separate medias and files in messages-files channels refs",
|
||||
builder: {},
|
||||
handler: async () => {
|
||||
const spinner = ora({ text: "Fixing messages references - " }).start();
|
||||
const platform = await twake.run(services);
|
||||
await gr.doInit(platform);
|
||||
const migrator = new MessageReferenceRepair(platform);
|
||||
|
||||
await migrator.run({});
|
||||
|
||||
return spinner.stop();
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
} from "../../../../core/platform/services/database/services/orm/decorators";
|
||||
|
||||
export const TYPE = "application";
|
||||
|
||||
@Entity(TYPE, {
|
||||
primaryKey: [["group_id"], "app_group_name", "id"],
|
||||
type: TYPE,
|
||||
})
|
||||
export default class PhpApplication {
|
||||
@Type(() => String)
|
||||
@Column("id", "timeuuid", { generator: "timeuuid" })
|
||||
id: string;
|
||||
|
||||
@Type(() => String)
|
||||
@Column("group_id", "timeuuid")
|
||||
group_id: string;
|
||||
|
||||
@Type(() => String)
|
||||
@Column("app_group_name", "string")
|
||||
app_group_name = "";
|
||||
|
||||
@Column("name", "encoded_string")
|
||||
depreciated_name: string;
|
||||
|
||||
@Column("simple_name", "encoded_string")
|
||||
depreciated_simple_name: string;
|
||||
|
||||
@Column("description", "encoded_string")
|
||||
depreciated_description: string;
|
||||
|
||||
@Column("icon_url", "encoded_string")
|
||||
depreciated_icon_url: string;
|
||||
|
||||
@Column("is_default", "twake_boolean")
|
||||
is_default: boolean;
|
||||
|
||||
@Column("public", "twake_boolean")
|
||||
depreciated_public: boolean;
|
||||
|
||||
@Column("twake_team_validation", "twake_boolean")
|
||||
depreciated_twake_team_validation: boolean;
|
||||
|
||||
@Column("is_available_to_public", "twake_boolean")
|
||||
depreciated_is_available_to_public: boolean; //Vrai si $public ET $twake_team_validation
|
||||
|
||||
@Column("api_events_url", "encoded_string")
|
||||
depreciated_api_events_url: string;
|
||||
|
||||
@Column("api_allowed_ip", "encoded_string")
|
||||
depreciated_api_allowed_ip: string;
|
||||
|
||||
@Column("api_private_key", "encoded_string")
|
||||
depreciated_api_private_key: string;
|
||||
|
||||
@Column("privileges", "encoded_string")
|
||||
depreciated_privileges = "[]";
|
||||
|
||||
@Column("capabilities", "encoded_string")
|
||||
depreciated_capabilities = "[]";
|
||||
|
||||
@Column("hooks", "encoded_string")
|
||||
depreciated_hooks = "[]";
|
||||
|
||||
@Column("display_configuration", "encoded_string")
|
||||
depreciated_display_configuration = "{}";
|
||||
}
|
||||
|
||||
export type DepreciatedDisplayConfiguration = {
|
||||
version?: 0; //Legacy
|
||||
tasks_module?: {
|
||||
can_connect_to_tasks?: boolean;
|
||||
};
|
||||
calendar_module?: {
|
||||
can_connect_to_calendar?: boolean;
|
||||
};
|
||||
drive_module?: {
|
||||
can_connect_to_directory?: boolean;
|
||||
can_open_files?: {
|
||||
url?: string; //Une url à appeler pour éditer le fichier (ouvert dans un onglet)
|
||||
preview_url?: string; //Une url à appeler pour prévisualiser un fichier (iframe)
|
||||
main_ext?: string[]; //Extensions principales
|
||||
other_ext?: string[]; //Extensions secondaires
|
||||
};
|
||||
can_create_files?: {
|
||||
url?: string;
|
||||
filename?: string;
|
||||
name?: string;
|
||||
}[];
|
||||
};
|
||||
member_app?: boolean; // Si défini, votre application génèrera un membre
|
||||
// virtuel dans l'espace de travail avec lequel les
|
||||
// utilisateurs pourront discuter.
|
||||
messages_module?: {
|
||||
in_plus?: {
|
||||
should_wait_for_popup: boolean;
|
||||
};
|
||||
right_icon?: {
|
||||
icon_url?: string; //If defined replace original icon url of your app
|
||||
should_wait_for_popup?: boolean;
|
||||
type?: string; //"file" | "call"
|
||||
};
|
||||
action?: {
|
||||
should_wait_for_popup?: boolean;
|
||||
description?: string; //Description de l'action, sinon remplacé par le nom de l'app
|
||||
};
|
||||
commands?: {
|
||||
command?: string; // my_app mycommand
|
||||
description?: string;
|
||||
}[];
|
||||
};
|
||||
channel?: {
|
||||
can_connect_to_channel?: string;
|
||||
};
|
||||
channel_tab?: {
|
||||
iframe?: string;
|
||||
};
|
||||
app?: {
|
||||
iframe?: string;
|
||||
plus_btn: {
|
||||
should_wait_for_popup?: boolean;
|
||||
};
|
||||
};
|
||||
configuration?: {
|
||||
can_configure_in_workspace?: boolean;
|
||||
can_configure_in_channel?: boolean;
|
||||
can_configure_in_calendar?: boolean;
|
||||
can_configure_in_tasks?: boolean;
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
} from "../../../../core/platform/services/database/services/orm/decorators";
|
||||
|
||||
export const TYPE = "device";
|
||||
@Entity(TYPE, {
|
||||
primaryKey: [["id"]],
|
||||
type: TYPE,
|
||||
})
|
||||
export class PhpDevice {
|
||||
@Column("id", "timeuuid")
|
||||
id: string;
|
||||
|
||||
@Column("user_id", "timeuuid")
|
||||
user_id: string;
|
||||
|
||||
@Column("type", "string")
|
||||
type: string;
|
||||
|
||||
@Column("version", "string")
|
||||
version: string;
|
||||
|
||||
@Column("value", "encoded_string")
|
||||
value: string;
|
||||
}
|
||||
+535
@@ -0,0 +1,535 @@
|
||||
import { logger } from "../../../../core/platform/framework";
|
||||
import { ExecutionContext, Pagination } from "../../../../core/platform/framework/api/crud-service";
|
||||
import { TwakePlatform } from "../../../../core/platform/platform";
|
||||
import Repository from "../../../../core/platform/services/database/services/orm/repository/repository";
|
||||
import { DriveFile, AccessInformation } from "../../../../services/documents/entities/drive-file";
|
||||
import {
|
||||
generateAccessToken,
|
||||
getDefaultDriveItem,
|
||||
getDefaultDriveItemVersion,
|
||||
} from "../../../../services/documents/utils";
|
||||
import globalResolver from "../../../../services/global-resolver";
|
||||
import Company from "../../../../services/user/entities/company";
|
||||
import Workspace from "../../../../services/workspaces/entities/workspace";
|
||||
import { PhpDriveFile } from "./php-drive-file-entity";
|
||||
import { PhpDriveFileService } from "./php-drive-service";
|
||||
import mimes from "../../../../utils/mime";
|
||||
import WorkspaceUser from "../../../../services/workspaces/entities/workspace_user";
|
||||
import CompanyUser from "src/services/user/entities/company_user";
|
||||
|
||||
interface CompanyExecutionContext extends ExecutionContext {
|
||||
company: {
|
||||
id: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface WorkspaceExecutionContext extends CompanyExecutionContext {
|
||||
workspace_id: string;
|
||||
}
|
||||
|
||||
class DriveMigrator {
|
||||
private phpDriveService: PhpDriveFileService;
|
||||
private nodeRepository: Repository<DriveFile>;
|
||||
|
||||
constructor(readonly _platform: TwakePlatform) {
|
||||
this.phpDriveService = new PhpDriveFileService();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run
|
||||
*/
|
||||
public run = async (): Promise<void> => {
|
||||
await globalResolver.doInit(this._platform);
|
||||
await this.phpDriveService.init();
|
||||
|
||||
this.nodeRepository = await globalResolver.database.getRepository<DriveFile>(
|
||||
"drive_files",
|
||||
DriveFile,
|
||||
);
|
||||
|
||||
let page: Pagination = { limitStr: "100" };
|
||||
const context: ExecutionContext = {
|
||||
user: {
|
||||
id: null,
|
||||
server_request: true,
|
||||
},
|
||||
};
|
||||
|
||||
do {
|
||||
const companyListResult = await globalResolver.services.companies.getCompanies(page);
|
||||
page = companyListResult.nextPage as Pagination;
|
||||
|
||||
for (const company of companyListResult.getEntities()) {
|
||||
await this.migrateCompany(company, {
|
||||
...context,
|
||||
company: { id: company.id },
|
||||
});
|
||||
}
|
||||
} while (page.page_token);
|
||||
};
|
||||
|
||||
/**
|
||||
* Migrate a company drive files.
|
||||
*
|
||||
* @param {Company} company - the company to migrate
|
||||
*/
|
||||
private migrateCompany = async (
|
||||
company: Company,
|
||||
context: CompanyExecutionContext,
|
||||
): Promise<void> => {
|
||||
logger.info(`Migrating company ${company.id}`);
|
||||
|
||||
const companyAdminOrOwnerId = await this.getCompanyOwnerOrAdminId(company.id, context);
|
||||
const workspaceList = await globalResolver.services.workspaces.getAllForCompany(company.id);
|
||||
|
||||
for (const workspace of workspaceList) {
|
||||
const wsContext = {
|
||||
...context,
|
||||
workspace_id: workspace.id,
|
||||
user: { id: companyAdminOrOwnerId, server_request: true },
|
||||
};
|
||||
const access = await this.getWorkspaceAccess(workspace, company, wsContext);
|
||||
|
||||
await this.migrateWorkspace(workspace, access, wsContext);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Migrates a workspace drive files.
|
||||
*
|
||||
* @param {Workspace} workspace - the workspace to migrate
|
||||
*/
|
||||
private migrateWorkspace = async (
|
||||
workspace: Workspace,
|
||||
access: AccessInformation,
|
||||
context: WorkspaceExecutionContext,
|
||||
): Promise<void> => {
|
||||
let page: Pagination = { limitStr: "100" };
|
||||
|
||||
console.debug(`Migrating workspace ${workspace.id} of company ${context.company.id}`);
|
||||
logger.info(`Migrating workspace ${workspace.id} root folder`);
|
||||
|
||||
const workspaceFolder = await this.createWorkspaceFolder(workspace, access, context);
|
||||
// Migrate the root folder.
|
||||
do {
|
||||
const phpDriveFiles = await this.phpDriveService.listDirectory(
|
||||
page,
|
||||
"",
|
||||
workspace.id,
|
||||
context,
|
||||
);
|
||||
page = phpDriveFiles.nextPage as Pagination;
|
||||
|
||||
for (const phpDriveFile of phpDriveFiles.getEntities()) {
|
||||
await this.migrateDriveFile(phpDriveFile, workspaceFolder.id, access, context);
|
||||
}
|
||||
} while (page.page_token);
|
||||
|
||||
logger.info(`Migrating workspace ${workspace.id} trash`);
|
||||
// Migrate the trash.
|
||||
page = { limitStr: "100" };
|
||||
|
||||
do {
|
||||
const phpDriveFiles = await this.phpDriveService.listDirectory(page, "trash", workspace.id);
|
||||
page = phpDriveFiles.nextPage as Pagination;
|
||||
|
||||
for (const phpDriveFile of phpDriveFiles.getEntities()) {
|
||||
await this.migrateDriveFile(phpDriveFile, "trash", access, context);
|
||||
}
|
||||
} while (page.page_token);
|
||||
};
|
||||
|
||||
/**
|
||||
* Migrates a php drive item to a Node drive file
|
||||
*
|
||||
* @param {PhpDriveItem} item - the php drive file to migrate.
|
||||
*/
|
||||
private migrateDriveFile = async (
|
||||
item: PhpDriveFile,
|
||||
parentId: string,
|
||||
access: AccessInformation,
|
||||
context: WorkspaceExecutionContext,
|
||||
): Promise<void> => {
|
||||
logger.info(`Migrating php drive item ${item.id} - parent: ${parentId ?? "root"}`);
|
||||
|
||||
try {
|
||||
const migrationRecord = await this.phpDriveService.getMigrationRecord(
|
||||
item.id,
|
||||
context.company.id,
|
||||
);
|
||||
|
||||
const newDriveItem = getDefaultDriveItem(
|
||||
{
|
||||
name: item.name || item.id,
|
||||
extension: item.extension,
|
||||
added: item.added.toString(),
|
||||
content_keywords:
|
||||
item.content_keywords && item.content_keywords.length
|
||||
? item.content_keywords.join(",")
|
||||
: "",
|
||||
creator: item.creator || context.user.id,
|
||||
is_directory: item.isdirectory,
|
||||
is_in_trash: item.isintrash,
|
||||
description: item.description,
|
||||
tags: item.tags || [],
|
||||
parent_id: parentId,
|
||||
company_id: context.company.id,
|
||||
access_info: access,
|
||||
},
|
||||
context,
|
||||
);
|
||||
|
||||
if (migrationRecord && migrationRecord.company_id === context.company.id) {
|
||||
console.debug(`${item.id} is already migrated`);
|
||||
} else {
|
||||
await this.nodeRepository.save(newDriveItem);
|
||||
}
|
||||
|
||||
if (item.isdirectory) {
|
||||
const newParentId =
|
||||
migrationRecord && migrationRecord.company_id === context.company.id
|
||||
? migrationRecord.new_id
|
||||
: newDriveItem.id;
|
||||
|
||||
let page: Pagination = { limitStr: "100" };
|
||||
|
||||
do {
|
||||
const directoryChildren = await this.phpDriveService.listDirectory(
|
||||
page,
|
||||
item.id,
|
||||
context.workspace_id,
|
||||
);
|
||||
page = directoryChildren.nextPage as Pagination;
|
||||
|
||||
for (const child of directoryChildren.getEntities()) {
|
||||
try {
|
||||
await this.migrateDriveFile(child, newParentId, access, context);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to migrate drive item ${child.id}`);
|
||||
console.error(`Failed to migrate drive item ${child.id}`);
|
||||
}
|
||||
}
|
||||
} while (page.page_token);
|
||||
} else {
|
||||
let versionPage: Pagination = { limitStr: "100" };
|
||||
if (
|
||||
migrationRecord &&
|
||||
migrationRecord.item_id === item.id &&
|
||||
migrationRecord.company_id === context.company.id
|
||||
) {
|
||||
logger.info(`item is already migrated - ${item.id} - skipping`);
|
||||
console.log(`item is already migrated - ${item.id} - skipping`);
|
||||
return;
|
||||
}
|
||||
|
||||
const mime = mimes[item.extension];
|
||||
|
||||
let createdVersions = 0;
|
||||
|
||||
do {
|
||||
const itemVersions = await this.phpDriveService.listItemVersions(
|
||||
versionPage,
|
||||
item.id,
|
||||
context,
|
||||
);
|
||||
versionPage = itemVersions.nextPage as Pagination;
|
||||
|
||||
for (const version of itemVersions.getEntities()) {
|
||||
try {
|
||||
const newVersion = getDefaultDriveItemVersion(
|
||||
{
|
||||
creator_id: version.creator_id || context.user.id,
|
||||
data: version.data,
|
||||
date_added: +version.date_added,
|
||||
drive_item_id: newDriveItem.id,
|
||||
file_size: version.file_size,
|
||||
filename: version.filename,
|
||||
key: version.key,
|
||||
provider: version.provider,
|
||||
realname: version.realname,
|
||||
mode: version.mode,
|
||||
},
|
||||
context,
|
||||
);
|
||||
|
||||
const file = await this.phpDriveService.migrate(
|
||||
version.file_id,
|
||||
item.workspace_id,
|
||||
version.id,
|
||||
{
|
||||
filename: version.filename,
|
||||
userId: version.creator_id || context.user.id,
|
||||
totalSize: version.file_size,
|
||||
waitForThumbnail: true,
|
||||
chunkNumber: 1,
|
||||
totalChunks: 1,
|
||||
type: mime,
|
||||
},
|
||||
context,
|
||||
);
|
||||
|
||||
if (!file) {
|
||||
throw Error("cannot download file version");
|
||||
}
|
||||
|
||||
newVersion.file_metadata = {
|
||||
external_id: file.id,
|
||||
mime: file.metadata.mime,
|
||||
name: file.metadata.name || version.filename,
|
||||
size: file.upload_data.size || version.file_size,
|
||||
};
|
||||
|
||||
await globalResolver.services.documents.documents.createVersion(
|
||||
newDriveItem.id,
|
||||
newVersion,
|
||||
context,
|
||||
);
|
||||
|
||||
createdVersions++;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to migrate version ${version.id} for drive item ${item.id}`);
|
||||
console.error(`Failed to migrate version ${version.id} for drive item ${item.id}`);
|
||||
}
|
||||
}
|
||||
} while (versionPage.page_token);
|
||||
|
||||
if (createdVersions === 0) {
|
||||
await this.nodeRepository.remove(newDriveItem);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!migrationRecord) {
|
||||
await this.phpDriveService.markAsMigrated(item.id, newDriveItem.id, context.company.id);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Failed to migrate Drive item ${item.id} / workspace ${item.workspace_id} / company_id: ${context.company.id}`,
|
||||
error,
|
||||
);
|
||||
console.error(`Failed to migrate Drive item ${item.id}`, error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the first found company owner or admin identifier.
|
||||
*
|
||||
* @param {string} companyId - the companyId
|
||||
* @param {ExecutionContext} context - the execution context
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
private getCompanyOwnerOrAdminId = async (
|
||||
companyId: string,
|
||||
context: ExecutionContext,
|
||||
): Promise<string> => {
|
||||
let pagination: Pagination = { limitStr: "100" };
|
||||
let companyOwnerOrAdminId = null;
|
||||
|
||||
do {
|
||||
const companyUsers = await globalResolver.services.companies.companyUserRepository.find(
|
||||
{ group_id: companyId },
|
||||
{ pagination },
|
||||
context,
|
||||
);
|
||||
|
||||
pagination = companyUsers.nextPage as Pagination;
|
||||
|
||||
const companyAdminOrOwner = companyUsers
|
||||
.getEntities()
|
||||
.find(({ role }) => ["admin", "owner"].includes(role));
|
||||
|
||||
if (companyAdminOrOwner) {
|
||||
companyOwnerOrAdminId = companyAdminOrOwner.id;
|
||||
}
|
||||
} while (pagination && !companyOwnerOrAdminId);
|
||||
|
||||
return companyOwnerOrAdminId;
|
||||
};
|
||||
|
||||
/**
|
||||
* Compute the Access Information for the workspace folder to be created.
|
||||
*
|
||||
* @param {Workspace} workspace - the target workspace
|
||||
* @param {Company} company - the target company
|
||||
* @param {WorkspaceExecutionContext} context - the execution context
|
||||
* @returns {Promise<AccessInformation>}
|
||||
*/
|
||||
private getWorkspaceAccess = async (
|
||||
workspace: Workspace,
|
||||
company: Company,
|
||||
context: WorkspaceExecutionContext,
|
||||
): Promise<AccessInformation> => {
|
||||
const companyUsersCount = await globalResolver.services.companies.getUsersCount(company.id);
|
||||
const workspaceUsersCount = await globalResolver.services.workspaces.getUsersCount(
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
if (companyUsersCount === workspaceUsersCount) {
|
||||
return {
|
||||
entities: [
|
||||
{
|
||||
id: "parent",
|
||||
type: "folder",
|
||||
level: "manage",
|
||||
},
|
||||
{
|
||||
id: company.id,
|
||||
type: "company",
|
||||
level: "none",
|
||||
},
|
||||
{
|
||||
id: context.user?.id,
|
||||
type: "user",
|
||||
level: "manage",
|
||||
},
|
||||
],
|
||||
public: {
|
||||
level: "none",
|
||||
token: generateAccessToken(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let workspaceUsers: WorkspaceUser[] = [];
|
||||
let wsUsersPagination: Pagination = { limitStr: "100" };
|
||||
|
||||
do {
|
||||
const wsUsersQuery = await globalResolver.services.workspaces.getUsers(
|
||||
{ workspaceId: workspace.id },
|
||||
wsUsersPagination,
|
||||
context,
|
||||
);
|
||||
wsUsersPagination = wsUsersQuery.nextPage as Pagination;
|
||||
|
||||
workspaceUsers = [...workspaceUsers, ...wsUsersQuery.getEntities()];
|
||||
} while (wsUsersPagination.page_token);
|
||||
|
||||
if (companyUsersCount < 30 || workspaceUsersCount < 30) {
|
||||
return {
|
||||
entities: [
|
||||
{
|
||||
id: "parent",
|
||||
type: "folder",
|
||||
level: "none",
|
||||
},
|
||||
{
|
||||
id: company.id,
|
||||
type: "company",
|
||||
level: "none",
|
||||
},
|
||||
{
|
||||
id: context.user?.id,
|
||||
type: "user",
|
||||
level: "manage",
|
||||
},
|
||||
...workspaceUsers.reduce((acc, curr) => {
|
||||
acc = [
|
||||
...acc,
|
||||
{
|
||||
id: curr.userId,
|
||||
type: "user",
|
||||
level: "manage",
|
||||
},
|
||||
];
|
||||
|
||||
return acc;
|
||||
}, []),
|
||||
],
|
||||
public: {
|
||||
level: "none",
|
||||
token: generateAccessToken(),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
let companyUsers: CompanyUser[] = [];
|
||||
let companyUsersPaginations: Pagination = { limitStr: "100" };
|
||||
do {
|
||||
const companyUsersQuery = await globalResolver.services.companies.getUsers(
|
||||
{ group_id: company.id },
|
||||
companyUsersPaginations,
|
||||
{},
|
||||
context,
|
||||
);
|
||||
companyUsersPaginations = companyUsersQuery.nextPage as Pagination;
|
||||
companyUsers = [...companyUsers, ...companyUsersQuery.getEntities()];
|
||||
} while (companyUsersPaginations.page_token);
|
||||
return {
|
||||
entities: [
|
||||
{
|
||||
id: "parent",
|
||||
type: "folder",
|
||||
level: "none",
|
||||
},
|
||||
{
|
||||
id: company.id,
|
||||
type: "company",
|
||||
level: "manage",
|
||||
},
|
||||
{
|
||||
id: context.user?.id,
|
||||
type: "user",
|
||||
level: "manage",
|
||||
},
|
||||
...companyUsers.reduce((acc, curr) => {
|
||||
if (workspaceUsers.find(({ userId }) => curr.user_id === userId)) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc = [
|
||||
...acc,
|
||||
{
|
||||
id: curr.user_id,
|
||||
type: "user",
|
||||
level: "none",
|
||||
},
|
||||
];
|
||||
|
||||
return acc;
|
||||
}, []),
|
||||
],
|
||||
public: {
|
||||
level: "none",
|
||||
token: generateAccessToken(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a folder for the workspace to migrate.
|
||||
*
|
||||
* @param {Workspace} workspace - the workspace to migrate.
|
||||
* @param {AccessInformation} access - the access information.
|
||||
* @param {WorkspaceExecutionContext} context - the execution context
|
||||
* @returns {Promise<DriveFile>}
|
||||
*/
|
||||
private createWorkspaceFolder = async (
|
||||
workspace: Workspace,
|
||||
access: AccessInformation,
|
||||
context: WorkspaceExecutionContext,
|
||||
): Promise<DriveFile> => {
|
||||
const workspaceFolder = getDefaultDriveItem(
|
||||
{
|
||||
name: workspace.name || workspace.id,
|
||||
extension: "",
|
||||
content_keywords: "",
|
||||
creator: context.user.id,
|
||||
is_directory: true,
|
||||
is_in_trash: false,
|
||||
description: "",
|
||||
tags: [],
|
||||
parent_id: "root",
|
||||
company_id: context.company.id,
|
||||
access_info: access,
|
||||
},
|
||||
context,
|
||||
);
|
||||
|
||||
await this.nodeRepository.save(workspaceFolder);
|
||||
await this.phpDriveService.markAsMigrated(workspace.id, workspaceFolder.id, context.company.id);
|
||||
|
||||
return workspaceFolder;
|
||||
};
|
||||
}
|
||||
|
||||
export default DriveMigrator;
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
} from "../../../../core/platform/services/database/services/orm/decorators";
|
||||
|
||||
export const TYPE = "drive_file";
|
||||
|
||||
@Entity(TYPE, {
|
||||
primaryKey: [["workspace_id"], "parent_id", "id"],
|
||||
type: TYPE,
|
||||
})
|
||||
export class PhpDriveFile {
|
||||
@Type(() => String)
|
||||
@Column("workspace_id", "timeuuid")
|
||||
workspace_id: string;
|
||||
|
||||
@Type(() => String)
|
||||
@Column("parent_id", "string")
|
||||
parent_id: string | "";
|
||||
|
||||
@Type(() => String)
|
||||
@Column("id", "timeuuid", { generator: "timeuuid" })
|
||||
id: string;
|
||||
|
||||
@Column("isintrash", "boolean")
|
||||
isintrash: boolean;
|
||||
|
||||
@Column("added", "string")
|
||||
added: string;
|
||||
|
||||
@Column("attachements", "encoded_json")
|
||||
attachements: unknown;
|
||||
|
||||
@Column("content_keywords", "encoded_json")
|
||||
content_keywords: string[] | null;
|
||||
|
||||
@Column("creator", "string")
|
||||
creator: string;
|
||||
|
||||
@Column("description", "string")
|
||||
description: string;
|
||||
|
||||
@Column("extension", "string")
|
||||
extension: string;
|
||||
|
||||
@Column("hidden_data", "encoded_json")
|
||||
hidden_data: unknown;
|
||||
|
||||
@Column("isdirectory", "boolean")
|
||||
isdirectory: boolean;
|
||||
|
||||
@Column("last_modified", "string")
|
||||
last_modified: string;
|
||||
|
||||
@Column("name", "encoded_string")
|
||||
name: string;
|
||||
|
||||
@Column("size", "number")
|
||||
size: number;
|
||||
|
||||
@Column("tags", "encoded_json")
|
||||
tags: string[] | null;
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
} from "../../../../core/platform/services/database/services/orm/decorators";
|
||||
|
||||
export const TYPE = "drive_file_version";
|
||||
|
||||
@Entity(TYPE, {
|
||||
primaryKey: [["file_id"], "id"],
|
||||
type: TYPE,
|
||||
})
|
||||
export class PhpDriveFileVersion {
|
||||
@Type(() => String)
|
||||
@Column("id", "timeuuid", { generator: "timeuuid" })
|
||||
id: string;
|
||||
|
||||
@Type(() => String)
|
||||
@Column("file_id", "string")
|
||||
file_id: string;
|
||||
|
||||
@Type(() => String)
|
||||
@Column("creator_id", "string")
|
||||
creator_id: string;
|
||||
|
||||
@Type(() => String)
|
||||
@Column("realname", "encoded_string")
|
||||
realname: string;
|
||||
|
||||
@Type(() => String)
|
||||
@Column("key", "string")
|
||||
key: string;
|
||||
|
||||
@Type(() => String)
|
||||
@Column("mode", "string")
|
||||
mode: string;
|
||||
|
||||
@Type(() => String)
|
||||
@Column("file_size", "number")
|
||||
file_size: number;
|
||||
|
||||
@Type(() => String)
|
||||
@Column("date_added", "string")
|
||||
date_added: string;
|
||||
|
||||
@Type(() => String)
|
||||
@Column("filename", "encoded_string")
|
||||
filename: string;
|
||||
|
||||
@Type(() => String)
|
||||
@Column("provider", "string")
|
||||
provider: string;
|
||||
|
||||
@Column("data", "encoded_json")
|
||||
data: unknown;
|
||||
}
|
||||
|
||||
export type PhpDriveFileVersionPrimaryKey = Pick<PhpDriveFileVersion, "file_id" | "id">;
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
} from "../../../../core/platform/services/database/services/orm/decorators";
|
||||
|
||||
export const TYPE = "php_drive_migration_record";
|
||||
|
||||
@Entity(TYPE, {
|
||||
primaryKey: [["company_id"], "item_id"],
|
||||
type: TYPE,
|
||||
})
|
||||
export class phpDriveMigrationRecord {
|
||||
@Type(() => String)
|
||||
@Column("item_id", "timeuuid")
|
||||
item_id: string;
|
||||
|
||||
@Type(() => String)
|
||||
@Column("company_id", "uuid")
|
||||
company_id: string;
|
||||
|
||||
@Type(() => String)
|
||||
@Column("new_id", "string")
|
||||
new_id: string;
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
import {
|
||||
ExecutionContext,
|
||||
ListResult,
|
||||
Pagination,
|
||||
} from "../../../../core/platform/framework/api/crud-service";
|
||||
import Repository from "../../../../core/platform/services/database/services/orm/repository/repository";
|
||||
import globalResolver from "../../../../services/global-resolver";
|
||||
import { PhpDriveFile, TYPE as DRIVE_FILE_TABLE } from "./php-drive-file-entity";
|
||||
import { Initializable, logger, TwakeServiceProvider } from "../../../../core/platform/framework";
|
||||
import {
|
||||
PhpDriveFileVersion,
|
||||
TYPE as DRIVE_FILE_VERSION_TABLE,
|
||||
} from "./php-drive-file-version-entity";
|
||||
import axios from "axios";
|
||||
import { Multipart } from "fastify-multipart";
|
||||
import { CompanyExecutionContext } from "../../../../services/files/web/types";
|
||||
import { File } from "../../../../services/files/entities/file";
|
||||
import { UploadOptions } from "../../../../services/files/types";
|
||||
import {
|
||||
phpDriveMigrationRecord,
|
||||
TYPE as MIGRATION_RECORD_TABLE,
|
||||
} from "./php-drive-migration-record-entity";
|
||||
|
||||
export interface MigrateOptions extends UploadOptions {
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface PhpDriveServiceAPI extends TwakeServiceProvider, Initializable {}
|
||||
|
||||
export class PhpDriveFileService implements PhpDriveServiceAPI {
|
||||
version: "1";
|
||||
public repository: Repository<PhpDriveFile>;
|
||||
public versionRepository: Repository<PhpDriveFileVersion>;
|
||||
public migrationRepository: Repository<phpDriveMigrationRecord>;
|
||||
|
||||
/**
|
||||
* Init the service.
|
||||
*
|
||||
* @returns {PhpDriveFileService}
|
||||
*/
|
||||
async init(): Promise<this> {
|
||||
this.repository = await globalResolver.database.getRepository<PhpDriveFile>(
|
||||
DRIVE_FILE_TABLE,
|
||||
PhpDriveFile,
|
||||
);
|
||||
|
||||
this.versionRepository = await globalResolver.database.getRepository<PhpDriveFileVersion>(
|
||||
DRIVE_FILE_VERSION_TABLE,
|
||||
PhpDriveFileVersion,
|
||||
);
|
||||
|
||||
this.migrationRepository = await globalResolver.database.getRepository<phpDriveMigrationRecord>(
|
||||
MIGRATION_RECORD_TABLE,
|
||||
phpDriveMigrationRecord,
|
||||
);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists the drive item directory children.
|
||||
*
|
||||
* @param {Pagination} pagination - the page.
|
||||
* @param {string} directory - the drive item / directory id to search within.
|
||||
* @param {string} workspaceId - the workspace id
|
||||
* @param {ExecutionContext} context - the execution context.
|
||||
* @returns {Promise<ListResult<PhpDriveFile>>} - the drive item children.
|
||||
*/
|
||||
listDirectory = async (
|
||||
pagination: Pagination,
|
||||
directory: string | "" | "trash",
|
||||
workspaceId: string,
|
||||
context?: ExecutionContext,
|
||||
): Promise<ListResult<PhpDriveFile>> =>
|
||||
await this.repository.find(
|
||||
{
|
||||
workspace_id: workspaceId,
|
||||
parent_id: directory,
|
||||
},
|
||||
{ pagination },
|
||||
context,
|
||||
);
|
||||
|
||||
/**
|
||||
* Lists the versions of a drive item.
|
||||
*
|
||||
* @param {Pagination} pagination - the page.
|
||||
* @param {string} itemId - the drive item id.
|
||||
* @param {ExecutionContext} context - the execution context.
|
||||
* @returns {Promise<ListResult<PhpDriveFileVersion>>} - the list of the item versions.
|
||||
*/
|
||||
listItemVersions = async (
|
||||
pagination: Pagination,
|
||||
itemId: string,
|
||||
context?: ExecutionContext,
|
||||
): Promise<ListResult<PhpDriveFileVersion>> =>
|
||||
await this.versionRepository.find(
|
||||
{
|
||||
file_id: itemId,
|
||||
},
|
||||
{ pagination },
|
||||
context,
|
||||
);
|
||||
|
||||
/**
|
||||
* Downloads a file version from the old drive and uploads it to the new Drive.
|
||||
*
|
||||
* @param {string} fileId - the old file id
|
||||
* @param {string} workspaceId - the workspace id
|
||||
* @param {string} versionId - the version id
|
||||
* @param {MigrateOptions} options - the file upload / migration options.
|
||||
* @param {CompanyExecutionContext} context - the company execution context.
|
||||
* @param {string} public_access_key - the file public access key.
|
||||
* @returns {Promise<File>} - the uploaded file information.
|
||||
*/
|
||||
migrate = async (
|
||||
fileId: string,
|
||||
workspaceId: string,
|
||||
versionId: string,
|
||||
options: MigrateOptions,
|
||||
context: CompanyExecutionContext,
|
||||
public_access_key?: string,
|
||||
): Promise<File> => {
|
||||
try {
|
||||
const url = `https://web.twake.app/ajax/drive/download?workspace_id=${workspaceId}&element_id=${fileId}&version_id=${versionId}&download=1${
|
||||
public_access_key ? `&public_access_key=${public_access_key}` : ""
|
||||
}`;
|
||||
|
||||
const response = await axios.get(url, {
|
||||
responseType: "stream",
|
||||
});
|
||||
|
||||
if (!response.data) {
|
||||
throw Error("invalid download response");
|
||||
}
|
||||
|
||||
const file = {
|
||||
file: response.data,
|
||||
};
|
||||
|
||||
return await globalResolver.services.files.save(null, file as Multipart, options, {
|
||||
...context,
|
||||
user: {
|
||||
id: options.userId,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Failed to migrate file ${fileId} on workspace ${workspaceId}`, error);
|
||||
throw Error(error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Saves a drive item.
|
||||
*
|
||||
* @param {PhpDriveFile} item - the php drive item.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
save = async (item: PhpDriveFile): Promise<void> => await this.repository.save(item);
|
||||
|
||||
/**
|
||||
* Marks a drive item as migrated.
|
||||
*
|
||||
* @param {string} itemId - the drive item.
|
||||
* @param {string} newId - the new drive item id.
|
||||
* @param {string} companyId - the company id.
|
||||
*/
|
||||
markAsMigrated = async (itemId: string, newId: string, companyId: string): Promise<void> => {
|
||||
const migrationRecord = new phpDriveMigrationRecord();
|
||||
migrationRecord.item_id = itemId;
|
||||
migrationRecord.new_id = newId;
|
||||
migrationRecord.company_id = companyId;
|
||||
|
||||
await this.migrationRepository.save(migrationRecord);
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetches the drive item migration record.
|
||||
*
|
||||
* @param {string} itemId - the drive item id.
|
||||
* @param {string} companyId - the company id.
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
getMigrationRecord = async (
|
||||
itemId: string,
|
||||
companyId: string,
|
||||
): Promise<phpDriveMigrationRecord> =>
|
||||
await this.migrationRepository.findOne({ item_id: itemId, company_id: companyId });
|
||||
}
|
||||
+560
@@ -0,0 +1,560 @@
|
||||
import { PhpMessagesService } from "./php-message-service";
|
||||
import { convertUuidV4ToV1 } from "./utils";
|
||||
import Company from "../../../../services/user/entities/company";
|
||||
import { Pagination } from "../../../../core/platform/framework/api/crud-service";
|
||||
import { PhpMessage } from "./php-message-entity";
|
||||
import { TwakePlatform } from "../../../../core/platform/platform";
|
||||
import {
|
||||
Message,
|
||||
MessageEdited,
|
||||
MessageOverride,
|
||||
MessagePinnedInfo,
|
||||
MessageReaction,
|
||||
} from "../../../../services/messages/entities/messages";
|
||||
import { MessageChannelRef } from "../../../../services/messages/entities/message-channel-refs";
|
||||
import { ParticipantObject, Thread } from "../../../../services/messages/entities/threads";
|
||||
import { Block } from "../../../../services/messages/blocks-types";
|
||||
import { WorkspaceExecutionContext } from "../../../../services/workspaces/types";
|
||||
import gr from "../../../../services/global-resolver";
|
||||
|
||||
type MigratedChannel = {
|
||||
id: string;
|
||||
workspace_id: string;
|
||||
company_id: string;
|
||||
owner?: string;
|
||||
};
|
||||
|
||||
type Options = {
|
||||
from?: string;
|
||||
onlyCompany?: string;
|
||||
onlyWorkspace?: string;
|
||||
onlyChannel?: string;
|
||||
ignoreExisting?: boolean;
|
||||
backToPhp?: boolean;
|
||||
dryRun?: boolean;
|
||||
};
|
||||
class MessageMigrator {
|
||||
private phpMessageService: PhpMessagesService;
|
||||
private migratedMessages = 0;
|
||||
private options: Options = {};
|
||||
|
||||
constructor(readonly _platform: TwakePlatform) {
|
||||
this.phpMessageService = new PhpMessagesService();
|
||||
}
|
||||
|
||||
public async run(options: Options = {}): Promise<void> {
|
||||
await gr.doInit(this._platform);
|
||||
this.options = options;
|
||||
|
||||
await this.phpMessageService.init();
|
||||
|
||||
if (this.options.onlyCompany) {
|
||||
const company = await gr.services.companies.getCompany({ id: options.onlyCompany });
|
||||
await this.migrateCompanyMessages(company);
|
||||
} else {
|
||||
let waitForCompany = false;
|
||||
if (this.options.from) {
|
||||
waitForCompany = true;
|
||||
}
|
||||
|
||||
// Get all companies
|
||||
let page: Pagination = { limitStr: "100" };
|
||||
// For each companies find workspaces
|
||||
do {
|
||||
const companyListResult = await gr.services.companies.getCompanies(page);
|
||||
page = companyListResult.nextPage as Pagination;
|
||||
|
||||
for (const company of companyListResult.getEntities()) {
|
||||
if (waitForCompany && this.options.from == `${company.id}`) {
|
||||
waitForCompany = false;
|
||||
}
|
||||
|
||||
if (!waitForCompany) {
|
||||
await this.migrateCompanyMessages(company);
|
||||
}
|
||||
}
|
||||
} while (page.page_token);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`Php Messages successfully migrated to node: (${this.migratedMessages} messages) !`,
|
||||
);
|
||||
}
|
||||
|
||||
private async migrateCompanyMessages(company: Company) {
|
||||
console.log(`Start migration for ${company.id}...`);
|
||||
|
||||
if (!this.options.onlyWorkspace || this.options.onlyWorkspace === "direct") {
|
||||
await this.migrateCompanyDirectMessages(company);
|
||||
console.log(
|
||||
`${company.id} - (1/2) migrated direct messages (total: ${this.migratedMessages} messages) ✅`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!this.options.onlyWorkspace || this.options.onlyWorkspace !== "direct") {
|
||||
await this.migrateCompanyChannelsMessages(company);
|
||||
console.log(`${company.id} - (2/2) completed (total: ${this.migratedMessages} messages) ✅`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all direct messages in company and set them to channelPhpMessages
|
||||
*/
|
||||
private async migrateCompanyDirectMessages(company: Company) {
|
||||
await gr.doInit(this._platform);
|
||||
let pageDirectChannels: Pagination = { limitStr: "100" };
|
||||
// For each directChannels find messages
|
||||
do {
|
||||
const directChannelsInCompanyResult = await gr.services.channels.channels.list(
|
||||
pageDirectChannels,
|
||||
{},
|
||||
{
|
||||
workspace: {
|
||||
workspace_id: "direct",
|
||||
company_id: company.id,
|
||||
},
|
||||
user: { id: null, server_request: true },
|
||||
},
|
||||
);
|
||||
|
||||
pageDirectChannels = directChannelsInCompanyResult.nextPage as Pagination;
|
||||
|
||||
for (const directChannel of directChannelsInCompanyResult.getEntities()) {
|
||||
await this.migrateChannelsMessages(company, {
|
||||
id: directChannel.id,
|
||||
workspace_id: "direct",
|
||||
company_id: directChannel.company_id,
|
||||
});
|
||||
}
|
||||
} while (pageDirectChannels.page_token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all messages in company and set them to channelPhpMessages
|
||||
*/
|
||||
private async migrateCompanyChannelsMessages(company: Company) {
|
||||
await gr.doInit(this._platform);
|
||||
|
||||
// Get all workspaces in company
|
||||
const workspacesInCompany = (
|
||||
await gr.services.workspaces.list({ limitStr: "" }, {}, {
|
||||
user: {
|
||||
id: null,
|
||||
server_request: true,
|
||||
},
|
||||
company_id: company.id,
|
||||
} as WorkspaceExecutionContext)
|
||||
).getEntities();
|
||||
|
||||
// For each workspaces find channels
|
||||
for (const workspace of workspacesInCompany) {
|
||||
if (this.options.onlyWorkspace && `${workspace.id}` !== this.options.onlyWorkspace) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all channels in workspace
|
||||
let pageChannels: Pagination = { limitStr: "1" };
|
||||
do {
|
||||
const channelsInWorkspace = await gr.services.channels.channels.list(
|
||||
pageChannels,
|
||||
{},
|
||||
{
|
||||
workspace: {
|
||||
workspace_id: workspace.id,
|
||||
company_id: workspace.company_id,
|
||||
},
|
||||
user: { id: null, server_request: true },
|
||||
},
|
||||
);
|
||||
pageChannels = channelsInWorkspace.nextPage as Pagination;
|
||||
|
||||
// For each channels find messages
|
||||
for (const channel of channelsInWorkspace.getEntities()) {
|
||||
await this.migrateChannelsMessages(company, {
|
||||
id: channel.id,
|
||||
workspace_id: channel.workspace_id,
|
||||
company_id: channel.company_id,
|
||||
owner: channel.owner,
|
||||
});
|
||||
}
|
||||
} while (pageChannels.page_token);
|
||||
}
|
||||
}
|
||||
|
||||
//Params: company, channel
|
||||
private async migrateChannelsMessages(company: Company, channel: MigratedChannel) {
|
||||
if (this.options.onlyChannel && `${channel.id}` !== this.options.onlyChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.options.backToPhp) {
|
||||
await this.migrateChannelsMessagesBackToPhp(company, channel);
|
||||
return;
|
||||
}
|
||||
|
||||
//This function will migrate all messages in a channel
|
||||
let pagePhpMessages: Pagination = { limitStr: "100" };
|
||||
do {
|
||||
const messages = await this.phpMessageService.list(
|
||||
pagePhpMessages,
|
||||
{},
|
||||
{
|
||||
channel_id: convertUuidV4ToV1(channel.id),
|
||||
user: { id: null, server_request: true },
|
||||
},
|
||||
);
|
||||
|
||||
for (const message of messages.getEntities()) {
|
||||
try {
|
||||
await this.migrateMessage(company, channel, message);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
|
||||
pagePhpMessages = messages.nextPage as Pagination;
|
||||
} while (pagePhpMessages.page_token);
|
||||
}
|
||||
|
||||
private migratedThreads: string[] = [];
|
||||
|
||||
/**
|
||||
* Migrate php message to node
|
||||
* @param company
|
||||
* @param channel
|
||||
* @param message
|
||||
*/
|
||||
private async migrateMessage(company: Company, channel: MigratedChannel, message: PhpMessage) {
|
||||
if (!message.id) {
|
||||
return;
|
||||
}
|
||||
await gr.doInit(this._platform);
|
||||
//Create thread first if not exists
|
||||
const threadId = message.parent_message_id || message.id;
|
||||
|
||||
if (this.options.ignoreExisting) {
|
||||
const msg = await gr.services.messages.messages.get({
|
||||
thread_id: threadId,
|
||||
id: message.id,
|
||||
});
|
||||
if (msg) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const threadDoesNotExists = !this.migratedThreads.includes(threadId);
|
||||
if (threadDoesNotExists) {
|
||||
await this.migratePhpMessageToNodeThread(message, channel, company);
|
||||
this.migratedThreads.push(threadId);
|
||||
}
|
||||
|
||||
//Migrate message itself
|
||||
await this.migratePhpMessageToNodeMessage(threadId, message, company);
|
||||
|
||||
this.migratedMessages++;
|
||||
|
||||
if (this.migratedMessages % 100 == 0) {
|
||||
console.log(`${company.id} - ... (total: ${this.migratedMessages} messages)`);
|
||||
}
|
||||
|
||||
//Force delay between channels
|
||||
await new Promise(r => {
|
||||
setTimeout(r, 40);
|
||||
});
|
||||
}
|
||||
|
||||
private async migrateChannelsMessagesBackToPhp(company: Company, channel: MigratedChannel) {
|
||||
await gr.doInit(this._platform);
|
||||
const channelRefRepository = await gr.database.getRepository(
|
||||
"message_channel_refs",
|
||||
MessageChannelRef,
|
||||
);
|
||||
const messageRepository = await gr.database.getRepository("messages", Message);
|
||||
|
||||
//This function will migrate all messages in a channel
|
||||
let pageMessages: Pagination = { limitStr: "100" };
|
||||
do {
|
||||
const messages = await channelRefRepository.find(
|
||||
{
|
||||
company_id: company.id,
|
||||
workspace_id: channel.workspace_id,
|
||||
channel_id: channel.id,
|
||||
},
|
||||
{ pagination: pageMessages },
|
||||
undefined,
|
||||
);
|
||||
|
||||
for (const messageRef of messages.getEntities()) {
|
||||
const messages = await messageRepository.find(
|
||||
{
|
||||
thread_id: messageRef.thread_id,
|
||||
},
|
||||
{},
|
||||
undefined,
|
||||
);
|
||||
|
||||
for (const message of messages.getEntities()) {
|
||||
const uuidv1_channel_id =
|
||||
channel.id.substring(0, 14) + "1" + channel.id.substring(14 + 1);
|
||||
|
||||
let phpMessage = await this.phpMessageService.get({
|
||||
id: message.id,
|
||||
});
|
||||
|
||||
if (!phpMessage) {
|
||||
phpMessage = await this.phpMessageService.get({
|
||||
channel_id: uuidv1_channel_id,
|
||||
parent_message_id: message.thread_id,
|
||||
id: message.id,
|
||||
});
|
||||
}
|
||||
|
||||
if (!phpMessage && message.subtype !== "deleted") {
|
||||
//This message doesn't exists in php, move it to php
|
||||
|
||||
const newPhpMessage = new PhpMessage();
|
||||
newPhpMessage.id = message.id;
|
||||
newPhpMessage.channel_id = uuidv1_channel_id;
|
||||
newPhpMessage.parent_message_id = message.thread_id;
|
||||
newPhpMessage.application_id = message.application_id || null;
|
||||
newPhpMessage.modification_date = Math.floor(
|
||||
message.edited?.edited_at || message.created_at,
|
||||
);
|
||||
newPhpMessage.creation_date = Math.floor(message.created_at);
|
||||
newPhpMessage.sender = message.user_id;
|
||||
newPhpMessage.pinned = !!message.pinned_info?.pinned_at;
|
||||
newPhpMessage.edited = !!message.edited?.edited_at;
|
||||
newPhpMessage.message_type =
|
||||
message.subtype === "application" ? 1 : message.subtype === "system" ? 2 : 0;
|
||||
newPhpMessage.hidden_data = message.context;
|
||||
newPhpMessage.reactions = "{}";
|
||||
newPhpMessage.responses_count = 0;
|
||||
|
||||
let prepared: any[] = [];
|
||||
(message.blocks || []).map(block => {
|
||||
if (block.type === "twacode") {
|
||||
prepared = block["elements"];
|
||||
}
|
||||
if (block.type === "section" && prepared.length > 0) {
|
||||
prepared = [
|
||||
{
|
||||
type: "twacode",
|
||||
content: block?.text?.text || "",
|
||||
},
|
||||
];
|
||||
}
|
||||
});
|
||||
newPhpMessage.content = {
|
||||
fallback_string: message.text,
|
||||
original_str: message.text,
|
||||
files: (message.files || []).map(f => {
|
||||
return {
|
||||
content: f,
|
||||
mode: "mini",
|
||||
type: "file",
|
||||
};
|
||||
}),
|
||||
prepared: prepared,
|
||||
};
|
||||
|
||||
if (!this.options.dryRun) {
|
||||
await this.phpMessageService.repository.save(newPhpMessage, undefined);
|
||||
}
|
||||
|
||||
this.migratedMessages++;
|
||||
if (this.migratedMessages % 100 == 0) {
|
||||
console.log(`${company.id} - ... (total: ${this.migratedMessages} messages)`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pageMessages = messages.nextPage as Pagination;
|
||||
} while (pageMessages.page_token);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate php message to node thread
|
||||
* @param message PhpMessage
|
||||
* @param channel MigratedChannel
|
||||
*/
|
||||
private async migratePhpMessageToNodeThread(
|
||||
message: PhpMessage,
|
||||
channel: MigratedChannel,
|
||||
company: Company,
|
||||
) {
|
||||
const thread = new Thread();
|
||||
|
||||
// Set nodeThread values
|
||||
thread.id = message.parent_message_id || message.id;
|
||||
thread.created_at = message.creation_date;
|
||||
thread.last_activity = message.modification_date;
|
||||
thread.answers = 0;
|
||||
thread.participants = [
|
||||
{
|
||||
type: "channel",
|
||||
id: channel.id,
|
||||
company_id: channel.company_id,
|
||||
workspace_id: channel.workspace_id,
|
||||
created_at: message.creation_date,
|
||||
created_by: message.sender,
|
||||
} as ParticipantObject,
|
||||
];
|
||||
|
||||
if (this.options.dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create nodeThread
|
||||
return await gr.services.messages.threads.save(
|
||||
thread,
|
||||
{},
|
||||
{ user: { id: null, server_request: true }, company },
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set message string type
|
||||
* @param integer 0 = null | 1 = system | 2 = application
|
||||
*/
|
||||
private setMessageType(integer: number): { type: Message["type"]; subtype: Message["subtype"] } {
|
||||
switch (integer) {
|
||||
case 1:
|
||||
return {
|
||||
type: "message",
|
||||
subtype: "application",
|
||||
};
|
||||
case 2:
|
||||
return {
|
||||
type: "message",
|
||||
subtype: "system",
|
||||
};
|
||||
case 0:
|
||||
default:
|
||||
return {
|
||||
type: "message",
|
||||
subtype: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set blocks array
|
||||
* @param content { [key: string]: any }
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
private setBlocks(content: { [key: string]: any }): Block[] {
|
||||
const blocks: Block[] = [];
|
||||
|
||||
if (!content) return blocks;
|
||||
|
||||
const new_block_format: Block = {
|
||||
type: "twacode",
|
||||
elements: content.formatted || content.prepared || content || [],
|
||||
};
|
||||
|
||||
blocks.push(new_block_format);
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set edited message Object
|
||||
* @param modification_date timestamp
|
||||
*/
|
||||
private setMessageEditedObject(message: PhpMessage): MessageEdited {
|
||||
return message.edited ? { edited_at: message.modification_date } : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set pinned message Object
|
||||
* @param message PhpMessage
|
||||
*/
|
||||
private setMessagePinnedObject(message: PhpMessage): MessagePinnedInfo {
|
||||
if (message.pinned) {
|
||||
return { pinned_at: 0, pinned_by: message.sender };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set reactions message object
|
||||
* @param reactions JSON
|
||||
*/
|
||||
private setMessageReactionsObject(reactions: string): MessageReaction[] {
|
||||
const parsed_reactions = JSON.parse(reactions);
|
||||
|
||||
if (!parsed_reactions) return [];
|
||||
|
||||
const new_reactions_array: MessageReaction[] = [];
|
||||
|
||||
for (const reaction_name in parsed_reactions) {
|
||||
const new_reaction_object: MessageReaction = {
|
||||
name: reaction_name,
|
||||
users: parsed_reactions[reaction_name].users,
|
||||
count: parsed_reactions[reaction_name].count,
|
||||
};
|
||||
|
||||
new_reactions_array.push(new_reaction_object);
|
||||
}
|
||||
|
||||
return new_reactions_array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set override message Object
|
||||
* @param message PhpMessage
|
||||
*/
|
||||
private setMessageOverrideObject(_message: PhpMessage): MessageOverride {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate php message to node message
|
||||
* @param message PhpMessage
|
||||
*/
|
||||
private async migratePhpMessageToNodeMessage(
|
||||
threadId: string,
|
||||
message: PhpMessage,
|
||||
company: Company,
|
||||
) {
|
||||
const nodeMessage = new Message();
|
||||
|
||||
// Set nodeMessage values
|
||||
nodeMessage.id = message.id;
|
||||
nodeMessage.thread_id = threadId;
|
||||
nodeMessage.type = this.setMessageType(message.message_type).type;
|
||||
nodeMessage.subtype = this.setMessageType(message.message_type).subtype;
|
||||
nodeMessage.created_at = message.creation_date;
|
||||
nodeMessage.user_id = message.sender;
|
||||
nodeMessage.application_id = message.application_id;
|
||||
nodeMessage.text = message.content?.original_str || "";
|
||||
nodeMessage.blocks = this.setBlocks(message.content);
|
||||
nodeMessage.context = message.hidden_data || {};
|
||||
nodeMessage.edited = this.setMessageEditedObject(message);
|
||||
nodeMessage.pinned_info = this.setMessagePinnedObject(message);
|
||||
nodeMessage.reactions = this.setMessageReactionsObject(message.reactions);
|
||||
nodeMessage.override = this.setMessageOverrideObject(message);
|
||||
|
||||
nodeMessage.context._front_id = message.id;
|
||||
|
||||
if (this.options.dryRun) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Create nodeMessage then add it to thread
|
||||
return await gr.services.messages.messages.save(
|
||||
nodeMessage,
|
||||
{
|
||||
enforceViewPropagation: true,
|
||||
},
|
||||
{
|
||||
user: { id: null, server_request: true },
|
||||
thread: { id: threadId },
|
||||
company: { id: company.id },
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MessageMigrator;
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Type } from "class-transformer";
|
||||
import { merge } from "lodash";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
} from "../../../../core/platform/services/database/services/orm/decorators";
|
||||
|
||||
export const TYPE = "message";
|
||||
@Entity(TYPE, {
|
||||
primaryKey: [["channel_id"], "parent_message_id", "id"],
|
||||
type: TYPE,
|
||||
})
|
||||
export class PhpMessage {
|
||||
@Type(() => String)
|
||||
@Column("id", "timeuuid", { generator: "timeuuid" })
|
||||
id: string;
|
||||
|
||||
@Type(() => String)
|
||||
@Column("channel_id", "timeuuid")
|
||||
channel_id: string;
|
||||
|
||||
@Type(() => String)
|
||||
@Column("parent_message_id", "encoded_string")
|
||||
parent_message_id: string;
|
||||
|
||||
@Type(() => String)
|
||||
@Column("application_id", "encoded_string")
|
||||
application_id: string | null;
|
||||
|
||||
@Column("modification_date", "twake_datetime")
|
||||
modification_date: number | null;
|
||||
|
||||
@Column("creation_date", "twake_datetime")
|
||||
creation_date: number;
|
||||
|
||||
@Column("message_type", "number")
|
||||
message_type: number;
|
||||
|
||||
@Column("sender_id", "timeuuid")
|
||||
sender: string;
|
||||
|
||||
@Column("content", "encoded_json")
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
content: { [key: string]: any } | null;
|
||||
|
||||
@Column("hidden_data", "encoded_json")
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
hidden_data: { [key: string]: any } | null;
|
||||
|
||||
@Column("reactions", "encoded_string")
|
||||
reactions: string | null;
|
||||
|
||||
@Column("pinned", "twake_boolean")
|
||||
pinned: boolean;
|
||||
|
||||
@Column("edited", "twake_boolean")
|
||||
edited: boolean;
|
||||
|
||||
@Column("responses_count", "twake_int")
|
||||
responses_count: number | null;
|
||||
}
|
||||
|
||||
export type PhpMessagePrimaryKey = Pick<PhpMessage, "parent_message_id" | "channel_id" | "id">;
|
||||
|
||||
export function getInstance(message: PhpMessage): PhpMessage {
|
||||
return merge(new PhpMessage(), message);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import {
|
||||
DeleteResult,
|
||||
ExecutionContext,
|
||||
ListResult,
|
||||
Pagination,
|
||||
} from "../../../../core/platform/framework/api/crud-service";
|
||||
import Repository, {
|
||||
FindFilter,
|
||||
} from "../../../../core/platform/services/database/services/orm/repository/repository";
|
||||
import { PhpMessagesServiceAPI } from "./types";
|
||||
import { PhpMessage, PhpMessagePrimaryKey } from "./php-message-entity";
|
||||
import gr from "../../../../services/global-resolver";
|
||||
|
||||
export interface PhpMessageExecutionContext extends ExecutionContext {
|
||||
channel_id: string;
|
||||
parent_message_id?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export class PhpMessagesService implements PhpMessagesServiceAPI {
|
||||
version: "1";
|
||||
public repository: Repository<PhpMessage>;
|
||||
|
||||
async init(): Promise<this> {
|
||||
this.repository = await gr.database.getRepository<PhpMessage>("message", PhpMessage);
|
||||
return this;
|
||||
}
|
||||
|
||||
get(pk: { parent_message_id?: string; channel_id?: string; id: string }): Promise<PhpMessage> {
|
||||
if (pk.channel_id) {
|
||||
pk.channel_id = `${pk.channel_id}`;
|
||||
pk.channel_id.substring(0, 14) + "1" + pk.channel_id.substring(14 + 1);
|
||||
}
|
||||
return this.repository.findOne(pk, {}, undefined);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
async delete(pk: PhpMessagePrimaryKey): Promise<DeleteResult<PhpMessage>> {
|
||||
throw Error("not implemented");
|
||||
}
|
||||
|
||||
async list<ListOptions>(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
pagination: Pagination,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
options?: ListOptions,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
context?: PhpMessageExecutionContext,
|
||||
): Promise<ListResult<PhpMessage>> {
|
||||
const findFilter: FindFilter = {
|
||||
channel_id: context.channel_id,
|
||||
parent_message_id: context.parent_message_id,
|
||||
//id: context.id,
|
||||
};
|
||||
|
||||
const list = await this.repository.find(findFilter, { pagination }, undefined);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Initializable, TwakeServiceProvider } from "../../../../core/platform/framework";
|
||||
|
||||
export interface PhpMessagesServiceAPI extends TwakeServiceProvider, Initializable {}
|
||||
@@ -0,0 +1,11 @@
|
||||
export const convertUuidV4ToV1 = (str: string): string => {
|
||||
const uuid = [...str];
|
||||
uuid[14] = "1";
|
||||
return uuid.join("");
|
||||
};
|
||||
|
||||
export const convertUuidV1ToV4 = (str: string): string => {
|
||||
const uuid = [...str];
|
||||
uuid[14] = "4";
|
||||
return uuid.join("");
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { merge } from "lodash";
|
||||
import {
|
||||
Column,
|
||||
Entity,
|
||||
} from "../../../../core/platform/services/database/services/orm/decorators";
|
||||
|
||||
export const TYPE = "workspace";
|
||||
@Entity(TYPE, {
|
||||
primaryKey: [["id"]],
|
||||
type: TYPE,
|
||||
})
|
||||
export class PhpWorkspace {
|
||||
@Column("id", "timeuuid")
|
||||
id: string;
|
||||
|
||||
@Column("group_id", "timeuuid")
|
||||
group_id: string;
|
||||
|
||||
@Column("name", "encoded_string")
|
||||
name: string;
|
||||
|
||||
@Column("logo", "encoded_string")
|
||||
logo: string;
|
||||
|
||||
@Column("stats", "encoded_string")
|
||||
stats: string;
|
||||
|
||||
@Column("is_deleted", "boolean")
|
||||
isDeleted: boolean;
|
||||
|
||||
@Column("is_archived", "boolean")
|
||||
isArchived: boolean;
|
||||
|
||||
@Column("is_default", "boolean")
|
||||
isDefault: boolean;
|
||||
|
||||
@Column("date_added", "number")
|
||||
dateAdded: number;
|
||||
}
|
||||
|
||||
export type PhpMessagePrimaryKey = Pick<PhpWorkspace, "id">;
|
||||
|
||||
export function getInstance(workspace: PhpWorkspace): PhpWorkspace {
|
||||
return merge(new PhpWorkspace(), workspace);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
import yargs from "yargs";
|
||||
import twake from "../../../twake";
|
||||
import ora from "ora";
|
||||
import { TwakePlatform } from "../../../core/platform/platform";
|
||||
import { DatabaseServiceAPI } from "../../../core/platform/services/database/api";
|
||||
import { PhpWorkspace, TYPE as phpTYPE } from "./php-workspace/php-workspace-entity";
|
||||
import { Pagination } from "../../../core/platform/framework/api/crud-service";
|
||||
import Workspace, { TYPE, getInstance } from "../../../services/workspaces/entities/workspace";
|
||||
import _ from "lodash";
|
||||
import gr from "../../../services/global-resolver";
|
||||
|
||||
type Options = {
|
||||
from?: string;
|
||||
onlyCompany?: string;
|
||||
onlyWorkspace?: string;
|
||||
replaceExisting?: boolean;
|
||||
};
|
||||
|
||||
class WorkspaceMigrator {
|
||||
database: DatabaseServiceAPI;
|
||||
|
||||
constructor(readonly platform: TwakePlatform) {
|
||||
this.database = this.platform.getProvider<DatabaseServiceAPI>("database");
|
||||
}
|
||||
|
||||
public async run(options: Options = {}): Promise<void> {
|
||||
const phpRepository = await this.database.getRepository(phpTYPE, PhpWorkspace);
|
||||
const repository = await this.database.getRepository(TYPE, Workspace);
|
||||
|
||||
let waitForCompany = false;
|
||||
if (options.from) {
|
||||
waitForCompany = true;
|
||||
}
|
||||
|
||||
// Get all companies
|
||||
let page: Pagination = { limitStr: "100" };
|
||||
// For each companies find workspaces
|
||||
do {
|
||||
const workspaceListResult = await phpRepository.find({}, { pagination: page }, undefined);
|
||||
page = workspaceListResult.nextPage as Pagination;
|
||||
|
||||
for (const workspace of workspaceListResult.getEntities()) {
|
||||
if (waitForCompany && options.from == `${workspace.group_id}`) {
|
||||
waitForCompany = false;
|
||||
}
|
||||
|
||||
if (!waitForCompany) {
|
||||
if (
|
||||
(!options.onlyCompany && !options.onlyWorkspace) ||
|
||||
options.onlyCompany == `${workspace.group_id}`
|
||||
) {
|
||||
if (
|
||||
!(await repository.findOne(
|
||||
{ company_id: workspace.group_id, id: workspace.id },
|
||||
{},
|
||||
undefined,
|
||||
)) ||
|
||||
options.replaceExisting
|
||||
) {
|
||||
const newWorkspace = getInstance(
|
||||
_.pick(
|
||||
workspace,
|
||||
"id",
|
||||
"company_id",
|
||||
"name",
|
||||
"logo",
|
||||
"stats",
|
||||
"is_deleted",
|
||||
"is_archived",
|
||||
"is_default",
|
||||
"date_added",
|
||||
),
|
||||
);
|
||||
newWorkspace.company_id = workspace.group_id;
|
||||
await repository.save(newWorkspace, undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} while (page.page_token);
|
||||
}
|
||||
}
|
||||
|
||||
const services = [
|
||||
"storage",
|
||||
"counter",
|
||||
"platform-services",
|
||||
"applications",
|
||||
"user",
|
||||
"search",
|
||||
"channels",
|
||||
"database",
|
||||
"webserver",
|
||||
"message-queue",
|
||||
"workspaces",
|
||||
"console",
|
||||
"auth",
|
||||
];
|
||||
|
||||
const command: yargs.CommandModule<unknown, unknown> = {
|
||||
command: "workspace",
|
||||
describe: "command that allow you to migrate php workspaces to node",
|
||||
builder: {
|
||||
from: {
|
||||
default: null,
|
||||
type: "string",
|
||||
description: "Start migration from this workspace ID",
|
||||
},
|
||||
onlyCompany: {
|
||||
default: null,
|
||||
type: "string",
|
||||
description: "Migrate only this workspace ID",
|
||||
},
|
||||
onlyWorkspace: {
|
||||
default: null,
|
||||
type: "string",
|
||||
description: "Migrate only this workspace ID",
|
||||
},
|
||||
replaceExisting: {
|
||||
default: false,
|
||||
type: "boolean",
|
||||
description: "Replace already migrated workspaces",
|
||||
},
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
handler: async argv => {
|
||||
const spinner = ora({ text: "Migrating php worskpaces - " }).start();
|
||||
const platform = await twake.run(services);
|
||||
await gr.doInit(platform);
|
||||
const migrator = new WorkspaceMigrator(platform);
|
||||
|
||||
const from = argv.from as string | null;
|
||||
const onlyCompany = argv.onlyCompany as string | null;
|
||||
const onlyWorkspace = argv.onlyWorkspace as string | null;
|
||||
const replaceExisting = (argv.replaceExisting || false) as boolean;
|
||||
|
||||
await migrator.run({
|
||||
from,
|
||||
onlyCompany,
|
||||
onlyWorkspace,
|
||||
replaceExisting,
|
||||
});
|
||||
|
||||
return spinner.stop();
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { CommandModule } from "yargs";
|
||||
|
||||
const command: CommandModule = {
|
||||
describe: "Work with search middleware",
|
||||
command: "search <command>",
|
||||
builder: yargs =>
|
||||
yargs.commandDir("search_cmds", {
|
||||
visit: commandModule => commandModule.default,
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
handler: () => {},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,160 @@
|
||||
import yargs from "yargs";
|
||||
import twake from "../../../twake";
|
||||
import ora from "ora";
|
||||
import { TwakePlatform } from "../../../core/platform/platform";
|
||||
import { DatabaseServiceAPI } from "../../../core/platform/services/database/api";
|
||||
import { Pagination } from "../../../core/platform/framework/api/crud-service";
|
||||
|
||||
import User, { TYPE as UserTYPE } from "../../../services/user/entities/user";
|
||||
import { Channel } from "../../../services/channels/entities";
|
||||
import Application, {
|
||||
TYPE as ApplicationTYPE,
|
||||
} from "../../../services/applications/entities/application";
|
||||
import Repository from "../../../core/platform/services/database/services/orm/repository/repository";
|
||||
import { SearchServiceAPI } from "../../../core/platform/services/search/api";
|
||||
import CompanyUser, { TYPE as CompanyUserTYPE } from "../../../services/user/entities/company_user";
|
||||
import { Message, TYPE as MessageTYPE } from "../../../services/messages/entities/messages";
|
||||
import gr from "../../../services/global-resolver";
|
||||
import {
|
||||
MessageFile,
|
||||
TYPE as MessageFileTYPE,
|
||||
} from "../../../services/messages/entities/message-files";
|
||||
|
||||
type Options = {
|
||||
repository?: string;
|
||||
repairEntities?: boolean;
|
||||
};
|
||||
|
||||
class SearchIndexAll {
|
||||
database: DatabaseServiceAPI;
|
||||
search: SearchServiceAPI;
|
||||
|
||||
constructor(readonly platform: TwakePlatform) {
|
||||
this.database = this.platform.getProvider<DatabaseServiceAPI>("database");
|
||||
this.search = this.platform.getProvider<SearchServiceAPI>("search");
|
||||
}
|
||||
|
||||
public async run(options: Options = {}): Promise<void> {
|
||||
const repositories: Map<string, Repository<any>> = new Map();
|
||||
repositories.set("messages", await this.database.getRepository(MessageTYPE, Message));
|
||||
repositories.set(
|
||||
"message_files",
|
||||
await this.database.getRepository(MessageFileTYPE, MessageFile),
|
||||
);
|
||||
repositories.set("users", await this.database.getRepository(UserTYPE, User));
|
||||
repositories.set("channels", await this.database.getRepository("channels", Channel));
|
||||
repositories.set(
|
||||
"applications",
|
||||
await this.database.getRepository(ApplicationTYPE, Application),
|
||||
);
|
||||
|
||||
const repository = repositories.get(options.repository);
|
||||
if (!repository) {
|
||||
throw (
|
||||
"No such repository ready for indexation, available are: " +
|
||||
Array.from(repositories.keys()).join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
// Complete user with companies in cache
|
||||
if (options.repository === "users" && options.repairEntities) {
|
||||
console.log("Complete user with companies in cache");
|
||||
const companiesUsersRepository = await this.database.getRepository(
|
||||
CompanyUserTYPE,
|
||||
CompanyUser,
|
||||
);
|
||||
const userRepository = await this.database.getRepository(UserTYPE, User);
|
||||
let page: Pagination = { limitStr: "100" };
|
||||
// For each rows
|
||||
do {
|
||||
const list = await userRepository.find({}, { pagination: page }, undefined);
|
||||
|
||||
for (const user of list.getEntities()) {
|
||||
const companies = await companiesUsersRepository.find(
|
||||
{ user_id: user.id },
|
||||
{},
|
||||
undefined,
|
||||
);
|
||||
|
||||
user.cache ||= { companies: [] };
|
||||
user.cache.companies = companies.getEntities().map(company => company.group_id);
|
||||
await repositories.get("users").save(user, undefined);
|
||||
}
|
||||
|
||||
page = list.nextPage as Pagination;
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
} while (page.page_token);
|
||||
}
|
||||
|
||||
console.log("Start indexing...");
|
||||
let count = 0;
|
||||
// Get all items
|
||||
let page: Pagination = { limitStr: "100" };
|
||||
do {
|
||||
console.log("Indexed " + count + " items...");
|
||||
const list = await repository.find({}, { pagination: page }, undefined);
|
||||
page = list.nextPage as Pagination;
|
||||
await this.search.upsert(list.getEntities());
|
||||
count += list.getEntities().length;
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
} while (page.page_token);
|
||||
|
||||
console.log("Emptying flush (10s)...");
|
||||
await new Promise(r => setTimeout(r, 10000));
|
||||
|
||||
console.log("Done!");
|
||||
}
|
||||
}
|
||||
|
||||
const services = [
|
||||
"search",
|
||||
"database",
|
||||
"webserver",
|
||||
"auth",
|
||||
"counter",
|
||||
"cron",
|
||||
"message-queue",
|
||||
"push",
|
||||
"realtime",
|
||||
"storage",
|
||||
"tracker",
|
||||
"websocket",
|
||||
];
|
||||
|
||||
const command: yargs.CommandModule<unknown, unknown> = {
|
||||
command: "index",
|
||||
describe: "command to reindex search middleware from db entities",
|
||||
builder: {
|
||||
repository: {
|
||||
default: "",
|
||||
type: "string",
|
||||
description: "Choose a repository to reindex",
|
||||
},
|
||||
repairEntities: {
|
||||
default: false,
|
||||
type: "boolean",
|
||||
description: "Choose to repair entities too when possible",
|
||||
},
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
handler: async argv => {
|
||||
const spinner = ora({ text: "Reindex repository - " }).start();
|
||||
const platform = await twake.run(services);
|
||||
await gr.doInit(platform);
|
||||
const migrator = new SearchIndexAll(platform);
|
||||
|
||||
const repository = (argv.repository || "") as string;
|
||||
|
||||
if (!repository) {
|
||||
throw "No repository was set.";
|
||||
}
|
||||
|
||||
await migrator.run({
|
||||
repository,
|
||||
});
|
||||
|
||||
return spinner.stop();
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { CommandModule } from "yargs";
|
||||
|
||||
const command: CommandModule = {
|
||||
describe: "Manage Twake Users",
|
||||
command: "users <command>",
|
||||
builder: yargs =>
|
||||
yargs.commandDir("users_cmds", {
|
||||
visit: commandModule => commandModule.default,
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
handler: () => {},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,85 @@
|
||||
import yargs from "yargs";
|
||||
import ora from "ora";
|
||||
import twake from "../../../twake";
|
||||
import Table from "cli-table";
|
||||
import { exit } from "process";
|
||||
import gr from "../../../services/global-resolver";
|
||||
|
||||
/**
|
||||
* Merge command parameters. Check the builder definition below for more details.
|
||||
*/
|
||||
type CLIArgs = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const services = [
|
||||
"storage",
|
||||
"counter",
|
||||
"message-queue",
|
||||
"platform-services",
|
||||
"user",
|
||||
"search",
|
||||
"database",
|
||||
"webserver",
|
||||
"statistics",
|
||||
"applications",
|
||||
"auth",
|
||||
"realtime",
|
||||
"websocket",
|
||||
];
|
||||
|
||||
const command: yargs.CommandModule<unknown, CLIArgs> = {
|
||||
command: "remove",
|
||||
describe: "command that allow you to remove one user",
|
||||
builder: {
|
||||
id: {
|
||||
default: "",
|
||||
type: "string",
|
||||
description: "User ID",
|
||||
},
|
||||
},
|
||||
handler: async argv => {
|
||||
const tableBefore = new Table({
|
||||
head: ["User ID", "Username", "Deleted"],
|
||||
colWidths: [40, 40, 10],
|
||||
});
|
||||
const tableAfter = new Table({
|
||||
head: ["User ID", "Username", "Deleted"],
|
||||
colWidths: [40, 40, 10],
|
||||
});
|
||||
const spinner = ora({ text: "Retrieving user" }).start();
|
||||
|
||||
const platform = await twake.run(services);
|
||||
await gr.doInit(platform);
|
||||
|
||||
const user = await gr.services.users.get({ id: argv.id });
|
||||
|
||||
if (!user) {
|
||||
console.error("Error: You need to provide User ID");
|
||||
return spinner.stop();
|
||||
}
|
||||
|
||||
if (user) {
|
||||
// Table before
|
||||
tableBefore.push([user.id, user.username_canonical, user.deleted]);
|
||||
|
||||
await gr.services.users.anonymizeAndDelete(
|
||||
{ id: user.id },
|
||||
{
|
||||
user: { id: user.id, server_request: true },
|
||||
},
|
||||
);
|
||||
|
||||
const finalUser = await gr.services.users.get({ id: argv.id });
|
||||
|
||||
// Table after
|
||||
tableAfter.push([finalUser.id, finalUser.username_canonical, finalUser.deleted]);
|
||||
|
||||
spinner.stop();
|
||||
}
|
||||
|
||||
exit();
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,14 @@
|
||||
import { CommandModule } from "yargs";
|
||||
|
||||
const command: CommandModule = {
|
||||
describe: "Manage Twake Workspaces",
|
||||
command: "workspace <command>",
|
||||
builder: yargs =>
|
||||
yargs.commandDir("workspace_cmds", {
|
||||
visit: commandModule => commandModule.default,
|
||||
}),
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
handler: () => {},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,47 @@
|
||||
import yargs from "yargs";
|
||||
import ora from "ora";
|
||||
import Table from "cli-table";
|
||||
import twake from "../../../twake";
|
||||
import gr from "../../../services/global-resolver";
|
||||
/**
|
||||
* Merge command parameters. Check the builder definition below for more details.
|
||||
*/
|
||||
type ListParams = {
|
||||
size: string;
|
||||
};
|
||||
|
||||
const services = [
|
||||
"platform-services",
|
||||
"user",
|
||||
"search",
|
||||
"channels",
|
||||
"notifications",
|
||||
"database",
|
||||
"webserver",
|
||||
"message-queue",
|
||||
];
|
||||
|
||||
const command: yargs.CommandModule<ListParams, ListParams> = {
|
||||
command: "list",
|
||||
describe: "List Twake workspaces",
|
||||
builder: {
|
||||
size: {
|
||||
default: "50",
|
||||
type: "string",
|
||||
description: "Number of workspaces to fetch",
|
||||
},
|
||||
},
|
||||
handler: async argv => {
|
||||
const table = new Table({ head: ["ID", "Name"], colWidths: [40, 50] });
|
||||
const spinner = ora({ text: "List Twake workspaces" }).start();
|
||||
const platform = await twake.run(services);
|
||||
await gr.doInit(platform);
|
||||
const workspaces = await gr.services.workspaces.list({ limitStr: argv.size });
|
||||
|
||||
spinner.stop();
|
||||
workspaces.getEntities().forEach(ws => table.push([ws.id, ws.name]));
|
||||
console.log(table.toString());
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
@@ -0,0 +1,49 @@
|
||||
import yargs from "yargs";
|
||||
import ora from "ora";
|
||||
import Table from "cli-table";
|
||||
import twake from "../../../twake";
|
||||
import gr from "../../../services/global-resolver";
|
||||
/**
|
||||
* Merge command parameters. Check the builder definition below for more details.
|
||||
*/
|
||||
type ListParams = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
const services = [
|
||||
"platform-services",
|
||||
"user",
|
||||
"search",
|
||||
"channels",
|
||||
"notifications",
|
||||
"database",
|
||||
"webserver",
|
||||
"message-queue",
|
||||
];
|
||||
|
||||
const command: yargs.CommandModule<ListParams, ListParams> = {
|
||||
command: "user",
|
||||
describe: "List workspace ers",
|
||||
builder: {
|
||||
id: {
|
||||
default: "f339d54a-e833-11ea-92c3-0242ac120004",
|
||||
type: "string",
|
||||
description: "Workspace ID",
|
||||
},
|
||||
},
|
||||
handler: async argv => {
|
||||
const table = new Table({ head: ["user ID", "Date Added"], colWidths: [40, 40] });
|
||||
const spinner = ora({ text: "Retrieving workspace users" }).start();
|
||||
const platform = await twake.run(services);
|
||||
await gr.doInit(platform);
|
||||
const users = await gr.services.workspaces.getUsers({ workspaceId: argv.id });
|
||||
|
||||
spinner.stop();
|
||||
users
|
||||
.getEntities()
|
||||
.forEach(u => table.push([u.id, new Date(u.dateAdded).toLocaleDateString()]));
|
||||
console.log(table.toString());
|
||||
},
|
||||
};
|
||||
|
||||
export default command;
|
||||
Reference in New Issue
Block a user