first init

This commit is contained in:
Power BI Dev
2026-08-17 10:52:09 +07:00
commit 88c7c653c2
9281 changed files with 2156575 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
# Server Configuration
PORT=4000
NODE_ENV=development
CLIENT_ORIGIN=http://localhost:5173
# Database Connection (Local SQLite for Zero-Setup Dev, PostgreSQL in Docker/Coolify)
DATABASE_URL="file:./dev.db"
# Rate Limiting
RATE_LIMIT_MAX_BOARDS_PER_HOUR=5
+10
View File
@@ -0,0 +1,10 @@
# Server Configuration
PORT=4000
NODE_ENV=development
CLIENT_ORIGIN=http://localhost:5173
# Database Connection (PostgreSQL)
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/retro_kanban?schema=public"
# Rate Limiting
RATE_LIMIT_MAX_BOARDS_PER_HOUR=5
+2
View File
@@ -0,0 +1,2 @@
import { Express } from 'express';
export declare function createApp(): Express;
+58
View File
@@ -0,0 +1,58 @@
import express from 'express';
import cors from 'cors';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
import boardRoutes from './routes/boardRoutes.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export function createApp() {
const app = express();
// CORS configuration
app.use(cors({
origin: (origin, callback) => {
callback(null, true);
},
credentials: true,
}));
app.use(express.json());
// Health check endpoint
app.get('/health', (_req, res) => {
res.json({
status: 'ok',
service: 'retro-kanban-server',
timestamp: new Date().toISOString(),
});
});
// API Routes
app.use('/api/boards', boardRoutes);
// Serve static frontend assets in production (when client build exists)
const clientDistPaths = [
path.join(__dirname, '../public'),
path.join(__dirname, '../../client/dist'),
path.join(__dirname, '../client/dist'),
path.join(process.cwd(), 'client/dist'),
path.join(process.cwd(), 'public'),
];
let staticPath = clientDistPaths.find((p) => fs.existsSync(p));
if (staticPath) {
console.log(`📁 [Static Assets] Serving frontend from: ${staticPath}`);
app.use(express.static(staticPath));
// SPA fallback: any non-API route serves index.html
app.get('*', (req, res, next) => {
if (req.path.startsWith('/api') || req.path.startsWith('/socket.io')) {
return next();
}
res.sendFile(path.join(staticPath, 'index.html'));
});
}
// Global Error Handler
app.use((err, _req, res, _next) => {
console.error('Unhandled server error:', err);
res.status(500).json({
error: 'Internal Server Error',
message: process.env.NODE_ENV === 'development' ? err.message : undefined,
});
});
return app;
}
+7
View File
@@ -0,0 +1,7 @@
export declare const config: {
port: number;
nodeEnv: string;
clientOrigin: string;
databaseUrl: string;
rateLimitMaxBoardsPerHour: number;
};
+9
View File
@@ -0,0 +1,9 @@
import dotenv from 'dotenv';
dotenv.config();
export const config = {
port: parseInt(process.env.PORT || '4000', 10),
nodeEnv: process.env.NODE_ENV || 'development',
clientOrigin: process.env.CLIENT_ORIGIN || 'http://localhost:5173',
databaseUrl: process.env.DATABASE_URL || 'postgresql://postgres:postgres@localhost:5432/retro_kanban?schema=public',
rateLimitMaxBoardsPerHour: parseInt(process.env.RATE_LIMIT_MAX_BOARDS_PER_HOUR || '5', 10),
};
+39
View File
@@ -0,0 +1,39 @@
import { Request, Response } from 'express';
import { AuthenticatedRequest } from '../middleware/auth.js';
export declare class BoardController {
/**
* POST /api/boards
* Create a new board with rate-limiting.
*/
static createBoard(req: Request, res: Response): Promise<void>;
/**
* GET /api/boards/:id
* Fetch board data using authorization key.
*/
static getBoard(req: AuthenticatedRequest, res: Response): Promise<void>;
/**
* GET /api/boards/:id/activity
* Fetch recent activity / movement logs for board.
*/
static getBoardActivity(req: AuthenticatedRequest, res: Response): Promise<void>;
/**
* GET /api/boards/:id/tickets/:ticketId/activity
* Fetch movement logs for specific ticket.
*/
static getTicketActivity(req: AuthenticatedRequest, res: Response): Promise<void>;
/**
* POST /api/boards/:id/rotate-member-key
* Rotate member key (Admin only).
*/
static rotateMemberKey(req: AuthenticatedRequest, res: Response): Promise<void>;
/**
* DELETE /api/boards/:id
* Delete board (Admin only).
*/
static deleteBoard(req: AuthenticatedRequest, res: Response): Promise<void>;
/**
* PATCH /api/boards/:id
* Update board title (Admin only).
*/
static updateBoard(req: AuthenticatedRequest, res: Response): Promise<void>;
}
+120
View File
@@ -0,0 +1,120 @@
import { BoardService } from '../services/boardService.js';
import { KanbanService } from '../services/kanbanService.js';
export class BoardController {
/**
* POST /api/boards
* Create a new board with rate-limiting.
*/
static async createBoard(req, res) {
try {
const { title } = req.body;
const result = await BoardService.createBoard(title);
res.status(201).json(result);
}
catch (error) {
console.error('Error creating board:', error);
res.status(500).json({ error: 'Failed to create board' });
}
}
/**
* GET /api/boards/:id
* Fetch board data using authorization key.
*/
static async getBoard(req, res) {
try {
const { boardId, role } = req.boardAuth;
const board = await BoardService.getBoardDetails(boardId, role);
if (!board) {
res.status(404).json({ error: 'Board not found' });
return;
}
res.json(board);
}
catch (error) {
console.error('Error getting board:', error);
res.status(500).json({ error: 'Failed to fetch board' });
}
}
/**
* GET /api/boards/:id/activity
* Fetch recent activity / movement logs for board.
*/
static async getBoardActivity(req, res) {
try {
const { boardId } = req.boardAuth;
const limit = parseInt(req.query.limit, 10) || 50;
const logs = await KanbanService.getBoardActivityLogs(boardId, limit);
res.json(logs);
}
catch (error) {
console.error('Error getting board activity:', error);
res.status(500).json({ error: 'Failed to fetch board activity logs' });
}
}
/**
* GET /api/boards/:id/tickets/:ticketId/activity
* Fetch movement logs for specific ticket.
*/
static async getTicketActivity(req, res) {
try {
const { ticketId } = req.params;
const limit = parseInt(req.query.limit, 10) || 30;
const logs = await KanbanService.getTicketActivityLogs(ticketId, limit);
res.json(logs);
}
catch (error) {
console.error('Error getting ticket activity:', error);
res.status(500).json({ error: 'Failed to fetch ticket activity logs' });
}
}
/**
* POST /api/boards/:id/rotate-member-key
* Rotate member key (Admin only).
*/
static async rotateMemberKey(req, res) {
try {
const { boardId } = req.boardAuth;
const result = await BoardService.rotateMemberKey(boardId);
res.json(result);
}
catch (error) {
console.error('Error rotating member key:', error);
res.status(500).json({ error: 'Failed to rotate member key' });
}
}
/**
* DELETE /api/boards/:id
* Delete board (Admin only).
*/
static async deleteBoard(req, res) {
try {
const { boardId } = req.boardAuth;
await BoardService.deleteBoard(boardId);
res.json({ success: true, message: 'Board deleted successfully' });
}
catch (error) {
console.error('Error deleting board:', error);
res.status(500).json({ error: 'Failed to delete board' });
}
}
/**
* PATCH /api/boards/:id
* Update board title (Admin only).
*/
static async updateBoard(req, res) {
try {
const { boardId } = req.boardAuth;
const { title } = req.body;
if (!title || !title.trim()) {
res.status(400).json({ error: 'Title is required' });
return;
}
await BoardService.updateBoard(boardId, { title });
res.json({ success: true, title: title.trim() });
}
catch (error) {
console.error('Error updating board:', error);
res.status(500).json({ error: 'Failed to update board title' });
}
}
}
+9
View File
@@ -0,0 +1,9 @@
/**
* Clean up inactive boards that have not been touched/updated in the last 30 days.
* Cascades to delete all associated columns and tickets.
*/
export declare function cleanupInactiveBoards(daysInactive?: number): Promise<number>;
/**
* Schedule periodic TTL cleanup in the server process (runs every 24 hours).
*/
export declare function startTtlCleanupSchedule(intervalHours?: number): NodeJS.Timeout;
+51
View File
@@ -0,0 +1,51 @@
import { prisma } from '../prisma.js';
/**
* Clean up inactive boards that have not been touched/updated in the last 30 days.
* Cascades to delete all associated columns and tickets.
*/
export async function cleanupInactiveBoards(daysInactive = 30) {
const cutoffDate = new Date();
cutoffDate.setDate(cutoffDate.getDate() - daysInactive);
console.log(`🧹 [TTL Cleanup] Checking for boards inactive since ${cutoffDate.toISOString()}...`);
try {
const result = await prisma.board.deleteMany({
where: {
updatedAt: {
lt: cutoffDate,
},
},
});
console.log(`🧹 [TTL Cleanup] Removed ${result.count} inactive boards (> ${daysInactive} days old).`);
return result.count;
}
catch (error) {
console.error('❌ [TTL Cleanup Error]:', error);
return 0;
}
}
/**
* Schedule periodic TTL cleanup in the server process (runs every 24 hours).
*/
export function startTtlCleanupSchedule(intervalHours = 24) {
console.log(`⏰ [TTL Schedule] Inactive board cleaner registered (Interval: every ${intervalHours}h)`);
// Run once after 1 minute on startup
setTimeout(() => {
cleanupInactiveBoards().catch(console.error);
}, 60000);
// Then run every intervalHours
return setInterval(() => {
cleanupInactiveBoards().catch(console.error);
}, intervalHours * 60 * 60 * 1000);
}
// Standalone execution support: `tsx src/cron/cleanup.ts`
if (process.argv[1]?.includes('cleanup.ts') || process.argv[1]?.includes('cleanup.js')) {
cleanupInactiveBoards()
.then((count) => {
console.log(`Done. Cleaned ${count} boards.`);
process.exit(0);
})
.catch((err) => {
console.error(err);
process.exit(1);
});
}
+1
View File
@@ -0,0 +1 @@
export {};
+22
View File
@@ -0,0 +1,22 @@
import http from 'http';
import { Server as SocketIOServer } from 'socket.io';
import { createApp } from './app.js';
import { config } from './config.js';
import { setupSocketHandlers } from './socket/index.js';
import { startTtlCleanupSchedule } from './cron/cleanup.js';
const app = createApp();
const server = http.createServer(app);
const io = new SocketIOServer(server, {
cors: {
origin: '*',
methods: ['GET', 'POST', 'PATCH', 'DELETE'],
},
});
// Setup real-time Socket.io handlers & authorization
setupSocketHandlers(io);
// Start periodic 30-day TTL cleanup (runs every 24 hours)
startTtlCleanupSchedule(24);
server.listen(config.port, () => {
console.log(`🎮 [Retro Kanban Server] Running on http://localhost:${config.port}`);
console.log(`🛡️ Environment: ${config.nodeEnv}`);
});
+15
View File
@@ -0,0 +1,15 @@
import { Request, Response, NextFunction } from 'express';
import { Role } from '../types/index.js';
export interface AuthenticatedRequest extends Request {
boardAuth?: {
boardId: string;
role: Role;
board: any;
};
}
/**
* Middleware to require board authorization.
* Role parameter defaults to allowing both 'admin' and 'member'.
* Pass 'admin' to restrict to superadmin only.
*/
export declare function requireBoardAuth(requiredRole?: Role): (req: AuthenticatedRequest, res: Response, next: NextFunction) => Promise<void>;
+55
View File
@@ -0,0 +1,55 @@
import { BoardService } from '../services/boardService.js';
/**
* Middleware to require board authorization.
* Role parameter defaults to allowing both 'admin' and 'member'.
* Pass 'admin' to restrict to superadmin only.
*/
export function requireBoardAuth(requiredRole) {
return async (req, res, next) => {
try {
const boardId = (req.params.boardId || req.params.id || req.body.boardId || req.query.boardId);
if (!boardId) {
res.status(400).json({ error: 'Missing board ID' });
return;
}
// Extract key from header, query, or body
let key = '';
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
key = authHeader.substring(7).trim();
}
else if (req.headers['x-board-key']) {
key = String(req.headers['x-board-key']).trim();
}
else if (req.query.key) {
key = String(req.query.key).trim();
}
else if (req.body.key) {
key = String(req.body.key).trim();
}
if (!key) {
res.status(401).json({ error: 'Unauthorized: Key is missing in request headers' });
return;
}
const authResult = await BoardService.authenticateKey(boardId, key);
if (!authResult) {
res.status(403).json({ error: 'Forbidden: Invalid key for this board' });
return;
}
if (requiredRole && requiredRole === 'admin' && authResult.role !== 'admin') {
res.status(403).json({ error: 'Forbidden: Superadmin access required' });
return;
}
req.boardAuth = {
boardId,
role: authResult.role,
board: authResult.board,
};
next();
}
catch (error) {
console.error('Error in auth middleware:', error);
res.status(500).json({ error: 'Internal Server Error during authentication' });
}
};
}
+1
View File
@@ -0,0 +1 @@
export declare const createBoardLimiter: import("express-rate-limit").RateLimitRequestHandler;
+12
View File
@@ -0,0 +1,12 @@
import rateLimit from 'express-rate-limit';
import { config } from '../config.js';
export const createBoardLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour window
max: config.rateLimitMaxBoardsPerHour, // max 5 boards per IP per hour
standardHeaders: true,
legacyHeaders: false,
message: {
error: 'Rate Limit Exceeded',
message: `You have reached the limit of ${config.rateLimitMaxBoardsPerHour} boards per hour. Please wait before creating another board.`,
},
});
+2
View File
@@ -0,0 +1,2 @@
import { PrismaClient } from '@prisma/client';
export declare const prisma: PrismaClient<import(".prisma/client").Prisma.PrismaClientOptions, never, import("@prisma/client/runtime/library").DefaultArgs>;
+8
View File
@@ -0,0 +1,8 @@
import { PrismaClient } from '@prisma/client';
const globalForPrisma = global;
export const prisma = globalForPrisma.prisma ||
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['warn', 'error'] : ['error'],
});
if (process.env.NODE_ENV !== 'production')
globalForPrisma.prisma = prisma;
+2
View File
@@ -0,0 +1,2 @@
declare const router: import("express-serve-static-core").Router;
export default router;
+20
View File
@@ -0,0 +1,20 @@
import { Router } from 'express';
import { BoardController } from '../controllers/boardController.js';
import { createBoardLimiter } from '../middleware/rateLimit.js';
import { requireBoardAuth } from '../middleware/auth.js';
const router = Router();
// Create new board (Rate limited: 5 boards/hour/IP)
router.post('/', createBoardLimiter, BoardController.createBoard);
// Get board details (Requires admin or member key)
router.get('/:id', requireBoardAuth(), BoardController.getBoard);
// Get board activity logs
router.get('/:id/activity', requireBoardAuth(), BoardController.getBoardActivity);
// Get ticket activity logs
router.get('/:id/tickets/:ticketId/activity', requireBoardAuth(), BoardController.getTicketActivity);
// Update board title (Requires admin key)
router.patch('/:id', requireBoardAuth('admin'), BoardController.updateBoard);
// Rotate member key (Requires admin key)
router.post('/:id/rotate-member-key', requireBoardAuth('admin'), BoardController.rotateMemberKey);
// Delete board (Requires admin key)
router.delete('/:id', requireBoardAuth('admin'), BoardController.deleteBoard);
export default router;
+36
View File
@@ -0,0 +1,36 @@
import { BoardCreationResult, BoardPayload, Role } from '../types/index.js';
export declare class BoardService {
/**
* Create a new Kanban board with admin and member keys and standard agile columns with WIP limits.
*/
static createBoard(title?: string): Promise<BoardCreationResult>;
/**
* Authenticate a key against a board ID. Returns the role ('admin' | 'member') or null if invalid.
*/
static authenticateKey(boardId: string, key: string): Promise<{
role: Role;
board: any;
} | null>;
/**
* Fetch full board data with columns and tickets ordered properly.
*/
static getBoardDetails(boardId: string, role: Role): Promise<BoardPayload | null>;
/**
* Regenerate member key (Key Rotation). Only superadmin can perform this.
*/
static rotateMemberKey(boardId: string): Promise<{
newMemberKey: string;
memberUrlFragment: string;
}>;
/**
* Delete a board. Only superadmin can perform this.
*/
static deleteBoard(boardId: string): Promise<void>;
/**
* Update board title & settings.
*/
static updateBoard(boardId: string, updates: {
title?: string;
autoMoveStale?: boolean;
}): Promise<void>;
}
+169
View File
@@ -0,0 +1,169 @@
import { prisma } from '../prisma.js';
import { generateSecureKey, hashKey, verifyKeyHash } from '../utils/crypto.js';
export class BoardService {
/**
* Create a new Kanban board with admin and member keys and standard agile columns with WIP limits.
*/
static async createBoard(title = 'Agile Kanban Board') {
const adminKey = generateSecureKey(32);
const memberKey = generateSecureKey(32);
const adminKeyHash = hashKey(adminKey);
const memberKeyHash = hashKey(memberKey);
const board = await prisma.board.create({
data: {
title: title.trim() || 'Agile Kanban Board',
adminKeyHash,
memberKeyHash,
autoMoveStale: false,
columns: {
create: [
{ title: '📋 Backlog', order: 0, maxWipLimit: 0, autoStaleHours: 0 },
{ title: '📝 To Do', order: 1, maxWipLimit: 0, autoStaleHours: 0 },
{ title: '⚡ In Progress', order: 2, maxWipLimit: 3, autoStaleHours: 24 }, // Standard 3-card WIP limit & 24h stale
{ title: '🔍 Review', order: 3, maxWipLimit: 4, autoStaleHours: 24 },
{ title: '✅ Done', order: 4, maxWipLimit: 0, autoStaleHours: 0 },
],
},
},
});
return {
id: board.id,
title: board.title,
adminKey,
memberKey,
adminUrlFragment: `/b/${board.id}#admin=${adminKey}`,
memberUrlFragment: `/b/${board.id}#member=${memberKey}`,
createdAt: board.createdAt.toISOString(),
};
}
/**
* Authenticate a key against a board ID. Returns the role ('admin' | 'member') or null if invalid.
*/
static async authenticateKey(boardId, key) {
if (!boardId || !key)
return null;
const board = await prisma.board.findUnique({
where: { id: boardId },
});
if (!board)
return null;
if (verifyKeyHash(key, board.adminKeyHash)) {
return { role: 'admin', board };
}
if (verifyKeyHash(key, board.memberKeyHash)) {
return { role: 'member', board };
}
return null;
}
/**
* Fetch full board data with columns and tickets ordered properly.
*/
static async getBoardDetails(boardId, role) {
const board = await prisma.board.findUnique({
where: { id: boardId },
include: {
columns: {
orderBy: { order: 'asc' },
include: {
tickets: {
orderBy: { order: 'asc' },
},
},
},
},
});
if (!board)
return null;
const columns = board.columns.map((col) => ({
id: col.id,
boardId: col.boardId,
title: col.title,
order: col.order,
maxWipLimit: col.maxWipLimit || 0,
autoStaleHours: col.autoStaleHours || 0,
createdAt: col.createdAt.toISOString(),
updatedAt: col.updatedAt.toISOString(),
tickets: col.tickets.map((t) => {
let parsedTags = [];
let parsedSubtasks = [];
try {
parsedTags = JSON.parse(t.tags || '[]');
}
catch {
parsedTags = [];
}
try {
parsedSubtasks = JSON.parse(t.subtasks || '[]');
}
catch {
parsedSubtasks = [];
}
return {
id: t.id,
columnId: t.columnId,
title: t.title,
description: t.description,
order: t.order,
priority: t.priority,
tags: parsedTags,
subtasks: parsedSubtasks,
dueDate: t.dueDate || null,
isBlocked: t.isBlocked || false,
blockedReason: t.blockedReason || null,
assignee: t.assignee,
movedAt: t.movedAt ? t.movedAt.toISOString() : t.createdAt.toISOString(),
createdAt: t.createdAt.toISOString(),
updatedAt: t.updatedAt.toISOString(),
};
}),
}));
return {
id: board.id,
title: board.title,
role,
autoMoveStale: board.autoMoveStale || false,
columns,
createdAt: board.createdAt.toISOString(),
updatedAt: board.updatedAt.toISOString(),
};
}
/**
* Regenerate member key (Key Rotation). Only superadmin can perform this.
*/
static async rotateMemberKey(boardId) {
const newMemberKey = generateSecureKey(32);
const newMemberKeyHash = hashKey(newMemberKey);
await prisma.board.update({
where: { id: boardId },
data: {
memberKeyHash: newMemberKeyHash,
},
});
return {
newMemberKey,
memberUrlFragment: `/b/${boardId}#member=${newMemberKey}`,
};
}
/**
* Delete a board. Only superadmin can perform this.
*/
static async deleteBoard(boardId) {
await prisma.board.delete({
where: { id: boardId },
});
}
/**
* Update board title & settings.
*/
static async updateBoard(boardId, updates) {
const data = {};
if (updates.title !== undefined)
data.title = updates.title.trim() || 'Agile Kanban Board';
if (updates.autoMoveStale !== undefined)
data.autoMoveStale = updates.autoMoveStale;
await prisma.board.update({
where: { id: boardId },
data,
});
}
}
+73
View File
@@ -0,0 +1,73 @@
import { Priority, TicketPayload, ColumnPayload, Subtask, ActivityLogPayload } from '../types/index.js';
export declare class KanbanService {
/**
* Create a new column in a board.
*/
static createColumn(boardId: string, title: string, maxWipLimit?: number, autoStaleHours?: number): Promise<ColumnPayload>;
/**
* Update a column (title, maxWipLimit, autoStaleHours).
*/
static updateColumn(columnId: string, updates: {
title?: string;
maxWipLimit?: number;
autoStaleHours?: number;
}): Promise<ColumnPayload>;
/**
* Delete a column and its tickets.
*/
static deleteColumn(columnId: string): Promise<string>;
/**
* Create a new ticket in a column and record activity log.
*/
static createTicket(columnId: string, title: string, description?: string, priority?: Priority, tags?: string[], assignee?: string, dueDate?: string, subtasks?: Subtask[], isBlocked?: boolean, blockedReason?: string, userName?: string): Promise<{
ticket: TicketPayload;
activity?: ActivityLogPayload;
}>;
/**
* Update ticket fields.
*/
static updateTicket(ticketId: string, updates: {
title?: string;
description?: string;
priority?: Priority;
tags?: string[];
subtasks?: Subtask[];
dueDate?: string | null;
isBlocked?: boolean;
blockedReason?: string | null;
assignee?: string | null;
}, userName?: string): Promise<{
ticket: TicketPayload;
activity?: ActivityLogPayload;
}>;
/**
* Delete a ticket.
*/
static deleteTicket(ticketId: string, userName?: string): Promise<{
boardId: string;
activity?: ActivityLogPayload;
}>;
/**
* Move ticket: changes column and/or reorders within a column.
* Updates orders in batch transaction, resets movedAt timestamp, and records movement log in DB.
*/
static moveTicket(ticketId: string, targetColumnId: string, newOrder: number, ticketIdsInTargetColumn: string[], movedBy?: string): Promise<ActivityLogPayload | null>;
/**
* Get recent activity logs for a board.
*/
static getBoardActivityLogs(boardId: string, limit?: number): Promise<ActivityLogPayload[]>;
/**
* Get activity logs for a specific ticket.
*/
static getTicketActivityLogs(ticketId: string, limit?: number): Promise<ActivityLogPayload[]>;
/**
* Reorder columns in a board.
*/
static reorderColumns(boardId: string, columnIdsInOrder: string[]): Promise<void>;
/**
* Check for stale tickets in active columns and auto-move them to Backlog if enabled.
*/
static processStaleTickets(boardId: string): Promise<{
movedTicketIds: string[];
}>;
}
+520
View File
@@ -0,0 +1,520 @@
import { prisma } from '../prisma.js';
export class KanbanService {
/**
* Create a new column in a board.
*/
static async createColumn(boardId, title, maxWipLimit = 0, autoStaleHours = 0) {
const lastCol = await prisma.column.findFirst({
where: { boardId },
orderBy: { order: 'desc' },
});
const nextOrder = lastCol ? lastCol.order + 1 : 0;
const col = await prisma.column.create({
data: {
boardId,
title: title.trim() || 'New Column',
order: nextOrder,
maxWipLimit: Math.max(0, maxWipLimit),
autoStaleHours: Math.max(0, autoStaleHours),
},
});
// Touch board updatedAt
await prisma.board.update({
where: { id: boardId },
data: { updatedAt: new Date() },
});
return {
id: col.id,
boardId: col.boardId,
title: col.title,
order: col.order,
maxWipLimit: col.maxWipLimit,
autoStaleHours: col.autoStaleHours,
tickets: [],
createdAt: col.createdAt.toISOString(),
updatedAt: col.updatedAt.toISOString(),
};
}
/**
* Update a column (title, maxWipLimit, autoStaleHours).
*/
static async updateColumn(columnId, updates) {
const data = {};
if (updates.title !== undefined)
data.title = updates.title.trim();
if (updates.maxWipLimit !== undefined)
data.maxWipLimit = Math.max(0, updates.maxWipLimit);
if (updates.autoStaleHours !== undefined)
data.autoStaleHours = Math.max(0, updates.autoStaleHours);
const col = await prisma.column.update({
where: { id: columnId },
data,
include: {
tickets: {
orderBy: { order: 'asc' },
},
},
});
await prisma.board.update({
where: { id: col.boardId },
data: { updatedAt: new Date() },
});
return {
id: col.id,
boardId: col.boardId,
title: col.title,
order: col.order,
maxWipLimit: col.maxWipLimit,
autoStaleHours: col.autoStaleHours,
tickets: col.tickets.map((t) => ({
id: t.id,
columnId: t.columnId,
title: t.title,
description: t.description,
order: t.order,
priority: t.priority,
tags: JSON.parse(t.tags || '[]'),
subtasks: JSON.parse(t.subtasks || '[]'),
dueDate: t.dueDate,
isBlocked: t.isBlocked,
blockedReason: t.blockedReason,
assignee: t.assignee,
movedAt: t.movedAt.toISOString(),
createdAt: t.createdAt.toISOString(),
updatedAt: t.updatedAt.toISOString(),
})),
createdAt: col.createdAt.toISOString(),
updatedAt: col.updatedAt.toISOString(),
};
}
/**
* Delete a column and its tickets.
*/
static async deleteColumn(columnId) {
const col = await prisma.column.delete({
where: { id: columnId },
});
await prisma.board.update({
where: { id: col.boardId },
data: { updatedAt: new Date() },
});
return col.boardId;
}
/**
* Create a new ticket in a column and record activity log.
*/
static async createTicket(columnId, title, description = '', priority = 'MEDIUM', tags = [], assignee = '', dueDate = '', subtasks = [], isBlocked = false, blockedReason = '', userName) {
const lastTicket = await prisma.ticket.findFirst({
where: { columnId },
orderBy: { order: 'desc' },
});
const nextOrder = lastTicket ? lastTicket.order + 1 : 0;
const ticket = await prisma.ticket.create({
data: {
columnId,
title: title.trim() || 'New Task',
description: description || '',
order: nextOrder,
priority,
tags: JSON.stringify(tags),
subtasks: JSON.stringify(subtasks),
dueDate: dueDate || null,
isBlocked: isBlocked || false,
blockedReason: blockedReason || null,
assignee: assignee || null,
movedAt: new Date(),
},
include: {
column: true,
},
});
// Create activity log in DB
let activityPayload;
try {
const act = await prisma.activityLog.create({
data: {
boardId: ticket.column.boardId,
ticketId: ticket.id,
ticketTitle: ticket.title,
action: 'CREATED',
toColumnTitle: ticket.column.title,
userName: userName || 'User',
details: `Tugas "${ticket.title}" dibuat di kolom "${ticket.column.title}"`,
},
});
activityPayload = {
id: act.id,
boardId: act.boardId,
ticketId: act.ticketId,
ticketTitle: act.ticketTitle,
action: act.action,
fromColumnTitle: act.fromColumnTitle,
toColumnTitle: act.toColumnTitle,
userName: act.userName,
details: act.details,
createdAt: act.createdAt.toISOString(),
};
}
catch (err) {
console.error('Failed to log ticket creation activity:', err);
}
// Touch board updatedAt
await prisma.board.update({
where: { id: ticket.column.boardId },
data: { updatedAt: new Date() },
});
return {
ticket: {
id: ticket.id,
columnId: ticket.columnId,
title: ticket.title,
description: ticket.description,
order: ticket.order,
priority: ticket.priority,
tags,
subtasks,
dueDate: ticket.dueDate,
isBlocked: ticket.isBlocked,
blockedReason: ticket.blockedReason,
assignee: ticket.assignee,
movedAt: ticket.movedAt.toISOString(),
createdAt: ticket.createdAt.toISOString(),
updatedAt: ticket.updatedAt.toISOString(),
},
activity: activityPayload,
};
}
/**
* Update ticket fields.
*/
static async updateTicket(ticketId, updates, userName) {
const data = {};
if (updates.title !== undefined)
data.title = updates.title;
if (updates.description !== undefined)
data.description = updates.description;
if (updates.priority !== undefined)
data.priority = updates.priority;
if (updates.tags !== undefined)
data.tags = JSON.stringify(updates.tags);
if (updates.subtasks !== undefined)
data.subtasks = JSON.stringify(updates.subtasks);
if (updates.dueDate !== undefined)
data.dueDate = updates.dueDate;
if (updates.isBlocked !== undefined)
data.isBlocked = updates.isBlocked;
if (updates.blockedReason !== undefined)
data.blockedReason = updates.blockedReason;
if (updates.assignee !== undefined)
data.assignee = updates.assignee;
const existingTicket = await prisma.ticket.findUnique({
where: { id: ticketId },
include: { column: true },
});
if (!existingTicket)
throw new Error('Ticket not found');
const ticket = await prisma.ticket.update({
where: { id: ticketId },
data,
include: { column: true },
});
// Touch board updatedAt
await prisma.board.update({
where: { id: ticket.column.boardId },
data: { updatedAt: new Date() },
});
let activityPayload;
if (updates.isBlocked !== undefined && updates.isBlocked !== existingTicket.isBlocked) {
try {
const act = await prisma.activityLog.create({
data: {
boardId: ticket.column.boardId,
ticketId: ticket.id,
ticketTitle: ticket.title,
action: updates.isBlocked ? 'BLOCKED' : 'UNBLOCKED',
toColumnTitle: ticket.column.title,
userName: userName || 'User',
details: updates.isBlocked
? `Status tugas ditandai MACET / BLOCKED (${updates.blockedReason || 'Tanpa keterangan'})`
: 'Status macet diatasi (Unblocked)',
},
});
activityPayload = {
id: act.id,
boardId: act.boardId,
ticketId: act.ticketId,
ticketTitle: act.ticketTitle,
action: act.action,
fromColumnTitle: act.fromColumnTitle,
toColumnTitle: act.toColumnTitle,
userName: act.userName,
details: act.details,
createdAt: act.createdAt.toISOString(),
};
}
catch (err) {
console.error('Failed to log blocked status activity:', err);
}
}
let parsedTags = [];
let parsedSubtasks = [];
try {
parsedTags = JSON.parse(ticket.tags || '[]');
}
catch {
parsedTags = [];
}
try {
parsedSubtasks = JSON.parse(ticket.subtasks || '[]');
}
catch {
parsedSubtasks = [];
}
return {
ticket: {
id: ticket.id,
columnId: ticket.columnId,
title: ticket.title,
description: ticket.description,
order: ticket.order,
priority: ticket.priority,
tags: parsedTags,
subtasks: parsedSubtasks,
dueDate: ticket.dueDate,
isBlocked: ticket.isBlocked,
blockedReason: ticket.blockedReason,
assignee: ticket.assignee,
movedAt: ticket.movedAt.toISOString(),
createdAt: ticket.createdAt.toISOString(),
updatedAt: ticket.updatedAt.toISOString(),
},
activity: activityPayload,
};
}
/**
* Delete a ticket.
*/
static async deleteTicket(ticketId, userName) {
const ticket = await prisma.ticket.findUnique({
where: { id: ticketId },
include: { column: true },
});
if (!ticket)
throw new Error('Ticket not found');
const boardId = ticket.column.boardId;
let activityPayload;
try {
const act = await prisma.activityLog.create({
data: {
boardId,
ticketId: ticket.id,
ticketTitle: ticket.title,
action: 'DELETED',
fromColumnTitle: ticket.column.title,
userName: userName || 'User',
details: `Tugas "${ticket.title}" dihapus dari kolom "${ticket.column.title}"`,
},
});
activityPayload = {
id: act.id,
boardId: act.boardId,
ticketId: act.ticketId,
ticketTitle: act.ticketTitle,
action: act.action,
fromColumnTitle: act.fromColumnTitle,
toColumnTitle: act.toColumnTitle,
userName: act.userName,
details: act.details,
createdAt: act.createdAt.toISOString(),
};
}
catch (err) {
console.error('Failed to log delete activity:', err);
}
await prisma.ticket.delete({
where: { id: ticketId },
});
await prisma.board.update({
where: { id: boardId },
data: { updatedAt: new Date() },
});
return { boardId, activity: activityPayload };
}
/**
* Move ticket: changes column and/or reorders within a column.
* Updates orders in batch transaction, resets movedAt timestamp, and records movement log in DB.
*/
static async moveTicket(ticketId, targetColumnId, newOrder, ticketIdsInTargetColumn, movedBy) {
const ticket = await prisma.ticket.findUnique({
where: { id: ticketId },
include: { column: true },
});
if (!ticket)
throw new Error('Ticket not found');
const sourceBoardId = ticket.column.boardId;
const isChangingColumn = ticket.columnId !== targetColumnId;
let targetColumn = ticket.column;
if (isChangingColumn) {
const foundTarget = await prisma.column.findUnique({
where: { id: targetColumnId },
});
if (foundTarget)
targetColumn = foundTarget;
}
let createdActivity = null;
await prisma.$transaction(async (tx) => {
// 1. Move target ticket to new column and update movedAt timestamp if changing column
await tx.ticket.update({
where: { id: ticketId },
data: {
columnId: targetColumnId,
order: newOrder,
movedAt: isChangingColumn ? new Date() : ticket.movedAt,
},
});
// 2. Re-assign sequential orders to all tickets in target column
if (ticketIdsInTargetColumn && ticketIdsInTargetColumn.length > 0) {
for (let i = 0; i < ticketIdsInTargetColumn.length; i++) {
const id = ticketIdsInTargetColumn[i];
await tx.ticket.update({
where: { id },
data: {
columnId: targetColumnId,
order: i,
},
});
}
}
// 3. Save Movement Log to DB if column changed
if (isChangingColumn) {
const act = await tx.activityLog.create({
data: {
boardId: sourceBoardId,
ticketId: ticket.id,
ticketTitle: ticket.title,
action: 'MOVED',
fromColumnTitle: ticket.column.title,
toColumnTitle: targetColumn.title,
userName: movedBy || 'User',
details: `Dipindahkan dari "${ticket.column.title}" ke "${targetColumn.title}"`,
},
});
createdActivity = {
id: act.id,
boardId: act.boardId,
ticketId: act.ticketId,
ticketTitle: act.ticketTitle,
action: act.action,
fromColumnTitle: act.fromColumnTitle,
toColumnTitle: act.toColumnTitle,
userName: act.userName,
details: act.details,
createdAt: act.createdAt.toISOString(),
};
}
// Touch board
await tx.board.update({
where: { id: sourceBoardId },
data: { updatedAt: new Date() },
});
});
return createdActivity;
}
/**
* Get recent activity logs for a board.
*/
static async getBoardActivityLogs(boardId, limit = 50) {
const logs = await prisma.activityLog.findMany({
where: { boardId },
orderBy: { createdAt: 'desc' },
take: Math.min(100, Math.max(1, limit)),
});
return logs.map((log) => ({
id: log.id,
boardId: log.boardId,
ticketId: log.ticketId,
ticketTitle: log.ticketTitle,
action: log.action,
fromColumnTitle: log.fromColumnTitle,
toColumnTitle: log.toColumnTitle,
userName: log.userName,
details: log.details,
createdAt: log.createdAt.toISOString(),
}));
}
/**
* Get activity logs for a specific ticket.
*/
static async getTicketActivityLogs(ticketId, limit = 30) {
const logs = await prisma.activityLog.findMany({
where: { ticketId },
orderBy: { createdAt: 'desc' },
take: Math.min(50, Math.max(1, limit)),
});
return logs.map((log) => ({
id: log.id,
boardId: log.boardId,
ticketId: log.ticketId,
ticketTitle: log.ticketTitle,
action: log.action,
fromColumnTitle: log.fromColumnTitle,
toColumnTitle: log.toColumnTitle,
userName: log.userName,
details: log.details,
createdAt: log.createdAt.toISOString(),
}));
}
/**
* Reorder columns in a board.
*/
static async reorderColumns(boardId, columnIdsInOrder) {
await prisma.$transaction(async (tx) => {
for (let i = 0; i < columnIdsInOrder.length; i++) {
const id = columnIdsInOrder[i];
await tx.column.update({
where: { id },
data: { order: i },
});
}
await tx.board.update({
where: { id: boardId },
data: { updatedAt: new Date() },
});
});
}
/**
* Check for stale tickets in active columns and auto-move them to Backlog if enabled.
*/
static async processStaleTickets(boardId) {
const board = await prisma.board.findUnique({
where: { id: boardId },
include: {
columns: {
orderBy: { order: 'asc' },
include: { tickets: true },
},
},
});
if (!board || board.columns.length < 2)
return { movedTicketIds: [] };
const backlogColumn = board.columns[0];
const movedTicketIds = [];
const now = Date.now();
for (let i = 1; i < board.columns.length; i++) {
const col = board.columns[i];
if (col.autoStaleHours && col.autoStaleHours > 0) {
const thresholdMs = col.autoStaleHours * 60 * 60 * 1000;
for (const ticket of col.tickets) {
const ticketMovedAt = new Date(ticket.movedAt || ticket.createdAt).getTime();
const ageMs = now - ticketMovedAt;
if (ageMs >= thresholdMs && board.autoMoveStale) {
// Auto move to backlog
await this.moveTicket(ticket.id, backlogColumn.id, 0, [ticket.id, ...backlogColumn.tickets.map((t) => t.id)], 'Sistem (Auto-Stale)');
movedTicketIds.push(ticket.id);
}
}
}
}
return { movedTicketIds };
}
}
+2
View File
@@ -0,0 +1,2 @@
import { Server } from 'socket.io';
export declare function setupSocketHandlers(io: Server): void;
+306
View File
@@ -0,0 +1,306 @@
import { BoardService } from '../services/boardService.js';
import { KanbanService } from '../services/kanbanService.js';
import { LockManager } from './lockManager.js';
const activeUsersPerBoard = new Map();
const RETRO_COLORS = ['#f87171', '#fbbf24', '#34d399', '#60a5fa', '#a78bfa', '#f472b6', '#38bdf8', '#fb923c'];
function getRandomColor(seed) {
let hash = 0;
for (let i = 0; i < seed.length; i++) {
hash = seed.charCodeAt(i) + ((hash << 5) - hash);
}
return RETRO_COLORS[Math.abs(hash) % RETRO_COLORS.length];
}
export function setupSocketHandlers(io) {
// Authentication & Handshake Middleware
io.use(async (socket, next) => {
try {
const auth = socket.handshake.auth;
const boardId = auth.boardId || socket.handshake.query.boardId;
const key = auth.key || socket.handshake.query.key;
const userName = auth.userName?.trim() || `User_${Math.floor(1000 + Math.random() * 9000)}`;
const userId = auth.userId || socket.id;
if (!boardId || !key) {
return next(new Error('AUTHENTICATION_REQUIRED: Missing boardId or key'));
}
const authResult = await BoardService.authenticateKey(boardId, key);
if (!authResult) {
return next(new Error('AUTHENTICATION_FAILED: Invalid key for board'));
}
const color = getRandomColor(userId);
socket.data = {
boardId,
role: authResult.role,
userId,
userName,
color,
};
next();
}
catch (err) {
console.error('[Socket Auth Error]:', err);
next(new Error('INTERNAL_SERVER_ERROR'));
}
});
io.on('connection', (socket) => {
const data = socket.data;
const { boardId, role, userId, userName, color } = data;
const roomName = `board:${boardId}`;
socket.join(roomName);
console.log(`[User Joined] ${userName} (${role}) joined board room ${boardId}`);
// Track active presence
if (!activeUsersPerBoard.has(boardId)) {
activeUsersPerBoard.set(boardId, new Map());
}
const boardUsers = activeUsersPerBoard.get(boardId);
boardUsers.set(socket.id, { socketId: socket.id, userId, userName, color, role });
// Broadcast updated presence list
io.to(roomName).emit('presence_update', Array.from(boardUsers.values()));
// Send active field locks
const activeLocks = LockManager.getActiveLocks();
socket.emit('active_locks', activeLocks);
// ==========================================
// 1. DRAG & DROP / CARD MOVED WITH DB LOGGING
// ==========================================
socket.on('card_moved', async (payload) => {
try {
const { ticketId, targetColumnId, newOrder, ticketIdsInTargetColumn } = payload;
// Broadcast instantly to other clients
socket.to(roomName).emit('card_moved', {
ticketId,
targetColumnId,
newOrder,
ticketIdsInTargetColumn,
movedBy: { userId, userName },
});
// Persist movement and log to database
const activity = await KanbanService.moveTicket(ticketId, targetColumnId, newOrder, ticketIdsInTargetColumn, userName);
if (activity) {
io.to(roomName).emit('activity_logged', { activity });
}
}
catch (err) {
console.error('[card_moved DB error]:', err);
socket.emit('card_move_failed', {
ticketId: payload.ticketId,
error: err.message || 'Failed to move ticket',
});
const freshBoard = await BoardService.getBoardDetails(boardId, role);
if (freshBoard) {
io.to(roomName).emit('board_refreshed', freshBoard);
}
}
});
// ==========================================
// 2. FIELD-LEVEL LOCKING & TEXT EDITING
// ==========================================
socket.on('user_typing_field', (payload) => {
const { ticketId, field } = payload;
const result = LockManager.acquireLock(ticketId, field, socket.id, userId, userName);
if (result.success && result.lock) {
socket.to(roomName).emit('field_locked', {
ticketId,
field,
userId,
userName,
color,
lockedAt: result.lock.lockedAt,
});
}
else if (!result.success && result.currentHolder) {
socket.emit('field_lock_rejected', {
ticketId,
field,
holder: result.currentHolder,
});
}
});
socket.on('update_ticket_text', async (payload) => {
try {
const { ticketId, field, text } = payload;
const updates = {};
if (field === 'title')
updates.title = text;
if (field === 'description')
updates.description = text;
const { ticket: updatedTicket, activity } = await KanbanService.updateTicket(ticketId, updates, userName);
LockManager.releaseLock(ticketId, field, socket.id);
io.to(roomName).emit('field_unlocked', {
ticketId,
field,
updatedTicket,
});
io.to(roomName).emit('ticket_updated', { ticket: updatedTicket });
if (activity) {
io.to(roomName).emit('activity_logged', { activity });
}
}
catch (err) {
console.error('[update_ticket_text error]:', err);
LockManager.releaseLock(payload.ticketId, payload.field, socket.id);
io.to(roomName).emit('field_unlocked', { ticketId: payload.ticketId, field: payload.field });
}
});
socket.on('cancel_typing_field', (payload) => {
const { ticketId, field } = payload;
LockManager.releaseLock(ticketId, field, socket.id);
io.to(roomName).emit('field_unlocked', { ticketId, field });
});
// ==========================================
// 3. TICKET CRUD WITH DUE DATE, SUBTASKS, BLOCKED
// ==========================================
socket.on('create_ticket', async (payload) => {
try {
const { ticket, activity } = await KanbanService.createTicket(payload.columnId, payload.title, payload.description, payload.priority, payload.tags, payload.assignee, payload.dueDate, payload.subtasks, payload.isBlocked, payload.blockedReason, userName);
io.to(roomName).emit('ticket_created', { ticket, createdBy: { userId, userName } });
if (activity) {
io.to(roomName).emit('activity_logged', { activity });
}
}
catch (err) {
console.error('[create_ticket error]:', err);
socket.emit('operation_error', { message: 'Failed to create ticket' });
}
});
socket.on('update_ticket', async (payload) => {
try {
const { ticket, activity } = await KanbanService.updateTicket(payload.ticketId, payload.updates, userName);
io.to(roomName).emit('ticket_updated', { ticket, updatedBy: { userId, userName } });
if (activity) {
io.to(roomName).emit('activity_logged', { activity });
}
}
catch (err) {
console.error('[update_ticket error]:', err);
socket.emit('operation_error', { message: 'Failed to update ticket' });
}
});
socket.on('delete_ticket', async (payload) => {
try {
const { activity } = await KanbanService.deleteTicket(payload.ticketId, userName);
io.to(roomName).emit('ticket_deleted', {
ticketId: payload.ticketId,
columnId: payload.columnId,
deletedBy: { userId, userName },
});
if (activity) {
io.to(roomName).emit('activity_logged', { activity });
}
}
catch (err) {
console.error('[delete_ticket error]:', err);
socket.emit('operation_error', { message: 'Failed to delete ticket' });
}
});
// ==========================================
// 4. COLUMN CRUD & WIP LIMITS
// ==========================================
socket.on('create_column', async (payload) => {
try {
if (role !== 'admin') {
socket.emit('operation_error', { message: 'Only superadmins can create columns' });
return;
}
const column = await KanbanService.createColumn(boardId, payload.title, payload.maxWipLimit || 0, payload.autoStaleHours || 0);
io.to(roomName).emit('column_created', { column });
}
catch (err) {
console.error('[create_column error]:', err);
socket.emit('operation_error', { message: 'Failed to create column' });
}
});
socket.on('update_column', async (payload) => {
try {
if (role !== 'admin') {
socket.emit('operation_error', { message: 'Only superadmins can update columns' });
return;
}
const updatedCol = await KanbanService.updateColumn(payload.columnId, payload.updates);
io.to(roomName).emit('column_updated', { column: updatedCol });
}
catch (err) {
console.error('[update_column error]:', err);
socket.emit('operation_error', { message: 'Failed to update column' });
}
});
socket.on('delete_column', async (payload) => {
try {
if (role !== 'admin') {
socket.emit('operation_error', { message: 'Only superadmins can delete columns' });
return;
}
await KanbanService.deleteColumn(payload.columnId);
io.to(roomName).emit('column_deleted', { columnId: payload.columnId });
}
catch (err) {
console.error('[delete_column error]:', err);
socket.emit('operation_error', { message: 'Failed to delete column' });
}
});
socket.on('reorder_columns', async (payload) => {
try {
if (role !== 'admin') {
socket.emit('operation_error', { message: 'Only superadmins can reorder columns' });
return;
}
socket.to(roomName).emit('columns_reordered', { columnIdsInOrder: payload.columnIdsInOrder });
await KanbanService.reorderColumns(boardId, payload.columnIdsInOrder);
}
catch (err) {
console.error('[reorder_columns error]:', err);
socket.emit('operation_error', { message: 'Failed to reorder columns' });
}
});
// Update Board Settings (autoMoveStale)
socket.on('update_board_settings', async (payload) => {
try {
if (role !== 'admin') {
socket.emit('operation_error', { message: 'Only superadmins can change board settings' });
return;
}
await BoardService.updateBoard(boardId, { autoMoveStale: payload.autoMoveStale });
io.to(roomName).emit('board_settings_updated', { autoMoveStale: payload.autoMoveStale });
}
catch (err) {
console.error('[update_board_settings error]:', err);
}
});
// ==========================================
// 5. MEMBER KEY ROTATION BROADCAST
// ==========================================
socket.on('member_key_rotated', () => {
if (role === 'admin') {
for (const [sId, u] of boardUsers.entries()) {
if (u.role === 'member') {
const memberSocket = io.sockets.sockets.get(sId);
if (memberSocket) {
memberSocket.emit('member_session_revoked', {
message: 'The superadmin regenerated the member invite link. Your session has been revoked.',
});
memberSocket.disconnect(true);
}
}
}
}
});
// ==========================================
// 6. DISCONNECT & CLEANUP
// ==========================================
socket.on('disconnect', () => {
console.log(`[User Left] ${userName} disconnected from board ${boardId}`);
const releasedLocks = LockManager.releaseSocketLocks(socket.id);
for (const lock of releasedLocks) {
io.to(roomName).emit('field_unlocked', { ticketId: lock.ticketId, field: lock.field });
}
const bUsers = activeUsersPerBoard.get(boardId);
if (bUsers) {
bUsers.delete(socket.id);
if (bUsers.size === 0) {
activeUsersPerBoard.delete(boardId);
}
else {
io.to(roomName).emit('presence_update', Array.from(bUsers.values()));
}
}
});
});
}
+27
View File
@@ -0,0 +1,27 @@
import { FieldLockInfo } from '../types/index.js';
export declare class LockManager {
private static locks;
private static LOCK_EXPIRATION_MS;
private static getLockKey;
/**
* Attempt to acquire or refresh a lock.
* Returns { success: boolean, lock?: FieldLockInfo, currentLock?: FieldLockInfo }
*/
static acquireLock(ticketId: string, field: 'title' | 'description', socketId: string, userId: string, userName: string): {
success: boolean;
lock?: FieldLockInfo;
currentHolder?: FieldLockInfo;
};
/**
* Release a lock by ticketId and field.
*/
static releaseLock(ticketId: string, field: 'title' | 'description', socketId?: string): FieldLockInfo | null;
/**
* Release all locks held by a disconnected socket.
*/
static releaseSocketLocks(socketId: string): FieldLockInfo[];
/**
* Get all active (non-expired) locks for a board/tickets.
*/
static getActiveLocks(): FieldLockInfo[];
}
+89
View File
@@ -0,0 +1,89 @@
export class LockManager {
// Map of lockKey (`${ticketId}:${field}`) -> FieldLockInfo
static locks = new Map();
// Auto-expire locks after 15 seconds of inactivity to prevent ghost locks
static LOCK_EXPIRATION_MS = 15000;
static getLockKey(ticketId, field) {
return `${ticketId}:${field}`;
}
/**
* Attempt to acquire or refresh a lock.
* Returns { success: boolean, lock?: FieldLockInfo, currentLock?: FieldLockInfo }
*/
static acquireLock(ticketId, field, socketId, userId, userName) {
const lockKey = this.getLockKey(ticketId, field);
const existing = this.locks.get(lockKey);
const now = Date.now();
// If lock exists and hasn't expired
if (existing && now - existing.lockedAt < this.LOCK_EXPIRATION_MS) {
// If the current socket or user holds it, refresh the timestamp
if (existing.socketId === socketId || existing.userId === userId) {
existing.lockedAt = now;
existing.userName = userName;
return { success: true, lock: existing };
}
// Held by another user
return { success: false, currentHolder: existing };
}
// Lock is either free or expired, acquire it
const newLock = {
ticketId,
field,
socketId,
userId,
userName,
lockedAt: now,
};
this.locks.set(lockKey, newLock);
return { success: true, lock: newLock };
}
/**
* Release a lock by ticketId and field.
*/
static releaseLock(ticketId, field, socketId) {
const lockKey = this.getLockKey(ticketId, field);
const existing = this.locks.get(lockKey);
if (!existing)
return null;
// If socketId specified, verify ownership
if (socketId && existing.socketId !== socketId) {
// If it has expired, allow release
if (Date.now() - existing.lockedAt >= this.LOCK_EXPIRATION_MS) {
this.locks.delete(lockKey);
return existing;
}
return null;
}
this.locks.delete(lockKey);
return existing;
}
/**
* Release all locks held by a disconnected socket.
*/
static releaseSocketLocks(socketId) {
const released = [];
for (const [key, lock] of this.locks.entries()) {
if (lock.socketId === socketId) {
released.push(lock);
this.locks.delete(key);
}
}
return released;
}
/**
* Get all active (non-expired) locks for a board/tickets.
*/
static getActiveLocks() {
const now = Date.now();
const active = [];
for (const [key, lock] of this.locks.entries()) {
if (now - lock.lockedAt < this.LOCK_EXPIRATION_MS) {
active.push(lock);
}
else {
this.locks.delete(key);
}
}
return active;
}
}
+1
View File
@@ -0,0 +1 @@
export {};
+50
View File
@@ -0,0 +1,50 @@
import assert from 'assert';
import { generateSecureKey, hashKey, verifyKeyHash } from '../utils/crypto.js';
import { LockManager } from '../socket/lockManager.js';
async function runTests() {
console.log('🧪 Starting Logic & Security Verification Tests...\n');
// Test 1: Key Generation & SHA-256 Hashing
console.log('Test 1: Key generation and SHA-256 hashing...');
const key1 = generateSecureKey(32);
const key2 = generateSecureKey(32);
assert.notStrictEqual(key1, key2, 'Keys must be uniquely generated');
assert(key1.length > 20, 'Key length should be substantial');
const hash1 = hashKey(key1);
const hash2 = hashKey(key2);
assert.notStrictEqual(hash1, hash2, 'Hashes must differ');
assert.strictEqual(hash1, hashKey(key1), 'Hash function must be deterministic');
assert.strictEqual(verifyKeyHash(key1, hash1), true, 'Key verification must succeed for valid key');
assert.strictEqual(verifyKeyHash('wrong-key', hash1), false, 'Key verification must fail for invalid key');
console.log('✅ Test 1 Passed: Crypto and Hashing work properly!\n');
// Test 2: In-Memory Field-Level Lock Manager
console.log('Test 2: Field-Level Lock Manager & Concurrency...');
const ticketId = 'ticket-uuid-123';
const userA = { socketId: 'sock-A', userId: 'user-A', userName: 'Player One' };
const userB = { socketId: 'sock-B', userId: 'user-B', userName: 'Player Two' };
// User A acquires lock
const resA = LockManager.acquireLock(ticketId, 'description', userA.socketId, userA.userId, userA.userName);
assert.strictEqual(resA.success, true, 'User A should acquire free lock');
assert.strictEqual(resA.lock?.userId, 'user-A');
// User B tries to acquire same lock
const resB = LockManager.acquireLock(ticketId, 'description', userB.socketId, userB.userId, userB.userName);
assert.strictEqual(resB.success, false, 'User B must be rejected while User A holds lock');
assert.strictEqual(resB.currentHolder?.userId, 'user-A');
// User A refreshes lock
const resARefresh = LockManager.acquireLock(ticketId, 'description', userA.socketId, userA.userId, userA.userName);
assert.strictEqual(resARefresh.success, true, 'User A should be able to refresh own lock');
// User A releases lock
const released = LockManager.releaseLock(ticketId, 'description', userA.socketId);
assert.notStrictEqual(released, null, 'Lock should be released');
// Now User B can acquire lock
const resB2 = LockManager.acquireLock(ticketId, 'description', userB.socketId, userB.userId, userB.userName);
assert.strictEqual(resB2.success, true, 'User B should acquire lock after release');
// Socket disconnect cleanup
const cleaned = LockManager.releaseSocketLocks(userB.socketId);
assert.strictEqual(cleaned.length, 1, 'Should clean up 1 lock on disconnect');
console.log('✅ Test 2 Passed: LockManager concurrency & conflict resolution works!\n');
console.log('🎉 All backend logic tests passed successfully!');
}
runTests().catch((err) => {
console.error('❌ Test failed:', err);
process.exit(1);
});
+79
View File
@@ -0,0 +1,79 @@
export type Role = 'admin' | 'member';
export type Priority = 'LOW' | 'MEDIUM' | 'HIGH' | 'URGENT';
export interface Subtask {
id: string;
title: string;
completed: boolean;
}
export interface ActivityLogPayload {
id: string;
boardId: string;
ticketId?: string | null;
ticketTitle: string;
action: 'MOVED' | 'CREATED' | 'BLOCKED' | 'UNBLOCKED' | 'DELETED';
fromColumnTitle?: string | null;
toColumnTitle?: string | null;
userName: string;
details?: string | null;
createdAt: string;
}
export interface BoardCreationResult {
id: string;
title: string;
adminKey: string;
memberKey: string;
adminUrlFragment: string;
memberUrlFragment: string;
createdAt: string;
}
export interface BoardPayload {
id: string;
title: string;
role: Role;
autoMoveStale: boolean;
columns: ColumnPayload[];
createdAt: string;
updatedAt: string;
}
export interface ColumnPayload {
id: string;
boardId: string;
title: string;
order: number;
maxWipLimit: number;
autoStaleHours: number;
tickets: TicketPayload[];
createdAt: string;
updatedAt: string;
}
export interface TicketPayload {
id: string;
columnId: string;
title: string;
description: string;
order: number;
priority: Priority;
tags: string[];
subtasks: Subtask[];
dueDate?: string | null;
isBlocked?: boolean;
blockedReason?: string | null;
assignee?: string | null;
movedAt: string;
createdAt: string;
updatedAt: string;
}
export interface FieldLockInfo {
ticketId: string;
field: 'title' | 'description';
socketId: string;
userId: string;
userName: string;
lockedAt: number;
}
export interface SocketAuthData {
boardId: string;
key: string;
userName?: string;
userId?: string;
}
+1
View File
@@ -0,0 +1 @@
export {};
+12
View File
@@ -0,0 +1,12 @@
/**
* Generate a cryptographically secure random string for authorization keys.
*/
export declare function generateSecureKey(length?: number): string;
/**
* Hash a plain text key with SHA-256 for secure database storage.
*/
export declare function hashKey(key: string): string;
/**
* Constant-time hash verification to prevent timing attacks.
*/
export declare function verifyKeyHash(key: string, storedHash: string): boolean;
+22
View File
@@ -0,0 +1,22 @@
import crypto from 'crypto';
/**
* Generate a cryptographically secure random string for authorization keys.
*/
export function generateSecureKey(length = 32) {
return crypto.randomBytes(length).toString('base64url');
}
/**
* Hash a plain text key with SHA-256 for secure database storage.
*/
export function hashKey(key) {
return crypto.createHash('sha256').update(key.trim()).digest('hex');
}
/**
* Constant-time hash verification to prevent timing attacks.
*/
export function verifyKeyHash(key, storedHash) {
const computedHash = hashKey(key);
if (computedHash.length !== storedHash.length)
return false;
return crypto.timingSafeEqual(Buffer.from(computedHash, 'hex'), Buffer.from(storedHash, 'hex'));
}
+22
View File
@@ -0,0 +1,22 @@
#!/bin/sh
set -e
echo "🚀 [TautKan] Starting container initialization..."
# Default DATABASE_URL if not provided (uses persistent SQLite)
if [ -z "$DATABASE_URL" ] || echo "$DATABASE_URL" | grep -q "file:\|sqlite:"; then
echo "📦 [Database] Using SQLite database ($DATABASE_URL)..."
mkdir -p /app/data
export DATABASE_URL="${DATABASE_URL:-file:/app/data/kanban.db}"
npx prisma generate --schema=prisma/schema.sqlite.prisma
npx prisma db push --schema=prisma/schema.sqlite.prisma --accept-data-loss
else
echo "🐘 [Database] Using PostgreSQL database..."
npx prisma generate --schema=prisma/schema.prisma
npx prisma db push --schema=prisma/schema.prisma --accept-data-loss
fi
echo "✨ [Database] Schema synchronized successfully."
echo "🌐 [Server] Launching TautKan on port ${PORT:-4000}..."
exec node dist/index.js
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../esbuild/bin/esbuild" "$@"
else
exec node "$basedir/../esbuild/bin/esbuild" "$@"
fi
+17
View File
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\esbuild\bin\esbuild" %*
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../esbuild/bin/esbuild" $args
} else {
& "$basedir/node$exe" "$basedir/../esbuild/bin/esbuild" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../esbuild/bin/esbuild" $args
} else {
& "node$exe" "$basedir/../esbuild/bin/esbuild" $args
}
$ret=$LASTEXITCODE
}
exit $ret
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../mime/cli.js" "$@"
else
exec node "$basedir/../mime/cli.js" "$@"
fi
+17
View File
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\mime\cli.js" %*
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../mime/cli.js" $args
} else {
& "$basedir/node$exe" "$basedir/../mime/cli.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../mime/cli.js" $args
} else {
& "node$exe" "$basedir/../mime/cli.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../prisma/build/index.js" "$@"
else
exec node "$basedir/../prisma/build/index.js" "$@"
fi
+17
View File
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\prisma\build\index.js" %*
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../prisma/build/index.js" $args
} else {
& "$basedir/node$exe" "$basedir/../prisma/build/index.js" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../prisma/build/index.js" $args
} else {
& "node$exe" "$basedir/../prisma/build/index.js" $args
}
$ret=$LASTEXITCODE
}
exit $ret
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
else
exec node "$basedir/../typescript/bin/tsc" "$@"
fi
+17
View File
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\typescript\bin\tsc" %*
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../typescript/bin/tsc" $args
} else {
& "$basedir/node$exe" "$basedir/../typescript/bin/tsc" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../typescript/bin/tsc" $args
} else {
& "node$exe" "$basedir/../typescript/bin/tsc" $args
}
$ret=$LASTEXITCODE
}
exit $ret
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
else
exec node "$basedir/../typescript/bin/tsserver" "$@"
fi
+17
View File
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\typescript\bin\tsserver" %*
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../typescript/bin/tsserver" $args
} else {
& "$basedir/node$exe" "$basedir/../typescript/bin/tsserver" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../typescript/bin/tsserver" $args
} else {
& "node$exe" "$basedir/../typescript/bin/tsserver" $args
}
$ret=$LASTEXITCODE
}
exit $ret
+16
View File
@@ -0,0 +1,16 @@
#!/bin/sh
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
case `uname` in
*CYGWIN*|*MINGW*|*MSYS*)
if command -v cygpath > /dev/null 2>&1; then
basedir=`cygpath -w "$basedir"`
fi
;;
esac
if [ -x "$basedir/node" ]; then
exec "$basedir/node" "$basedir/../tsx/dist/cli.mjs" "$@"
else
exec node "$basedir/../tsx/dist/cli.mjs" "$@"
fi
+17
View File
@@ -0,0 +1,17 @@
@ECHO off
GOTO start
:find_dp0
SET dp0=%~dp0
EXIT /b
:start
SETLOCAL
CALL :find_dp0
IF EXIST "%dp0%\node.exe" (
SET "_prog=%dp0%\node.exe"
) ELSE (
SET "_prog=node"
SET PATHEXT=%PATHEXT:;.JS;=;%
)
endLocal & goto #_undefined_# 2>NUL || title %COMSPEC% & "%_prog%" "%dp0%\..\tsx\dist\cli.mjs" %*
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env pwsh
$basedir=Split-Path $MyInvocation.MyCommand.Definition -Parent
$exe=""
if ($PSVersionTable.PSVersion -lt "6.0" -or $IsWindows) {
# Fix case when both the Windows and Linux builds of Node
# are installed in the same directory
$exe=".exe"
}
$ret=0
if (Test-Path "$basedir/node$exe") {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "$basedir/node$exe" "$basedir/../tsx/dist/cli.mjs" $args
} else {
& "$basedir/node$exe" "$basedir/../tsx/dist/cli.mjs" $args
}
$ret=$LASTEXITCODE
} else {
# Support pipeline input
if ($MyInvocation.ExpectingInput) {
$input | & "node$exe" "$basedir/../tsx/dist/cli.mjs" $args
} else {
& "node$exe" "$basedir/../tsx/dist/cli.mjs" $args
}
$ret=$LASTEXITCODE
}
exit $ret
@@ -0,0 +1 @@
b72732897f7fb1d24ea9d26cb254b78b10d4257eaeb27f04d9fb82d20addc5d5
@@ -0,0 +1 @@
7d58cada77c5833e57d2ab4ad61ea2948247b2caa8575066b2fe3bc7e4ea4e5a
@@ -0,0 +1 @@
cfdcce35f151ea8e57772f07fd909b6118389119b76e51ab1105ef86f955048b
@@ -0,0 +1 @@
a7d949e16cc5937aa77d67888c8993118ef16c764e536e9ed7c17cfe61bb65ad
+1862
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
export * from "./index"
+1
View File
@@ -0,0 +1 @@
module.exports = { ...require('.') }
+9
View File
@@ -0,0 +1,9 @@
class PrismaClient {
constructor() {
throw new Error(
'@prisma/client/deno/edge did not initialize yet. Please run "prisma generate" and try to import it again.',
)
}
}
export { PrismaClient }
+1
View File
@@ -0,0 +1 @@
export * from "./default"
+242
View File
File diff suppressed because one or more lines are too long
+234
View File
@@ -0,0 +1,234 @@
Object.defineProperty(exports, "__esModule", { value: true });
const {
Decimal,
objectEnumValues,
makeStrictEnum,
Public,
getRuntime,
skip
} = require('@prisma/client/runtime/index-browser.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 5.22.0
* Query Engine version: 605197351a3c8bdd595af2d2a9bc3025bca48ea2
*/
Prisma.prismaVersion = {
client: "5.22.0",
engine: "605197351a3c8bdd595af2d2a9bc3025bca48ea2"
}
Prisma.PrismaClientKnownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)};
Prisma.PrismaClientUnknownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientRustPanicError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientInitializationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientValidationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.NotFoundError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`NotFoundError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.empty = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.join = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.raw = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.defineExtension = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = objectEnumValues.instances.DbNull
Prisma.JsonNull = objectEnumValues.instances.JsonNull
Prisma.AnyNull = objectEnumValues.instances.AnyNull
Prisma.NullTypes = {
DbNull: objectEnumValues.classes.DbNull,
JsonNull: objectEnumValues.classes.JsonNull,
AnyNull: objectEnumValues.classes.AnyNull
}
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
});
exports.Prisma.BoardScalarFieldEnum = {
id: 'id',
title: 'title',
adminKeyHash: 'adminKeyHash',
memberKeyHash: 'memberKeyHash',
autoMoveStale: 'autoMoveStale',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.ColumnScalarFieldEnum = {
id: 'id',
boardId: 'boardId',
title: 'title',
order: 'order',
maxWipLimit: 'maxWipLimit',
autoStaleHours: 'autoStaleHours',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.TicketScalarFieldEnum = {
id: 'id',
columnId: 'columnId',
title: 'title',
description: 'description',
order: 'order',
priority: 'priority',
tags: 'tags',
subtasks: 'subtasks',
dueDate: 'dueDate',
isBlocked: 'isBlocked',
blockedReason: 'blockedReason',
assignee: 'assignee',
movedAt: 'movedAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.ActivityLogScalarFieldEnum = {
id: 'id',
boardId: 'boardId',
ticketId: 'ticketId',
ticketTitle: 'ticketTitle',
action: 'action',
fromColumnTitle: 'fromColumnTitle',
toColumnTitle: 'toColumnTitle',
userName: 'userName',
details: 'details',
createdAt: 'createdAt'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.QueryMode = {
default: 'default',
insensitive: 'insensitive'
};
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.Priority = exports.$Enums.Priority = {
LOW: 'LOW',
MEDIUM: 'MEDIUM',
HIGH: 'HIGH',
URGENT: 'URGENT'
};
exports.Prisma.ModelName = {
Board: 'Board',
Column: 'Column',
Ticket: 'Ticket',
ActivityLog: 'ActivityLog'
};
/**
* This is a stub Prisma Client that will error at runtime if called.
*/
class PrismaClient {
constructor() {
return new Proxy(this, {
get(target, prop) {
let message
const runtime = getRuntime()
if (runtime.isEdge) {
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
- Use Prisma Accelerate: https://pris.ly/d/accelerate
- Use Driver Adapters: https://pris.ly/d/driver-adapters
`;
} else {
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
}
message += `
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
throw new Error(message)
}
})
}
}
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)
+7860
View File
File diff suppressed because it is too large Load Diff
+263
View File
File diff suppressed because one or more lines are too long
+97
View File
@@ -0,0 +1,97 @@
{
"name": "prisma-client-707de3376002bd6fcf73029f63f5ff56eba5d98740c80eb33d934815fd8d0296",
"main": "index.js",
"types": "index.d.ts",
"browser": "index-browser.js",
"exports": {
"./package.json": "./package.json",
".": {
"require": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"import": {
"node": "./index.js",
"edge-light": "./wasm.js",
"workerd": "./wasm.js",
"worker": "./wasm.js",
"browser": "./index-browser.js",
"default": "./index.js"
},
"default": "./index.js"
},
"./edge": {
"types": "./edge.d.ts",
"require": "./edge.js",
"import": "./edge.js",
"default": "./edge.js"
},
"./react-native": {
"types": "./react-native.d.ts",
"require": "./react-native.js",
"import": "./react-native.js",
"default": "./react-native.js"
},
"./extension": {
"types": "./extension.d.ts",
"require": "./extension.js",
"import": "./extension.js",
"default": "./extension.js"
},
"./index-browser": {
"types": "./index.d.ts",
"require": "./index-browser.js",
"import": "./index-browser.js",
"default": "./index-browser.js"
},
"./index": {
"types": "./index.d.ts",
"require": "./index.js",
"import": "./index.js",
"default": "./index.js"
},
"./wasm": {
"types": "./wasm.d.ts",
"require": "./wasm.js",
"import": "./wasm.js",
"default": "./wasm.js"
},
"./runtime/library": {
"types": "./runtime/library.d.ts",
"require": "./runtime/library.js",
"import": "./runtime/library.js",
"default": "./runtime/library.js"
},
"./runtime/binary": {
"types": "./runtime/binary.d.ts",
"require": "./runtime/binary.js",
"import": "./runtime/binary.js",
"default": "./runtime/binary.js"
},
"./generator-build": {
"require": "./generator-build/index.js",
"import": "./generator-build/index.js",
"default": "./generator-build/index.js"
},
"./sql": {
"require": {
"types": "./sql.d.ts",
"node": "./sql.js",
"default": "./sql.js"
},
"import": {
"types": "./sql.d.ts",
"node": "./sql.mjs",
"default": "./sql.mjs"
},
"default": "./sql.js"
},
"./*": "./*"
},
"version": "5.22.0",
"sideEffects": false
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+73
View File
@@ -0,0 +1,73 @@
// Prisma schema for Local Development (SQLite)
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
model Board {
id String @id @default(uuid())
title String @default("Agile Kanban Board")
adminKeyHash String
memberKeyHash String
autoMoveStale Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
columns Column[]
activityLogs ActivityLog[]
}
model Column {
id String @id @default(uuid())
boardId String
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
title String
order Int @default(0)
maxWipLimit Int @default(0)
autoStaleHours Int @default(0)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
tickets Ticket[]
}
model Ticket {
id String @id @default(uuid())
columnId String
column Column @relation(fields: [columnId], references: [id], onDelete: Cascade)
title String
description String @default("")
order Int @default(0)
priority String @default("MEDIUM")
tags String @default("[]")
subtasks String @default("[]")
dueDate String? @default("")
isBlocked Boolean @default(false)
blockedReason String? @default("")
assignee String? @default("")
movedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
activityLogs ActivityLog[]
}
model ActivityLog {
id String @id @default(uuid())
boardId String
board Board @relation(fields: [boardId], references: [id], onDelete: Cascade)
ticketId String?
ticket Ticket? @relation(fields: [ticketId], references: [id], onDelete: Cascade)
ticketTitle String @default("")
action String @default("MOVED") // MOVED, CREATED, BLOCKED, UNBLOCKED, DELETED
fromColumnTitle String? @default("")
toColumnTitle String? @default("")
userName String @default("User")
details String? @default("")
createdAt DateTime @default(now())
}
+1
View File
@@ -0,0 +1 @@
export * from "./index"
+234
View File
@@ -0,0 +1,234 @@
Object.defineProperty(exports, "__esModule", { value: true });
const {
Decimal,
objectEnumValues,
makeStrictEnum,
Public,
getRuntime,
skip
} = require('@prisma/client/runtime/index-browser.js')
const Prisma = {}
exports.Prisma = Prisma
exports.$Enums = {}
/**
* Prisma Client JS version: 5.22.0
* Query Engine version: 605197351a3c8bdd595af2d2a9bc3025bca48ea2
*/
Prisma.prismaVersion = {
client: "5.22.0",
engine: "605197351a3c8bdd595af2d2a9bc3025bca48ea2"
}
Prisma.PrismaClientKnownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientKnownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)};
Prisma.PrismaClientUnknownRequestError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientUnknownRequestError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientRustPanicError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientRustPanicError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientInitializationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientInitializationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.PrismaClientValidationError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`PrismaClientValidationError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.NotFoundError = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`NotFoundError is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.Decimal = Decimal
/**
* Re-export of sql-template-tag
*/
Prisma.sql = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`sqltag is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.empty = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`empty is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.join = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`join is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.raw = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`raw is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.validator = Public.validator
/**
* Extensions
*/
Prisma.getExtensionContext = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.getExtensionContext is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
Prisma.defineExtension = () => {
const runtimeName = getRuntime().prettyName;
throw new Error(`Extensions.defineExtension is unable to run in this browser environment, or has been bundled for the browser (running in ${runtimeName}).
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report`,
)}
/**
* Shorthand utilities for JSON filtering
*/
Prisma.DbNull = objectEnumValues.instances.DbNull
Prisma.JsonNull = objectEnumValues.instances.JsonNull
Prisma.AnyNull = objectEnumValues.instances.AnyNull
Prisma.NullTypes = {
DbNull: objectEnumValues.classes.DbNull,
JsonNull: objectEnumValues.classes.JsonNull,
AnyNull: objectEnumValues.classes.AnyNull
}
/**
* Enums
*/
exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
ReadUncommitted: 'ReadUncommitted',
ReadCommitted: 'ReadCommitted',
RepeatableRead: 'RepeatableRead',
Serializable: 'Serializable'
});
exports.Prisma.BoardScalarFieldEnum = {
id: 'id',
title: 'title',
adminKeyHash: 'adminKeyHash',
memberKeyHash: 'memberKeyHash',
autoMoveStale: 'autoMoveStale',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.ColumnScalarFieldEnum = {
id: 'id',
boardId: 'boardId',
title: 'title',
order: 'order',
maxWipLimit: 'maxWipLimit',
autoStaleHours: 'autoStaleHours',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.TicketScalarFieldEnum = {
id: 'id',
columnId: 'columnId',
title: 'title',
description: 'description',
order: 'order',
priority: 'priority',
tags: 'tags',
subtasks: 'subtasks',
dueDate: 'dueDate',
isBlocked: 'isBlocked',
blockedReason: 'blockedReason',
assignee: 'assignee',
movedAt: 'movedAt',
createdAt: 'createdAt',
updatedAt: 'updatedAt'
};
exports.Prisma.ActivityLogScalarFieldEnum = {
id: 'id',
boardId: 'boardId',
ticketId: 'ticketId',
ticketTitle: 'ticketTitle',
action: 'action',
fromColumnTitle: 'fromColumnTitle',
toColumnTitle: 'toColumnTitle',
userName: 'userName',
details: 'details',
createdAt: 'createdAt'
};
exports.Prisma.SortOrder = {
asc: 'asc',
desc: 'desc'
};
exports.Prisma.QueryMode = {
default: 'default',
insensitive: 'insensitive'
};
exports.Prisma.NullsOrder = {
first: 'first',
last: 'last'
};
exports.Priority = exports.$Enums.Priority = {
LOW: 'LOW',
MEDIUM: 'MEDIUM',
HIGH: 'HIGH',
URGENT: 'URGENT'
};
exports.Prisma.ModelName = {
Board: 'Board',
Column: 'Column',
Ticket: 'Ticket',
ActivityLog: 'ActivityLog'
};
/**
* This is a stub Prisma Client that will error at runtime if called.
*/
class PrismaClient {
constructor() {
return new Proxy(this, {
get(target, prop) {
let message
const runtime = getRuntime()
if (runtime.isEdge) {
message = `PrismaClient is not configured to run in ${runtime.prettyName}. In order to run Prisma Client on edge runtime, either:
- Use Prisma Accelerate: https://pris.ly/d/accelerate
- Use Driver Adapters: https://pris.ly/d/driver-adapters
`;
} else {
message = 'PrismaClient is unable to run in this browser environment, or has been bundled for the browser (running in `' + runtime.prettyName + '`).'
}
message += `
If this is unexpected, please open an issue: https://pris.ly/prisma-prisma-bug-report`
throw new Error(message)
}
})
}
}
exports.PrismaClient = PrismaClient
Object.assign(exports, Prisma)
+3
View File
@@ -0,0 +1,3 @@
# esbuild
This is the Windows 64-bit binary for esbuild, a JavaScript bundler and minifier. See https://github.com/evanw/esbuild for details.
Binary file not shown.
+20
View File
@@ -0,0 +1,20 @@
{
"name": "@esbuild/win32-x64",
"version": "0.28.2",
"description": "The Windows 64-bit binary for esbuild, a JavaScript bundler.",
"repository": {
"type": "git",
"url": "git+https://github.com/evanw/esbuild.git"
},
"license": "MIT",
"preferUnplugged": true,
"engines": {
"node": ">=18"
},
"os": [
"win32"
],
"cpu": [
"x64"
]
}
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+27
View File
@@ -0,0 +1,27 @@
# Prisma Client &middot; [![npm version](https://img.shields.io/npm/v/@prisma/client.svg?style=flat)](https://www.npmjs.com/package/@prisma/client) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/prisma/prisma/blob/main/CONTRIBUTING.md) [![GitHub license](https://img.shields.io/badge/license-Apache%202-blue)](https://github.com/prisma/prisma/blob/main/LICENSE) [![Discord](https://img.shields.io/discord/937751382725886062?label=Discord)](https://pris.ly/discord)
Prisma Client JS is an **auto-generated query builder** that enables **type-safe** database access and **reduces boilerplate**. You can use it as an alternative to traditional ORMs such as Sequelize, TypeORM or SQL query builders like knex.js.
It is part of the [Prisma](https://www.prisma.io/) ecosystem. Prisma provides database tools for data access, declarative data modeling, schema migrations and visual data management. Learn more in the main [`prisma`](https://github.com/prisma/prisma/) repository or read the [documentation](https://www.prisma.io/docs/).
## Getting started
Follow one of these guides to get started with Prisma Client JS:
- [Quickstart](https://www.prisma.io/docs/getting-started/quickstart) (5 min)
- [Set up a new project with Prisma (SQL migrations)](https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch-sql) (15 min)
- [Set up a new project with Prisma (Prisma Migrate)](https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch-prisma-migrate) (15 min)
- [Add Prisma to an existing project](https://www.prisma.io/docs/getting-started/setup-prisma/add-to-existing-project) (15 min)
Alternatively you can explore the ready-to-run [examples](https://github.com/prisma/prisma-examples/) (REST, GraphQL, gRPC, plain JavaScript and TypeScript demos, ...) or watch the [demo videos](https://www.youtube.com/watch?v=0RhtQgIs-TE&list=PLn2e1F9Rfr6k9PnR_figWOcSHgc_erDr5&index=1) (1-2 min per video).
## Contributing
Refer to our [contribution guidelines](https://github.com/prisma/prisma/blob/main/CONTRIBUTING.md) and [Code of Conduct for contributors](https://github.com/prisma/prisma/blob/main/CODE_OF_CONDUCT.md).
## Tests Status
- Prisma Tests Status:
[![CI](https://github.com/prisma/prisma/actions/workflows/test.yml/badge.svg)](https://github.com/prisma/prisma/actions/workflows/test.yml)
- Ecosystem Tests Status:
[![Actions Status](https://github.com/prisma/ecosystem-tests/workflows/test/badge.svg)](https://github.com/prisma/ecosystem-tests/actions)
+1
View File
@@ -0,0 +1 @@
export * from '.prisma/client/default'
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
...require('.prisma/client/default'),
}
+1
View File
@@ -0,0 +1 @@
export * from '.prisma/client/edge'
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
// https://github.com/prisma/prisma/pull/12907
...require('.prisma/client/edge'),
}
+1
View File
@@ -0,0 +1 @@
export * from './scripts/default-index'
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
// https://github.com/prisma/prisma/pull/12907
...require('./scripts/default-index'),
}
File diff suppressed because it is too large Load Diff
+3
View File
@@ -0,0 +1,3 @@
const prisma = require('.prisma/client/index-browser')
module.exports = prisma
+1
View File
@@ -0,0 +1 @@
export * from '.prisma/client/default'
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
// https://github.com/prisma/prisma/pull/12907
...require('.prisma/client/default'),
}
+280
View File
@@ -0,0 +1,280 @@
{
"name": "@prisma/client",
"version": "5.22.0",
"description": "Prisma Client is an auto-generated, type-safe and modern JavaScript/TypeScript ORM for Node.js that's tailored to your data. Supports PostgreSQL, CockroachDB, MySQL, MariaDB, SQL Server, SQLite & MongoDB databases.",
"keywords": [
"ORM",
"Prisma",
"prisma2",
"Prisma Client",
"client",
"query",
"query-builder",
"database",
"db",
"JavaScript",
"JS",
"TypeScript",
"TS",
"SQL",
"SQLite",
"pg",
"Postgres",
"PostgreSQL",
"CockroachDB",
"MySQL",
"MariaDB",
"MSSQL",
"SQL Server",
"SQLServer",
"MongoDB",
"react-native"
],
"main": "default.js",
"types": "default.d.ts",
"browser": "index-browser.js",
"exports": {
"./package.json": "./package.json",
".": {
"require": {
"types": "./default.d.ts",
"node": "./default.js",
"edge-light": "./default.js",
"workerd": "./default.js",
"worker": "./default.js",
"browser": "./index-browser.js"
},
"import": {
"types": "./default.d.ts",
"node": "./default.js",
"edge-light": "./default.js",
"workerd": "./default.js",
"worker": "./default.js",
"browser": "./index-browser.js"
},
"default": "./default.js"
},
"./edge": {
"types": "./edge.d.ts",
"require": "./edge.js",
"import": "./edge.js",
"default": "./edge.js"
},
"./react-native": {
"types": "./react-native.d.ts",
"require": "./react-native.js",
"import": "./react-native.js",
"default": "./react-native.js"
},
"./extension": {
"types": "./extension.d.ts",
"require": "./extension.js",
"import": "./extension.js",
"default": "./extension.js"
},
"./index-browser": {
"types": "./index.d.ts",
"require": "./index-browser.js",
"import": "./index-browser.js",
"default": "./index-browser.js"
},
"./index": {
"types": "./index.d.ts",
"require": "./index.js",
"import": "./index.js",
"default": "./index.js"
},
"./wasm": {
"types": "./wasm.d.ts",
"require": "./wasm.js",
"import": "./wasm.js",
"default": "./wasm.js"
},
"./runtime/library": {
"types": "./runtime/library.d.ts",
"require": "./runtime/library.js",
"import": "./runtime/library.js",
"default": "./runtime/library.js"
},
"./runtime/binary": {
"types": "./runtime/binary.d.ts",
"require": "./runtime/binary.js",
"import": "./runtime/binary.js",
"default": "./runtime/binary.js"
},
"./generator-build": {
"require": "./generator-build/index.js",
"import": "./generator-build/index.js",
"default": "./generator-build/index.js"
},
"./sql": {
"require": {
"types": "./sql.d.ts",
"node": "./sql.js",
"default": "./sql.js"
},
"import": {
"types": "./sql.d.ts",
"node": "./sql.mjs",
"default": "./sql.mjs"
},
"default": "./sql.js"
},
"./*": "./*"
},
"license": "Apache-2.0",
"engines": {
"node": ">=16.13"
},
"homepage": "https://www.prisma.io",
"repository": {
"type": "git",
"url": "https://github.com/prisma/prisma.git",
"directory": "packages/client"
},
"author": "Tim Suchanek <suchanek@prisma.io>",
"bugs": "https://github.com/prisma/prisma/issues",
"files": [
"README.md",
"runtime",
"!runtime/*.map",
"scripts",
"generator-build",
"edge.js",
"edge.d.ts",
"wasm.js",
"wasm.d.ts",
"index.js",
"index.d.ts",
"react-native.js",
"react-native.d.ts",
"default.js",
"default.d.ts",
"index-browser.js",
"extension.js",
"extension.d.ts",
"sql.d.ts",
"sql.js",
"sql.mjs"
],
"devDependencies": {
"@cloudflare/workers-types": "4.20240614.0",
"@codspeed/benchmark.js-plugin": "3.1.1",
"@faker-js/faker": "8.4.1",
"@fast-check/jest": "1.8.2",
"@inquirer/prompts": "5.0.5",
"@jest/create-cache-key-function": "29.7.0",
"@jest/globals": "29.7.0",
"@jest/test-sequencer": "29.7.0",
"@libsql/client": "0.8.0",
"@neondatabase/serverless": "0.9.3",
"@opentelemetry/api": "1.9.0",
"@opentelemetry/context-async-hooks": "1.25.1",
"@opentelemetry/instrumentation": "0.52.1",
"@opentelemetry/resources": "1.25.1",
"@opentelemetry/sdk-trace-base": "1.25.1",
"@opentelemetry/semantic-conventions": "1.25.1",
"@planetscale/database": "1.18.0",
"@prisma/engines-version": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
"@prisma/mini-proxy": "0.9.5",
"@prisma/query-engine-wasm": "5.22.0-44.605197351a3c8bdd595af2d2a9bc3025bca48ea2",
"@snaplet/copycat": "0.17.3",
"@swc-node/register": "1.10.9",
"@swc/core": "1.6.13",
"@swc/jest": "0.2.36",
"@timsuchanek/copy": "1.4.5",
"@types/debug": "4.1.12",
"@types/fs-extra": "9.0.13",
"@types/jest": "29.5.12",
"@types/js-levenshtein": "1.1.3",
"@types/mssql": "9.1.5",
"@types/node": "18.19.31",
"@types/pg": "8.11.6",
"arg": "5.0.2",
"benchmark": "2.1.4",
"ci-info": "4.0.0",
"decimal.js": "10.4.3",
"detect-runtime": "1.0.4",
"env-paths": "2.2.1",
"esbuild": "0.23.0",
"execa": "5.1.1",
"expect-type": "0.19.0",
"flat-map-polyfill": "0.3.8",
"fs-extra": "11.1.1",
"get-stream": "6.0.1",
"globby": "11.1.0",
"indent-string": "4.0.0",
"jest": "29.7.0",
"jest-extended": "4.0.2",
"jest-junit": "16.0.0",
"jest-serializer-ansi-escapes": "3.0.0",
"jest-snapshot": "29.7.0",
"js-levenshtein": "1.1.6",
"kleur": "4.1.5",
"klona": "2.0.6",
"mariadb": "3.3.1",
"memfs": "4.9.3",
"mssql": "11.0.1",
"new-github-issue-url": "0.2.1",
"node-fetch": "3.3.2",
"p-retry": "4.6.2",
"pg": "8.11.5",
"pkg-up": "3.1.0",
"pluralize": "8.0.0",
"resolve": "1.22.8",
"rimraf": "3.0.2",
"simple-statistics": "7.8.5",
"sort-keys": "4.2.0",
"source-map-support": "0.5.21",
"sql-template-tag": "5.2.1",
"stacktrace-parser": "0.1.10",
"strip-ansi": "6.0.1",
"strip-indent": "3.0.0",
"ts-node": "10.9.2",
"ts-pattern": "5.2.0",
"tsd": "0.31.1",
"typescript": "5.4.5",
"undici": "5.28.4",
"wrangler": "3.62.0",
"zx": "7.2.3",
"@prisma/adapter-d1": "5.22.0",
"@prisma/adapter-libsql": "5.22.0",
"@prisma/adapter-neon": "5.22.0",
"@prisma/adapter-pg": "5.22.0",
"@prisma/adapter-planetscale": "5.22.0",
"@prisma/driver-adapter-utils": "5.22.0",
"@prisma/adapter-pg-worker": "5.22.0",
"@prisma/debug": "5.22.0",
"@prisma/engines": "5.22.0",
"@prisma/fetch-engine": "5.22.0",
"@prisma/generator-helper": "5.22.0",
"@prisma/get-platform": "5.22.0",
"@prisma/instrumentation": "5.22.0",
"@prisma/internals": "5.22.0",
"@prisma/migrate": "5.22.0",
"@prisma/pg-worker": "5.22.0"
},
"peerDependencies": {
"prisma": "*"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
}
},
"sideEffects": false,
"scripts": {
"dev": "DEV=true tsx helpers/build.ts",
"build": "tsx helpers/build.ts",
"test": "dotenv -e ../../.db.env -- jest --silent",
"test:e2e": "dotenv -e ../../.db.env -- tsx tests/e2e/_utils/run.ts",
"test:functional": "dotenv -e ../../.db.env -- tsx helpers/functional-test/run-tests.ts",
"test:memory": "dotenv -e ../../.db.env -- tsx helpers/memory-tests.ts",
"test:functional:code": "dotenv -e ../../.db.env -- tsx helpers/functional-test/run-tests.ts --no-types",
"test:functional:types": "dotenv -e ../../.db.env -- tsx helpers/functional-test/run-tests.ts --types-only",
"test-notypes": "dotenv -e ../../.db.env -- jest --testPathIgnorePatterns src/__tests__/types/types.test.ts",
"generate": "node scripts/postinstall.js",
"postinstall": "node scripts/postinstall.js",
"new-test": "tsx ./helpers/new-test/new-test.ts"
}
}
+1
View File
@@ -0,0 +1 @@
export * from '.prisma/client/react-native'
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
...require('.prisma/client/react-native'),
}
+1
View File
@@ -0,0 +1 @@
export * from "./library"
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+365
View File
@@ -0,0 +1,365 @@
declare class AnyNull extends NullTypesEnumValue {
}
declare type Args<T, F extends Operation> = T extends {
[K: symbol]: {
types: {
operations: {
[K in F]: {
args: any;
};
};
};
};
} ? T[symbol]['types']['operations'][F]['args'] : any;
declare class DbNull extends NullTypesEnumValue {
}
export declare namespace Decimal {
export type Constructor = typeof Decimal;
export type Instance = Decimal;
export type Rounding = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
export type Modulo = Rounding | 9;
export type Value = string | number | Decimal;
// http://mikemcl.github.io/decimal.js/#constructor-properties
export interface Config {
precision?: number;
rounding?: Rounding;
toExpNeg?: number;
toExpPos?: number;
minE?: number;
maxE?: number;
crypto?: boolean;
modulo?: Modulo;
defaults?: boolean;
}
}
export declare class Decimal {
readonly d: number[];
readonly e: number;
readonly s: number;
constructor(n: Decimal.Value);
absoluteValue(): Decimal;
abs(): Decimal;
ceil(): Decimal;
clampedTo(min: Decimal.Value, max: Decimal.Value): Decimal;
clamp(min: Decimal.Value, max: Decimal.Value): Decimal;
comparedTo(n: Decimal.Value): number;
cmp(n: Decimal.Value): number;
cosine(): Decimal;
cos(): Decimal;
cubeRoot(): Decimal;
cbrt(): Decimal;
decimalPlaces(): number;
dp(): number;
dividedBy(n: Decimal.Value): Decimal;
div(n: Decimal.Value): Decimal;
dividedToIntegerBy(n: Decimal.Value): Decimal;
divToInt(n: Decimal.Value): Decimal;
equals(n: Decimal.Value): boolean;
eq(n: Decimal.Value): boolean;
floor(): Decimal;
greaterThan(n: Decimal.Value): boolean;
gt(n: Decimal.Value): boolean;
greaterThanOrEqualTo(n: Decimal.Value): boolean;
gte(n: Decimal.Value): boolean;
hyperbolicCosine(): Decimal;
cosh(): Decimal;
hyperbolicSine(): Decimal;
sinh(): Decimal;
hyperbolicTangent(): Decimal;
tanh(): Decimal;
inverseCosine(): Decimal;
acos(): Decimal;
inverseHyperbolicCosine(): Decimal;
acosh(): Decimal;
inverseHyperbolicSine(): Decimal;
asinh(): Decimal;
inverseHyperbolicTangent(): Decimal;
atanh(): Decimal;
inverseSine(): Decimal;
asin(): Decimal;
inverseTangent(): Decimal;
atan(): Decimal;
isFinite(): boolean;
isInteger(): boolean;
isInt(): boolean;
isNaN(): boolean;
isNegative(): boolean;
isNeg(): boolean;
isPositive(): boolean;
isPos(): boolean;
isZero(): boolean;
lessThan(n: Decimal.Value): boolean;
lt(n: Decimal.Value): boolean;
lessThanOrEqualTo(n: Decimal.Value): boolean;
lte(n: Decimal.Value): boolean;
logarithm(n?: Decimal.Value): Decimal;
log(n?: Decimal.Value): Decimal;
minus(n: Decimal.Value): Decimal;
sub(n: Decimal.Value): Decimal;
modulo(n: Decimal.Value): Decimal;
mod(n: Decimal.Value): Decimal;
naturalExponential(): Decimal;
exp(): Decimal;
naturalLogarithm(): Decimal;
ln(): Decimal;
negated(): Decimal;
neg(): Decimal;
plus(n: Decimal.Value): Decimal;
add(n: Decimal.Value): Decimal;
precision(includeZeros?: boolean): number;
sd(includeZeros?: boolean): number;
round(): Decimal;
sine() : Decimal;
sin() : Decimal;
squareRoot(): Decimal;
sqrt(): Decimal;
tangent() : Decimal;
tan() : Decimal;
times(n: Decimal.Value): Decimal;
mul(n: Decimal.Value) : Decimal;
toBinary(significantDigits?: number): string;
toBinary(significantDigits: number, rounding: Decimal.Rounding): string;
toDecimalPlaces(decimalPlaces?: number): Decimal;
toDecimalPlaces(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
toDP(decimalPlaces?: number): Decimal;
toDP(decimalPlaces: number, rounding: Decimal.Rounding): Decimal;
toExponential(decimalPlaces?: number): string;
toExponential(decimalPlaces: number, rounding: Decimal.Rounding): string;
toFixed(decimalPlaces?: number): string;
toFixed(decimalPlaces: number, rounding: Decimal.Rounding): string;
toFraction(max_denominator?: Decimal.Value): Decimal[];
toHexadecimal(significantDigits?: number): string;
toHexadecimal(significantDigits: number, rounding: Decimal.Rounding): string;
toHex(significantDigits?: number): string;
toHex(significantDigits: number, rounding?: Decimal.Rounding): string;
toJSON(): string;
toNearest(n: Decimal.Value, rounding?: Decimal.Rounding): Decimal;
toNumber(): number;
toOctal(significantDigits?: number): string;
toOctal(significantDigits: number, rounding: Decimal.Rounding): string;
toPower(n: Decimal.Value): Decimal;
pow(n: Decimal.Value): Decimal;
toPrecision(significantDigits?: number): string;
toPrecision(significantDigits: number, rounding: Decimal.Rounding): string;
toSignificantDigits(significantDigits?: number): Decimal;
toSignificantDigits(significantDigits: number, rounding: Decimal.Rounding): Decimal;
toSD(significantDigits?: number): Decimal;
toSD(significantDigits: number, rounding: Decimal.Rounding): Decimal;
toString(): string;
truncated(): Decimal;
trunc(): Decimal;
valueOf(): string;
static abs(n: Decimal.Value): Decimal;
static acos(n: Decimal.Value): Decimal;
static acosh(n: Decimal.Value): Decimal;
static add(x: Decimal.Value, y: Decimal.Value): Decimal;
static asin(n: Decimal.Value): Decimal;
static asinh(n: Decimal.Value): Decimal;
static atan(n: Decimal.Value): Decimal;
static atanh(n: Decimal.Value): Decimal;
static atan2(y: Decimal.Value, x: Decimal.Value): Decimal;
static cbrt(n: Decimal.Value): Decimal;
static ceil(n: Decimal.Value): Decimal;
static clamp(n: Decimal.Value, min: Decimal.Value, max: Decimal.Value): Decimal;
static clone(object?: Decimal.Config): Decimal.Constructor;
static config(object: Decimal.Config): Decimal.Constructor;
static cos(n: Decimal.Value): Decimal;
static cosh(n: Decimal.Value): Decimal;
static div(x: Decimal.Value, y: Decimal.Value): Decimal;
static exp(n: Decimal.Value): Decimal;
static floor(n: Decimal.Value): Decimal;
static hypot(...n: Decimal.Value[]): Decimal;
static isDecimal(object: any): object is Decimal;
static ln(n: Decimal.Value): Decimal;
static log(n: Decimal.Value, base?: Decimal.Value): Decimal;
static log2(n: Decimal.Value): Decimal;
static log10(n: Decimal.Value): Decimal;
static max(...n: Decimal.Value[]): Decimal;
static min(...n: Decimal.Value[]): Decimal;
static mod(x: Decimal.Value, y: Decimal.Value): Decimal;
static mul(x: Decimal.Value, y: Decimal.Value): Decimal;
static noConflict(): Decimal.Constructor; // Browser only
static pow(base: Decimal.Value, exponent: Decimal.Value): Decimal;
static random(significantDigits?: number): Decimal;
static round(n: Decimal.Value): Decimal;
static set(object: Decimal.Config): Decimal.Constructor;
static sign(n: Decimal.Value): number;
static sin(n: Decimal.Value): Decimal;
static sinh(n: Decimal.Value): Decimal;
static sqrt(n: Decimal.Value): Decimal;
static sub(x: Decimal.Value, y: Decimal.Value): Decimal;
static sum(...n: Decimal.Value[]): Decimal;
static tan(n: Decimal.Value): Decimal;
static tanh(n: Decimal.Value): Decimal;
static trunc(n: Decimal.Value): Decimal;
static readonly default?: Decimal.Constructor;
static readonly Decimal?: Decimal.Constructor;
static readonly precision: number;
static readonly rounding: Decimal.Rounding;
static readonly toExpNeg: number;
static readonly toExpPos: number;
static readonly minE: number;
static readonly maxE: number;
static readonly crypto: boolean;
static readonly modulo: Decimal.Modulo;
static readonly ROUND_UP: 0;
static readonly ROUND_DOWN: 1;
static readonly ROUND_CEIL: 2;
static readonly ROUND_FLOOR: 3;
static readonly ROUND_HALF_UP: 4;
static readonly ROUND_HALF_DOWN: 5;
static readonly ROUND_HALF_EVEN: 6;
static readonly ROUND_HALF_CEIL: 7;
static readonly ROUND_HALF_FLOOR: 8;
static readonly EUCLID: 9;
}
declare type Exact<A, W> = (A extends unknown ? (W extends A ? {
[K in keyof A]: Exact<A[K], W[K]>;
} : W) : never) | (A extends Narrowable ? A : never);
export declare function getRuntime(): GetRuntimeOutput;
declare type GetRuntimeOutput = {
id: Runtime;
prettyName: string;
isEdge: boolean;
};
declare class JsonNull extends NullTypesEnumValue {
}
/**
* Generates more strict variant of an enum which, unlike regular enum,
* throws on non-existing property access. This can be useful in following situations:
* - we have an API, that accepts both `undefined` and `SomeEnumType` as an input
* - enum values are generated dynamically from DMMF.
*
* In that case, if using normal enums and no compile-time typechecking, using non-existing property
* will result in `undefined` value being used, which will be accepted. Using strict enum
* in this case will help to have a runtime exception, telling you that you are probably doing something wrong.
*
* Note: if you need to check for existence of a value in the enum you can still use either
* `in` operator or `hasOwnProperty` function.
*
* @param definition
* @returns
*/
export declare function makeStrictEnum<T extends Record<PropertyKey, string | number>>(definition: T): T;
declare type Narrowable = string | number | bigint | boolean | [];
declare class NullTypesEnumValue extends ObjectEnumValue {
_getNamespace(): string;
}
/**
* Base class for unique values of object-valued enums.
*/
declare abstract class ObjectEnumValue {
constructor(arg?: symbol);
abstract _getNamespace(): string;
_getName(): string;
toString(): string;
}
export declare const objectEnumValues: {
classes: {
DbNull: typeof DbNull;
JsonNull: typeof JsonNull;
AnyNull: typeof AnyNull;
};
instances: {
DbNull: DbNull;
JsonNull: JsonNull;
AnyNull: AnyNull;
};
};
declare type Operation = 'findFirst' | 'findFirstOrThrow' | 'findUnique' | 'findUniqueOrThrow' | 'findMany' | 'create' | 'createMany' | 'createManyAndReturn' | 'update' | 'updateMany' | 'upsert' | 'delete' | 'deleteMany' | 'aggregate' | 'count' | 'groupBy' | '$queryRaw' | '$executeRaw' | '$queryRawUnsafe' | '$executeRawUnsafe' | 'findRaw' | 'aggregateRaw' | '$runCommandRaw';
declare namespace Public {
export {
validator
}
}
export { Public }
declare type Runtime = "edge-routine" | "workerd" | "deno" | "lagon" | "react-native" | "netlify" | "electron" | "node" | "bun" | "edge-light" | "fastly" | "unknown";
declare function validator<V>(): <S>(select: Exact<S, V>) => S;
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation>(client: C, model: M, operation: O): <S>(select: Exact<S, Args<C[M], O>>) => S;
declare function validator<C, M extends Exclude<keyof C, `$${string}`>, O extends keyof C[M] & Operation, P extends keyof Args<C[M], O>>(client: C, model: M, operation: O, prop: P): <S>(select: Exact<S, Args<C[M], O>[P]>) => S;
export { }
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More