diff --git a/.github/actions/node/tests/action.yaml b/.github/actions/node/tests/action.yaml index f5201cc..011e09a 100644 --- a/.github/actions/node/tests/action.yaml +++ b/.github/actions/node/tests/action.yaml @@ -25,12 +25,13 @@ runs: yarn lint:check yarn format:check + - name: Build and run migrations + shell: bash + run: NODE_ENV=test yarn migration:run + - name: Execute tests shell: bash run: | yarn test yarn test:e2e - - name: Test build - shell: bash - run: yarn build diff --git a/.gitignore b/.gitignore index 1eb06b8..8782db1 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,7 @@ lerna-debug.log* .env.test.local .env.production.local .env.local +local.env.ts # temp directory .temp diff --git a/env/custom.env.ts b/env/custom.env.ts index e25880d..d134ecb 100644 --- a/env/custom.env.ts +++ b/env/custom.env.ts @@ -22,6 +22,7 @@ export default () => { database: process.env.DATABASE_CONNECTION_DATABASE, host: process.env.DATABASE_CONNECTION_HOST, password: process.env.DATABASE_CONNECTION_PASSWORD, + port: process.env.DATABASE_CONNECTION_PORT ?? 5432, username: process.env.DATABASE_CONNECTION_USERNAME, }, driver: process.env.DATABASE_DRIVER, @@ -34,5 +35,5 @@ export default () => { }, }; - return merge(configuration(), customConfig); + return merge(customConfig, configuration()); }; diff --git a/env/dev.env.ts b/env/dev.env.ts index 56bf55f..66e4eed 100644 --- a/env/dev.env.ts +++ b/env/dev.env.ts @@ -1 +1,13 @@ -export default () => ({}); +export default () => ({ + database: { + connection: { + database: 'postgres', + host: 'aws-0-eu-west-3.pooler.supabase.com', + port: 5432, + }, + driver: 'postgres', + }, + graphql: { + debug: true, + }, +}); diff --git a/env/prd.env.ts b/env/prd.env.ts index 6134c3c..a260329 100644 --- a/env/prd.env.ts +++ b/env/prd.env.ts @@ -1,4 +1,12 @@ export default () => ({ + database: { + connection: { + database: 'postgres', + host: 'aws-0-eu-west-3.pooler.supabase.com', + port: 5432, + }, + driver: 'postgres', + }, graphql: { debug: false, }, diff --git a/env/test.env.ts b/env/test.env.ts index e748337..245bf1e 100644 --- a/env/test.env.ts +++ b/env/test.env.ts @@ -3,6 +3,7 @@ export default () => ({ connection: { database: 'test', host: 'localhost', + port: '5432', username: 'postgres', }, driver: 'postgres', diff --git a/package.json b/package.json index 24bf717..70e39ba 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,6 @@ "author": "", "private": true, "license": "UNLICENSED", - "type": "module", "scripts": { "build": "nest build", "docker": "docker-compose -f docker-compose.dev.yml up", @@ -13,6 +12,9 @@ "format:fix": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", "lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", "lint:check": "eslint \"{src,apps,libs,test}/**/*.ts\"", + "migration:generate": "yarn typeorm migration:generate", + "migration:revert": "yarn typeorm migration:revert", + "migration:run": "yarn build && yarn typeorm migration:run -d ./dist/src/database/datasource.config.js", "prepare": "husky", "start:debug": "nest start --debug --watch", "start:dev": "nest start --watch", @@ -23,7 +25,8 @@ "test:e2e": "jest --config jest-e2e.config.cjs --bail --runInBand --detectOpenHandles --forceExit", "test:watch": "jest --watch", "test:unit": "jest --config jest.config.cjs --bail --runInBand --detectOpenHandles --forceExit", - "test": "yarn test:unit && yarn test:e2e", + "test": "NODE_ENV=test yarn test:unit && yarn test:e2e", + "typeorm": "./node_modules/typeorm/cli.js", "type:check": "tsc" }, "dependencies": { @@ -40,7 +43,8 @@ "pg": "^8.12.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", - "typeorm": "^0.3.20" + "typeorm": "^0.3.20", + "typeorm-naming-strategies": "^4.1.0" }, "devDependencies": { "@nestjs/cli": "^10.0.0", diff --git a/src/config/types/database.config.ts b/src/config/types/database.config.ts index a6d70a5..537d35f 100644 --- a/src/config/types/database.config.ts +++ b/src/config/types/database.config.ts @@ -2,6 +2,7 @@ export type Database = { connection: { database: string; host: string; + port?: number; password?: string; username: string; }; diff --git a/src/database/base.repository.ts b/src/database/base.repository.ts index 74ca392..4835127 100644 --- a/src/database/base.repository.ts +++ b/src/database/base.repository.ts @@ -66,15 +66,14 @@ export class BaseRepository extends Repository { async patchById(id: string, payload: Partial): Promise> { const qb = this.createQueryBuilder(); - const result = await qb - .update() - .set(payload) - .where(`${this.primaryColumn} = :id`, { id }) - .returning('*') - .execute(); + + await qb.update().set(payload).where(`${this.primaryColumn} = :id`, { id }).execute(); + + // We need to fetch to get the data serialized. + const { data } = await this.findById(id); return { - data: head(result.raw), + data, message: `${this.metadata.tableName} successfully patched and fetched`, }; } diff --git a/src/database/database.module.ts b/src/database/database.module.ts index ce7c38e..efbbf41 100644 --- a/src/database/database.module.ts +++ b/src/database/database.module.ts @@ -1,30 +1,27 @@ import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { DataSource } from 'typeorm'; -import { Database } from '@config/types/database.config'; -import { Spot } from '@spot/entities/spot.entity'; +import datasource, { datasourceConfig } from '@database/datasource.config'; @Module({ - exports: [TypeOrmModule], imports: [ TypeOrmModule.forRootAsync({ imports: [ConfigModule], inject: [ConfigService], - useFactory: (configService: ConfigService) => { - const database = configService.get('database'); - - return { - database: database.connection.database, - entities: [Spot], - host: database.connection.host, - password: database.connection.password, - synchronize: true, - type: database.driver, - username: database.connection.username, - }; + useFactory: () => { + return datasourceConfig; }, }), ], + providers: [ + { + provide: DataSource, + useFactory: () => { + return datasource.initialize(); + }, + }, + ], }) export class DatabaseModule {} diff --git a/src/database/datasource.config.ts b/src/database/datasource.config.ts new file mode 100644 index 0000000..29cb217 --- /dev/null +++ b/src/database/datasource.config.ts @@ -0,0 +1,25 @@ +import { get } from 'lodash'; +import { DataSource, DataSourceOptions } from 'typeorm'; +import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; + +import { Database } from '@app/config/types/database.config'; +import { Spot } from '@spot/entities/spot.entity'; + +import config from '../../env/custom.env'; + +const databaseConfig: Database = get(config(), 'database'); +const { + connection: { database, host, password, port, username }, +} = databaseConfig; + +export const datasourceConfig: DataSourceOptions = { + entities: [Spot], + migrations: [__dirname + '/migrations/*.js'], + migrationsRun: false, + namingStrategy: new SnakeNamingStrategy(), + synchronize: false, + type: databaseConfig.driver, + url: `postgresql://${username}:${password}@${host}:${port}/${database}`, +}; + +export default new DataSource(datasourceConfig); diff --git a/src/database/migrations/1733768671997-create-spot.ts b/src/database/migrations/1733768671997-create-spot.ts new file mode 100644 index 0000000..d3acdb7 --- /dev/null +++ b/src/database/migrations/1733768671997-create-spot.ts @@ -0,0 +1,42 @@ +import { MigrationInterface, QueryRunner, Table } from 'typeorm'; + +export class CreateSpot1733768671997 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createTable( + new Table({ + columns: [ + { + default: 'now()', + name: 'created_at', + type: 'timestamp with time zone', + }, + { + default: 'uuid_generate_v4()', + isPrimary: true, + name: 'id', + type: 'uuid', + }, + { + name: 'location', + type: 'varchar', + }, + { + name: 'name', + type: 'varchar', + }, + { + default: 'now()', + name: 'updated_at', + type: 'timestamp with time zone', + }, + ], + name: 'spot', + }), + true, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + return queryRunner.dropTable('spot'); + } +} diff --git a/src/spot/entities/spot.entity.ts b/src/spot/entities/spot.entity.ts index f80dc95..ffb5656 100644 --- a/src/spot/entities/spot.entity.ts +++ b/src/spot/entities/spot.entity.ts @@ -10,7 +10,7 @@ import { @Entity() @ObjectType() export class Spot { - @CreateDateColumn() + @CreateDateColumn({ type: 'timestamp with time zone' }) @Field() createdAt: Date; @@ -26,7 +26,7 @@ export class Spot { @Field() location: string; - @UpdateDateColumn() + @UpdateDateColumn({ type: 'timestamp with time zone' }) @Field() updatedAt: Date; } diff --git a/src/spot/spot.module.ts b/src/spot/spot.module.ts index 79e539f..f1791a1 100644 --- a/src/spot/spot.module.ts +++ b/src/spot/spot.module.ts @@ -1,13 +1,10 @@ import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; -import { Spot } from '@spot/entities/spot.entity'; import { SpotRepository } from '@spot/spot.repository'; import { SpotResolver } from '@spot/spot.resolver'; import { SpotService } from '@spot/spot.service'; @Module({ - imports: [TypeOrmModule.forFeature([Spot])], providers: [SpotRepository, SpotResolver, SpotService], }) export class SpotModule {} diff --git a/test/fixtures/spot-fixture.ts b/test/fixtures/spot-fixture.ts index a758ca7..5e81f08 100644 --- a/test/fixtures/spot-fixture.ts +++ b/test/fixtures/spot-fixture.ts @@ -18,4 +18,4 @@ export const spotFixture = async (module: TestingModule, payload?: SpotFixturePa * Types. */ -export type SpotFixturePayload = Omit; +export type SpotFixturePayload = Partial>; diff --git a/test/utils/database-util.ts b/test/utils/database-util.ts index 0d79466..a046719 100644 --- a/test/utils/database-util.ts +++ b/test/utils/database-util.ts @@ -1,20 +1,6 @@ import { INestApplication } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import { Spot } from '@spot/entities/spot.entity'; - -export function createConnection() { - return new DataSource({ - database: 'test', - entities: [Spot], - host: 'localhost', - port: 5432, - synchronize: true, - type: 'postgres', - username: 'postgres', - }); -} - export const clearDatabase = async (app: INestApplication) => { const dataSource = app.get(DataSource); const entities = dataSource.entityMetadatas; @@ -24,6 +10,4 @@ export const clearDatabase = async (app: INestApplication) => { await repository.clear(); } - - await dataSource.synchronize(true); }; diff --git a/test/utils/nest-testing-module.ts b/test/utils/nest-testing-module.ts index b5d00a9..3a2d648 100644 --- a/test/utils/nest-testing-module.ts +++ b/test/utils/nest-testing-module.ts @@ -9,7 +9,6 @@ export const initializeApp = async (): Promise => { const app = module.createNestApplication(); const dataSource = app.get(DataSource); - await dataSource.synchronize(true); await app.init(); return { app, dataSource, module }; diff --git a/yarn.lock b/yarn.lock index efa20ee..4b9bf89 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1064,6 +1064,11 @@ resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== +"@pkgr/core@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@pkgr/core/-/core-0.1.1.tgz#1ec17e2edbec25c8306d424ecfbf13c7de1aaa31" + integrity sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA== + "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" @@ -2932,19 +2937,13 @@ eslint-plugin-jest@^28.8.0: dependencies: "@typescript-eslint/utils" "^6.0.0 || ^7.0.0 || ^8.0.0" -eslint-plugin-prettier@4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b" - integrity sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ== +eslint-plugin-prettier@^5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz#d1c8f972d8f60e414c25465c163d16f209411f95" + integrity sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw== dependencies: prettier-linter-helpers "^1.0.0" - -eslint-plugin-sort-destructure-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-sort-destructure-keys/-/eslint-plugin-sort-destructure-keys-2.0.0.tgz#23d26e3db4a8fb73fcd0dfceb2de4c517e6d603f" - integrity sha512-4w1UQCa3o/YdfWaLr9jY8LfGowwjwjmwClyFLxIsToiyIdZMq3x9Ti44nDn34DtTPP7PWg96tUONKVmATKhYGQ== - dependencies: - natural-compare-lite "^1.4.0" + synckit "^0.9.1" eslint-plugin-sort-destructure-keys@^2.0.0: version "2.0.0" @@ -5799,6 +5798,14 @@ symbol-observable@^1.0.4: resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== +synckit@^0.9.1: + version "0.9.2" + resolved "https://registry.yarnpkg.com/synckit/-/synckit-0.9.2.tgz#a3a935eca7922d48b9e7d6c61822ee6c3ae4ec62" + integrity sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw== + dependencies: + "@pkgr/core" "^0.1.0" + tslib "^2.6.2" + tapable@^2.1.1, tapable@^2.2.0, tapable@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" @@ -6063,6 +6070,11 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== +typeorm-naming-strategies@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/typeorm-naming-strategies/-/typeorm-naming-strategies-4.1.0.tgz#1ec6eb296c8d7b69bb06764d5b9083ff80e814a9" + integrity sha512-vPekJXzZOTZrdDvTl1YoM+w+sUIfQHG4kZTpbFYoTsufyv9NIBRe4Q+PdzhEAFA2std3D9LZHEb1EjE9zhRpiQ== + typeorm@^0.3.20: version "0.3.20" resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.20.tgz#4b61d737c6fed4e9f63006f88d58a5e54816b7ab"