diff --git a/package.json b/package.json index d44a9c9..c5ce48f 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "pino-pretty": "^4.7.1", "qs": "^6.10.3", "semaphore": "^1.1.0", + "semver": "^7.6.0", "swagger-jsdoc": "^6.2.8", "swagger-ui-express": "^5.0.0", "yup": "^1.3.2" diff --git a/src/advertising/slise.ts b/src/advertising/external-ads.ts similarity index 56% rename from src/advertising/slise.ts rename to src/advertising/external-ads.ts index 3e935e2..a1bc5a2 100644 --- a/src/advertising/slise.ts +++ b/src/advertising/external-ads.ts @@ -1,3 +1,5 @@ +import { satisfies as versionSatisfiesRange } from 'semver'; + import { objectStorageMethodsFactory, redisClient } from '../redis'; /** Style properties names that are likely to be unnecessary for banners are skipped */ @@ -77,12 +79,16 @@ export const stylePropsNames = [ ]; export type StylePropName = (typeof stylePropsNames)[number]; -interface SliseAdStylesOverrides { +interface AdStylesOverrides { parentDepth: number; style: Record; } -export interface SliseAdPlacesRule { +interface ExtVersionConstraints { + extVersion: string; +} + +export interface AdPlacesRule extends ExtVersionConstraints { urlRegexes: string[]; selector: { isMultiple: boolean; @@ -91,11 +97,11 @@ export interface SliseAdPlacesRule { shouldUseDivWrapper: boolean; divWrapperStyle?: Record; }; - stylesOverrides?: SliseAdStylesOverrides[]; + stylesOverrides?: AdStylesOverrides[]; shouldHideOriginal?: boolean; } -export interface PermanentSliseAdPlacesRule { +export interface PermanentAdPlacesRule extends ExtVersionConstraints { urlRegexes: string[]; adSelector: { isMultiple: boolean; @@ -115,48 +121,58 @@ export interface PermanentSliseAdPlacesRule { elementStyle?: Record; divWrapperStyle?: Record; elementToMeasureSelector?: string; - stylesOverrides?: SliseAdStylesOverrides[]; + stylesOverrides?: AdStylesOverrides[]; shouldHideOriginal?: boolean; } -export interface SliseAdProvidersByDomainRule { +export interface AdProvidersByDomainRule extends ExtVersionConstraints { urlRegexes: string[]; providers: string[]; } -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 interface AdProviderSelectorsRule extends ExtVersionConstraints { + selectors: string[]; +} + +export interface AdProviderForAllSitesRule extends ExtVersionConstraints { + providers: string[]; +} + +const AD_PLACES_RULES_KEY = 'ad_places_rules'; +const AD_PROVIDERS_BY_SITES_KEY = 'ad_providers_by_sites'; +const AD_PROVIDERS_ALL_SITES_KEY = 'ad_providers_all_sites'; +const AD_PROVIDERS_LIST_KEY = 'ad_providers_list'; +const PERMANENT_AD_PLACES_RULES_KEY = 'permanent_ad_places_rules'; const PERMANENT_NATIVE_AD_PLACES_RULES_KEY = 'permanent_native_ad_places_rules'; -export const sliseAdPlacesRulesMethods = objectStorageMethodsFactory( - SLISE_AD_PLACES_RULES_KEY, - [] -); +export const adPlacesRulesMethods = objectStorageMethodsFactory(AD_PLACES_RULES_KEY, []); -export const sliseAdProvidersByDomainRulesMethods = objectStorageMethodsFactory( - SLISE_AD_PROVIDERS_BY_SITES_KEY, +export const adProvidersByDomainRulesMethods = objectStorageMethodsFactory( + AD_PROVIDERS_BY_SITES_KEY, [] ); -export const sliseAdProvidersMethods = objectStorageMethodsFactory(SLISE_AD_PROVIDERS_LIST_KEY, []); +export const adProvidersMethods = objectStorageMethodsFactory(AD_PROVIDERS_LIST_KEY, []); -export const permanentSliseAdPlacesMethods = objectStorageMethodsFactory( - PERMANENT_SLISE_AD_PLACES_RULES_KEY, +export const permanentAdPlacesMethods = objectStorageMethodsFactory( + PERMANENT_AD_PLACES_RULES_KEY, [] ); -export const permanentNativeAdPlacesMethods = objectStorageMethodsFactory( +export const permanentNativeAdPlacesMethods = objectStorageMethodsFactory( PERMANENT_NATIVE_AD_PLACES_RULES_KEY, [] ); -export const getSliseAdProvidersForAllSites = async () => redisClient.smembers(SLISE_AD_PROVIDERS_ALL_SITES_KEY); +export const getAdProvidersForAllSites = async () => redisClient.smembers(AD_PROVIDERS_ALL_SITES_KEY); + +export const addAdProvidersForAllSites = async (providers: string[]) => + redisClient.sadd(AD_PROVIDERS_ALL_SITES_KEY, ...providers); + +export const removeAdProvidersForAllSites = async (providers: string[]) => + redisClient.srem(AD_PROVIDERS_ALL_SITES_KEY, ...providers); -export const addSliseAdProvidersForAllSites = async (providers: string[]) => - redisClient.sadd(SLISE_AD_PROVIDERS_ALL_SITES_KEY, ...providers); +const FALLBACK_VERSION = '0.0.0'; -export const removeSliseAdProvidersForAllSites = async (providers: string[]) => - redisClient.srem(SLISE_AD_PROVIDERS_ALL_SITES_KEY, ...providers); +export const filterByVersion = (rules: T[], version?: string) => + rules.filter(({ extVersion }) => versionSatisfiesRange(version ?? FALLBACK_VERSION, extVersion)); diff --git a/src/index.ts b/src/index.ts index 19ca0cd..9938eb9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ import { getNotifications } from './notifications/utils/get-notifications.util'; import { getParsedContent } from './notifications/utils/get-parsed-content.util'; import { getPlatforms } from './notifications/utils/get-platforms.util'; import { redisClient } from './redis'; -import { sliseRulesRouter } from './routers/slise-ad-rules'; +import { adRulesRouter } from './routers/slise-ad-rules'; import { getABData } from './utils/ab-test'; import { cancelAliceBobOrder } from './utils/alice-bob/cancel-alice-bob-order'; import { createAliceBobOrder } from './utils/alice-bob/create-alice-bob-order'; @@ -328,7 +328,7 @@ app.get('/api/advertising-info', (_req, res) => { } }); -app.use('/api/slise-ad-rules', sliseRulesRouter); +app.use('/api/slise-ad-rules', adRulesRouter); app.post('/api/magic-square-quest/start', async (req, res) => { try { diff --git a/src/routers/slise-ad-rules/ad-places.ts b/src/routers/slise-ad-rules/ad-places.ts index c3803b8..22cd924 100644 --- a/src/routers/slise-ad-rules/ad-places.ts +++ b/src/routers/slise-ad-rules/ad-places.ts @@ -1,24 +1,25 @@ import { Router } from 'express'; import { + filterByVersion, permanentNativeAdPlacesMethods, - permanentSliseAdPlacesMethods, - sliseAdPlacesRulesMethods -} from '../../advertising/slise'; + permanentAdPlacesMethods, + adPlacesRulesMethods +} from '../../advertising/external-ads'; import { addObjectStorageMethodsToRouter } from '../../utils/express-helpers'; import { hostnamesListSchema, - permanentSliseAdPlacesRulesDictionarySchema, - sliseAdPlacesRulesDictionarySchema + permanentAdPlacesRulesDictionarySchema, + adPlacesRulesDictionarySchema } from '../../utils/schemas'; /** * @swagger * tags: - * name: Slise ad places + * name: Ad places * components: * schemas: - * SliseAdPlacesRuleSelector: + * AdPlacesRuleSelector: * type: object * required: * - isMultiple @@ -42,13 +43,13 @@ import { * children and so on. * shouldUseDivWrapper: * type: boolean - * description: Whether the Slise ads banner should be wrapped in a div + * description: Whether the ads banner should be wrapped in a div * divWrapperStyle: * type: object * description: Style of the div wrapper * additionalProperties: * type: string - * SliseAdStylesOverrides: + * AdStylesOverrides: * type: object * required: * - parentDepth @@ -65,41 +66,52 @@ import { * description: New style of the parent element * additionalProperties: * type: string - * SliseAdPlacesRule: + * ExtVersionConstraints: * type: object - * required: - * - urlRegexes - * - selector * properties: - * urlRegexes: - * type: array - * items: - * type: string - * format: regex - * selector: - * $ref: '#/components/schemas/SliseAdPlacesRuleSelector' - * 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://goerli\.etherscan\.io/?$' - * selector: - * isMultiple: false - * cssString: 'main > section div.row > div:nth-child(2) > div' - * parentDepth: 0 - * shouldUseDivWrapper: false - * SliseAdPlacesRulesDictionary: + * extVersion: + * type: string + * description: > + * A range of versions where the rule is applicable. If not specified, the rule is applicable + * for all versions. See the [ranges format](https://www.npmjs.com/package/semver#ranges) + * default: '*' + * AdPlacesRule: + * allOf: + * - $ref: '#/components/schemas/ExtVersionConstraints' + * - type: object + * required: + * - urlRegexes + * - selector + * properties: + * urlRegexes: + * type: array + * items: + * type: string + * format: regex + * selector: + * $ref: '#/components/schemas/AdPlacesRuleSelector' + * stylesOverrides: + * type: array + * items: + * $ref: '#/components/schemas/AdStylesOverrides' + * shouldHideOriginal: + * type: boolean + * description: Whether original ads banners should be hidden but not removed + * default: false + * example: + * urlRegexes: + * - '^https://goerli\.etherscan\.io/?$' + * selector: + * isMultiple: false + * cssString: 'main > section div.row > div:nth-child(2) > div' + * parentDepth: 0 + * shouldUseDivWrapper: false + * AdPlacesRulesDictionary: * type: object * additionalProperties: * type: array * items: - * $ref: '#/components/schemas/SliseAdPlacesRule' + * $ref: '#/components/schemas/AdPlacesRule' * example: * goerli.etherscan.io: * - urlRegexes: @@ -124,131 +136,133 @@ import { * 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 + * PermanentAdPlacesRule: + * allOf: + * - $ref: '#/components/schemas/ExtVersionConstraints' + * - 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. + * 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: - * - isMultiple - * - cssString - * - parentDepth + * - urlRegexes + * - adSelector + * - parentSelector + * - shouldUseDivWrapper * properties: - * isMultiple: - * type: boolean - * description: Whether the selector should return multiple elements - * cssString: - * type: string - * description: CSS selector - * parentDepth: + * 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 - * 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: + * 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: CSS selector - * parentDepth: + * 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 - * min: 0 * integer: true + * min: 1 + * default: 1 * 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 - * elementStyle: - * type: object - * description: Style of the new ad banner - * additionalProperties: - * type: string - * 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: + * Describes how many new ads banners should be inserted in case if original ads banners are not found. + * shouldUseDivWrapper: + * type: boolean + * description: Whether the ads banner should be wrapped in a div + * elementStyle: + * type: object + * description: Style of the new ad banner + * additionalProperties: + * type: string + * 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/AdStylesOverrides' + * 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 + * PermanentAdPlacesRulesDictionary: * type: object * additionalProperties: * type: array * items: - * $ref: '#/components/schemas/PermanentSliseAdPlacesRule' + * $ref: '#/components/schemas/PermanentAdPlacesRule' * example: * etherscan.io: * - urlRegexes: @@ -265,15 +279,53 @@ import { * shouldUseDivWrapper: false */ -export const sliseAdPlacesRulesRouter = Router(); +export const adPlacesRulesRouter = Router(); /** * @swagger + * /api/slise-ad-rules/ad-places/permanent-native/raw/all: + * get: + * summary: Get all rules for permanent native ads places + * tags: + * - Ad places + * responses: + * '200': + * description: Domain - rules list dictionary + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PermanentAdPlacesRulesDictionary' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * /api/slise-ad-rules/ad-places/permanent-native/{domain}/raw: + * get: + * summary: Get all rules for permanent native ads places for the specified domain + * tags: + * - 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/PermanentAdPlacesRule' + * '500': + * $ref: '#/components/responses/ErrorResponse' * /api/slise-ad-rules/ad-places/permanent-native/{domain}: * get: - * summary: Get rules for permanent native ads places for the specified domain + * summary: Get rules for permanent native ads places for the specified domain filtered by extension version * tags: - * - Slise ad places + * - Ad places * parameters: * - in: path * name: domain @@ -282,6 +334,12 @@ export const sliseAdPlacesRulesRouter = Router(); * type: string * format: hostname * example: 'etherscan.io' + * - in: query + * name: extVersion + * schema: + * type: string + * default: '0.0.0' + * description: The extension version for which the rules should be returned * responses: * '200': * description: Rules list @@ -290,27 +348,34 @@ export const sliseAdPlacesRulesRouter = Router(); * schema: * type: array * items: - * $ref: '#/components/schemas/PermanentSliseAdPlacesRule' + * $ref: '#/components/schemas/PermanentAdPlacesRule' * '500': * $ref: '#/components/responses/ErrorResponse' * /api/slise-ad-rules/ad-places/permanent-native: * get: - * summary: Get all rules for permanent native ads places + * summary: Get all rules for permanent native ads places filtered by extension version * tags: - * - Slise ad places + * - Ad places + * parameters: + * - in: query + * name: extVersion + * schema: + * type: string + * default: '0.0.0' + * description: The extension version for which the rules should be returned * responses: * '200': * description: Domain - rules list dictionary * content: * application/json: * schema: - * $ref: '#/components/schemas/PermanentSliseAdPlacesRulesDictionary' + * $ref: '#/components/schemas/PermanentAdPlacesRulesDictionary' * '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 + * - Ad places * security: * - basicAuth: [] * requestBody: @@ -318,7 +383,7 @@ export const sliseAdPlacesRulesRouter = Router(); * content: * application/json: * schema: - * $ref: '#/components/schemas/PermanentSliseAdPlacesRulesDictionary' + * $ref: '#/components/schemas/PermanentAdPlacesRulesDictionary' * responses: * '200': * $ref: '#/components/responses/SuccessResponse' @@ -331,7 +396,7 @@ export const sliseAdPlacesRulesRouter = Router(); * delete: * summary: Remove rules for permanent ads places * tags: - * - Slise ad places + * - Ad places * security: * - basicAuth: [] * requestBody: @@ -355,23 +420,61 @@ export const sliseAdPlacesRulesRouter = Router(); * '500': * $ref: '#/components/responses/ErrorResponse' */ -addObjectStorageMethodsToRouter( - sliseAdPlacesRulesRouter, - '/permanent-native', - permanentNativeAdPlacesMethods, - 'domain', - permanentSliseAdPlacesRulesDictionarySchema, - hostnamesListSchema, - entriesCount => `${entriesCount} entries have been removed` -); +addObjectStorageMethodsToRouter(adPlacesRulesRouter, { + path: '/permanent-native', + methods: permanentNativeAdPlacesMethods, + keyName: 'domain', + objectValidationSchema: permanentAdPlacesRulesDictionarySchema, + keysArrayValidationSchema: hostnamesListSchema, + successfulRemovalMessage: entriesCount => `${entriesCount} entries have been removed`, + transformGotValueFn: (value, req) => filterByVersion(value, req.query.extVersion as string | undefined) +}); /** * @swagger + * /api/slise-ad-rules/ad-places/permanent/raw/all: + * get: + * summary: Get all rules for permanent ads places + * tags: + * - Ad places + * responses: + * '200': + * description: Domain - rules list dictionary + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PermanentAdPlacesRulesDictionary' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * /api/slise-ad-rules/ad-places/permanent/{domain}/raw: + * get: + * summary: Get all rules for permanent ads places for the specified domain + * tags: + * - 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/PermanentAdPlacesRule' + * '500': + * $ref: '#/components/responses/ErrorResponse' * /api/slise-ad-rules/ad-places/permanent/{domain}: * get: - * summary: Get rules for permanent ads places for the specified domain + * summary: Get rules for permanent ads places for the specified domain filtered by extension version * tags: - * - Slise ad places + * - Ad places * parameters: * - in: path * name: domain @@ -380,6 +483,12 @@ addObjectStorageMethodsToRouter( * type: string * format: hostname * example: 'etherscan.io' + * - in: query + * name: extVersion + * schema: + * type: string + * default: '0.0.0' + * description: The extension version for which the rules should be returned * responses: * '200': * description: Rules list @@ -388,27 +497,34 @@ addObjectStorageMethodsToRouter( * schema: * type: array * items: - * $ref: '#/components/schemas/PermanentSliseAdPlacesRule' + * $ref: '#/components/schemas/PermanentAdPlacesRule' * '500': * $ref: '#/components/responses/ErrorResponse' * /api/slise-ad-rules/ad-places/permanent: * get: - * summary: Get all rules for permanent ads places + * summary: Get all rules for permanent ads places filtered by extension version * tags: - * - Slise ad places + * - Ad places + * parameters: + * - in: query + * name: extVersion + * schema: + * type: string + * default: '0.0.0' + * description: The extension version for which the rules should be returned * responses: * '200': * description: Domain - rules list dictionary * content: * application/json: * schema: - * $ref: '#/components/schemas/PermanentSliseAdPlacesRulesDictionary' + * $ref: '#/components/schemas/PermanentAdPlacesRulesDictionary' * '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 + * - Ad places * security: * - basicAuth: [] * requestBody: @@ -416,7 +532,7 @@ addObjectStorageMethodsToRouter( * content: * application/json: * schema: - * $ref: '#/components/schemas/PermanentSliseAdPlacesRulesDictionary' + * $ref: '#/components/schemas/PermanentAdPlacesRulesDictionary' * responses: * '200': * $ref: '#/components/responses/SuccessResponse' @@ -429,7 +545,7 @@ addObjectStorageMethodsToRouter( * delete: * summary: Remove rules for permanent ads places * tags: - * - Slise ad places + * - Ad places * security: * - basicAuth: [] * requestBody: @@ -453,23 +569,61 @@ addObjectStorageMethodsToRouter( * '500': * $ref: '#/components/responses/ErrorResponse' */ -addObjectStorageMethodsToRouter( - sliseAdPlacesRulesRouter, - '/permanent', - permanentSliseAdPlacesMethods, - 'domain', - permanentSliseAdPlacesRulesDictionarySchema, - hostnamesListSchema, - entriesCount => `${entriesCount} entries have been removed` -); +addObjectStorageMethodsToRouter(adPlacesRulesRouter, { + path: '/permanent', + methods: permanentAdPlacesMethods, + keyName: 'domain', + objectValidationSchema: permanentAdPlacesRulesDictionarySchema, + keysArrayValidationSchema: hostnamesListSchema, + successfulRemovalMessage: entriesCount => `${entriesCount} entries have been removed`, + transformGotValueFn: (value, req) => filterByVersion(value, req.query.extVersion as string | undefined) +}); /** * @swagger + * /api/slise-ad-rules/ad-places/raw/all: + * get: + * summary: Get all rules for ads places + * tags: + * - Ad places + * responses: + * '200': + * description: Domain - rules list dictionary + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AdPlacesRulesDictionary' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * /api/slise-ad-rules/ad-places/{domain}/raw: + * get: + * summary: Get all rules for ads places for the specified domain + * tags: + * - Ad places + * parameters: + * - in: path + * name: domain + * required: true + * schema: + * type: string + * format: hostname + * example: 'goerli.etherscan.io' + * responses: + * '200': + * description: Rules list + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/AdPlacesRule' + * '500': + * $ref: '#/components/responses/ErrorResponse' * /api/slise-ad-rules/ad-places/{domain}: * get: - * summary: Get rules for ads places for the specified domain + * summary: Get rules for ads places for the specified domain filtered by extension version * tags: - * - Slise ad places + * - Ad places * parameters: * - in: path * name: domain @@ -478,6 +632,12 @@ addObjectStorageMethodsToRouter( * type: string * format: hostname * example: 'goerli.etherscan.io' + * - in: query + * name: extVersion + * schema: + * type: string + * default: '0.0.0' + * description: The extension version for which the rules should be returned * responses: * '200': * description: Rules list @@ -486,27 +646,34 @@ addObjectStorageMethodsToRouter( * schema: * type: array * items: - * $ref: '#/components/schemas/SliseAdPlacesRule' + * $ref: '#/components/schemas/AdPlacesRule' * '500': * $ref: '#/components/responses/ErrorResponse' * /api/slise-ad-rules/ad-places: * get: - * summary: Get all rules for ads places + * summary: Get all rules for ads places filtered by extension version * tags: - * - Slise ad places + * - Ad places + * parameters: + * - in: query + * name: extVersion + * schema: + * type: string + * default: '0.0.0' + * description: The extension version for which the rules should be returned * responses: * '200': * description: Domain - rules list dictionary * content: * application/json: * schema: - * $ref: '#/components/schemas/SliseAdPlacesRulesDictionary' + * $ref: '#/components/schemas/AdPlacesRulesDictionary' * '500': * $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 + * - Ad places * security: * - basicAuth: [] * requestBody: @@ -514,7 +681,7 @@ addObjectStorageMethodsToRouter( * content: * application/json: * schema: - * $ref: '#/components/schemas/SliseAdPlacesRulesDictionary' + * $ref: '#/components/schemas/AdPlacesRulesDictionary' * responses: * '200': * $ref: '#/components/responses/SuccessResponse' @@ -527,7 +694,7 @@ addObjectStorageMethodsToRouter( * delete: * summary: Remove rules for ads places * tags: - * - Slise ad places + * - Ad places * security: * - basicAuth: [] * requestBody: @@ -551,12 +718,12 @@ addObjectStorageMethodsToRouter( * '500': * $ref: '#/components/responses/ErrorResponse' */ -addObjectStorageMethodsToRouter( - sliseAdPlacesRulesRouter, - '/', - sliseAdPlacesRulesMethods, - 'domain', - sliseAdPlacesRulesDictionarySchema, - hostnamesListSchema, - entriesCount => `${entriesCount} entries have been removed` -); +addObjectStorageMethodsToRouter(adPlacesRulesRouter, { + path: '/', + methods: adPlacesRulesMethods, + keyName: 'domain', + objectValidationSchema: adPlacesRulesDictionarySchema, + keysArrayValidationSchema: hostnamesListSchema, + successfulRemovalMessage: entriesCount => `${entriesCount} entries have been removed`, + transformGotValueFn: (value, req) => filterByVersion(value, req.query.extVersion as string | undefined) +}); diff --git a/src/routers/slise-ad-rules/index.ts b/src/routers/slise-ad-rules/index.ts index 0dca519..a56c849 100644 --- a/src/routers/slise-ad-rules/index.ts +++ b/src/routers/slise-ad-rules/index.ts @@ -1,7 +1,7 @@ import { Router } from 'express'; -import { sliseAdPlacesRulesRouter } from './ad-places'; -import { sliseAdProvidersRouter } from './providers'; +import { adPlacesRulesRouter } from './ad-places'; +import { adProvidersRouter } from './providers'; /** * @swagger @@ -37,7 +37,7 @@ import { sliseAdProvidersRouter } from './providers'; * type: string */ -export const sliseRulesRouter = Router(); +export const adRulesRouter = Router(); -sliseRulesRouter.use('/ad-places', sliseAdPlacesRulesRouter); -sliseRulesRouter.use('/providers', sliseAdProvidersRouter); +adRulesRouter.use('/ad-places', adPlacesRulesRouter); +adRulesRouter.use('/providers', adProvidersRouter); diff --git a/src/routers/slise-ad-rules/providers.ts b/src/routers/slise-ad-rules/providers.ts index 8c95059..e281600 100644 --- a/src/routers/slise-ad-rules/providers.ts +++ b/src/routers/slise-ad-rules/providers.ts @@ -1,19 +1,21 @@ import { Router } from 'express'; import { - addSliseAdProvidersForAllSites, - getSliseAdProvidersForAllSites, - removeSliseAdProvidersForAllSites, - sliseAdProvidersMethods, - sliseAdProvidersByDomainRulesMethods -} from '../../advertising/slise'; + addAdProvidersForAllSites, + getAdProvidersForAllSites, + removeAdProvidersForAllSites, + adProvidersMethods, + adProvidersByDomainRulesMethods, + AdProviderSelectorsRule, + filterByVersion +} from '../../advertising/external-ads'; import { basicAuth } from '../../middlewares/basic-auth.middleware'; import { addObjectStorageMethodsToRouter, withBodyValidation, withExceptionHandler } from '../../utils/express-helpers'; import { adTypesListSchema, hostnamesListSchema, - sliseAdProvidersByDomainsRulesDictionarySchema, - sliseAdProvidersDictionarySchema + adProvidersByDomainsRulesDictionarySchema, + adProvidersDictionarySchema } from '../../utils/schemas'; /** @@ -22,7 +24,7 @@ import { * name: Known ads providers * components: * schemas: - * SliseAdProvidersByDomainRule: + * AdProvidersByDomainRule: * type: object * required: * - urlRegexes @@ -43,12 +45,12 @@ import { * providers: * - 'coinzilla' * - 'bitmedia' - * SliseAdProvidersByDomainsRulesDictionary: + * AdProvidersByDomainsRulesDictionary: * type: object * additionalProperties: * type: array * items: - * $ref: '#/components/schemas/SliseAdProvidersByDomainRule' + * $ref: '#/components/schemas/AdProvidersByDomainRule' * example: * polygonscan.com: * - urlRegexes: @@ -56,7 +58,18 @@ import { * providers: * - 'coinzilla' * - 'bitmedia' - * SliseAdProvidersDictionary: + * AdProvidersInputValue: + * allOf: + * - $ref: '#/components/schemas/ExtVersionConstraints' + * - type: object + * required: + * - selectors + * properties: + * selectors: + * type: array + * items: + * type: string + * AdProvidersDictionary: * type: object * additionalProperties: * type: array @@ -67,9 +80,15 @@ import { * - '#Ads_google_bottom_wide' * - '.GoogleAdInfo' * - 'a[href^="https://googleads.g.doubleclick.net/pcs/click"]' + * AdProvidersInputsDictionary: + * type: object + * additionalProperties: + * type: array + * items: + * $ref: '#/components/schemas/AdProvidersInputValue' */ -export const sliseAdProvidersRouter = Router(); +export const adProvidersRouter = Router(); /** * @swagger @@ -147,11 +166,11 @@ export const sliseAdProvidersRouter = Router(); * '500': * $ref: '#/components/responses/ErrorResponse' */ -sliseAdProvidersRouter +adProvidersRouter .route('/all-sites') .get( withExceptionHandler(async (_req, res) => { - const providers = await getSliseAdProvidersForAllSites(); + const providers = await getAdProvidersForAllSites(); res.status(200).send(providers); }) @@ -160,7 +179,7 @@ sliseAdProvidersRouter basicAuth, withExceptionHandler( withBodyValidation(adTypesListSchema, async (req, res) => { - const providersAddedCount = await addSliseAdProvidersForAllSites(req.body); + const providersAddedCount = await addAdProvidersForAllSites(req.body); res.status(200).send({ message: `${providersAddedCount} providers have been added` }); }) @@ -170,7 +189,7 @@ sliseAdProvidersRouter basicAuth, withExceptionHandler( withBodyValidation(adTypesListSchema, async (req, res) => { - const providersRemovedCount = await removeSliseAdProvidersForAllSites(req.body); + const providersRemovedCount = await removeAdProvidersForAllSites(req.body); res.status(200).send({ message: `${providersRemovedCount} providers have been removed` }); }) @@ -200,7 +219,7 @@ sliseAdProvidersRouter * schema: * type: array * items: - * $ref: '#/components/schemas/SliseAdProvidersByDomainRule' + * $ref: '#/components/schemas/AdProvidersByDomainRule' * '500': * $ref: '#/components/responses/ErrorResponse' * /api/slise-ad-rules/providers/by-sites: @@ -214,7 +233,7 @@ sliseAdProvidersRouter * content: * application/json: * schema: - * $ref: '#/components/schemas/SliseAdProvidersByDomainsRulesDictionary' + * $ref: '#/components/schemas/AdProvidersByDomainsRulesDictionary' * '500': * $ref: '#/components/responses/ErrorResponse' * post: @@ -230,7 +249,7 @@ sliseAdProvidersRouter * content: * application/json: * schema: - * $ref: '#/components/schemas/SliseAdProvidersByDomainsRulesDictionary' + * $ref: '#/components/schemas/AdProvidersByDomainsRulesDictionary' * responses: * '200': * $ref: '#/components/responses/SuccessResponse' @@ -268,21 +287,57 @@ sliseAdProvidersRouter * '500': * $ref: '#/components/responses/ErrorResponse' */ -addObjectStorageMethodsToRouter( - sliseAdProvidersRouter, - '/by-sites', - sliseAdProvidersByDomainRulesMethods, - 'domain', - sliseAdProvidersByDomainsRulesDictionarySchema, - hostnamesListSchema, - entriesCount => `${entriesCount} entries have been removed` -); +addObjectStorageMethodsToRouter(adProvidersRouter, { + path: '/by-sites', + methods: adProvidersByDomainRulesMethods, + keyName: 'domain', + objectValidationSchema: adProvidersByDomainsRulesDictionarySchema, + keysArrayValidationSchema: hostnamesListSchema, + successfulRemovalMessage: entriesCount => `${entriesCount} entries have been removed` +}); /** * @swagger + * /api/slise-ad-rules/providers/raw/all: + * get: + * summary: Get selectors for all providers and all extensions versions + * tags: + * - Known ads providers + * responses: + * '200': + * description: Provider - selectors dictionary + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/AdProvidersInputsDictionary' + * '500': + * $ref: '#/components/responses/ErrorResponse' + * /api/slise-ad-rules/providers/{providerId}/raw: + * get: + * summary: Get selectors for a provider for all extensions versions + * tags: + * - Known ads providers + * parameters: + * - in: path + * name: providerId + * required: true + * schema: + * type: string + * example: 'google' + * responses: + * '200': + * description: Lists of CSS selectors for all extension versions + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/AdProvidersInputValue' + * '500': + * $ref: '#/components/responses/ErrorResponse' * /api/slise-ad-rules/providers/{providerId}: * get: - * summary: Get selectors for a provider + * summary: Get selectors for a provider filtered by extension version * tags: * - Known ads providers * parameters: @@ -292,6 +347,12 @@ addObjectStorageMethodsToRouter( * schema: * type: string * example: 'google' + * - in: query + * name: extVersion + * schema: + * type: string + * default: '0.0.0' + * description: The extension version for which the rules should be returned * responses: * '200': * description: List of CSS selectors @@ -309,16 +370,23 @@ addObjectStorageMethodsToRouter( * $ref: '#/components/responses/ErrorResponse' * /api/slise-ad-rules/providers: * get: - * summary: Get all providers + * summary: Get selectors for all providers filtered by extension version * tags: * - Known ads providers + * parameters: + * - in: query + * name: extVersion + * schema: + * type: string + * default: '0.0.0' + * description: The extension version for which the rules should be returned * responses: * '200': * description: Provider - selectors dictionary * content: * application/json: * schema: - * $ref: '#/components/schemas/SliseAdProvidersDictionary' + * $ref: '#/components/schemas/AdProvidersDictionary' * '500': * $ref: '#/components/responses/ErrorResponse' * post: @@ -332,7 +400,7 @@ addObjectStorageMethodsToRouter( * content: * application/json: * schema: - * $ref: '#/components/schemas/SliseAdProvidersDictionary' + * $ref: '#/components/schemas/AdProvidersInputsDictionary' * responses: * '200': * $ref: '#/components/responses/SuccessResponse' @@ -369,12 +437,19 @@ addObjectStorageMethodsToRouter( * '500': * $ref: '#/components/responses/ErrorResponse' */ -addObjectStorageMethodsToRouter( - sliseAdProvidersRouter, - '/', - sliseAdProvidersMethods, - 'providerId', - sliseAdProvidersDictionarySchema, - adTypesListSchema, - entriesCount => `${entriesCount} providers have been removed` -); +addObjectStorageMethodsToRouter(adProvidersRouter, { + path: '/', + methods: adProvidersMethods, + keyName: 'providerId', + objectValidationSchema: adProvidersDictionarySchema, + keysArrayValidationSchema: adTypesListSchema, + successfulRemovalMessage: entriesCount => `${entriesCount} providers have been removed`, + transformGotValueFn: (rules, req) => + Array.from( + new Set( + filterByVersion(rules, req.query.extVersion as string | undefined) + .map(({ selectors }) => selectors) + .flat() + ) + ) +}); diff --git a/src/utils/express-helpers.ts b/src/utils/express-helpers.ts index 8a6e05b..c514977 100644 --- a/src/utils/express-helpers.ts +++ b/src/utils/express-helpers.ts @@ -21,6 +21,7 @@ export const withBodyValidation = (schema: Schema, handler: TypedBodyRequestHandler): RequestHandler => async (req, res, next) => { try { + console.log('oy vey 1', JSON.stringify(req.body), JSON.stringify(await schema.validate(req.body))); req.body = await schema.validate(req.body); } catch (error) { if (error instanceof ValidationError) { @@ -44,17 +45,41 @@ export const withExceptionHandler = } }; -export const addObjectStorageMethodsToRouter = ( +interface ObjectStorageMethodsEntrypointsConfig { + path: string; + methods: ObjectStorageMethods; + keyName: string; + objectValidationSchema: IObjectSchema>; + keysArrayValidationSchema: IArraySchema; + successfulRemovalMessage: (removedEntriesCount: number) => string; + transformGotValueFn?: (value: U, req: Request) => V; +} + +export const addObjectStorageMethodsToRouter = ( router: Router, - path: string, - methods: ObjectStorageMethods, - keyName: string, - objectValidationSchema: IObjectSchema>, - keysArrayValidationSchema: IArraySchema, - successfulRemovalMessage: (removedEntriesCount: number) => string + config: ObjectStorageMethodsEntrypointsConfig ) => { + const { + path, + methods, + keyName, + objectValidationSchema, + keysArrayValidationSchema, + successfulRemovalMessage, + transformGotValueFn = value => value as unknown as V + } = config; + router.get( - path === '/' ? `/:${keyName}` : `${path}/:${keyName}`, + path === '/' ? '/raw/all' : `${path}/raw/all`, + withExceptionHandler(async (_req, res) => { + const values = await methods.getAllValues(); + + res.status(200).send(values); + }) + ); + + router.get( + path === '/' ? `/:${keyName}/raw` : `${path}/:${keyName}/raw`, withExceptionHandler(async (req, res) => { const { [keyName]: key } = req.params; @@ -64,13 +89,28 @@ export const addObjectStorageMethodsToRouter = ( }) ); + router.get( + path === '/' ? `/:${keyName}` : `${path}/:${keyName}`, + withExceptionHandler(async (req, res) => { + const { [keyName]: key } = req.params; + + const value = await methods.getByKey(key); + + res.status(200).send(transformGotValueFn(value, req)); + }) + ); + router .route(path) .get( - withExceptionHandler(async (_req, res) => { + withExceptionHandler(async (req, res) => { const values = await methods.getAllValues(); - res.status(200).send(values); + res + .status(200) + .send( + Object.fromEntries(Object.entries(values).map(([key, value]) => [key, transformGotValueFn(value, req)])) + ); }) ) .post( diff --git a/src/utils/schemas.ts b/src/utils/schemas.ts index d8bff7e..7533c51 100644 --- a/src/utils/schemas.ts +++ b/src/utils/schemas.ts @@ -1,3 +1,4 @@ +import { validRange as getValidatedRange } from 'semver'; import { array as arraySchema, ArraySchema as IArraySchema, @@ -11,12 +12,13 @@ import { } from 'yup'; import { - SliseAdPlacesRule, - PermanentSliseAdPlacesRule, - SliseAdProvidersByDomainRule, + AdPlacesRule, + PermanentAdPlacesRule, + AdProvidersByDomainRule, StylePropName, - stylePropsNames -} from '../advertising/slise'; + stylePropsNames, + AdProviderSelectorsRule +} from '../advertising/external-ads'; import { isValidSelectorsGroup } from '../utils/selectors.min.js'; import { isDefined } from './helpers'; @@ -46,7 +48,7 @@ const makeDictionarySchema = (keySchema: IStringSchema, valueSchema: Schema> = makeDictionary stringSchema().required() ); -const sliseAdStylesOverridesSchema = objectSchema().shape({ +const adStylesOverridesSchema = objectSchema().shape({ parentDepth: numberSchema().integer().min(0).required(), style: styleSchema.clone().required() }); -const sliseAdPlacesRulesSchema = arraySchema() +const adPlacesRulesSchema = arraySchema() .of( objectSchema() .shape({ @@ -115,17 +121,20 @@ const sliseAdPlacesRulesSchema = arraySchema() divWrapperStyle: styleSchema }) .required(), - stylesOverrides: arraySchema().of(sliseAdStylesOverridesSchema.clone().required()), - shouldHideOriginal: booleanSchema().default(false) + stylesOverrides: arraySchema().of(adStylesOverridesSchema.clone().required()), + shouldHideOriginal: booleanSchema(), + extVersion: versionRangeSchema.clone().required() }) .required() ) .required(); -export const sliseAdPlacesRulesDictionarySchema: IObjectSchema> = - makeDictionarySchema(hostnameSchema, sliseAdPlacesRulesSchema).required(); +export const adPlacesRulesDictionarySchema: IObjectSchema> = makeDictionarySchema( + hostnameSchema, + adPlacesRulesSchema +).required(); -const permanentSliseAdPlacesRulesSchema = arraySchema() +const permanentAdPlacesRulesSchema = arraySchema() .of( objectSchema() .shape({ @@ -147,15 +156,16 @@ const permanentSliseAdPlacesRulesSchema = arraySchema() insertionIndex: numberSchema().integer(), insertBeforeSelector: cssSelectorSchema, insertAfterSelector: cssSelectorSchema, - insertionsCount: numberSchema().integer().min(1).default(1), + insertionsCount: numberSchema().integer().min(1), shouldUseDivWrapper: booleanSchema().required(), elementStyle: styleSchema, divWrapperStyle: styleSchema, elementToMeasureSelector: cssSelectorSchema, - stylesOverrides: arraySchema().of(sliseAdStylesOverridesSchema.clone().required()), - shouldHideOriginal: booleanSchema().default(false) + stylesOverrides: arraySchema().of(adStylesOverridesSchema.clone().required()), + shouldHideOriginal: booleanSchema(), + extVersion: versionRangeSchema.clone().required() }) - .test('insertion-place-specified', (value: PermanentSliseAdPlacesRule | undefined) => { + .test('insertion-place-specified', (value: PermanentAdPlacesRule | undefined) => { if (!value) { return true; } @@ -175,25 +185,31 @@ const permanentSliseAdPlacesRulesSchema = arraySchema() ) .required(); -export const permanentSliseAdPlacesRulesDictionarySchema: IObjectSchema> = - makeDictionarySchema(hostnameSchema, permanentSliseAdPlacesRulesSchema).required(); +export const permanentAdPlacesRulesDictionarySchema: IObjectSchema> = + makeDictionarySchema(hostnameSchema, permanentAdPlacesRulesSchema).required(); -const sliseAdProvidersByDomainRulesSchema = arraySchema() +const adProvidersByDomainRulesSchema = arraySchema() .of( objectSchema() .shape({ urlRegexes: arraySchema().of(regexStringSchema.clone().required()).required(), - providers: arraySchema().of(stringSchema().required()).required() + providers: arraySchema().of(stringSchema().required()).required(), + extVersion: versionRangeSchema.clone().required() }) .required() ) .required(); -export const sliseAdProvidersByDomainsRulesDictionarySchema: IObjectSchema< - Record -> = makeDictionarySchema(hostnameSchema, sliseAdProvidersByDomainRulesSchema).required(); +export const adProvidersByDomainsRulesDictionarySchema: IObjectSchema> = + makeDictionarySchema(hostnameSchema, adProvidersByDomainRulesSchema).required(); -export const sliseAdProvidersDictionarySchema: IObjectSchema> = makeDictionarySchema( - adTypeSchema.clone().required(), - cssSelectorsListSchema.clone().required() -).required(); +const adProvidersSelectorsRuleSchema = objectSchema().shape({ + selectors: cssSelectorsListSchema.clone().required(), + extVersion: versionRangeSchema.clone().required() +}); + +export const adProvidersDictionarySchema: IObjectSchema> = + makeDictionarySchema( + adTypeSchema.clone().required(), + arraySchema().of(adProvidersSelectorsRuleSchema.clone().required()).required() + ).required(); diff --git a/yarn.lock b/yarn.lock index 4649e53..44c2709 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4014,6 +4014,13 @@ semver@^7.2.1, semver@^7.3.7, semver@^7.3.8: dependencies: lru-cache "^6.0.0" +semver@^7.6.0: + version "7.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== + dependencies: + lru-cache "^6.0.0" + send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"