@contember/migrations-client 2.2.0-alpha.7 → 2.2.0-alpha.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@contember/migrations-client",
3
- "version": "2.2.0-alpha.7",
3
+ "version": "2.2.0-alpha.9",
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.2.0-alpha.7",
30
- "@contember/graphql-client": "2.2.0-alpha.7",
31
- "@contember/schema": "2.2.0-alpha.7",
32
- "@contember/schema-migrations": "2.2.0-alpha.7",
33
- "@contember/schema-utils": "2.2.0-alpha.7"
29
+ "@contember/engine-common": "2.2.0-alpha.9",
30
+ "@contember/graphql-client": "2.2.0-alpha.9",
31
+ "@contember/schema": "2.2.0-alpha.9",
32
+ "@contember/schema-migrations": "2.2.0-alpha.9",
33
+ "@contember/schema-utils": "2.2.0-alpha.9"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@types/node": "^24.12.0"
@@ -1,10 +1,12 @@
1
1
  import { Schema } from '@contember/schema'
2
2
  import { emptySchema } from '@contember/schema-utils'
3
- import { SchemaMigrator } from '@contember/schema-migrations'
3
+ import { Migration, SchemaMigrator } from '@contember/schema-migrations'
4
4
  import { MigrationsResolver } from './MigrationsResolver.js'
5
5
  import { SchemaStateManager } from './SchemaStateManager.js'
6
6
 
7
7
  export class SchemaVersionBuilder {
8
+ private lastBuildUntil: { migrations: Migration[]; schema: Schema } | null = null
9
+
8
10
  constructor(
9
11
  private readonly migrationsResolver: MigrationsResolver,
10
12
  private readonly schemaMigrator: SchemaMigrator,
@@ -20,8 +22,21 @@ export class SchemaVersionBuilder {
20
22
  return schema
21
23
  }
22
24
 
25
+ /**
26
+ * Content migrations call this once each, with ascending versions. Continuing from the previous
27
+ * build keeps a run linear instead of replaying every schema migration from scratch each time.
28
+ */
23
29
  async buildSchemaUntil(targetVersion: string): Promise<Schema> {
24
- return this.buildSchemaAdvanced(emptySchema, version => version < targetVersion)
30
+ const migrations = (await this.migrationsResolver.getSchemaMigrations()).filter(({ version }) => version < targetVersion)
31
+ const previous = this.lastBuildUntil
32
+ const canContinue = previous !== null
33
+ && previous.migrations.length <= migrations.length
34
+ && previous.migrations.every((migration, index) => migration === migrations[index])
35
+ const start = canContinue ? previous : { migrations: [], schema: emptySchema }
36
+
37
+ const schema = this.applyMigrations(start.schema, migrations.slice(start.migrations.length))
38
+ this.lastBuildUntil = { migrations, schema }
39
+ return schema
25
40
  }
26
41
 
27
42
  async continue(schema: Schema, previousVersion: string | null, targetVersion: string): Promise<Schema> {
@@ -29,11 +44,14 @@ export class SchemaVersionBuilder {
29
44
  }
30
45
 
31
46
  public async buildSchemaAdvanced(initialSchema: Schema, condition: (version: string) => boolean): Promise<Schema> {
32
- return (await this.migrationsResolver.getSchemaMigrations())
33
- .filter(({ version }) => condition(version))
34
- .reduce<Schema>(
35
- (schema, { modifications, formatVersion }) => this.schemaMigrator.applyModifications(schema, modifications, formatVersion),
36
- initialSchema,
37
- )
47
+ const migrations = (await this.migrationsResolver.getSchemaMigrations()).filter(({ version }) => condition(version))
48
+ return this.applyMigrations(initialSchema, migrations)
49
+ }
50
+
51
+ private applyMigrations(initialSchema: Schema, migrations: Migration[]): Schema {
52
+ return migrations.reduce<Schema>(
53
+ (schema, { modifications, formatVersion }) => this.schemaMigrator.applyModifications(schema, modifications, formatVersion),
54
+ initialSchema,
55
+ )
38
56
  }
39
57
  }
@@ -2,8 +2,15 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
2
2
  import * as fs from 'node:fs/promises'
3
3
  import * as os from 'node:os'
4
4
  import * as path from 'node:path'
5
- import { MigrationsResolver, SchemaStateManager, SchemaVersionBuilder } from '../../../src/index.js'
6
- import { ModificationHandlerFactory, SchemaMigrator } from '@contember/schema-migrations'
5
+ import {
6
+ JsonLoader,
7
+ MigrationFilesManager,
8
+ MigrationParser,
9
+ MigrationsResolver,
10
+ SchemaStateManager,
11
+ SchemaVersionBuilder,
12
+ } from '../../../src/index.js'
13
+ import { Migration, ModificationHandlerFactory, SchemaMigrator } from '@contember/schema-migrations'
7
14
  import { emptySchema } from '@contember/schema-utils'
8
15
  import { Schema } from '@contember/schema'
9
16
 
@@ -56,3 +63,63 @@ describe('SchemaVersionBuilder schema state merge', () => {
56
63
  expect(schema.acl).toStrictEqual(emptySchema.acl)
57
64
  })
58
65
  })
