refactor: migrate backend to pnpm monorepo with Hono module architecture
- Replace Bun with pnpm 9 + Turborepo + tsx; apps/api renamed to apps/backend - Split backend into http/, modules/, lib/ layers mirroring tai-specguard - Add use-case + Result pattern, Problem Details RFC 7807, basePath /api/v1 - Mount Better Auth at /api/v1/auth, keep session-auth whitelist - Split Prisma schema into prisma/models/*, generate into generated/ - Rework packages/shared into lib/ + schemas/ with pagination DTOs - Implement health, groups, students, payments, waitlist modules - Add vitest suite with prisma mocks (21 tests), biome lint - Point web client to /api/v1/auth and /api/v1/groups/from-organization
This commit is contained in:
@@ -6,17 +6,19 @@
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./schemas/*": "./src/schemas/*"
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "biome check .",
|
||||
"lint:fix": "biome check . --write"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.24.0"
|
||||
"zod": "3.24.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@prisma/client": "^7.10.0",
|
||||
"@biomejs/biome": "^2.4.15",
|
||||
"@gruperly/config": "workspace:*",
|
||||
"typescript": "^5.7.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,9 @@
|
||||
export * from './schemas/group.schema'
|
||||
export * from './schemas/student.schema'
|
||||
export * from './schemas/payment.schema'
|
||||
export * from './lib/problem-details.js';
|
||||
export * from './lib/result.js';
|
||||
export * from './schemas/groups.js';
|
||||
export * from './schemas/health-check.js';
|
||||
export * from './schemas/pagination.js';
|
||||
export * from './schemas/payments.js';
|
||||
export * from './schemas/problem-details.js';
|
||||
export * from './schemas/students.js';
|
||||
export * from './schemas/waitlist.js';
|
||||
9
packages/shared/src/lib/problem-details.ts
Normal file
9
packages/shared/src/lib/problem-details.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export type ProblemDetails = {
|
||||
type?: string;
|
||||
title: string;
|
||||
status: number;
|
||||
detail?: string;
|
||||
instance?: string;
|
||||
code?: string;
|
||||
errors?: Record<string, string[]>;
|
||||
};
|
||||
13
packages/shared/src/lib/result.ts
Normal file
13
packages/shared/src/lib/result.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export type Result<TValue, TError> =
|
||||
| { ok: true; value: TValue }
|
||||
| { ok: false; error: TError };
|
||||
|
||||
export const ok = <TValue>(value: TValue): Result<TValue, never> => ({
|
||||
ok: true,
|
||||
value,
|
||||
});
|
||||
|
||||
export const err = <TError>(error: TError): Result<never, TError> => ({
|
||||
ok: false,
|
||||
error,
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const createGroupSchema = z.object({
|
||||
name: z.string().min(1, 'El nombre es obligatorio'),
|
||||
description: z.string().optional(),
|
||||
})
|
||||
|
||||
export const updateGroupSchema = createGroupSchema.partial()
|
||||
|
||||
export const groupIdParamsSchema = z.object({
|
||||
id: z.string().min(1),
|
||||
})
|
||||
|
||||
export type CreateGroupInput = z.infer<typeof createGroupSchema>
|
||||
export type UpdateGroupInput = z.infer<typeof updateGroupSchema>
|
||||
42
packages/shared/src/schemas/groups.ts
Normal file
42
packages/shared/src/schemas/groups.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { z } from 'zod';
|
||||
import { createPageSchema, createPageSizeSchema, createPaginationSchema } from './pagination.js';
|
||||
|
||||
const isoDateTimeSchema = z.string().datetime();
|
||||
const pageSchema = createPageSchema();
|
||||
const pageSizeSchema = createPageSizeSchema(10);
|
||||
const paginationSchema = createPaginationSchema();
|
||||
|
||||
export const GroupDtoSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string().min(1).max(100),
|
||||
description: z.string().nullable(),
|
||||
createdById: z.string(),
|
||||
createdAt: isoDateTimeSchema,
|
||||
updatedAt: isoDateTimeSchema,
|
||||
});
|
||||
export type GroupDto = z.output<typeof GroupDtoSchema>;
|
||||
|
||||
export const GroupQuerySchema = z.object({
|
||||
page: pageSchema,
|
||||
pageSize: pageSizeSchema,
|
||||
});
|
||||
export type GroupQuery = z.output<typeof GroupQuerySchema>;
|
||||
|
||||
export const GroupListSchema = z.object({
|
||||
data: z.array(GroupDtoSchema),
|
||||
pagination: paginationSchema,
|
||||
});
|
||||
export type GroupList = z.output<typeof GroupListSchema>;
|
||||
|
||||
export const CreateGroupFromOrganizationSchema = z.strictObject({
|
||||
organizationId: z.string().min(1),
|
||||
});
|
||||
export type CreateGroupFromOrganization = z.output<typeof CreateGroupFromOrganizationSchema>;
|
||||
|
||||
export const CreateGroupFromOrganizationResultSchema = z.object({
|
||||
group: GroupDtoSchema,
|
||||
alreadyExists: z.boolean(),
|
||||
});
|
||||
export type CreateGroupFromOrganizationResult = z.output<
|
||||
typeof CreateGroupFromOrganizationResultSchema
|
||||
>;
|
||||
11
packages/shared/src/schemas/health-check.ts
Normal file
11
packages/shared/src/schemas/health-check.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export const HealthCheckSchema = z.object({
|
||||
status: z.string(),
|
||||
timestamp: z.string().datetime(),
|
||||
checks: z.object({
|
||||
database: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export type HealthCheck = z.output<typeof HealthCheckSchema>;
|
||||
18
packages/shared/src/schemas/pagination.ts
Normal file
18
packages/shared/src/schemas/pagination.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
export function createPageSchema() {
|
||||
return z.coerce.number().int().min(1).default(1);
|
||||
}
|
||||
|
||||
export function createPageSizeSchema(defaultValue: number = 10) {
|
||||
return z.coerce.number().int().min(1).max(100).default(defaultValue);
|
||||
}
|
||||
|
||||
export function createPaginationSchema() {
|
||||
return z.object({
|
||||
page: z.number().int().min(1),
|
||||
pageSize: z.number().int().min(1).max(100),
|
||||
total: z.number().int().min(0),
|
||||
totalPages: z.number().int().min(0),
|
||||
});
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const paymentStatusSchema = z.enum(['PENDING', 'PAID', 'OVERDUE', 'CANCELLED'])
|
||||
|
||||
export const createPaymentSchema = z.object({
|
||||
groupId: z.string().min(1),
|
||||
studentId: z.string().min(1),
|
||||
amount: z.coerce.number().positive(),
|
||||
currency: z.string().default('MXN'),
|
||||
dueDate: z.coerce.date(),
|
||||
status: paymentStatusSchema.optional(),
|
||||
})
|
||||
|
||||
export const updatePaymentSchema = createPaymentSchema.partial()
|
||||
|
||||
export type CreatePaymentInput = z.infer<typeof createPaymentSchema>
|
||||
export type UpdatePaymentInput = z.infer<typeof updatePaymentSchema>
|
||||
35
packages/shared/src/schemas/payments.ts
Normal file
35
packages/shared/src/schemas/payments.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { z } from 'zod';
|
||||
import { createPageSchema, createPageSizeSchema, createPaginationSchema } from './pagination.js';
|
||||
|
||||
const isoDateTimeSchema = z.string().datetime();
|
||||
const pageSchema = createPageSchema();
|
||||
const pageSizeSchema = createPageSizeSchema(10);
|
||||
const paginationSchema = createPaginationSchema();
|
||||
|
||||
export const paymentStatusSchema = z.enum(['PENDING', 'PAID', 'OVERDUE', 'CANCELLED']);
|
||||
|
||||
export const PaymentDtoSchema = z.object({
|
||||
id: z.string(),
|
||||
groupId: z.string(),
|
||||
studentId: z.string(),
|
||||
amount: z.coerce.number().positive(),
|
||||
currency: z.string(),
|
||||
status: paymentStatusSchema,
|
||||
dueDate: isoDateTimeSchema,
|
||||
paidAt: isoDateTimeSchema.nullable(),
|
||||
createdAt: isoDateTimeSchema,
|
||||
updatedAt: isoDateTimeSchema,
|
||||
});
|
||||
export type PaymentDto = z.output<typeof PaymentDtoSchema>;
|
||||
|
||||
export const PaymentQuerySchema = z.object({
|
||||
page: pageSchema,
|
||||
pageSize: pageSizeSchema,
|
||||
});
|
||||
export type PaymentQuery = z.output<typeof PaymentQuerySchema>;
|
||||
|
||||
export const PaymentListSchema = z.object({
|
||||
data: z.array(PaymentDtoSchema),
|
||||
pagination: paginationSchema,
|
||||
});
|
||||
export type PaymentList = z.output<typeof PaymentListSchema>;
|
||||
12
packages/shared/src/schemas/problem-details.ts
Normal file
12
packages/shared/src/schemas/problem-details.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { z } from 'zod';
|
||||
import type { ProblemDetails } from '../lib/problem-details.js';
|
||||
|
||||
export const ProblemDetailsSchema: z.ZodType<ProblemDetails> = z.object({
|
||||
type: z.string().optional(),
|
||||
title: z.string(),
|
||||
status: z.number().int().min(100).max(599),
|
||||
detail: z.string().optional(),
|
||||
instance: z.string().optional(),
|
||||
code: z.string().optional(),
|
||||
errors: z.record(z.string(), z.array(z.string())).optional(),
|
||||
});
|
||||
@@ -1,16 +0,0 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
export const createStudentSchema = z.object({
|
||||
groupId: z.string().min(1),
|
||||
fullName: z.string().min(1, 'El nombre completo es obligatorio'),
|
||||
email: z.string().email().optional().or(z.literal('')),
|
||||
phone: z.string().optional().or(z.literal('')),
|
||||
guardianName: z.string().optional().or(z.literal('')),
|
||||
guardianPhone: z.string().optional().or(z.literal('')),
|
||||
notes: z.string().optional().or(z.literal('')),
|
||||
})
|
||||
|
||||
export const updateStudentSchema = createStudentSchema.omit({ groupId: true }).partial()
|
||||
|
||||
export type CreateStudentInput = z.infer<typeof createStudentSchema>
|
||||
export type UpdateStudentInput = z.infer<typeof updateStudentSchema>
|
||||
33
packages/shared/src/schemas/students.ts
Normal file
33
packages/shared/src/schemas/students.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { z } from 'zod';
|
||||
import { createPageSchema, createPageSizeSchema, createPaginationSchema } from './pagination.js';
|
||||
|
||||
const isoDateTimeSchema = z.string().datetime();
|
||||
const pageSchema = createPageSchema();
|
||||
const pageSizeSchema = createPageSizeSchema(10);
|
||||
const paginationSchema = createPaginationSchema();
|
||||
|
||||
export const StudentDtoSchema = z.object({
|
||||
id: z.string(),
|
||||
groupId: z.string(),
|
||||
fullName: z.string(),
|
||||
email: z.string().nullable(),
|
||||
phone: z.string().nullable(),
|
||||
guardianName: z.string().nullable(),
|
||||
guardianPhone: z.string().nullable(),
|
||||
notes: z.string().nullable(),
|
||||
createdAt: isoDateTimeSchema,
|
||||
updatedAt: isoDateTimeSchema,
|
||||
});
|
||||
export type StudentDto = z.output<typeof StudentDtoSchema>;
|
||||
|
||||
export const StudentQuerySchema = z.object({
|
||||
page: pageSchema,
|
||||
pageSize: pageSizeSchema,
|
||||
});
|
||||
export type StudentQuery = z.output<typeof StudentQuerySchema>;
|
||||
|
||||
export const StudentListSchema = z.object({
|
||||
data: z.array(StudentDtoSchema),
|
||||
pagination: paginationSchema,
|
||||
});
|
||||
export type StudentList = z.output<typeof StudentListSchema>;
|
||||
31
packages/shared/src/schemas/waitlist.ts
Normal file
31
packages/shared/src/schemas/waitlist.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { z } from 'zod';
|
||||
import { createPageSchema, createPageSizeSchema, createPaginationSchema } from './pagination.js';
|
||||
|
||||
const isoDateTimeSchema = z.string().datetime();
|
||||
const pageSchema = createPageSchema();
|
||||
const pageSizeSchema = createPageSizeSchema(10);
|
||||
const paginationSchema = createPaginationSchema();
|
||||
|
||||
export const waitlistStatusSchema = z.enum(['PENDING', 'INVITED', 'JOINED', 'DECLINED']);
|
||||
|
||||
export const WaitlistEntryDtoSchema = z.object({
|
||||
id: z.string(),
|
||||
email: z.string(),
|
||||
name: z.string().nullable(),
|
||||
status: waitlistStatusSchema,
|
||||
createdAt: isoDateTimeSchema,
|
||||
updatedAt: isoDateTimeSchema,
|
||||
});
|
||||
export type WaitlistEntryDto = z.output<typeof WaitlistEntryDtoSchema>;
|
||||
|
||||
export const WaitlistQuerySchema = z.object({
|
||||
page: pageSchema,
|
||||
pageSize: pageSizeSchema,
|
||||
});
|
||||
export type WaitlistQuery = z.output<typeof WaitlistQuerySchema>;
|
||||
|
||||
export const WaitlistListSchema = z.object({
|
||||
data: z.array(WaitlistEntryDtoSchema),
|
||||
pagination: paginationSchema,
|
||||
});
|
||||
export type WaitlistList = z.output<typeof WaitlistListSchema>;
|
||||
@@ -2,9 +2,10 @@
|
||||
"extends": "@gruperly/config/tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "bundler",
|
||||
"paths": {
|
||||
"@gruperly/shared": ["./src/index.ts"]
|
||||
}
|
||||
"declaration": false,
|
||||
"noEmit": false,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user