mirror of
https://github.com/vee1e/workorder-desk.git
synced 2026-09-01 09:50:13 +00:00
feat(shared): add zod schemas and shared API types
This commit is contained in:
parent
99458eb562
commit
df1827dd55
5 changed files with 276 additions and 0 deletions
26
packages/shared/package.json
Normal file
26
packages/shared/package.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "@workorders/shared",
|
||||
"version": "1.2.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.json",
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit",
|
||||
"lint": "eslint \"src/**/*.ts\"",
|
||||
"test": "echo \"shared: no tests (types only)\" && exit 0"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.5.4"
|
||||
}
|
||||
}
|
||||
2
packages/shared/src/index.ts
Normal file
2
packages/shared/src/index.ts
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export * from './types.js';
|
||||
export * from './schemas.js';
|
||||
137
packages/shared/src/schemas.ts
Normal file
137
packages/shared/src/schemas.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const emailSchema = z.string().trim().toLowerCase().email().max(255);
|
||||
|
||||
export const passwordSchema = z
|
||||
.string()
|
||||
.min(8, 'at least 8 characters')
|
||||
.max(72, 'at most 72 characters')
|
||||
.regex(/[a-zA-Z]/, 'at least one letter')
|
||||
.regex(/[0-9]/, 'at least one number');
|
||||
|
||||
export const nameSchema = z.string().trim().min(1).max(80);
|
||||
|
||||
export const roleSchema = z.enum(['admin', 'user', 'viewer']);
|
||||
|
||||
export const workOrderStatusSchema = z.enum(['pending', 'in_progress', 'done']);
|
||||
export const workOrderPrioritySchema = z.enum(['low', 'medium', 'high']);
|
||||
|
||||
// ── Auth ──────────────────────────────────────────────────────────────
|
||||
|
||||
export const registerSchema = z
|
||||
.object({
|
||||
email: emailSchema,
|
||||
password: passwordSchema,
|
||||
name: nameSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const loginSchema = z
|
||||
.object({
|
||||
email: z.string().trim().toLowerCase().max(255),
|
||||
password: z.string().min(1).max(72),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const forgotPasswordSchema = z
|
||||
.object({
|
||||
email: z.string().trim().toLowerCase().max(255),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const resetPasswordSchema = z
|
||||
.object({
|
||||
token: z.string().min(1).max(128),
|
||||
password: passwordSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
// ── Work orders ──────────────────────────────────────────────────────
|
||||
|
||||
export const createWorkOrderSchema = z
|
||||
.object({
|
||||
title: z.string().trim().min(3).max(100),
|
||||
description: z.string().trim().max(2000).nullable().optional(),
|
||||
priority: workOrderPrioritySchema.optional().default('medium'),
|
||||
status: workOrderStatusSchema.optional().default('pending'),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const updateWorkOrderSchema = z
|
||||
.object({
|
||||
title: z.string().trim().min(3).max(100).optional(),
|
||||
description: z.string().trim().max(2000).nullable().optional(),
|
||||
priority: workOrderPrioritySchema.optional(),
|
||||
status: workOrderStatusSchema.optional(),
|
||||
version: z.number().int().min(1),
|
||||
})
|
||||
.strict()
|
||||
.refine((v) => Object.keys(v).length > 1, { message: 'at least one field besides version is required' });
|
||||
|
||||
export const deleteWorkOrderSchema = z
|
||||
.object({
|
||||
version: z.number().int().min(1),
|
||||
})
|
||||
.strict();
|
||||
|
||||
// ── Profile ──────────────────────────────────────────────────────────
|
||||
|
||||
export const updateProfileSchema = z
|
||||
.object({
|
||||
name: nameSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const changePasswordSchema = z
|
||||
.object({
|
||||
currentPassword: z.string().min(1).max(72),
|
||||
newPassword: passwordSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
// ── Admin ────────────────────────────────────────────────────────────
|
||||
|
||||
export const updateRoleSchema = z
|
||||
.object({
|
||||
role: roleSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const updateStatusSchema = z
|
||||
.object({
|
||||
isActive: z.boolean(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
// ── Query params ─────────────────────────────────────────────────────
|
||||
|
||||
export const cursorQuerySchema = z.object({
|
||||
cursor: z.string().max(1024).optional(),
|
||||
limit: z.coerce.number().int().min(1).max(100).default(20),
|
||||
status: workOrderStatusSchema.optional(),
|
||||
priority: workOrderPrioritySchema.optional(),
|
||||
search: z.string().trim().max(64).optional(),
|
||||
});
|
||||
|
||||
export const offsetQuerySchema = z.object({
|
||||
page: z.coerce.number().int().min(1).default(1),
|
||||
limit: z.coerce.number().int().min(1).max(100).default(20),
|
||||
role: roleSchema.optional(),
|
||||
search: z.string().trim().max(64).optional(),
|
||||
});
|
||||
|
||||
// ── Inferred input types ─────────────────────────────────────────────
|
||||
|
||||
export type RegisterInput = z.infer<typeof registerSchema>;
|
||||
export type LoginInput = z.infer<typeof loginSchema>;
|
||||
export type ForgotPasswordInput = z.infer<typeof forgotPasswordSchema>;
|
||||
export type ResetPasswordInput = z.infer<typeof resetPasswordSchema>;
|
||||
export type CreateWorkOrderInput = z.infer<typeof createWorkOrderSchema>;
|
||||
export type UpdateWorkOrderInput = z.infer<typeof updateWorkOrderSchema>;
|
||||
export type DeleteWorkOrderInput = z.infer<typeof deleteWorkOrderSchema>;
|
||||
export type UpdateProfileInput = z.infer<typeof updateProfileSchema>;
|
||||
export type ChangePasswordInput = z.infer<typeof changePasswordSchema>;
|
||||
export type UpdateRoleInput = z.infer<typeof updateRoleSchema>;
|
||||
export type UpdateStatusInput = z.infer<typeof updateStatusSchema>;
|
||||
export type CursorQuery = z.infer<typeof cursorQuerySchema>;
|
||||
export type OffsetQuery = z.infer<typeof offsetQuerySchema>;
|
||||
101
packages/shared/src/types.ts
Normal file
101
packages/shared/src/types.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
export type Role = 'admin' | 'user' | 'viewer';
|
||||
|
||||
export type WorkOrderStatus = 'pending' | 'in_progress' | 'done';
|
||||
|
||||
export type WorkOrderPriority = 'low' | 'medium' | 'high';
|
||||
|
||||
export interface UserPublic {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: Role;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface UserAdmin extends UserPublic {
|
||||
isActive: boolean;
|
||||
lastLoginAt: string | null;
|
||||
}
|
||||
|
||||
export interface WorkOrderPublic {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string | null;
|
||||
priority: WorkOrderPriority;
|
||||
status: WorkOrderStatus;
|
||||
owner: { id: string; name: string; email: string };
|
||||
version: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface CursorPage<T> {
|
||||
items: T[];
|
||||
nextCursor: string | null;
|
||||
}
|
||||
|
||||
export interface OffsetPage<T> {
|
||||
items: T[];
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
export type ErrorCode =
|
||||
| 'VALIDATION_ERROR'
|
||||
| 'UNAUTHORIZED'
|
||||
| 'FORBIDDEN'
|
||||
| 'NOT_FOUND'
|
||||
| 'CONFLICT_VERSION'
|
||||
| 'RATE_LIMITED'
|
||||
| 'ACCOUNT_LOCKED'
|
||||
| 'AUTH_GENERIC'
|
||||
| 'EMAIL_TAKEN'
|
||||
| 'REFRESH_REUSE'
|
||||
| 'INTERNAL';
|
||||
|
||||
export interface ApiErrorBody {
|
||||
code: ErrorCode;
|
||||
message: string;
|
||||
details?: { field: string; message: string }[];
|
||||
}
|
||||
|
||||
export interface SuccessEnvelope<T> {
|
||||
success: true;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export interface ErrorEnvelope {
|
||||
success: false;
|
||||
error: ApiErrorBody;
|
||||
requestId: string;
|
||||
}
|
||||
|
||||
export interface Metrics {
|
||||
users: number;
|
||||
workOrders: number;
|
||||
uptimeSeconds: number;
|
||||
}
|
||||
|
||||
export interface OkResponse {
|
||||
ok: true;
|
||||
}
|
||||
|
||||
export interface HealthResponse {
|
||||
status: 'ok';
|
||||
}
|
||||
|
||||
export interface ReadyResponse {
|
||||
status: 'ok' | 'degraded';
|
||||
}
|
||||
|
||||
export const ACCESS_COOKIE = 'access_token';
|
||||
export const REFRESH_COOKIE = 'refresh_token';
|
||||
export const ACCESS_TOKEN_TTL_SECONDS = 900;
|
||||
export const REFRESH_TOKEN_TTL_SECONDS = 604800;
|
||||
|
||||
export const APP_ISS = 'workorders';
|
||||
export const APP_AUD = 'workorders-api';
|
||||
|
||||
export const CURSOR_SECRET_MIN_LENGTH = 32;
|
||||
10
packages/shared/tsconfig.json
Normal file
10
packages/shared/tsconfig.json
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler"
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue