feat(shared): add zod schemas and shared API types

This commit is contained in:
lakshit verma 2026-08-17 01:31:48 +05:30
parent 99458eb562
commit df1827dd55
No known key found for this signature in database
5 changed files with 276 additions and 0 deletions

View 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"
}
}

View file

@ -0,0 +1,2 @@
export * from './types.js';
export * from './schemas.js';

View 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>;

View 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;

View file

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"module": "ESNext",
"moduleResolution": "Bundler"
},
"include": ["src/**/*.ts"]
}