first init
This commit is contained in:
+10
@@ -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
|
||||
@@ -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
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { Express } from 'express';
|
||||
export declare function createApp(): Express;
|
||||
Vendored
+58
@@ -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;
|
||||
}
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
export declare const config: {
|
||||
port: number;
|
||||
nodeEnv: string;
|
||||
clientOrigin: string;
|
||||
databaseUrl: string;
|
||||
rateLimitMaxBoardsPerHour: number;
|
||||
};
|
||||
Vendored
+9
@@ -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
@@ -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
@@ -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' });
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+9
@@ -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;
|
||||
Vendored
+51
@@ -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);
|
||||
});
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Vendored
+22
@@ -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}`);
|
||||
});
|
||||
Vendored
+15
@@ -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>;
|
||||
Vendored
+55
@@ -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
@@ -0,0 +1 @@
|
||||
export declare const createBoardLimiter: import("express-rate-limit").RateLimitRequestHandler;
|
||||
Vendored
+12
@@ -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.`,
|
||||
},
|
||||
});
|
||||
Vendored
+2
@@ -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>;
|
||||
Vendored
+8
@@ -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;
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
declare const router: import("express-serve-static-core").Router;
|
||||
export default router;
|
||||
Vendored
+20
@@ -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
@@ -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>;
|
||||
}
|
||||
Vendored
+169
@@ -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
@@ -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
@@ -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 };
|
||||
}
|
||||
}
|
||||
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { Server } from 'socket.io';
|
||||
export declare function setupSocketHandlers(io: Server): void;
|
||||
Vendored
+306
@@ -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()));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
Vendored
+27
@@ -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[];
|
||||
}
|
||||
Vendored
+89
@@ -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;
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Vendored
+50
@@ -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);
|
||||
});
|
||||
Vendored
+79
@@ -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;
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {};
|
||||
Vendored
+12
@@ -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;
|
||||
Vendored
+22
@@ -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'));
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
b72732897f7fb1d24ea9d26cb254b78b10d4257eaeb27f04d9fb82d20addc5d5
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
7d58cada77c5833e57d2ab4ad61ea2948247b2caa8575066b2fe3bc7e4ea4e5a
|
||||
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
cfdcce35f151ea8e57772f07fd909b6118389119b76e51ab1105ef86f955048b
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
a7d949e16cc5937aa77d67888c8993118ef16c764e536e9ed7c17cfe61bb65ad
|
||||
+1862
File diff suppressed because it is too large
Load Diff
+1
@@ -0,0 +1 @@
|
||||
export * from "./index"
|
||||
+1
@@ -0,0 +1 @@
|
||||
module.exports = { ...require('.') }
|
||||
+9
@@ -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
@@ -0,0 +1 @@
|
||||
export * from "./default"
|
||||
+242
File diff suppressed because one or more lines are too long
+234
@@ -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
File diff suppressed because it is too large
Load Diff
+263
File diff suppressed because one or more lines are too long
+97
@@ -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
|
||||
}
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+73
@@ -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
@@ -0,0 +1 @@
|
||||
export * from "./index"
|
||||
+234
@@ -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
@@ -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.
|
||||
BIN
Binary file not shown.
+20
@@ -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
@@ -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
@@ -0,0 +1,27 @@
|
||||
# Prisma Client · [](https://www.npmjs.com/package/@prisma/client) [](https://github.com/prisma/prisma/blob/main/CONTRIBUTING.md) [](https://github.com/prisma/prisma/blob/main/LICENSE) [](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:
|
||||
[](https://github.com/prisma/prisma/actions/workflows/test.yml)
|
||||
- Ecosystem Tests Status:
|
||||
[](https://github.com/prisma/ecosystem-tests/actions)
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from '.prisma/client/default'
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
...require('.prisma/client/default'),
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from '.prisma/client/edge'
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
// https://github.com/prisma/prisma/pull/12907
|
||||
...require('.prisma/client/edge'),
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from './scripts/default-index'
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
// https://github.com/prisma/prisma/pull/12907
|
||||
...require('./scripts/default-index'),
|
||||
}
|
||||
+10351
File diff suppressed because it is too large
Load Diff
+3
@@ -0,0 +1,3 @@
|
||||
const prisma = require('.prisma/client/index-browser')
|
||||
|
||||
module.exports = prisma
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from '.prisma/client/default'
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
// https://github.com/prisma/prisma/pull/12907
|
||||
...require('.prisma/client/default'),
|
||||
}
|
||||
+280
@@ -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
@@ -0,0 +1 @@
|
||||
export * from '.prisma/client/react-native'
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
...require('.prisma/client/react-native'),
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export * from "./library"
|
||||
+210
File diff suppressed because one or more lines are too long
+31
File diff suppressed because one or more lines are too long
+31
File diff suppressed because one or more lines are too long
+365
@@ -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 { }
|
||||
+13
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
Reference in New Issue
Block a user