Skip to content

Commit

Permalink
Merge pull request #4070 from serlo/some-migration-code-changes
Browse files Browse the repository at this point in the history
feat(editor-package): Add migration and some code changes in `storage-format.ts`
  • Loading branch information
LarsTheGlidingSquirrel authored Sep 5, 2024
2 parents 2ad7bb8 + 668e998 commit e660143
Showing 1 changed file with 121 additions and 79 deletions.
200 changes: 121 additions & 79 deletions packages/editor/src/package/storage-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,38 +9,84 @@ import { getEditorVersion } from './editor-version'
/** The creator of the saved data -> Serlo editor */
const documentType = 'https://serlo.org/editor'

type Migration = (state: unknown, variant: EditorVariant) => StorageFormat
/** The variant of the Serlo editor that created this saved data */
const EditorVariantType = t.union([
t.literal('https://github.com/serlo/serlo-editor-for-edusharing'),
t.literal('lti-tool'),
t.literal('serlo-org'),
t.literal('kiron'),
t.literal('scobees'),
t.literal('moodle'),
t.literal('chancenwerk'),
t.literal('unknown'),
])

export type EditorVariant = t.TypeOf<typeof EditorVariantType>

const EditorStateType = t.type({
plugin: t.string,
state: t.unknown,
})

type EditorState = t.TypeOf<typeof EditorStateType>

type Migration = (
state: OldStorageFormat | StorageFormat
) => OldStorageFormat | StorageFormat

const OldStorageFormatType_0 = t.type({
type: t.literal(documentType),
variant: EditorVariantType,
version: t.literal(0),
dateModified: t.string,
document: EditorStateType,
})

type OldStorageFormat_0 = t.TypeOf<typeof OldStorageFormatType_0>

const OldStorageFormatType_1 = t.type({
id: t.string,
type: t.literal(documentType),
variant: EditorVariantType,
version: t.literal(1),
editorVersion: t.string,
dateModified: t.string,
document: EditorStateType,
})

type OldStorageFormat_1 = t.TypeOf<typeof OldStorageFormatType_1>

// Add new outdated storage formats here ...

