Pārlūkot izejas kodu

Basic input validation

Leszek Wiesner 3 gadi atpakaļ
vecāks
revīzija
23752b6b52

+ 32 - 0
cli/src/Types.ts

@@ -17,6 +17,8 @@ import {
 } from '@joystream/content-metadata-protobuf'
 import { ContentId, ContentParameters } from '@joystream/types/storage'
 
+import { JSONSchema7, JSONSchema7Definition } from 'json-schema'
+
 // KeyringPair type extended with mandatory "meta.name"
 // It's used for accounts/keys management within CLI.
 // If not provided in the account json file, the meta.name value is set to "Unnamed Account"
@@ -246,3 +248,33 @@ export type ChannelInputParameters = Omit<ChannelMetadata.AsObject, 'coverPhoto'
 export type ChannelCategoryInputParameters = ChannelCategoryMetadata.AsObject
 
 export type VideoCategoryInputParameters = VideoCategoryMetadata.AsObject
+
+// JSONSchema utility types
+export type JSONTypeName<T> = T extends string
+  ? 'string'
+  : T extends number
+  ? 'number'
+  : T extends any[]
+  ? 'array'
+  : T extends Record<string, unknown>
+  ? 'object'
+  : T extends boolean
+  ? 'boolean'
+  : never
+
+export type PropertySchema<P> = Omit<
+  JSONSchema7Definition & {
+    type: JSONTypeName<P>
+    properties: P extends Record<string, unknown> ? JsonSchemaProperties<P> : never
+  },
+  P extends Record<string, unknown> ? '' : 'properties'
+>
+
+export type JsonSchemaProperties<T extends Record<string, unknown>> = {
+  [K in keyof Required<T>]: PropertySchema<Required<T>[K]>
+}
+
+export type JsonSchema<T extends Record<string, unknown>> = JSONSchema7 & {
+  type: 'object'
+  properties: JsonSchemaProperties<T>
+}

+ 2 - 1
cli/src/commands/content/createChannel.ts

@@ -4,6 +4,7 @@ import { metadataToBytes, channelMetadataFromInput } from '../../helpers/seriali
 import { flags } from '@oclif/command'
 import { CreateInterface } from '@joystream/types'
 import { ChannelCreationParameters } from '@joystream/types/content'
+import { ChannelInputSchema } from '../../json-schemas/ContentDirectory'
 import ContentDirectoryCommandBase from '../../base/ContentDirectoryCommandBase'
 import UploadCommandBase from '../../base/UploadCommandBase'
 
