Browse Source

Format code, move functions used by multiple commands in to the CommandBase, implement skipping questions the user shouldn't be allowed to update

Edvin 4 years ago
parent
commit
b050e7c3fc

+ 90 - 0
cli/src/base/ContentDirectoryCommandBase.ts

@@ -23,6 +23,7 @@ import { RolesCommandBase } from './WorkingGroupsCommandBase'
 import { createType } from '@joystream/types'
 import chalk from 'chalk'
 import { flags } from '@oclif/command'
+import { DistinctQuestion } from 'inquirer'
 
 const CONTEXTS = ['Member', 'Curator', 'Lead'] as const
 type Context = typeof CONTEXTS[number]
@@ -376,4 +377,93 @@ export default abstract class ContentDirectoryCommandBase extends RolesCommandBa
 
     return parsedEntities.filter((entity) => filters.every(([pName, pValue]) => entity[pName] === pValue))
   }
+
+  async getActor(context: typeof CONTEXTS[number], pickedClass: Class) {
+    let actor: Actor
+    if (context === 'Member') {
+      if (pickedClass.class_permissions.any_member.isFalse) {
+        this.error(`You're not allowed to createEntity of className: ${pickedClass.name.toString()}!`)
+      }
+
+      const memberId = await this.getRequiredMemberId()
+
+      actor = this.createType('Actor', { Member: memberId })
+    } else if (context === 'Curator') {
+      actor = await this.getCuratorContext([pickedClass.name.toString()])
+    } else {
+      await this.getRequiredLead()
+
+      actor = this.createType('Actor', { Lead: null })
+    }
+
+    return actor
+  }
+
+  getQuestionsFromClass = (
+    classData: Class,
+    defaults?: { [key: string]: { 'value': unknown; locked: boolean } }
+  ): DistinctQuestion[] => {
+    return classData.properties.reduce((previousValue, { name, property_type: propertyType, required }, index) => {
+      const propertySubtype = propertyType.subtype
+      const questionType = propertySubtype === 'Bool' ? 'list' : 'input'
+      const isSubtypeNumber = propertySubtype.toLowerCase().includes('int')
+      const isSubtypeReference = propertyType.isOfType('Single') && propertyType.asType('Single').isOfType('Reference')
+      const isQuestionLocked = defaults?.[Object.keys(defaults)[index]].locked
+
+      if (isQuestionLocked) {
+        return previousValue
+      }
+
+      const optionalQuestionProperties = {
+        ...{
+          filter: (answer: string) => {
+            if (required.isFalse && !answer) {
+              return null
+            }
+
+            if ((isSubtypeNumber || isSubtypeReference) && isFinite(+answer)) {
+              return +answer
+            }
+
+            return answer
+          },
+          validate: async (answer: string | null) => {
+            if (answer && isSubtypeReference && isFinite(+answer)) {
+              const { class_id: classId } = await this.getEntity(+answer)
+
+              if (classId.toString() !== propertyType.asType('Single').asType('Reference')[0].toString()) {
+                return 'This entity is not of the right class id!'
+              }
+            }
+
+            return true
+          },
+        },
+        ...(propertySubtype === 'Bool' && {
+          choices: ['true', 'false'],
+          filter: (answer: string) => {
+            return answer === 'true' || false
+          },
+        }),
+      }
+
+      const isQuestionOptional = propertySubtype === 'Bool' ? '' : required.isTrue ? '(required)' : '(optional)'
+      const classId = isSubtypeReference
+        ? ` [Class Id: ${propertyType.asType('Single').asType('Reference')[0].toString()}]`
+        : ''
+
+      return [
+        ...previousValue,
+        {
+          name: name.toString(),
+          message: `${name} - ${propertySubtype}${classId} ${isQuestionOptional}`,
+          type: questionType,
+          ...optionalQuestionProperties,
+          ...(defaults && {
+            default: defaults[Object.keys(defaults)[index]].value,
+          }),
+        },
+      ]
+    }, [] as DistinctQuestion[])
+  }
 }

+ 3 - 124
cli/src/commands/content-directory/createEntity.ts

@@ -1,28 +1,7 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import { flags } from '@oclif/command'
-import { Class as ContentDirectoryClass } from '@joystream/types/content-directory'
 import inquirer from 'inquirer'
