Переглянути джерело

✨ feat(infrastructure): create shared-utils package

- 创建 package.json 配置
- 迁移通用工具函数(jwt.util.ts、errorHandler.ts、parseWithAwait.ts等)
- 迁移数据库连接配置(data-source.ts)
- 配置 TypeScript 编译选项(包含 "composite": true)
- 编写单元测试(放在 tests/unit/)

📝 docs(infrastructure): update package split documentation

- 更新 shared-utils package 任务状态为已完成
- 添加 Agent Model Used 信息(Claude Sonnet 4.5)
- 添加 Debug Log References 部分
- 添加 Completion Notes List 部分
- 添加 File List 部分

📦 build(shared-utils): add package dependencies and configuration

- 添加 package.json 配置文件
- 添加 tsconfig.json 配置(包含 composite: true)
- 添加 vitest.config.ts 测试配置
- 添加必要依赖(reflect-metadata, @hono/zod-openapi等)
- 保持依赖版本与 packages/server 一致

♻️ refactor(data-source): migrate and refactor database connection

- 创建 createDataSource 函数支持环境变量配置
- 实现 initializeDataSource 初始化默认数据源
- 添加测试环境特殊配置(自动同步和删除schema)
- 编写单元测试验证不同环境配置

✅ test(shared-utils): add unit tests for utility functions

- 添加 data-source.test.ts 验证数据源配置
- 添加 jwt.util.test.ts 验证JWT工具函数
- 添加 parseWithAwait.test.ts 验证异步解析功能
- 确保所有测试通过并覆盖主要功能点

🌐 i18n(types): add JWTPayload type definition

- 在 shared-types 中添加 JWTPayload 接口定义
- 包含 id, username, roles 和 openid 字段定义
yourname 3 тижнів тому
батько
коміт
31cd0763e0

+ 39 - 9
docs/stories/005.001.infrastructure-packages-split.md

@@ -32,12 +32,12 @@ Draft
   - [x] 配置 TypeScript 编译选项(包含 `"composite": true`)
   - [x] 编写基础测试(放在 tests/unit/)
 
-- [ ] 创建 shared-utils package (AC: 2)
-  - [ ] 创建 package.json 配置
-  - [ ] 迁移通用工具函数(jwt.util.ts、errorHandler.ts、parseWithAwait.ts等)
-  - [ ] 迁移数据库连接配置(data-source.ts)
-  - [ ] 配置 TypeScript 编译选项(包含 `"composite": true`)
-  - [ ] 编写单元测试(放在 tests/unit/)
+- [x] 创建 shared-utils package (AC: 2)
+  - [x] 创建 package.json 配置
+  - [x] 迁移通用工具函数(jwt.util.ts、errorHandler.ts、parseWithAwait.ts等)
+  - [x] 迁移数据库连接配置(data-source.ts)
+  - [x] 配置 TypeScript 编译选项(包含 `"composite": true`)
+  - [x] 编写单元测试(放在 tests/unit/)
 
 - [ ] 创建 shared-crud package (AC: 3)
   - [ ] 创建 package.json 配置
@@ -304,10 +304,40 @@ Draft
 *此部分由开发代理在实现过程中填写*
 
 ### Agent Model Used
-{{agent_model_name_version}}
+Claude Sonnet 4.5 (claude-sonnet-4-5-20250929)
 
 ### Debug Log References
+- 修复了数据源测试中的环境变量计算时机问题
+- 修复了 JWT 测试中的时间计算问题
+- 添加了缺失的依赖(reflect-metadata, @hono/zod-openapi)
 
 ### Completion Notes List
