123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- import ExitCodes from '../ExitCodes';
- import AccountsCommandBase from './AccountsCommandBase';
- import { flags } from '@oclif/command';
- import { WorkingGroups, AvailableGroups, NamedKeyringPair, GroupMember, GroupOpening, ApiMethodArg, ApiMethodNamedArgs, OpeningStatus, GroupApplication } from '../Types';
- import { apiModuleByGroup } from '../Api';
- import { CLIError } from '@oclif/errors';
- import fs from 'fs';
- import path from 'path';
- import _ from 'lodash';
- import { ApplicationStageKeys } from '@joystream/types/hiring';
- const DEFAULT_GROUP = WorkingGroups.StorageProviders;
- const DRAFTS_FOLDER = 'opening-drafts';
- /**
- * Abstract base class for commands related to working groups
- */
- export default abstract class WorkingGroupsCommandBase extends AccountsCommandBase {
- group: WorkingGroups = DEFAULT_GROUP;
- static flags = {
- group: flags.string({
- char: 'g',
- description:
- "The working group context in which the command should be executed\n" +
- `Available values are: ${AvailableGroups.join(', ')}.`,
- required: true,
- default: DEFAULT_GROUP
- }),
- };
- // Use when lead access is required in given command
- async getRequiredLead(): Promise<GroupMember> {
- let selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount();
- let lead = await this.getApi().groupLead(this.group);
- if (!lead || lead.roleAccount.toString() !== selectedAccount.address) {
- this.error('Lead access required for this command!', { exit: ExitCodes.AccessDenied });
- }
- return lead;
- }
- // Use when worker access is required in given command
- async getRequiredWorker(): Promise<GroupMember> {
- let selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount();
- let groupMembers = await this.getApi().groupMembers(this.group);
- let groupMembersByAccount = groupMembers.filter(m => m.roleAccount.toString() === selectedAccount.address);
- if (!groupMembersByAccount.length) {
- this.error('Worker access required for this command!', { exit: ExitCodes.AccessDenied });
- }
- else if (groupMembersByAccount.length === 1) {
- return groupMembersByAccount[0];
- }
- else {
- return await this.promptForWorker(groupMembersByAccount);
- }
- }
- // Use when member controller access is required, but one of the associated roles is expected to be selected
- async getRequiredWorkerByMemberController(): Promise<GroupMember> {
- const selectedAccount: NamedKeyringPair = await this.getRequiredSelectedAccount();
- const memberIds = await this.getApi().getMemberIdsByControllerAccount(selectedAccount.address);
- const controlledWorkers = (await this.getApi().groupMembers(this.group))
- .filter(groupMember => memberIds.some(memberId => groupMember.memberId.eq(memberId)));
- if (!controlledWorkers.length) {
- this.error(
- `Member controller account with some associated ${this.group} group roles needs to be selected!`,
- { exit: ExitCodes.AccessDenied }
- );
- }
- else if (controlledWorkers.length === 1) {
- return controlledWorkers[0];
- }
- else {
- return await this.promptForWorker(controlledWorkers);
- }
- }
- async promptForWorker(groupMembers: GroupMember[]): Promise<GroupMember> {
- const chosenWorkerIndex = await this.simplePrompt({
- message: 'Choose the intended worker context:',
- type: 'list',
- choices: groupMembers.map((groupMember, index) => ({
- name: `Worker ID ${ groupMember.workerId.toString() }`,
- value: index
- }))
- });
- return groupMembers[chosenWorkerIndex];
- }
- async promptForApplicationsToAccept(opening: GroupOpening): Promise<number[]> {
- const acceptableApplications = opening.applications.filter(a => a.stage === ApplicationStageKeys.Active);
- const acceptedApplications = await this.simplePrompt({
- message: 'Select succesful applicants',
- type: 'checkbox',
- choices: acceptableApplications.map(a => ({
- name: ` ${a.wgApplicationId}: ${a.member?.handle.toString()}`,
- value: a.wgApplicationId,
- }))
- });
- return acceptedApplications;
- }
- async promptForNewOpeningDraftName() {
- let
- draftName: string = '',
- fileExists: boolean = false,
- overrideConfirmed: boolean = false;
- do {
- draftName = await this.simplePrompt({
- type: 'input',
- message: 'Provide the draft name',
- validate: val => (typeof val === 'string' && val.length >= 1) || 'Draft name is required!'
- });
- fileExists = fs.existsSync(this.getOpeningDraftPath(draftName));
- if (fileExists) {
- overrideConfirmed = await this.simplePrompt({
- type: 'confirm',
- message: 'Such draft already exists. Do you wish to override it?',
- default: false
- });
- }
- } while(fileExists && !overrideConfirmed);
- return draftName;
- }
- async promptForOpeningDraft() {
- let draftFiles: string[] = [];
- try {
- draftFiles = fs.readdirSync(this.getOpeingDraftsPath());
- }
- catch(e) {
- throw this.createDataReadError(DRAFTS_FOLDER);
- }
- if (!draftFiles.length) {
- throw new CLIError('No drafts available!', { exit: ExitCodes.FileNotFound });
- }
- const draftNames = draftFiles.map(fileName => _.startCase(fileName.replace('.json', '')));
- const selectedDraftName = await this.simplePrompt({
- message: 'Select a draft',
- type: 'list',
- choices: draftNames
- });
- return selectedDraftName;
- }
- async getOpeningForLeadAction(id: number, requiredStatus?: OpeningStatus): Promise<GroupOpening> {
- const opening = await this.getApi().groupOpening(this.group, id);
- if (!opening.type.isOfType('Worker')) {
- this.error('A lead can only manage Worker openings!', { exit: ExitCodes.AccessDenied });
- }
- if (requiredStatus && opening.stage.status !== requiredStatus) {
- this.error(
- `The opening needs to be in "${_.startCase(requiredStatus)}" stage! ` +
- `This one is: "${_.startCase(opening.stage.status)}"`,
- { exit: ExitCodes.InvalidInput }
- );
- }
- return opening;
- }
- // An alias for better code readibility in case we don't need the actual return value
- validateOpeningForLeadAction = this.getOpeningForLeadAction
- async getApplicationForLeadAction(id: number, requiredStatus?: ApplicationStageKeys): Promise<GroupApplication> {
- const application = await this.getApi().groupApplication(this.group, id);
- const opening = await this.getApi().groupOpening(this.group, application.wgOpeningId);
- if (!opening.type.isOfType('Worker')) {
- this.error('A lead can only manage Worker opening applications!', { exit: ExitCodes.AccessDenied });
- }
- if (requiredStatus && application.stage !== requiredStatus) {
- this.error(
- `The application needs to have "${_.startCase(requiredStatus)}" status! ` +
- `This one has: "${_.startCase(application.stage)}"`,
- { exit: ExitCodes.InvalidInput }
- );
- }
- return application;
- }
- async getWorkerForLeadAction(id: number, requireStakeProfile: boolean = false) {
- const groupMember = await this.getApi().groupMember(this.group, id);
- const groupLead = await this.getApi().groupLead(this.group);
- if (groupLead?.workerId.eq(groupMember.workerId)) {
- this.error('A lead cannot manage his own role this way!', { exit: ExitCodes.AccessDenied });
- }
- if (requireStakeProfile && !groupMember.stake) {
- this.error('This worker has no associated role stake profile!', { exit: ExitCodes.InvalidInput });
- }
- return groupMember;
- }
- // Helper for better TS handling.
- // We could also use some magic with conditional types instead, but those don't seem be very well supported yet.
- async getWorkerWithStakeForLeadAction(id: number) {
- return (await this.getWorkerForLeadAction(id, true)) as (GroupMember & Required<Pick<GroupMember, 'stake'>>);
- }
- loadOpeningDraftParams(draftName: string): ApiMethodNamedArgs {
- const draftFilePath = this.getOpeningDraftPath(draftName);
- const params = this.extrinsicArgsFromDraft(
- apiModuleByGroup[this.group],
- 'addOpening',
- draftFilePath
- );
- return params;
- }
- getOpeingDraftsPath() {
- return path.join(this.getAppDataPath(), DRAFTS_FOLDER);
- }
- getOpeningDraftPath(draftName: string) {
- return path.join(this.getOpeingDraftsPath(), _.snakeCase(draftName)+'.json');
- }
- saveOpeningDraft(draftName: string, params: ApiMethodArg[]) {
- const paramsJson = JSON.stringify(
- params.map(p => p.toJSON()),
- null,
- 2
- );
- try {
- fs.writeFileSync(this.getOpeningDraftPath(draftName), paramsJson);
- } catch(e) {
- throw this.createDataWriteError(DRAFTS_FOLDER);
- }
- }
- private initOpeningDraftsDir(): void {
- if (!fs.existsSync(this.getOpeingDraftsPath())) {
- fs.mkdirSync(this.getOpeingDraftsPath());
- }
- }
- async init() {
- await super.init();
- try {
- this.initOpeningDraftsDir();
- } catch (e) {
- throw this.createDataDirInitError();
- }
- const { flags } = this.parse(this.constructor as typeof WorkingGroupsCommandBase);
- if (!AvailableGroups.includes(flags.group as any)) {
- throw new CLIError(`Invalid group! Available values are: ${AvailableGroups.join(', ')}`, { exit: ExitCodes.InvalidInput });
- }
- this.group = flags.group as WorkingGroups;
- }
- }
|