Skip to content

Commit 07a8412

Browse files
committed
fix(mysql): boolean values return numbers
also refactored transformers by extracting them from entity loader service
1 parent 254c884 commit 07a8412

12 files changed

+225
-12
lines changed

package-lock.json

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"moduleFileExtensions": ["js", "json", "ts"],
3+
"rootDir": ".",
4+
"testEnvironment": "node",
5+
"testRegex": ".e2e-spec.ts$",
6+
"transform": {
7+
"^.+\\.ts$": "ts-jest"
8+
},
9+
"maxWorkers": 1,
10+
"setupFilesAfterEnv": ["./jest.mysql-setup.ts"],
11+
"moduleNameMapper": {
12+
"^@repo/types$": "<rootDir>/../../types/src",
13+
"^@repo/common$": "<rootDir>/../../common/src",
14+
"^@repo/json-schema$": "<rootDir>/../../json-schema/src"
15+
}
16+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { Test, TestingModule } from '@nestjs/testing'
2+
import { AppModule } from '../src/app.module'
3+
import { YamlService } from '../src/manifest/services/yaml.service'
4+
import { INestApplication } from '@nestjs/common'
5+
import supertest from 'supertest'
6+
import { load } from 'js-yaml'
7+
import fs from 'fs'
8+
import { SwaggerModule } from '@nestjs/swagger'
9+
import { OpenApiService } from '../src/open-api/services/open-api.service'
10+
import { SeederService } from '../src/seed/services/seeder.service'
11+
import { MySqlContainer, StartedMySqlContainer } from '@testcontainers/mysql'
12+
13+
import path from 'path'
14+
15+
let app: INestApplication
16+
17+
jest.setTimeout(30000) // Increase the timeout because the MySQL container takes a while to start.
18+
19+
beforeAll(async () => {
20+
// Set environment variables for testing.
21+
process.env.NODE_ENV = 'test'
22+
process.env.TOKEN_SECRET_KEY = 'test'
23+
process.env.MANIFEST_HANDLERS_FOLDER = path.join(
24+
__dirname,
25+
'assets',
26+
'handlers'
27+
)
28+
29+
process.env.DB_CONNECTION = 'mysql'
30+
process.env.DB_DROP_SCHEMA = 'true'
31+
32+
// Start a PostgreSQL test container
33+
const mysqlContainer: StartedMySqlContainer = await new MySqlContainer()
34+
.withDatabase('test')
35+
.withUsername('test')
36+
.withRootPassword('test')
37+
.start()
38+
39+
process.env.DB_HOST = mysqlContainer.getHost()
40+
process.env.DB_PORT = mysqlContainer.getPort().toString()
41+
process.env.DB_USERNAME = 'test'
42+
process.env.DB_PASSWORD = 'test'
43+
process.env.DB_DATABASE = 'test'
44+
45+
// Start the NestJS application mocking some services.
46+
const moduleFixture: TestingModule = await Test.createTestingModule({
47+
imports: [AppModule]
48+
})
49+
.overrideProvider(YamlService)
50+
.useValue({
51+
load: () =>
52+
load(
53+
fs.readFileSync(
54+
`${process.cwd()}/e2e/assets/mock-backend.yml`,
55+
'utf8'
56+
)
57+
)
58+
})
59+
.compile()
60+
61+
app = moduleFixture.createNestApplication()
62+
63+
// Seed the database with the mock data.
64+
const seedService = app.get(SeederService)
65+
await seedService.seed('admin')
66+
await seedService.seed('cat')
67+
await seedService.seed('university')
68+
await seedService.seed('author')
69+
await seedService.seed('tag')
70+
await seedService.seed('note')
71+
72+
// Store request object in global scope to use in tests.
73+
global.request = supertest(app.getHttpServer())
74+
75+
// Set the SwaggerModule to serve the OpenAPI doc.
76+
const openApiService = app.get(OpenApiService)
77+
SwaggerModule.setup('api', app, openApiService.generateOpenApiObject())
78+
79+
await app.init()
80+
})
81+
82+
afterAll(async () => {
83+
delete global.request
84+
await app.close()
85+
})

