All files / owid-grapher/grapher/core EntityUrlBuilder.ts

90.84% Statements 119/131
85.71% Branches 12/14
100% Functions 7/7
90.84% Lines 119/131

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 1671x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 7x 7x 7x 7x 1x 1x 1x 2x 2x 2x 2x 2x 1x 1x 18x 18x 7x 7x 1x 1x 10x 10x 10x 1x 1x 623x 623x             623x 623x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 7x 7x 7x 7x 7x 7x 7x             7x 7x 7x 1x 1x 623x 623x 1x 1x 1x 1x 1x 1x 1x 1x 622x 622x 1x 1x 1x 1x                                                                        
import { EntityName } from "../../coreTable/OwidTableConstants"
import { Url } from "../../clientUtils/urls/Url"
import { codeToEntityName, entityNameToCode } from "./EntityCodes"
import {
    performUrlMigrations,
    UrlMigration,
} from "../../clientUtils/urls/UrlMigration"
 
/*
 * Migration #1: Switch from + to ~ delimited entities.
 *
 * Implemented: May 2020
 *
 * See PR discussion on how we decided on ~ (tilde): https://github.com/owid/owid-grapher/pull/446
 * And the initial issue (Facebook rewriting our URLs): https://github.com/owid/owid-grapher/issues/397
 *
 * In short:
 *
 * Facebook replaces `%20` in URLs with `+`. Before this migration we encoded
 * ["North America", "South America"] → "North%20America+South%20America".
 * Facebook would turn this into "North+America+South+America" making the delimiters and spaces
 * ambiguous.
 *
 * We chose ~ (tilde) because no entities existed in the database that contain that symbol, so the
 * existence of that symbol could be used to detect legacy URLs.
 *
 */
 
// Todo: ensure EntityName never contains the v2Delimiter
 
const V1_DELIMITER = "+"
export const ENTITY_V2_DELIMITER = "~"
 
const isV1Param = (encodedQueryParam: string): boolean => {
    // No legacy entities have a v2Delimiter in their name,
    // so if a v2Delimiter is present we know it's a v2 link.
    return !decodeURIComponent(encodedQueryParam).includes(ENTITY_V2_DELIMITER)
}
 
const entityNamesFromV1EncodedParam = (
    encodedQueryParam: string
): EntityName[] => {
    // need to use still-encoded URL params because we need to
    // distinguish between `+` and `%20` in legacy URLs
    return encodedQueryParam.split(V1_DELIMITER).map(decodeURIComponent)
}
 
const entityNamesToV2Param = (entityNames: EntityName[]): string => {
    // Always include a v2Delimiter in a v2 link. When decoding we will drop any empty strings.
    if (entityNames.length === 1) return ENTITY_V2_DELIMITER + entityNames[0]
    return entityNames.join(ENTITY_V2_DELIMITER)
}
 
const entityNamesFromV2Param = (queryParam: string): EntityName[] => {
    // Facebook turns %20 into +. v2 links will never contain a +, so we can safely replace all of them with %20.
    return queryParam.split(ENTITY_V2_DELIMITER).filter((item) => item)
}
 
const migrateV1Delimited: UrlMigration = (url) => {
    const { country } = url.encodedQueryParams
    if (country !== undefined && isV1Param(country)) {
        return url.updateQueryParams({
            country: entityNamesToV2Param(
                entityNamesFromV1EncodedParam(country)
            ),
        })
    }
    return url
}
 
/*
 * Migration #2: Drop dimension keys from selected entities.
 *
 * Implemented: March 2021
 *
 * When plotting multiple variables on a chart, it used to be possible to pick which
 * variable-entity pairs get plotted.
 *
 * For example, if you had a line chart with 3 variables: Energy consumption from Coal, Oil and Gas,
 * then you (as a user) could select individual variable-entity pairs to plot:
 *
 * - France - Coal ("FRA-0")
 * - France - Oil ("FRA-1")
 * - France - Gas ("FRA-2")
 * - ...
 *
 * The index of the dimension was appended to the entity (e.g. 0 for Coal).
 *
 * We dropped this feature in March 2021 in order to simplify the selection-handling logic, and it
 * was also, in most cases, not desirable to present users with variable-entity options.
 *
 */
 
const LegacyDimensionRegex = /\-\d+$/
 
const injectEntityNamesInLegacyDimension = (
    entityNames: EntityName[]
): EntityName[] => {
    // If an entity has the old name-dimension encoding, removing the dimension part and add it as
    // a new selection. So USA-1 becomes USA.
    const newNames: EntityName[] = []
    entityNames.forEach((entityName) => {
        newNames.push(entityName)
        if (LegacyDimensionRegex.test(entityName)) {
            const nonDimensionName = entityName.replace(
                LegacyDimensionRegex,
                ""
            )
            newNames.push(nonDimensionName)
        }
    })
    return newNames
}
 
const migrateLegacyDimensionPairs: UrlMigration = (url) => {
    const { country } = url.queryParams
    if (country) {
        return url.updateQueryParams({
            country: entityNamesToV2Param(
                injectEntityNamesInLegacyDimension(
                    entityNamesFromV2Param(country)
                )
            ),
        })
    }
    return url
}
 
/*
 * Combining all migrations
 */
 
const urlMigrations: UrlMigration[] = [
    migrateV1Delimited,
    migrateLegacyDimensionPairs,
]
 
export const migrateSelectedEntityNamesParam: UrlMigration = (
    url: Url
): Url => {
    return performUrlMigrations(urlMigrations, url)
}
 
/*
 * Accessors
 */
 
export const getSelectedEntityNamesParam = (
    url: Url
): EntityName[] | undefined => {
    const { country } = migrateSelectedEntityNamesParam(url).queryParams
    return country !== undefined
        ? entityNamesFromV2Param(country).map(codeToEntityName)
        : undefined
}
 
export const setSelectedEntityNamesParam = (
    url: Url,
    entityNames: EntityName[] | undefined
): Url => {
    return migrateSelectedEntityNamesParam(url).updateQueryParams({
        country: entityNames
            ? entityNamesToV2Param(entityNames.map(entityNameToCode))
            : undefined,
    })
}