@contember/migrations-client 2.1.0-beta.2 → 2.2.0-alpha.2

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.
Files changed (136) hide show
  1. package/dist/development/index.cjs +4 -0
  2. package/dist/development/index.cjs.map +1 -1
  3. package/dist/development/index.js +4 -0
  4. package/dist/development/index.js.map +1 -1
  5. package/dist/development/src/JsLoader.cjs.map +1 -1
  6. package/dist/development/src/JsLoader.js.map +1 -1
  7. package/dist/development/src/JsonLoader.cjs.map +1 -1
  8. package/dist/development/src/JsonLoader.js.map +1 -1
  9. package/dist/development/src/MigrationCreator.cjs +2 -2
  10. package/dist/development/src/MigrationCreator.cjs.map +1 -1
  11. package/dist/development/src/MigrationCreator.js +2 -2
  12. package/dist/development/src/MigrationCreator.js.map +1 -1
  13. package/dist/development/src/MigrationExecuteHelper.cjs +14 -5
  14. package/dist/development/src/MigrationExecuteHelper.cjs.map +1 -1
  15. package/dist/development/src/MigrationExecuteHelper.js +14 -5
  16. package/dist/development/src/MigrationExecuteHelper.js.map +1 -1
  17. package/dist/development/src/MigrationFile.cjs.map +1 -1
  18. package/dist/development/src/MigrationFile.js.map +1 -1
  19. package/dist/development/src/MigrationFilesManager.cjs +11 -11
  20. package/dist/development/src/MigrationFilesManager.cjs.map +1 -1
  21. package/dist/development/src/MigrationFilesManager.js.map +1 -1
  22. package/dist/development/src/MigrationParser.cjs.map +1 -1
  23. package/dist/development/src/MigrationParser.js.map +1 -1
  24. package/dist/development/src/MigrationsResolver.cjs.map +1 -1
  25. package/dist/development/src/MigrationsResolver.js.map +1 -1
  26. package/dist/development/src/SchemaStateManager.cjs +82 -0
  27. package/dist/development/src/SchemaStateManager.cjs.map +1 -0
  28. package/dist/development/src/SchemaStateManager.js +64 -0
  29. package/dist/development/src/SchemaStateManager.js.map +1 -0
  30. package/dist/development/src/SchemaVersionBuilder.cjs +8 -2
  31. package/dist/development/src/SchemaVersionBuilder.cjs.map +1 -1
  32. package/dist/development/src/SchemaVersionBuilder.js +8 -2
  33. package/dist/development/src/SchemaVersionBuilder.js.map +1 -1
  34. package/dist/development/src/SnapshotManager.cjs +46 -0
  35. package/dist/development/src/SnapshotManager.cjs.map +1 -0
  36. package/dist/development/src/SnapshotManager.js +29 -0
  37. package/dist/development/src/SnapshotManager.js.map +1 -0
  38. package/dist/development/src/SystemClient.cjs +29 -4
  39. package/dist/development/src/SystemClient.cjs.map +1 -1
  40. package/dist/development/src/SystemClient.js +29 -4
  41. package/dist/development/src/SystemClient.js.map +1 -1
  42. package/dist/development/src/migrations.cjs.map +1 -1
  43. package/dist/development/src/migrations.js.map +1 -1
  44. package/dist/production/index.cjs +4 -0
  45. package/dist/production/index.cjs.map +1 -1
  46. package/dist/production/index.js +4 -0
  47. package/dist/production/index.js.map +1 -1
  48. package/dist/production/src/JsLoader.cjs.map +1 -1
  49. package/dist/production/src/JsLoader.js.map +1 -1
  50. package/dist/production/src/JsonLoader.cjs.map +1 -1
  51. package/dist/production/src/JsonLoader.js.map +1 -1
  52. package/dist/production/src/MigrationCreator.cjs +2 -2
  53. package/dist/production/src/MigrationCreator.cjs.map +1 -1
  54. package/dist/production/src/MigrationCreator.js +2 -2
  55. package/dist/production/src/MigrationCreator.js.map +1 -1
  56. package/dist/production/src/MigrationExecuteHelper.cjs +14 -5
  57. package/dist/production/src/MigrationExecuteHelper.cjs.map +1 -1
  58. package/dist/production/src/MigrationExecuteHelper.js +14 -5
  59. package/dist/production/src/MigrationExecuteHelper.js.map +1 -1
  60. package/dist/production/src/MigrationFile.cjs.map +1 -1
  61. package/dist/production/src/MigrationFile.js.map +1 -1
  62. package/dist/production/src/MigrationFilesManager.cjs +11 -11
  63. package/dist/production/src/MigrationFilesManager.cjs.map +1 -1
  64. package/dist/production/src/MigrationFilesManager.js.map +1 -1
  65. package/dist/production/src/MigrationParser.cjs.map +1 -1
  66. package/dist/production/src/MigrationParser.js.map +1 -1
  67. package/dist/production/src/MigrationsResolver.cjs.map +1 -1
  68. package/dist/production/src/MigrationsResolver.js.map +1 -1
  69. package/dist/production/src/SchemaStateManager.cjs +82 -0
  70. package/dist/production/src/SchemaStateManager.cjs.map +1 -0
  71. package/dist/production/src/SchemaStateManager.js +64 -0
  72. package/dist/production/src/SchemaStateManager.js.map +1 -0
  73. package/dist/production/src/SchemaVersionBuilder.cjs +8 -2
  74. package/dist/production/src/SchemaVersionBuilder.cjs.map +1 -1
  75. package/dist/production/src/SchemaVersionBuilder.js +8 -2
  76. package/dist/production/src/SchemaVersionBuilder.js.map +1 -1
  77. package/dist/production/src/SnapshotManager.cjs +46 -0
  78. package/dist/production/src/SnapshotManager.cjs.map +1 -0
  79. package/dist/production/src/SnapshotManager.js +29 -0
  80. package/dist/production/src/SnapshotManager.js.map +1 -0
  81. package/dist/production/src/SystemClient.cjs +29 -4
  82. package/dist/production/src/SystemClient.cjs.map +1 -1
  83. package/dist/production/src/SystemClient.js +29 -4
  84. package/dist/production/src/SystemClient.js.map +1 -1
  85. package/dist/production/src/migrations.cjs.map +1 -1
  86. package/dist/production/src/migrations.js.map +1 -1
  87. package/dist/types/JsLoader.d.ts +3 -3
  88. package/dist/types/JsLoader.d.ts.map +1 -1
  89. package/dist/types/JsonLoader.d.ts +4 -4
  90. package/dist/types/JsonLoader.d.ts.map +1 -1
  91. package/dist/types/MigrationCreator.d.ts +4 -2
  92. package/dist/types/MigrationCreator.d.ts.map +1 -1
  93. package/dist/types/MigrationExecuteHelper.d.ts +6 -4
  94. package/dist/types/MigrationExecuteHelper.d.ts.map +1 -1
  95. package/dist/types/MigrationFile.d.ts +2 -2
  96. package/dist/types/MigrationFile.d.ts.map +1 -1
  97. package/dist/types/MigrationFileLoader.d.ts +1 -1
  98. package/dist/types/MigrationFileLoader.d.ts.map +1 -1
  99. package/dist/types/MigrationFilesManager.d.ts +2 -2
  100. package/dist/types/MigrationFilesManager.d.ts.map +1 -1
  101. package/dist/types/MigrationParser.d.ts +2 -2
  102. package/dist/types/MigrationParser.d.ts.map +1 -1
  103. package/dist/types/MigrationsResolver.d.ts +2 -2
  104. package/dist/types/MigrationsResolver.d.ts.map +1 -1
  105. package/dist/types/SchemaStateManager.d.ts +19 -0
  106. package/dist/types/SchemaStateManager.d.ts.map +1 -0
  107. package/dist/types/SchemaVersionBuilder.d.ts +4 -2
  108. package/dist/types/SchemaVersionBuilder.d.ts.map +1 -1
  109. package/dist/types/SnapshotManager.d.ts +31 -0
  110. package/dist/types/SnapshotManager.d.ts.map +1 -0
  111. package/dist/types/SystemClient.d.ts +11 -3
  112. package/dist/types/SystemClient.d.ts.map +1 -1
  113. package/dist/types/index.d.ts +14 -12
  114. package/dist/types/index.d.ts.map +1 -1
  115. package/dist/types/migrations.d.ts +1 -1
  116. package/dist/types/migrations.d.ts.map +1 -1
  117. package/dist/types/tsconfig.tsbuildinfo +1 -1
  118. package/package.json +6 -6
  119. package/src/JsLoader.ts +3 -3
  120. package/src/JsonLoader.ts +3 -3
  121. package/src/MigrationCreator.ts +7 -3
  122. package/src/MigrationExecuteHelper.ts +19 -8
  123. package/src/MigrationFile.ts +2 -2
  124. package/src/MigrationFileLoader.ts +1 -1
  125. package/src/MigrationFilesManager.ts +2 -2
  126. package/src/MigrationParser.ts +2 -2
  127. package/src/MigrationsResolver.ts +2 -2
  128. package/src/SchemaStateManager.ts +75 -0
  129. package/src/SchemaVersionBuilder.ts +9 -2
  130. package/src/SnapshotManager.ts +52 -0
  131. package/src/SystemClient.ts +41 -4
  132. package/src/index.ts +14 -12
  133. package/src/migrations.ts +1 -1
  134. package/tests/cases/unit/SchemaStateManager.test.ts +96 -0
  135. package/tests/cases/unit/SchemaVersionBuilder.test.ts +58 -0
  136. 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.1.0-beta.2",
3
+ "version": "2.2.0-alpha.2",
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.1",
30
- "@contember/graphql-client": "2.1.0-beta.1",
31
- "@contember/schema": "2.1.0-beta.1",
32
- "@contember/schema-migrations": "2.1.0-beta.1",
33
- "@contember/schema-utils": "2.1.0-beta.1"
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(
@@ -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 }: { skipInitialSchemaValidation?: boolean } = {},
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
- log('Executing...')
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
- log('Migration executed')
87
+ if (migrations.length > 0) {
88
+ log('Migration executed')
89
+ }
79
90
  }
80
91
  }
@@ -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,4 +1,4 @@
1
- import { MigrationContent, MigrationFile } from './MigrationFile'
1
+ import { MigrationContent, MigrationFile } from './MigrationFile.js'
2
2
 
3
3
  export interface MigrationFileLoader {
4
4
  load(file: MigrationFile): Promise<MigrationContent>
@@ -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
 
@@ -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
- return this.buildSchemaAdvanced(emptySchema, version => !targetVersion || version <= targetVersion)
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
+ }
@@ -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 './SchemaVersionBuilder'
21
- export * from './SystemClient'
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
+ })