@contember/schema-migrations
Advanced tools
Comparing version 1.2.0-alpha.18 to 1.2.0-alpha.19
@@ -0,1 +1,2 @@ | ||
import { SchemaValidatorSkippedErrors } from '@contember/schema-utils'; | ||
export interface MigrationInfo { | ||
@@ -5,2 +6,3 @@ readonly version: string; | ||
readonly formatVersion: number; | ||
readonly skippedErrors?: SchemaValidatorSkippedErrors[]; | ||
} | ||
@@ -7,0 +9,0 @@ interface Migration extends MigrationInfo { |
@@ -7,6 +7,6 @@ "use strict"; | ||
exports.calculateMigrationChecksum = void 0; | ||
const crypto_1 = __importDefault(require("crypto")); | ||
const node_crypto_1 = __importDefault(require("node:crypto")); | ||
const calculateMigrationChecksum = (migration) => { | ||
const canonicalMigration = JSON.stringify(migration.modifications); | ||
return crypto_1.default // | ||
return node_crypto_1.default // | ||
.createHash('md5') | ||
@@ -13,0 +13,0 @@ .update(canonicalMigration) |
@@ -9,3 +9,5 @@ import { MigrationFilesManager } from './MigrationFilesManager'; | ||
constructor(migrationFilesManager: MigrationFilesManager, schemaDiffer: SchemaDiffer); | ||
prepareMigration(initialSchema: Schema, newSchema: Schema, migrationName: string): Promise<{ | ||
prepareMigration(initialSchema: Schema, newSchema: Schema, migrationName: string, { skipInitialSchemaValidation }?: { | ||
skipInitialSchemaValidation?: boolean; | ||
}): Promise<{ | ||
migration: Migration; | ||
@@ -12,0 +14,0 @@ initialSchema: Schema; |
@@ -11,5 +11,5 @@ "use strict"; | ||
} | ||
async prepareMigration(initialSchema, newSchema, migrationName) { | ||
async prepareMigration(initialSchema, newSchema, migrationName, { skipInitialSchemaValidation = false } = {}) { | ||
await this.migrationFilesManager.createDirIfNotExist(); | ||
const modifications = this.schemaDiffer.diffSchemas(initialSchema, newSchema); | ||
const modifications = this.schemaDiffer.diffSchemas(initialSchema, newSchema, { skipInitialSchemaValidation }); | ||
if (modifications.length === 0) { | ||
@@ -16,0 +16,0 @@ return null; |
@@ -28,13 +28,4 @@ "use strict"; | ||
const MigrationVersionHelper_1 = require("./MigrationVersionHelper"); | ||
const fs = __importStar(require("fs")); | ||
const util_1 = require("util"); | ||
const path = __importStar(require("path")); | ||
const readFile = (0, util_1.promisify)(fs.readFile); | ||
const fsWrite = (0, util_1.promisify)(fs.writeFile); | ||
const fsRemove = (0, util_1.promisify)(fs.unlink); | ||
const fsRealpath = (0, util_1.promisify)(fs.realpath); | ||
const mkdir = (0, util_1.promisify)(fs.mkdir); | ||
const lstatFile = (0, util_1.promisify)(fs.lstat); | ||
const readDir = (0, util_1.promisify)(fs.readdir); | ||
const mvFile = (0, util_1.promisify)(fs.rename); | ||
const fs = __importStar(require("node:fs/promises")); | ||
const path = __importStar(require("node:path")); | ||
class MigrationFilesManager { | ||
@@ -46,15 +37,15 @@ constructor(directory) { | ||
const path = this.formatPath(name); | ||
await fsWrite(path, content, { encoding: 'utf8' }); | ||
return await fsRealpath(path); | ||
await fs.writeFile(path, content, { encoding: 'utf8' }); | ||
return await fs.realpath(path); | ||
} | ||
async removeFile(name) { | ||
const path = this.formatPath(name); | ||
await fsRemove(path); | ||
await fs.unlink(path); | ||
} | ||
async moveFile(oldName, newName) { | ||
await mvFile(this.formatPath(oldName), this.formatPath(newName)); | ||
await fs.rename(this.formatPath(oldName), this.formatPath(newName)); | ||
} | ||
async createDirIfNotExist() { | ||
try { | ||
await mkdir(this.directory); | ||
await fs.mkdir(this.directory); | ||
} | ||
@@ -72,3 +63,3 @@ catch (e) { | ||
.filter(async (file) => { | ||
return (await lstatFile(`${this.directory}/${file}`)).isFile(); | ||
return (await fs.lstat(`${this.directory}/${file}`)).isFile(); | ||
})); | ||
@@ -79,3 +70,3 @@ return filteredFiles.sort(); | ||
try { | ||
return await readDir(this.directory); | ||
return await fs.readdir(this.directory); | ||
} | ||
@@ -97,3 +88,3 @@ catch (e) { | ||
path: `${this.directory}/${filename}`, | ||
content: await readFile(`${this.directory}/${filename}`, { encoding: 'utf8' }), | ||
content: await fs.readFile(`${this.directory}/${filename}`, { encoding: 'utf8' }), | ||
})); | ||
@@ -100,0 +91,0 @@ return await Promise.all(filesWithContent); |
@@ -15,2 +15,3 @@ "use strict"; | ||
return (await this.migrationFilesManager.readFiles()).map(({ filename, content }) => { | ||
var _a; | ||
const parsed = JSON.parse(content); | ||
@@ -22,2 +23,3 @@ return { | ||
modifications: parsed.modifications, | ||
skippedErrors: (_a = parsed.skippedErrors) !== null && _a !== void 0 ? _a : [], | ||
}; | ||
@@ -24,0 +26,0 @@ }); |
@@ -14,2 +14,3 @@ "use strict"; | ||
const indexes_1 = require("./indexes"); | ||
const settings_1 = require("./settings"); | ||
class ModificationHandlerFactory { | ||
@@ -29,2 +30,3 @@ constructor(map) { | ||
const handlers = [ | ||
settings_1.updateSettingsModification, | ||
acl_1.updateAclSchemaModification, | ||
@@ -31,0 +33,0 @@ acl_1.patchAclSchemaModification, |
@@ -164,3 +164,10 @@ "use strict"; | ||
const { [field.name]: removed, ...fields } = entity.fields; | ||
return { ...entity, fields }; | ||
const indexes = Object.entries(entity.indexes).filter(([, index]) => !index.fields.includes(field.name)); | ||
const unique = Object.entries(entity.unique).filter(([, index]) => !index.fields.includes(field.name)); | ||
return { | ||
...entity, | ||
fields, | ||
indexes: Object.fromEntries(indexes), | ||
unique: Object.fromEntries(unique), | ||
}; | ||
}), (0, schema_utils_1.isRelation)(field) && (0, schema_utils_1.isInverseRelation)(field) | ||
@@ -167,0 +174,0 @@ ? (0, exports.updateEntity)(field.target, (0, exports.updateField)(field.ownedBy, ({ field: { inversedBy, ...field } }) => field)) |
@@ -5,6 +5,10 @@ import { Schema } from '@contember/schema'; | ||
import { Migration } from './Migration'; | ||
declare type DiffOptions = { | ||
skipRecreateValidation?: boolean; | ||
skipInitialSchemaValidation?: boolean; | ||
}; | ||
export declare class SchemaDiffer { | ||
private readonly schemaMigrator; | ||
constructor(schemaMigrator: SchemaMigrator); | ||
diffSchemas(originalSchema: Schema, updatedSchema: Schema, checkRecreate?: boolean): Migration.Modification[]; | ||
diffSchemas(originalSchema: Schema, updatedSchema: Schema, { skipInitialSchemaValidation, skipRecreateValidation, }?: DiffOptions): Migration.Modification[]; | ||
} | ||
@@ -15,2 +19,3 @@ export declare class InvalidSchemaException extends Error { | ||
} | ||
export {}; | ||
//# sourceMappingURL=SchemaDiffer.d.ts.map |
@@ -9,2 +9,3 @@ "use strict"; | ||
const indexes_1 = require("./modifications/indexes"); | ||
const settings_1 = require("./modifications/settings"); | ||
class SchemaDiffer { | ||
@@ -14,6 +15,8 @@ constructor(schemaMigrator) { | ||
} | ||
diffSchemas(originalSchema, updatedSchema, checkRecreate = true) { | ||
const originalErrors = schema_utils_1.SchemaValidator.validate(originalSchema); | ||
if (originalErrors.length > 0) { | ||
throw new InvalidSchemaException('original schema is not valid', originalErrors); | ||
diffSchemas(originalSchema, updatedSchema, { skipInitialSchemaValidation = false, skipRecreateValidation = false, } = {}) { | ||
if (!skipInitialSchemaValidation) { | ||
const originalErrors = schema_utils_1.SchemaValidator.validate(originalSchema); | ||
if (originalErrors.length > 0) { | ||
throw new InvalidSchemaException('original schema is not valid', originalErrors); | ||
} | ||
} | ||
@@ -25,2 +28,3 @@ const updatedErrors = schema_utils_1.SchemaValidator.validate(updatedSchema); | ||
const differs = [ | ||
new settings_1.UpdateSettingsDiffer(), | ||
new modifications_1.ConvertOneToManyRelationDiffer(), | ||
@@ -51,4 +55,4 @@ new modifications_1.ConvertOneHasManyToManyHasManyRelationDiffer(), | ||
new modifications_1.CreateColumnDiffer(), | ||
new modifications_1.CreateRelationDiffer(), | ||
new modifications_1.CreateViewDiffer(), | ||
new modifications_1.CreateRelationDiffer(), | ||
new modifications_1.CreateRelationInverseSideDiffer(), | ||
@@ -68,3 +72,3 @@ new modifications_1.CreateUniqueConstraintDiffer(), | ||
} | ||
if (checkRecreate) { | ||
if (!skipRecreateValidation) { | ||
const { meta, ...appliedDiffsSchema2 } = appliedDiffsSchema; | ||
@@ -71,0 +75,0 @@ const errors = (0, schema_utils_1.deepCompare)(updatedSchema, appliedDiffsSchema2, []); |
@@ -103,2 +103,82 @@ "use strict"; | ||
}); | ||
var ViewAddRelationOriginalSchema; | ||
(function (ViewAddRelationOriginalSchema) { | ||
class Article { | ||
constructor() { | ||
this.title = schema_definition_1.SchemaDefinition.stringColumn(); | ||
} | ||
} | ||
ViewAddRelationOriginalSchema.Article = Article; | ||
class Category { | ||
constructor() { | ||
this.name = schema_definition_1.SchemaDefinition.stringColumn(); | ||
} | ||
} | ||
ViewAddRelationOriginalSchema.Category = Category; | ||
})(ViewAddRelationOriginalSchema || (ViewAddRelationOriginalSchema = {})); | ||
var ViewAddRelationUpdateSchema; | ||
(function (ViewAddRelationUpdateSchema) { | ||
class Article { | ||
constructor() { | ||
this.title = schema_definition_1.SchemaDefinition.stringColumn(); | ||
this.category = schema_definition_1.SchemaDefinition.manyHasOne(Category); | ||
this.stats = schema_definition_1.SchemaDefinition.oneHasOneInverse(ArticleStats, 'article'); | ||
} | ||
} | ||
ViewAddRelationUpdateSchema.Article = Article; | ||
class Category { | ||
constructor() { | ||
this.name = schema_definition_1.SchemaDefinition.stringColumn(); | ||
} | ||
} | ||
ViewAddRelationUpdateSchema.Category = Category; | ||
let ArticleStats = class ArticleStats { | ||
constructor() { | ||
this.article = schema_definition_1.SchemaDefinition.oneHasOne(Article, 'stats'); | ||
this.visitCount = schema_definition_1.SchemaDefinition.intColumn(); | ||
} | ||
}; | ||
ArticleStats = __decorate([ | ||
schema_definition_1.SchemaDefinition.View('SELECT 1') | ||
], ArticleStats); | ||
ViewAddRelationUpdateSchema.ArticleStats = ArticleStats; | ||
})(ViewAddRelationUpdateSchema || (ViewAddRelationUpdateSchema = {})); | ||
(0, tests_js_1.testMigrations)('create a relation and a view', { | ||
originalSchema: schema_definition_1.SchemaDefinition.createModel(ViewAddRelationOriginalSchema), updatedSchema: schema_definition_1.SchemaDefinition.createModel(ViewAddRelationUpdateSchema), diff: [ | ||
{ | ||
modification: 'createRelation', | ||
entityName: 'Article', | ||
owningSide: { name: 'category', nullable: true, type: 'ManyHasOne', target: 'Category', joiningColumn: { columnName: 'category_id', onDelete: 'restrict' } }, | ||
}, | ||
{ | ||
modification: 'createView', | ||
entity: { name: 'ArticleStats', | ||
primary: 'id', | ||
primaryColumn: 'id', | ||
unique: {}, | ||
indexes: {}, | ||
fields: { id: { name: 'id', columnName: 'id', nullable: false, type: 'Uuid', columnType: 'uuid' }, | ||
article: { name: 'article', inversedBy: 'stats', nullable: true, type: 'OneHasOne', target: 'Article', joiningColumn: { columnName: 'article_id', onDelete: 'restrict' } }, | ||
visitCount: { name: 'visitCount', columnName: 'visit_count', nullable: true, type: 'Integer', columnType: 'integer' } }, | ||
tableName: 'article_stats', | ||
eventLog: { enabled: true }, | ||
view: { sql: 'SELECT 1' } }, | ||
}, | ||
{ | ||
modification: 'createRelationInverseSide', | ||
entityName: 'Article', | ||
relation: { | ||
name: 'stats', | ||
ownedBy: 'article', | ||
target: 'ArticleStats', | ||
type: 'OneHasOne', | ||
nullable: true, | ||
}, | ||
}, | ||
], | ||
sql: (0, tags_js_1.SQL) `ALTER TABLE "article" ADD "category_id" uuid; | ||
ALTER TABLE "article" ADD CONSTRAINT "fk_article_category_id_703b8b" FOREIGN KEY ("category_id") REFERENCES "category"("id") ON DELETE NO ACTION DEFERRABLE INITIALLY IMMEDIATE; | ||
CREATE INDEX "article_category_id_index" ON "article" ("category_id"); | ||
CREATE VIEW "article_stats" AS SELECT 1;`, | ||
}); | ||
//# sourceMappingURL=createView.test.js.map |
"use strict"; | ||
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { | ||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; | ||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); | ||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; | ||
return c > 3 && r && Object.defineProperty(target, key, r), r; | ||
}; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
@@ -7,2 +13,3 @@ const tests_1 = require("../../src/tests"); | ||
const tags_1 = require("../../src/tags"); | ||
const schema_definition_2 = require("@contember/schema-definition"); | ||
(0, tests_1.testMigrations)('remove relation (many has one)', { | ||
@@ -115,2 +122,67 @@ originalSchema: new schema_definition_1.SchemaBuilder() | ||
}); | ||
var DropIndexOrigSchema; | ||
(function (DropIndexOrigSchema) { | ||
let Article = class Article { | ||
constructor() { | ||
this.title = schema_definition_2.SchemaDefinition.stringColumn(); | ||
this.author = schema_definition_2.SchemaDefinition.manyHasOne(Author); | ||
} | ||
}; | ||
Article = __decorate([ | ||
schema_definition_2.SchemaDefinition.Unique('title', 'author'), | ||
schema_definition_2.SchemaDefinition.Index('title', 'author') | ||
], Article); | ||
DropIndexOrigSchema.Article = Article; | ||
class Author { | ||
constructor() { | ||
this.name = schema_definition_2.SchemaDefinition.stringColumn(); | ||
} | ||
} | ||
DropIndexOrigSchema.Author = Author; | ||
})(DropIndexOrigSchema || (DropIndexOrigSchema = {})); | ||
var DropIndexUpSchema; | ||
(function (DropIndexUpSchema) { | ||
let Article = class Article { | ||
constructor() { | ||
this.title = schema_definition_2.SchemaDefinition.stringColumn(); | ||
this.author = schema_definition_2.SchemaDefinition.stringColumn(); | ||
} | ||
}; | ||
Article = __decorate([ | ||
schema_definition_2.SchemaDefinition.Unique('title', 'author'), | ||
schema_definition_2.SchemaDefinition.Index('title', 'author') | ||
], Article); | ||
DropIndexUpSchema.Article = Article; | ||
class Author { | ||
constructor() { | ||
this.name = schema_definition_2.SchemaDefinition.stringColumn(); | ||
} | ||
} | ||
DropIndexUpSchema.Author = Author; | ||
})(DropIndexUpSchema || (DropIndexUpSchema = {})); | ||
(0, tests_1.testMigrations)('test drop index / unique when removing a field', { | ||
originalSchema: schema_definition_2.SchemaDefinition.createModel(DropIndexOrigSchema), | ||
updatedSchema: schema_definition_2.SchemaDefinition.createModel(DropIndexUpSchema), | ||
diff: [{ | ||
modification: 'removeField', | ||
entityName: 'Article', | ||
fieldName: 'author', | ||
}, { | ||
modification: 'createColumn', | ||
entityName: 'Article', | ||
field: { name: 'author', columnName: 'author', nullable: true, type: 'String', columnType: 'text' }, | ||
}, { | ||
modification: 'createUniqueConstraint', | ||
entityName: 'Article', | ||
unique: { name: 'unique_Article_title_author_7157ea', fields: ['title', 'author'] }, | ||
}, { | ||
modification: 'createIndex', | ||
entityName: 'Article', | ||
index: { name: 'idx_Article_title_author_7157ea', fields: ['title', 'author'] }, | ||
}], | ||
sql: (0, tags_1.SQL) `ALTER TABLE "article" DROP "author_id"; | ||
ALTER TABLE "article" ADD "author" text; | ||
ALTER TABLE "article" ADD CONSTRAINT "unique_Article_title_author_7157ea" UNIQUE ("title", "author"); | ||
CREATE INDEX "idx_Article_title_author_7157ea" ON "article" ("title", "author");`, | ||
}); | ||
//# sourceMappingURL=removeField.test.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
const path_1 = require("path"); | ||
const node_path_1 = require("node:path"); | ||
const src_1 = require("../../src"); | ||
@@ -8,4 +8,4 @@ const schema_utils_1 = require("@contember/schema-utils"); | ||
// eslint-disable-next-line no-console | ||
console.log((0, path_1.relative)(process.cwd(), process.argv[2])); | ||
const migrationsResolver = new src_1.MigrationsResolver(new src_1.MigrationFilesManager((0, path_1.relative)(process.cwd(), process.argv[2]))); | ||
console.log((0, node_path_1.relative)(process.cwd(), process.argv[2])); | ||
const migrationsResolver = new src_1.MigrationsResolver(new src_1.MigrationFilesManager((0, node_path_1.relative)(process.cwd(), process.argv[2]))); | ||
const modificationHandlerFactory = new src_1.ModificationHandlerFactory(src_1.ModificationHandlerFactory.defaultFactoryMap); | ||
@@ -18,3 +18,4 @@ const differ = new src_1.SchemaDiffer(new src_1.SchemaMigrator(modificationHandlerFactory)); | ||
(0, schema_utils_1.schemaType)(nextSchema); | ||
differ.diffSchemas(schema, nextSchema); | ||
const { meta, ...nextSchemaWithoutMeta } = nextSchema; | ||
differ.diffSchemas(schema, nextSchemaWithoutMeta); | ||
schema = nextSchema; | ||
@@ -21,0 +22,0 @@ } |
@@ -7,2 +7,3 @@ "use strict"; | ||
const vitest_1 = require("vitest"); | ||
const schema_utils_1 = require("@contember/schema-utils"); | ||
const modificationFactory = new src_1.ModificationHandlerFactory(src_1.ModificationHandlerFactory.defaultFactoryMap); | ||
@@ -13,6 +14,14 @@ const schemaMigrator = new src_1.SchemaMigrator(modificationFactory); | ||
function testDiffSchemas(originalModel, updatedModel, expectedDiff, originalAcl = emptyAcl, updatedAcl = emptyAcl) { | ||
const actualDiff = schemaDiffer.diffSchemas({ model: originalModel, acl: originalAcl, validation: {} }, { model: updatedModel, acl: updatedAcl, validation: {} }, false); | ||
vitest_1.assert.deepStrictEqual(actualDiff, expectedDiff); | ||
const { meta, ...schema } = schemaMigrator.applyModifications({ model: originalModel, acl: originalAcl, validation: {} }, actualDiff, src_1.VERSION_LATEST); | ||
const actualDiff = schemaDiffer.diffSchemas({ ...schema_utils_1.emptySchema, model: originalModel, acl: originalAcl, validation: {} }, { ...schema_utils_1.emptySchema, model: updatedModel, acl: updatedAcl, validation: {} }, { skipRecreateValidation: true }); | ||
try { | ||
vitest_1.assert.deepStrictEqual(actualDiff, expectedDiff); | ||
} | ||
catch (e) { | ||
// eslint-disable-next-line no-console | ||
console.log(JSON.stringify(actualDiff)); | ||
throw e; | ||
} | ||
const { meta, ...schema } = schemaMigrator.applyModifications({ ...schema_utils_1.emptySchema, model: originalModel, acl: originalAcl, validation: {} }, actualDiff, src_1.VERSION_LATEST); | ||
vitest_1.assert.deepStrictEqual(schema, { | ||
...schema_utils_1.emptySchema, | ||
model: updatedModel, | ||
@@ -25,4 +34,5 @@ acl: updatedAcl, | ||
function testApplyDiff(originalModel, diff, expectedModel, originalAcl = emptyAcl, expectedAcl = emptyAcl) { | ||
const { meta, ...actualSchema } = schemaMigrator.applyModifications({ model: originalModel, acl: originalAcl, validation: {} }, diff, src_1.VERSION_LATEST); | ||
const { meta, ...actualSchema } = schemaMigrator.applyModifications({ ...schema_utils_1.emptySchema, model: originalModel, acl: originalAcl, validation: {} }, diff, src_1.VERSION_LATEST); | ||
vitest_1.assert.deepStrictEqual(actualSchema, { | ||
...schema_utils_1.emptySchema, | ||
model: expectedModel, | ||
@@ -35,3 +45,3 @@ acl: expectedAcl, | ||
function testGenerateSql(originalSchema, diff, expectedSql) { | ||
let schema = { model: originalSchema, acl: emptyAcl, validation: {} }; | ||
let schema = { ...schema_utils_1.emptySchema, model: originalSchema, acl: emptyAcl, validation: {} }; | ||
const builder = (0, database_migrations_1.createMigrationBuilder)(); | ||
@@ -38,0 +48,0 @@ for (let { modification, ...data } of diff) { |
{ | ||
"name": "@contember/schema-migrations", | ||
"version": "1.2.0-alpha.18", | ||
"version": "1.2.0-alpha.19", | ||
"license": "Apache-2.0", | ||
@@ -11,6 +11,6 @@ "main": "dist/src/index.js", | ||
"dependencies": { | ||
"@contember/database-migrations": "^1.2.0-alpha.18", | ||
"@contember/engine-common": "^1.2.0-alpha.18", | ||
"@contember/schema": "^1.2.0-alpha.18", | ||
"@contember/schema-utils": "^1.2.0-alpha.18", | ||
"@contember/database": "^1.2.0-alpha.19", | ||
"@contember/database-migrations": "^1.2.0-alpha.19", | ||
"@contember/schema": "^1.2.0-alpha.19", | ||
"@contember/schema-utils": "^1.2.0-alpha.19", | ||
"fast-deep-equal": "^3.1.3", | ||
@@ -20,3 +20,3 @@ "rfc6902": "^5.0.1" | ||
"devDependencies": { | ||
"@contember/schema-definition": "^1.2.0-alpha.18" | ||
"@contember/schema-definition": "^1.2.0-alpha.19" | ||
}, | ||
@@ -23,0 +23,0 @@ "peerDependencies": { |
@@ -1,2 +0,3 @@ | ||
import crypto from 'crypto' | ||
import { SchemaValidatorSkippedErrors } from '@contember/schema-utils' | ||
import crypto from 'node:crypto' | ||
@@ -7,2 +8,3 @@ export interface MigrationInfo { | ||
readonly formatVersion: number | ||
readonly skippedErrors?: SchemaValidatorSkippedErrors[] | ||
} | ||
@@ -9,0 +11,0 @@ |
@@ -18,6 +18,7 @@ import { MigrationFilesManager } from './MigrationFilesManager' | ||
migrationName: string, | ||
{ skipInitialSchemaValidation = false }: { skipInitialSchemaValidation?: boolean } = {}, | ||
): Promise<{ migration: Migration; initialSchema: Schema } | null> { | ||
await this.migrationFilesManager.createDirIfNotExist() | ||
const modifications = this.schemaDiffer.diffSchemas(initialSchema, newSchema) | ||
const modifications = this.schemaDiffer.diffSchemas(initialSchema, newSchema, { skipInitialSchemaValidation }) | ||
if (modifications.length === 0) { | ||
@@ -24,0 +25,0 @@ return null |
import { MigrationVersionHelper } from './MigrationVersionHelper' | ||
import * as fs from 'fs' | ||
import { promisify } from 'util' | ||
import * as path from 'path' | ||
import * as fs from 'node:fs/promises' | ||
import * as path from 'node:path' | ||
const readFile = promisify(fs.readFile) | ||
const fsWrite = promisify(fs.writeFile) | ||
const fsRemove = promisify(fs.unlink) | ||
const fsRealpath = promisify(fs.realpath) | ||
const mkdir = promisify(fs.mkdir) | ||
const lstatFile = promisify(fs.lstat) | ||
const readDir = promisify(fs.readdir) | ||
const mvFile = promisify(fs.rename) | ||
class MigrationFilesManager { | ||
@@ -20,4 +10,4 @@ constructor(public readonly directory: string) {} | ||
const path = this.formatPath(name) | ||
await fsWrite(path, content, { encoding: 'utf8' }) | ||
return await fsRealpath(path) | ||
await fs.writeFile(path, content, { encoding: 'utf8' }) | ||
return await fs.realpath(path) | ||
} | ||
@@ -27,7 +17,7 @@ | ||
const path = this.formatPath(name) | ||
await fsRemove(path) | ||
await fs.unlink(path) | ||
} | ||
public async moveFile(oldName: string, newName: string) { | ||
await mvFile(this.formatPath(oldName), this.formatPath(newName)) | ||
await fs.rename(this.formatPath(oldName), this.formatPath(newName)) | ||
} | ||
@@ -37,3 +27,3 @@ | ||
try { | ||
await mkdir(this.directory) | ||
await fs.mkdir(this.directory) | ||
} catch (e) { | ||
@@ -53,3 +43,3 @@ if (!(e instanceof Error) || !('code' in e) || (e as any).code !== 'EEXIST') { | ||
.filter(async file => { | ||
return (await lstatFile(`${this.directory}/${file}`)).isFile() | ||
return (await fs.lstat(`${this.directory}/${file}`)).isFile() | ||
}), | ||
@@ -62,3 +52,3 @@ ) | ||
try { | ||
return await readDir(this.directory) | ||
return await fs.readdir(this.directory) | ||
} catch (e) { | ||
@@ -80,3 +70,3 @@ if (e instanceof Error && 'code' in e && (e as any).code === 'ENOENT') { | ||
path: `${this.directory}/${filename}`, | ||
content: await readFile(`${this.directory}/${filename}`, { encoding: 'utf8' }), | ||
content: await fs.readFile(`${this.directory}/${filename}`, { encoding: 'utf8' }), | ||
})) | ||
@@ -83,0 +73,0 @@ |
@@ -21,2 +21,3 @@ import { MigrationFilesManager } from './MigrationFilesManager' | ||
modifications: parsed.modifications, | ||
skippedErrors: parsed.skippedErrors ?? [], | ||
} | ||
@@ -23,0 +24,0 @@ }) |
@@ -33,2 +33,3 @@ import { Schema } from '@contember/schema' | ||
import { SchemaWithMeta } from './utils/schemaMeta' | ||
import { updateSettingsModification } from './settings' | ||
@@ -51,2 +52,3 @@ | ||
const handlers = [ | ||
updateSettingsModification, | ||
updateAclSchemaModification, | ||
@@ -53,0 +55,0 @@ patchAclSchemaModification, |
@@ -274,3 +274,10 @@ import { Acl, Model, Schema, Writable } from '@contember/schema' | ||
const { [field.name]: removed, ...fields } = entity.fields | ||
return { ...entity, fields } | ||
const indexes = Object.entries(entity.indexes).filter(([, index]) => !index.fields.includes(field.name)) | ||
const unique = Object.entries(entity.unique).filter(([, index]) => !index.fields.includes(field.name)) | ||
return { | ||
...entity, | ||
fields, | ||
indexes: Object.fromEntries(indexes), | ||
unique: Object.fromEntries(unique), | ||
} | ||
}), | ||
@@ -277,0 +284,0 @@ isRelation(field) && isInverseRelation(field) |
@@ -47,10 +47,18 @@ import { Schema } from '@contember/schema' | ||
import { SchemaWithMeta } from './modifications/utils/schemaMeta' | ||
import { UpdateSettingsDiffer } from './modifications/settings' | ||
type DiffOptions = { skipRecreateValidation?: boolean; skipInitialSchemaValidation?: boolean } | ||
export class SchemaDiffer { | ||
constructor(private readonly schemaMigrator: SchemaMigrator) {} | ||
diffSchemas(originalSchema: Schema, updatedSchema: Schema, checkRecreate: boolean = true): Migration.Modification[] { | ||
const originalErrors = SchemaValidator.validate(originalSchema) | ||
if (originalErrors.length > 0) { | ||
throw new InvalidSchemaException('original schema is not valid', originalErrors) | ||
diffSchemas(originalSchema: Schema, updatedSchema: Schema, { | ||
skipInitialSchemaValidation = false, | ||
skipRecreateValidation = false, | ||
}: DiffOptions = {}): Migration.Modification[] { | ||
if (!skipInitialSchemaValidation) { | ||
const originalErrors = SchemaValidator.validate(originalSchema) | ||
if (originalErrors.length > 0) { | ||
throw new InvalidSchemaException('original schema is not valid', originalErrors) | ||
} | ||
} | ||
@@ -63,2 +71,3 @@ const updatedErrors = SchemaValidator.validate(updatedSchema) | ||
const differs: Differ[] = [ | ||
new UpdateSettingsDiffer(), | ||
new ConvertOneToManyRelationDiffer(), | ||
@@ -89,4 +98,4 @@ new ConvertOneHasManyToManyHasManyRelationDiffer(), | ||
new CreateColumnDiffer(), | ||
new CreateRelationDiffer(), | ||
new CreateViewDiffer(), | ||
new CreateRelationDiffer(), | ||
new CreateRelationInverseSideDiffer(), | ||
@@ -109,3 +118,3 @@ new CreateUniqueConstraintDiffer(), | ||
if (checkRecreate) { | ||
if (!skipRecreateValidation) { | ||
const { meta, ...appliedDiffsSchema2 } = appliedDiffsSchema as SchemaWithMeta | ||
@@ -112,0 +121,0 @@ const errors = deepCompare(updatedSchema, appliedDiffsSchema2, []) |
@@ -10,6 +10,6 @@ { | ||
{ | ||
"path": "../../database-migrations/src" | ||
"path": "../../database/src" | ||
}, | ||
{ | ||
"path": "../../engine-common/src" | ||
"path": "../../database-migrations/src" | ||
}, | ||
@@ -16,0 +16,0 @@ { |
@@ -89,1 +89,71 @@ import { testMigrations } from '../../src/tests.js' | ||
namespace ViewAddRelationOriginalSchema { | ||
export class Article { | ||
title = def.stringColumn() | ||
} | ||
export class Category { | ||
name = def.stringColumn() | ||
} | ||
} | ||
namespace ViewAddRelationUpdateSchema { | ||
export class Article { | ||
title = def.stringColumn() | ||
category = def.manyHasOne(Category) | ||
stats = def.oneHasOneInverse(ArticleStats, 'article') | ||
} | ||
export class Category { | ||
name = def.stringColumn() | ||
} | ||
@def.View('SELECT 1') | ||
export class ArticleStats { | ||
article = def.oneHasOne(Article, 'stats') | ||
visitCount = def.intColumn() | ||
} | ||
} | ||
testMigrations('create a relation and a view', { | ||
originalSchema: def.createModel(ViewAddRelationOriginalSchema), updatedSchema: def.createModel(ViewAddRelationUpdateSchema), diff: [ | ||
{ | ||
modification: 'createRelation', | ||
entityName: 'Article', | ||
owningSide: { name: 'category', nullable: true, type: 'ManyHasOne', target: 'Category', joiningColumn: { columnName: 'category_id', onDelete: 'restrict' } }, | ||
}, | ||
{ | ||
modification: 'createView', | ||
entity: { name: 'ArticleStats', | ||
primary: 'id', | ||
primaryColumn: 'id', | ||
unique: {}, | ||
indexes: {}, | ||
fields: { id: { name: 'id', columnName: 'id', nullable: false, type: 'Uuid', columnType: 'uuid' }, | ||
article: { name: 'article', inversedBy: 'stats', nullable: true, type: 'OneHasOne', target: 'Article', joiningColumn: { columnName: 'article_id', onDelete: 'restrict' } }, | ||
visitCount: { name: 'visitCount', columnName: 'visit_count', nullable: true, type: 'Integer', columnType: 'integer' } }, | ||
tableName: 'article_stats', | ||
eventLog: { enabled: true }, | ||
view: { sql: 'SELECT 1' } }, | ||
}, | ||
{ | ||
modification: 'createRelationInverseSide', | ||
entityName: 'Article', | ||
relation: { | ||
name: 'stats', | ||
ownedBy: 'article', | ||
target: 'ArticleStats', | ||
type: 'OneHasOne', | ||
nullable: true, | ||
}, | ||
}, | ||
], | ||
sql: SQL`ALTER TABLE "article" ADD "category_id" uuid; | ||
ALTER TABLE "article" ADD CONSTRAINT "fk_article_category_id_703b8b" FOREIGN KEY ("category_id") REFERENCES "category"("id") ON DELETE NO ACTION DEFERRABLE INITIALLY IMMEDIATE; | ||
CREATE INDEX "article_category_id_index" ON "article" ("category_id"); | ||
CREATE VIEW "article_stats" AS SELECT 1;`, | ||
}) | ||
@@ -5,3 +5,5 @@ import { testMigrations } from '../../src/tests' | ||
import { SQL } from '../../src/tags' | ||
import { SchemaDefinition as def } from '@contember/schema-definition' | ||
testMigrations('remove relation (many has one)', { | ||
@@ -132,1 +134,57 @@ originalSchema: new SchemaBuilder() | ||
}) | ||
namespace DropIndexOrigSchema { | ||
@def.Unique('title', 'author') | ||
@def.Index('title', 'author') | ||
export class Article { | ||
title = def.stringColumn() | ||
author = def.manyHasOne(Author) | ||
} | ||
export class Author { | ||
name = def.stringColumn() | ||
} | ||
} | ||
namespace DropIndexUpSchema { | ||
@def.Unique('title', 'author') | ||
@def.Index('title', 'author') | ||
export class Article { | ||
title = def.stringColumn() | ||
author = def.stringColumn() | ||
} | ||
export class Author { | ||
name = def.stringColumn() | ||
} | ||
} | ||
testMigrations('test drop index / unique when removing a field', { | ||
originalSchema: def.createModel(DropIndexOrigSchema), | ||
updatedSchema: def.createModel(DropIndexUpSchema), | ||
diff: [{ | ||
modification: 'removeField', | ||
entityName: 'Article', | ||
fieldName: 'author', | ||
}, { | ||
modification: 'createColumn', | ||
entityName: 'Article', | ||
field: { name: 'author', columnName: 'author', nullable: true, type: 'String', columnType: 'text' }, | ||
}, { | ||
modification: 'createUniqueConstraint', | ||
entityName: 'Article', | ||
unique: { name: 'unique_Article_title_author_7157ea', fields: ['title', 'author'] }, | ||
}, { | ||
modification: 'createIndex', | ||
entityName: 'Article', | ||
index: { name: 'idx_Article_title_author_7157ea', fields: ['title', 'author'] }, | ||
}], | ||
sql: SQL`ALTER TABLE "article" DROP "author_id"; | ||
ALTER TABLE "article" ADD "author" text; | ||
ALTER TABLE "article" ADD CONSTRAINT "unique_Article_title_author_7157ea" UNIQUE ("title", "author"); | ||
CREATE INDEX "idx_Article_title_author_7157ea" ON "article" ("title", "author");`, | ||
}) | ||
@@ -1,2 +0,2 @@ | ||
import { relative } from 'path' | ||
import { relative } from 'node:path' | ||
import { | ||
@@ -21,3 +21,4 @@ MigrationFilesManager, | ||
schemaType(nextSchema) | ||
differ.diffSchemas(schema, nextSchema) | ||
const { meta, ...nextSchemaWithoutMeta } = nextSchema | ||
differ.diffSchemas(schema, nextSchemaWithoutMeta) | ||
@@ -24,0 +25,0 @@ schema = nextSchema |
@@ -6,2 +6,3 @@ import { Migration, ModificationHandlerFactory, SchemaDiffer, SchemaMigrator, VERSION_LATEST } from '../../src' | ||
import { SchemaWithMeta } from '../../src/modifications/utils/schemaMeta' | ||
import { emptySchema } from '@contember/schema-utils' | ||
@@ -32,9 +33,15 @@ const modificationFactory = new ModificationHandlerFactory(ModificationHandlerFactory.defaultFactoryMap) | ||
const actualDiff = schemaDiffer.diffSchemas( | ||
{ model: originalModel, acl: originalAcl, validation: {} }, | ||
{ model: updatedModel, acl: updatedAcl, validation: {} }, | ||
false, | ||
{ ...emptySchema, model: originalModel, acl: originalAcl, validation: {} }, | ||
{ ...emptySchema, model: updatedModel, acl: updatedAcl, validation: {} }, | ||
{ skipRecreateValidation: true }, | ||
) | ||
assert.deepStrictEqual(actualDiff, expectedDiff) | ||
try { | ||
assert.deepStrictEqual(actualDiff, expectedDiff) | ||
} catch (e) { | ||
// eslint-disable-next-line no-console | ||
console.log(JSON.stringify(actualDiff)) | ||
throw e | ||
} | ||
const { meta, ...schema } = schemaMigrator.applyModifications( | ||
{ model: originalModel, acl: originalAcl, validation: {} }, | ||
{ ...emptySchema, model: originalModel, acl: originalAcl, validation: {} }, | ||
actualDiff, | ||
@@ -44,2 +51,3 @@ VERSION_LATEST, | ||
assert.deepStrictEqual(schema, { | ||
...emptySchema, | ||
model: updatedModel, | ||
@@ -59,3 +67,3 @@ acl: updatedAcl, | ||
const { meta, ...actualSchema } = schemaMigrator.applyModifications( | ||
{ model: originalModel, acl: originalAcl, validation: {} }, | ||
{ ...emptySchema, model: originalModel, acl: originalAcl, validation: {} }, | ||
diff, | ||
@@ -66,2 +74,3 @@ VERSION_LATEST, | ||
assert.deepStrictEqual(actualSchema, { | ||
...emptySchema, | ||
model: expectedModel, | ||
@@ -74,3 +83,3 @@ acl: expectedAcl, | ||
export function testGenerateSql(originalSchema: Model.Schema, diff: Migration.Modification[], expectedSql: string) { | ||
let schema = { model: originalSchema, acl: emptyAcl, validation: {} } | ||
let schema = { ...emptySchema, model: originalSchema, acl: emptyAcl, validation: {} } | ||
const builder = createMigrationBuilder() | ||
@@ -77,0 +86,0 @@ for (let { modification, ...data } of diff) { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
1040856
598
15732
1
- Removed@contember/engine-common@1.4.7(transitive)
- Removed@contember/logger@1.4.7(transitive)
- Removedchalk@4.1.2(transitive)
- Removedhas-flag@4.0.0(transitive)
- Removedsupports-color@7.2.0(transitive)