-
-
Notifications
You must be signed in to change notification settings - Fork 229
/
Copy pathmultiDim.ts
331 lines (313 loc) · 10.9 KB
/
multiDim.ts
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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
import { uniq } from "lodash"
import {
defaultGrapherConfig,
migrateGrapherConfigToLatestVersion,
} from "@ourworldindata/grapher"
import {
Base64String,
ChartConfigsTableName,
DbPlainMultiDimDataPage,
DbPlainMultiDimXChartConfig,
DbRawChartConfig,
GrapherInterface,
IndicatorConfig,
IndicatorEntryBeforePreProcessing,
IndicatorsBeforePreProcessing,
MultiDimDataPageConfigEnriched,
MultiDimDataPageConfigPreProcessed,
MultiDimDataPageConfigRaw,
MultiDimDataPagesTableName,
MultiDimDimensionChoices,
MultiDimXChartConfigsTableName,
View,
} from "@ourworldindata/types"
import {
mergeGrapherConfigs,
MultiDimDataPageConfig,
slugify,
} from "@ourworldindata/utils"
import * as db from "../db/db.js"
import {
isMultiDimDataPagePublished,
upsertMultiDimDataPage,
} from "../db/model/MultiDimDataPage.js"
import { upsertMultiDimXChartConfigs } from "../db/model/MultiDimXChartConfigs.js"
import {
getMergedGrapherConfigsForVariables,
getVariableIdsByCatalogPath,
} from "../db/model/Variable.js"
import {
deleteGrapherConfigFromR2ByUUID,
saveMultiDimConfigToR2,
} from "./chartConfigR2Helpers.js"
import {
saveNewChartConfigInDbAndR2,
updateChartConfigInDbAndR2,
} from "./chartConfigHelpers.js"
function dimensionsToViewId(dimensions: MultiDimDimensionChoices) {
return Object.entries(dimensions)
.sort(([keyA], [keyB]) => keyA.localeCompare(keyB))
.map(([_, value]) => slugify(value))
.join("__")
.toLowerCase()
}
function catalogPathFromIndicatorEntry(
entry: IndicatorEntryBeforePreProcessing
): string | undefined {
if (typeof entry === "string") return entry
if (typeof entry === "object" && "catalogPath" in entry) {
return entry.catalogPath
}
return undefined
}
function getAllCatalogPaths(views: View<IndicatorsBeforePreProcessing>[]) {
const paths = []
for (const view of views) {
const { y, x, size, color } = view.indicators
if (y) {
if (Array.isArray(y)) {
paths.push(...y.map(catalogPathFromIndicatorEntry))
} else {
paths.push(catalogPathFromIndicatorEntry(y))
}
}
for (const entry of [x, size, color]) {
if (entry) paths.push(catalogPathFromIndicatorEntry(entry))
}
}
return paths.filter((path) => path !== undefined)
}
async function resolveMultiDimDataPageCatalogPathsToIndicatorIds(
knex: db.KnexReadonlyTransaction,
rawConfig: MultiDimDataPageConfigRaw
): Promise<MultiDimDataPageConfigPreProcessed> {
const allCatalogPaths = getAllCatalogPaths(rawConfig.views)
const catalogPathToIndicatorIdMap = await getVariableIdsByCatalogPath(
allCatalogPaths,
knex
)
const missingCatalogPaths = new Set(
allCatalogPaths.filter(
(indicator) => !catalogPathToIndicatorIdMap.has(indicator)
)
)
if (missingCatalogPaths.size > 0) {
throw new Error(
`Could not find the following catalog paths for MDD ${rawConfig.title} in the database: ${Array.from(
missingCatalogPaths
).join(", ")}`
)
}
function resolveSingleField(
indicator?: IndicatorEntryBeforePreProcessing
): IndicatorConfig | undefined {
switch (typeof indicator) {
case "number":
return { id: indicator }
case "string": {
const id = catalogPathToIndicatorIdMap.get(indicator)
return id ? { id } : undefined
}
case "object": {
if ("id" in indicator) return indicator
if ("catalogPath" in indicator) {
const id = catalogPathToIndicatorIdMap.get(
indicator.catalogPath
)
return id ? { ...indicator, id } : undefined
}
return undefined
}
default:
return undefined
}
}
function resolveSingleOrArrayField(
indicator:
| IndicatorEntryBeforePreProcessing
| IndicatorEntryBeforePreProcessing[]
) {
const indicatorIds = []
if (Array.isArray(indicator)) {
for (const item of indicator) {
const resolved = resolveSingleField(item)
if (resolved) indicatorIds.push(resolved)
}
} else {
const resolved = resolveSingleField(indicator)
if (resolved) indicatorIds.push(resolved)
}
return indicatorIds
}
return {
...rawConfig,
views: rawConfig.views.map((view) => ({
...view,
indicators: {
y: resolveSingleOrArrayField(view.indicators.y),
x: resolveSingleField(view.indicators.x),
size: resolveSingleField(view.indicators.size),
color: resolveSingleField(view.indicators.color),
},
})),
}
}
async function getViewIdToChartConfigIdMap(
knex: db.KnexReadonlyTransaction,
slug: string
) {
const rows = await db.knexRaw<DbPlainMultiDimXChartConfig>(
knex,
`-- sql
SELECT viewId, chartConfigId
FROM multi_dim_x_chart_configs mdxcc
JOIN multi_dim_data_pages mddp ON mddp.id = mdxcc.multiDimId
WHERE mddp.slug = ?`,
[slug]
)
return new Map(
rows.map((row) => [row.viewId, row.chartConfigId as Base64String])
)
}
async function saveMultiDimConfig(
knex: db.KnexReadWriteTransaction,
slug: string,
config: MultiDimDataPageConfigEnriched
) {
const id = await upsertMultiDimDataPage(knex, {
slug,
config: JSON.stringify(config),
})
if (id === 0) {
// There are no updates to the config, return the existing id.
console.debug(`There are no changes to multi dim config slug=${slug}`)
const result = await knex<DbPlainMultiDimDataPage>(
MultiDimDataPagesTableName
)
.select("id")
.where({ slug })
.first()
return result!.id
}
// We need to get the full config and the md5 hash from the database instead of
// computing our own md5 hash because MySQL normalizes JSON and our
// client computed md5 would be different from the ones computed by and stored in R2
const result = await knex<DbPlainMultiDimDataPage>(
MultiDimDataPagesTableName
)
.select("config", "configMd5")
.where({ id })
.first()
const { config: normalizedConfig, configMd5 } = result!
await saveMultiDimConfigToR2(normalizedConfig, slug, configMd5)
return id
}
async function cleanUpOrphanedChartConfigs(
knex: db.KnexReadWriteTransaction,
orphanedChartConfigIds: string[]
) {
await knex<DbPlainMultiDimXChartConfig>(MultiDimXChartConfigsTableName)
.whereIn("chartConfigId", orphanedChartConfigIds)
.delete()
await knex<DbRawChartConfig>(ChartConfigsTableName)
.whereIn("id", orphanedChartConfigIds)
.delete()
for (const id of orphanedChartConfigIds) {
await deleteGrapherConfigFromR2ByUUID(id)
}
}
export async function createMultiDimConfig(
knex: db.KnexReadWriteTransaction,
slug: string,
rawConfig: MultiDimDataPageConfigRaw
): Promise<number> {
const config = await resolveMultiDimDataPageCatalogPathsToIndicatorIds(
knex,
rawConfig
)
const variableConfigs = await getMergedGrapherConfigsForVariables(
knex,
uniq(config.views.map((view) => view.indicators.y[0].id))
)
const existingViewIdsToChartConfigIds = await getViewIdToChartConfigIdMap(
knex,
slug
)
const reusedChartConfigIds = new Set<string>()
const { grapherConfigSchema } = config
// TODO: Remove when we build a way to publish mdims in the admin.
const isPublished = await isMultiDimDataPagePublished(knex, slug)
const enrichedViews = await Promise.all(
config.views.map(async (view) => {
const variableId = view.indicators.y[0].id
// Main config for each view.
const mainGrapherConfig: GrapherInterface = {
$schema: defaultGrapherConfig.$schema,
dimensions: MultiDimDataPageConfig.viewToDimensionsConfig(view),
selectedEntityNames: config.defaultSelection ?? [],
slug,
}
if (isPublished) {
mainGrapherConfig.isPublished = true
}
let viewGrapherConfig = {}
if (view.config) {
viewGrapherConfig = grapherConfigSchema
? { $schema: grapherConfigSchema, ...view.config }
: view.config
if ("$schema" in viewGrapherConfig) {
viewGrapherConfig =
migrateGrapherConfigToLatestVersion(viewGrapherConfig)
}
}
const patchGrapherConfig = mergeGrapherConfigs(
viewGrapherConfig,
mainGrapherConfig
)
const fullGrapherConfig = mergeGrapherConfigs(
variableConfigs.get(variableId) ?? {},
patchGrapherConfig
)
const existingChartConfigId = existingViewIdsToChartConfigIds.get(
dimensionsToViewId(view.dimensions)
)
let chartConfigId
if (existingChartConfigId) {
chartConfigId = existingChartConfigId
await updateChartConfigInDbAndR2(
knex,
chartConfigId,
patchGrapherConfig,
fullGrapherConfig
)
reusedChartConfigIds.add(chartConfigId)
console.debug(`Chart config updated id=${chartConfigId}`)
} else {
const result = await saveNewChartConfigInDbAndR2(
knex,
undefined,
patchGrapherConfig,
fullGrapherConfig
)
chartConfigId = result.chartConfigId
console.debug(`Chart config created id=${chartConfigId}`)
}
return { ...view, fullConfigId: chartConfigId }
})
)
const orphanedChartConfigIds = Array.from(
existingViewIdsToChartConfigIds.values()
).filter((id) => !reusedChartConfigIds.has(id))
await cleanUpOrphanedChartConfigs(knex, orphanedChartConfigIds)
const enrichedConfig = { ...config, views: enrichedViews }
const multiDimId = await saveMultiDimConfig(knex, slug, enrichedConfig)
for (const view of enrichedConfig.views) {
await upsertMultiDimXChartConfigs(knex, {
multiDimId,
viewId: dimensionsToViewId(view.dimensions),
variableId: view.indicators.y[0].id,
chartConfigId: view.fullConfigId,
})
}
return multiDimId
}