From d6148a48337ba32280264aa0a662b60e40f73a06 Mon Sep 17 00:00:00 2001 From: Daria Carlotta Maino Date: Tue, 15 Apr 2025 17:31:15 +0200 Subject: [PATCH 1/3] chore: bump drizzle to 0.42.0 and reproduce enum issue --- src/app.mock.spec.ts | 1 + src/db/migrations/0001_add_user_role.sql | 2 + src/db/migrations/meta/0001_snapshot.json | 208 ++++++++++++++++++++++ src/db/migrations/meta/_journal.json | 7 + src/db/schema/user.ts | 3 + src/modules/users/services/UserService.ts | 6 + test/fixtures/testUsers.ts | 1 + 7 files changed, 228 insertions(+) create mode 100644 src/db/migrations/0001_add_user_role.sql create mode 100644 src/db/migrations/meta/0001_snapshot.json diff --git a/src/app.mock.spec.ts b/src/app.mock.spec.ts index b5a785d9..3045e5a7 100644 --- a/src/app.mock.spec.ts +++ b/src/app.mock.spec.ts @@ -18,6 +18,7 @@ class FakeUserService extends UserService { age: null, email: 'dummy', name: 'dummy', + role: 'Reader', }) } } diff --git a/src/db/migrations/0001_add_user_role.sql b/src/db/migrations/0001_add_user_role.sql new file mode 100644 index 00000000..422d02b3 --- /dev/null +++ b/src/db/migrations/0001_add_user_role.sql @@ -0,0 +1,2 @@ +CREATE TYPE "user"."user_role" AS ENUM('Admin', 'Writer', 'Reader');--> statement-breakpoint +ALTER TABLE "user"."user" ADD COLUMN "role" "user"."user_role" NOT NULL; \ No newline at end of file diff --git a/src/db/migrations/meta/0001_snapshot.json b/src/db/migrations/meta/0001_snapshot.json new file mode 100644 index 00000000..ba268152 --- /dev/null +++ b/src/db/migrations/meta/0001_snapshot.json @@ -0,0 +1,208 @@ +{ + "id": "3eb92b94-c6c5-4dcf-badf-2de64458718f", + "prevId": "4a8f3238-87cc-4b5f-ad9c-5071375bb015", + "version": "7", + "dialect": "postgresql", + "tables": { + "post.post": { + "name": "post", + "schema": "post", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "varchar(256)", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "published": { + "name": "published", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "author_id": { + "name": "author_id", + "type": "varchar", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "author_id_idx": { + "name": "author_id_idx", + "columns": [ + { + "expression": "author_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "user.profile": { + "name": "profile", + "schema": "user", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "bio": { + "name": "bio", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "age": { + "name": "age", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "profile_email_unique": { + "name": "profile_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + }, + "profile_user_id_unique": { + "name": "profile_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "user.user": { + "name": "user", + "schema": "user", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "age": { + "name": "age", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "user_role", + "typeSchema": "user", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "user.user_role": { + "name": "user_role", + "schema": "user", + "values": ["Admin", "Writer", "Reader"] + } + }, + "schemas": { + "post": "post", + "user": "user" + }, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index 6114b3e7..8b8ff49c 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1731603966543, "tag": "0000_init_user_post_tables", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1744730504852, + "tag": "0001_add_user_role", + "breakpoints": true } ] } diff --git a/src/db/schema/user.ts b/src/db/schema/user.ts index 789f1022..dadd69e2 100644 --- a/src/db/schema/user.ts +++ b/src/db/schema/user.ts @@ -5,11 +5,14 @@ import z from 'zod' export const userSchema = pgSchema('user') +export const userRoleEnum = userSchema.enum('user_role', ['Admin', 'Writer', 'Reader']) + export const user = userSchema.table('user', { id: uuid('id').primaryKey().defaultRandom().notNull(), age: integer('age'), email: varchar('email').unique().notNull(), name: varchar('name').notNull(), + role: userRoleEnum('role').notNull(), }) const selectUserSchema = createSelectSchema(user) diff --git a/src/modules/users/services/UserService.ts b/src/modules/users/services/UserService.ts index 3e220f7c..415d422e 100644 --- a/src/modules/users/services/UserService.ts +++ b/src/modules/users/services/UserService.ts @@ -80,4 +80,10 @@ export class UserService { requestContext.logger.debug({ id }, 'User does not exist') return null } + + async isUserAdmin(id: string): Promise { + const result = await this.userRepository.getUser(id) + + return result ? result.role === 'Admin' : false + } } diff --git a/test/fixtures/testUsers.ts b/test/fixtures/testUsers.ts index fedb8051..f835e3a8 100644 --- a/test/fixtures/testUsers.ts +++ b/test/fixtures/testUsers.ts @@ -3,4 +3,5 @@ import type { NewUser } from '../../src/db/schema/user.ts' export const TEST_USER_1: NewUser = { name: 'John', email: 'john@test.com', + role: 'Reader', } From cf4dadfaef684a85c41a19807e425a49020ab97c Mon Sep 17 00:00:00 2001 From: Daria Carlotta Maino Date: Thu, 15 May 2025 18:03:59 +0200 Subject: [PATCH 2/3] chore: general clean up --- src/db/schema/user.ts | 1 + src/modules/users/controllers/UserController.ts | 3 ++- src/modules/users/job-queue-processors/UserImportJob.ts | 1 + src/modules/users/schemas/userSchemas.ts | 4 ++++ src/modules/users/services/UserService.ts | 1 + 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/db/schema/user.ts b/src/db/schema/user.ts index dadd69e2..a60c2688 100644 --- a/src/db/schema/user.ts +++ b/src/db/schema/user.ts @@ -25,6 +25,7 @@ const updateUserSchema = createInsertSchema(user, { age: z.number().optional(), email: z.string().optional(), name: z.string().optional(), + role: z.enum(['Admin', 'Writer', 'Reader']).optional(), }).omit({ id: true }) export type UpdatedUser = z.infer diff --git a/src/modules/users/controllers/UserController.ts b/src/modules/users/controllers/UserController.ts index 99992f46..2d496db6 100644 --- a/src/modules/users/controllers/UserController.ts +++ b/src/modules/users/controllers/UserController.ts @@ -29,13 +29,14 @@ export class UserController extends AbstractController { - const { name, email, age } = req.body + const { name, email, age, role } = req.body const { userService } = req.diScope.cradle const createdUser = await userService.createUser({ name, email, age, + role, }) return reply.status(201).send({ diff --git a/src/modules/users/job-queue-processors/UserImportJob.ts b/src/modules/users/job-queue-processors/UserImportJob.ts index fde93ff2..c4374272 100644 --- a/src/modules/users/job-queue-processors/UserImportJob.ts +++ b/src/modules/users/job-queue-processors/UserImportJob.ts @@ -12,6 +12,7 @@ export const USER_IMPORT_JOB_PAYLOAD = BASE_JOB_PAYLOAD_SCHEMA.extend({ name: z.string(), age: z.number(), email: z.string(), + role: z.enum(['Admin', 'Writer', 'Reader']), }) type UserImportJobPayload = z.infer diff --git a/src/modules/users/schemas/userSchemas.ts b/src/modules/users/schemas/userSchemas.ts index 6e2e046d..8bb7273d 100644 --- a/src/modules/users/schemas/userSchemas.ts +++ b/src/modules/users/schemas/userSchemas.ts @@ -1,6 +1,8 @@ import { toNumberPreprocessor } from '@lokalise/zod-extras' import z from 'zod' +export const USER_ROLE_ENUM = z.enum(['Admin', 'Writer', 'Reader']) + export const USER_SCHEMA = z.object({ id: z.string(), name: z.string(), @@ -12,6 +14,7 @@ export const CREATE_USER_BODY_SCHEMA = z.object({ name: z.string(), age: z.optional(z.nullable(z.preprocess(toNumberPreprocessor, z.number()))), email: z.string().email(), + role: USER_ROLE_ENUM, }) export const CREATE_USER_RESPONSE_BODY_SCHEMA = z.object({ @@ -21,6 +24,7 @@ export const CREATE_USER_RESPONSE_BODY_SCHEMA = z.object({ export const UPDATE_USER_BODY_SCHEMA = z.object({ name: z.optional(z.string()), email: z.optional(z.string().email()), + role: z.optional(USER_ROLE_ENUM), }) export const GET_USER_PARAMS_SCHEMA = z.object({ diff --git a/src/modules/users/services/UserService.ts b/src/modules/users/services/UserService.ts index 415d422e..74e30958 100644 --- a/src/modules/users/services/UserService.ts +++ b/src/modules/users/services/UserService.ts @@ -30,6 +30,7 @@ export class UserService { name: user.name ?? null, age: user.age ?? null, email: user.email, + role: user.role, }) await this.userLoader.invalidateCacheFor(newUser.id.toString()) return newUser From 8f80892282cb1181c7679aa48dd9c172ca484c38 Mon Sep 17 00:00:00 2001 From: Daria Carlotta Maino Date: Thu, 15 May 2025 18:18:42 +0200 Subject: [PATCH 3/3] chore: fix tests --- src/db/schema/user.ts | 3 ++- .../users/consumers/PermissionsConsumer.spec.ts | 2 ++ .../users/controllers/UserController.e2e.spec.ts | 11 +++++++++-- .../users/job-queue-processors/UserImportJob.spec.ts | 2 ++ .../users/job-queue-processors/UserImportJob.ts | 3 ++- src/modules/users/schemas/userSchemas.ts | 1 + 6 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/db/schema/user.ts b/src/db/schema/user.ts index a60c2688..b0de7e4e 100644 --- a/src/db/schema/user.ts +++ b/src/db/schema/user.ts @@ -2,6 +2,7 @@ import { relations } from 'drizzle-orm' import { integer, pgSchema, uuid, varchar } from 'drizzle-orm/pg-core' import { createInsertSchema, createSelectSchema } from 'drizzle-zod' import z from 'zod' +import { USER_ROLE_ENUM } from '../../modules/users/schemas/userSchemas.js' export const userSchema = pgSchema('user') @@ -25,7 +26,7 @@ const updateUserSchema = createInsertSchema(user, { age: z.number().optional(), email: z.string().optional(), name: z.string().optional(), - role: z.enum(['Admin', 'Writer', 'Reader']).optional(), + role: USER_ROLE_ENUM.optional(), }).omit({ id: true }) export type UpdatedUser = z.infer diff --git a/src/modules/users/consumers/PermissionsConsumer.spec.ts b/src/modules/users/consumers/PermissionsConsumer.spec.ts index f754f731..0dc733c3 100644 --- a/src/modules/users/consumers/PermissionsConsumer.spec.ts +++ b/src/modules/users/consumers/PermissionsConsumer.spec.ts @@ -17,6 +17,7 @@ import { asSingletonClass } from 'opinionated-machine' import { user as userTable } from '../../../db/schema/user.ts' import type { PublisherManager } from '../../../infrastructure/CommonModule.ts' import { buildQueueMessage } from '../../../utils/queueUtils.ts' +import type { UserRole } from '../schemas/userSchemas.js' import { PermissionConsumer } from './PermissionConsumer.ts' import type { PermissionsMessages } from './permissionsMessageSchemas.ts' @@ -31,6 +32,7 @@ async function createUsers(drizzle: PostgresJsDatabase, userIdsToCreate: string[ id: userId, name: userId.toString(), email: `test${userId}@email.lt`, + role: 'Reader' as UserRole, } }), ) diff --git a/src/modules/users/controllers/UserController.e2e.spec.ts b/src/modules/users/controllers/UserController.e2e.spec.ts index e7e92767..013f0050 100644 --- a/src/modules/users/controllers/UserController.e2e.spec.ts +++ b/src/modules/users/controllers/UserController.e2e.spec.ts @@ -10,7 +10,11 @@ import type { UserRepository } from '../repositories/UserRepository.ts' import type { UserCreateDTO } from '../services/UserService.ts' import { UserController } from './UserController.ts' -const NEW_USER_FIXTURE = { name: 'dummy', email: 'email@test.com' } satisfies UserCreateDTO +const NEW_USER_FIXTURE = { + name: 'dummy', + email: 'email@test.com', + role: 'Reader', +} satisfies UserCreateDTO describe('UserController', () => { let app: AppInstance @@ -33,7 +37,7 @@ describe('UserController', () => { headers: { authorization: `Bearer ${token}`, }, - body: { name: 'dummy', email: 'test' }, + body: { name: 'dummy', email: 'test', role: 'Admin' }, }) expect(response.statusCode).toBe(400) @@ -81,6 +85,7 @@ describe('UserController', () => { email: 'email@test.com', id: expect.any(String), name: 'dummy', + role: 'Reader', }, }) }) @@ -150,6 +155,7 @@ describe('UserController', () => { const updateResponse = await injectPatch(app, UserController.contracts.updateUser, { body: { name: 'updated', + role: 'Admin', }, pathParams: { userId: id, @@ -166,6 +172,7 @@ describe('UserController', () => { age: null, id, name: 'updated', + role: 'Admin', }) }) }) diff --git a/src/modules/users/job-queue-processors/UserImportJob.spec.ts b/src/modules/users/job-queue-processors/UserImportJob.spec.ts index c86889de..f9632e5d 100644 --- a/src/modules/users/job-queue-processors/UserImportJob.spec.ts +++ b/src/modules/users/job-queue-processors/UserImportJob.spec.ts @@ -6,6 +6,7 @@ import { type TestContext, testContextFactory } from '../../../../test/TestConte import type { QueueManager } from '@lokalise/background-jobs-common' import { user as userTable } from '../../../db/schema/user.ts' import type { BullmqSupportedQueues } from '../../../infrastructure/CommonModule.ts' +import type { UserRole } from '../schemas/userSchemas.js' import { UserImportJob } from './UserImportJob.ts' describe('UserImportJob', () => { @@ -37,6 +38,7 @@ describe('UserImportJob', () => { name: 'name', age: 33, email: 'test@email.lt', + role: 'Admin' as UserRole, } const jobId = await bullmqQueueManager.schedule('UserImportJob', { diff --git a/src/modules/users/job-queue-processors/UserImportJob.ts b/src/modules/users/job-queue-processors/UserImportJob.ts index c4374272..6bf78a03 100644 --- a/src/modules/users/job-queue-processors/UserImportJob.ts +++ b/src/modules/users/job-queue-processors/UserImportJob.ts @@ -6,13 +6,14 @@ import z from 'zod' import type { Dependencies } from '../../../infrastructure/CommonModule.ts' import { SERVICE_NAME } from '../../../infrastructure/config.ts' import { AbstractEnqueuedJobProcessor } from '../../../infrastructure/jobs/AbstractEnqueuedJobProcessor.ts' +import { USER_ROLE_ENUM } from '../schemas/userSchemas.js' import type { UserService } from '../services/UserService.ts' export const USER_IMPORT_JOB_PAYLOAD = BASE_JOB_PAYLOAD_SCHEMA.extend({ name: z.string(), age: z.number(), email: z.string(), - role: z.enum(['Admin', 'Writer', 'Reader']), + role: USER_ROLE_ENUM, }) type UserImportJobPayload = z.infer diff --git a/src/modules/users/schemas/userSchemas.ts b/src/modules/users/schemas/userSchemas.ts index 8bb7273d..ce5dfe01 100644 --- a/src/modules/users/schemas/userSchemas.ts +++ b/src/modules/users/schemas/userSchemas.ts @@ -2,6 +2,7 @@ import { toNumberPreprocessor } from '@lokalise/zod-extras' import z from 'zod' export const USER_ROLE_ENUM = z.enum(['Admin', 'Writer', 'Reader']) +export type UserRole = z.infer export const USER_SCHEMA = z.object({ id: z.string(),