-import { CLIError } from '@oclif/errors'
 import { InputParser } from '@joystream/cd-schemas'
 
-const CONTEXTS = ['Member', 'Curator', 'Lead'] as const
-
-type CreateEntityArgs = {
-  className: string
-}
-
-type CreateEntityFlags = {
-  context: typeof CONTEXTS[number]
-}
-
-type Question = {
-  name: string
-  message: string
-  type: 'list' | 'input'
-  choices?: string[]
-  filter?: (answer: string) => string | null | number | boolean
-}
-
 export default class CreateEntityCommand extends ContentDirectoryCommandBase {
   static description =
     'Creates a new entity in the specified class (can be executed in Member, Curator or Lead context)'
@@ -36,110 +15,12 @@ export default class CreateEntityCommand extends ContentDirectoryCommandBase {
   ]
 
   static flags = {
-    context: flags.string({
-      char: 'c',
-      description: 'Actor context to execute the command in (Member/Curator/Lead)',
-      required: true,
-      options: [...CONTEXTS],
-    }),
-  }
-
-  getQuestionsFromClass = (classData: ContentDirectoryClass): Question[] => {
-    return classData.properties.map(({ name, property_type: propertyType, required }) => {
-      const propertySubtype = propertyType.subtype
-      const questionType = propertySubtype === 'Bool' ? 'list' : 'input'
-      const isSubtypeNumber = propertySubtype.toLowerCase().includes('int')
-      const isSubtypeReference = propertyType.isOfType('Single') && propertyType.asType('Single').isOfType('Reference')
-
-      const optionalQuestionProperties = {
-        ...{
-          filter: (answer: string) => {
-            if (required.isFalse && !answer) {
-              return null
-            }
-
-            if ((isSubtypeNumber || isSubtypeReference) && isFinite(+answer)) {
-              return +answer
-            }
-
-            return answer
-          },
-        },
-        ...(propertySubtype === 'Bool' && {
-          choices: ['true', 'false'],
-          filter: (answer: string) => {
-            return answer === 'true' || false
-          },
-        }),
-      }
-
-      const isQuestionOptional = propertySubtype === 'Bool' ? '' : required.isTrue ? '(required)' : '(optional)'
-      const classId = isSubtypeReference
-        ? ` [Class Id: ${propertyType.asType('Single').asType('Reference')[0].toString()}]`
-        : ''
-
-      return {
-        name: name.toString(),
-        message: `${name} - ${propertySubtype}${classId} ${isQuestionOptional}`,
-        type: questionType,
-        ...optionalQuestionProperties,
-      }
-    })
-  }
-
-  checkReferencesValidity = async (
-    answers: {
-      [key: string]: string | number | null
-    },
-    { properties }: ContentDirectoryClass
-  ) => {
-    const propertyReferenceClassIds: (string | null)[] = []
-
-    const references = await Promise.all(
-      Object.keys(answers).map((key: string, index) => {
-        const propertyType = properties[index].property_type
-
-        if (propertyType.isOfType('Single') && propertyType.asType('Single').isOfType('Reference')) {
-          if (answers[key] !== null) {
-            propertyReferenceClassIds.push(propertyType.asType('Single').asType('Reference')[0].toString())
-            return this.getEntity(answers[key] as number | string)
-          }
-        }
-
-        propertyReferenceClassIds.push(null)
-      })
-    )
-
-    references.forEach((reference, index) => {
-      if (reference) {
-        if (reference.class_id.toString() !== propertyReferenceClassIds[index]) {
-          throw new CLIError(`The #${propertyReferenceClassIds[index]} entity is not of the right class id!`)
-        }
-      }
-    })
-  }
-
-  async getActor(context: typeof CONTEXTS[number], pickedClass: ContentDirectoryClass) {
-    if (context === 'Member') {
-      if (pickedClass.class_permissions.any_member.isFalse) {
-        this.error(`You're not allowed to createEntity of className: ${pickedClass.name.toString()}!`)
-      }
-
-      const memberId = await this.getRequiredMemberId()
-
-      return this.createType('Actor', { Member: memberId })
-    } else if (context === 'Curator') {
-      return await this.getCuratorContext([pickedClass.name.toString()])
-    } else {
-      await this.getRequiredLead()
-
-      return this.createType('Actor', { Lead: null })
-    }
+    context: ContentDirectoryCommandBase.contextFlag,
   }
 
   async run() {
-    const { className } = this.parse(CreateEntityCommand).args as CreateEntityArgs
-    const { context } = this.parse(CreateEntityCommand).flags as CreateEntityFlags
+    const { className } = this.parse(CreateEntityCommand).args
+    const { context } = this.parse(CreateEntityCommand).flags
 
     const currentAccount = await this.getRequiredSelectedAccount()
     await this.requestAccountDecoding(currentAccount)
@@ -151,8 +32,6 @@ export default class CreateEntityCommand extends ContentDirectoryCommandBase {
       [key: string]: string | number | null
     } = await inquirer.prompt(this.getQuestionsFromClass(entityClass))
 
-    await this.checkReferencesValidity(answers, entityClass)
-
     this.jsonPrettyPrint(JSON.stringify(answers))
     await this.requireConfirmation('Do you confirm the provided input?')
 

+ 33 - 124
cli/src/commands/content-directory/updateEntityPropertyValues.ts

@@ -1,28 +1,8 @@
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
-import { flags } from '@oclif/command'
-import { Class as ContentDirectoryClass } from '@joystream/types/content-directory'
+import { Actor, Class as ContentDirectoryClass } from '@joystream/types/content-directory'
 import inquirer from 'inquirer'
-import { CLIError } from '@oclif/errors'
 import { InputParser } from '@joystream/cd-schemas'
 
-const CONTEXTS = ['Member', 'Curator', 'Lead'] as const
-
-type CreateEntityArgs = {
-  id: string
-}
-
-type CreateEntityFlags = {
-  context: typeof CONTEXTS[number]
-}
-
-type Question = {
-  name: string
-  message: string
-  type: 'list' | 'input'
-  choices?: string[]
-  filter?: (answer: string) => string | null | number | boolean
-}
-
 export default class UpdateEntityPropertyValues extends ContentDirectoryCommandBase {
   static description =
     'Updates the property values of the specified entity (can be executed in Member, Curator or Lead context)'
@@ -36,116 +16,45 @@ export default class UpdateEntityPropertyValues extends ContentDirectoryCommandB
   ]
 
   static flags = {
-    context: flags.string({
-      char: 'c',
-      description: 'Actor context to execute the command in (Member/Curator/Lead)',
-      required: true,
-      options: [...CONTEXTS],
-    }),
+    context: ContentDirectoryCommandBase.contextFlag,
   }
 
-  getQuestionsFromClass = (
-    classData: ContentDirectoryClass,
-    defaults: { [key: string]: { 'value': unknown } }
-  ): Question[] => {
-    const defaultKeys = Object.keys(defaults)
-
-    return classData.properties.map(({ name, property_type: propertyType, required }, index) => {
-      const propertySubtype = propertyType.subtype
-      const questionType = propertySubtype === 'Bool' ? 'list' : 'input'
-      const isSubtypeNumber = propertySubtype.toLowerCase().includes('int')
-      const isSubtypeReference = propertyType.isOfType('Single') && propertyType.asType('Single').isOfType('Reference')
-
-      const optionalQuestionProperties = {
-        ...{
-          filter: (answer: string) => {
-            if (required.isFalse && !answer) {
-              return null
-            }
-
-            if ((isSubtypeNumber || isSubtypeReference) && isFinite(+answer)) {
-              return +answer
-            }
-
-            return answer
-          },
-        },
-        ...(propertySubtype === 'Bool' && {
-          choices: ['true', 'false'],
-          filter: (answer: string) => {
-            return answer === 'true' || false
-          },
-        }),
-      }
+  async parseDefaults(
+    defaults: { [key: string]: { 'value': unknown } },
+    actor: Actor,
+    pickedClass: ContentDirectoryClass
+  ) {
+    let parsedDefaults: { [key: string]: { 'value': unknown; locked: boolean } } = {}
 
-      const isQuestionOptional = propertySubtype === 'Bool' ? '' : required.isTrue ? '(required)' : '(optional)'
-      const classId = isSubtypeReference
-        ? ` [Class Id: ${propertyType.asType('Single').asType('Reference')[0].toString()}]`
-        : ''
-
-      return {
-        name: name.toString(),
-        message: `${name} - ${propertySubtype}${classId} ${isQuestionOptional}`,
-        type: questionType,
-        ...optionalQuestionProperties,
-        default: defaults[defaultKeys[index]].value,
-      }
-    })
-  }
+    const context = actor.type === 'Curator' ? 'Maintainer' : 'Controller'
+    let propertyLockedFromUser: boolean[] = []
 
-  checkReferencesValidity = async (
-    answers: {
-      [key: string]: string | number | null
-    },
-    { properties }: ContentDirectoryClass
-  ) => {
-    const propertyReferenceClassIds: (string | null)[] = []
-
-    const references = await Promise.all(
-      Object.keys(answers).map((key: string, index) => {
-        const propertyType = properties[index].property_type
-
-        if (propertyType.isOfType('Single') && propertyType.asType('Single').isOfType('Reference')) {
-          if (answers[key] !== null) {
-            propertyReferenceClassIds.push(propertyType.asType('Single').asType('Reference')[0].toString())
-            return this.getEntity(answers[key] as number | string)
-          }
-        }
-
-        propertyReferenceClassIds.push(null)
-      })
-    )
-
-    references.forEach((reference, index) => {
-      if (reference) {
-        if (reference.class_id.toString() !== propertyReferenceClassIds[index]) {
-          throw new CLIError(`The #${propertyReferenceClassIds[index]} entity is not of the right class id!`)
-        }
-      }
-    })
-  }
+    if (context === 'Maintainer') {
+      propertyLockedFromUser = pickedClass.properties.map(
+        (property) => property.locking_policy.is_locked_from_maintainer.isTrue
+      )
+    } else {
+      propertyLockedFromUser = pickedClass.properties.map(
+        (property) => property.locking_policy.is_locked_from_controller.isTrue
+      )
+    }
 
-  async getActor(context: typeof CONTEXTS[number], pickedClass: ContentDirectoryClass) {
-    if (context === 'Member') {
-      if (pickedClass.class_permissions.any_member.isFalse) {
-        this.error(`You're not allowed to createEntity of className: ${pickedClass.name.toString()}!`)
+    Object.keys(defaults).forEach((key, index) => {
+      parsedDefaults = {
+        ...parsedDefaults,
+        [key]: {
+          ...defaults[key],
+          locked: propertyLockedFromUser[index],
+        },
       }
+    })
 
-      const memberId = await this.getRequiredMemberId()
-
-      return this.createType('Actor', { Member: memberId })
-    } else if (context === 'Curator') {
-      return await this.getCuratorContext([pickedClass.name.toString()])
-    } else {
-      await this.getRequiredLead()
-
-      return this.createType('Actor', { Lead: null })
-    }
+    return parsedDefaults
   }
 
   async run() {
-    const { id } = this.parse(UpdateEntityPropertyValues).args as CreateEntityArgs
-    const { context } = this.parse(UpdateEntityPropertyValues).flags as CreateEntityFlags
+    const { id } = this.parse(UpdateEntityPropertyValues).args
+    const { context } = this.parse(UpdateEntityPropertyValues).flags
 
     const currentAccount = await this.getRequiredSelectedAccount()
     await this.requestAccountDecoding(currentAccount)
@@ -156,11 +65,11 @@ export default class UpdateEntityPropertyValues extends ContentDirectoryCommandB
 
     const actor = await this.getActor(context, entityClass)
 
+    const parsedDefaults = await this.parseDefaults(defaults, actor, entityClass)
+
     const answers: {
       [key: string]: string | number | null
-    } = await inquirer.prompt(this.getQuestionsFromClass(entityClass, defaults))
-
-    await this.checkReferencesValidity(answers, entityClass)
+    } = await inquirer.prompt(this.getQuestionsFromClass(entityClass, parsedDefaults))
 
     this.jsonPrettyPrint(JSON.stringify(answers))
     await this.requireConfirmation('Do you confirm the provided input?')