From 5def9deb5591721b29393523f9f3aa6b98ac9468 Mon Sep 17 00:00:00 2001 From: Aramik Date: Wed, 18 Sep 2024 10:03:43 -0700 Subject: [PATCH] content-watcher-api: added validation (#516) # Problem Adds extra verification on dtos Close #499 ## Steps to Verify: 1. run the api 2. open swagger ui 3. call endpoints with invalid types or amounts or requests --- apps/content-watcher/src/metadata.ts | 2 +- docs/content-watcher/index.html | 4577 ++--------------- .../dtos/content-watcher/chain.watch.dto.ts | 12 +- .../src/dtos/content-watcher/common.dto.ts | 13 +- .../content-search-request.dto.ts | 16 +- .../subscription.webhook.dto.ts | 13 +- openapi-specs/content-watcher.openapi.json | 45 +- 7 files changed, 472 insertions(+), 4206 deletions(-) diff --git a/apps/content-watcher/src/metadata.ts b/apps/content-watcher/src/metadata.ts index 47163405..f447d3c0 100644 --- a/apps/content-watcher/src/metadata.ts +++ b/apps/content-watcher/src/metadata.ts @@ -5,5 +5,5 @@ export default async () => { ["../../../libs/types/src/dtos/content-watcher/announcement.dto"]: await import("../../../libs/types/src/dtos/content-watcher/announcement.dto"), ["../../../libs/types/src/dtos/content-watcher/chain.watch.dto"]: await import("../../../libs/types/src/dtos/content-watcher/chain.watch.dto") }; - return { "@nestjs/swagger": { "models": [[import("../../../libs/types/src/dtos/content-watcher/chain.watch.dto"), { "ChainWatchOptionsDto": { schemaIds: { required: true, type: () => [Number] }, dsnpIds: { required: true, type: () => [String] } } }], [import("../../../libs/types/src/dtos/content-watcher/subscription.webhook.dto"), { "WebhookRegistrationDto": { url: { required: true, type: () => String }, announcementTypes: { required: true, type: () => [String] } } }], [import("../../../libs/types/src/dtos/content-watcher/activity.dto"), { "LocationDto": { name: { required: true, type: () => String, minLength: 1 }, accuracy: { required: false, type: () => Number, minimum: 0, maximum: 100 }, altitude: { required: false, type: () => Number }, latitude: { required: false, type: () => Number }, longitude: { required: false, type: () => Number }, radius: { required: false, type: () => Number, minimum: 0 }, units: { required: false, enum: t["../../../libs/types/src/dtos/content-watcher/activity.dto"].UnitTypeDto } }, "AssetReferenceDto": { referenceId: { required: true, type: () => String, minLength: 1 }, height: { required: false, type: () => Number, minimum: 1 }, width: { required: false, type: () => Number, minimum: 1 }, duration: { required: false, type: () => String, pattern: "DURATION_REGEX" } }, "TagDto": { type: { required: true, enum: t["../../../libs/types/src/dtos/content-watcher/activity.dto"].TagTypeDto }, name: { required: false, type: () => String, minLength: 1 }, mentionedId: { required: false, type: () => String, minLength: 1, pattern: "DSNP_USER_URI_REGEX" } }, "AssetDto": { type: { required: true, enum: t["../../../libs/types/src/dtos/content-watcher/activity.dto"].AttachmentTypeDto }, references: { required: false, type: () => [t["../../../libs/types/src/dtos/content-watcher/activity.dto"].AssetReferenceDto] }, name: { required: false, type: () => String, minLength: 1 }, href: { required: false, type: () => String, minLength: 1 } }, "BaseActivityDto": { name: { required: false, type: () => String }, tag: { required: false, type: () => [t["../../../libs/types/src/dtos/content-watcher/activity.dto"].TagDto] }, location: { required: false, type: () => t["../../../libs/types/src/dtos/content-watcher/activity.dto"].LocationDto } }, "NoteActivityDto": { content: { required: true, type: () => String, minLength: 1 }, published: { required: true, type: () => String, pattern: "ISO8601_REGEX" }, assets: { required: false, type: () => [t["../../../libs/types/src/dtos/content-watcher/activity.dto"].AssetDto] } }, "ProfileActivityDto": { icon: { required: false, type: () => [t["../../../libs/types/src/dtos/content-watcher/activity.dto"].AssetReferenceDto] }, summary: { required: false, type: () => String }, published: { required: false, type: () => String, pattern: "ISO8601_REGEX" } } }], [import("../../../libs/types/src/dtos/content-watcher/announcement.dto"), { "BroadcastDto": { content: { required: true, type: () => t["../../../libs/types/src/dtos/content-watcher/activity.dto"].NoteActivityDto } }, "ReplyDto": { inReplyTo: { required: true, type: () => String, pattern: "DSNP_CONTENT_URI_REGEX" }, content: { required: true, type: () => t["../../../libs/types/src/dtos/content-watcher/activity.dto"].NoteActivityDto } }, "TombstoneDto": { targetContentHash: { required: true, type: () => String, pattern: "DSNP_CONTENT_HASH_REGEX" }, targetAnnouncementType: { required: true, enum: t["../../../libs/types/src/dtos/content-watcher/announcement.dto"].ModifiableAnnouncementTypeDto } }, "UpdateDto": { targetContentHash: { required: true, type: () => String, pattern: "DSNP_CONTENT_HASH_REGEX" }, targetAnnouncementType: { required: true, enum: t["../../../libs/types/src/dtos/content-watcher/announcement.dto"].ModifiableAnnouncementTypeDto }, content: { required: true, type: () => t["../../../libs/types/src/dtos/content-watcher/activity.dto"].NoteActivityDto } }, "ReactionDto": { emoji: { required: true, type: () => String, minLength: 1, pattern: "DSNP_EMOJI_REGEX" }, apply: { required: true, type: () => Number, minimum: 0, maximum: 255 }, inReplyTo: { required: true, type: () => String, pattern: "DSNP_CONTENT_URI_REGEX" } }, "ProfileDto": { profile: { required: true, type: () => t["../../../libs/types/src/dtos/content-watcher/activity.dto"].ProfileActivityDto } } }], [import("../../../libs/types/src/dtos/content-watcher/common.dto"), { "DsnpUserIdParam": { userDsnpId: { required: true, type: () => String } }, "AnnouncementResponseDto": { referenceId: { required: true, type: () => String } }, "UploadResponseDto": { assetIds: { required: true, type: () => [String] } }, "FilesUploadDto": { files: { required: true, type: () => [Object] } }, "ResetScannerDto": { blockNumber: { required: false, type: () => Number, minimum: 1 }, rewindOffset: { required: false, type: () => Number }, immediate: { required: false, type: () => Boolean } } }], [import("../../../libs/types/src/dtos/content-watcher/content-search-request.dto"), { "ContentSearchRequestDto": { clientReferenceId: { required: true, type: () => String }, startBlock: { required: true, type: () => Number, minimum: 1 }, blockCount: { required: true, type: () => Number, minimum: 1 }, filters: { required: true, type: () => t["../../../libs/types/src/dtos/content-watcher/chain.watch.dto"].ChainWatchOptionsDto }, webhookUrl: { required: true, type: () => String } } }]], "controllers": [[import("./controllers/health.controller"), { "HealthController": { "healthz": {}, "livez": {}, "readyz": {} } }], [import("./controllers/v1/scanner.controller"), { "ScanControllerV1": { "resetScanner": {}, "getWatchOptions": { type: t["../../../libs/types/src/dtos/content-watcher/chain.watch.dto"].ChainWatchOptionsDto }, "setWatchOptions": {}, "pauseScanner": {}, "startScanner": {} } }], [import("./controllers/v1/search.controller"), { "SearchControllerV1": { "search": {} } }], [import("./controllers/v1/webhook.controller"), { "WebhookControllerV1": { "registerWebhook": {}, "clearAllWebHooks": {}, "getRegisteredWebhooks": {} } }]] } }; + return { "@nestjs/swagger": { "models": [[import("../../../libs/types/src/dtos/content-watcher/chain.watch.dto"), { "ChainWatchOptionsDto": { schemaIds: { required: true, type: () => [Number], minimum: 0, maximum: 65536 }, dsnpIds: { required: true, type: () => [String] } } }], [import("../../../libs/types/src/dtos/content-watcher/subscription.webhook.dto"), { "WebhookRegistrationDto": { url: { required: true, type: () => String }, announcementTypes: { required: true, type: () => [String] } } }], [import("../../../libs/types/src/dtos/content-watcher/activity.dto"), { "LocationDto": { name: { required: true, type: () => String, minLength: 1 }, accuracy: { required: false, type: () => Number, minimum: 0, maximum: 100 }, altitude: { required: false, type: () => Number }, latitude: { required: false, type: () => Number }, longitude: { required: false, type: () => Number }, radius: { required: false, type: () => Number, minimum: 0 }, units: { required: false, enum: t["../../../libs/types/src/dtos/content-watcher/activity.dto"].UnitTypeDto } }, "AssetReferenceDto": { referenceId: { required: true, type: () => String, minLength: 1 }, height: { required: false, type: () => Number, minimum: 1 }, width: { required: false, type: () => Number, minimum: 1 }, duration: { required: false, type: () => String, pattern: "DURATION_REGEX" } }, "TagDto": { type: { required: true, enum: t["../../../libs/types/src/dtos/content-watcher/activity.dto"].TagTypeDto }, name: { required: false, type: () => String, minLength: 1 }, mentionedId: { required: false, type: () => String, minLength: 1, pattern: "DSNP_USER_URI_REGEX" } }, "AssetDto": { type: { required: true, enum: t["../../../libs/types/src/dtos/content-watcher/activity.dto"].AttachmentTypeDto }, references: { required: false, type: () => [t["../../../libs/types/src/dtos/content-watcher/activity.dto"].AssetReferenceDto] }, name: { required: false, type: () => String, minLength: 1 }, href: { required: false, type: () => String, minLength: 1 } }, "BaseActivityDto": { name: { required: false, type: () => String }, tag: { required: false, type: () => [t["../../../libs/types/src/dtos/content-watcher/activity.dto"].TagDto] }, location: { required: false, type: () => t["../../../libs/types/src/dtos/content-watcher/activity.dto"].LocationDto } }, "NoteActivityDto": { content: { required: true, type: () => String, minLength: 1 }, published: { required: true, type: () => String, pattern: "ISO8601_REGEX" }, assets: { required: false, type: () => [t["../../../libs/types/src/dtos/content-watcher/activity.dto"].AssetDto] } }, "ProfileActivityDto": { icon: { required: false, type: () => [t["../../../libs/types/src/dtos/content-watcher/activity.dto"].AssetReferenceDto] }, summary: { required: false, type: () => String }, published: { required: false, type: () => String, pattern: "ISO8601_REGEX" } } }], [import("../../../libs/types/src/dtos/content-watcher/announcement.dto"), { "BroadcastDto": { content: { required: true, type: () => t["../../../libs/types/src/dtos/content-watcher/activity.dto"].NoteActivityDto } }, "ReplyDto": { inReplyTo: { required: true, type: () => String, pattern: "DSNP_CONTENT_URI_REGEX" }, content: { required: true, type: () => t["../../../libs/types/src/dtos/content-watcher/activity.dto"].NoteActivityDto } }, "TombstoneDto": { targetContentHash: { required: true, type: () => String, pattern: "DSNP_CONTENT_HASH_REGEX" }, targetAnnouncementType: { required: true, enum: t["../../../libs/types/src/dtos/content-watcher/announcement.dto"].ModifiableAnnouncementTypeDto } }, "UpdateDto": { targetContentHash: { required: true, type: () => String, pattern: "DSNP_CONTENT_HASH_REGEX" }, targetAnnouncementType: { required: true, enum: t["../../../libs/types/src/dtos/content-watcher/announcement.dto"].ModifiableAnnouncementTypeDto }, content: { required: true, type: () => t["../../../libs/types/src/dtos/content-watcher/activity.dto"].NoteActivityDto } }, "ReactionDto": { emoji: { required: true, type: () => String, minLength: 1, pattern: "DSNP_EMOJI_REGEX" }, apply: { required: true, type: () => Number, minimum: 0, maximum: 255 }, inReplyTo: { required: true, type: () => String, pattern: "DSNP_CONTENT_URI_REGEX" } }, "ProfileDto": { profile: { required: true, type: () => t["../../../libs/types/src/dtos/content-watcher/activity.dto"].ProfileActivityDto } } }], [import("../../../libs/types/src/dtos/content-watcher/common.dto"), { "DsnpUserIdParam": { userDsnpId: { required: true, type: () => String } }, "AnnouncementResponseDto": { referenceId: { required: true, type: () => String } }, "UploadResponseDto": { assetIds: { required: true, type: () => [String] } }, "FilesUploadDto": { files: { required: true, type: () => [Object] } }, "ResetScannerDto": { blockNumber: { required: false, type: () => Number, minimum: 0, maximum: 4294967296 }, rewindOffset: { required: false, type: () => Number, minimum: 0, maximum: 4294967296 }, immediate: { required: false, type: () => Boolean } } }], [import("../../../libs/types/src/dtos/content-watcher/content-search-request.dto"), { "ContentSearchRequestDto": { clientReferenceId: { required: true, type: () => String }, startBlock: { required: true, type: () => Number, minimum: 0, maximum: 4294967296 }, blockCount: { required: true, type: () => Number, minimum: 0, maximum: 4294967296 }, filters: { required: true, type: () => t["../../../libs/types/src/dtos/content-watcher/chain.watch.dto"].ChainWatchOptionsDto }, webhookUrl: { required: true, type: () => String } } }]], "controllers": [[import("./controllers/health.controller"), { "HealthController": { "healthz": {}, "livez": {}, "readyz": {} } }], [import("./controllers/v1/scanner.controller"), { "ScanControllerV1": { "resetScanner": {}, "getWatchOptions": { type: t["../../../libs/types/src/dtos/content-watcher/chain.watch.dto"].ChainWatchOptionsDto }, "setWatchOptions": {}, "pauseScanner": {}, "startScanner": {} } }], [import("./controllers/v1/search.controller"), { "SearchControllerV1": { "search": {} } }], [import("./controllers/v1/webhook.controller"), { "WebhookControllerV1": { "registerWebhook": {}, "clearAllWebHooks": {}, "getRegisteredWebhooks": {} } }]] } }; }; \ No newline at end of file diff --git a/docs/content-watcher/index.html b/docs/content-watcher/index.html index bf6872bb..6b5adcb2 100644 --- a/docs/content-watcher/index.html +++ b/docs/content-watcher/index.html @@ -1,1926 +1,353 @@ - + - - - Content Watcher Service API - - - - - - - - -
-
- -
-
- - - + Content Watcher Service API + + + + + + + + + +
- - - - - -
-
-
-
-
-
-

- Content Watcher Service API - (1.0) -

-

- Download OpenAPI specification:Download -

-
-
-
- - -
-
-
-
-
-

Content Watcher Service API

-
-
-
-
-
-
-
-

- v1/scanner -

-
-
-
-
-
-
-

- Reset blockchain scan to a specific block number or offset from the current position -

-
- Request Body schema: application/json -
required
-
-
-

blockNumber

-
- - - - - - - - - - - - - - - -
- blockNumber - -
-
- number - >= 1 -
-
-
-

The block number to reset the scanner to

-
-
-
-
- rewindOffset - -
-
- number -
-
-
-

- Number of blocks to rewind the scanner to (from blockNumber if supplied; - else from latest block -

-
-
-
-
- immediate - -
-
- boolean -
-
-
-

- Whether to schedule the new scan immediately or wait for the next scheduled interval -

-
-
-
-
-
-

Responses

-
- -
-
-
-
-
- - -
-
-

Request samples

-
-
    - -
-
-
-
- Content type -
application/json
-
-
-
-
- -
-
-
- { -
    -
  • -
    - "blockNumber": - 0, -
    -
  • -
  • -
    - "rewindOffset": - 100, -
    -
  • -
  • -
    - "immediate": - true -
    -
  • -
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

- Get the current watch options for the blockchain content event scanner -

-
-

Responses

-
- -
-
-
-
-
- - -
-
-

Response samples

-
-
    - -
-
-
-
- Content type -
application/json
-
-
-
-
- -
-
-
- { -
    -
  • -
    - "schemaIds": - [ -
      -
    • - -
    • -
    • - -
    • -
    - ], -
    -
  • -
  • -
    - "dsnpIds": - [ -
      -
    • - -
    • -
    • - -
    • -
    - ] -
    -
  • -
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

- Set watch options to filter the blockchain content scanner by schemas or MSA Ids -

-
- Request Body schema: application/json -
required
-
-
-

watchOptions: Filter contents by schemaIds and/or msaIds

-
- - - - - - - - - - - -
- schemaIds - -
-
- Array of numbers -
-
-
-

Specific schema ids to watch for

-
-
-
-
- dsnpIds - -
-
- Array of strings -
-
-
-

Specific dsnpIds (msa_id) to watch for

-
-
-
-
-
-

Responses

-
- -
-
-
-
-
- - -
-
-

Request samples

-
-
    - -
-
-
-
- Content type -
application/json
-
-
-
-
- -
-
-
- { -
    -
  • -
    - "schemaIds": - [ -
      -
    • - -
    • -
    • - -
    • -
    - ], -
    -
  • -
  • -
    - "dsnpIds": - [ -
      -
    • - -
    • -
    • - -
    • -
    - ] -
    -
  • -
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

- Pause the blockchain scanner -

-
-

Responses

-
- -
-
-
-
-
- - -
-
-
-
-
-
-
-

- Resume the blockchain content event scanner -

-
-
- query - Parameters -
- - - - - - - -
- immediate - -
-
- boolean -
-
-
-

- Immediate: whether to resume scan immediately (true), or wait until next scheduled - scan (false) -

-
-
-
-
-
-
-

Responses

-
- -
-
-
-
-
- - -
-
-
-
-
-
-
-

- v1/search -

-
-
-
- -
-
-
-

- v1/webhooks -

-
-
-
-
-
-
-

- Register a webhook to be called when new content is encountered on the chain -

-
- Request Body schema: application/json -
required
-
-
-

Register a webhook to be called when a new content is encountered

-
- - - - - - - - - - - -
- url -
required
-
-
-
- string -
-
-
-

Webhook URL

-
-
-
-
- announcementTypes -
required
-
-
-
- Array of strings -
-
-
-

Announcement types to send to the webhook

-
-
-
-
-
-

Responses

-
- -
-
-
-
-
- - -
-
-

Request samples

-
-
    - -
-
-
-
- Content type -
application/json
-
-
-
-
- -
-
-
- { -
    -
  • -
    - "url": - "https://example.com/webhook", -
    -
  • -
  • -
    - "announcementTypes": - [ -
      -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    • - -
    • -
    - ] -
    -
  • -
- }
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

- Clear all previously registered webhooks -

-
-

Responses

-
- -
-
-
-
-
- - -
-
-
-
-
-
-
-

- Get the list of currently registered webhooks -

-
-

Responses

-
- -
-
-
-
-
- - -
-
-

Response samples

-
-
    - -
-
-
-
- Content type -
application/json
-
-
-
-
- -
-
-
- [ -
    -
  • -
    - { -
      -
    • - -
    • -
    • - -
    • -
    - } -
    -
  • -
- ]
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-

- health -

-
-
-
-
-
-
-

- Check the health status of the service -

-
-

Responses

-
- -
-
-
-
-
- - -
-
-
-
-
-
-
-

- Check the live status of the service -

-
-

Responses

-
- -
-
-
-
-
- - -
-
-
-
-
-
-
-

- Check the ready status of the service -

-
-

Responses

-
- -
-
-
-
-
- - -
-
-
-
-
-
-
-
- - + + + + diff --git a/libs/types/src/dtos/content-watcher/chain.watch.dto.ts b/libs/types/src/dtos/content-watcher/chain.watch.dto.ts index 92f2007b..1831e012 100644 --- a/libs/types/src/dtos/content-watcher/chain.watch.dto.ts +++ b/libs/types/src/dtos/content-watcher/chain.watch.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsArray, IsOptional } from 'class-validator'; +import { ArrayNotEmpty, ArrayUnique, IsArray, IsInt, IsNumberString, IsOptional, Max, Min } from 'class-validator'; /** * Interface for chain filter options @@ -12,7 +12,11 @@ export class ChainWatchOptionsDto { // Specific schema ids to watch for @IsOptional() @IsArray() - @Type(() => Number) + @ArrayNotEmpty() + @ArrayUnique() + @IsInt({ each: true }) + @Min(0, { each: true }) + @Max(65_536, { each: true }) @ApiProperty({ type: 'array', items: { @@ -27,7 +31,9 @@ export class ChainWatchOptionsDto { // Specific dsnpIds (msa_id) to watch for @IsOptional() @IsArray() - @Type(() => String) + @ArrayNotEmpty() + @ArrayUnique() + @IsNumberString({ no_symbols: true }, { each: true }) @ApiProperty({ description: 'Specific dsnpIds (msa_id) to watch for', required: false, diff --git a/libs/types/src/dtos/content-watcher/common.dto.ts b/libs/types/src/dtos/content-watcher/common.dto.ts index 2c6a4bbc..530809ec 100644 --- a/libs/types/src/dtos/content-watcher/common.dto.ts +++ b/libs/types/src/dtos/content-watcher/common.dto.ts @@ -3,7 +3,7 @@ */ // eslint-disable-next-line max-classes-per-file import { ApiProperty } from '@nestjs/swagger'; -import { IsBoolean, IsNotEmpty, IsNumber, IsNumberString, IsOptional, IsPositive } from 'class-validator'; +import { IsBoolean, IsInt, IsNotEmpty, IsNumberString, IsOptional, Max, Min } from 'class-validator'; import { IScanReset } from '#types/interfaces/content-watcher'; export class DsnpUserIdParam { @@ -27,17 +27,20 @@ export class FilesUploadDto { export class ResetScannerDto implements IScanReset { @IsOptional() - @IsNumber() - @IsPositive() + @IsInt() + @Min(0) + @Max(4_294_967_296) @ApiProperty({ required: false, type: 'number', description: 'The block number to reset the scanner to', example: 0 }) blockNumber?: number; @IsOptional() - @IsNumber() + @IsInt() + @Min(0) + @Max(4_294_967_296) @ApiProperty({ required: false, type: 'number', - description: 'Number of blocks to rewind the scanner to (from `blockNumber` if supplied; else from latest block', + description: 'Number of blocks to rewind the scanner to (from `blockNumber` if supplied; else from latest block)', example: 100, }) rewindOffset?: number; diff --git a/libs/types/src/dtos/content-watcher/content-search-request.dto.ts b/libs/types/src/dtos/content-watcher/content-search-request.dto.ts index bab45408..9561f6bb 100644 --- a/libs/types/src/dtos/content-watcher/content-search-request.dto.ts +++ b/libs/types/src/dtos/content-watcher/content-search-request.dto.ts @@ -1,6 +1,7 @@ -import { IsInt, IsOptional, IsPositive, IsString, IsUrl } from 'class-validator'; +import { IsInt, IsNotEmpty, IsOptional, IsPositive, IsString, IsUrl, Max, Min, ValidateNested } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; import { ChainWatchOptionsDto } from './chain.watch.dto'; +import { Type } from 'class-transformer'; export class ContentSearchRequestDto { @IsOptional() @@ -13,7 +14,8 @@ export class ContentSearchRequestDto { @IsOptional() @IsInt() - @IsPositive() + @Min(0) + @Max(4_294_967_296) @ApiProperty({ description: 'The block number to search (backward) from', required: false, @@ -21,8 +23,10 @@ export class ContentSearchRequestDto { }) startBlock: number; + @IsNotEmpty() @IsInt() - @IsPositive() + @Min(0) + @Max(4_294_967_296) @ApiProperty({ description: 'The number of blocks to scan (backwards)', required: true, @@ -31,16 +35,20 @@ export class ContentSearchRequestDto { blockCount: number; @IsOptional() + @ValidateNested() + @Type(() => ChainWatchOptionsDto) @ApiProperty({ description: 'The schemaIds/dsnpIds to filter by', required: false, }) filters: ChainWatchOptionsDto; - @IsUrl({ require_tld: false }) + @IsNotEmpty() + @IsUrl({ require_tld: false, require_protocol: true, require_valid_protocol: true }) @ApiProperty({ description: 'A webhook URL to be notified of the results of this search', required: true, + example: 'https://example.com', }) webhookUrl: string; } diff --git a/libs/types/src/dtos/content-watcher/subscription.webhook.dto.ts b/libs/types/src/dtos/content-watcher/subscription.webhook.dto.ts index 8c0c05e6..5b6a265c 100644 --- a/libs/types/src/dtos/content-watcher/subscription.webhook.dto.ts +++ b/libs/types/src/dtos/content-watcher/subscription.webhook.dto.ts @@ -1,5 +1,6 @@ -import { IsArray, IsString } from 'class-validator'; +import { ArrayNotEmpty, IsArray, IsEnum, IsNotEmpty, IsUrl } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; +import { AnnouncementTypeName } from '#types/enums'; export interface IWebhookRegistration { url: string; @@ -7,7 +8,8 @@ export interface IWebhookRegistration { } export class WebhookRegistrationDto implements IWebhookRegistration { - @IsString() + @IsNotEmpty() + @IsUrl({ require_tld: false, require_protocol: true, require_valid_protocol: true }) @ApiProperty({ description: 'Webhook URL', example: 'https://example.com/webhook', @@ -15,9 +17,14 @@ export class WebhookRegistrationDto implements IWebhookRegistration { url: string; // Webhook URL @IsArray() + @ArrayNotEmpty() + @IsEnum(AnnouncementTypeName, { each: true }) @ApiProperty({ description: 'Announcement types to send to the webhook', - example: ['Broadcast', 'Reaction', 'Tombstone', 'Reply', 'Update'], + isArray: true, + example: ['broadcast', 'reaction', 'tombstone', 'reply', 'update'], + enum: AnnouncementTypeName, + enumName: 'AnnouncementTypeName', }) announcementTypes: string[]; // Announcement types to send to the webhook } diff --git a/openapi-specs/content-watcher.openapi.json b/openapi-specs/content-watcher.openapi.json index 9cc4892c..43396fef 100644 --- a/openapi-specs/content-watcher.openapi.json +++ b/openapi-specs/content-watcher.openapi.json @@ -269,13 +269,16 @@ "properties": { "blockNumber": { "type": "number", - "minimum": 1, + "minimum": 0, + "maximum": 4294967296, "description": "The block number to reset the scanner to", "example": 0 }, "rewindOffset": { "type": "number", - "description": "Number of blocks to rewind the scanner to (from `blockNumber` if supplied; else from latest block", + "minimum": 0, + "maximum": 4294967296, + "description": "Number of blocks to rewind the scanner to (from `blockNumber` if supplied; else from latest block)", "example": 100 }, "immediate": { @@ -290,6 +293,8 @@ "properties": { "schemaIds": { "type": "array", + "minimum": 0, + "maximum": 65536, "items": { "type": "number" }, @@ -321,13 +326,15 @@ }, "startBlock": { "type": "number", - "minimum": 1, + "minimum": 0, + "maximum": 4294967296, "description": "The block number to search (backward) from", "example": 100 }, "blockCount": { "type": "number", - "minimum": 1, + "minimum": 0, + "maximum": 4294967296, "description": "The number of blocks to scan (backwards)", "example": 101 }, @@ -341,7 +348,8 @@ }, "webhookUrl": { "type": "string", - "description": "A webhook URL to be notified of the results of this search" + "description": "A webhook URL to be notified of the results of this search", + "example": "https://example.com" } }, "required": [ @@ -349,6 +357,18 @@ "webhookUrl" ] }, + "AnnouncementTypeName": { + "type": "string", + "description": "Announcement types to send to the webhook", + "enum": [ + "tombstone", + "broadcast", + "reply", + "reaction", + "profile", + "update" + ] + }, "WebhookRegistrationDto": { "type": "object", "properties": { @@ -358,17 +378,16 @@ "example": "https://example.com/webhook" }, "announcementTypes": { - "description": "Announcement types to send to the webhook", + "type": "array", "example": [ - "Broadcast", - "Reaction", - "Tombstone", - "Reply", - "Update" + "broadcast", + "reaction", + "tombstone", + "reply", + "update" ], - "type": "array", "items": { - "type": "string" + "$ref": "#/components/schemas/AnnouncementTypeName" } } },