66
+
67
+ class CountingSchemaMigrator extends SchemaMigrator {
68
+ public appliedMigrations = 0
69
+
70
+ constructor() {
71
+ super(new ModificationHandlerFactory(ModificationHandlerFactory.defaultFactoryMap))
72
+ }
73
+
74
+ public override applyModifications(schema: Schema, diff: readonly Migration.Modification[], formatVersion: number): Schema {
75
+ this.appliedMigrations++
76
+ return super.applyModifications(schema, diff, formatVersion)
77
+ }
78
+ }
79
+
80
+ describe('SchemaVersionBuilder buildSchemaUntil', () => {
81
+ let migrationsDir: string
82
+ let filesManager: MigrationFilesManager
83
+ let migrator: CountingSchemaMigrator
84
+ let builder: SchemaVersionBuilder
85
+
86
+ const createEnumMigration = (version: string, enumName: string) =>
87
+ filesManager.createFile(
88
+ JSON.stringify({ formatVersion: 6, modifications: [{ modification: 'createEnum', enumName, values: ['a'] }] }),
89
+ `${version}-${enumName}`,
90
+ )
91
+ const enumNames = (schema: Schema) => Object.keys(schema.model.enums)
92
+
93
+ beforeEach(async () => {
94
+ migrationsDir = await fs.mkdtemp(path.join(os.tmpdir(), 'contember-svb-until-'))
95
+ filesManager = new MigrationFilesManager(migrationsDir, { json: new JsonLoader(new MigrationParser()) })
96
+ migrator = new CountingSchemaMigrator()
97
+ builder = new SchemaVersionBuilder(new MigrationsResolver(filesManager), migrator, new SchemaStateManager(path.join(migrationsDir, 'state')))
98
+ await createEnumMigration('2024-01-01-000000', 'first')
99
+ await createEnumMigration('2024-01-02-000000', 'second')
100
+ await createEnumMigration('2024-01-03-000000', 'third')
101
+ })
102
+
103
+ afterEach(async () => {
104
+ await fs.rm(migrationsDir, { recursive: true, force: true })
105
+ })
106
+
107
+ test('continues from the previous build for an ascending target', async () => {
108
+ expect(enumNames(await builder.buildSchemaUntil('2024-01-02-000000'))).toStrictEqual(['first'])
109
+ expect(enumNames(await builder.buildSchemaUntil('2024-01-04-000000'))).toStrictEqual(['first', 'second', 'third'])
110
+ expect(migrator.appliedMigrations).toBe(3)
111
+ })
112
+
113
+ test('rebuilds from scratch for a descending target', async () => {
114
+ await builder.buildSchemaUntil('2024-01-04-000000')
115
+
116
+ expect(enumNames(await builder.buildSchemaUntil('2024-01-02-000000'))).toStrictEqual(['first'])
117
+ })
118
+
119
+ test('rebuilds from scratch when the migration files change', async () => {
120
+ await builder.buildSchemaUntil('2024-01-03-000000')
121
+ await createEnumMigration('2024-01-01-120000', 'inserted')
122
+
123
+ expect(enumNames(await builder.buildSchemaUntil('2024-01-04-000000'))).toStrictEqual(['first', 'inserted', 'second', 'third'])
124
+ })
125
+ })