-
-### File List
+- ✅ shared-utils package 创建完成
+- ✅ 所有通用工具函数已迁移
+- ✅ 数据库连接配置已迁移并重构为通用函数
+- ✅ TypeScript 配置完成(包含 composite: true)
+- ✅ 单元测试编写完成并通过
+- ✅ 依赖版本与 packages/server 保持一致
+
+### File List
+**新增文件:**
+- `packages/shared-utils/package.json` - 包配置
+- `packages/shared-utils/tsconfig.json` - TypeScript 配置
+- `packages/shared-utils/vitest.config.ts` - 测试配置
+- `packages/shared-utils/src/index.ts` - 包入口
+- `packages/shared-utils/src/utils/jwt.util.ts` - JWT 工具函数
+- `packages/shared-utils/src/utils/errorHandler.ts` - 错误处理
+- `packages/shared-utils/src/utils/parseWithAwait.ts` - 异步解析工具
+- `packages/shared-utils/src/utils/logger.ts` - 日志工具
+- `packages/shared-utils/src/data-source.ts` - 数据库连接配置
+- `packages/shared-utils/tests/unit/jwt.util.test.ts` - JWT 测试
+- `packages/shared-utils/tests/unit/parseWithAwait.test.ts` - 异步解析测试
+- `packages/shared-utils/tests/unit/data-source.test.ts` - 数据源测试
+
+**修改文件:**
+- `packages/shared-types/src/index.ts` - 添加 JWTPayload 类型定义
+- `tsconfig.json` - 创建根目录 TypeScript 配置
+
+**依赖关系:**
+- shared-utils 依赖 shared-types
+- 所有外部依赖版本与 packages/server 完全一致

+ 8 - 0
packages/shared-types/src/index.ts

