feat(mvp): phase 2 - database schema & services layer
Define full Prisma schema (10 models), run initial migration, build core services (auth, user, group, app, board, permission), Zod validators, type definitions, API response envelope, constants, and seed script.
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
UserRole,
|
||||
AuthMode,
|
||||
WidgetType,
|
||||
IconType,
|
||||
PermissionLevel,
|
||||
EntityType,
|
||||
TargetType,
|
||||
HealthcheckMethod
|
||||
} from './constants.js';
|
||||
|
||||
// --- Auth ---
|
||||
|
||||
export const loginSchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
password: z.string().min(1, 'Password is required')
|
||||
});
|
||||
|
||||
export const registerSchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
password: z.string().min(6, 'Password must be at least 6 characters'),
|
||||
displayName: z.string().min(1, 'Display name is required').max(100)
|
||||
});
|
||||
|
||||
// --- User ---
|
||||
|
||||
export const createUserSchema = z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
password: z.string().min(6).optional(),
|
||||
displayName: z.string().min(1).max(100),
|
||||
avatarUrl: z.string().url().optional(),
|
||||
authProvider: z.enum([AuthMode.LOCAL, AuthMode.OAUTH]).optional(),
|
||||
role: z.enum([UserRole.ADMIN, UserRole.USER]).optional()
|
||||
});
|
||||
|
||||
export const updateUserSchema = z.object({
|
||||
displayName: z.string().min(1).max(100).optional(),
|
||||
avatarUrl: z.string().url().nullable().optional(),
|
||||
role: z.enum([UserRole.ADMIN, UserRole.USER]).optional()
|
||||
});
|
||||
|
||||
// --- Group ---
|
||||
|
||||
export const createGroupSchema = z.object({
|
||||
name: z.string().min(1, 'Group name is required').max(100),
|
||||
description: z.string().max(500).optional(),
|
||||
isDefault: z.boolean().optional()
|
||||
});
|
||||
|
||||
export const updateGroupSchema = z.object({
|
||||
name: z.string().min(1).max(100).optional(),
|
||||
description: z.string().max(500).nullable().optional(),
|
||||
isDefault: z.boolean().optional()
|
||||
});
|
||||
|
||||
// --- App ---
|
||||
|
||||
export const createAppSchema = z.object({
|
||||
name: z.string().min(1, 'App name is required').max(200),
|
||||
url: z.string().url('Invalid URL'),
|
||||
icon: z.string().max(500).optional(),
|
||||
iconType: z.enum([IconType.LUCIDE, IconType.SIMPLE, IconType.URL, IconType.EMOJI]).optional(),
|
||||
description: z.string().max(1000).optional(),
|
||||
category: z.string().max(100).optional(),
|
||||
tags: z.string().max(500).optional(),
|
||||
healthcheckEnabled: z.boolean().optional(),
|
||||
healthcheckInterval: z.number().int().min(30).max(86400).optional(),
|
||||
healthcheckMethod: z.enum([HealthcheckMethod.GET, HealthcheckMethod.HEAD]).optional(),
|
||||
healthcheckExpectedStatus: z.number().int().min(100).max(599).optional(),
|
||||
healthcheckTimeout: z.number().int().min(1000).max(30000).optional()
|
||||
});
|
||||
|
||||
export const updateAppSchema = z.object({
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
url: z.string().url().optional(),
|
||||
icon: z.string().max(500).nullable().optional(),
|
||||
iconType: z.enum([IconType.LUCIDE, IconType.SIMPLE, IconType.URL, IconType.EMOJI]).optional(),
|
||||
description: z.string().max(1000).nullable().optional(),
|
||||
category: z.string().max(100).nullable().optional(),
|
||||
tags: z.string().max(500).optional(),
|
||||
healthcheckEnabled: z.boolean().optional(),
|
||||
healthcheckInterval: z.number().int().min(30).max(86400).optional(),
|
||||
healthcheckMethod: z.enum([HealthcheckMethod.GET, HealthcheckMethod.HEAD]).optional(),
|
||||
healthcheckExpectedStatus: z.number().int().min(100).max(599).optional(),
|
||||
healthcheckTimeout: z.number().int().min(1000).max(30000).optional()
|
||||
});
|
||||
|
||||
// --- Board ---
|
||||
|
||||
export const createBoardSchema = z.object({
|
||||
name: z.string().min(1, 'Board name is required').max(200),
|
||||
icon: z.string().max(500).optional(),
|
||||
description: z.string().max(1000).optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
isGuestAccessible: z.boolean().optional(),
|
||||
backgroundConfig: z.string().optional()
|
||||
});
|
||||
|
||||
export const updateBoardSchema = z.object({
|
||||
name: z.string().min(1).max(200).optional(),
|
||||
icon: z.string().max(500).nullable().optional(),
|
||||
description: z.string().max(1000).nullable().optional(),
|
||||
isDefault: z.boolean().optional(),
|
||||
isGuestAccessible: z.boolean().optional(),
|
||||
backgroundConfig: z.string().nullable().optional()
|
||||
});
|
||||
|
||||
// --- Section ---
|
||||
|
||||
export const createSectionSchema = z.object({
|
||||
boardId: z.string().cuid(),
|
||||
title: z.string().min(1, 'Section title is required').max(200),
|
||||
icon: z.string().max(500).optional(),
|
||||
order: z.number().int().min(0).optional(),
|
||||
isExpandedByDefault: z.boolean().optional()
|
||||
});
|
||||
|
||||
export const updateSectionSchema = z.object({
|
||||
title: z.string().min(1).max(200).optional(),
|
||||
icon: z.string().max(500).nullable().optional(),
|
||||
order: z.number().int().min(0).optional(),
|
||||
isExpandedByDefault: z.boolean().optional()
|
||||
});
|
||||
|
||||
// --- Widget ---
|
||||
|
||||
export const createWidgetSchema = z.object({
|
||||
sectionId: z.string().cuid(),
|
||||
type: z.enum([WidgetType.APP, WidgetType.BOOKMARK, WidgetType.NOTE, WidgetType.EMBED, WidgetType.STATUS]),
|
||||
order: z.number().int().min(0).optional(),
|
||||
config: z.string().optional(),
|
||||
appId: z.string().cuid().optional()
|
||||
});
|
||||
|
||||
export const updateWidgetSchema = z.object({
|
||||
type: z
|
||||
.enum([WidgetType.APP, WidgetType.BOOKMARK, WidgetType.NOTE, WidgetType.EMBED, WidgetType.STATUS])
|
||||
.optional(),
|
||||
order: z.number().int().min(0).optional(),
|
||||
config: z.string().optional(),
|
||||
appId: z.string().cuid().nullable().optional()
|
||||
});
|
||||
|
||||
// --- Permission ---
|
||||
|
||||
export const createPermissionSchema = z.object({
|
||||
entityType: z.enum([EntityType.BOARD, EntityType.APP]),
|
||||
entityId: z.string().cuid(),
|
||||
targetType: z.enum([TargetType.USER, TargetType.GROUP]),
|
||||
targetId: z.string().cuid(),
|
||||
level: z.enum([PermissionLevel.VIEW, PermissionLevel.EDIT, PermissionLevel.ADMIN])
|
||||
});
|
||||
|
||||
// --- System Settings ---
|
||||
|
||||
export const updateSystemSettingsSchema = z.object({
|
||||
authMode: z.enum([AuthMode.LOCAL, AuthMode.OAUTH, AuthMode.BOTH]).optional(),
|
||||
registrationEnabled: z.boolean().optional(),
|
||||
oauthClientId: z.string().nullable().optional(),
|
||||
oauthClientSecret: z.string().nullable().optional(),
|
||||
oauthDiscoveryUrl: z.string().url().nullable().optional(),
|
||||
defaultTheme: z.enum(['dark', 'light']).optional(),
|
||||
defaultPrimaryColor: z
|
||||
.string()
|
||||
.regex(/^#[0-9a-fA-F]{6}$/, 'Invalid hex color')
|
||||
.optional(),
|
||||
healthcheckDefaults: z.string().optional()
|
||||
});
|
||||
Reference in New Issue
Block a user