@jungvonmatt/sb-migrate
Version:
CLI tool for managing Storyblok schema and content migrations
503 lines • 19.9 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const vitest_1 = require("vitest");
const migration_1 = require("../../utils/migration");
const api_1 = require("../../utils/api");
const storyblok_1 = require("../../utils/storyblok");
const fs_1 = __importDefault(require("fs"));
const setup_1 = require("../setup");
// Mock dependencies
vitest_1.vi.mock("../../utils/api", () => ({
api: {
stories: {
getAll: vitest_1.vi.fn(),
get: vitest_1.vi.fn(),
update: vitest_1.vi.fn(),
},
components: {
create: vitest_1.vi.fn(),
get: vitest_1.vi.fn(),
update: vitest_1.vi.fn(),
},
},
}));
vitest_1.vi.mock("../../utils/storyblok", () => ({
processMigration: vitest_1.vi.fn(),
createRollbackFile: vitest_1.vi.fn(),
addOrUpdateDatasource: vitest_1.vi.fn(),
addOrUpdateFields: vitest_1.vi.fn((instance, fields) => ({
...instance,
schema: fields,
})),
moveToTab: vitest_1.vi.fn((instance, name, keys) => ({
...instance,
schema: {
...instance.schema,
[name]: keys,
},
})),
}));
vitest_1.vi.mock("picocolors", () => ({
default: {
blue: (str) => str,
green: (str) => str,
red: (str) => str,
yellow: (str) => str,
cyan: (str) => str,
},
}));
vitest_1.vi.mock("fs", () => ({
default: {
existsSync: vitest_1.vi.fn(),
readdirSync: vitest_1.vi.fn(),
statSync: vitest_1.vi.fn(),
},
existsSync: vitest_1.vi.fn(),
readdirSync: vitest_1.vi.fn(),
statSync: vitest_1.vi.fn(),
}));
(0, vitest_1.describe)("Migration Utilities", () => {
(0, vitest_1.describe)("isStoryPublishedWithoutChanges", () => {
(0, vitest_1.it)("should return true for published story without changes", () => {
// Arrange
const story = {
published: true,
unpublished_changes: false,
};
// Act
const result = (0, migration_1.isStoryPublishedWithoutChanges)(story);
// Assert
(0, vitest_1.expect)(result).toBe(true);
});
(0, vitest_1.it)("should return false for unpublished story", () => {
// Arrange
const story = {
published: false,
unpublished_changes: false,
};
// Act
const result = (0, migration_1.isStoryPublishedWithoutChanges)(story);
// Assert
(0, vitest_1.expect)(result).toBe(false);
});
(0, vitest_1.it)("should return false for story with unpublished changes", () => {
// Arrange
const story = {
published: true,
unpublished_changes: true,
};
// Act
const result = (0, migration_1.isStoryPublishedWithoutChanges)(story);
// Assert
(0, vitest_1.expect)(result).toBe(false);
});
});
(0, vitest_1.describe)("isStoryWithUnpublishedChanges", () => {
(0, vitest_1.it)("should return true for published story with changes", () => {
// Arrange
const story = {
published: true,
unpublished_changes: true,
};
// Act
const result = (0, migration_1.isStoryWithUnpublishedChanges)(story);
// Assert
(0, vitest_1.expect)(result).toBe(true);
});
(0, vitest_1.it)("should return false for unpublished story", () => {
// Arrange
const story = {
published: false,
unpublished_changes: true,
};
// Act
const result = (0, migration_1.isStoryWithUnpublishedChanges)(story);
// Assert
(0, vitest_1.expect)(result).toBe(false);
});
(0, vitest_1.it)("should return false for story without changes", () => {
// Arrange
const story = {
published: true,
unpublished_changes: false,
};
// Act
const result = (0, migration_1.isStoryWithUnpublishedChanges)(story);
// Assert
(0, vitest_1.expect)(result).toBe(false);
});
});
(0, vitest_1.describe)("runMigration", () => {
const mockStories = [
{
id: 1,
full_slug: "test-story-1",
content: { component: "test-component" },
published: true,
unpublished_changes: false,
},
{
id: 2,
full_slug: "test-story-2",
content: { component: "test-component" },
published: true,
unpublished_changes: false,
},
];
(0, vitest_1.beforeEach)(() => {
vitest_1.vi.clearAllMocks();
api_1.api.stories.getAll.mockResolvedValue({
data: { stories: mockStories },
});
api_1.api.stories.get.mockResolvedValue({
data: { story: mockStories[0] },
});
api_1.api.stories.update.mockResolvedValue({});
storyblok_1.processMigration.mockImplementation((content) => {
content.component = "updated-component";
});
});
(0, vitest_1.it)("should execute migration successfully", async () => {
// Arrange
const migrationFn = vitest_1.vi.fn();
// Act
const result = await (0, migration_1.runMigration)("test-component", migrationFn);
// Assert
(0, vitest_1.expect)(result.executed).toBe(true);
(0, vitest_1.expect)(api_1.api.stories.getAll).toHaveBeenCalledWith({
contain_component: "test-component",
});
(0, vitest_1.expect)(api_1.api.stories.get).toHaveBeenCalled();
(0, vitest_1.expect)(api_1.api.stories.update).toHaveBeenCalled();
(0, vitest_1.expect)(storyblok_1.processMigration).toHaveBeenCalled();
});
(0, vitest_1.it)("should handle no stories found", async () => {
// Arrange
api_1.api.stories.getAll.mockResolvedValue({
data: { stories: [] },
});
const migrationFn = vitest_1.vi.fn();
// Act
const result = await (0, migration_1.runMigration)("test-component", migrationFn);
// Assert
(0, vitest_1.expect)(result.executed).toBe(false);
(0, vitest_1.expect)(result.motive).toBe("NO_STORIES");
});
(0, vitest_1.it)("should handle dry run mode", async () => {
// Arrange
const migrationFn = vitest_1.vi.fn();
// Act
const result = await (0, migration_1.runMigration)("test-component", migrationFn, {
isDryrun: true,
});
// Assert
(0, vitest_1.expect)(result.executed).toBe(true);
(0, vitest_1.expect)(api_1.api.stories.update).not.toHaveBeenCalled();
});
(0, vitest_1.it)("should handle errors gracefully", async () => {
// Arrange
api_1.api.stories.get.mockRejectedValue(new Error("API Error"));
const migrationFn = vitest_1.vi.fn();
// Act
const result = await (0, migration_1.runMigration)("test-component", migrationFn);
// Assert
(0, vitest_1.expect)(result.executed).toBe(true);
});
});
(0, vitest_1.describe)("showMigrationChanges", () => {
(0, vitest_1.beforeEach)(() => {
(0, setup_1.clearConsoleOutput)();
});
(0, vitest_1.it)("should show created field", async () => {
// Arrange
const field = "newField";
const value = "value";
// Act
await (0, migration_1.showMigrationChanges)(field, value, undefined);
// Assert
const output = (0, setup_1.getConsoleOutput)();
(0, vitest_1.expect)(output).toContainEqual({
type: "log",
args: [vitest_1.expect.stringContaining('Created field "newField"')],
});
});
(0, vitest_1.it)("should show removed field", async () => {
// Arrange
const field = "oldField";
const oldValue = "value";
// Act
await (0, migration_1.showMigrationChanges)(field, undefined, oldValue);
// Assert
const output = (0, setup_1.getConsoleOutput)();
(0, vitest_1.expect)(output).toContainEqual({
type: "log",
args: [vitest_1.expect.stringContaining('Removed the field "oldField"')],
});
});
(0, vitest_1.it)("should show updated field", async () => {
// Arrange
const field = "field";
const newValue = "newValue";
const oldValue = "oldValue";
// Act
await (0, migration_1.showMigrationChanges)(field, newValue, oldValue);
// Assert
const output = (0, setup_1.getConsoleOutput)();
(0, vitest_1.expect)(output).toContainEqual({
type: "log",
args: [vitest_1.expect.stringContaining('Updated field "field"')],
});
});
});
(0, vitest_1.describe)("defineMigration", () => {
(0, vitest_1.it)("should return the same migration object", () => {
// Arrange
const migration = {
type: "create-component",
schema: {
name: "Test Component",
display_name: "Test Component",
is_root: false,
is_nestable: true,
schema: {},
},
};
// Act
const result = (0, migration_1.defineMigration)(migration);
// Assert
(0, vitest_1.expect)(result).toBe(migration);
});
(0, vitest_1.it)("should maintain type safety", () => {
// Arrange
const migration = {
type: "create-component",
schema: {
name: "Test Component",
display_name: "Test Component",
is_root: false,
is_nestable: true,
schema: {},
},
};
// Act
const result = (0, migration_1.defineMigration)(migration);
// Assert
(0, vitest_1.expect)(result.type).toBe("create-component");
(0, vitest_1.expect)(result.schema.name).toBe("Test Component");
});
});
(0, vitest_1.describe)("Component", () => {
let component;
(0, vitest_1.beforeEach)(() => {
component = new migration_1.Component();
vitest_1.vi.clearAllMocks();
});
(0, vitest_1.it)("should create a new component", async () => {
// Arrange
const mockComponent = {
name: "test-component",
schema: {},
created_at: new Date().toISOString(),
id: 1,
};
api_1.api.components.create.mockResolvedValue({
data: { component: mockComponent },
});
// Act
await component.create("test-component");
// Assert
(0, vitest_1.expect)(component.instance).toBe(mockComponent);
(0, vitest_1.expect)(api_1.api.components.create).toHaveBeenCalledWith({
name: "test-component",
});
});
(0, vitest_1.it)("should load an existing component", async () => {
// Arrange
const mockComponent = {
name: "test-component",
schema: {},
created_at: new Date().toISOString(),
id: 1,
};
api_1.api.components.get.mockResolvedValue({
data: { component: mockComponent },
});
// Act
await component.load("test-component");
// Assert
(0, vitest_1.expect)(component.instance).toBe(mockComponent);
(0, vitest_1.expect)(api_1.api.components.get).toHaveBeenCalledWith("test-component");
});
(0, vitest_1.it)("should add or update fields", () => {
// Arrange
const mockComponent = {
name: "test-component",
schema: {},
created_at: new Date().toISOString(),
id: 1,
};
component.instance = mockComponent;
const fields = {
field1: { type: "text", pos: 0 },
field2: { type: "number", pos: 1 },
};
// Act
component.addOrUpdateFields(fields);
// Assert
(0, vitest_1.expect)(component.instance.schema).toBeDefined();
});
});
(0, vitest_1.describe)("helper", () => {
(0, vitest_1.it)("should update existing component", async () => {
// Arrange
const mockComponent = {
name: "test-component",
schema: {},
created_at: new Date().toISOString(),
id: 1,
};
api_1.api.components.get.mockResolvedValue({
data: { component: mockComponent },
});
// Act
const component = await migration_1.helper.updateComponent("test-component");
// Assert
(0, vitest_1.expect)(component).toBeInstanceOf(migration_1.Component);
(0, vitest_1.expect)(api_1.api.components.get).toHaveBeenCalledWith("test-component");
});
(0, vitest_1.it)("should create new component if not found", async () => {
// Arrange
api_1.api.components.get.mockRejectedValue(new Error("Not found"));
api_1.api.components.create.mockResolvedValue({
data: {
component: {
name: "test-component",
schema: {},
created_at: new Date().toISOString(),
id: 1,
},
},
});
// Act
const component = await migration_1.helper.updateComponent("test-component");
// Assert
(0, vitest_1.expect)(component).toBeInstanceOf(migration_1.Component);
(0, vitest_1.expect)(api_1.api.components.create).toHaveBeenCalledWith({
name: "test-component",
});
});
});
(0, vitest_1.describe)("findMigrations", () => {
(0, vitest_1.beforeEach)(() => {
vitest_1.vi.clearAllMocks();
});
(0, vitest_1.it)("should return empty array if migrations directory doesn't exist", async () => {
// Arrange
fs_1.default.existsSync.mockReturnValue(false);
// Act
const result = await (0, migration_1.findMigrations)();
// Assert
(0, vitest_1.expect)(result).toEqual([]);
(0, vitest_1.expect)(fs_1.default.existsSync).toHaveBeenCalledWith(vitest_1.expect.stringContaining("migrations"));
});
(0, vitest_1.it)("should find .js and .ts files in migrations directory", async () => {
// Arrange
fs_1.default.existsSync.mockReturnValue(true);
fs_1.default.readdirSync.mockReturnValue([
{ name: "migration1.js", isFile: () => true, isDirectory: () => false },
{ name: "migration2.ts", isFile: () => true, isDirectory: () => false },
{ name: "other.txt", isFile: () => true, isDirectory: () => false },
]);
// Act
const result = await (0, migration_1.findMigrations)();
// Assert
(0, vitest_1.expect)(result).toEqual(["migration1.js", "migration2.ts"]);
(0, vitest_1.expect)(result).not.toContain("other.txt");
});
(0, vitest_1.it)("should recursively search subdirectories", async () => {
// Arrange
fs_1.default.existsSync.mockReturnValue(true);
fs_1.default.readdirSync
.mockImplementationOnce(() => [
{ name: "schema", isFile: () => false, isDirectory: () => true },
{ name: "content", isFile: () => false, isDirectory: () => true },
])
.mockImplementationOnce(() => [
{
name: "migration1.js",
isFile: () => true,
isDirectory: () => false,
},
{
name: "migration2.ts",
isFile: () => true,
isDirectory: () => false,
},
])
.mockImplementationOnce(() => [
{
name: "migration3.js",
isFile: () => true,
isDirectory: () => false,
},
]);
// Act
const result = await (0, migration_1.findMigrations)();
// Assert
(0, vitest_1.expect)(result).toEqual([
"content/migration3.js",
"schema/migration1.js",
"schema/migration2.ts",
]);
});
(0, vitest_1.it)("should ignore non-migration files", async () => {
// Arrange
fs_1.default.existsSync.mockReturnValue(true);
fs_1.default.readdirSync.mockReturnValue([
{ name: "migration1.js", isFile: () => true, isDirectory: () => false },
{ name: "config.json", isFile: () => true, isDirectory: () => false },
{ name: "README.md", isFile: () => true, isDirectory: () => false },
{ name: "migration2.ts", isFile: () => true, isDirectory: () => false },
]);
// Act
const result = await (0, migration_1.findMigrations)();
// Assert
(0, vitest_1.expect)(result).toEqual(["migration1.js", "migration2.ts"]);
(0, vitest_1.expect)(result).not.toContain("config.json");
(0, vitest_1.expect)(result).not.toContain("README.md");
});
(0, vitest_1.it)("should return sorted list of migrations", async () => {
// Arrange
fs_1.default.existsSync.mockReturnValue(true);
fs_1.default.readdirSync.mockReturnValue([
{
name: "z-migration.js",
isFile: () => true,
isDirectory: () => false,
},
{
name: "a-migration.ts",
isFile: () => true,
isDirectory: () => false,
},
{
name: "m-migration.js",
isFile: () => true,
isDirectory: () => false,
},
]);
// Act
const result = await (0, migration_1.findMigrations)();
// Assert
(0, vitest_1.expect)(result).toEqual([
"a-migration.ts",
"m-migration.js",
"z-migration.js",
]);
});
});
});
//# sourceMappingURL=migration.test.js.map