@@ -66,4 +66,12 @@ export interface AuthContextType<T> {
 export interface GlobalConfig {
   OSS_BASE_URL: string;
   APP_NAME: string;
+}
+
+// JWT Payload 类型
+export interface JWTPayload {
+  id: number;
+  username: string;
+  roles?: string[];
+  openid?: string;
 }

+ 42 - 0
packages/shared-utils/package.json

@@ -0,0 +1,42 @@
+{
+  "name": "@d8d/shared-utils",
+  "version": "1.0.0",
+  "type": "module",
+  "description": "D8D Shared Utility Functions and Database Configuration",
+  "main": "src/index.ts",
+  "types": "src/index.ts",
+  "exports": {
+    ".": {
+      "types": "./src/index.ts",
+      "import": "./src/index.ts",
+      "require": "./src/index.ts"
+    }
+  },
+  "scripts": {
+    "typecheck": "tsc --noEmit",
+    "test": "vitest",
+    "test:unit": "vitest run tests/unit"
+  },
+  "dependencies": {
+    "@d8d/shared-types": "workspace:*",
+    "jsonwebtoken": "^9.0.2",
+    "bcrypt": "^6.0.0",
+    "typeorm": "^0.3.20",
+    "pg": "^8.16.3",
+    "debug": "^4.4.3",
+    "reflect-metadata": "^0.2.2",
+    "@hono/zod-openapi": "1.0.2"
+  },
+  "devDependencies": {
+    "@types/bcrypt": "^6.0.0",
+    "@types/jsonwebtoken": "^9.0.7",
+    "@types/pg": "^8.11.10",
+    "@types/debug": "^4.1.12",
+    "hono": "^4.8.5",
+    "typescript": "^5.8.3",
+    "vitest": "^3.2.4"
+  },
+  "files": [
+    "src"
+  ]
+}

+ 54 - 0
packages/shared-utils/src/data-source.ts

@@ -0,0 +1,54 @@
+import "reflect-metadata"
+import { DataSource, EntitySchema } from "typeorm"
+import process from 'node:process'
+
+// 实体目标类型,可以是类、函数或实体模式
+type EntityTarget = Function | EntitySchema<any> | string
+
+/**
+ * 创建数据源配置
+ * @param entities 实体类数组
+ * @returns DataSource 实例
+ */
+export function createDataSource(entities: EntityTarget<any>[]): DataSource {
+  // 在测试环境下使用测试数据库配置
+  const isTestEnv = process.env.NODE_ENV === 'test';
+  const testDatabaseUrl = process.env.TEST_DATABASE_URL || 'postgresql://postgres:test_password@localhost:5432/test_d8dai';
+
+  return isTestEnv && testDatabaseUrl
+    ? new DataSource({
+        type: "postgres",
+        url: testDatabaseUrl,
+        entities,
+        migrations: [],
+        synchronize: true, // 测试环境总是同步schema
+        dropSchema: true,  // 测试环境每次重新创建schema
+        logging: false,    // 测试环境关闭日志
+      })
+    : new DataSource({
+        type: "postgres",
+        host: process.env.DB_HOST || "localhost",
+        port: parseInt(process.env.DB_PORT || "5432"),
+        username: process.env.DB_USERNAME || "postgres",
+        password: process.env.DB_PASSWORD || "",
+        database: process.env.DB_DATABASE || "postgres",
+        entities,
+        migrations: [],
+        synchronize: process.env.DB_SYNCHRONIZE !== "false",
+        logging: process.env.DB_LOGGING === "true",
+      });
+}
+
+/**
+ * 默认数据源实例(需要传入实体)
+ * 注意:这个实例需要在具体模块中传入实体类
+ */
+export let AppDataSource: DataSource;
+
+/**
+ * 初始化默认数据源
+ * @param entities 实体类数组
+ */
+export function initializeDataSource(entities: EntityTarget<any>[]): void {
+  AppDataSource = createDataSource(entities);
+}

+ 6 - 0
packages/shared-utils/src/index.ts

@@ -0,0 +1,6 @@
+// 导出所有工具函数和数据库配置
+export * from './utils/jwt.util';
+export * from './utils/errorHandler';
+export * from './utils/parseWithAwait';
+export * from './utils/logger';
+export * from './data-source';

+ 21 - 0
packages/shared-utils/src/utils/errorHandler.ts

@@ -0,0 +1,21 @@
+import { Context } from 'hono'
+import { z } from '@hono/zod-openapi'
+
+export const ErrorSchema = z.object({
+  code: z.number().openapi({
+    example: 400,
+  }),
+  message: z.string().openapi({
+    example: 'Bad Request',
+  }),
+})
+
+export const errorHandler = async (err: Error, c: Context) => {
+  return c.json(
+    {
+      code: 500,
+      message: err.message || 'Internal Server Error'
+    },
+    500
+  )
+}

+ 87 - 0
packages/shared-utils/src/utils/jwt.util.ts

@@ -0,0 +1,87 @@
+import jwt, { SignOptions } from 'jsonwebtoken';
+import { JWTPayload } from '@d8d/shared-types';
+import debug from 'debug';
+
+const logger = {
+  info: debug('backend:jwt:info'),
+  error: debug('backend:jwt:error')
+};
+
+const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
+const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '7d';
+
+export class JWTUtil {
+  /**
+   * 生成 JWT token
+   * @param user 用户实体
+   * @param additionalPayload 额外的 payload 数据
+   * @param expiresIn 过期时间
+   * @returns JWT token
+   */
+  static generateToken(user: { id: number; username: string; roles?: { name: string }[]; openid?: string }, additionalPayload: Partial<JWTPayload> = {}, expiresIn?: string): string {
+    if (!user.id || !user.username) {
+      throw new Error('用户ID和用户名不能为空');
+    }
+
+    const payload: JWTPayload = {
+      id: user.id,
+      username: user.username,
+      roles: user.roles?.map(role => role.name) || [],
+      openid: user.openid || undefined,
+      ...additionalPayload
+    };
+
+    try {
+      const options: SignOptions = {
+        expiresIn: expiresIn || JWT_EXPIRES_IN
+      };
+      return jwt.sign(payload, JWT_SECRET, options);
+    } catch (error) {
+      logger.error('生成JWT token失败:', error);
+      throw new Error('生成token失败');
+    }
+  }
+
+  /**
+   * 验证 JWT token
+   * @param token JWT token
+   * @returns 验证后的 payload
+   */
+  static verifyToken(token: string): JWTPayload {
+    try {
+      return jwt.verify(token, JWT_SECRET) as JWTPayload;
+    } catch (error) {
+      logger.error('验证JWT token失败:', error);
+      throw new Error('无效的token');
+    }
+  }
+
+  /**
+   * 解码 JWT token(不验证签名)
+   * @param token JWT token
+   * @returns 解码后的 payload
+   */
+  static decodeToken(token: string): JWTPayload | null {
+    try {
+      return jwt.decode(token) as JWTPayload;
+    } catch (error) {
+      logger.error('解码JWT token失败:', error);
+      return null;
+    }
+  }
+
+  /**
+   * 获取 token 的剩余有效期(秒)
+   * @param token JWT token
+   * @returns 剩余有效期(秒),如果 token 无效则返回 0
+   */
+  static getTokenRemainingTime(token: string): number {
+    try {
+      const decoded = jwt.verify(token, JWT_SECRET) as JWTPayload & { exp: number };
+      const currentTime = Math.floor(Date.now() / 1000);
+      return Math.max(0, decoded.exp - currentTime);
+    } catch (error) {
+      return 0;
+    }
+  }
+}

+ 8 - 0
packages/shared-utils/src/utils/logger.ts

@@ -0,0 +1,8 @@
+import debug from 'debug';
+
+export const logger = {
+  error: debug('backend:error'),
+  api: debug('backend:api'),
+  db: debug('backend:db'),
+  middleware: debug('backend:middleware'),
+};

+ 59 - 0
packages/shared-utils/src/utils/parseWithAwait.ts

@@ -0,0 +1,59 @@
+import { z } from '@hono/zod-openapi';
+
+export async function parseWithAwait<T>(schema: z.ZodSchema<T>, data: unknown): Promise<T> {
+    // 先尝试同步解析,捕获 Promise 错误
+    const syncResult = schema.safeParse(data);
+
+    if (!syncResult.success) {
+      // 提取 Promise 错误的路径信息
+      const promiseErrors = syncResult.error.issues.filter(issue =>
+        issue.code === 'invalid_type' &&
+        issue.message.includes('received Promise')
+      );
+
+      if (promiseErrors.length > 0) {
+        // 根据路径直接 await Promise
+        const processedData = await resolvePromisesByPath(data, promiseErrors);
+
+        // 重新解析处理后的数据
+        return schema.parse(processedData) as T;
+      }
+
+      throw syncResult.error;
+    }
+
+    return syncResult.data as T;
+  }
+
+  async function resolvePromisesByPath(data: any, promiseErrors: any[]): Promise<any> {
+    const clonedData = JSON.parse(JSON.stringify(data, (_, value) => {
+      // 保留 Promise 对象,不进行序列化
+      return typeof value?.then === 'function' ? value : value;
+    }));
+
+    // 根据错误路径逐个处理 Promise
+    for (const error of promiseErrors) {
+      const path = error.path;
+      const promiseValue = getValueByPath(data, path);
+
+      if (promiseValue && typeof promiseValue.then === 'function') {
+        const resolvedValue = await promiseValue;
+        setValueByPath(clonedData, path, resolvedValue);
+      }
+    }
+
+    return clonedData;
+  }
+
+  function getValueByPath(obj: any, path: (string | number)[]): any {
+    return path.reduce((current, key) => current?.[key], obj);
+  }
+
+  function setValueByPath(obj: any, path: (string | number)[], value: any): void {
+    const lastKey = path[path.length - 1];
+    const parentPath = path.slice(0, -1);
+    const parent = getValueByPath(obj, parentPath);
+    if (parent && lastKey !== undefined) {
+      parent[lastKey] = value;
+    }
+  }

+ 109 - 0
packages/shared-utils/tests/unit/data-source.test.ts

@@ -0,0 +1,109 @@
+import { describe, it, expect, beforeEach, afterEach } from 'vitest';
+import { createDataSource, initializeDataSource } from '../../src/data-source';
+
+// 模拟实体类
+class MockEntity {}
+
+describe('data-source', () => {
+  const originalEnv = process.env;
+
+  beforeEach(() => {
+    // 重置环境变量
+    process.env = { ...originalEnv };
+  });
+
+  afterEach(() => {
+    process.env = originalEnv;
+  });
+
+  describe('createDataSource', () => {
+    it('应该创建生产环境数据源', () => {
+      process.env.NODE_ENV = 'production';
+
+      const dataSource = createDataSource([MockEntity]);
+
+      expect(dataSource).toBeDefined();
+      expect(dataSource.options.type).toBe('postgres');
+      expect(dataSource.options.entities).toEqual([MockEntity]);
+      expect(dataSource.options.synchronize).toBe(true); // 默认值
+    });
+
+    it('应该创建测试环境数据源', () => {
+      process.env.NODE_ENV = 'test';
+      process.env.TEST_DATABASE_URL = 'postgresql://test:test@localhost:5432/test';
+
+      const dataSource = createDataSource([MockEntity]);
+
+      expect(dataSource).toBeDefined();
+      expect(dataSource.options.type).toBe('postgres');
+      expect(dataSource.options.url).toBe('postgresql://test:test@localhost:5432/test');
+      expect(dataSource.options.synchronize).toBe(true);
+      expect(dataSource.options.dropSchema).toBe(true);
+      expect(dataSource.options.logging).toBe(false);
+    });
+
+    it('应该使用环境变量配置', () => {
+      process.env.NODE_ENV = 'production';
+      // 确保没有设置 TEST_DATABASE_URL,这样会使用生产环境配置
+      delete process.env.TEST_DATABASE_URL;
+      process.env.DB_HOST = 'custom-host';
+      process.env.DB_PORT = '5433';
+      process.env.DB_USERNAME = 'custom-user';
+      process.env.DB_PASSWORD = 'custom-password';
+      process.env.DB_DATABASE = 'custom-db';
+      process.env.DB_SYNCHRONIZE = 'false';
+      process.env.DB_LOGGING = 'true';
+
+      const dataSource = createDataSource([MockEntity]);
+
+      expect(dataSource.options).toMatchObject({
+        host: 'custom-host',
+        port: 5433,
+        username: 'custom-user',
+        password: 'custom-password',
+        database: 'custom-db',
+        synchronize: false,
+        logging: true
+      });
+    });
+
+    it('应该使用默认值当环境变量未设置时', () => {
+      process.env.NODE_ENV = 'production';
+      // 确保没有设置 TEST_DATABASE_URL,这样会使用生产环境配置
+      delete process.env.TEST_DATABASE_URL;
+      delete process.env.DB_HOST;
+      delete process.env.DB_PORT;
+      delete process.env.DB_USERNAME;
+      delete process.env.DB_PASSWORD;
+      delete process.env.DB_DATABASE;
+      delete process.env.DB_SYNCHRONIZE;
+      delete process.env.DB_LOGGING;
+
+      const dataSource = createDataSource([MockEntity]);
+
+      expect(dataSource.options).toMatchObject({
+        host: 'localhost',
+        port: 5432,
+        username: 'postgres',
+        password: '',
+        database: 'postgres',
+        synchronize: true,
+        logging: false
+      });
+    });
+  });
+
+  describe('initializeDataSource', () => {
+    it('应该初始化默认数据源', () => {
+      process.env.NODE_ENV = 'production';
+
+      initializeDataSource([MockEntity]);
+
+      // 导入 AppDataSource 来验证它被正确设置
+      import('../../src/data-source').then(({ AppDataSource }) => {
+        expect(AppDataSource).toBeDefined();
+        expect(AppDataSource.options.entities).toEqual([MockEntity]);
+      });
+    });
+  });
+});

+ 109 - 0
packages/shared-utils/tests/unit/jwt.util.test.ts

@@ -0,0 +1,109 @@
+import { describe, it, expect, vi, beforeEach } from 'vitest';
+import { JWTUtil } from '../../src/utils/jwt.util';
+
+describe('JWTUtil', () => {
+  const mockUser = {
+    id: 1,
+    username: 'testuser',
+    roles: [{ name: 'admin' }, { name: 'user' }],
+    openid: 'test-openid'
+  };
+
+  beforeEach(() => {
+    // 重置环境变量
+    process.env.JWT_SECRET = 'test-secret-key';
+    process.env.JWT_EXPIRES_IN = '1h';
+  });
+
+  describe('generateToken', () => {
+    it('应该成功生成JWT token', () => {
+      const token = JWTUtil.generateToken(mockUser);
+
+      expect(token).toBeDefined();
+      expect(typeof token).toBe('string');
+      expect(token.split('.')).toHaveLength(3); // JWT token 应该有3部分
+    });
+
+    it('应该使用额外的payload数据', () => {
+      const additionalPayload = { customField: 'customValue' };
+      const token = JWTUtil.generateToken(mockUser, additionalPayload);
+
+      const decoded = JWTUtil.decodeToken(token);
+      expect(decoded).toMatchObject({
+        id: mockUser.id,
+        username: mockUser.username,
+        roles: ['admin', 'user'],
+        openid: mockUser.openid,
+        customField: 'customValue'
+      });
+    });
+
+    it('当用户缺少必要字段时应该抛出错误', () => {
+      const invalidUser = { id: 1 } as any; // 缺少 username
+
+      expect(() => {
+        JWTUtil.generateToken(invalidUser);
+      }).toThrow('用户ID和用户名不能为空');
+    });
+  });
+
+  describe('verifyToken', () => {
+    it('应该成功验证有效的token', () => {
+      const token = JWTUtil.generateToken(mockUser);
+      const payload = JWTUtil.verifyToken(token);
+
+      expect(payload).toMatchObject({
+        id: mockUser.id,
+        username: mockUser.username,
+        roles: ['admin', 'user'],
+        openid: mockUser.openid
+      });
+    });
+
+    it('当token无效时应该抛出错误', () => {
+      const invalidToken = 'invalid.token.here';
+
+      expect(() => {
+        JWTUtil.verifyToken(invalidToken);
+      }).toThrow('无效的token');
+    });
+  });
+
+  describe('decodeToken', () => {
+    it('应该成功解码token', () => {
+      const token = JWTUtil.generateToken(mockUser);
+      const payload = JWTUtil.decodeToken(token);
+
+      expect(payload).toMatchObject({
+        id: mockUser.id,
+        username: mockUser.username,
+        roles: ['admin', 'user'],
+        openid: mockUser.openid
+      });
+    });
+
+    it('当token格式错误时应该返回null', () => {
+      const invalidToken = 'invalid';
+      const payload = JWTUtil.decodeToken(invalidToken);
+
+      expect(payload).toBeNull();
+    });
+  });
+
+  describe('getTokenRemainingTime', () => {
+    it('应该返回有效的剩余时间', () => {
+      const token = JWTUtil.generateToken(mockUser, {}, '1h'); // 明确指定1小时过期
+      const remainingTime = JWTUtil.getTokenRemainingTime(token);
+
+      expect(remainingTime).toBeGreaterThan(0);
+      expect(remainingTime).toBeLessThanOrEqual(3600); // 1小时
+    });
+
+    it('当token无效时应该返回0', () => {
+      const invalidToken = 'invalid.token.here';
+      const remainingTime = JWTUtil.getTokenRemainingTime(invalidToken);
+
+      expect(remainingTime).toBe(0);
+    });
+  });
+});

+ 107 - 0
packages/shared-utils/tests/unit/parseWithAwait.test.ts

@@ -0,0 +1,107 @@
+import { describe, it, expect } from 'vitest';
+import { z } from '@hono/zod-openapi';
+import { parseWithAwait } from '../../src/utils/parseWithAwait';
+
+describe('parseWithAwait', () => {
+  const testSchema = z.object({
+    name: z.string(),
+    age: z.number(),
+    email: z.string().email()
+  });
+
+  it('应该成功解析同步数据', async () => {
+    const data = {
+      name: 'John Doe',
+      age: 30,
+      email: 'john@example.com'
+    };
+
+    const result = await parseWithAwait(testSchema, data);
+
+    expect(result).toEqual(data);
+  });
+
+  it('应该成功解析包含Promise的数据', async () => {
+    const data = {
+      name: Promise.resolve('John Doe'),
+      age: 30,
+      email: 'john@example.com'
+    };
+
+    const result = await parseWithAwait(testSchema, data);
+
+    expect(result).toEqual({
+      name: 'John Doe',
+      age: 30,
+      email: 'john@example.com'
+    });
+  });
+
+  it('应该处理嵌套对象中的Promise', async () => {
+    const nestedSchema = z.object({
+      user: z.object({
+        name: z.string(),
+        profile: z.object({
+          age: z.number()
+        })
+      })
+    });
+
+    const data = {
+      user: {
+        name: Promise.resolve('John Doe'),
+        profile: {
+          age: Promise.resolve(30)
+        }
+      }
+    };
+
+    const result = await parseWithAwait(nestedSchema, data);
+
+    expect(result).toEqual({
+      user: {
+        name: 'John Doe',
+        profile: {
+          age: 30
+        }
+      }
+    });
+  });
+
+  it('当数据不符合schema时应该抛出错误', async () => {
+    const invalidData = {
+      name: 'John Doe',
+      age: 'not-a-number', // 应该是数字
+      email: 'john@example.com'
+    };
+
+    await expect(parseWithAwait(testSchema, invalidData))
+      .rejects
+      .toThrow();
+  });
+
+  it('应该处理数组中的Promise', async () => {
+    const arraySchema = z.object({
+      items: z.array(z.object({
+        name: z.string(),
+        value: z.number()
+      }))
+    });
+
+    const data = {
+      items: [
+        { name: Promise.resolve('Item 1'), value: 10 },
+        { name: 'Item 2', value: Promise.resolve(20) }
+      ]
+    };
+
+    const result = await parseWithAwait(arraySchema, data);
+
+    expect(result).toEqual({
+      items: [
+        { name: 'Item 1', value: 10 },
+        { name: 'Item 2', value: 20 }
+      ]
+    });
+  });
+});

+ 16 - 0
packages/shared-utils/tsconfig.json

@@ -0,0 +1,16 @@
+{
+  "extends": "../../tsconfig.json",
+  "compilerOptions": {
+    "composite": true,
+    "rootDir": "src",
+    "outDir": "dist"
+  },
+  "include": [
+    "src/**/*"
+  ],
+  "exclude": [
+    "node_modules",
+    "dist",
+    "tests"
+  ]
+}

+ 14 - 0
packages/shared-utils/vitest.config.ts

@@ -0,0 +1,14 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+  test: {
+    globals: true,
+    environment: 'node',
+    include: ['tests/**/*.test.ts'],
+    coverage: {
+      provider: 'v8',
+      reporter: ['text', 'json', 'html'],
+      exclude: ['node_modules/', 'tests/']
+    }
+  }
+});