@@ -29,7 +30,7 @@ export default class CreateChannelCommand extends UploadCommandBase {
     const actor = await this.getActor(context)
     await this.requestAccountDecoding(account)
 
-    const channelInput = await getInputJson<ChannelInputParameters>(input)
+    const channelInput = await getInputJson<ChannelInputParameters>(input, ChannelInputSchema)
 
     const meta = channelMetadataFromInput(channelInput)
     const { coverPhotoPath, avatarPhotoPath } = channelInput

+ 2 - 1
cli/src/commands/content/createChannelCategory.ts

@@ -5,6 +5,7 @@ import { channelCategoryMetadataFromInput, metadataToBytes } from '../../helpers
 import { flags } from '@oclif/command'
 import { CreateInterface } from '@joystream/types'
 import { ChannelCategoryCreationParameters } from '@joystream/types/content'
+import { ChannelCategoryInputSchema } from '../../json-schemas/ContentDirectory'
 
 export default class CreateChannelCategoryCommand extends ContentDirectoryCommandBase {
   static description = 'Create channel category inside content directory.'
@@ -25,7 +26,7 @@ export default class CreateChannelCategoryCommand extends ContentDirectoryComman
 
     const actor = context ? await this.getActor(context) : await this.getCategoryManagementActor()
 
-    const channelCategoryInput = await getInputJson<ChannelCategoryInputParameters>(input)
+    const channelCategoryInput = await getInputJson<ChannelCategoryInputParameters>(input, ChannelCategoryInputSchema)
 
     const meta = channelCategoryMetadataFromInput(channelCategoryInput)
 

+ 2 - 1
cli/src/commands/content/createVideo.ts

@@ -6,6 +6,7 @@ import { CreateInterface } from '@joystream/types'
 import { flags } from '@oclif/command'
 import { VideoCreationParameters } from '@joystream/types/content'
 import { MediaType, VideoMetadata } from '@joystream/content-metadata-protobuf'
+import { VideoInputSchema } from '../../json-schemas/ContentDirectory'
 
 export default class CreateVideoCommand extends UploadCommandBase {
   static description = 'Create video under specific channel inside content directory.'
@@ -45,7 +46,7 @@ export default class CreateVideoCommand extends UploadCommandBase {
     await this.requestAccountDecoding(account)
 
     // Get input from file
-    const videoCreationParametersInput = await getInputJson<VideoInputParameters>(input)
+    const videoCreationParametersInput = await getInputJson<VideoInputParameters>(input, VideoInputSchema)
 
     const meta = videoMetadataFromInput(videoCreationParametersInput)
 

+ 2 - 1
cli/src/commands/content/createVideoCategory.ts

@@ -5,6 +5,7 @@ import { metadataToBytes, videoCategoryMetadataFromInput } from '../../helpers/s
 import { flags } from '@oclif/command'
 import { CreateInterface } from '@joystream/types'
 import { VideoCategoryCreationParameters } from '@joystream/types/content'
+import { VideoCategoryInputSchema } from '../../json-schemas/ContentDirectory'
 
 export default class CreateVideoCategoryCommand extends ContentDirectoryCommandBase {
   static description = 'Create video category inside content directory.'
@@ -25,7 +26,7 @@ export default class CreateVideoCategoryCommand extends ContentDirectoryCommandB
 
     const actor = context ? await this.getActor(context) : await this.getCategoryManagementActor()
 
-    const videoCategoryInput = await getInputJson<VideoCategoryInputParameters>(input)
+    const videoCategoryInput = await getInputJson<VideoCategoryInputParameters>(input, VideoCategoryInputSchema)
 
     const meta = videoCategoryMetadataFromInput(videoCategoryInput)
 

+ 2 - 1
cli/src/commands/content/updateChannel.ts

@@ -5,6 +5,7 @@ import { flags } from '@oclif/command'
 import UploadCommandBase from '../../base/UploadCommandBase'
 import { CreateInterface } from '@joystream/types'
 import { ChannelUpdateParameters } from '@joystream/types/content'
+import { ChannelInputSchema } from '../../json-schemas/ContentDirectory'
 
 export default class UpdateChannelCommand extends UploadCommandBase {
   static description = 'Update existing content directory channel.'
@@ -36,7 +37,7 @@ export default class UpdateChannelCommand extends UploadCommandBase {
     const actor = await this.getChannelOwnerActor(channel)
     await this.requestAccountDecoding(currentAccount)
 
-    const channelInput = await getInputJson<ChannelInputParameters>(input)
+    const channelInput = await getInputJson<ChannelInputParameters>(input, ChannelInputSchema)
 
     const meta = channelMetadataFromInput(channelInput)
 

+ 2 - 2
cli/src/commands/content/updateChannelCategory.ts

@@ -5,7 +5,7 @@ import { channelCategoryMetadataFromInput, metadataToBytes } from '../../helpers
 import { CreateInterface } from '@joystream/types'
 import { ChannelCategoryUpdateParameters } from '@joystream/types/content'
 import { flags } from '@oclif/command'
-
+import { ChannelCategoryInputSchema } from '../../json-schemas/ContentDirectory'
 export default class UpdateChannelCategoryCommand extends ContentDirectoryCommandBase {
   static description = 'Update channel category inside content directory.'
   static flags = {
@@ -35,7 +35,7 @@ export default class UpdateChannelCategoryCommand extends ContentDirectoryComman
 
     const actor = context ? await this.getActor(context) : await this.getCategoryManagementActor()
 
-    const channelCategoryInput = await getInputJson<ChannelCategoryInputParameters>(input)
+    const channelCategoryInput = await getInputJson<ChannelCategoryInputParameters>(input, ChannelCategoryInputSchema)
 
     const meta = channelCategoryMetadataFromInput(channelCategoryInput)
 

+ 2 - 1
cli/src/commands/content/updateVideo.ts

@@ -5,6 +5,7 @@ import UploadCommandBase from '../../base/UploadCommandBase'
 import { flags } from '@oclif/command'
 import { CreateInterface } from '@joystream/types'
 import { VideoUpdateParameters } from '@joystream/types/content'
+import { VideoInputSchema } from '../../json-schemas/ContentDirectory'
 
 export default class UpdateVideoCommand extends UploadCommandBase {
   static description = 'Update video under specific id.'
@@ -37,7 +38,7 @@ export default class UpdateVideoCommand extends UploadCommandBase {
     const actor = await this.getChannelOwnerActor(channel)
     await this.requestAccountDecoding(currentAccount)
 
-    const videoInput = await getInputJson<VideoInputParameters>(input)
+    const videoInput = await getInputJson<VideoInputParameters>(input, VideoInputSchema)
 
     const meta = videoMetadataFromInput(videoInput)
     const { videoPath, thumbnailPhotoPath } = videoInput

+ 2 - 1
cli/src/commands/content/updateVideoCategory.ts

@@ -5,6 +5,7 @@ import { metadataToBytes, videoCategoryMetadataFromInput } from '../../helpers/s
 import { flags } from '@oclif/command'
 import { CreateInterface } from '@joystream/types'
 import { VideoCategoryUpdateParameters } from '@joystream/types/content'
+import { VideoCategoryInputSchema } from '../../json-schemas/ContentDirectory'
 
 export default class UpdateVideoCategoryCommand extends ContentDirectoryCommandBase {
   static description = 'Update video category inside content directory.'
@@ -35,7 +36,7 @@ export default class UpdateVideoCategoryCommand extends ContentDirectoryCommandB
 
     const actor = context ? await this.getActor(context) : await this.getCategoryManagementActor()
 
-    const videoCategoryInput = await getInputJson<VideoCategoryInputParameters>(input)
+    const videoCategoryInput = await getInputJson<VideoCategoryInputParameters>(input, VideoCategoryInputSchema)
 
     const meta = videoCategoryMetadataFromInput(videoCategoryInput)
 

+ 7 - 4
cli/src/helpers/InputOutput.ts

@@ -20,7 +20,7 @@ export const IOFlags = {
   }),
 }
 
-export async function getInputJson<T>(inputPath: string, schema?: Record<string, unknown>): Promise<T> {
+export async function getInputJson<T>(inputPath: string, schema?: unknown): Promise<T> {
   let content, jsonObj
   try {
     content = fs.readFileSync(inputPath).toString()
@@ -39,11 +39,14 @@ export async function getInputJson<T>(inputPath: string, schema?: Record<string,
   return jsonObj as T
 }
 
-export async function validateInput(input: unknown, schema: Record<string, unknown>): Promise<void> {
+export async function validateInput(input: unknown, schema: unknown): Promise<void> {
   const ajv = new Ajv({ allErrors: true })
-  const valid = ajv.validate(schema, input) as boolean
+  const valid = ajv.validate(schema as any, input) as boolean
   if (!valid) {
-    throw new CLIError(`Input JSON file is not valid: ${ajv.errorsText()}`)
+    throw new CLIError(
+      `Input JSON file is not valid:\n` +
+        ajv.errors?.map((e) => `${e.dataPath}: ${e.message} (${JSON.stringify(e.params)})`).join('\n')
+    )
   }
 }
 

+ 93 - 0
cli/src/json-schemas/ContentDirectory.ts

@@ -0,0 +1,93 @@
+import {
+  ChannelInputParameters,
+  VideoInputParameters,
+  VideoCategoryInputParameters,
+  ChannelCategoryInputParameters,
+  JsonSchema,
+} from '../Types'
+
+export const VideoCategoryInputSchema: JsonSchema<VideoCategoryInputParameters> = {
+  type: 'object',
+  additionalProperties: false,
+  properties: {
+    name: {
+      type: 'string',
+    },
+  },
+}
+
+export const ChannelCategoryInputSchema: JsonSchema<ChannelCategoryInputParameters> = VideoCategoryInputSchema
+
+export const ChannelInputSchema: JsonSchema<ChannelInputParameters> = {
+  type: 'object',
+  additionalProperties: false,
+  properties: {
+    category: { type: 'number' },
+    description: { type: 'string' },
+    isPublic: { type: 'boolean' },
+    language: { type: 'string' },
+    title: { type: 'string' },
+    coverPhotoPath: { type: 'string' },
+    avatarPhotoPath: { type: 'string' },
+    rewardAccount: { type: 'string' },
+  },
+}
+
+export const VideoInputSchema: JsonSchema<VideoInputParameters> = {
+  type: 'object',
+  additionalProperties: false,
+  properties: {
+    category: { type: 'number' },
+    description: { type: 'string' },
+    duration: { type: 'number' },
+    hasMarketing: { type: 'boolean' },
+    isExplicit: { type: 'boolean' },
+    isPublic: { type: 'boolean' },
+    language: { type: 'string' },
+    license: {
+      type: 'object',
+      properties: {
+        code: {
+          type: 'number',
+        },
+        attribution: {
+          type: 'string',
+        },
+        customText: {
+          type: 'string',
+        },
+      },
+    },
+    mediaPixelHeight: { type: 'number' },
+    mediaPixelWidth: { type: 'number' },
+    mediaType: {
+      type: 'object',
+      properties: {
+        codecName: {
+          type: 'string',
+        },
+        container: {
+          type: 'string',
+        },
+        mimeMediaType: {
+          type: 'string',
+        },
+      },
+    },
+    personsList: { type: 'array' },
+    publishedBeforeJoystream: {
+      type: 'object',
+      properties: {
+        date: {
+          type: 'string',
+        },
+        isPublished: {
+          type: 'boolean',
+        },
+      },
+    },
+    thumbnailPhotoPath: { type: 'string' },
+    title: { type: 'string' },
+    videoPath: { type: 'string' },
+  },
+}