@contember/migrations-client 1.4.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/JsLoader.d.ts +10 -0
- package/dist/src/JsLoader.d.ts.map +1 -0
- package/dist/src/JsLoader.js +33 -0
- package/dist/src/JsLoader.js.map +1 -0
- package/dist/src/JsonLoader.d.ts +9 -0
- package/dist/src/JsonLoader.d.ts.map +1 -0
- package/dist/src/JsonLoader.js +37 -0
- package/dist/src/JsonLoader.js.map +1 -0
- package/dist/src/MigrationCreator.d.ts +21 -0
- package/dist/src/MigrationCreator.d.ts.map +1 -0
- package/dist/src/MigrationCreator.js +47 -0
- package/dist/src/MigrationCreator.js.map +1 -0
- package/dist/src/MigrationExecuteHelper.d.ts +13 -0
- package/dist/src/MigrationExecuteHelper.d.ts.map +1 -0
- package/dist/src/MigrationExecuteHelper.js +61 -0
- package/dist/src/MigrationExecuteHelper.js.map +1 -0
- package/dist/src/MigrationFile.d.ts +36 -0
- package/dist/src/MigrationFile.d.ts.map +1 -0
- package/dist/src/MigrationFile.js +6 -0
- package/dist/src/MigrationFile.js.map +1 -0
- package/dist/src/MigrationFileLoader.d.ts +5 -0
- package/dist/src/MigrationFileLoader.d.ts.map +1 -0
- package/dist/src/MigrationFileLoader.js +3 -0
- package/dist/src/MigrationFileLoader.js.map +1 -0
- package/dist/src/MigrationFilesManager.d.ts +18 -0
- package/dist/src/MigrationFilesManager.d.ts.map +1 -0
- package/dist/src/MigrationFilesManager.js +114 -0
- package/dist/src/MigrationFilesManager.js.map +1 -0
- package/dist/src/MigrationParser.d.ts +6 -0
- package/dist/src/MigrationParser.d.ts.map +1 -0
- package/dist/src/MigrationParser.js +33 -0
- package/dist/src/MigrationParser.js.map +1 -0
- package/dist/src/MigrationsResolver.d.ts +14 -0
- package/dist/src/MigrationsResolver.d.ts.map +1 -0
- package/dist/src/MigrationsResolver.js +37 -0
- package/dist/src/MigrationsResolver.js.map +1 -0
- package/dist/src/SchemaVersionBuilder.d.ts +13 -0
- package/dist/src/SchemaVersionBuilder.d.ts.map +1 -0
- package/dist/src/SchemaVersionBuilder.js +26 -0
- package/dist/src/SchemaVersionBuilder.js.map +1 -0
- package/dist/src/SystemClient.d.ts +29 -0
- package/dist/src/SystemClient.d.ts.map +1 -0
- package/dist/src/SystemClient.js +117 -0
- package/dist/src/SystemClient.js.map +1 -0
- package/dist/src/index.d.ts +14 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +37 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/migrations.d.ts +56 -0
- package/dist/src/migrations.d.ts.map +1 -0
- package/dist/src/migrations.js +87 -0
- package/dist/src/migrations.js.map +1 -0
- package/dist/src/tsconfig.tsbuildinfo +1 -0
- package/dist/src/utils/assertNever.d.ts +2 -0
- package/dist/src/utils/assertNever.d.ts.map +1 -0
- package/dist/src/utils/assertNever.js +8 -0
- package/dist/src/utils/assertNever.js.map +1 -0
- package/dist/src/utils/json.d.ts +7 -0
- package/dist/src/utils/json.d.ts.map +1 -0
- package/dist/src/utils/json.js +3 -0
- package/dist/src/utils/json.js.map +1 -0
- package/package.json +24 -0
- package/src/JsLoader.ts +39 -0
- package/src/JsonLoader.ts +18 -0
- package/src/MigrationCreator.ts +60 -0
- package/src/MigrationExecuteHelper.ts +79 -0
- package/src/MigrationFile.ts +52 -0
- package/src/MigrationFileLoader.ts +5 -0
- package/src/MigrationFilesManager.ts +102 -0
- package/src/MigrationParser.ts +32 -0
- package/src/MigrationsResolver.ts +44 -0
- package/src/SchemaVersionBuilder.ts +33 -0
- package/src/SystemClient.ts +150 -0
- package/src/index.ts +23 -0
- package/src/migrations.ts +157 -0
- package/src/tsconfig.json +13 -0
- package/src/utils/assertNever.ts +3 -0
- package/src/utils/json.ts +4 -0
- package/tsconfig.json +5 -0
- package/tsconfig.settings.json +3 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Migration } from '@contember/schema-migrations'
|
|
2
|
+
import { JSONObject } from './utils/json'
|
|
3
|
+
|
|
4
|
+
export interface MigrationFile {
|
|
5
|
+
path: string
|
|
6
|
+
version: string
|
|
7
|
+
name: string
|
|
8
|
+
|
|
9
|
+
getContent: () => Promise<MigrationContent>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const isSchemaMigration = (content: MigrationContent): content is SchemaMigration => typeof content !== 'function' && 'modifications' in content
|
|
13
|
+
|
|
14
|
+
export type ResolvedMigrationContent =
|
|
15
|
+
| SchemaMigration
|
|
16
|
+
| ContentMigration
|
|
17
|
+
|
|
18
|
+
export type MigrationContent =
|
|
19
|
+
| ResolvedMigrationContent
|
|
20
|
+
| ContentMigrationFactory
|
|
21
|
+
|
|
22
|
+
export type SchemaMigration =
|
|
23
|
+
& {
|
|
24
|
+
type: 'schema'
|
|
25
|
+
}
|
|
26
|
+
& Migration
|
|
27
|
+
|
|
28
|
+
export type ContentMigration = {
|
|
29
|
+
type: 'content'
|
|
30
|
+
version: string
|
|
31
|
+
name: string
|
|
32
|
+
queries: ContentMigrationQuery[]
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type ContentMigrationFactoryArgs = {
|
|
36
|
+
apiBaseUrl: string
|
|
37
|
+
projectName: string
|
|
38
|
+
apiToken: string
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type ContentMigrationFactory = {
|
|
42
|
+
type: 'factory'
|
|
43
|
+
factory: (args: ContentMigrationFactoryArgs) => Promise<ContentMigration>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ContentMigrationQuery = {
|
|
47
|
+
query: string
|
|
48
|
+
variables?: JSONObject
|
|
49
|
+
stage?: string
|
|
50
|
+
checkMutationResult?: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { MigrationVersionHelper } from '@contember/engine-common'
|
|
2
|
+
import * as fs from 'node:fs/promises'
|
|
3
|
+
import { MigrationFileLoader } from './MigrationFileLoader'
|
|
4
|
+
import { MigrationContent, MigrationFile } from './MigrationFile'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { Dirent } from 'node:fs'
|
|
7
|
+
|
|
8
|
+
export class MigrationFilesManager {
|
|
9
|
+
private migrations: null | Promise<MigrationFile[]> = null
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
public readonly directory: string,
|
|
13
|
+
private readonly loaders: Record<string, MigrationFileLoader>,
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
public async createFile(content: string, name: string, extension = 'json'): Promise<string> {
|
|
17
|
+
this.migrations = null
|
|
18
|
+
const path = this.formatPath(name, extension)
|
|
19
|
+
await fs.writeFile(path, content, { encoding: 'utf8' })
|
|
20
|
+
return await fs.realpath(path)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public async removeFile(name: string) {
|
|
24
|
+
this.migrations = null
|
|
25
|
+
const path = this.formatPath(name)
|
|
26
|
+
await fs.unlink(path)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public async moveFile(oldName: string, newName: string) {
|
|
30
|
+
this.migrations = null
|
|
31
|
+
await fs.rename(this.formatPath(oldName), this.formatPath(newName))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public async createDirIfNotExist(): Promise<void> {
|
|
35
|
+
try {
|
|
36
|
+
await fs.mkdir(this.directory)
|
|
37
|
+
} catch (e) {
|
|
38
|
+
if (!(e instanceof Error) || !('code' in e) || (e as any).code !== 'EEXIST') {
|
|
39
|
+
throw e
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public async readFiles(): Promise<MigrationFile[]> {
|
|
45
|
+
return this.migrations ??= this.forceReadFiles()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public async forceReadFiles(): Promise<MigrationFile[]> {
|
|
49
|
+
const files = (await this.listFiles()).map(it => ({
|
|
50
|
+
filename: it,
|
|
51
|
+
version: MigrationVersionHelper.extractVersion(it),
|
|
52
|
+
}))
|
|
53
|
+
|
|
54
|
+
return files.map(({ version, filename }): MigrationFile => {
|
|
55
|
+
const ext = filename.substring(filename.lastIndexOf('.') + 1)
|
|
56
|
+
const path = join(this.directory, filename)
|
|
57
|
+
let content: Promise<MigrationContent> | null = null
|
|
58
|
+
const file: MigrationFile = {
|
|
59
|
+
path: path,
|
|
60
|
+
version: version,
|
|
61
|
+
name: MigrationVersionHelper.extractName(path.substring(path.lastIndexOf('/') + 1)),
|
|
62
|
+
getContent: () => {
|
|
63
|
+
content ??= this.loaders[ext].load(file)
|
|
64
|
+
return content
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
return file
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private async tryReadDir(): Promise<Dirent[]> {
|
|
72
|
+
try {
|
|
73
|
+
return await fs.readdir(this.directory, { withFileTypes: true })
|
|
74
|
+
} catch (e) {
|
|
75
|
+
if (e instanceof Error && 'code' in e && (e as any).code === 'ENOENT') {
|
|
76
|
+
return []
|
|
77
|
+
}
|
|
78
|
+
throw e
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private async listFiles(): Promise<string[]> {
|
|
83
|
+
const files = await this.tryReadDir()
|
|
84
|
+
|
|
85
|
+
const extensions = Object.keys(this.loaders).join('|')
|
|
86
|
+
const regex = new RegExp('^\\d{4}-\\d{2}-\\d{2}-\\d{6}-[\\w-]+\\.(' + extensions + ')$')
|
|
87
|
+
const filteredFiles = await Promise.all(
|
|
88
|
+
files
|
|
89
|
+
.filter(it => it.isFile())
|
|
90
|
+
.map(it => it.name)
|
|
91
|
+
.filter(it => it.match(regex)),
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return filteredFiles.sort()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private formatPath(version: string, extension = 'json'): string {
|
|
98
|
+
const filename = `${version}.${extension}`
|
|
99
|
+
const path = `${this.directory}/${filename}`
|
|
100
|
+
return path
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Migration, VERSION_INITIAL } from '@contember/schema-migrations'
|
|
2
|
+
import { ContentMigrationQuery, MigrationFile, ResolvedMigrationContent } from './MigrationFile'
|
|
3
|
+
import { JSONValue } from './utils/json'
|
|
4
|
+
|
|
5
|
+
export class MigrationParser {
|
|
6
|
+
public parse(migrationFile: MigrationFile, content: JSONValue): ResolvedMigrationContent {
|
|
7
|
+
if (typeof content !== 'object' || content === null) {
|
|
8
|
+
throw new Error('Invalid migration')
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if ('modifications' in content) {
|
|
12
|
+
const validated = content as Migration // todo validate
|
|
13
|
+
|
|
14
|
+
return {
|
|
15
|
+
type: 'schema',
|
|
16
|
+
version: migrationFile.version,
|
|
17
|
+
name: migrationFile.name,
|
|
18
|
+
formatVersion: validated.formatVersion || VERSION_INITIAL,
|
|
19
|
+
modifications: validated.modifications,
|
|
20
|
+
...(validated.skippedErrors ? { skippedErrors: validated.skippedErrors } : {}),
|
|
21
|
+
}
|
|
22
|
+
} else if ('queries' in content || 'query' in content) {
|
|
23
|
+
return {
|
|
24
|
+
type: 'content',
|
|
25
|
+
version: migrationFile.version,
|
|
26
|
+
name: migrationFile.name,
|
|
27
|
+
queries: ('queries' in content ? content.queries : [content]) as ContentMigrationQuery[],
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
throw new Error('Invalid migration content')
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { MigrationFilesManager } from './MigrationFilesManager'
|
|
2
|
+
import { Migration } from '@contember/schema-migrations'
|
|
3
|
+
import { isSchemaMigration, MigrationFile } from './MigrationFile'
|
|
4
|
+
import { MigrationVersionHelper } from '@contember/engine-common'
|
|
5
|
+
|
|
6
|
+
export class MigrationsResolver {
|
|
7
|
+
|
|
8
|
+
constructor(private readonly migrationFilesManager: MigrationFilesManager) {}
|
|
9
|
+
|
|
10
|
+
public get directory(): string {
|
|
11
|
+
return this.migrationFilesManager.directory
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public async getMigrationFiles(): Promise<MigrationFile[]> {
|
|
15
|
+
return await this.migrationFilesManager.readFiles()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
public async getSchemaMigrations(): Promise<Migration[]> {
|
|
19
|
+
const migrationFiles = await this.getMigrationFiles()
|
|
20
|
+
|
|
21
|
+
const migrations = await Promise.all(migrationFiles.map(this.convertMigrationFileToSchemaMigration))
|
|
22
|
+
|
|
23
|
+
return migrations.filter((it): it is Migration => it !== null)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public async findLatestSchemaMigration(): Promise<Migration | null> {
|
|
27
|
+
const migrations = await this.getSchemaMigrations()
|
|
28
|
+
return migrations.length > 0 ? migrations[migrations.length - 1] : null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public async findSchemaMigrationByVersion(version: string): Promise<Migration | null> {
|
|
32
|
+
const migrations = await this.getSchemaMigrations()
|
|
33
|
+
return migrations
|
|
34
|
+
.find(it =>
|
|
35
|
+
MigrationVersionHelper.extractVersion(it.version) === MigrationVersionHelper.extractVersion(version),
|
|
36
|
+
)
|
|
37
|
+
?? null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private async convertMigrationFileToSchemaMigration(file: MigrationFile): Promise<Migration | null> {
|
|
41
|
+
const result = await file.getContent()
|
|
42
|
+
return isSchemaMigration(result) ? result : null
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Schema } from '@contember/schema'
|
|
2
|
+
import { emptySchema } from '@contember/schema-utils'
|
|
3
|
+
import { SchemaMigrator } from '@contember/schema-migrations'
|
|
4
|
+
import { MigrationsResolver } from './MigrationsResolver'
|
|
5
|
+
|
|
6
|
+
export class SchemaVersionBuilder {
|
|
7
|
+
constructor(
|
|
8
|
+
private readonly migrationsResolver: MigrationsResolver,
|
|
9
|
+
private readonly schemaMigrator: SchemaMigrator,
|
|
10
|
+
) {}
|
|
11
|
+
|
|
12
|
+
async buildSchema(targetVersion?: string): Promise<Schema> {
|
|
13
|
+
return this.buildSchemaAdvanced(emptySchema, version => !targetVersion || version <= targetVersion)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async buildSchemaUntil(targetVersion: string): Promise<Schema> {
|
|
17
|
+
return this.buildSchemaAdvanced(emptySchema, version => version < targetVersion)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async continue(schema: Schema, previousVersion: string | null, targetVersion: string): Promise<Schema> {
|
|
21
|
+
return this.buildSchemaAdvanced(schema, version => version <= targetVersion && version > (previousVersion || ''))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public async buildSchemaAdvanced(initialSchema: Schema, condition: (version: string) => boolean): Promise<Schema> {
|
|
25
|
+
return (await this.migrationsResolver.getSchemaMigrations())
|
|
26
|
+
.filter(({ version }) => condition(version))
|
|
27
|
+
.reduce<Schema>(
|
|
28
|
+
(schema, { modifications, formatVersion }) =>
|
|
29
|
+
this.schemaMigrator.applyModifications(schema, modifications, formatVersion),
|
|
30
|
+
initialSchema,
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { GraphQLClient } from 'graphql-request'
|
|
2
|
+
import { ExecutedMigration, ExecutedMigrationInfo } from './migrations'
|
|
3
|
+
|
|
4
|
+
export type MigrateError = {
|
|
5
|
+
readonly code: MigrateErrorCode
|
|
6
|
+
readonly migration: string
|
|
7
|
+
readonly message: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export enum MigrateErrorCode {
|
|
11
|
+
MustFollowLatest = 'MUST_FOLLOW_LATEST',
|
|
12
|
+
AlreadyExecuted = 'ALREADY_EXECUTED',
|
|
13
|
+
InvalidFormat = 'INVALID_FORMAT',
|
|
14
|
+
InvalidSchema = 'INVALID_SCHEMA',
|
|
15
|
+
MigrationFailed = 'MIGRATION_FAILED',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type MigrateResponse = {
|
|
19
|
+
readonly ok: boolean
|
|
20
|
+
readonly errors: MigrateError[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class SystemClient {
|
|
24
|
+
constructor(private readonly apiClient: GraphQLClient) {}
|
|
25
|
+
|
|
26
|
+
public static create(baseUrl: string, projectName: string, apiToken: string): SystemClient {
|
|
27
|
+
const graphqlClient = new GraphQLClient(createSystemUrl(baseUrl, projectName), {
|
|
28
|
+
headers: {
|
|
29
|
+
Authorization: `Bearer ${apiToken}`,
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
return new SystemClient(graphqlClient)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
public async migrate(migrations: any[], force = false): Promise<void> {
|
|
36
|
+
const query = `
|
|
37
|
+
mutation($migrations: [Migration!]!) {
|
|
38
|
+
migrate: ${force ? 'forceMigrate' : 'migrate'}(migrations: $migrations) {
|
|
39
|
+
ok
|
|
40
|
+
errors {
|
|
41
|
+
code
|
|
42
|
+
migration
|
|
43
|
+
message: developerMessage
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
`
|
|
48
|
+
const result = (
|
|
49
|
+
await this.apiClient.request<{
|
|
50
|
+
migrate: { ok: boolean; errors: { code: MigrateErrorCode; migration: string; message: string }[] }
|
|
51
|
+
}>(query, {
|
|
52
|
+
migrations,
|
|
53
|
+
})
|
|
54
|
+
).migrate
|
|
55
|
+
if (!result.ok) {
|
|
56
|
+
throw result.errors
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public async migrationDelete(version: string): Promise<void> {
|
|
61
|
+
const query = `
|
|
62
|
+
mutation($version: String!) {
|
|
63
|
+
migrationDelete(migration: $version) {
|
|
64
|
+
ok
|
|
65
|
+
error {
|
|
66
|
+
developerMessage
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
`
|
|
71
|
+
const result = (
|
|
72
|
+
await this.apiClient.request<{
|
|
73
|
+
migrationDelete: { ok: boolean; error?: { developerMessage: string } }
|
|
74
|
+
}>(query, {
|
|
75
|
+
version,
|
|
76
|
+
})
|
|
77
|
+
).migrationDelete
|
|
78
|
+
if (!result.ok) {
|
|
79
|
+
throw result.error?.developerMessage
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public async migrationModify(version: string, modification: Partial<any>): Promise<void> {
|
|
84
|
+
const query = `
|
|
85
|
+
mutation($version: String!, $modification: MigrationModification!) {
|
|
86
|
+
migrationModify(migration: $version, modification: $modification) {
|
|
87
|
+
ok
|
|
88
|
+
error {
|
|
89
|
+
developerMessage
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
`
|
|
94
|
+
const result = (
|
|
95
|
+
await this.apiClient.request<{
|
|
96
|
+
migrationModify: { ok: boolean; error?: { developerMessage: string } }
|
|
97
|
+
}>(query, {
|
|
98
|
+
version,
|
|
99
|
+
modification,
|
|
100
|
+
})
|
|
101
|
+
).migrationModify
|
|
102
|
+
if (!result.ok) {
|
|
103
|
+
throw result.error?.developerMessage
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public async listExecutedMigrations(): Promise<ExecutedMigrationInfo[]> {
|
|
108
|
+
const query = `query {
|
|
109
|
+
executedMigrations {
|
|
110
|
+
name
|
|
111
|
+
version
|
|
112
|
+
formatVersion
|
|
113
|
+
checksum
|
|
114
|
+
executedAt
|
|
115
|
+
}
|
|
116
|
+
}`
|
|
117
|
+
return (
|
|
118
|
+
await this.apiClient.request<{
|
|
119
|
+
executedMigrations: ExecutedMigrationInfo[]
|
|
120
|
+
}>(query, {})
|
|
121
|
+
).executedMigrations.map(it => ({ ...it, executedAt: new Date(it.executedAt) }))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
public async getExecutedMigration(version: string): Promise<ExecutedMigration> {
|
|
125
|
+
const query = `query($version: String!) {
|
|
126
|
+
executedMigrations(version: $version) {
|
|
127
|
+
name
|
|
128
|
+
version
|
|
129
|
+
formatVersion
|
|
130
|
+
checksum
|
|
131
|
+
executedAt
|
|
132
|
+
modifications
|
|
133
|
+
}
|
|
134
|
+
}`
|
|
135
|
+
return (
|
|
136
|
+
(
|
|
137
|
+
await this.apiClient.request<{
|
|
138
|
+
executedMigrations: ExecutedMigration[]
|
|
139
|
+
}>(query, { version })
|
|
140
|
+
).executedMigrations.map(it => ({ ...it, executedAt: new Date(it.executedAt) }))[0] || null
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const createSystemUrl = (baseUrl: string, projectName: string) => {
|
|
146
|
+
if (baseUrl.endsWith('/')) {
|
|
147
|
+
baseUrl = baseUrl.substring(0, baseUrl.length - 1)
|
|
148
|
+
}
|
|
149
|
+
return baseUrl + '/system/' + projectName
|
|
150
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export {
|
|
2
|
+
Migration,
|
|
3
|
+
MigrationDescriber,
|
|
4
|
+
MigrationInfo,
|
|
5
|
+
ModificationHandlerFactory,
|
|
6
|
+
SchemaDiffer,
|
|
7
|
+
SchemaMigrator,
|
|
8
|
+
VERSION_LATEST,
|
|
9
|
+
calculateMigrationChecksum,
|
|
10
|
+
} from '@contember/schema-migrations'
|
|
11
|
+
|
|
12
|
+
export * from './JsLoader'
|
|
13
|
+
export * from './JsonLoader'
|
|
14
|
+
export * from './MigrationCreator'
|
|
15
|
+
export * from './MigrationExecuteHelper'
|
|
16
|
+
export * from './MigrationFile'
|
|
17
|
+
export * from './MigrationFileLoader'
|
|
18
|
+
export * from './MigrationFilesManager'
|
|
19
|
+
export * from './MigrationParser'
|
|
20
|
+
export * from './migrations'
|
|
21
|
+
export * from './MigrationsResolver'
|
|
22
|
+
export * from './SchemaVersionBuilder'
|
|
23
|
+
export * from './SystemClient'
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { calculateMigrationChecksum, Migration, MigrationInfo } from '@contember/schema-migrations'
|
|
2
|
+
import { isSchemaMigration, MigrationFile } from './MigrationFile'
|
|
3
|
+
|
|
4
|
+
export interface ExecutedMigrationInfo extends MigrationInfo {
|
|
5
|
+
readonly executedAt: Date
|
|
6
|
+
readonly checksum: string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ExecutedMigration extends Migration {
|
|
10
|
+
readonly executedAt: Date
|
|
11
|
+
readonly checksum: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export enum MigrationState {
|
|
15
|
+
TO_EXECUTE_OK = 'to_execute_ok',
|
|
16
|
+
TO_EXECUTE_ERROR = 'to_execute_error',
|
|
17
|
+
EXECUTED_OK = 'executed_ok',
|
|
18
|
+
EXECUTED_ERROR = 'executed_error',
|
|
19
|
+
EXECUTED_MISSING = 'executed_missing',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface MigrationStatusBase<S extends MigrationState> {
|
|
23
|
+
state: S
|
|
24
|
+
version: string
|
|
25
|
+
name: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MigrationToExecuteOkStatus extends MigrationStatusBase<MigrationState.TO_EXECUTE_OK> {
|
|
29
|
+
localMigration: MigrationFile
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface MigrationToExecuteInvalidStatus extends MigrationStatusBase<MigrationState.TO_EXECUTE_ERROR> {
|
|
33
|
+
localMigration: MigrationFile
|
|
34
|
+
errorMessage: string
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface MigrationExecutedOkStatus extends MigrationStatusBase<MigrationState.EXECUTED_OK>{
|
|
38
|
+
executedMigration: ExecutedMigrationInfo
|
|
39
|
+
localMigration: MigrationFile
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface MigrationExecutedMissingStatus extends MigrationStatusBase<MigrationState.EXECUTED_MISSING>{
|
|
43
|
+
executedMigration: ExecutedMigrationInfo
|
|
44
|
+
errorMessage: string
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface MigrationExecutedErrorStatus extends MigrationStatusBase<MigrationState.EXECUTED_ERROR>{
|
|
48
|
+
executedMigration: ExecutedMigrationInfo
|
|
49
|
+
localMigration: MigrationFile
|
|
50
|
+
errorMessage: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type ErrorMigrationStatus =
|
|
54
|
+
| MigrationToExecuteInvalidStatus
|
|
55
|
+
| MigrationExecutedMissingStatus
|
|
56
|
+
| MigrationExecutedErrorStatus
|
|
57
|
+
|
|
58
|
+
export type AnyMigrationStatus =
|
|
59
|
+
| ErrorMigrationStatus
|
|
60
|
+
| MigrationToExecuteOkStatus
|
|
61
|
+
| MigrationExecutedOkStatus
|
|
62
|
+
|
|
63
|
+
const isErrorMigrationStatus = (migration: AnyMigrationStatus): migration is ErrorMigrationStatus =>
|
|
64
|
+
[MigrationState.EXECUTED_ERROR, MigrationState.EXECUTED_MISSING, MigrationState.TO_EXECUTE_ERROR].includes(
|
|
65
|
+
migration.state,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
export const sortMigrations = <M extends { version: string }>(migrations: M[]): M[] => {
|
|
69
|
+
return [...migrations].sort((a, b) => a.version.localeCompare(b.version))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class MigrationsStatusResolver {
|
|
73
|
+
|
|
74
|
+
async getMigrationsStatus(
|
|
75
|
+
executedMigrations: ExecutedMigrationInfo[],
|
|
76
|
+
localMigrations: MigrationFile[],
|
|
77
|
+
force: boolean = false,
|
|
78
|
+
): Promise<{
|
|
79
|
+
allMigrations: AnyMigrationStatus[]
|
|
80
|
+
errorMigrations: ErrorMigrationStatus[]
|
|
81
|
+
migrationsToExecute: MigrationToExecuteOkStatus[]
|
|
82
|
+
}> {
|
|
83
|
+
const sortedExecuted = sortMigrations(executedMigrations)
|
|
84
|
+
|
|
85
|
+
const allMigrations: AnyMigrationStatus[] = []
|
|
86
|
+
for (const executed of sortedExecuted) {
|
|
87
|
+
const localMigration = localMigrations.find(it => it.version === executed.version)
|
|
88
|
+
if (!localMigration) {
|
|
89
|
+
allMigrations.push({
|
|
90
|
+
state: MigrationState.EXECUTED_MISSING,
|
|
91
|
+
executedMigration: executed,
|
|
92
|
+
version: executed.version,
|
|
93
|
+
name: executed.name,
|
|
94
|
+
errorMessage: `Migration ${executed.name} is missing locally`,
|
|
95
|
+
})
|
|
96
|
+
continue
|
|
97
|
+
}
|
|
98
|
+
const migrationContent = await localMigration.getContent()
|
|
99
|
+
|
|
100
|
+
const localChecksum = !isSchemaMigration(migrationContent) ? null : calculateMigrationChecksum(migrationContent)
|
|
101
|
+
|
|
102
|
+
if (localChecksum !== executed.checksum) {
|
|
103
|
+
allMigrations.push({
|
|
104
|
+
state: MigrationState.EXECUTED_ERROR,
|
|
105
|
+
executedMigration: executed,
|
|
106
|
+
version: executed.version,
|
|
107
|
+
name: executed.name,
|
|
108
|
+
localMigration,
|
|
109
|
+
errorMessage: `Checksum of executed migration "${executed.checksum}" does not match "${localChecksum}"`,
|
|
110
|
+
})
|
|
111
|
+
continue
|
|
112
|
+
}
|
|
113
|
+
allMigrations.push({
|
|
114
|
+
state: MigrationState.EXECUTED_OK,
|
|
115
|
+
executedMigration: executed,
|
|
116
|
+
version: executed.version,
|
|
117
|
+
name: executed.name,
|
|
118
|
+
localMigration,
|
|
119
|
+
})
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const latestExecuted = sortedExecuted.length > 0 ? sortedExecuted[sortedExecuted.length - 1] : null
|
|
123
|
+
for (const localMigration of localMigrations) {
|
|
124
|
+
if (allMigrations.find(it => it.version === localMigration.version)) {
|
|
125
|
+
continue
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (latestExecuted && latestExecuted.version > localMigration.version) {
|
|
129
|
+
allMigrations.push({
|
|
130
|
+
state: MigrationState.TO_EXECUTE_ERROR,
|
|
131
|
+
localMigration,
|
|
132
|
+
version: localMigration.version,
|
|
133
|
+
name: localMigration.name,
|
|
134
|
+
errorMessage: `Must follow latest executed migration ${latestExecuted.name}`,
|
|
135
|
+
})
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
allMigrations.push({
|
|
139
|
+
state: MigrationState.TO_EXECUTE_OK,
|
|
140
|
+
localMigration,
|
|
141
|
+
version: localMigration.version,
|
|
142
|
+
name: localMigration.name,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
const allMigrationsSorted = sortMigrations(allMigrations)
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
allMigrations: allMigrationsSorted,
|
|
149
|
+
errorMigrations: allMigrationsSorted.filter(isErrorMigrationStatus),
|
|
150
|
+
migrationsToExecute: allMigrationsSorted.filter(
|
|
151
|
+
(it): it is MigrationToExecuteOkStatus =>
|
|
152
|
+
it.state === MigrationState.TO_EXECUTE_OK || (force && it.state === MigrationState.TO_EXECUTE_ERROR),
|
|
153
|
+
),
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../tsconfig.settings.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"esModuleInterop": true,
|
|
5
|
+
"outDir": "../dist/src"
|
|
6
|
+
},
|
|
7
|
+
"references": [
|
|
8
|
+
{ "path": "../../engine-common/src" },
|
|
9
|
+
{ "path": "../../schema/src" },
|
|
10
|
+
{ "path": "../../schema-migrations/src" },
|
|
11
|
+
{ "path": "../../schema-utils/src" }
|
|
12
|
+
]
|
|
13
|
+
}
|
package/tsconfig.json
ADDED