+ 68 - 0
pnpm-lock.yaml

@@ -229,6 +229,25 @@ importers:
         specifier: ^0.0.10
         version: 0.0.10(webpack@5.91.0(@swc/core@1.3.96))
 
+  packages/auth-core:
+    dependencies:
+      '@d8d/shared-types':
+        specifier: workspace:*
+        version: link:../shared-types
+      bcrypt:
+        specifier: ^6.0.0
+        version: 6.0.0
+      jsonwebtoken:
+        specifier: ^9.0.2
+        version: 9.0.2
+    devDependencies:
+      typescript:
+        specifier: ^5.8.3
+        version: 5.8.3
+      vitest:
+        specifier: ^3.2.4
+        version: 3.2.4(@types/debug@4.1.12)(@types/node@24.9.1)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@24.1.3)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
+
   packages/server:
     dependencies:
       '@asteasolutions/zod-to-openapi':
@@ -317,6 +336,55 @@ importers:
         specifier: ^3.2.4
         version: 3.2.4(@types/debug@4.1.12)(@types/node@24.9.1)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@24.1.3)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
 
+  packages/shared-utils:
+    dependencies:
+      '@d8d/shared-types':
+        specifier: workspace:*
+        version: link:../shared-types
+      '@hono/zod-openapi':
+        specifier: 1.0.2
+        version: 1.0.2(hono@4.8.5)(zod@4.1.12)
+      bcrypt:
+        specifier: ^6.0.0
+        version: 6.0.0
+      debug:
+        specifier: ^4.4.3
+        version: 4.4.3
+      jsonwebtoken:
+        specifier: ^9.0.2
+        version: 9.0.2
+      pg:
+        specifier: ^8.16.3
+        version: 8.16.3
+      reflect-metadata:
+        specifier: ^0.2.2
+        version: 0.2.2
+      typeorm:
+        specifier: ^0.3.20
+        version: 0.3.27(ioredis@5.8.2)(pg@8.16.3)(redis@4.7.1)(reflect-metadata@0.2.2)
+    devDependencies:
+      '@types/bcrypt':
+        specifier: ^6.0.0
+        version: 6.0.0
+      '@types/debug':
+        specifier: ^4.1.12
+        version: 4.1.12
+      '@types/jsonwebtoken':
+        specifier: ^9.0.7
+        version: 9.0.10
+      '@types/pg':
+        specifier: ^8.11.10
+        version: 8.15.5
+      hono:
+        specifier: ^4.8.5
+        version: 4.8.5
+      typescript:
+        specifier: ^5.8.3
+        version: 5.8.3
+      vitest:
+        specifier: ^3.2.4
+        version: 3.2.4(@types/debug@4.1.12)(@types/node@24.9.1)(happy-dom@18.0.1)(jiti@2.6.1)(jsdom@24.1.3)(lightningcss@1.30.2)(sass@1.93.2)(stylus@0.64.0)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)
+
   web:
     dependencies:
       '@ant-design/icons':

+ 22 - 0
tsconfig.json

@@ -0,0 +1,22 @@
+{
+  "compilerOptions": {
+    "target": "ES2022",
+    "lib": ["ES2022"],
+    "module": "ESNext",
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "resolveJsonModule": true,
+    "allowJs": true,
+    "strict": true,
+    "noEmit": true,
+    "esModuleInterop": true,
+    "allowSyntheticDefaultImports": true,
+    "forceConsistentCasingInFileNames": true,
+    "skipLibCheck": true,
+    "experimentalDecorators": true,
+    "emitDecoratorMetadata": true,
+    "declaration": true,
+    "declarationMap": true,
+    "sourceMap": true
+  }
+}