Skip to content

Commit

Permalink
🔨 (grapher) let charts inherit defaults
Browse files Browse the repository at this point in the history
  • Loading branch information
sophiamersmann committed Jul 17, 2024
1 parent be4487e commit 659e513
Show file tree
Hide file tree
Showing 27 changed files with 864 additions and 108 deletions.
25 changes: 22 additions & 3 deletions adminSiteClient/ChartEditorPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
groupBy,
extractDetailsFromSyntax,
getIndexableKeys,
diffGrapherConfigs,
} from "@ourworldindata/utils"
import {
Topic,
Expand Down Expand Up @@ -104,7 +105,7 @@ export class ChartEditorPage
extends React.Component<{
grapherId?: number
newGrapherIndex?: number
grapherConfig?: any
grapherConfig?: GrapherInterface
}>
implements ChartEditorManager
{
Expand All @@ -124,7 +125,8 @@ export class ChartEditorPage

@observable simulateVisionDeficiency?: VisionDeficiency

fetchedGrapherConfig?: any
fetchedGrapherConfig?: GrapherInterface
baseGrapherConfig?: GrapherInterface

async fetchGrapher(): Promise<void> {
const { grapherId } = this.props
Expand All @@ -143,7 +145,13 @@ export class ChartEditorPage
}

@action.bound private updateGrapher(): void {
const config = this.fetchedGrapherConfig ?? this.props.grapherConfig
let config = this.fetchedGrapherConfig ?? this.props.grapherConfig

// if there is a base layer, update the patch instead of the full config
if (config && this.baseGrapherConfig) {
config = diffGrapherConfigs(config, this.baseGrapherConfig)
}

const grapherConfig = {
...config,
// binds the grapher instance to this.grapher
Expand All @@ -165,6 +173,16 @@ export class ChartEditorPage
this._isDbSet = true
}

async fetchBaseGrapher(): Promise<void> {
const { admin } = this.context
const { grapherId } = this.props
if (grapherId !== undefined) {
this.baseGrapherConfig = await admin.getJSON(
`/api/charts/${grapherId}.base.json`
)
}
}

async fetchData(): Promise<void> {
const { admin } = this.context

Expand Down Expand Up @@ -362,6 +380,7 @@ export class ChartEditorPage

@action.bound refresh(): void {
void this.fetchGrapher()
void this.fetchBaseGrapher()
void this.fetchDetails()
void this.fetchData()
void this.fetchLogs()
Expand Down
1 change: 1 addition & 0 deletions adminSiteClient/GrapherConfigGridEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,7 @@ export class GrapherConfigGridEditor extends React.Component<GrapherConfigGridEd
selectedRowContent.id
)

// TODO(inheritance): use mergeGrapherConfigs instead
const mergedConfig = merge(grapherConfig, finalConfigLayer)
this.loadGrapherJson(mergedConfig)
}
Expand Down
188 changes: 124 additions & 64 deletions adminSiteServer/apiRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ import {
Json,
checkIsGdocPostExcludingFragments,
checkIsPlainObjectWithGuard,
mergeGrapherConfigs,
diffGrapherConfigs,
} from "@ourworldindata/utils"
import { applyPatch } from "../adminShared/patchHelper.js"
import {
Expand Down Expand Up @@ -85,6 +87,7 @@ import {
DbRawChartConfig,
} from "@ourworldindata/types"
import {
defaultGrapherConfig,
getVariableDataRoute,
getVariableMetadataRoute,
} from "@ourworldindata/grapher"
Expand Down Expand Up @@ -268,14 +271,108 @@ const expectChartById = async (
throw new JsonError(`No chart found for id ${chartId}`, 404)
}

const getBinaryUUID = async (
knex: db.KnexReadonlyTransaction
): Promise<Buffer> => {
const { id } = (await db.knexRawFirst<{ id: Buffer }>(
const saveNewChart = async (
knex: db.KnexReadWriteTransaction,
{ config, user }: { config: GrapherInterface; user: DbPlainUser }
): Promise<GrapherInterface> => {
// if the schema version is missing, assume it's the latest
if (!config["$schema"]) {
config["$schema"] = defaultGrapherConfig["$schema"]
}

// compute patch and full configs
const baseConfig = getBaseLayerConfig()
const patchConfig = diffGrapherConfigs(config, baseConfig)
const fullConfig = mergeGrapherConfigs(baseConfig, patchConfig)

// insert patch & full configs into the chart_configs table
const configId = await db.getBinaryUUID(knex)
await db.knexRaw(
knex,
`-- sql
INSERT INTO chart_configs (id, patch, full)
VALUES (?, ?, ?)
`,
[configId, JSON.stringify(patchConfig), JSON.stringify(fullConfig)]
)

// add a new chart to the charts table
const result = await db.knexRawInsert(
knex,
`-- sql
INSERT INTO charts (configId, lastEditedAt, lastEditedByUserId)
VALUES (?, ?, ?)
`,
[configId, new Date(), user.id]
)

// The chart config itself has an id field that should store the id of the chart - update the chart now so this is true
const chartId = result.insertId
patchConfig.id = chartId
fullConfig.id = chartId
await db.knexRaw(
knex,
`-- sql
UPDATE chart_configs cc
JOIN charts c ON c.configId = cc.id
SET
cc.patch=JSON_SET(cc.patch, '$.id', ?),
cc.full=JSON_SET(cc.full, '$.id', ?)
WHERE c.id = ?
`,
[chartId, chartId, chartId]
)

return patchConfig
}

const updateExistingChart = async (
knex: db.KnexReadWriteTransaction,
{
config,
user,
chartId,
}: { config: GrapherInterface; user: DbPlainUser; chartId: number }
): Promise<GrapherInterface> => {
// make sure that the id of the incoming config matches the chart id
config.id = chartId

// if the schema version is missing, assume it's the latest
if (!config["$schema"]) {
config["$schema"] = defaultGrapherConfig["$schema"]
}

// compute patch and full configs
const baseConfig = getBaseLayerConfig()
const patchConfig = diffGrapherConfigs(config, baseConfig)
const fullConfig = mergeGrapherConfigs(baseConfig, patchConfig)

// update configs
await db.knexRaw(
knex,
`-- sql
UPDATE chart_configs cc
JOIN charts c ON c.configId = cc.id
SET
cc.patch=?,
cc.full=?
WHERE c.id = ?
`,
[JSON.stringify(patchConfig), JSON.stringify(fullConfig), chartId]
)

// update charts row
await db.knexRaw(
knex,
`SELECT UUID_TO_BIN(UUID(), 1) AS id`
))!
return id
`-- sql
UPDATE charts
SET lastEditedAt=?, lastEditedByUserId=?
WHERE id = ?
`,
[new Date(), user.id, chartId]
)

return patchConfig
}

const saveGrapher = async (
Expand Down Expand Up @@ -358,64 +455,17 @@ const saveGrapher = async (
else newConfig.version = 1

// Execute the actual database update or creation
const now = new Date()
let chartId = existingConfig && existingConfig.id
const newJsonConfig = JSON.stringify(newConfig)
let chartId: number
if (existingConfig) {
await db.knexRaw(
knex,
`-- sql
UPDATE chart_configs cc
JOIN charts c ON c.configId = cc.id
SET
cc.patch=?,
cc.full=?
WHERE c.id = ?
`,
[newJsonConfig, newJsonConfig, chartId]
)
await db.knexRaw(
knex,
`-- sql
UPDATE charts
SET lastEditedAt=?, lastEditedByUserId=?
WHERE id = ?
`,
[now, user.id, chartId]
)
chartId = existingConfig.id!
newConfig = await updateExistingChart(knex, {
config: newConfig,
user,
chartId,
})
} else {
const configId = await getBinaryUUID(knex)
await db.knexRaw(
knex,
`-- sql
INSERT INTO chart_configs (id, patch, full)
VALUES (?, ?, ?)
`,
[configId, newJsonConfig, newJsonConfig]
)
const result = await db.knexRawInsert(
knex,
`-- sql
INSERT INTO charts (configId, lastEditedAt, lastEditedByUserId)
VALUES (?, ?, ?)
`,
[configId, now, user.id]
)
chartId = result.insertId
// The chart config itself has an id field that should store the id of the chart - update the chart now so this is true
newConfig.id = chartId
await db.knexRaw(
knex,
`-- sql
UPDATE chart_configs cc
JOIN charts c ON c.configId = cc.id
SET
cc.patch=JSON_SET(cc.patch, '$.id', ?),
cc.full=JSON_SET(cc.full, '$.id', ?)
WHERE c.id = ?
`,
[chartId, chartId, chartId]
)
newConfig = await saveNewChart(knex, { config: newConfig, user })
chartId = newConfig.id!
}

// Record this change in version history
Expand Down Expand Up @@ -470,7 +520,7 @@ const saveGrapher = async (
await db.knexRaw(
knex,
`UPDATE charts SET publishedAt=?, publishedByUserId=? WHERE id = ? `,
[now, user.id, chartId]
[new Date(), user.id, chartId]
)
await triggerStaticBuild(user, `Publishing chart ${newConfig.slug}`)
} else if (
Expand Down Expand Up @@ -572,6 +622,16 @@ getRouteWithROTransaction(
async (req, res, trx) => expectChartById(trx, req.params.chartId)
)

function getBaseLayerConfig(): GrapherInterface {
return defaultGrapherConfig
}

getRouteWithROTransaction(
apiRouter,
"/charts/:chartId.base.json",
async (req, res, trx) => getBaseLayerConfig()

Check warning on line 632 in adminSiteServer/apiRouter.ts

View workflow job for this annotation

GitHub Actions / eslint

'trx' is defined but never used. Allowed unused args must match /^(res|req)$/u

Check warning on line 632 in adminSiteServer/apiRouter.ts

View workflow job for this annotation

GitHub Actions / eslint

'trx' is defined but never used. Allowed unused args must match /^(res|req)$/u
)

getRouteWithROTransaction(
apiRouter,
"/editorData/namespaces.json",
Expand Down
1 change: 1 addition & 0 deletions baker/GrapherBaker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export async function renderDataPageV2(
// this is true for preview pages for datapages on the indicator level but false
// if we are on Grapher pages. Once we have a good way in the grapher admin for how
// to use indicator level defaults, we should reconsider how this works here.
// TODO(inheritance): use mergeGrapherConfigs instead
const grapher = useIndicatorGrapherConfigs
? mergePartialGrapherConfigs(grapherConfigForVariable, pageGrapher)
: pageGrapher ?? {}
Expand Down
1 change: 1 addition & 0 deletions baker/siteRenderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -791,6 +791,7 @@ export const renderExplorerPage = async (
config: row.grapherConfigETL as string,
})
: {}
// TODO(inheritance): use mergeGrapherConfigs instead
return mergePartialGrapherConfigs(etlConfig, adminConfig)
})

Expand Down
10 changes: 10 additions & 0 deletions db/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -668,3 +668,13 @@ export async function getLinkedIndicatorSlugs({
.then((gdocs) => gdocs.flatMap((gdoc) => gdoc.linkedKeyIndicatorSlugs))
.then((slugs) => new Set(slugs))
}

export const getBinaryUUID = async (
knex: KnexReadonlyTransaction
): Promise<Buffer> => {
const { id } = (await knexRawFirst<{ id: Buffer }>(
knex,
`SELECT UUID_TO_BIN(UUID(), 1) AS id`
))!
return id
}
59 changes: 59 additions & 0 deletions db/migration/1720600092980-MakeChartsInheritDefaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { MigrationInterface, QueryRunner } from "typeorm"
import { diffGrapherConfigs, mergeGrapherConfigs } from "@ourworldindata/utils"
import { defaultGrapherConfig } from "@ourworldindata/grapher"

export class MakeChartsInheritDefaults1720600092980
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
const charts = (await queryRunner.query(
`-- sql
SELECT id, patch as config FROM chart_configs
`
)) as { id: string; config: string }[]

for (const chart of charts) {
const originalConfig = JSON.parse(chart.config)

// if the schema version is missing, assume it's the latest
if (!originalConfig["$schema"]) {
originalConfig["$schema"] = defaultGrapherConfig["$schema"]
}

const patchConfig = diffGrapherConfigs(
originalConfig,
defaultGrapherConfig
)
const fullConfig = mergeGrapherConfigs(
defaultGrapherConfig,
patchConfig
)

await queryRunner.query(
`-- sql
UPDATE chart_configs
SET
patch = ?,
full = ?
WHERE id = ?
`,
[
JSON.stringify(patchConfig),
JSON.stringify(fullConfig),
chart.id,
]
)
}
}

public async down(queryRunner: QueryRunner): Promise<void> {
// we can't recover the original configs,
// but the patched one is the next best thing
await queryRunner.query(
`-- sql
UPDATE chart_configs
SET full = patch
`
)
}
}
Loading

0 comments on commit 659e513

Please sign in to comment.