// Usage: Do not change existing migrations. Only if you are sure that they never ran in any system where content needs to be supported long term. Instead, create a new migration. It needs to be at the end of the array. The last migration should return `StorageFormat`
const migrations: Migration[] = [
// Migration 1: Add editorVersion, domainOrigin and id
(state): StorageFormat => {
// We already have the right format, we can skip this migration.
if (StorageFormat.is(state)) {
return state
// Migration 0: Add editorVersion & id
(state) => {
if (!OldStorageFormatType_0.is(state))
throw new Error(
`Unexpected type during migration. Expected ${JSON.stringify(OldStorageFormatType_0)} but got ${JSON.stringify(state)}`
)

return {
...state,
editorVersion: getEditorVersion(),
id: uuid_v4(),
}
},

const expectedType = t.type({
type: t.literal(documentType),
variant: EditorVariantType,
version: t.number,
dateModified: t.string,
document: t.type({
plugin: t.string,
state: t.unknown,
}),
})
if (!expectedType.is(state))
// Migration 1: Add domainOrigin
(state): StorageFormat => {
if (!OldStorageFormatType_1.is(state))
throw new Error(
`Unexpected type during migration. Expected ${JSON.stringify(expectedType)} but got ${JSON.stringify(state)}`
`Unexpected type during migration. Expected ${JSON.stringify(OldStorageFormatType_1)} but got ${JSON.stringify(state)}`
)

return {
...state,
editorVersion: getEditorVersion(),
domainOrigin: window.location.origin,
id: uuid_v4(),
}
},

// ...
// Add new migrations here. Make sure the last one returns the new StorageFormat.
]
Expand Down Expand Up @@ -69,88 +115,84 @@ export function createEmptyDocument(
}
}

function deepCopy(obj: unknown) {
return JSON.parse(JSON.stringify(obj)) as unknown
}

type OldStorageFormat = OldStorageFormat_0 | OldStorageFormat_1

/** Migrates outdated states to the most recent `StorageFormat`. */
export function migrate(
stateBeforeMigration: unknown,
variant: EditorVariant
): {
migratedState: StorageFormat
stateChanged: boolean
} {
let migratedState: StorageFormat
let stateChanged = false

// Check if the state is the (old format)
if (
DocumentType.is(stateBeforeMigration) &&
!StorageFormat.is(stateBeforeMigration)
!OldStorageFormatType_0.is(stateBeforeMigration) &&
!OldStorageFormatType_1.is(stateBeforeMigration) &&
!StorageFormatType.is(stateBeforeMigration) &&
!EditorStateType.is(stateBeforeMigration)
) {
migratedState = {
...createEmptyDocument(variant),
version: 0,
document: stateBeforeMigration,
throw new Error(
`Unknown state type when trying to run migrations. Got ${JSON.stringify(stateBeforeMigration)}`
)
}

let stateChanged = false

// If the state is in the old format ({ plugin: string, state: unknown }) & missing `version` property -> Add metadata (including `version`) so that type matches what should be present before running migrations[0]
let migratingState = prepareStateForMigrations()
function prepareStateForMigrations() {
if (EditorStateType.is(stateBeforeMigration)) {
stateChanged = true
const statePlusMetadata: OldStorageFormat_0 = {
type: documentType,
variant,
version: 0,
dateModified: getCurrentDatetime(),
document: deepCopy(stateBeforeMigration) as EditorState,
}
return statePlusMetadata
} else {
return deepCopy(stateBeforeMigration) as StorageFormat | OldStorageFormat
}
stateChanged = true
} else {
// Make a deep copy
migratedState = JSON.parse(
JSON.stringify(stateBeforeMigration)
) as StorageFormat
}

for (let i = migratedState.version; i < migrations.length; i++) {
migratedState = migrations[i](migratedState, variant)
// The property `version` tells us which entries in the migrations array we still have to run. Example: `version: 2` means we need to run migration[2], migration[3], ... if they exist
const nextMigrationIndex = migratingState.version
for (let i = nextMigrationIndex; i < migrations.length; i++) {
migratingState = migrations[i](migratingState)
stateChanged = true
migratedState.version = i + 1
migratingState.version = i + 1
}

if (!StorageFormat.is(migratedState))
if (!StorageFormatType.is(migratingState))
throw new Error(
'Storage format after migrations does not match StorageFormatType'
)

if (stateChanged) {
migratedState.editorVersion = getEditorVersion()
migratedState.dateModified = getCurrentDatetime()
migratingState.editorVersion = getEditorVersion()
migratingState.dateModified = getCurrentDatetime()
}

return { migratedState, stateChanged }
return { migratedState: migratingState, stateChanged }
}

/** The variant of the Serlo editor that created this saved data */
const EditorVariantType = t.union([
t.literal('https://github.com/serlo/serlo-editor-for-edusharing'),
t.literal('lti-tool'),
t.literal('serlo-org'),
t.literal('kiron'),
t.literal('scobees'),
t.literal('moodle'),
t.literal('chancenwerk'),
t.literal('unknown'),
])

export type EditorVariant = t.TypeOf<typeof EditorVariantType>

const DocumentType = t.type({
plugin: t.string,
state: t.unknown,
const StorageFormatType = t.type({
// Constant values (set at creation)
id: t.string, // https://dini-ag-kim.github.io/amb/20231019/#id
type: t.literal(documentType),
variant: EditorVariantType,
domainOrigin: t.string,

// Variable values (can change when state modified)
version: t.literal(currentVersion), // Index of the next migration to apply
editorVersion: t.string,
dateModified: t.string,
document: EditorStateType,
})

const StorageFormat = t.intersection([
t.type({
// Constant values (set at creation)
id: t.string, // https://dini-ag-kim.github.io/amb/20231019/#id
type: t.literal(documentType),
variant: EditorVariantType,

// Variable values (can change when state modified)
version: t.number, // Index of the next migration to apply
editorVersion: t.string,
dateModified: t.string,
document: DocumentType,
}),
t.partial({
// Optional fields
domainOrigin: t.string,
}),
])
export type StorageFormat = t.TypeOf<typeof StorageFormat>
export type StorageFormat = t.TypeOf<typeof StorageFormatType>

0 comments on commit e660143

Please sign in to comment.