shortcuts final touches

This commit is contained in:
dal 2025-09-12 11:25:43 -06:00
parent 7d4dc4db63
commit 2b46a49841
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
15 changed files with 58 additions and 33 deletions

View File

@ -20,7 +20,7 @@ export async function createShortcutHandler(
const { organizationId } = userOrg;
// Check if user has permission to create workspace shortcuts
if (data.sharedWithWorkspace) {
if (data.shareWithWorkspace) {
// Only workspace_admin or data_admin can create workspace shortcuts
if (userOrg.role !== 'workspace_admin' && userOrg.role !== 'data_admin') {
throw new HTTPException(403, {
@ -34,11 +34,11 @@ export async function createShortcutHandler(
name: data.name,
userId: user.id,
organizationId,
isWorkspace: data.sharedWithWorkspace,
isWorkspace: data.shareWithWorkspace,
});
if (isDuplicate) {
const scope = data.sharedWithWorkspace ? 'workspace' : 'your personal shortcuts';
const scope = data.shareWithWorkspace ? 'workspace' : 'your personal shortcuts';
throw new HTTPException(409, {
message: `A shortcut named '${data.name}' already exists in ${scope}`,
});
@ -50,7 +50,7 @@ export async function createShortcutHandler(
instructions: data.instructions,
createdBy: user.id,
organizationId,
sharedWithWorkspace: data.sharedWithWorkspace,
shareWithWorkspace: data.shareWithWorkspace,
});
if (!shortcut) {

View File

@ -36,13 +36,13 @@ export async function deleteShortcutHandler(
// For personal shortcuts, only creator can delete
// For workspace shortcuts, check admin permission (TODO)
if (!existingShortcut.sharedWithWorkspace && existingShortcut.createdBy !== user.id) {
if (!existingShortcut.shareWithWorkspace && existingShortcut.createdBy !== user.id) {
throw new HTTPException(403, {
message: 'You can only delete your own shortcuts',
});
}
if (existingShortcut.sharedWithWorkspace) {
if (existingShortcut.shareWithWorkspace) {
// 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) {

View File

@ -27,7 +27,7 @@ export async function getShortcutHandler(user: User, shortcutId: string): Promis
// Check permissions: user must be creator or shortcut must be workspace-shared
if (
shortcut.organizationId !== organizationId ||
(!shortcut.sharedWithWorkspace && shortcut.createdBy !== user.id)
(!shortcut.shareWithWorkspace && shortcut.createdBy !== user.id)
) {
throw new HTTPException(403, {
message: 'You do not have permission to view this shortcut',

View File

@ -42,14 +42,14 @@ export async function updateShortcutHandler(
}
// For personal shortcuts, only creator can update
if (!existingShortcut.sharedWithWorkspace && existingShortcut.createdBy !== user.id) {
if (!existingShortcut.shareWithWorkspace && existingShortcut.createdBy !== user.id) {
throw new HTTPException(403, {
message: 'You can only update your own shortcuts',
});
}
// For workspace shortcuts, check admin permission
if (existingShortcut.sharedWithWorkspace) {
if (existingShortcut.shareWithWorkspace) {
// Only workspace_admin, data_admin, or the creator can update workspace shortcuts
const isAdmin = userOrg.role === 'workspace_admin' || userOrg.role === 'data_admin';
const isCreator = existingShortcut.createdBy === user.id;
@ -62,18 +62,36 @@ export async function updateShortcutHandler(
}
}
// Check permission to change sharing status
if (data.shareWithWorkspace !== undefined && data.shareWithWorkspace !== existingShortcut.shareWithWorkspace) {
// Only admins can change sharing status
const isAdmin = userOrg.role === 'workspace_admin' || userOrg.role === 'data_admin';
if (!isAdmin) {
throw new HTTPException(403, {
message: 'Only workspace admins and data admins can change shortcut sharing settings',
});
}
}
// Determine the scope for duplicate checking
const willBeWorkspaceShortcut =
data.shareWithWorkspace !== undefined
? data.shareWithWorkspace
: existingShortcut.shareWithWorkspace;
// 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,
isWorkspace: willBeWorkspaceShortcut,
excludeId: shortcutId,
});
if (isDuplicate) {
const scope = existingShortcut.sharedWithWorkspace
const scope = willBeWorkspaceShortcut
? 'workspace'
: 'your personal shortcuts';
throw new HTTPException(409, {
@ -87,6 +105,7 @@ export async function updateShortcutHandler(
id: shortcutId,
name: data.name,
instructions: data.instructions,
shareWithWorkspace: data.shareWithWorkspace,
updatedBy: user.id,
});

View File

@ -5,14 +5,14 @@ CREATE TABLE "shortcuts" (
"created_by" uuid NOT NULL,
"updated_by" uuid,
"organization_id" uuid NOT NULL,
"shared_with_workspace" boolean DEFAULT false NOT NULL,
"share_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 "users" ALTER COLUMN "suggested_prompts" SET DEFAULT '{"suggestedPrompts":{"report":["provide a trend analysis of quarterly profits","evaluate product performance across regions"],"dashboard":["create a sales performance dashboard","design a revenue forecast dashboard"],"visualization":["create a metric for monthly sales","show top vendors by purchase volume"],"help":["what types of analyses can you perform?","what questions can I as buster?","what data models are available for queries?","can you explain your forecasting capabilities?"]},"updatedAt":"2025-09-12T16:57:43.191Z"}'::jsonb;--> statement-breakpoint
ALTER TABLE "users" ALTER COLUMN "suggested_prompts" SET DEFAULT '{"suggestedPrompts":{"report":["provide a trend analysis of quarterly profits","evaluate product performance across regions"],"dashboard":["create a sales performance dashboard","design a revenue forecast dashboard"],"visualization":["create a metric for monthly sales","show top vendors by purchase volume"],"help":["what types of analyses can you perform?","what questions can I as buster?","what data models are available for queries?","can you explain your forecasting capabilities?"]},"updatedAt":"2025-09-12T17:22:58.487Z"}'::jsonb;--> statement-breakpoint
ALTER TABLE "messages" ADD COLUMN "metadata" jsonb DEFAULT '{}'::jsonb NOT NULL;--> statement-breakpoint
ALTER TABLE "users" ADD COLUMN "last_used_shortcuts" jsonb DEFAULT '[]'::jsonb NOT NULL;--> 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
@ -20,4 +20,4 @@ ALTER TABLE "shortcuts" ADD CONSTRAINT "shortcuts_updated_by_fkey" FOREIGN KEY (
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;
CREATE UNIQUE INDEX "shortcuts_workspace_unique" ON "shortcuts" USING btree ("name","organization_id") WHERE "shortcuts"."share_with_workspace" = true;

View File

@ -1,5 +1,5 @@
{
"id": "0613296e-fdd2-4a35-9d79-2275d0c7a78e",
"id": "155b0fb6-d5e9-46bc-aadd-f71a6f9e48f7",
"prevId": "f71f7e1c-314d-413d-8da9-a525bd4a3519",
"version": "7",
"dialect": "postgresql",
@ -5043,8 +5043,8 @@
"primaryKey": false,
"notNull": true
},
"shared_with_workspace": {
"name": "shared_with_workspace",
"share_with_workspace": {
"name": "share_with_workspace",
"type": "boolean",
"primaryKey": false,
"notNull": true,
@ -5127,7 +5127,7 @@
}
],
"isUnique": true,
"where": "\"shortcuts\".\"shared_with_workspace\" = true",
"where": "\"shortcuts\".\"share_with_workspace\" = true",
"concurrently": false,
"method": "btree",
"with": {}
@ -6914,7 +6914,7 @@
"type": "jsonb",
"primaryKey": false,
"notNull": true,
"default": "'{\"suggestedPrompts\":{\"report\":[\"provide a trend analysis of quarterly profits\",\"evaluate product performance across regions\"],\"dashboard\":[\"create a sales performance dashboard\",\"design a revenue forecast dashboard\"],\"visualization\":[\"create a metric for monthly sales\",\"show top vendors by purchase volume\"],\"help\":[\"what types of analyses can you perform?\",\"what questions can I as buster?\",\"what data models are available for queries?\",\"can you explain your forecasting capabilities?\"]},\"updatedAt\":\"2025-09-12T16:57:43.191Z\"}'::jsonb"
"default": "'{\"suggestedPrompts\":{\"report\":[\"provide a trend analysis of quarterly profits\",\"evaluate product performance across regions\"],\"dashboard\":[\"create a sales performance dashboard\",\"design a revenue forecast dashboard\"],\"visualization\":[\"create a metric for monthly sales\",\"show top vendors by purchase volume\"],\"help\":[\"what types of analyses can you perform?\",\"what questions can I as buster?\",\"what data models are available for queries?\",\"can you explain your forecasting capabilities?\"]},\"updatedAt\":\"2025-09-12T17:22:58.487Z\"}'::jsonb"
},
"personalization_enabled": {
"name": "personalization_enabled",

View File

@ -677,8 +677,8 @@
{
"idx": 96,
"version": "7",
"when": 1757696263222,
"tag": "0096_outgoing_shotgun",
"when": 1757697778519,
"tag": "0096_chubby_agent_zero",
"breakpoints": true
}
]

View File

@ -29,7 +29,7 @@ export async function checkDuplicateName(input: CheckDuplicateNameInput): Promis
if (validated.isWorkspace) {
// Check for existing workspace shortcut
conditions.push(eq(shortcuts.sharedWithWorkspace, true));
conditions.push(eq(shortcuts.shareWithWorkspace, true));
} else {
// Check for existing personal shortcut
conditions.push(eq(shortcuts.createdBy, validated.userId));

View File

@ -8,7 +8,7 @@ export const CreateShortcutInputSchema = z.object({
instructions: z.string().min(1),
createdBy: z.string().uuid(),
organizationId: z.string().uuid(),
sharedWithWorkspace: z.boolean(),
shareWithWorkspace: z.boolean(),
});
export type CreateShortcutInput = z.infer<typeof CreateShortcutInputSchema>;
@ -24,8 +24,8 @@ export async function createShortcut(input: CreateShortcutInput) {
and(
eq(shortcuts.name, validated.name),
eq(shortcuts.organizationId, validated.organizationId),
validated.sharedWithWorkspace
? eq(shortcuts.sharedWithWorkspace, true)
validated.shareWithWorkspace
? eq(shortcuts.shareWithWorkspace, true)
: eq(shortcuts.createdBy, validated.createdBy),
isNotNull(shortcuts.deletedAt)
)
@ -38,7 +38,7 @@ export async function createShortcut(input: CreateShortcutInput) {
.update(shortcuts)
.set({
instructions: validated.instructions,
sharedWithWorkspace: validated.sharedWithWorkspace,
shareWithWorkspace: validated.shareWithWorkspace,
updatedBy: validated.createdBy,
updatedAt: new Date().toISOString(),
deletedAt: null,
@ -58,7 +58,7 @@ export async function createShortcut(input: CreateShortcutInput) {
createdBy: validated.createdBy,
updatedBy: validated.createdBy,
organizationId: validated.organizationId,
sharedWithWorkspace: validated.sharedWithWorkspace,
shareWithWorkspace: validated.shareWithWorkspace,
})
.returning();

View File

@ -40,7 +40,7 @@ export async function findShortcutByName(input: FindShortcutByNameInput) {
and(
eq(shortcuts.name, validated.name),
eq(shortcuts.organizationId, validated.organizationId),
eq(shortcuts.sharedWithWorkspace, true),
eq(shortcuts.shareWithWorkspace, true),
isNull(shortcuts.deletedAt)
)
)

View File

@ -19,7 +19,7 @@ export async function getUserShortcuts(input: GetUserShortcutsInput) {
.where(
and(
eq(shortcuts.organizationId, validated.organizationId),
or(eq(shortcuts.createdBy, validated.userId), eq(shortcuts.sharedWithWorkspace, true)),
or(eq(shortcuts.createdBy, validated.userId), eq(shortcuts.shareWithWorkspace, true)),
isNull(shortcuts.deletedAt)
)
)

View File

@ -7,6 +7,7 @@ export const UpdateShortcutInputSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(255).optional(),
instructions: z.string().min(1).optional(),
shareWithWorkspace: z.boolean().optional(),
updatedBy: z.string().uuid(),
});
@ -28,6 +29,10 @@ export async function updateShortcut(input: UpdateShortcutInput) {
updateData.instructions = validated.instructions;
}
if (validated.shareWithWorkspace !== undefined) {
updateData.shareWithWorkspace = validated.shareWithWorkspace;
}
const [updated] = await db
.update(shortcuts)
.set(updateData)

View File

@ -2327,7 +2327,7 @@ export const shortcuts = pgTable(
createdBy: uuid('created_by').notNull(),
updatedBy: uuid('updated_by'),
organizationId: uuid('organization_id').notNull(),
sharedWithWorkspace: boolean('shared_with_workspace').default(false).notNull(),
shareWithWorkspace: boolean('share_with_workspace').default(false).notNull(),
createdAt: timestamp('created_at', { withTimezone: true, mode: 'string' })
.defaultNow()
.notNull(),
@ -2365,7 +2365,7 @@ export const shortcuts = pgTable(
// Conditional unique constraint for workspace shortcuts
uniqueIndex('shortcuts_workspace_unique')
.on(table.name, table.organizationId)
.where(sql`${table.sharedWithWorkspace} = true`),
.where(sql`${table.shareWithWorkspace} = true`),
]
);

View File

@ -19,7 +19,7 @@ export const createShortcutRequestSchema = z.object({
.string()
.min(1, 'Instructions are required')
.max(10000, 'Instructions must be 10,000 characters or less'),
sharedWithWorkspace: z.boolean(),
shareWithWorkspace: z.boolean(),
});
export const updateShortcutRequestSchema = z.object({
@ -29,6 +29,7 @@ export const updateShortcutRequestSchema = z.object({
.min(1, 'Instructions are required')
.max(10000, 'Instructions must be 10,000 characters or less')
.optional(),
shareWithWorkspace: z.boolean().optional(),
});
// Export types inferred from schemas

View File

@ -7,7 +7,7 @@ export const shortcutSchema = z.object({
createdBy: z.string().uuid(),
updatedBy: z.string().uuid().nullable(),
organizationId: z.string().uuid(),
sharedWithWorkspace: z.boolean(),
shareWithWorkspace: z.boolean(),
createdAt: z.string(), // ISO string
updatedAt: z.string(), // ISO string
deletedAt: z.string().nullable(), // ISO string or null