@contember/migrations-client 2.1.0-beta.2 → 2.2.0-alpha.1
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/development/index.cjs +4 -0
- package/dist/development/index.cjs.map +1 -1
- package/dist/development/index.js +4 -0
- package/dist/development/index.js.map +1 -1
- package/dist/development/src/JsLoader.cjs.map +1 -1
- package/dist/development/src/JsLoader.js.map +1 -1
- package/dist/development/src/JsonLoader.cjs.map +1 -1
- package/dist/development/src/JsonLoader.js.map +1 -1
- package/dist/development/src/MigrationCreator.cjs +2 -2
- package/dist/development/src/MigrationCreator.cjs.map +1 -1
- package/dist/development/src/MigrationCreator.js +2 -2
- package/dist/development/src/MigrationCreator.js.map +1 -1
- package/dist/development/src/MigrationExecuteHelper.cjs +14 -5
- package/dist/development/src/MigrationExecuteHelper.cjs.map +1 -1
- package/dist/development/src/MigrationExecuteHelper.js +14 -5
- package/dist/development/src/MigrationExecuteHelper.js.map +1 -1
- package/dist/development/src/MigrationFile.cjs.map +1 -1
- package/dist/development/src/MigrationFile.js.map +1 -1
- package/dist/development/src/MigrationFilesManager.cjs +11 -11
- package/dist/development/src/MigrationFilesManager.cjs.map +1 -1
- package/dist/development/src/MigrationFilesManager.js.map +1 -1
- package/dist/development/src/MigrationParser.cjs.map +1 -1
- package/dist/development/src/MigrationParser.js.map +1 -1
- package/dist/development/src/MigrationsResolver.cjs.map +1 -1
- package/dist/development/src/MigrationsResolver.js.map +1 -1
- package/dist/development/src/SchemaStateManager.cjs +82 -0
- package/dist/development/src/SchemaStateManager.cjs.map +1 -0
- package/dist/development/src/SchemaStateManager.js +64 -0
- package/dist/development/src/SchemaStateManager.js.map +1 -0
- package/dist/development/src/SchemaVersionBuilder.cjs +8 -2
- package/dist/development/src/SchemaVersionBuilder.cjs.map +1 -1
- package/dist/development/src/SchemaVersionBuilder.js +8 -2
- package/dist/development/src/SchemaVersionBuilder.js.map +1 -1
- package/dist/development/src/SnapshotManager.cjs +46 -0
- package/dist/development/src/SnapshotManager.cjs.map +1 -0
- package/dist/development/src/SnapshotManager.js +29 -0
- package/dist/development/src/SnapshotManager.js.map +1 -0
- package/dist/development/src/SystemClient.cjs +29 -4
- package/dist/development/src/SystemClient.cjs.map +1 -1
- package/dist/development/src/SystemClient.js +29 -4
- package/dist/development/src/SystemClient.js.map +1 -1
- package/dist/development/src/migrations.cjs.map +1 -1
- package/dist/development/src/migrations.js.map +1 -1
- package/dist/production/index.cjs +4 -0
- package/dist/production/index.cjs.map +1 -1
- package/dist/production/index.js +4 -0
- package/dist/production/index.js.map +1 -1
- package/dist/production/src/JsLoader.cjs.map +1 -1
- package/dist/production/src/JsLoader.js.map +1 -1
- package/dist/production/src/JsonLoader.cjs.map +1 -1
- package/dist/production/src/JsonLoader.js.map +1 -1
- package/dist/production/src/MigrationCreator.cjs +2 -2
- package/dist/production/src/MigrationCreator.cjs.map +1 -1
- package/dist/production/src/MigrationCreator.js +2 -2
- package/dist/production/src/MigrationCreator.js.map +1 -1
- package/dist/production/src/MigrationExecuteHelper.cjs +14 -5
- package/dist/production/src/MigrationExecuteHelper.cjs.map +1 -1
- package/dist/production/src/MigrationExecuteHelper.js +14 -5
- package/dist/production/src/MigrationExecuteHelper.js.map +1 -1
- package/dist/production/src/MigrationFile.cjs.map +1 -1
- package/dist/production/src/MigrationFile.js.map +1 -1
- package/dist/production/src/MigrationFilesManager.cjs +11 -11
- package/dist/production/src/MigrationFilesManager.cjs.map +1 -1
- package/dist/production/src/MigrationFilesManager.js.map +1 -1
- package/dist/production/src/MigrationParser.cjs.map +1 -1
- package/dist/production/src/MigrationParser.js.map +1 -1
- package/dist/production/src/MigrationsResolver.cjs.map +1 -1
- package/dist/production/src/MigrationsResolver.js.map +1 -1
- package/dist/production/src/SchemaStateManager.cjs +82 -0
- package/dist/production/src/SchemaStateManager.cjs.map +1 -0
- package/dist/production/src/SchemaStateManager.js +64 -0
- package/dist/production/src/SchemaStateManager.js.map +1 -0
- package/dist/production/src/SchemaVersionBuilder.cjs +8 -2
- package/dist/production/src/SchemaVersionBuilder.cjs.map +1 -1
- package/dist/production/src/SchemaVersionBuilder.js +8 -2
- package/dist/production/src/SchemaVersionBuilder.js.map +1 -1
- package/dist/production/src/SnapshotManager.cjs +46 -0
- package/dist/production/src/SnapshotManager.cjs.map +1 -0
- package/dist/production/src/SnapshotManager.js +29 -0
- package/dist/production/src/SnapshotManager.js.map +1 -0
- package/dist/production/src/SystemClient.cjs +29 -4
- package/dist/production/src/SystemClient.cjs.map +1 -1
- package/dist/production/src/SystemClient.js +29 -4
- package/dist/production/src/SystemClient.js.map +1 -1
- package/dist/production/src/migrations.cjs.map +1 -1
- package/dist/production/src/migrations.js.map +1 -1
- package/dist/types/JsLoader.d.ts +3 -3
- package/dist/types/JsLoader.d.ts.map +1 -1
- package/dist/types/JsonLoader.d.ts +4 -4
- package/dist/types/JsonLoader.d.ts.map +1 -1
- package/dist/types/MigrationCreator.d.ts +4 -2
- package/dist/types/MigrationCreator.d.ts.map +1 -1
- package/dist/types/MigrationExecuteHelper.d.ts +6 -4
- package/dist/types/MigrationExecuteHelper.d.ts.map +1 -1
- package/dist/types/MigrationFile.d.ts +2 -2
- package/dist/types/MigrationFile.d.ts.map +1 -1
- package/dist/types/MigrationFileLoader.d.ts +1 -1
- package/dist/types/MigrationFileLoader.d.ts.map +1 -1
- package/dist/types/MigrationFilesManager.d.ts +2 -2
- package/dist/types/MigrationFilesManager.d.ts.map +1 -1
- package/dist/types/MigrationParser.d.ts +2 -2
- package/dist/types/MigrationParser.d.ts.map +1 -1
- package/dist/types/MigrationsResolver.d.ts +2 -2
- package/dist/types/MigrationsResolver.d.ts.map +1 -1
- package/dist/types/SchemaStateManager.d.ts +19 -0
- package/dist/types/SchemaStateManager.d.ts.map +1 -0
- package/dist/types/SchemaVersionBuilder.d.ts +4 -2
- package/dist/types/SchemaVersionBuilder.d.ts.map +1 -1
- package/dist/types/SnapshotManager.d.ts +31 -0
- package/dist/types/SnapshotManager.d.ts.map +1 -0
- package/dist/types/SystemClient.d.ts +11 -3
- package/dist/types/SystemClient.d.ts.map +1 -1
- package/dist/types/index.d.ts +14 -12
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/migrations.d.ts +1 -1
- package/dist/types/migrations.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/src/JsLoader.ts +3 -3
- package/src/JsonLoader.ts +3 -3
- package/src/MigrationCreator.ts +7 -3
- package/src/MigrationExecuteHelper.ts +19 -8
- package/src/MigrationFile.ts +2 -2
- package/src/MigrationFileLoader.ts +1 -1
- package/src/MigrationFilesManager.ts +2 -2
- package/src/MigrationParser.ts +2 -2
- package/src/MigrationsResolver.ts +2 -2
- package/src/SchemaStateManager.ts +75 -0
- package/src/SchemaVersionBuilder.ts +9 -2
- package/src/SnapshotManager.ts +52 -0
- package/src/SystemClient.ts +41 -4
- package/src/index.ts +14 -12
- package/src/migrations.ts +1 -1
- package/tests/cases/unit/SchemaStateManager.test.ts +96 -0
- package/tests/cases/unit/SchemaVersionBuilder.test.ts +58 -0
- package/tests/cases/unit/SnapshotManager.test.ts +64 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@contember/migrations-client",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0-alpha.1",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"main": "./dist/production/index.js",
|
|
6
6
|
"typings": "./dist/types/index.d.ts",
|
|
@@ -26,11 +26,11 @@
|
|
|
26
26
|
"test": "echo 'No tests'"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@contember/engine-common": "2.1.0-beta.
|
|
30
|
-
"@contember/graphql-client": "2.1.0-beta.
|
|
31
|
-
"@contember/schema": "2.1.0-beta.
|
|
32
|
-
"@contember/schema-migrations": "2.1.0-beta.
|
|
33
|
-
"@contember/schema-utils": "2.1.0-beta.
|
|
29
|
+
"@contember/engine-common": "2.1.0-beta.2",
|
|
30
|
+
"@contember/graphql-client": "2.1.0-beta.2",
|
|
31
|
+
"@contember/schema": "2.1.0-beta.2",
|
|
32
|
+
"@contember/schema-migrations": "2.1.0-beta.2",
|
|
33
|
+
"@contember/schema-utils": "2.1.0-beta.2"
|
|
34
34
|
},
|
|
35
35
|
"devDependencies": {
|
|
36
36
|
"@types/node": "^24.12.0"
|
package/src/JsLoader.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { MigrationFileLoader } from './MigrationFileLoader'
|
|
2
|
-
import { MigrationParser } from './MigrationParser'
|
|
3
|
-
import { ContentMigration, ContentMigrationFactoryArgs, MigrationContent, MigrationFile } from './MigrationFile'
|
|
1
|
+
import { MigrationFileLoader } from './MigrationFileLoader.js'
|
|
2
|
+
import { MigrationParser } from './MigrationParser.js'
|
|
3
|
+
import { ContentMigration, ContentMigrationFactoryArgs, MigrationContent, MigrationFile } from './MigrationFile.js'
|
|
4
4
|
|
|
5
5
|
export class JsLoader implements MigrationFileLoader {
|
|
6
6
|
constructor(
|
package/src/JsonLoader.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { MigrationFileLoader } from './MigrationFileLoader'
|
|
1
|
+
import { MigrationFileLoader } from './MigrationFileLoader.js'
|
|
2
2
|
import * as fs from 'node:fs/promises'
|
|
3
|
-
import { MigrationParser } from './MigrationParser'
|
|
4
|
-
import { MigrationFile } from './MigrationFile'
|
|
3
|
+
import { MigrationParser } from './MigrationParser.js'
|
|
4
|
+
import { MigrationFile } from './MigrationFile.js'
|
|
5
5
|
|
|
6
6
|
export class JsonLoader implements MigrationFileLoader {
|
|
7
7
|
constructor(
|
package/src/MigrationCreator.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { MigrationFilesManager } from './MigrationFilesManager'
|
|
1
|
+
import { MigrationFilesManager } from './MigrationFilesManager.js'
|
|
2
2
|
import { Schema } from '@contember/schema'
|
|
3
3
|
import { Migration, SchemaDiffer, VERSION_LATEST } from '@contember/schema-migrations'
|
|
4
4
|
import { MigrationVersionHelper } from '@contember/engine-common'
|
|
@@ -25,11 +25,15 @@ export class MigrationCreator {
|
|
|
25
25
|
initialSchema: Schema,
|
|
26
26
|
newSchema: Schema,
|
|
27
27
|
migrationName: string,
|
|
28
|
-
{ skipInitialSchemaValidation = false
|
|
28
|
+
{ skipInitialSchemaValidation = false, skipNonModelDiffers = false, recreateViews = false }: {
|
|
29
|
+
skipInitialSchemaValidation?: boolean
|
|
30
|
+
skipNonModelDiffers?: boolean
|
|
31
|
+
recreateViews?: boolean
|
|
32
|
+
} = {},
|
|
29
33
|
): Promise<{ migration: Migration; initialSchema: Schema } | null> {
|
|
30
34
|
await this.migrationFilesManager.createDirIfNotExist()
|
|
31
35
|
|
|
32
|
-
const modifications = this.schemaDiffer.diffSchemas(initialSchema, newSchema, { skipInitialSchemaValidation })
|
|
36
|
+
const modifications = this.schemaDiffer.diffSchemas(initialSchema, newSchema, { skipInitialSchemaValidation, skipNonModelDiffers, recreateViews })
|
|
33
37
|
if (modifications.length === 0) {
|
|
34
38
|
return null
|
|
35
39
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { MigrationToExecuteOkStatus } from './migrations'
|
|
2
|
-
import { ContentMigrationFactoryArgs, ResolvedMigrationContent } from './MigrationFile'
|
|
3
|
-
import { assertNever } from './utils/assertNever'
|
|
4
|
-
import { SystemClient } from './SystemClient'
|
|
1
|
+
import { MigrationToExecuteOkStatus } from './migrations.js'
|
|
2
|
+
import { ContentMigrationFactoryArgs, ResolvedMigrationContent } from './MigrationFile.js'
|
|
3
|
+
import { assertNever } from './utils/assertNever.js'
|
|
4
|
+
import { SystemClient } from './SystemClient.js'
|
|
5
|
+
import { SchemaState } from './SchemaStateManager.js'
|
|
5
6
|
|
|
6
7
|
export class MigrationExecutor {
|
|
7
8
|
async executeMigrations({
|
|
@@ -9,6 +10,7 @@ export class MigrationExecutor {
|
|
|
9
10
|
migrations,
|
|
10
11
|
contentMigrationFactoryArgs,
|
|
11
12
|
force,
|
|
13
|
+
schemaState,
|
|
12
14
|
log = () => null,
|
|
13
15
|
}: {
|
|
14
16
|
client: SystemClient
|
|
@@ -16,14 +18,16 @@ export class MigrationExecutor {
|
|
|
16
18
|
contentMigrationFactoryArgs: Omit<ContentMigrationFactoryArgs, 'migration'>
|
|
17
19
|
log: (message: string) => void
|
|
18
20
|
force?: boolean
|
|
21
|
+
schemaState?: SchemaState
|
|
19
22
|
}): Promise<void> {
|
|
20
|
-
if (migrations.length === 0) {
|
|
23
|
+
if (migrations.length === 0 && !schemaState) {
|
|
21
24
|
return
|
|
22
25
|
}
|
|
23
26
|
let migrationsToRun: ResolvedMigrationContent[] = []
|
|
27
|
+
let batchSchemaState: SchemaState | undefined
|
|
24
28
|
|
|
25
29
|
const executeMigrations = async () => {
|
|
26
|
-
if (migrationsToRun.length === 0) {
|
|
30
|
+
if (migrationsToRun.length === 0 && !batchSchemaState) {
|
|
27
31
|
return
|
|
28
32
|
}
|
|
29
33
|
await client.migrate(
|
|
@@ -51,14 +55,18 @@ export class MigrationExecutor {
|
|
|
51
55
|
return assertNever(it)
|
|
52
56
|
}),
|
|
53
57
|
force,
|
|
58
|
+
batchSchemaState,
|
|
54
59
|
)
|
|
55
60
|
migrationsToRun.forEach(it => {
|
|
56
61
|
log(it.name)
|
|
57
62
|
})
|
|
58
63
|
migrationsToRun = []
|
|
64
|
+
batchSchemaState = undefined
|
|
59
65
|
}
|
|
60
66
|
|
|
61
|
-
|
|
67
|
+
if (migrations.length > 0) {
|
|
68
|
+
log('Executing...')
|
|
69
|
+
}
|
|
62
70
|
|
|
63
71
|
for (const migration of migrations) {
|
|
64
72
|
const migrationContent = await migration.localMigration.getContent()
|
|
@@ -74,7 +82,10 @@ export class MigrationExecutor {
|
|
|
74
82
|
migrationsToRun.push(migrationContent)
|
|
75
83
|
}
|
|
76
84
|
}
|
|
85
|
+
batchSchemaState = schemaState
|
|
77
86
|
await executeMigrations()
|
|
78
|
-
|
|
87
|
+
if (migrations.length > 0) {
|
|
88
|
+
log('Migration executed')
|
|
89
|
+
}
|
|
79
90
|
}
|
|
80
91
|
}
|
package/src/MigrationFile.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Migration } from '@contember/schema-migrations'
|
|
2
|
-
import { JSONObject } from './utils/json'
|
|
3
|
-
import { SchemaVersionBuilder } from './SchemaVersionBuilder'
|
|
2
|
+
import { JSONObject } from './utils/json.js'
|
|
3
|
+
import { SchemaVersionBuilder } from './SchemaVersionBuilder.js'
|
|
4
4
|
|
|
5
5
|
export interface MigrationFile {
|
|
6
6
|
path: string
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { MigrationVersionHelper } from '@contember/engine-common'
|
|
2
2
|
import * as fs from 'node:fs/promises'
|
|
3
|
-
import { MigrationFileLoader } from './MigrationFileLoader'
|
|
4
|
-
import { MigrationContent, MigrationFile } from './MigrationFile'
|
|
3
|
+
import { MigrationFileLoader } from './MigrationFileLoader.js'
|
|
4
|
+
import { MigrationContent, MigrationFile } from './MigrationFile.js'
|
|
5
5
|
import { join } from 'node:path'
|
|
6
6
|
import { Dirent } from 'node:fs'
|
|
7
7
|
|
package/src/MigrationParser.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Migration, VERSION_INITIAL } from '@contember/schema-migrations'
|
|
2
|
-
import { ContentMigrationQuery, MigrationFile, ResolvedMigrationContent } from './MigrationFile'
|
|
3
|
-
import { JSONValue } from './utils/json'
|
|
2
|
+
import { ContentMigrationQuery, MigrationFile, ResolvedMigrationContent } from './MigrationFile.js'
|
|
3
|
+
import { JSONValue } from './utils/json.js'
|
|
4
4
|
|
|
5
5
|
export class MigrationParser {
|
|
6
6
|
public parse(migrationFile: MigrationFile, content: JSONValue): ResolvedMigrationContent {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { MigrationFilesManager } from './MigrationFilesManager'
|
|
1
|
+
import { MigrationFilesManager } from './MigrationFilesManager.js'
|
|
2
2
|
import { Migration } from '@contember/schema-migrations'
|
|
3
|
-
import { isSchemaMigration, MigrationFile } from './MigrationFile'
|
|
3
|
+
import { isSchemaMigration, MigrationFile } from './MigrationFile.js'
|
|
4
4
|
import { MigrationVersionHelper } from '@contember/engine-common'
|
|
5
5
|
|
|
6
6
|
export class MigrationsResolver {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Acl, Actions, Schema, Settings, Validation } from '@contember/schema'
|
|
2
|
+
import * as fs from 'node:fs/promises'
|
|
3
|
+
import * as path from 'node:path'
|
|
4
|
+
|
|
5
|
+
export type SchemaState = {
|
|
6
|
+
acl: Acl.Schema
|
|
7
|
+
validation: Validation.Schema
|
|
8
|
+
actions: Actions.Schema
|
|
9
|
+
settings: Settings.Schema
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const stateFiles = ['acl', 'validation', 'actions', 'settings'] as const
|
|
13
|
+
type StateKey = typeof stateFiles[number]
|
|
14
|
+
|
|
15
|
+
export class SchemaStateManager {
|
|
16
|
+
constructor(private readonly stateDir: string) {}
|
|
17
|
+
|
|
18
|
+
async isStateMode(): Promise<boolean> {
|
|
19
|
+
try {
|
|
20
|
+
const stat = await fs.stat(this.stateDir)
|
|
21
|
+
return stat.isDirectory()
|
|
22
|
+
} catch {
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async readState(): Promise<SchemaState> {
|
|
28
|
+
const state: Partial<SchemaState> = {}
|
|
29
|
+
for (const key of stateFiles) {
|
|
30
|
+
const filePath = path.join(this.stateDir, `${key}.json`)
|
|
31
|
+
try {
|
|
32
|
+
const content = await fs.readFile(filePath, 'utf8')
|
|
33
|
+
state[key] = JSON.parse(content)
|
|
34
|
+
} catch (e) {
|
|
35
|
+
throw new Error(`Failed to read schema state file ${filePath}: ${e instanceof Error ? e.message : e}`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return state as SchemaState
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async writeState(state: SchemaState, { dryRun = false }: { dryRun?: boolean } = {}): Promise<boolean> {
|
|
42
|
+
let changed = false
|
|
43
|
+
for (const key of stateFiles) {
|
|
44
|
+
const filePath = path.join(this.stateDir, `${key}.json`)
|
|
45
|
+
const newContent = JSON.stringify(state[key], undefined, '\t') + '\n'
|
|
46
|
+
let currentContent: string | null = null
|
|
47
|
+
try {
|
|
48
|
+
currentContent = await fs.readFile(filePath, 'utf8')
|
|
49
|
+
} catch {
|
|
50
|
+
// file doesn't exist yet
|
|
51
|
+
}
|
|
52
|
+
if (currentContent !== newContent) {
|
|
53
|
+
if (!dryRun) {
|
|
54
|
+
await fs.writeFile(filePath, newContent, 'utf8')
|
|
55
|
+
}
|
|
56
|
+
changed = true
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return changed
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async extractState(schema: Schema): Promise<void> {
|
|
63
|
+
await fs.mkdir(this.stateDir, { recursive: true })
|
|
64
|
+
await this.writeState(SchemaStateManager.schemaStateFromSchema(schema))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static schemaStateFromSchema(schema: Schema): SchemaState {
|
|
68
|
+
return {
|
|
69
|
+
acl: schema.acl,
|
|
70
|
+
validation: schema.validation,
|
|
71
|
+
actions: schema.actions,
|
|
72
|
+
settings: schema.settings,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import { Schema } from '@contember/schema'
|
|
2
2
|
import { emptySchema } from '@contember/schema-utils'
|
|
3
3
|
import { SchemaMigrator } from '@contember/schema-migrations'
|
|
4
|
-
import { MigrationsResolver } from './MigrationsResolver'
|
|
4
|
+
import { MigrationsResolver } from './MigrationsResolver.js'
|
|
5
|
+
import { SchemaStateManager } from './SchemaStateManager.js'
|
|
5
6
|
|
|
6
7
|
export class SchemaVersionBuilder {
|
|
7
8
|
constructor(
|
|
8
9
|
private readonly migrationsResolver: MigrationsResolver,
|
|
9
10
|
private readonly schemaMigrator: SchemaMigrator,
|
|
11
|
+
private readonly schemaStateManager: SchemaStateManager,
|
|
10
12
|
) {}
|
|
11
13
|
|
|
12
14
|
async buildSchema(targetVersion?: string): Promise<Schema> {
|
|
13
|
-
|
|
15
|
+
let schema = await this.buildSchemaAdvanced(emptySchema, version => !targetVersion || version <= targetVersion)
|
|
16
|
+
if (!targetVersion && await this.schemaStateManager.isStateMode()) {
|
|
17
|
+
const state = await this.schemaStateManager.readState()
|
|
18
|
+
schema = { ...schema, ...state }
|
|
19
|
+
}
|
|
20
|
+
return schema
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
async buildSchemaUntil(targetVersion: string): Promise<Schema> {
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Migration } from '@contember/schema-migrations'
|
|
2
|
+
import * as fs from 'node:fs/promises'
|
|
3
|
+
|
|
4
|
+
export type CoveredMigration = {
|
|
5
|
+
version: string
|
|
6
|
+
name: string
|
|
7
|
+
type: 'schema' | 'content'
|
|
8
|
+
/** schema migration checksum, or null for content migrations (mirrors the executed registry) */
|
|
9
|
+
checksum: string | null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type SnapshotFile = {
|
|
13
|
+
/** last covered migration version (X) */
|
|
14
|
+
version: string
|
|
15
|
+
/** whether the project used schema state mode when the snapshot was created */
|
|
16
|
+
stateMode: boolean
|
|
17
|
+
/** collapsed schema (empty -> X), applied in a single step */
|
|
18
|
+
snapshot: {
|
|
19
|
+
formatVersion: number
|
|
20
|
+
modifications: Migration.Modification[]
|
|
21
|
+
}
|
|
22
|
+
/** migrations subsumed by the snapshot, recorded as executed without replaying them */
|
|
23
|
+
covers: CoveredMigration[]
|
|
24
|
+
/** versions of content migrations within the covered range whose data is NOT reproduced */
|
|
25
|
+
contentMigrations: string[]
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export class SnapshotManager {
|
|
29
|
+
constructor(private readonly snapshotPath: string) {}
|
|
30
|
+
|
|
31
|
+
async exists(): Promise<boolean> {
|
|
32
|
+
try {
|
|
33
|
+
const stat = await fs.stat(this.snapshotPath)
|
|
34
|
+
return stat.isFile()
|
|
35
|
+
} catch {
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async read(): Promise<SnapshotFile> {
|
|
41
|
+
try {
|
|
42
|
+
const content = await fs.readFile(this.snapshotPath, 'utf8')
|
|
43
|
+
return JSON.parse(content) as SnapshotFile
|
|
44
|
+
} catch (e) {
|
|
45
|
+
throw new Error(`Failed to read snapshot file ${this.snapshotPath}: ${e instanceof Error ? e.message : e}`)
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async write(snapshot: SnapshotFile): Promise<void> {
|
|
50
|
+
await fs.writeFile(this.snapshotPath, JSON.stringify(snapshot, undefined, '\t') + '\n', 'utf8')
|
|
51
|
+
}
|
|
52
|
+
}
|
package/src/SystemClient.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { GraphQlClient } from '@contember/graphql-client'
|
|
2
|
-
import { ExecutedMigration, ExecutedMigrationInfo } from './migrations'
|
|
2
|
+
import { ExecutedMigration, ExecutedMigrationInfo } from './migrations.js'
|
|
3
|
+
import { SchemaState } from './SchemaStateManager.js'
|
|
3
4
|
|
|
4
5
|
export type MigrateError = {
|
|
5
6
|
readonly code: MigrateErrorCode
|
|
@@ -13,6 +14,13 @@ export enum MigrateErrorCode {
|
|
|
13
14
|
InvalidFormat = 'INVALID_FORMAT',
|
|
14
15
|
InvalidSchema = 'INVALID_SCHEMA',
|
|
15
16
|
MigrationFailed = 'MIGRATION_FAILED',
|
|
17
|
+
ProjectNotEmpty = 'PROJECT_NOT_EMPTY',
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type SnapshotInput = {
|
|
21
|
+
formatVersion: number
|
|
22
|
+
modifications: unknown[]
|
|
23
|
+
covers: unknown[]
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
export type MigrateResponse = {
|
|
@@ -28,10 +36,10 @@ export class SystemClient {
|
|
|
28
36
|
return new SystemClient(graphqlClient)
|
|
29
37
|
}
|
|
30
38
|
|
|
31
|
-
public async migrate(migrations: any[], force = false): Promise<void> {
|
|
39
|
+
public async migrate(migrations: any[], force = false, schemaState?: SchemaState): Promise<void> {
|
|
32
40
|
const query = `
|
|
33
|
-
mutation($migrations: [Migration!]
|
|
34
|
-
migrate: ${force ? 'forceMigrate' : 'migrate'}(migrations: $migrations) {
|
|
41
|
+
mutation($migrations: [Migration!]!, $schemaState: SchemaStateInput) {
|
|
42
|
+
migrate: ${force ? 'forceMigrate' : 'migrate'}(migrations: $migrations, schemaState: $schemaState) {
|
|
35
43
|
ok
|
|
36
44
|
errors {
|
|
37
45
|
code
|
|
@@ -47,6 +55,7 @@ mutation($migrations: [Migration!]!) {
|
|
|
47
55
|
}>(query, {
|
|
48
56
|
variables: {
|
|
49
57
|
migrations,
|
|
58
|
+
schemaState: schemaState ?? null,
|
|
50
59
|
},
|
|
51
60
|
})
|
|
52
61
|
).migrate
|
|
@@ -55,6 +64,34 @@ mutation($migrations: [Migration!]!) {
|
|
|
55
64
|
}
|
|
56
65
|
}
|
|
57
66
|
|
|
67
|
+
public async migrateFromSnapshot(snapshot: SnapshotInput, schemaState?: SchemaState): Promise<void> {
|
|
68
|
+
const query = `
|
|
69
|
+
mutation($snapshot: SnapshotInput!, $schemaState: SchemaStateInput) {
|
|
70
|
+
migrateFromSnapshot(snapshot: $snapshot, schemaState: $schemaState) {
|
|
71
|
+
ok
|
|
72
|
+
errors {
|
|
73
|
+
code
|
|
74
|
+
migration
|
|
75
|
+
message: developerMessage
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
`
|
|
80
|
+
const result = (
|
|
81
|
+
await this.apiClient.execute<{
|
|
82
|
+
migrateFromSnapshot: { ok: boolean; errors: { code: MigrateErrorCode; migration: string; message: string }[] }
|
|
83
|
+
}>(query, {
|
|
84
|
+
variables: {
|
|
85
|
+
snapshot,
|
|
86
|
+
schemaState: schemaState ?? null,
|
|
87
|
+
},
|
|
88
|
+
})
|
|
89
|
+
).migrateFromSnapshot
|
|
90
|
+
if (!result.ok) {
|
|
91
|
+
throw result.errors
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
58
95
|
public async migrationDelete(version: string): Promise<void> {
|
|
59
96
|
const query = `
|
|
60
97
|
mutation($version: String!) {
|
package/src/index.ts
CHANGED
|
@@ -7,15 +7,17 @@ export {
|
|
|
7
7
|
VERSION_LATEST,
|
|
8
8
|
} from '@contember/schema-migrations'
|
|
9
9
|
export type { Migration, MigrationInfo } from '@contember/schema-migrations'
|
|
10
|
-
export * from './JsLoader'
|
|
11
|
-
export * from './JsonLoader'
|
|
12
|
-
export * from './MigrationCreator'
|
|
13
|
-
export * from './MigrationExecuteHelper'
|
|
14
|
-
export * from './MigrationFile'
|
|
15
|
-
export * from './MigrationFileLoader'
|
|
16
|
-
export * from './MigrationFilesManager'
|
|
17
|
-
export * from './MigrationParser'
|
|
18
|
-
export * from './migrations'
|
|
19
|
-
export * from './MigrationsResolver'
|
|
20
|
-
export * from './
|
|
21
|
-
export * from './
|
|
10
|
+
export * from './JsLoader.js'
|
|
11
|
+
export * from './JsonLoader.js'
|
|
12
|
+
export * from './MigrationCreator.js'
|
|
13
|
+
export * from './MigrationExecuteHelper.js'
|
|
14
|
+
export * from './MigrationFile.js'
|
|
15
|
+
export * from './MigrationFileLoader.js'
|
|
16
|
+
export * from './MigrationFilesManager.js'
|
|
17
|
+
export * from './MigrationParser.js'
|
|
18
|
+
export * from './migrations.js'
|
|
19
|
+
export * from './MigrationsResolver.js'
|
|
20
|
+
export * from './SchemaStateManager.js'
|
|
21
|
+
export * from './SchemaVersionBuilder.js'
|
|
22
|
+
export * from './SnapshotManager.js'
|
|
23
|
+
export * from './SystemClient.js'
|
package/src/migrations.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { calculateMigrationChecksum, Migration, MigrationInfo } from '@contember/schema-migrations'
|
|
2
|
-
import { isSchemaMigration, MigrationFile } from './MigrationFile'
|
|
2
|
+
import { isSchemaMigration, MigrationFile } from './MigrationFile.js'
|
|
3
3
|
|
|
4
4
|
export interface ExecutedMigrationInfo extends MigrationInfo {
|
|
5
5
|
readonly executedAt: Date
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import * as fs from 'node:fs/promises'
|
|
3
|
+
import * as os from 'node:os'
|
|
4
|
+
import * as path from 'node:path'
|
|
5
|
+
import { SchemaStateManager } from '../../../src/index.js'
|
|
6
|
+
import { emptySchema } from '@contember/schema-utils'
|
|
7
|
+
import { Schema } from '@contember/schema'
|
|
8
|
+
|
|
9
|
+
const schemaWithAcl: Schema = {
|
|
10
|
+
...emptySchema,
|
|
11
|
+
acl: {
|
|
12
|
+
roles: {
|
|
13
|
+
admin: { variables: {}, stages: '*', entities: {} },
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
settings: { content: { useExistsInHasManyFilter: true } },
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe('SchemaStateManager', () => {
|
|
20
|
+
let baseDir: string
|
|
21
|
+
let stateDir: string
|
|
22
|
+
let manager: SchemaStateManager
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
baseDir = await fs.mkdtemp(path.join(os.tmpdir(), 'contember-state-'))
|
|
26
|
+
stateDir = path.join(baseDir, 'state')
|
|
27
|
+
manager = new SchemaStateManager(stateDir)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
afterEach(async () => {
|
|
31
|
+
await fs.rm(baseDir, { recursive: true, force: true })
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('isStateMode is false when the state directory does not exist', async () => {
|
|
35
|
+
expect(await manager.isStateMode()).toBe(false)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test('isStateMode is false when the path is a file, not a directory', async () => {
|
|
39
|
+
await fs.writeFile(stateDir, 'not a dir')
|
|
40
|
+
expect(await manager.isStateMode()).toBe(false)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
test('extractState creates the directory and round-trips through readState', async () => {
|
|
44
|
+
await manager.extractState(schemaWithAcl)
|
|
45
|
+
|
|
46
|
+
expect(await manager.isStateMode()).toBe(true)
|
|
47
|
+
|
|
48
|
+
const state = await manager.readState()
|
|
49
|
+
expect(state.acl).toStrictEqual(schemaWithAcl.acl)
|
|
50
|
+
expect(state.validation).toStrictEqual(schemaWithAcl.validation)
|
|
51
|
+
expect(state.actions).toStrictEqual(schemaWithAcl.actions)
|
|
52
|
+
expect(state.settings).toStrictEqual(schemaWithAcl.settings)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('extractState writes one JSON file per state part', async () => {
|
|
56
|
+
await manager.extractState(schemaWithAcl)
|
|
57
|
+
|
|
58
|
+
const files = (await fs.readdir(stateDir)).sort()
|
|
59
|
+
expect(files).toStrictEqual(['acl.json', 'actions.json', 'settings.json', 'validation.json'])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
test('writeState in dryRun mode reports a change but does not touch the filesystem', async () => {
|
|
63
|
+
await fs.mkdir(stateDir, { recursive: true })
|
|
64
|
+
|
|
65
|
+
const changed = await manager.writeState(SchemaStateManager.schemaStateFromSchema(schemaWithAcl), { dryRun: true })
|
|
66
|
+
|
|
67
|
+
expect(changed).toBe(true)
|
|
68
|
+
expect(await fs.readdir(stateDir)).toStrictEqual([])
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('writeState is idempotent: a second identical write reports no change', async () => {
|
|
72
|
+
const state = SchemaStateManager.schemaStateFromSchema(schemaWithAcl)
|
|
73
|
+
await fs.mkdir(stateDir, { recursive: true })
|
|
74
|
+
|
|
75
|
+
expect(await manager.writeState(state)).toBe(true)
|
|
76
|
+
expect(await manager.writeState(state)).toBe(false)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
test('writeState detects a change when a state part differs', async () => {
|
|
80
|
+
await manager.extractState(schemaWithAcl)
|
|
81
|
+
|
|
82
|
+
const modified = SchemaStateManager.schemaStateFromSchema({
|
|
83
|
+
...schemaWithAcl,
|
|
84
|
+
acl: { roles: {} },
|
|
85
|
+
})
|
|
86
|
+
expect(await manager.writeState(modified, { dryRun: true })).toBe(true)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('readState throws a descriptive error when a state file is missing', async () => {
|
|
90
|
+
await fs.mkdir(stateDir, { recursive: true })
|
|
91
|
+
await fs.writeFile(path.join(stateDir, 'acl.json'), JSON.stringify({ roles: {} }))
|
|
92
|
+
// validation.json / actions.json / settings.json are missing
|
|
93
|
+
|
|
94
|
+
await expect(manager.readState()).rejects.toThrow(/Failed to read schema state file/)
|
|
95
|
+
})
|
|
96
|
+
})
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
|
2
|
+
import * as fs from 'node:fs/promises'
|
|
3
|
+
import * as os from 'node:os'
|
|
4
|
+
import * as path from 'node:path'
|
|
5
|
+
import { MigrationsResolver, SchemaStateManager, SchemaVersionBuilder } from '../../../src/index.js'
|
|
6
|
+
import { ModificationHandlerFactory, SchemaMigrator } from '@contember/schema-migrations'
|
|
7
|
+
import { emptySchema } from '@contember/schema-utils'
|
|
8
|
+
import { Schema } from '@contember/schema'
|
|
9
|
+
|
|
10
|
+
const schemaMigrator = new SchemaMigrator(new ModificationHandlerFactory(ModificationHandlerFactory.defaultFactoryMap))
|
|
11
|
+
|
|
12
|
+
// No migrations on disk — buildSchemaAdvanced reduces to the initial (empty) schema, isolating the state-merge behavior.
|
|
13
|
+
const emptyResolver = { getSchemaMigrations: async () => [] } as unknown as MigrationsResolver
|
|
14
|
+
|
|
15
|
+
const stateAcl: Schema['acl'] = { roles: { admin: { variables: {}, stages: '*', entities: {} } } }
|
|
16
|
+
|
|
17
|
+
describe('SchemaVersionBuilder schema state merge', () => {
|
|
18
|
+
let baseDir: string
|
|
19
|
+
let stateDir: string
|
|
20
|
+
let stateManager: SchemaStateManager
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
baseDir = await fs.mkdtemp(path.join(os.tmpdir(), 'contember-svb-'))
|
|
24
|
+
stateDir = path.join(baseDir, 'state')
|
|
25
|
+
stateManager = new SchemaStateManager(stateDir)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
await fs.rm(baseDir, { recursive: true, force: true })
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('buildSchema does not merge state when state mode is off', async () => {
|
|
33
|
+
const builder = new SchemaVersionBuilder(emptyResolver, schemaMigrator, stateManager)
|
|
34
|
+
|
|
35
|
+
const schema = await builder.buildSchema()
|
|
36
|
+
|
|
37
|
+
expect(schema.acl).toStrictEqual(emptySchema.acl)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('buildSchema merges state from disk when state mode is on', async () => {
|
|
41
|
+
await stateManager.extractState({ ...emptySchema, acl: stateAcl })
|
|
42
|
+
const builder = new SchemaVersionBuilder(emptyResolver, schemaMigrator, stateManager)
|
|
43
|
+
|
|
44
|
+
const schema = await builder.buildSchema()
|
|
45
|
+
|
|
46
|
+
expect(schema.acl).toStrictEqual(stateAcl)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
test('buildSchema with a target version skips the state merge', async () => {
|
|
50
|
+
await stateManager.extractState({ ...emptySchema, acl: stateAcl })
|
|
51
|
+
const builder = new SchemaVersionBuilder(emptyResolver, schemaMigrator, stateManager)
|
|
52
|
+
|
|
53
|
+
// The merge is guarded by `!targetVersion`, so requesting a historical version must not pull in current state.
|
|
54
|
+
const schema = await builder.buildSchema('2024-01-01-000000')
|
|
55
|
+
|
|
56
|
+
expect(schema.acl).toStrictEqual(emptySchema.acl)
|
|
57
|
+
})
|
|
58
|
+
})
|