packages/core/manifest/e2e/jest.pg-setup.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,6 @@ beforeAll(async () => {
4444
process.env.DB_USERNAME = 'test'
4545
process.env.DB_PASSWORD = 'test'
4646
process.env.DB_DATABASE = 'test'
47-
process.env.DB_TYPE = 'postgres'
4847

4948
// Start the NestJS application mocking some services.
5049
const moduleFixture: TestingModule = await Test.createTestingModule({

packages/core/manifest/e2e/tests/collection-crud.e2e-spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ describe('Collection CRUD (e2e)', () => {
99
birthdate: '2024-01-01',
1010
price: 100,
1111
isGoodBoy: true,
12-
acquiredAt: new Date().toISOString(),
12+
acquiredAt: '2025-01-05T08:19:02.000Z',
1313
1414
favoriteToy: 'ball',
1515
location: { lat: 12, lng: 13 }

packages/core/manifest/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
4646
"test:e2e:sqlite": "jest --config ./e2e/jest-e2e.sqlite-config.json",
4747
"test:e2e:postgres": "jest --config ./e2e/jest-e2e.pg-config.json",
48+
"test:e2e:mysql": "jest --config ./e2e/jest-e2e.mysql-config.json",
4849
"test:e2e": "npm run test:e2e:sqlite && npm run test:e2e:postgres",
4950
"test:e2e:ci": "npm run test:e2e:sqlite -- --ci && npm run test:e2e:postgres -- --ci",
5051
"test:unit": "jest --config ./jest.config.json",
@@ -98,6 +99,7 @@
9899
"@nestjs/cli": "^10.4.7",
99100
"@nestjs/schematics": "^10.2.3",
100101
"@nestjs/testing": "^10.4.8",
102+
"@testcontainers/mysql": "^10.18.0",
101103
"@testcontainers/postgresql": "^10.18.0",
102104
"@types/bcrypt": "^5.0.2",
103105
"@types/dasherize": "^2.0.3",

packages/core/manifest/src/entity/services/entity-loader.service.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import { EntityManifestService } from '../../manifest/services/entity-manifest.s
1919
import { mysqlPropTypeColumnTypes } from '../columns/mysql-prop-type-column-types'
2020
import { postgresPropTypeColumnTypes } from '../columns/postgres-prop-type-column-types copy'
2121
import { ColumnService } from './column.service'
22+
import { BooleanTransformer } from '../transformers/boolean-transformer'
23+
import { NumberTransformer } from '../transformers/number-transformer'
24+
import { TimestampTransformer } from '../transformers/timestamp-transformer'
2225

2326
@Injectable()
2427
export class EntityLoaderService {
@@ -72,19 +75,16 @@ export class EntityLoaderService {
7275
propManifest.type === PropType.Number ||
7376
propManifest.type === PropType.Money
7477
) {
75-
transformer = {
76-
from: (value: string | number) => Number(value),
77-
to: (value: string | number) => value
78-
}
78+
transformer = new NumberTransformer()
7979
}
8080

8181
// Ensure it returns strings for timestamps (SQLite returns Date objects by default).
8282
if (propManifest.type === PropType.Timestamp) {
83-
transformer = {
84-
from: (value: Date | string) =>
85-
value instanceof Date ? value.toISOString() : value,
86-
to: (value: string) => value // Store as string
87-
}
83+
transformer = new TimestampTransformer()
84+
}
85+
86+
if (propManifest.type === PropType.Boolean) {
87+
transformer = new BooleanTransformer(dbConnection)
8888
}
8989

9090
acc[propManifest.name] = {

packages/core/manifest/src/entity/tests/entity-loader.service.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ describe('EntityLoaderService', () => {
140140
expect(mysqlResult[0].options.columns['timestamp-prop'].type).toBe(
141141
'datetime'
142142
)
143-
expect(mysqlResult[0].options.columns['choice-prop'].type).toBe('enum')
143+
expect(mysqlResult[0].options.columns['choice-prop'].type).toBe('varchar')
144144
expect(mysqlResult[0].options.columns['location-prop'].type).toBe('json')
145145
expect(mysqlResult[0].options.columns['image-prop'].type).toBe('json')
146146
})
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { BooleanTransformer } from '../transformers/boolean-transformer'
2+
import { NumberTransformer } from '../transformers/number-transformer'
3+
import { TimestampTransformer } from '../transformers/timestamp-transformer'
4+
5+
describe('Transformers', () => {
6+
describe('BooleanTransformer', () => {
7+
it('should convert boolean to number for MySQL', () => {
8+
const connection = 'mysql'
9+
const transformer = new BooleanTransformer(connection)
10+
const value = true
11+
const result = transformer.to(value)
12+
expect(result).toBe(1)
13+
})
14+
15+
it('should convert number to boolean for MySQL', () => {
16+
const connection = 'mysql'
17+
const transformer = new BooleanTransformer(connection)
18+
const value = 1
19+
const result = transformer.from(value)
20+
expect(result).toBe(true)
21+
})
22+
})
23+
24+
describe('NumberTransformer', () => {
25+
it('should always return a number', () => {
26+
const transformer = new NumberTransformer()
27+
const value = '1'
28+
const result = transformer.from(value)
29+
expect(result).toBe(1)
30+
})
31+
})
32+
33+
describe('TimestampTransformer', () => {
34+
it('should always return a string', () => {
35+
const transformer = new TimestampTransformer()
36+
const value = new Date()
37+
const result = transformer.from(value)
38+
expect(typeof result).toBe('string')
39+
})
40+
})
41+
})
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { ValueTransformer } from 'typeorm'
2+
import { DatabaseConnection } from '../../../../types/src'
3+
4+
/**
5+
* Boolean transformer for TypeORM.
6+
*
7+
* This transformer is used to convert boolean values to numbers for MySQL as it returns 1 for true and 0 for false.
8+
*/
9+
export class BooleanTransformer implements ValueTransformer {
10+
private connection: DatabaseConnection
11+
12+
constructor(connection: DatabaseConnection) {
13+
this.connection = connection
14+
}
15+
16+
to(value: boolean): number {
17+
if (this.connection === 'mysql') {
18+
return value ? 1 : 0
19+
}
20+
}
21+
22+
from(value: number): boolean {
23+
if (this.connection === 'mysql') {
24+
return value === 1
25+
}
26+
}
27+
}

0 commit comments

Comments
 (0)