twenty / packages /twenty-server /src /database /commands /__tests__ /instance-command-generation.service.spec.ts
Jules
Initial clean CRM deployment with prebuilt assets
d9494a5
Raw
History Blame Contribute Delete
6.95 kB
import { Test } from '@nestjs/testing';
import { getDataSourceToken } from '@nestjs/typeorm';
import { InstanceCommandGenerationService } from 'src/database/commands/instance-command-generation.service';
import { type TwentyAllVersion } from 'src/engine/core-modules/upgrade/constants/twenty-all-versions.constant';
const VERSION_A = '10.20.0' as TwentyAllVersion;
const VERSION_B = '30.40.0' as TwentyAllVersion;
const FIXED_TIMESTAMP = 1775000000000;
const buildMockDataSource = (
upQueries: { query: string; parameters?: unknown[] }[],
downQueries: { query: string; parameters?: unknown[] }[],
) => ({
driver: {
createSchemaBuilder: () => ({
log: jest.fn().mockResolvedValue({ upQueries, downQueries }),
}),
},
});
describe('InstanceCommandGenerationService', () => {
const buildService = async (
upQueries: { query: string; parameters?: unknown[] }[] = [],
downQueries: { query: string; parameters?: unknown[] }[] = [],
) => {
const module = await Test.createTestingModule({
providers: [
InstanceCommandGenerationService,
{
provide: getDataSourceToken(),
useValue: buildMockDataSource(upQueries, downQueries),
},
],
}).compile();
return module.get(InstanceCommandGenerationService);
};
it('should return null when no schema changes are detected', async () => {
const service = await buildService();
const result = await service.generateInstanceCommand({
migrationName: 'no-changes',
version: VERSION_A,
timestamp: FIXED_TIMESTAMP,
});
expect(result).toBeNull();
});
it('should generate a migration with a single up/down query', async () => {
const service = await buildService(
[{ query: 'ALTER TABLE "core"."user" ADD "foo" varchar' }],
[{ query: 'ALTER TABLE "core"."user" DROP COLUMN "foo"' }],
);
const result = await service.generateInstanceCommand({
migrationName: 'add-foo-column',
version: VERSION_A,
timestamp: FIXED_TIMESTAMP,
});
expect(result).toMatchSnapshot();
});
it('should generate a migration with multiple queries', async () => {
const service = await buildService(
[
{
query:
'CREATE TABLE "core"."task" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" varchar NOT NULL)',
},
{
query:
'ALTER TABLE "core"."task" ADD CONSTRAINT "PK_task" PRIMARY KEY ("id")',
},
],
[
{ query: 'ALTER TABLE "core"."task" DROP CONSTRAINT "PK_task"' },
{ query: 'DROP TABLE "core"."task"' },
],
);
const result = await service.generateInstanceCommand({
migrationName: 'create-task-table',
version: VERSION_A,
timestamp: FIXED_TIMESTAMP,
});
expect(result).toMatchSnapshot();
});
it('should generate a migration with query parameters', async () => {
const service = await buildService(
[
{
query:
'INSERT INTO "core"."setting" ("key", "value") VALUES ($1, $2)',
parameters: ['theme', 'dark'],
},
],
[
{
query: 'DELETE FROM "core"."setting" WHERE "key" = $1',
parameters: ['theme'],
},
],
);
const result = await service.generateInstanceCommand({
migrationName: 'seed-setting',
version: VERSION_A,
timestamp: FIXED_TIMESTAMP,
});
expect(result).toMatchSnapshot();
});
it('should escape single quotes in SQL queries', async () => {
const service = await buildService(
[{ query: 'UPDATE "core"."config" SET "value" = \'it\'\'s done\'' }],
[{ query: 'UPDATE "core"."config" SET "value" = \'original\'' }],
);
const result = await service.generateInstanceCommand({
migrationName: 'update-config',
version: VERSION_A,
timestamp: FIXED_TIMESTAMP,
});
expect(result).toMatchSnapshot();
});
it('should escape backslashes in SQL queries', async () => {
const service = await buildService(
[
{
query: 'UPDATE "core"."config" SET "value" = E\'path\\\\to\\\\file\'',
},
],
[{ query: 'UPDATE "core"."config" SET "value" = NULL' }],
);
const result = await service.generateInstanceCommand({
migrationName: 'update-path',
version: VERSION_A,
timestamp: FIXED_TIMESTAMP,
});
expect(result).toMatchSnapshot();
});
it('should use default migration name in class and file names', async () => {
const service = await buildService(
[{ query: 'ALTER TABLE "core"."user" ADD "bar" integer' }],
[{ query: 'ALTER TABLE "core"."user" DROP COLUMN "bar"' }],
);
const result = await service.generateInstanceCommand({
migrationName: 'auto-generated',
version: VERSION_A,
timestamp: FIXED_TIMESTAMP,
});
expect(result).toMatchSnapshot();
});
it('should encode version correctly in file and class names', async () => {
const service = await buildService(
[{ query: 'SELECT 1' }],
[{ query: 'SELECT 1' }],
);
const result = await service.generateInstanceCommand({
migrationName: 'test',
version: VERSION_B,
timestamp: FIXED_TIMESTAMP,
});
expect(result).toMatchSnapshot();
});
it('should return null for slow type when no schema changes are detected', async () => {
const service = await buildService();
const result = await service.generateInstanceCommand({
migrationName: 'no-changes',
version: VERSION_A,
timestamp: FIXED_TIMESTAMP,
type: 'slow',
});
expect(result).toBeNull();
});
it('should generate a slow instance command with populated up/down', async () => {
const service = await buildService(
[
{
query: 'ALTER TABLE "core"."user" ALTER COLUMN "email" SET NOT NULL',
},
],
[
{
query: 'ALTER TABLE "core"."user" ALTER COLUMN "email" DROP NOT NULL',
},
],
);
const result = await service.generateInstanceCommand({
migrationName: 'make-column-not-nullable',
version: VERSION_A,
timestamp: FIXED_TIMESTAMP,
type: 'slow',
});
expect(result).toMatchSnapshot();
});
it('should use correct file naming for slow instance commands', async () => {
const service = await buildService(
[{ query: 'SELECT 1' }],
[{ query: 'SELECT 1' }],
);
const result = await service.generateInstanceCommand({
migrationName: 'backfill-data',
version: VERSION_B,
timestamp: FIXED_TIMESTAMP,
type: 'slow',
});
const versionSlug = VERSION_B.split('.').slice(0, 2).join('-');
expect(result?.fileName).toBe(
`${versionSlug}-instance-command-slow-${FIXED_TIMESTAMP}-backfill-data.ts`,
);
expect(result?.className).toBe('BackfillDataSlowInstanceCommand');
});
});