mirror of https://github.com/buster-so/buster.git
Implement shortcuts feature in chat and API routes
- Added shortcuts routes to the API. - Enhanced chat handler to process shortcuts in user prompts. - Introduced shortcuts table schema in the database. - Updated relevant queries and shared types to include shortcuts. - Configured environment variables for database connection.
This commit is contained in:
parent
7159e7e8ea
commit
1b3150f466
|
@ -9,6 +9,10 @@ import {
|
|||
import { tasks } from '@trigger.dev/sdk';
|
||||
import { handleAssetChat, handleAssetChatWithPrompt } from './services/chat-helpers';
|
||||
import { initializeChat } from './services/chat-service';
|
||||
import {
|
||||
enhanceMessageWithShortcut,
|
||||
enhanceMessageWithShortcutId,
|
||||
} from './services/shortcut-service';
|
||||
|
||||
/**
|
||||
* Handler function for creating a new chat.
|
||||
|
@ -56,13 +60,37 @@ export async function createChatHandler(
|
|||
throw new ChatError(ChatErrorCode.INVALID_REQUEST, 'prompt or asset_id is required', 400);
|
||||
}
|
||||
|
||||
// Process shortcuts if present
|
||||
let enhancedPrompt = request.prompt;
|
||||
if (request.prompt) {
|
||||
// Check for shortcut ID first (explicit shortcut reference)
|
||||
if (request.shortcut_id) {
|
||||
enhancedPrompt = await enhanceMessageWithShortcutId(
|
||||
request.prompt,
|
||||
request.shortcut_id,
|
||||
user.id,
|
||||
organizationId
|
||||
);
|
||||
} else {
|
||||
// Check for shortcut pattern in message (e.g., /weekly-report)
|
||||
enhancedPrompt = await enhanceMessageWithShortcut(request.prompt, user.id, organizationId);
|
||||
}
|
||||
}
|
||||
|
||||
// Update request with enhanced prompt
|
||||
const processedRequest = { ...request, prompt: enhancedPrompt };
|
||||
|
||||
// Initialize chat (new or existing)
|
||||
// When we have both asset and prompt, we'll skip creating the initial message
|
||||
// since handleAssetChatWithPrompt will create both the import and prompt messages
|
||||
const shouldCreateInitialMessage = !(request.asset_id && request.asset_type && request.prompt);
|
||||
const shouldCreateInitialMessage = !(
|
||||
processedRequest.asset_id &&
|
||||
processedRequest.asset_type &&
|
||||
processedRequest.prompt
|
||||
);
|
||||
const modifiedRequest = shouldCreateInitialMessage
|
||||
? request
|
||||
: { ...request, prompt: undefined };
|
||||
? processedRequest
|
||||
: { ...processedRequest, prompt: undefined };
|
||||
|
||||
const { chatId, messageId, chat } = await initializeChat(modifiedRequest, user, organizationId);
|
||||
|
||||
|
@ -71,14 +99,14 @@ export async function createChatHandler(
|
|||
let actualMessageId = messageId; // Track the actual message ID to use for triggering
|
||||
let shouldTriggerAnalyst = true; // Flag to control whether to trigger analyst task
|
||||
|
||||
if (request.asset_id && request.asset_type) {
|
||||
if (!request.prompt) {
|
||||
if (processedRequest.asset_id && processedRequest.asset_type) {
|
||||
if (!processedRequest.prompt) {
|
||||
// Original flow: just import the asset without a prompt
|
||||
finalChat = await handleAssetChat(
|
||||
chatId,
|
||||
messageId,
|
||||
request.asset_id,
|
||||
request.asset_type,
|
||||
processedRequest.asset_id,
|
||||
processedRequest.asset_type,
|
||||
user,
|
||||
chat
|
||||
);
|
||||
|
@ -89,9 +117,9 @@ export async function createChatHandler(
|
|||
finalChat = await handleAssetChatWithPrompt(
|
||||
chatId,
|
||||
messageId,
|
||||
request.asset_id,
|
||||
request.asset_type,
|
||||
request.prompt,
|
||||
processedRequest.asset_id,
|
||||
processedRequest.asset_type,
|
||||
processedRequest.prompt,
|
||||
user,
|
||||
chat
|
||||
);
|
||||
|
@ -104,7 +132,7 @@ export async function createChatHandler(
|
|||
}
|
||||
|
||||
// Trigger background analysis only if we have a prompt or it's not an asset-only request
|
||||
if (shouldTriggerAnalyst && (request.prompt || !request.asset_id)) {
|
||||
if (shouldTriggerAnalyst && (processedRequest.prompt || !processedRequest.asset_id)) {
|
||||
try {
|
||||
// Just queue the background job - should be <100ms
|
||||
const taskHandle = await tasks.trigger(
|
||||
|
@ -146,8 +174,8 @@ export async function createChatHandler(
|
|||
target: '500ms',
|
||||
user,
|
||||
chatId,
|
||||
hasPrompt: !!request.prompt,
|
||||
hasAsset: !!request.asset_id,
|
||||
hasPrompt: !!processedRequest.prompt,
|
||||
hasAsset: !!processedRequest.asset_id,
|
||||
suggestion: 'Check database performance and trigger service',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import { findShortcutByName, getShortcutById } from '@buster/database';
|
||||
import { ChatError, ChatErrorCode } from '@buster/server-shared/chats';
|
||||
|
||||
/**
|
||||
* Parse a message to check if it starts with a shortcut pattern
|
||||
* @param message The user's message
|
||||
* @returns Object with shortcut name and additional context
|
||||
*/
|
||||
export function parseShortcutFromMessage(message: string): {
|
||||
shortcutName: string | null;
|
||||
additionalContext: string;
|
||||
} {
|
||||
// Match lowercase shortcut names with hyphens (e.g., /weekly-report)
|
||||
const match = message.match(/^\/([a-z][a-z0-9-]*)\s?(.*)/);
|
||||
|
||||
if (!match) {
|
||||
return { shortcutName: null, additionalContext: message };
|
||||
}
|
||||
|
||||
return {
|
||||
shortcutName: match[1],
|
||||
additionalContext: match[2] || '',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance a message with shortcut instructions
|
||||
* @param message The original message
|
||||
* @param userId The user's ID
|
||||
* @param organizationId The organization's ID
|
||||
* @returns Enhanced message with shortcut instructions
|
||||
*/
|
||||
export async function enhanceMessageWithShortcut(
|
||||
message: string,
|
||||
userId: string,
|
||||
organizationId: string
|
||||
): Promise<string> {
|
||||
const { shortcutName, additionalContext } = parseShortcutFromMessage(message);
|
||||
|
||||
if (!shortcutName) {
|
||||
return message;
|
||||
}
|
||||
|
||||
const shortcut = await findShortcutByName({
|
||||
name: shortcutName,
|
||||
userId,
|
||||
organizationId,
|
||||
});
|
||||
|
||||
if (!shortcut) {
|
||||
throw new ChatError(ChatErrorCode.INVALID_REQUEST, `Shortcut /${shortcutName} not found`, 404);
|
||||
}
|
||||
|
||||
// Simply concatenate instructions with any additional context
|
||||
return additionalContext
|
||||
? `${shortcut.instructions} ${additionalContext}`
|
||||
: shortcut.instructions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance a message with a specific shortcut ID
|
||||
* @param message The original message (optional)
|
||||
* @param shortcutId The shortcut ID to apply
|
||||
* @param userId The user's ID
|
||||
* @param organizationId The organization's ID
|
||||
* @returns Enhanced message with shortcut instructions
|
||||
*/
|
||||
export async function enhanceMessageWithShortcutId(
|
||||
message: string | undefined,
|
||||
shortcutId: string,
|
||||
userId: string,
|
||||
organizationId: string
|
||||
): Promise<string> {
|
||||
const shortcut = await getShortcutById({ id: shortcutId });
|
||||
|
||||
if (!shortcut) {
|
||||
throw new ChatError(ChatErrorCode.INVALID_REQUEST, 'Shortcut not found', 404);
|
||||
}
|
||||
|
||||
// Check permissions: user must be in same org and either creator or shortcut is workspace-shared
|
||||
if (
|
||||
shortcut.organizationId !== organizationId ||
|
||||
(!shortcut.sharedWithWorkspace && shortcut.createdBy !== userId)
|
||||
) {
|
||||
throw new ChatError(
|
||||
ChatErrorCode.INVALID_REQUEST,
|
||||
'You do not have permission to use this shortcut',
|
||||
403
|
||||
);
|
||||
}
|
||||
|
||||
// Concatenate shortcut instructions with any additional message
|
||||
return message ? `${shortcut.instructions} ${message}` : shortcut.instructions;
|
||||
}
|
|
@ -10,6 +10,7 @@ import organizationRoutes from './organization';
|
|||
import reportsRoutes from './reports';
|
||||
import s3IntegrationsRoutes from './s3-integrations';
|
||||
import securityRoutes from './security';
|
||||
import shortcutsRoutes from './shortcuts';
|
||||
import slackRoutes from './slack';
|
||||
import supportRoutes from './support';
|
||||
import titleRoutes from './title';
|
||||
|
@ -25,6 +26,7 @@ const app = new Hono()
|
|||
.route('/slack', slackRoutes)
|
||||
.route('/support', supportRoutes)
|
||||
.route('/security', securityRoutes)
|
||||
.route('/shortcuts', shortcutsRoutes)
|
||||
.route('/organizations', organizationRoutes)
|
||||
.route('/dictionaries', dictionariesRoutes)
|
||||
.route('/title', titleRoutes)
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import type { User } from '@buster/database';
|
||||
import { checkDuplicateName, createShortcut, getUserOrganizationId } from '@buster/database';
|
||||
import type { CreateShortcutRequest, Shortcut } from '@buster/server-shared/shortcuts';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
|
||||
export async function createShortcutHandler(
|
||||
user: User,
|
||||
data: CreateShortcutRequest
|
||||
): Promise<Shortcut> {
|
||||
try {
|
||||
// Get user's organization ID
|
||||
const userOrg = await getUserOrganizationId(user.id);
|
||||
|
||||
if (!userOrg) {
|
||||
throw new HTTPException(400, {
|
||||
message: 'User must belong to an organization',
|
||||
});
|
||||
}
|
||||
|
||||
const { organizationId } = userOrg;
|
||||
|
||||
// Check if user has permission to create workspace shortcuts
|
||||
if (data.sharedWithWorkspace) {
|
||||
// TODO: Check if user is admin/has permission to create workspace shortcuts
|
||||
// For now, we'll allow any authenticated user to create workspace shortcuts
|
||||
// This should be updated based on your permission system
|
||||
}
|
||||
|
||||
// Check for duplicate name
|
||||
const isDuplicate = await checkDuplicateName({
|
||||
name: data.name,
|
||||
userId: user.id,
|
||||
organizationId,
|
||||
isWorkspace: data.sharedWithWorkspace,
|
||||
});
|
||||
|
||||
if (isDuplicate) {
|
||||
const scope = data.sharedWithWorkspace ? 'workspace' : 'your personal shortcuts';
|
||||
throw new HTTPException(409, {
|
||||
message: `A shortcut named '${data.name}' already exists in ${scope}`,
|
||||
});
|
||||
}
|
||||
|
||||
// Create the shortcut
|
||||
const shortcut = await createShortcut({
|
||||
name: data.name,
|
||||
instructions: data.instructions,
|
||||
createdBy: user.id,
|
||||
organizationId,
|
||||
sharedWithWorkspace: data.sharedWithWorkspace,
|
||||
});
|
||||
|
||||
return shortcut;
|
||||
} catch (error) {
|
||||
console.error('Error in createShortcutHandler:', {
|
||||
userId: user.id,
|
||||
data,
|
||||
error: error instanceof Error ? error.message : error,
|
||||
});
|
||||
|
||||
if (error instanceof HTTPException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new HTTPException(500, {
|
||||
message: 'Failed to create shortcut',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
import type { User } from '@buster/database';
|
||||
import { deleteShortcut, getShortcutById, getUserOrganizationId } from '@buster/database';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
|
||||
export async function deleteShortcutHandler(
|
||||
user: User,
|
||||
shortcutId: string
|
||||
): Promise<{ success: boolean }> {
|
||||
try {
|
||||
// Get user's organization ID
|
||||
const userOrg = await getUserOrganizationId(user.id);
|
||||
|
||||
if (!userOrg) {
|
||||
throw new HTTPException(400, {
|
||||
message: 'User must belong to an organization',
|
||||
});
|
||||
}
|
||||
|
||||
const { organizationId } = userOrg;
|
||||
|
||||
// Get the existing shortcut
|
||||
const existingShortcut = await getShortcutById({ id: shortcutId });
|
||||
|
||||
if (!existingShortcut) {
|
||||
throw new HTTPException(404, {
|
||||
message: 'Shortcut not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
if (existingShortcut.organizationId !== organizationId) {
|
||||
throw new HTTPException(403, {
|
||||
message: 'You do not have permission to delete this shortcut',
|
||||
});
|
||||
}
|
||||
|
||||
// For personal shortcuts, only creator can delete
|
||||
// For workspace shortcuts, check admin permission (TODO)
|
||||
if (!existingShortcut.sharedWithWorkspace && existingShortcut.createdBy !== user.id) {
|
||||
throw new HTTPException(403, {
|
||||
message: 'You can only delete your own shortcuts',
|
||||
});
|
||||
}
|
||||
|
||||
if (existingShortcut.sharedWithWorkspace) {
|
||||
// TODO: Check if user is admin/has permission to delete workspace shortcuts
|
||||
// For now, we'll allow the creator to delete their workspace shortcuts
|
||||
if (existingShortcut.createdBy !== user.id) {
|
||||
throw new HTTPException(403, {
|
||||
message: 'Only administrators can delete workspace shortcuts',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Soft delete the shortcut
|
||||
await deleteShortcut({
|
||||
id: shortcutId,
|
||||
deletedBy: user.id,
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error('Error in deleteShortcutHandler:', {
|
||||
userId: user.id,
|
||||
shortcutId,
|
||||
error: error instanceof Error ? error.message : error,
|
||||
});
|
||||
|
||||
if (error instanceof HTTPException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new HTTPException(500, {
|
||||
message: 'Failed to delete shortcut',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
import type { User } from '@buster/database';
|
||||
import { getShortcutById, getUserOrganizationId } from '@buster/database';
|
||||
import type { Shortcut } from '@buster/server-shared/shortcuts';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
|
||||
export async function getShortcutHandler(user: User, shortcutId: string): Promise<Shortcut> {
|
||||
try {
|
||||
// Get user's organization ID
|
||||
const userOrg = await getUserOrganizationId(user.id);
|
||||
|
||||
if (!userOrg) {
|
||||
throw new HTTPException(400, {
|
||||
message: 'User must belong to an organization',
|
||||
});
|
||||
}
|
||||
|
||||
const { organizationId } = userOrg;
|
||||
|
||||
const shortcut = await getShortcutById({ id: shortcutId });
|
||||
|
||||
if (!shortcut) {
|
||||
throw new HTTPException(404, {
|
||||
message: 'Shortcut not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Check permissions: user must be creator or shortcut must be workspace-shared
|
||||
if (
|
||||
shortcut.organizationId !== organizationId ||
|
||||
(!shortcut.sharedWithWorkspace && shortcut.createdBy !== user.id)
|
||||
) {
|
||||
throw new HTTPException(403, {
|
||||
message: 'You do not have permission to view this shortcut',
|
||||
});
|
||||
}
|
||||
|
||||
return shortcut;
|
||||
} catch (error) {
|
||||
console.error('Error in getShortcutHandler:', {
|
||||
userId: user.id,
|
||||
shortcutId,
|
||||
error: error instanceof Error ? error.message : error,
|
||||
});
|
||||
|
||||
if (error instanceof HTTPException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new HTTPException(500, {
|
||||
message: 'Failed to fetch shortcut',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
import {
|
||||
createShortcutRequestSchema,
|
||||
updateShortcutRequestSchema,
|
||||
} from '@buster/server-shared/shortcuts';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { Hono } from 'hono';
|
||||
import { z } from 'zod';
|
||||
import { requireAuth } from '../../../middleware/auth';
|
||||
import '../../../types/hono.types';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
import { createShortcutHandler } from './create-shortcut';
|
||||
import { deleteShortcutHandler } from './delete-shortcut';
|
||||
import { getShortcutHandler } from './get-shortcut';
|
||||
import { listShortcutsHandler } from './list-shortcuts';
|
||||
import { updateShortcutHandler } from './update-shortcut';
|
||||
|
||||
// Schema for path params
|
||||
const shortcutIdParamSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
});
|
||||
|
||||
const app = new Hono()
|
||||
// Apply authentication middleware
|
||||
.use('*', requireAuth)
|
||||
|
||||
// List all accessible shortcuts (personal + workspace)
|
||||
.get('/', async (c) => {
|
||||
const user = c.get('busterUser');
|
||||
const response = await listShortcutsHandler(user);
|
||||
return c.json(response);
|
||||
})
|
||||
|
||||
// Get a single shortcut by ID
|
||||
.get('/:id', zValidator('param', shortcutIdParamSchema), async (c) => {
|
||||
const user = c.get('busterUser');
|
||||
const { id } = c.req.valid('param');
|
||||
const response = await getShortcutHandler(user, id);
|
||||
return c.json(response);
|
||||
})
|
||||
|
||||
// Create a new shortcut
|
||||
.post('/', zValidator('json', createShortcutRequestSchema), async (c) => {
|
||||
const user = c.get('busterUser');
|
||||
const data = c.req.valid('json');
|
||||
const response = await createShortcutHandler(user, data);
|
||||
return c.json(response, 201);
|
||||
})
|
||||
|
||||
// Update an existing shortcut
|
||||
.put(
|
||||
'/:id',
|
||||
zValidator('param', shortcutIdParamSchema),
|
||||
zValidator('json', updateShortcutRequestSchema),
|
||||
async (c) => {
|
||||
const user = c.get('busterUser');
|
||||
const { id } = c.req.valid('param');
|
||||
const data = c.req.valid('json');
|
||||
const response = await updateShortcutHandler(user, id, data);
|
||||
return c.json(response);
|
||||
}
|
||||
)
|
||||
|
||||
// Delete a shortcut
|
||||
.delete('/:id', zValidator('param', shortcutIdParamSchema), async (c) => {
|
||||
const user = c.get('busterUser');
|
||||
const { id } = c.req.valid('param');
|
||||
const response = await deleteShortcutHandler(user, id);
|
||||
return c.json(response);
|
||||
})
|
||||
|
||||
// Error handling
|
||||
.onError((e, _c) => {
|
||||
if (e instanceof HTTPException) {
|
||||
return e.getResponse();
|
||||
}
|
||||
|
||||
console.error('Unhandled error in shortcuts API:', e);
|
||||
throw new HTTPException(500, {
|
||||
message: 'Internal server error',
|
||||
});
|
||||
});
|
||||
|
||||
export default app;
|
|
@ -0,0 +1,42 @@
|
|||
import type { User } from '@buster/database';
|
||||
import { getUserShortcuts, getUserOrganizationId } from '@buster/database';
|
||||
import type { ListShortcutsResponse } from '@buster/server-shared/shortcuts';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
|
||||
export async function listShortcutsHandler(user: User): Promise<ListShortcutsResponse> {
|
||||
try {
|
||||
// Get user's organization ID
|
||||
const userOrg = await getUserOrganizationId(user.id);
|
||||
|
||||
if (!userOrg) {
|
||||
throw new HTTPException(400, {
|
||||
message: 'User must belong to an organization',
|
||||
});
|
||||
}
|
||||
|
||||
const { organizationId } = userOrg;
|
||||
|
||||
// Get all accessible shortcuts (personal + workspace) sorted alphabetically
|
||||
const shortcuts = await getUserShortcuts({
|
||||
userId: user.id,
|
||||
organizationId,
|
||||
});
|
||||
|
||||
return {
|
||||
shortcuts,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error in listShortcutsHandler:', {
|
||||
userId: user.id,
|
||||
error: error instanceof Error ? error.message : error,
|
||||
});
|
||||
|
||||
if (error instanceof HTTPException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new HTTPException(500, {
|
||||
message: 'Failed to fetch shortcuts',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
import type { User } from '@buster/database';
|
||||
import { checkDuplicateName, getShortcutById, updateShortcut, getUserOrganizationId } from '@buster/database';
|
||||
import type { Shortcut, UpdateShortcutRequest } from '@buster/server-shared/shortcuts';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
|
||||
export async function updateShortcutHandler(
|
||||
user: User,
|
||||
shortcutId: string,
|
||||
data: UpdateShortcutRequest
|
||||
): Promise<Shortcut> {
|
||||
try {
|
||||
// Get user's organization ID
|
||||
const userOrg = await getUserOrganizationId(user.id);
|
||||
|
||||
if (!userOrg) {
|
||||
throw new HTTPException(400, {
|
||||
message: 'User must belong to an organization',
|
||||
});
|
||||
}
|
||||
|
||||
const { organizationId } = userOrg;
|
||||
|
||||
// Get the existing shortcut
|
||||
const existingShortcut = await getShortcutById({ id: shortcutId });
|
||||
|
||||
if (!existingShortcut) {
|
||||
throw new HTTPException(404, {
|
||||
message: 'Shortcut not found',
|
||||
});
|
||||
}
|
||||
|
||||
// Check permissions
|
||||
if (existingShortcut.organizationId !== organizationId) {
|
||||
throw new HTTPException(403, {
|
||||
message: 'You do not have permission to update this shortcut',
|
||||
});
|
||||
}
|
||||
|
||||
// For personal shortcuts, only creator can update
|
||||
// For workspace shortcuts, check admin permission (TODO)
|
||||
if (!existingShortcut.sharedWithWorkspace && existingShortcut.createdBy !== user.id) {
|
||||
throw new HTTPException(403, {
|
||||
message: 'You can only update your own shortcuts',
|
||||
});
|
||||
}
|
||||
|
||||
if (existingShortcut.sharedWithWorkspace) {
|
||||
// TODO: Check if user is admin/has permission to update workspace shortcuts
|
||||
// For now, we'll allow the creator to update their workspace shortcuts
|
||||
if (existingShortcut.createdBy !== user.id) {
|
||||
throw new HTTPException(403, {
|
||||
message: 'Only administrators can update workspace shortcuts',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If name is being changed, check for duplicates
|
||||
if (data.name && data.name !== existingShortcut.name) {
|
||||
const isDuplicate = await checkDuplicateName({
|
||||
name: data.name,
|
||||
userId: user.id,
|
||||
organizationId,
|
||||
isWorkspace: existingShortcut.sharedWithWorkspace,
|
||||
excludeId: shortcutId,
|
||||
});
|
||||
|
||||
if (isDuplicate) {
|
||||
const scope = existingShortcut.sharedWithWorkspace
|
||||
? 'workspace'
|
||||
: 'your personal shortcuts';
|
||||
throw new HTTPException(409, {
|
||||
message: `A shortcut named '${data.name}' already exists in ${scope}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Update the shortcut
|
||||
const updatedShortcut = await updateShortcut({
|
||||
id: shortcutId,
|
||||
name: data.name,
|
||||
instructions: data.instructions,
|
||||
updatedBy: user.id,
|
||||
});
|
||||
|
||||
if (!updatedShortcut) {
|
||||
throw new HTTPException(500, {
|
||||
message: 'Failed to update shortcut',
|
||||
});
|
||||
}
|
||||
|
||||
return updatedShortcut;
|
||||
} catch (error) {
|
||||
console.error('Error in updateShortcutHandler:', {
|
||||
userId: user.id,
|
||||
shortcutId,
|
||||
data,
|
||||
error: error instanceof Error ? error.message : error,
|
||||
});
|
||||
|
||||
if (error instanceof HTTPException) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
throw new HTTPException(500, {
|
||||
message: 'Failed to update shortcut',
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,378 @@
|
|||
# Shortcuts API Examples
|
||||
|
||||
## Base URL
|
||||
```
|
||||
http://localhost:3000/api/v2/shortcuts
|
||||
```
|
||||
|
||||
## Authentication
|
||||
All endpoints require authentication. Include your auth token in the headers:
|
||||
```json
|
||||
{
|
||||
"Authorization": "Bearer YOUR_AUTH_TOKEN"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 1. Create a Personal Shortcut
|
||||
|
||||
### Request
|
||||
```http
|
||||
POST /api/v2/shortcuts
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "weekly-sales-report",
|
||||
"instructions": "Build me a report that pulls in cumulative YTD sales for this week, total deals closed this week, and top rep of this week. I want you to highlight interesting trends and anomalies.",
|
||||
"sharedWithWorkspace": false
|
||||
}
|
||||
```
|
||||
|
||||
### Response (201 Created)
|
||||
```json
|
||||
{
|
||||
"id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"name": "weekly-sales-report",
|
||||
"instructions": "Build me a report that pulls in cumulative YTD sales for this week, total deals closed this week, and top rep of this week. I want you to highlight interesting trends and anomalies.",
|
||||
"createdBy": "user-123",
|
||||
"updatedBy": "user-123",
|
||||
"organizationId": "org-456",
|
||||
"sharedWithWorkspace": false,
|
||||
"createdAt": "2024-01-15T10:00:00.000Z",
|
||||
"updatedAt": "2024-01-15T10:00:00.000Z",
|
||||
"deletedAt": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Create a Workspace Shortcut
|
||||
|
||||
### Request
|
||||
```http
|
||||
POST /api/v2/shortcuts
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "customer-analysis",
|
||||
"instructions": "Analyze customer data focusing on: 1) Customer acquisition trends, 2) Churn rate analysis, 3) Customer lifetime value, 4) Segment performance. Present findings with clear visualizations.",
|
||||
"sharedWithWorkspace": true
|
||||
}
|
||||
```
|
||||
|
||||
### Response (201 Created)
|
||||
```json
|
||||
{
|
||||
"id": "456e7890-e89b-12d3-a456-426614174001",
|
||||
"name": "customer-analysis",
|
||||
"instructions": "Analyze customer data focusing on: 1) Customer acquisition trends, 2) Churn rate analysis, 3) Customer lifetime value, 4) Segment performance. Present findings with clear visualizations.",
|
||||
"createdBy": "user-123",
|
||||
"updatedBy": "user-123",
|
||||
"organizationId": "org-456",
|
||||
"sharedWithWorkspace": true,
|
||||
"createdAt": "2024-01-15T10:00:00.000Z",
|
||||
"updatedAt": "2024-01-15T10:00:00.000Z",
|
||||
"deletedAt": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. List All Shortcuts (Personal + Workspace)
|
||||
|
||||
### Request
|
||||
```http
|
||||
GET /api/v2/shortcuts
|
||||
```
|
||||
|
||||
### Response (200 OK)
|
||||
```json
|
||||
{
|
||||
"shortcuts": [
|
||||
{
|
||||
"id": "456e7890-e89b-12d3-a456-426614174001",
|
||||
"name": "customer-analysis",
|
||||
"instructions": "Analyze customer data focusing on: 1) Customer acquisition trends, 2) Churn rate analysis, 3) Customer lifetime value, 4) Segment performance. Present findings with clear visualizations.",
|
||||
"createdBy": "admin-user",
|
||||
"updatedBy": "admin-user",
|
||||
"organizationId": "org-456",
|
||||
"sharedWithWorkspace": true,
|
||||
"createdAt": "2024-01-15T09:00:00.000Z",
|
||||
"updatedAt": "2024-01-15T09:00:00.000Z",
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"id": "789e0123-e89b-12d3-a456-426614174002",
|
||||
"name": "daily-standup",
|
||||
"instructions": "Generate a summary of: Yesterday's completed tasks, today's priorities, and any blockers. Format as bullet points.",
|
||||
"createdBy": "user-123",
|
||||
"updatedBy": "user-123",
|
||||
"organizationId": "org-456",
|
||||
"sharedWithWorkspace": false,
|
||||
"createdAt": "2024-01-14T14:30:00.000Z",
|
||||
"updatedAt": "2024-01-14T14:30:00.000Z",
|
||||
"deletedAt": null
|
||||
},
|
||||
{
|
||||
"id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"name": "weekly-sales-report",
|
||||
"instructions": "Build me a report that pulls in cumulative YTD sales for this week, total deals closed this week, and top rep of this week. I want you to highlight interesting trends and anomalies.",
|
||||
"createdBy": "user-123",
|
||||
"updatedBy": "user-123",
|
||||
"organizationId": "org-456",
|
||||
"sharedWithWorkspace": false,
|
||||
"createdAt": "2024-01-15T10:00:00.000Z",
|
||||
"updatedAt": "2024-01-15T10:00:00.000Z",
|
||||
"deletedAt": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Get a Single Shortcut
|
||||
|
||||
### Request
|
||||
```http
|
||||
GET /api/v2/shortcuts/123e4567-e89b-12d3-a456-426614174000
|
||||
```
|
||||
|
||||
### Response (200 OK)
|
||||
```json
|
||||
{
|
||||
"id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"name": "weekly-sales-report",
|
||||
"instructions": "Build me a report that pulls in cumulative YTD sales for this week, total deals closed this week, and top rep of this week. I want you to highlight interesting trends and anomalies.",
|
||||
"createdBy": "user-123",
|
||||
"updatedBy": "user-123",
|
||||
"organizationId": "org-456",
|
||||
"sharedWithWorkspace": false,
|
||||
"createdAt": "2024-01-15T10:00:00.000Z",
|
||||
"updatedAt": "2024-01-15T10:00:00.000Z",
|
||||
"deletedAt": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Update a Shortcut
|
||||
|
||||
### Request
|
||||
```http
|
||||
PUT /api/v2/shortcuts/123e4567-e89b-12d3-a456-426614174000
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "weekly-sales-summary",
|
||||
"instructions": "Build me a comprehensive sales report including: 1) YTD cumulative sales, 2) Weekly deals closed with breakdown by type, 3) Top 5 performers with their key metrics, 4) Week-over-week comparison. Include charts for visual representation."
|
||||
}
|
||||
```
|
||||
|
||||
### Response (200 OK)
|
||||
```json
|
||||
{
|
||||
"id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"name": "weekly-sales-summary",
|
||||
"instructions": "Build me a comprehensive sales report including: 1) YTD cumulative sales, 2) Weekly deals closed with breakdown by type, 3) Top 5 performers with their key metrics, 4) Week-over-week comparison. Include charts for visual representation.",
|
||||
"createdBy": "user-123",
|
||||
"updatedBy": "user-123",
|
||||
"organizationId": "org-456",
|
||||
"sharedWithWorkspace": false,
|
||||
"createdAt": "2024-01-15T10:00:00.000Z",
|
||||
"updatedAt": "2024-01-15T11:00:00.000Z",
|
||||
"deletedAt": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Update Only Name
|
||||
|
||||
### Request
|
||||
```http
|
||||
PUT /api/v2/shortcuts/123e4567-e89b-12d3-a456-426614174000
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "sales-weekly"
|
||||
}
|
||||
```
|
||||
|
||||
### Response (200 OK)
|
||||
```json
|
||||
{
|
||||
"id": "123e4567-e89b-12d3-a456-426614174000",
|
||||
"name": "sales-weekly",
|
||||
"instructions": "Build me a comprehensive sales report including: 1) YTD cumulative sales, 2) Weekly deals closed with breakdown by type, 3) Top 5 performers with their key metrics, 4) Week-over-week comparison. Include charts for visual representation.",
|
||||
"createdBy": "user-123",
|
||||
"updatedBy": "user-123",
|
||||
"organizationId": "org-456",
|
||||
"sharedWithWorkspace": false,
|
||||
"createdAt": "2024-01-15T10:00:00.000Z",
|
||||
"updatedAt": "2024-01-15T11:30:00.000Z",
|
||||
"deletedAt": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Delete a Shortcut
|
||||
|
||||
### Request
|
||||
```http
|
||||
DELETE /api/v2/shortcuts/123e4567-e89b-12d3-a456-426614174000
|
||||
```
|
||||
|
||||
### Response (200 OK)
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Responses
|
||||
|
||||
### 409 Conflict - Duplicate Name
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"message": "A shortcut named 'weekly-sales-report' already exists in your personal shortcuts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 404 Not Found
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"message": "Shortcut not found"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 403 Forbidden - Permission Denied
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"message": "You can only update your own shortcuts"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 400 Bad Request - Validation Error
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"issues": [
|
||||
{
|
||||
"code": "invalid_string",
|
||||
"message": "Name must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens",
|
||||
"path": ["name"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chat Integration Examples
|
||||
|
||||
### Using Shortcut with Pattern in Chat Message
|
||||
|
||||
```http
|
||||
POST /api/v2/chats
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"prompt": "/weekly-sales-report for Q4 2024"
|
||||
}
|
||||
```
|
||||
|
||||
The system will:
|
||||
1. Detect the `/weekly-sales-report` pattern
|
||||
2. Look up the shortcut (personal first, then workspace)
|
||||
3. Replace the message with: "Build me a report that pulls in cumulative YTD sales for this week, total deals closed this week, and top rep of this week. I want you to highlight interesting trends and anomalies. for Q4 2024"
|
||||
|
||||
### Using Shortcut with ID
|
||||
|
||||
```http
|
||||
POST /api/v2/chats
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"prompt": "Focus on enterprise customers only",
|
||||
"shortcut_id": "123e4567-e89b-12d3-a456-426614174000"
|
||||
}
|
||||
```
|
||||
|
||||
The system will prepend the shortcut instructions to the prompt.
|
||||
|
||||
---
|
||||
|
||||
## Validation Rules
|
||||
|
||||
### Name Requirements
|
||||
- Must start with a lowercase letter
|
||||
- Can only contain lowercase letters, numbers, and hyphens
|
||||
- Cannot have consecutive hyphens (--)
|
||||
- Minimum 1 character, maximum 255 characters
|
||||
- Examples of valid names:
|
||||
- `weekly-report`
|
||||
- `sales-q3`
|
||||
- `customer-360-view`
|
||||
- `kpi-dashboard`
|
||||
|
||||
### Invalid Names (will be rejected)
|
||||
- `Weekly-Report` (uppercase letters)
|
||||
- `weekly_report` (underscores not allowed)
|
||||
- `weekly--report` (consecutive hyphens)
|
||||
- `123-report` (starts with number)
|
||||
- `report!` (special characters)
|
||||
|
||||
### Instructions Requirements
|
||||
- Minimum 1 character
|
||||
- Maximum 10,000 characters
|
||||
- Can contain any text, including newlines and special characters
|
||||
|
||||
---
|
||||
|
||||
## CURL Examples
|
||||
|
||||
### Create Shortcut
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/v2/shortcuts \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "market-analysis",
|
||||
"instructions": "Analyze market trends and competitor positioning",
|
||||
"sharedWithWorkspace": false
|
||||
}'
|
||||
```
|
||||
|
||||
### List Shortcuts
|
||||
```bash
|
||||
curl http://localhost:3000/api/v2/shortcuts \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
||||
|
||||
### Update Shortcut
|
||||
```bash
|
||||
curl -X PUT http://localhost:3000/api/v2/shortcuts/SHORTCUT_ID \
|
||||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"instructions": "Updated analysis instructions with more detail"
|
||||
}'
|
||||
```
|
||||
|
||||
### Delete Shortcut
|
||||
```bash
|
||||
curl -X DELETE http://localhost:3000/api/v2/shortcuts/SHORTCUT_ID \
|
||||
-H "Authorization: Bearer YOUR_TOKEN"
|
||||
```
|
|
@ -1,5 +1,9 @@
|
|||
import { config } from 'dotenv';
|
||||
import { defineConfig } from 'drizzle-kit';
|
||||
|
||||
// Load specific .env file
|
||||
config({ path: '../../.env' }); // or '.env.development', '.env.production', etc.
|
||||
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
|
||||
if (!connectionString) {
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
CREATE TABLE "shortcuts" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" varchar(255) NOT NULL,
|
||||
"instructions" text NOT NULL,
|
||||
"created_by" uuid NOT NULL,
|
||||
"updated_by" uuid,
|
||||
"organization_id" uuid NOT NULL,
|
||||
"shared_with_workspace" boolean DEFAULT false NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"deleted_at" timestamp with time zone,
|
||||
CONSTRAINT "shortcuts_personal_unique" UNIQUE("name","organization_id","created_by")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "shortcuts" ADD CONSTRAINT "shortcuts_created_by_fkey" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "shortcuts" ADD CONSTRAINT "shortcuts_updated_by_fkey" FOREIGN KEY ("updated_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE cascade;--> statement-breakpoint
|
||||
ALTER TABLE "shortcuts" ADD CONSTRAINT "shortcuts_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
CREATE INDEX "shortcuts_org_user_idx" ON "shortcuts" USING btree ("organization_id" uuid_ops,"created_by" uuid_ops);--> statement-breakpoint
|
||||
CREATE INDEX "shortcuts_name_idx" ON "shortcuts" USING btree ("name");--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX "shortcuts_workspace_unique" ON "shortcuts" USING btree ("name","organization_id") WHERE "shortcuts"."shared_with_workspace" = true;
|
File diff suppressed because it is too large
Load Diff
|
@ -638,6 +638,13 @@
|
|||
"when": 1756098017282,
|
||||
"tag": "0090_low_beast",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 91,
|
||||
"version": "7",
|
||||
"when": 1756404533775,
|
||||
"tag": "0091_sad_saracen",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
|
@ -15,3 +15,4 @@ export * from './s3-integrations';
|
|||
export * from './vault';
|
||||
export * from './cascading-permissions';
|
||||
export * from './github-integrations';
|
||||
export * from './shortcuts';
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
import { and, eq, isNull, ne } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../connection';
|
||||
import { shortcuts } from '../../schema';
|
||||
|
||||
export const CheckDuplicateNameInputSchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
userId: z.string().uuid(),
|
||||
organizationId: z.string().uuid(),
|
||||
isWorkspace: z.boolean(),
|
||||
excludeId: z.string().uuid().optional(),
|
||||
});
|
||||
|
||||
export type CheckDuplicateNameInput = z.infer<typeof CheckDuplicateNameInputSchema>;
|
||||
|
||||
export async function checkDuplicateName(input: CheckDuplicateNameInput): Promise<boolean> {
|
||||
const validated = CheckDuplicateNameInputSchema.parse(input);
|
||||
|
||||
const conditions = [
|
||||
eq(shortcuts.name, validated.name),
|
||||
eq(shortcuts.organizationId, validated.organizationId),
|
||||
isNull(shortcuts.deletedAt),
|
||||
];
|
||||
|
||||
// Add exclude condition if updating
|
||||
if (validated.excludeId) {
|
||||
conditions.push(ne(shortcuts.id, validated.excludeId));
|
||||
}
|
||||
|
||||
if (validated.isWorkspace) {
|
||||
// Check for existing workspace shortcut
|
||||
conditions.push(eq(shortcuts.sharedWithWorkspace, true));
|
||||
} else {
|
||||
// Check for existing personal shortcut
|
||||
conditions.push(eq(shortcuts.createdBy, validated.userId));
|
||||
}
|
||||
|
||||
const [existing] = await db
|
||||
.select({ id: shortcuts.id })
|
||||
.from(shortcuts)
|
||||
.where(and(...conditions))
|
||||
.limit(1);
|
||||
|
||||
return !!existing;
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
import { and, eq, isNotNull, isNull } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../connection';
|
||||
import { shortcuts } from '../../schema';
|
||||
|
||||
export const CreateShortcutInputSchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
instructions: z.string().min(1),
|
||||
createdBy: z.string().uuid(),
|
||||
organizationId: z.string().uuid(),
|
||||
sharedWithWorkspace: z.boolean(),
|
||||
});
|
||||
|
||||
export type CreateShortcutInput = z.infer<typeof CreateShortcutInputSchema>;
|
||||
|
||||
export async function createShortcut(input: CreateShortcutInput) {
|
||||
const validated = CreateShortcutInputSchema.parse(input);
|
||||
|
||||
// Check if there's a soft-deleted shortcut with the same name
|
||||
const [existingDeleted] = await db
|
||||
.select()
|
||||
.from(shortcuts)
|
||||
.where(
|
||||
and(
|
||||
eq(shortcuts.name, validated.name),
|
||||
eq(shortcuts.organizationId, validated.organizationId),
|
||||
validated.sharedWithWorkspace
|
||||
? eq(shortcuts.sharedWithWorkspace, true)
|
||||
: eq(shortcuts.createdBy, validated.createdBy),
|
||||
isNotNull(shortcuts.deletedAt)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (existingDeleted) {
|
||||
// Upsert: restore the soft-deleted record with new data
|
||||
const [restored] = await db
|
||||
.update(shortcuts)
|
||||
.set({
|
||||
instructions: validated.instructions,
|
||||
sharedWithWorkspace: validated.sharedWithWorkspace,
|
||||
updatedBy: validated.createdBy,
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
})
|
||||
.where(eq(shortcuts.id, existingDeleted.id))
|
||||
.returning();
|
||||
|
||||
return restored;
|
||||
}
|
||||
|
||||
// Create new shortcut
|
||||
const [created] = await db
|
||||
.insert(shortcuts)
|
||||
.values({
|
||||
name: validated.name,
|
||||
instructions: validated.instructions,
|
||||
createdBy: validated.createdBy,
|
||||
updatedBy: validated.createdBy,
|
||||
organizationId: validated.organizationId,
|
||||
sharedWithWorkspace: validated.sharedWithWorkspace,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return created;
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import { and, eq, isNull } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../connection';
|
||||
import { shortcuts } from '../../schema';
|
||||
|
||||
export const DeleteShortcutInputSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
deletedBy: z.string().uuid(),
|
||||
});
|
||||
|
||||
export type DeleteShortcutInput = z.infer<typeof DeleteShortcutInputSchema>;
|
||||
|
||||
export async function deleteShortcut(input: DeleteShortcutInput) {
|
||||
const validated = DeleteShortcutInputSchema.parse(input);
|
||||
|
||||
const [deleted] = await db
|
||||
.update(shortcuts)
|
||||
.set({
|
||||
deletedAt: new Date().toISOString(),
|
||||
updatedBy: validated.deletedBy,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
.where(and(eq(shortcuts.id, validated.id), isNull(shortcuts.deletedAt)))
|
||||
.returning();
|
||||
|
||||
return deleted;
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import { and, eq, isNull } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../connection';
|
||||
import { shortcuts } from '../../schema';
|
||||
|
||||
export const FindShortcutByNameInputSchema = z.object({
|
||||
name: z.string().min(1).max(255),
|
||||
userId: z.string().uuid(),
|
||||
organizationId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export type FindShortcutByNameInput = z.infer<typeof FindShortcutByNameInputSchema>;
|
||||
|
||||
export async function findShortcutByName(input: FindShortcutByNameInput) {
|
||||
const validated = FindShortcutByNameInputSchema.parse(input);
|
||||
|
||||
// First try to find personal shortcut
|
||||
const [personalShortcut] = await db
|
||||
.select()
|
||||
.from(shortcuts)
|
||||
.where(
|
||||
and(
|
||||
eq(shortcuts.name, validated.name),
|
||||
eq(shortcuts.createdBy, validated.userId),
|
||||
eq(shortcuts.organizationId, validated.organizationId),
|
||||
isNull(shortcuts.deletedAt)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
if (personalShortcut) {
|
||||
return personalShortcut;
|
||||
}
|
||||
|
||||
// Then try to find workspace shortcut
|
||||
const [workspaceShortcut] = await db
|
||||
.select()
|
||||
.from(shortcuts)
|
||||
.where(
|
||||
and(
|
||||
eq(shortcuts.name, validated.name),
|
||||
eq(shortcuts.organizationId, validated.organizationId),
|
||||
eq(shortcuts.sharedWithWorkspace, true),
|
||||
isNull(shortcuts.deletedAt)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
return workspaceShortcut || null;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import { and, eq, isNull } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../connection';
|
||||
import { shortcuts } from '../../schema';
|
||||
|
||||
export const GetShortcutByIdInputSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
});
|
||||
|
||||
export type GetShortcutByIdInput = z.infer<typeof GetShortcutByIdInputSchema>;
|
||||
|
||||
export async function getShortcutById(input: GetShortcutByIdInput) {
|
||||
const validated = GetShortcutByIdInputSchema.parse(input);
|
||||
|
||||
const [shortcut] = await db
|
||||
.select()
|
||||
.from(shortcuts)
|
||||
.where(and(eq(shortcuts.id, validated.id), isNull(shortcuts.deletedAt)))
|
||||
.limit(1);
|
||||
|
||||
return shortcut || null;
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import { and, asc, eq, isNull, or } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../connection';
|
||||
import { shortcuts } from '../../schema';
|
||||
|
||||
export const GetUserShortcutsInputSchema = z.object({
|
||||
userId: z.string().uuid(),
|
||||
organizationId: z.string().uuid(),
|
||||
});
|
||||
|
||||
export type GetUserShortcutsInput = z.infer<typeof GetUserShortcutsInputSchema>;
|
||||
|
||||
export async function getUserShortcuts(input: GetUserShortcutsInput) {
|
||||
const validated = GetUserShortcutsInputSchema.parse(input);
|
||||
|
||||
const userShortcuts = await db
|
||||
.select()
|
||||
.from(shortcuts)
|
||||
.where(
|
||||
and(
|
||||
eq(shortcuts.organizationId, validated.organizationId),
|
||||
or(eq(shortcuts.createdBy, validated.userId), eq(shortcuts.sharedWithWorkspace, true)),
|
||||
isNull(shortcuts.deletedAt)
|
||||
)
|
||||
)
|
||||
.orderBy(asc(shortcuts.name));
|
||||
|
||||
return userShortcuts;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
export { createShortcut } from './create-shortcut';
|
||||
export { updateShortcut } from './update-shortcut';
|
||||
export { deleteShortcut } from './delete-shortcut';
|
||||
export { getShortcutById } from './get-shortcut-by-id';
|
||||
export { getUserShortcuts } from './get-user-shortcuts';
|
||||
export { findShortcutByName } from './find-shortcut-by-name';
|
||||
export { checkDuplicateName } from './check-duplicate-name';
|
||||
|
||||
// Export types
|
||||
export type { CreateShortcutInput } from './create-shortcut';
|
||||
export type { UpdateShortcutInput } from './update-shortcut';
|
||||
export type { DeleteShortcutInput } from './delete-shortcut';
|
||||
export type { GetShortcutByIdInput } from './get-shortcut-by-id';
|
||||
export type { GetUserShortcutsInput } from './get-user-shortcuts';
|
||||
export type { FindShortcutByNameInput } from './find-shortcut-by-name';
|
||||
export type { CheckDuplicateNameInput } from './check-duplicate-name';
|
|
@ -0,0 +1,38 @@
|
|||
import { and, eq, isNull } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { db } from '../../connection';
|
||||
import { shortcuts } from '../../schema';
|
||||
|
||||
export const UpdateShortcutInputSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
instructions: z.string().min(1).optional(),
|
||||
updatedBy: z.string().uuid(),
|
||||
});
|
||||
|
||||
export type UpdateShortcutInput = z.infer<typeof UpdateShortcutInputSchema>;
|
||||
|
||||
export async function updateShortcut(input: UpdateShortcutInput) {
|
||||
const validated = UpdateShortcutInputSchema.parse(input);
|
||||
|
||||
const updateData: Record<string, any> = {
|
||||
updatedBy: validated.updatedBy,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
if (validated.name !== undefined) {
|
||||
updateData.name = validated.name;
|
||||
}
|
||||
|
||||
if (validated.instructions !== undefined) {
|
||||
updateData.instructions = validated.instructions;
|
||||
}
|
||||
|
||||
const [updated] = await db
|
||||
.update(shortcuts)
|
||||
.set(updateData)
|
||||
.where(and(eq(shortcuts.id, validated.id), isNull(shortcuts.deletedAt)))
|
||||
.returning();
|
||||
|
||||
return updated;
|
||||
}
|
|
@ -2285,3 +2285,54 @@ export const messagesToSlackMessages = pgTable(
|
|||
),
|
||||
]
|
||||
);
|
||||
|
||||
export const shortcuts = pgTable(
|
||||
'shortcuts',
|
||||
{
|
||||
id: uuid().defaultRandom().primaryKey().notNull(),
|
||||
name: varchar({ length: 255 }).notNull(),
|
||||
instructions: text().notNull(),
|
||||
createdBy: uuid('created_by').notNull(),
|
||||
updatedBy: uuid('updated_by'),
|
||||
organizationId: uuid('organization_id').notNull(),
|
||||
sharedWithWorkspace: boolean('shared_with_workspace').default(false).notNull(),
|
||||
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
updatedAt: timestamp('updated_at', { withTimezone: true, mode: 'string' })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
deletedAt: timestamp('deleted_at', { withTimezone: true, mode: 'string' }),
|
||||
},
|
||||
(table) => [
|
||||
// Foreign keys
|
||||
foreignKey({
|
||||
columns: [table.createdBy],
|
||||
foreignColumns: [users.id],
|
||||
name: 'shortcuts_created_by_fkey',
|
||||
}).onUpdate('cascade'),
|
||||
foreignKey({
|
||||
columns: [table.updatedBy],
|
||||
foreignColumns: [users.id],
|
||||
name: 'shortcuts_updated_by_fkey',
|
||||
}).onUpdate('cascade'),
|
||||
foreignKey({
|
||||
columns: [table.organizationId],
|
||||
foreignColumns: [organizations.id],
|
||||
name: 'shortcuts_organization_id_fkey',
|
||||
}).onDelete('cascade'),
|
||||
// Unique constraints
|
||||
unique('shortcuts_personal_unique').on(table.name, table.organizationId, table.createdBy),
|
||||
// Indexes
|
||||
index('shortcuts_org_user_idx').using(
|
||||
'btree',
|
||||
table.organizationId.asc().nullsLast().op('uuid_ops'),
|
||||
table.createdBy.asc().nullsLast().op('uuid_ops')
|
||||
),
|
||||
index('shortcuts_name_idx').using('btree', table.name.asc().nullsLast()),
|
||||
// Conditional unique constraint for workspace shortcuts
|
||||
uniqueIndex('shortcuts_workspace_unique')
|
||||
.on(table.name, table.organizationId)
|
||||
.where(sql`${table.sharedWithWorkspace} = true`),
|
||||
]
|
||||
);
|
||||
|
|
|
@ -79,6 +79,10 @@
|
|||
"./s3-integrations": {
|
||||
"types": "./dist/s3-integrations/index.d.ts",
|
||||
"default": "./dist/s3-integrations/index.js"
|
||||
},
|
||||
"./shortcuts": {
|
||||
"types": "./dist/shortcuts/index.d.ts",
|
||||
"default": "./dist/shortcuts/index.js"
|
||||
}
|
||||
},
|
||||
"module": "src/index.ts",
|
||||
|
|
|
@ -51,6 +51,7 @@ export const ChatCreateHandlerRequestSchema = z.object({
|
|||
message_id: z.string().optional(),
|
||||
asset_id: z.string().optional(),
|
||||
asset_type: ChatAssetTypeSchema.optional(),
|
||||
shortcut_id: z.string().uuid().optional(),
|
||||
});
|
||||
|
||||
// Cancel chat params schema
|
||||
|
|
|
@ -22,3 +22,4 @@ export * from './teams';
|
|||
export * from './title';
|
||||
export * from './type-utilities';
|
||||
export * from './user';
|
||||
export * from './shortcuts';
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
// Export request types and schemas
|
||||
export {
|
||||
shortcutNameSchema,
|
||||
createShortcutRequestSchema,
|
||||
updateShortcutRequestSchema,
|
||||
type CreateShortcutRequest,
|
||||
type UpdateShortcutRequest,
|
||||
} from './requests.types';
|
||||
|
||||
// Export response types and schemas
|
||||
export {
|
||||
shortcutSchema,
|
||||
listShortcutsResponseSchema,
|
||||
shortcutErrorSchema,
|
||||
type Shortcut,
|
||||
type ListShortcutsResponse,
|
||||
type ShortcutError,
|
||||
} from './responses.types';
|
|
@ -0,0 +1,36 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
// Shortcut name validation: lowercase letters, numbers, and hyphens only
|
||||
export const shortcutNameSchema = z
|
||||
.string()
|
||||
.min(1, 'Name is required')
|
||||
.max(255, 'Name must be 255 characters or less')
|
||||
.regex(/^[a-z][a-z0-9-]*$/, {
|
||||
message:
|
||||
'Name must start with a lowercase letter and contain only lowercase letters, numbers, and hyphens',
|
||||
})
|
||||
.refine((name) => !name.includes('--'), {
|
||||
message: 'Name cannot contain consecutive hyphens',
|
||||
});
|
||||
|
||||
export const createShortcutRequestSchema = z.object({
|
||||
name: shortcutNameSchema,
|
||||
instructions: z
|
||||
.string()
|
||||
.min(1, 'Instructions are required')
|
||||
.max(10000, 'Instructions must be 10,000 characters or less'),
|
||||
sharedWithWorkspace: z.boolean(),
|
||||
});
|
||||
|
||||
export const updateShortcutRequestSchema = z.object({
|
||||
name: shortcutNameSchema.optional(),
|
||||
instructions: z
|
||||
.string()
|
||||
.min(1, 'Instructions are required')
|
||||
.max(10000, 'Instructions must be 10,000 characters or less')
|
||||
.optional(),
|
||||
});
|
||||
|
||||
// Export types inferred from schemas
|
||||
export type CreateShortcutRequest = z.infer<typeof createShortcutRequestSchema>;
|
||||
export type UpdateShortcutRequest = z.infer<typeof updateShortcutRequestSchema>;
|
|
@ -0,0 +1,28 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
export const shortcutSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string(),
|
||||
instructions: z.string(),
|
||||
createdBy: z.string().uuid(),
|
||||
updatedBy: z.string().uuid().nullable(),
|
||||
organizationId: z.string().uuid(),
|
||||
sharedWithWorkspace: z.boolean(),
|
||||
createdAt: z.string(), // ISO string
|
||||
updatedAt: z.string(), // ISO string
|
||||
deletedAt: z.string().nullable(), // ISO string or null
|
||||
});
|
||||
|
||||
export const listShortcutsResponseSchema = z.object({
|
||||
shortcuts: z.array(shortcutSchema),
|
||||
});
|
||||
|
||||
export const shortcutErrorSchema = z.object({
|
||||
code: z.enum(['DUPLICATE_NAME', 'NOT_FOUND', 'PERMISSION_DENIED']),
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
// Export types inferred from schemas
|
||||
export type Shortcut = z.infer<typeof shortcutSchema>;
|
||||
export type ListShortcutsResponse = z.infer<typeof listShortcutsResponseSchema>;
|
||||
export type ShortcutError = z.infer<typeof shortcutErrorSchema>;
|
Loading…
Reference in New Issue