From cb42f6f9459528969d2b9bc16c01d47cda294e7d Mon Sep 17 00:00:00 2001 From: Nikita Kudinov Date: Wed, 17 Jan 2024 01:12:41 +0300 Subject: [PATCH] feat: initialize drizzle with tables --- package.json | 4 + pnpm-lock.yaml | 158 +++++++++++++++++- src/core/services/base.service.ts | 94 +++++++++++ src/infra/postgres/other/base-columns.ts | 28 ++++ src/infra/postgres/postgres.module.ts | 27 ++- src/infra/postgres/tables/index.ts | 10 ++ .../postgres/tables/opened-packs.table.ts | 33 ++++ .../postgres/tables/pack-pokemons.table.ts | 26 +++ src/infra/postgres/tables/packs.table.ts | 22 +++ src/infra/postgres/tables/pokemons.table.ts | 19 +++ .../tables/quick-sold-user-items.table.ts | 25 +++ .../tables/trade-receiver-items.table.ts | 20 +++ .../tables/trade-sender-items.table.ts | 20 +++ src/infra/postgres/tables/trades.table.ts | 61 +++++++ src/infra/postgres/tables/user-items.table.ts | 29 ++++ src/infra/postgres/tables/users.table.ts | 26 +++ 16 files changed, 599 insertions(+), 3 deletions(-) create mode 100644 src/core/services/base.service.ts create mode 100644 src/infra/postgres/other/base-columns.ts create mode 100644 src/infra/postgres/tables/index.ts create mode 100644 src/infra/postgres/tables/opened-packs.table.ts create mode 100644 src/infra/postgres/tables/pack-pokemons.table.ts create mode 100644 src/infra/postgres/tables/packs.table.ts create mode 100644 src/infra/postgres/tables/pokemons.table.ts create mode 100644 src/infra/postgres/tables/quick-sold-user-items.table.ts create mode 100644 src/infra/postgres/tables/trade-receiver-items.table.ts create mode 100644 src/infra/postgres/tables/trade-sender-items.table.ts create mode 100644 src/infra/postgres/tables/trades.table.ts create mode 100644 src/infra/postgres/tables/user-items.table.ts create mode 100644 src/infra/postgres/tables/users.table.ts diff --git a/package.json b/package.json index d0c9040..a4583bb 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "@automapper/classes": "8.7.7", "@automapper/core": "8.7.7", "@automapper/nestjs": "8.7.7", + "@automapper/pojos": "^8.7.7", + "@knaadh/nestjs-drizzle-pg": "^1.0.0", "@nestjs/common": "9.0.8", "@nestjs/config": "3.1.1", "@nestjs/core": "9.0.8", @@ -34,6 +36,7 @@ "class-transformer": "0.5.1", "class-validator": "0.14.0", "dotenv": "16.3.1", + "drizzle-orm": "^0.29.3", "nest-transact": "9.1.2", "nestjs-seeder": "0.3.2", "nestjs-typeorm-paginate": "^4.0.4", @@ -55,6 +58,7 @@ "@types/node": "20.10.5", "@types/passport-jwt": "3.0.13", "@types/passport-local": "1.0.38", + "@types/pg": "^8.10.9", "@typescript-eslint/eslint-plugin": "6.15.0", "@typescript-eslint/parser": "6.15.0", "eslint": "8.56.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2277c09..4d3fb74 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,12 @@ dependencies: '@automapper/nestjs': specifier: 8.7.7 version: 8.7.7(@automapper/core@8.7.7)(@nestjs/common@9.0.8)(@nestjs/core@9.0.8) + '@automapper/pojos': + specifier: ^8.7.7 + version: 8.7.7(@automapper/core@8.7.7) + '@knaadh/nestjs-drizzle-pg': + specifier: ^1.0.0 + version: 1.0.0(@nestjs/common@9.0.8)(drizzle-orm@0.29.3)(pg@8.11.3) '@nestjs/common': specifier: 9.0.8 version: 9.0.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) @@ -50,6 +56,9 @@ dependencies: dotenv: specifier: 16.3.1 version: 16.3.1 + drizzle-orm: + specifier: ^0.29.3 + version: 0.29.3(@types/pg@8.10.9)(pg@8.11.3) nest-transact: specifier: 9.1.2 version: 9.1.2(@nestjs/common@9.0.8)(@nestjs/core@9.0.8)(reflect-metadata@0.1.13)(rxjs@7.8.1)(typeorm@0.3.17) @@ -109,6 +118,9 @@ devDependencies: '@types/passport-local': specifier: 1.0.38 version: 1.0.38 + '@types/pg': + specifier: ^8.10.9 + version: 8.10.9 '@typescript-eslint/eslint-plugin': specifier: 6.15.0 version: 6.15.0(@typescript-eslint/parser@6.15.0)(eslint@8.56.0)(typescript@4.7.4) @@ -224,6 +236,15 @@ packages: '@nestjs/core': 9.0.8(@nestjs/common@9.0.8)(@nestjs/platform-express@9.0.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) dev: false + /@automapper/pojos@8.7.7(@automapper/core@8.7.7): + resolution: {integrity: sha512-8w0qY8ymSZPfmIAVbbcFjJ3XceFbBhn07cAau7mU+Dg37UNUTf5Ggh8knnxh4hOzkOiN0rBxs7z9fotwXVv8Jw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@automapper/core': 8.7.7 + dependencies: + '@automapper/core': 8.7.7 + dev: false + /@automapper/types@6.3.1: resolution: {integrity: sha512-f0Y/Q4dVfKH6SQyzS5bPk+hZ4pSxzVNAOPEHdEAUe3gAc6ou43DCBYERuOaVSZ3zbWJbVt1KgWVNLP1dj9OIeg==} engines: {node: '>=10.0.0'} @@ -374,6 +395,19 @@ packages: '@jridgewell/resolve-uri': 3.1.1 '@jridgewell/sourcemap-codec': 1.4.15 + /@knaadh/nestjs-drizzle-pg@1.0.0(@nestjs/common@9.0.8)(drizzle-orm@0.29.3)(pg@8.11.3): + resolution: {integrity: sha512-21Ywzn/5y0QQz3cx1Nc2ZKfzW7LAjWCjATymkT148YECc+BIwpwvWvpejnjPOjFcyM0XfNT2axvRb0913GVfWg==} + peerDependencies: + '@nestjs/common': '>=9.0.0' + drizzle-orm: '>=0.28.6' + pg: '>=8' + dependencies: + '@nestjs/common': 9.0.8(class-transformer@0.5.1)(class-validator@0.14.0)(reflect-metadata@0.1.13)(rxjs@7.8.1) + drizzle-orm: 0.29.3(@types/pg@8.10.9)(pg@8.11.3) + pg: 8.11.3 + tslib: 2.6.2 + dev: false + /@mapbox/node-pre-gyp@1.0.11: resolution: {integrity: sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==} hasBin: true @@ -799,6 +833,13 @@ packages: '@types/express': 4.17.21 dev: true + /@types/pg@8.10.9: + resolution: {integrity: sha512-UksbANNE/f8w0wOMxVKKIrLCbEMV+oM1uKejmwXr39olg4xqcfBDbXxObJAt6XxHbDa4XTKOlUEcEltXDX+XLQ==} + dependencies: + '@types/node': 20.10.5 + pg-protocol: 1.6.0 + pg-types: 4.0.1 + /@types/qs@6.9.10: resolution: {integrity: sha512-3Gnx08Ns1sEoCrWssEgTSJs/rsT2vhGP+Ja9cnnk9k4ALxinORlQneLXFeFKOTJMOeZUFD1s7w+w2AphTpvzZw==} dev: true @@ -1859,6 +1900,81 @@ packages: engines: {node: '>=12'} dev: false + /drizzle-orm@0.29.3(@types/pg@8.10.9)(pg@8.11.3): + resolution: {integrity: sha512-uSE027csliGSGYD0pqtM+SAQATMREb3eSM/U8s6r+Y0RFwTKwftnwwSkqx3oS65UBgqDOM0gMTl5UGNpt6lW0A==} + peerDependencies: + '@aws-sdk/client-rds-data': '>=3' + '@cloudflare/workers-types': '>=3' + '@libsql/client': '*' + '@neondatabase/serverless': '>=0.1' + '@opentelemetry/api': ^1.4.1 + '@planetscale/database': '>=1' + '@types/better-sqlite3': '*' + '@types/pg': '*' + '@types/react': '>=18' + '@types/sql.js': '*' + '@vercel/postgres': '*' + better-sqlite3: '>=7' + bun-types: '*' + expo-sqlite: '>=13.2.0' + knex: '*' + kysely: '*' + mysql2: '>=2' + pg: '>=8' + postgres: '>=3' + react: '>=18' + sql.js: '>=1' + sqlite3: '>=5' + peerDependenciesMeta: + '@aws-sdk/client-rds-data': + optional: true + '@cloudflare/workers-types': + optional: true + '@libsql/client': + optional: true + '@neondatabase/serverless': + optional: true + '@opentelemetry/api': + optional: true + '@planetscale/database': + optional: true + '@types/better-sqlite3': + optional: true + '@types/pg': + optional: true + '@types/react': + optional: true + '@types/sql.js': + optional: true + '@vercel/postgres': + optional: true + better-sqlite3: + optional: true + bun-types: + optional: true + expo-sqlite: + optional: true + knex: + optional: true + kysely: + optional: true + mysql2: + optional: true + pg: + optional: true + postgres: + optional: true + react: + optional: true + sql.js: + optional: true + sqlite3: + optional: true + dependencies: + '@types/pg': 8.10.9 + pg: 8.11.3 + dev: false + /ecdsa-sig-formatter@1.0.11: resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} dependencies: @@ -3189,6 +3305,9 @@ packages: resolution: {integrity: sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==} dev: false + /obuf@1.1.2: + resolution: {integrity: sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==} + /on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -3400,7 +3519,10 @@ packages: /pg-int8@1.0.1: resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} engines: {node: '>=4.0.0'} - dev: false + + /pg-numeric@1.0.2: + resolution: {integrity: sha512-BM/Thnrw5jm2kKLE5uJkXqqExRUY/toLHda65XgFTBTFYZyopbKjBe29Ii3RbkvlsMoFwD+tHeGaCjjv0gHlyw==} + engines: {node: '>=4'} /pg-pool@3.6.1(pg@8.11.3): resolution: {integrity: sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==} @@ -3412,7 +3534,6 @@ packages: /pg-protocol@1.6.0: resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==} - dev: false /pg-types@2.2.0: resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} @@ -3425,6 +3546,18 @@ packages: postgres-interval: 1.2.0 dev: false + /pg-types@4.0.1: + resolution: {integrity: sha512-hRCSDuLII9/LE3smys1hRHcu5QGcLs9ggT7I/TCs0IE+2Eesxi9+9RWAAwZ0yaGjxoWICF/YHLOEjydGujoJ+g==} + engines: {node: '>=10'} + dependencies: + pg-int8: 1.0.1 + pg-numeric: 1.0.2 + postgres-array: 3.0.2 + postgres-bytea: 3.0.0 + postgres-date: 2.0.1 + postgres-interval: 3.0.0 + postgres-range: 1.1.3 + /pg@8.11.3: resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==} engines: {node: '>= 8.0.0'} @@ -3470,16 +3603,30 @@ packages: engines: {node: '>=4'} dev: false + /postgres-array@3.0.2: + resolution: {integrity: sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==} + engines: {node: '>=12'} + /postgres-bytea@1.0.0: resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} engines: {node: '>=0.10.0'} dev: false + /postgres-bytea@3.0.0: + resolution: {integrity: sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==} + engines: {node: '>= 6'} + dependencies: + obuf: 1.1.2 + /postgres-date@1.0.7: resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} engines: {node: '>=0.10.0'} dev: false + /postgres-date@2.0.1: + resolution: {integrity: sha512-YtMKdsDt5Ojv1wQRvUhnyDJNSr2dGIC96mQVKz7xufp07nfuFONzdaowrMHjlAzY6GDLd4f+LUHHAAM1h4MdUw==} + engines: {node: '>=12'} + /postgres-interval@1.2.0: resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} engines: {node: '>=0.10.0'} @@ -3487,6 +3634,13 @@ packages: xtend: 4.0.2 dev: false + /postgres-interval@3.0.0: + resolution: {integrity: sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==} + engines: {node: '>=12'} + + /postgres-range@1.1.3: + resolution: {integrity: sha512-VdlZoocy5lCP0c/t66xAfclglEapXPCIVhqqJRncYpvbCgImF0w67aPKfbqUMr72tO2k5q0TdTZwCLjPTI6C9g==} + /prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} diff --git a/src/core/services/base.service.ts b/src/core/services/base.service.ts new file mode 100644 index 0000000..a37e7a2 --- /dev/null +++ b/src/core/services/base.service.ts @@ -0,0 +1,94 @@ +import { eq, ExtractTablesWithRelations } from 'drizzle-orm'; +import { PgInsertValue, PgTransaction, PgUpdateSetSource } from 'drizzle-orm/pg-core'; +import { PostgresJsDatabase, PostgresJsQueryResultHKT } from 'drizzle-orm/postgres-js'; +import { RemovePropertiesWithNever } from 'src/infra/postgres/other/types'; +import * as tables from 'src/infra/postgres/tables'; + +export class BaseService< + TableName extends keyof RemovePropertiesWithNever<{ + [K in keyof ExtractTablesWithRelations]: + 'id' extends keyof ExtractTablesWithRelations[K]['columns'] + ? K + : never + }> +> { + public constructor( + private readonly tableName: TableName, + protected readonly drizzle: PostgresJsDatabase, + ) {} + + public async findOne( + config?: Parameters['query'][TableName]['findFirst']>[0], + ) { + return this.drizzle.query[this.tableName] + .findFirst(config) + .then((entity) => entity ?? null); + } + + public async exists( + config?: Parameters['query'][TableName]['findFirst']>[0], + ): Promise { + return this.findOne(config) + .then((entity) => Boolean(entity)); + } + + public async findMany( + config?: Parameters['query'][TableName]['findMany']>[0], + ) { + return this.drizzle.query[this.tableName] + .findMany(config); + } + + public async findManyWithPagination( + paginationOptions: { page: number, limit: number }, + config?: Parameters['query'][TableName]['findMany']>[0], + ) { + // TODO: check for boundaries + const { limit, offset } = { + limit: paginationOptions.limit, + offset: (paginationOptions.page - 1) * paginationOptions.limit, + }; + + return this.drizzle.query[this.tableName] + .findMany({ + ...config, + limit, + offset, + }); + } + + public async createOne( + values: PgInsertValue, + tx?: PgTransaction>, + ): Promise { + return (tx ?? this.drizzle) + .insert(tables[this.tableName]) + .values(values) + .returning() + .then(([entity]) => entity!); + } + + public async updateOne( + entity: typeof tables[TableName]['$inferSelect'], + values: PgUpdateSetSource, + tx?: PgTransaction>, + ): Promise { + return (tx ?? this.drizzle) + .update(tables[this.tableName]) + .set(values) + .where(eq(tables[this.tableName].id, entity.id)) + .returning() + .then(([entity]) => entity!); + } + + public async deleteOne( + entity: typeof tables[TableName]['$inferSelect'], + tx?: PgTransaction>, + ): Promise { + return (tx ?? this.drizzle) + .delete(tables[this.tableName]) + .where(eq(tables[this.tableName].id, entity.id)) + .returning() + .then(([entity]) => entity!); + } +} diff --git a/src/infra/postgres/other/base-columns.ts b/src/infra/postgres/other/base-columns.ts new file mode 100644 index 0000000..050ba93 --- /dev/null +++ b/src/infra/postgres/other/base-columns.ts @@ -0,0 +1,28 @@ +import { uuid, timestamp } from 'drizzle-orm/pg-core'; +import { UUIDv4 } from 'src/common/types'; + +export const baseIdColumn = { + id: uuid('id') + .$type() + .notNull() + .primaryKey() + .defaultRandom(), +}; + +export const baseCreatedAtColumn = { + createdAt: timestamp('created_at', { withTimezone: true }) + .notNull() + .defaultNow(), +}; + +export const baseUpdatedAtColumn = { + updatedAt: timestamp('updated_at', { withTimezone: true }) + .notNull() + .defaultNow(), +}; + +export const baseColumns = { + ...baseIdColumn, + ...baseCreatedAtColumn, + ...baseUpdatedAtColumn, +} diff --git a/src/infra/postgres/postgres.module.ts b/src/infra/postgres/postgres.module.ts index dad368c..91c58c7 100644 --- a/src/infra/postgres/postgres.module.ts +++ b/src/infra/postgres/postgres.module.ts @@ -1,10 +1,16 @@ -import { DynamicModule, Module } from '@nestjs/common'; +import { DynamicModule, Inject, Module } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { TypeOrmModule } from '@nestjs/typeorm'; import { EnvVariables } from '../config/validation'; import { TypeOrmConfigService } from '../config/typeorm'; import { ConfigModule } from "@nestjs/config"; import { validate } from '../config/validation'; +import { DrizzlePGModule } from '@knaadh/nestjs-drizzle-pg'; +import * as schema from './tables'; + +// TODO: move to separate file +const DRIZZLE_DB_TAG = 'DRIZZLE_DB_TAG'; +export const InjectDrizzle = () => Inject(DRIZZLE_DB_TAG); @Module({ imports: [ @@ -17,6 +23,25 @@ import { validate } from '../config/validation'; inject: [ConfigService], useClass: TypeOrmConfigService, }), + DrizzlePGModule.registerAsync({ + tag: DRIZZLE_DB_TAG, + inject: [ConfigService], + useFactory: (configService: ConfigService) => ({ + pg: { + connection: 'client', + config: { + user: configService.getOrThrow('POSTGRES_USER'), + password: configService.getOrThrow('POSTGRES_PASSWORD'), + host: configService.getOrThrow('POSTGRES_HOST'), + database: configService.getOrThrow('POSTGRES_DB'), + port: configService.getOrThrow('POSTGRES_PORT') + } + }, + config: { + schema, + }, + }) + }) ], }) export class PostgresModule { diff --git a/src/infra/postgres/tables/index.ts b/src/infra/postgres/tables/index.ts new file mode 100644 index 0000000..e30537f --- /dev/null +++ b/src/infra/postgres/tables/index.ts @@ -0,0 +1,10 @@ +export * from './opened-packs.table'; +export * from './pack-pokemons.table'; +export * from './packs.table'; +export * from './pokemons.table'; +export * from './quick-sold-user-items.table'; +export * from './trade-receiver-items.table'; +export * from './trade-sender-items.table'; +export * from './trades.table'; +export * from './user-items.table'; +export * from './users.table'; diff --git a/src/infra/postgres/tables/opened-packs.table.ts b/src/infra/postgres/tables/opened-packs.table.ts new file mode 100644 index 0000000..2f93db5 --- /dev/null +++ b/src/infra/postgres/tables/opened-packs.table.ts @@ -0,0 +1,33 @@ +import { uuid, pgTable, integer, timestamp } from 'drizzle-orm/pg-core'; +import { baseIdColumn } from '../other/base-columns'; +import { users } from './users.table'; +import { packs } from './packs.table'; +import { pokemons } from './pokemons.table'; +import { UUIDv4 } from 'src/common/types'; +import { relations } from 'drizzle-orm'; + +export const openedPacks = pgTable('opened_packs', { + ...baseIdColumn, + openedAt: timestamp('opened_at', { withTimezone: true }) + .notNull() + .defaultNow(), + userId: uuid('user_id') + .$type() + .notNull() + .references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + packId: uuid('pack_id') + .$type() + .notNull() + .references(() => packs.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + pokemonId: integer('pokemon_id') + .notNull() + .references(() => pokemons.id, { onDelete: 'cascade', onUpdate: 'cascade' }), +}) + +export const openedPacksRelations = relations(openedPacks, ({ one }) => ({ + user: one(users), + pack: one(packs), + pokemon: one(pokemons), +})) + +export type OpenedPackEntity = typeof openedPacks.$inferSelect; diff --git a/src/infra/postgres/tables/pack-pokemons.table.ts b/src/infra/postgres/tables/pack-pokemons.table.ts new file mode 100644 index 0000000..777222d --- /dev/null +++ b/src/infra/postgres/tables/pack-pokemons.table.ts @@ -0,0 +1,26 @@ +import { relations } from 'drizzle-orm'; +import { uuid, integer, pgTable, index, primaryKey } from 'drizzle-orm/pg-core'; +import { UUIDv4 } from 'src/common/types'; +import { packs } from './packs.table'; +import { pokemons } from './pokemons.table'; + +export const packPokemons = pgTable('pack_pokemons', { + packId: uuid('pack_id') + .$type() + .notNull() + .references(() => packs.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + pokemonId: integer('pokemon_id') + .notNull() + .references(() => pokemons.id, { onDelete: 'cascade', onUpdate: 'cascade' }), +}, (table) => ({ + primaryKey: primaryKey({ columns: [table.packId, table.pokemonId] }), + packIdIdx: index().on(table.packId), + pokemonIdIdx: index().on(table.pokemonId), +})); + +export const packPokemonsRelations = relations(packPokemons, ({ one }) => ({ + pack: one(packs), + pokemon: one(pokemons), +})); + +// export type PackPokemonEntity = typeof packPokemons.$inferSelect; diff --git a/src/infra/postgres/tables/packs.table.ts b/src/infra/postgres/tables/packs.table.ts new file mode 100644 index 0000000..e275961 --- /dev/null +++ b/src/infra/postgres/tables/packs.table.ts @@ -0,0 +1,22 @@ +import { relations } from 'drizzle-orm'; +import { integer, pgTable, text } from 'drizzle-orm/pg-core'; +import { baseColumns } from '../other/base-columns'; +import { pokemons } from './pokemons.table'; + +export const packs = pgTable('packs', { + ...baseColumns, + name: text('name') + .notNull(), + description: text('description') + .notNull(), + price: integer('price') + .notNull(), + image: text('image') + .notNull(), +}) + +export const packsRelations = relations(packs, ({ many }) => ({ + pokemons: many(pokemons), +})) + +export type PackEntity = typeof packs.$inferSelect; diff --git a/src/infra/postgres/tables/pokemons.table.ts b/src/infra/postgres/tables/pokemons.table.ts new file mode 100644 index 0000000..3fbdd00 --- /dev/null +++ b/src/infra/postgres/tables/pokemons.table.ts @@ -0,0 +1,19 @@ +import { pgTable, text, integer } from 'drizzle-orm/pg-core'; + +export const pokemons = pgTable('pokemons', { + id: integer('id') + .notNull() + .primaryKey(), + name: text('name') + .notNull(), + worth: integer('worth') + .notNull(), + height: integer('height') + .notNull(), + weight: integer('weight') + .notNull(), + image: text('image') + .notNull(), +}) + +export type PokemonEntity = typeof pokemons.$inferSelect; diff --git a/src/infra/postgres/tables/quick-sold-user-items.table.ts b/src/infra/postgres/tables/quick-sold-user-items.table.ts new file mode 100644 index 0000000..a275b76 --- /dev/null +++ b/src/infra/postgres/tables/quick-sold-user-items.table.ts @@ -0,0 +1,25 @@ +import { relations } from 'drizzle-orm'; +import { uuid, pgTable, timestamp } from 'drizzle-orm/pg-core'; +import { UUIDv4 } from 'src/common/types'; +import { pokemons } from './pokemons.table'; +import { userItemsColumns } from './user-items.table'; +import { users } from './users.table'; + +export const quickSoldUserItems = pgTable('quick_sold_user_items', { + ...userItemsColumns, + // NOTE: Overriding id column without default value + id: uuid('id') + .$type() + .notNull() + .primaryKey(), + soldAt: timestamp('sold_at', { withTimezone: true }) + .notNull() + .defaultNow(), +}) + +export const quickSoldUserItemsRelations = relations(quickSoldUserItems, ({ one }) => ({ + user: one(users), + pokemon: one(pokemons), +})); + +export type QuickSoldUserItemEntity = typeof quickSoldUserItems.$inferSelect; diff --git a/src/infra/postgres/tables/trade-receiver-items.table.ts b/src/infra/postgres/tables/trade-receiver-items.table.ts new file mode 100644 index 0000000..f588101 --- /dev/null +++ b/src/infra/postgres/tables/trade-receiver-items.table.ts @@ -0,0 +1,20 @@ +import { uuid, pgTable, index, primaryKey } from 'drizzle-orm/pg-core'; +import { UUIDv4 } from 'src/common/types'; +import { trades } from './trades.table'; + +export const tradeReceiverItems = pgTable('trade_receiver_items', { + tradeId: uuid('trade_id') + .$type() + .notNull() + .references(() => trades.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + userItemId: uuid('user_item_id') + .$type() + .notNull() + .references(() => trades.id, { onDelete: 'cascade', onUpdate: 'cascade' }), +}, (table) => ({ + primaryKey: primaryKey({ columns: [table.tradeId, table.userItemId] }), + tradeIdIdx: index().on(table.tradeId), + userItemIdIdx: index().on(table.userItemId), +})); + +// export type TradeReceiverItemEntity = typeof tradeReceiverItems.$inferSelect; diff --git a/src/infra/postgres/tables/trade-sender-items.table.ts b/src/infra/postgres/tables/trade-sender-items.table.ts new file mode 100644 index 0000000..d6aa643 --- /dev/null +++ b/src/infra/postgres/tables/trade-sender-items.table.ts @@ -0,0 +1,20 @@ +import { uuid, pgTable, index, primaryKey } from 'drizzle-orm/pg-core'; +import { UUIDv4 } from 'src/common/types'; +import { trades } from './trades.table'; + +export const tradeSenderItems = pgTable('trade_sender_items', { + tradeId: uuid('trade_id') + .$type() + .notNull() + .references(() => trades.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + userItemId: uuid('user_item_id') + .$type() + .notNull() + .references(() => trades.id, { onDelete: 'cascade', onUpdate: 'cascade' }), +}, (table) => ({ + primaryKey: primaryKey({ columns: [table.tradeId, table.userItemId] }), + tradeIdIdx: index().on(table.tradeId), + userItemIdIdx: index().on(table.userItemId), +})); + +// export type TradeSenderItemEntity = typeof tradeSenderItems.$inferSelect; diff --git a/src/infra/postgres/tables/trades.table.ts b/src/infra/postgres/tables/trades.table.ts new file mode 100644 index 0000000..7dd89f7 --- /dev/null +++ b/src/infra/postgres/tables/trades.table.ts @@ -0,0 +1,61 @@ +import { relations } from 'drizzle-orm'; +import { uuid, pgTable, timestamp, index, pgEnum } from 'drizzle-orm/pg-core'; +import { UUIDv4 } from 'src/common/types'; +import { baseColumns } from '../other/base-columns'; +import { tradeReceiverItems } from './trade-receiver-items.table'; +import { tradeSenderItems } from './trade-sender-items.table'; +import { users } from './users.table'; + +const tradeStatusEnum = pgEnum('trade_status', [ + 'PENDING', + 'CANCELLED', + 'ACCEPTED', + 'REJECTED', +]); + +export const trades = pgTable('trades', { + ...baseColumns, + status: tradeStatusEnum('status') + .notNull(), + senderId: uuid('sender_id') + .$type() + .notNull() + .references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + receiverId: uuid('receiver_id') + .$type() + .notNull() + .references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + cancelledAt: timestamp('accepted_at', { withTimezone: true }), + acceptedAt: timestamp('accepted_at', { withTimezone: true }), + rejectedAt: timestamp('rejected_at', { withTimezone: true }), +}, (table) => ({ + statusIdx: index().on(table.status), +})); + +export const tradesRelations = relations(trades, ({ one, many }) => ({ + sender: one(users), + senderItems: many(tradeSenderItems), + receiver: one(users), + receiverItems: many(tradeReceiverItems), +})) + +export type TradeStatus = typeof tradeStatusEnum.enumValues[number]; + +export type PendingTradeEntity = Omit & { + status: typeof tradeStatusEnum.enumValues[0]; +} +export type CancelledTradeEntity = Omit & { + status: typeof tradeStatusEnum.enumValues[1]; +} +export type AcceptedTradeEntity = Omit & { + status: typeof tradeStatusEnum.enumValues[2]; +} +export type RejectedTradeEntity = Omit & { + status: typeof tradeStatusEnum.enumValues[3]; +} + +export type TradeEntity = + | PendingTradeEntity + | CancelledTradeEntity + | AcceptedTradeEntity + | RejectedTradeEntity diff --git a/src/infra/postgres/tables/user-items.table.ts b/src/infra/postgres/tables/user-items.table.ts new file mode 100644 index 0000000..a9862ba --- /dev/null +++ b/src/infra/postgres/tables/user-items.table.ts @@ -0,0 +1,29 @@ +import { uuid, integer, pgTable, timestamp } from 'drizzle-orm/pg-core'; +import { baseIdColumn } from '../other/base-columns'; +import { users } from './users.table'; +import { pokemons } from './pokemons.table'; +import { UUIDv4 } from 'src/common/types'; +import { relations } from 'drizzle-orm'; + +export const userItemsColumns = { + ...baseIdColumn, + receivedAt: timestamp('received_at', { withTimezone: true }) + .notNull() + .defaultNow(), + userId: uuid('user_id') + .$type() + .notNull() + .references(() => users.id, { onDelete: 'cascade', onUpdate: 'cascade' }), + pokemonId: integer('pokemon_id') + .notNull() + .references(() => pokemons.id, { onDelete: 'cascade', onUpdate: 'cascade' }), +} + +export const userItems = pgTable('user_items', userItemsColumns); + +export const userItemsRelations = relations(userItems, ({ one }) => ({ + user: one(users), + pokemon: one(pokemons), +})); + +export type UserItemEntity = typeof userItems.$inferSelect; diff --git a/src/infra/postgres/tables/users.table.ts b/src/infra/postgres/tables/users.table.ts new file mode 100644 index 0000000..188cb4f --- /dev/null +++ b/src/infra/postgres/tables/users.table.ts @@ -0,0 +1,26 @@ +import { relations } from 'drizzle-orm'; +import { pgTable, text, integer } from 'drizzle-orm/pg-core'; +import { baseColumns } from '../other/base-columns'; +import { openedPacks } from './opened-packs.table'; +import { quickSoldUserItems } from './quick-sold-user-items.table'; +import { userItems } from './user-items.table'; + +export const users = pgTable('users', { + ...baseColumns, + name: text('name') + .notNull() + .unique(), + hashedPassword: text('hashed_password') + .notNull(), + balance: integer('balance') + .notNull() + .default(0), +}); + +export const usersRelations = relations(users, ({ many }) => ({ + items: many(userItems), + openedPacks: many(openedPacks), + quickSoldItems: many(quickSoldUserItems), +})) + +export type UserEntity = typeof users.$inferSelect;