mirror of
https://github.com/vee1e/workorder-desk.git
synced 2026-09-01 17:57:11 +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