diff --git a/src/advertising/slise.ts b/src/advertising/slise.ts index c01bc87..89a6398 100644 --- a/src/advertising/slise.ts +++ b/src/advertising/slise.ts @@ -89,6 +89,29 @@ export interface SliseAdPlacesRule { stylesOverrides?: SliseAdStylesOverrides[]; } +export interface PermanentSliseAdPlacesRule { + urlRegexes: string[]; + adSelector: { + isMultiple: boolean; + cssString: string; + parentDepth: number; + }; + parentSelector: { + isMultiple: boolean; + cssString: string; + parentDepth: number; + }; + insertionIndex?: number; + insertBeforeSelector?: string; + insertAfterSelector?: string; + insertionsCount?: number; + shouldUseDivWrapper: boolean; + divWrapperStyle?: Record; + elementToMeasureSelector?: string; + stylesOverrides?: SliseAdStylesOverrides[]; + shouldHideOriginal?: boolean; +} + export interface SliseAdProvidersByDomainRule { urlRegexes: string[]; providers: string[]; @@ -98,6 +121,7 @@ const SLISE_AD_PLACES_RULES_KEY = 'slise_ad_places_rules'; const SLISE_AD_PROVIDERS_BY_SITES_KEY = 'slise_ad_providers_by_sites'; const SLISE_AD_PROVIDERS_ALL_SITES_KEY = 'slise_ad_providers_all_sites'; const SLISE_AD_PROVIDERS_LIST_KEY = 'slise_ad_providers_list'; +const PERMANENT_SLISE_AD_PLACES_RULES_KEY = 'permanent_slise_ad_places_rules'; export const { getByKey: getSliseAdPlacesRulesByDomain, @@ -120,6 +144,13 @@ export const { removeValues: removeProviders } = objectStorageMethodsFactory(SLISE_AD_PROVIDERS_LIST_KEY, []); +export const { + getByKey: getPermanentSliseAdPlacesRulesByDomain, + getAllValues: getAllPermanentSliseAdPlacesRules, + upsertValues: upsertPermanentSliseAdPlacesRules, + removeValues: removePermanentSliseAdPlacesRules +} = objectStorageMethodsFactory(PERMANENT_SLISE_AD_PLACES_RULES_KEY, []); + export const getSliseAdProvidersForAllSites = async () => redisClient.smembers(SLISE_AD_PROVIDERS_ALL_SITES_KEY); export const addSliseAdProvidersForAllSites = async (providers: string[]) => diff --git a/src/routers/slise-ad-rules/ad-places.ts b/src/routers/slise-ad-rules/ad-places.ts index d8bc841..e8d2f95 100644 --- a/src/routers/slise-ad-rules/ad-places.ts +++ b/src/routers/slise-ad-rules/ad-places.ts @@ -1,16 +1,26 @@ import { Router } from 'express'; import { + getAllPermanentSliseAdPlacesRules, getAllSliseAdPlacesRules, + getPermanentSliseAdPlacesRulesByDomain, getSliseAdPlacesRulesByDomain, + removePermanentSliseAdPlacesRules, removeSliseAdPlacesRules, + upsertPermanentSliseAdPlacesRules, upsertSliseAdPlacesRules } from '../../advertising/slise'; import { addObjectStorageMethodsToRouter } from '../../utils/express-helpers'; -import { hostnamesListSchema, sliseAdPlacesRulesDictionarySchema } from '../../utils/schemas'; +import { + hostnamesListSchema, + permanentSliseAdPlacesRulesDictionarySchema, + sliseAdPlacesRulesDictionarySchema +} from '../../utils/schemas'; /** * @swagger + * tags: + * name: Slise ad places * components: * schemas: * SliseAdPlacesRuleSelector: @@ -115,15 +125,254 @@ import { hostnamesListSchema, sliseAdPlacesRulesDictionarySchema } from '../../u * cssString: 'div.left-container > app-pe-banner:nth-child(2)' * parentDepth: 0 * shouldUseDivWrapper: true + * PermanentSliseAdPlacesRule: + * type: object + * description: > + * This object describes rules of replacing ads banners if they are found and inserting new ads banners if + * they are not found. Exactly one of `insertionIndex`, `insertBeforeSelector` and `insertAfterSelector` + * properties must be specified. + * required: + * - urlRegexes + * - adSelector + * - parentSelector + * - shouldUseDivWrapper + * properties: + * urlRegexes: + * type: array + * items: + * type: string + * format: regex + * adSelector: + * type: object + * description: > + * This object describes rules of selecting ads banners in the parents of new ads banners selected + * according to the rules described in the `parentSelector` property. + * required: + * - isMultiple + * - cssString + * - parentDepth + * properties: + * isMultiple: + * type: boolean + * description: Whether the selector should return multiple elements + * cssString: + * type: string + * description: CSS selector + * parentDepth: + * type: number + * min: 0 + * integer: true + * description: > + * Indicates the depth of the parent element of the selected element, i. e. 0 means that the selected + * elements are ads banners themselves, 1 means that the selected elements are ads banners' direct + * children and so on. + * parentSelector: + * type: object + * required: + * - isMultiple + * - cssString + * - parentDepth + * properties: + * isMultiple: + * type: boolean + * description: Whether the selector should return multiple elements + * cssString: + * type: string + * description: CSS selector + * parentDepth: + * type: number + * min: 0 + * integer: true + * description: > + * Indicates the depth of the parent element of the selected element, i. e. 0 means that the selected + * elements are parents of new ads banners themselves, 1 means that the selected elements are their + * direct children and so on. + * insertionIndex: + * type: number + * integer: true + * description: > + * Describes where to insert new ads banners in the selected parents of new ads banners in case if original + * ads banners are not found. If the value is negative, the insertion index will be calculated from the end. + * The counting starts from 0. + * insertBeforeSelector: + * type: string + * description: A selector for the element before which new ads banners should be inserted + * insertAfterSelector: + * type: string + * description: A selector for the element after which new ads banners should be inserted + * insertionsCount: + * type: number + * integer: true + * min: 1 + * default: 1 + * description: > + * Describes how many new ads banners should be inserted in case if original ads banners are not found. + * shouldUseDivWrapper: + * type: boolean + * description: Whether the Slise ads banner should be wrapped in a div + * divWrapperStyle: + * type: object + * description: Style of the div wrapper + * additionalProperties: + * type: string + * elementToMeasureSelector: + * type: string + * description: A selector of the element which should be measured to define banner size + * stylesOverrides: + * type: array + * items: + * $ref: '#/components/schemas/SliseAdStylesOverrides' + * shouldHideOriginal: + * type: boolean + * description: Whether original ads banners should be hidden but not removed + * default: false + * example: + * urlRegexes: + * - '^https://etherscan\.io/tx/' + * adSelector: + * isMultiple: false + * cssString: '.coinzilla' + * parentDepth: 0 + * parentSelector: + * isMultiple: false + * cssString: '#ContentPlaceHolder1_maintable > * > .row:nth-child(8) > :nth-child(2) > * > *' + * parentDepth: 0 + * insertionIndex: 0 + * shouldUseDivWrapper: false + * PermanentSliseAdPlacesRulesDictionary: + * type: object + * additionalProperties: + * type: array + * items: + * $ref: '#/components/schemas/PermanentSliseAdPlacesRule' + * example: + * etherscan.io: + * - urlRegexes: + * - '^https://etherscan\.io/tx/' + * adSelector: + * isMultiple: false + * cssString: '.coinzilla' + * parentDepth: 0 + * parentSelector: + * isMultiple: false + * cssString: '#ContentPlaceHolder1_maintable > * > .row:nth-child(8) > :nth-child(2) > * > *' + * parentDepth: 0 + * insertionIndex: 0 + * shouldUseDivWrapper: false */ export const sliseAdPlacesRulesRouter = Router(); +/** + * @swagger + * /api/slise-ad-rules/ad-places/permanent/{domain}: + * get: + * summary: Get rules for permanent ads places for the specified domain + * tags: + * - Slise ad places + * parameters: + * - in: path + * name: domain + * required: true + * schema: + * type: string + * format: hostname + * example: 'etherscan.io' + * responses: + * '200': + * description: Rules list + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/PermanentSliseAdPlacesRule' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * /api/slise-ad-rules/ad-places/permanent: + * get: + * summary: Get all rules for permanent ads places + * tags: + * - Slise ad places + * responses: + * '200': + * description: Domain - rules list dictionary + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PermanentSliseAdPlacesRulesDictionary' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * post: + * summary: Add rules for permanent ads places. If rules for a domain already exist, they will be overwritten + * tags: + * - Slise ad places + * security: + * - basicAuth: [] + * requestBody: + * description: Domain - rules list dictionary + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PermanentSliseAdPlacesRulesDictionary' + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * delete: + * summary: Remove rules for permanent ads places + * tags: + * - Slise ad places + * security: + * - basicAuth: [] + * requestBody: + * description: List of domain names to remove rules for + * content: + * application/json: + * schema: + * type: array + * items: + * type: string + * format: hostname + * example: + * - 'etherscan.io' + * responses: + * '200': + * $ref: '#/components/responses/SuccessResponse' + * '400': + * $ref: '#/components/responses/ErrorResponse' + * '401': + * $ref: '#/components/responses/UnauthorizedError' + * '500': + * $ref: '#/components/responses/ErrorResponse' + */ +addObjectStorageMethodsToRouter( + sliseAdPlacesRulesRouter, + '/permanent', + { + getByKey: getPermanentSliseAdPlacesRulesByDomain, + getAllValues: getAllPermanentSliseAdPlacesRules, + upsertValues: upsertPermanentSliseAdPlacesRules, + removeValues: removePermanentSliseAdPlacesRules + }, + 'domain', + permanentSliseAdPlacesRulesDictionarySchema, + hostnamesListSchema, + entriesCount => `${entriesCount} entries have been removed` +); + /** * @swagger * /api/slise-ad-rules/ad-places/{domain}: * get: * summary: Get rules for ads places for the specified domain + * tags: + * - Slise ad places * parameters: * - in: path * name: domain @@ -146,6 +395,8 @@ export const sliseAdPlacesRulesRouter = Router(); * /api/slise-ad-rules/ad-places: * get: * summary: Get all rules for ads places + * tags: + * - Slise ad places * responses: * '200': * description: Domain - rules list dictionary @@ -157,6 +408,8 @@ export const sliseAdPlacesRulesRouter = Router(); * $ref: '#/components/responses/ErrorResponse' * post: * summary: Add rules for ads places. If rules for a domain already exist, they will be overwritten + * tags: + * - Slise ad places * security: * - basicAuth: [] * requestBody: @@ -176,6 +429,8 @@ export const sliseAdPlacesRulesRouter = Router(); * $ref: '#/components/responses/ErrorResponse' * delete: * summary: Remove rules for ads places + * tags: + * - Slise ad places * security: * - basicAuth: [] * requestBody: diff --git a/src/routers/slise-ad-rules/providers.ts b/src/routers/slise-ad-rules/providers.ts index c7056bd..7d8b610 100644 --- a/src/routers/slise-ad-rules/providers.ts +++ b/src/routers/slise-ad-rules/providers.ts @@ -24,6 +24,8 @@ import { /** * @swagger + * tags: + * name: Known ads providers * components: * schemas: * SliseAdProvidersByDomainRule: @@ -80,6 +82,8 @@ export const sliseAdProvidersRouter = Router(); * /api/slise-ad-rules/providers/all-sites: * get: * summary: Get providers of ads for which ads should be replaced at all sites + * tags: + * - Known ads providers * responses: * '200': * description: List of providers @@ -98,6 +102,8 @@ export const sliseAdProvidersRouter = Router(); * summary: > * Add providers of ads for which ads should be replaced at all sites. They will not be removed * from lists of providers from specific sites. Checks for providers existence are not performed + * tags: + * - Known ads providers * security: * - basicAuth: [] * requestBody: @@ -122,6 +128,8 @@ export const sliseAdProvidersRouter = Router(); * $ref: '#/components/responses/ErrorResponse' * delete: * summary: Remove providers of ads for which ads should be replaced at all sites + * tags: + * - Known ads providers * security: * - basicAuth: [] * requestBody: @@ -180,6 +188,8 @@ sliseAdProvidersRouter * /api/slise-ad-rules/providers/by-sites/{domain}: * get: * summary: Get rules for providers of ads for which ads should be replaced at the specified site + * tags: + * - Known ads providers * parameters: * - in: path * name: domain @@ -202,6 +212,8 @@ sliseAdProvidersRouter * /api/slise-ad-rules/providers/by-sites: * get: * summary: Get rules for providers of ads for which ads should be replaced at all sites + * tags: + * - Known ads providers * responses: * '200': * description: Domain - rules list dictionary @@ -215,6 +227,8 @@ sliseAdProvidersRouter * summary: > * Add rules for providers of ads for the specified sites. They will not be removed from lists * of providers from all sites. Checks for providers existence are not performed + * tags: + * - Known ads providers * security: * - basicAuth: [] * requestBody: @@ -234,6 +248,8 @@ sliseAdProvidersRouter * $ref: '#/components/responses/ErrorResponse' * delete: * summary: Remove rules for providers of ads for the specified sites + * tags: + * - Known ads providers * security: * - basicAuth: [] * requestBody: @@ -278,6 +294,8 @@ addObjectStorageMethodsToRouter( * /api/slise-ad-rules/providers/{providerId}: * get: * summary: Get selectors for a provider + * tags: + * - Known ads providers * parameters: * - in: path * name: providerId @@ -303,6 +321,8 @@ addObjectStorageMethodsToRouter( * /api/slise-ad-rules/providers: * get: * summary: Get all providers + * tags: + * - Known ads providers * responses: * '200': * description: Provider - selectors dictionary @@ -314,6 +334,8 @@ addObjectStorageMethodsToRouter( * $ref: '#/components/responses/ErrorResponse' * post: * summary: Upserts providers. Providers that have existed before will be overwritten + * tags: + * - Known ads providers * security: * - basicAuth: [] * requestBody: @@ -333,6 +355,8 @@ addObjectStorageMethodsToRouter( * $ref: '#/components/responses/ErrorResponse' * delete: * summary: Delete specified providers. Cascade delete rules are not applied + * tags: + * - Known ads providers * security: * - basicAuth: [] * requestBody: diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts index 5f887b4..5497f1b 100644 --- a/src/utils/schemas.ts +++ b/src/utils/schemas.ts @@ -10,7 +10,13 @@ import { StringSchema as IStringSchema } from 'yup'; -import { SliseAdPlacesRule, SliseAdProvidersByDomainRule, StylePropName, stylePropsNames } from '../advertising/slise'; +import { + SliseAdPlacesRule, + PermanentSliseAdPlacesRule, + SliseAdProvidersByDomainRule, + StylePropName, + stylePropsNames +} from '../advertising/slise'; import { isValidSelectorsGroup } from '../utils/selectors.min.js'; import { isDefined } from './helpers'; @@ -55,7 +61,7 @@ export const regexStringListSchema = arraySchema().of(regexStringSchema.clone(). const cssSelectorSchema = stringSchema().test('is-css-selector', function (value: string | undefined) { // eslint-disable-next-line @typescript-eslint/strict-boolean-expressions - if (isDefined(value) && isValidSelectorsGroup(value)) { + if (!isDefined(value) || isValidSelectorsGroup(value)) { return true; } @@ -92,7 +98,7 @@ const styleSchema: IObjectSchema> = makeDictionary const sliseAdStylesOverridesSchema = objectSchema().shape({ parentDepth: numberSchema().integer().min(0).required(), - style: styleSchema.required() + style: styleSchema.clone().required() }); const sliseAdPlacesRulesSchema = arraySchema() @@ -118,6 +124,58 @@ const sliseAdPlacesRulesSchema = arraySchema() export const sliseAdPlacesRulesDictionarySchema: IObjectSchema> = makeDictionarySchema(hostnameSchema, sliseAdPlacesRulesSchema).required(); +const permanentSliseAdPlacesRulesSchema = arraySchema() + .of( + objectSchema() + .shape({ + urlRegexes: arraySchema().of(regexStringSchema.clone().required()).required(), + adSelector: objectSchema() + .shape({ + isMultiple: booleanSchema().required(), + cssString: cssSelectorSchema.clone().required(), + parentDepth: numberSchema().integer().min(0).required() + }) + .required(), + parentSelector: objectSchema() + .shape({ + isMultiple: booleanSchema().required(), + cssString: cssSelectorSchema.clone().required(), + parentDepth: numberSchema().integer().min(0).required() + }) + .required(), + insertionIndex: numberSchema().integer(), + insertBeforeSelector: cssSelectorSchema, + insertAfterSelector: cssSelectorSchema, + insertionsCount: numberSchema().integer().min(1).default(1), + shouldUseDivWrapper: booleanSchema().required(), + divWrapperStyle: styleSchema, + elementToMeasureSelector: cssSelectorSchema, + stylesOverrides: arraySchema().of(sliseAdStylesOverridesSchema.clone().required()), + shouldHideOriginal: booleanSchema().default(false) + }) + .test('insertion-place-specified', (value: PermanentSliseAdPlacesRule | undefined) => { + if (!value) { + return true; + } + + const { insertionIndex, insertBeforeSelector, insertAfterSelector } = value; + const definedValuesCount = [insertionIndex, insertBeforeSelector, insertAfterSelector].filter(isDefined).length; + + if (definedValuesCount !== 1) { + throw new Error( + 'Exactly one of insertionIndex, insertBeforeSelector and insertAfterSelector must be specified' + ); + } + + return true; + }) + .required() + ) + .required(); + +export const permanentSliseAdPlacesRulesDictionarySchema: IObjectSchema> = + makeDictionarySchema(hostnameSchema, permanentSliseAdPlacesRulesSchema).required(); + const sliseAdProvidersByDomainRulesSchema = arraySchema() .of( objectSchema()