mirror of https://github.com/buster-so/buster.git
Merge pull request #1250 from buster-so/wells-bus-2026-create-backend-for-library
Adding the Backend for library
This commit is contained in:
commit
e3a14d9d99
|
@ -10,6 +10,7 @@ import dictionariesRoutes from './dictionaries';
|
|||
import docsRoutes from './docs';
|
||||
import electricShapeRoutes from './electric-shape';
|
||||
import githubRoutes from './github';
|
||||
import libraryRoutes from './library';
|
||||
import metricFilesRoutes from './metric_files';
|
||||
import organizationRoutes from './organization';
|
||||
import publicRoutes from './public';
|
||||
|
@ -33,6 +34,7 @@ const app = new Hono()
|
|||
.route('/electric-shape', electricShapeRoutes)
|
||||
.route('/healthcheck', healthcheckRoutes)
|
||||
.route('/chats', chatsRoutes)
|
||||
.route('/library', libraryRoutes)
|
||||
.route('/metric_files', metricFilesRoutes)
|
||||
.route('/github', githubRoutes)
|
||||
.route('/slack', slackRoutes)
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
import { checkPermission } from '@buster/access-controls';
|
||||
import { bulkUpdateLibraryField, getUserOrganizationId } from '@buster/database/queries';
|
||||
import type { LibraryDeleteRequestBody, LibraryDeleteResponse } from '@buster/server-shared';
|
||||
import { LibraryDeleteRequestBodySchema } from '@buster/server-shared';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { Hono } from 'hono';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
|
||||
const app = new Hono().delete(
|
||||
'/',
|
||||
zValidator('json', LibraryDeleteRequestBodySchema),
|
||||
async (c) => {
|
||||
const assets = c.req.valid('json');
|
||||
const user = c.get('busterUser');
|
||||
|
||||
const userOrg = await getUserOrganizationId(user.id);
|
||||
if (!userOrg) {
|
||||
throw new HTTPException(403, { message: 'User not associated with any organization' });
|
||||
}
|
||||
|
||||
const failedAssets: LibraryDeleteResponse['failedItems'] = [];
|
||||
const assetsToUpdate: LibraryDeleteRequestBody = [];
|
||||
|
||||
const permissionCheckPromises = assets.map((asset) =>
|
||||
checkPermission({
|
||||
userId: user.id,
|
||||
assetId: asset.assetId,
|
||||
assetType: asset.assetType,
|
||||
organizationId: userOrg.organizationId,
|
||||
requiredRole: 'full_access',
|
||||
})
|
||||
);
|
||||
|
||||
const permissionChecks = await Promise.all(permissionCheckPromises);
|
||||
|
||||
for (let i = 0; i < assets.length; i++) {
|
||||
const permissionCheck = permissionChecks[i];
|
||||
const asset = assets[i];
|
||||
|
||||
if (!asset || !permissionCheck) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (permissionCheck.hasAccess) {
|
||||
assetsToUpdate.push(asset);
|
||||
} else {
|
||||
failedAssets.push({
|
||||
assetId: asset.assetId,
|
||||
assetType: asset.assetType,
|
||||
error: 'User does not have permission to save to library',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updateResult = await bulkUpdateLibraryField(assetsToUpdate, false);
|
||||
const success = updateResult.success && failedAssets.length === 0;
|
||||
|
||||
const output: LibraryDeleteResponse = {
|
||||
success,
|
||||
successItems: updateResult.successItems,
|
||||
failedItems: [...failedAssets, ...updateResult.failedItems],
|
||||
};
|
||||
|
||||
return c.json(output);
|
||||
}
|
||||
);
|
||||
|
||||
export default app;
|
|
@ -0,0 +1,40 @@
|
|||
import { getUserOrganizationId, listPermissionedLibraryAssets } from '@buster/database/queries';
|
||||
import { GetLibraryAssetsRequestQuerySchema, type LibraryGetResponse } from '@buster/server-shared';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { Hono } from 'hono';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
|
||||
const app = new Hono().get(
|
||||
'/',
|
||||
zValidator('query', GetLibraryAssetsRequestQuerySchema),
|
||||
async (c) => {
|
||||
const { page, page_size, assetTypes, endDate, startDate, includeCreatedBy, excludeCreatedBy } =
|
||||
c.req.valid('query');
|
||||
const user = c.get('busterUser');
|
||||
|
||||
// Get user's organization
|
||||
const userOrg = await getUserOrganizationId(user.id);
|
||||
if (!userOrg?.organizationId) {
|
||||
throw new HTTPException(403, { message: 'User not associated with any organization' });
|
||||
}
|
||||
try {
|
||||
const response: LibraryGetResponse = await listPermissionedLibraryAssets({
|
||||
userId: user.id,
|
||||
organizationId: userOrg.organizationId,
|
||||
page,
|
||||
page_size,
|
||||
assetTypes,
|
||||
endDate,
|
||||
startDate,
|
||||
includeCreatedBy,
|
||||
excludeCreatedBy,
|
||||
});
|
||||
return c.json(response);
|
||||
} catch (error) {
|
||||
console.error('Error while listing permissioned library assets:', error);
|
||||
throw new HTTPException(500, { message: 'Error while listing library assets' });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export default app;
|
|
@ -0,0 +1,66 @@
|
|||
import { checkPermission } from '@buster/access-controls';
|
||||
import { bulkUpdateLibraryField, getUserOrganizationId } from '@buster/database/queries';
|
||||
import type { LibraryAssetIdentifier } from '@buster/database/schema-types';
|
||||
import {
|
||||
type LibraryPostRequestBody,
|
||||
LibraryPostRequestBodySchema,
|
||||
type LibraryPostResponse,
|
||||
} from '@buster/server-shared';
|
||||
import { zValidator } from '@hono/zod-validator';
|
||||
import { Hono } from 'hono';
|
||||
import { HTTPException } from 'hono/http-exception';
|
||||
|
||||
const app = new Hono().post('/', zValidator('json', LibraryPostRequestBodySchema), async (c) => {
|
||||
const assets = c.req.valid('json');
|
||||
const user = c.get('busterUser');
|
||||
|
||||
const userOrg = await getUserOrganizationId(user.id);
|
||||
if (!userOrg) {
|
||||
throw new HTTPException(403, { message: 'User not associated with any organization' });
|
||||
}
|
||||
|
||||
const failedAssets: LibraryPostResponse['failedItems'] = [];
|
||||
const assetsToSave: LibraryPostRequestBody = [];
|
||||
|
||||
// User needs direct full_access permissions to save to library
|
||||
const permissionCheckPromises = assets.map((asset) =>
|
||||
checkPermission({
|
||||
userId: user.id,
|
||||
assetId: asset.assetId,
|
||||
assetType: asset.assetType,
|
||||
organizationId: userOrg.organizationId,
|
||||
requiredRole: 'full_access',
|
||||
})
|
||||
);
|
||||
|
||||
const permissionChecks = await Promise.all(permissionCheckPromises);
|
||||
|
||||
for (let i = 0; i < assets.length; i++) {
|
||||
const permissionCheck = permissionChecks[i];
|
||||
const asset = assets[i];
|
||||
if (permissionCheck && asset) {
|
||||
if (permissionCheck.hasAccess) {
|
||||
assetsToSave.push(asset);
|
||||
} else {
|
||||
failedAssets.push({
|
||||
assetId: asset.assetId,
|
||||
assetType: asset.assetType,
|
||||
error: 'User does not have permission to save to library',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const savedAssetResponse = await bulkUpdateLibraryField(assetsToSave, true);
|
||||
const success = savedAssetResponse.success && failedAssets.length === 0;
|
||||
|
||||
const output: LibraryPostResponse = {
|
||||
success,
|
||||
successItems: savedAssetResponse.successItems,
|
||||
failedItems: [...failedAssets, ...savedAssetResponse.failedItems],
|
||||
};
|
||||
|
||||
return c.json(output);
|
||||
});
|
||||
|
||||
export default app;
|
|
@ -0,0 +1,15 @@
|
|||
import { Hono } from 'hono';
|
||||
import { requireAuth } from '../../../middleware/auth';
|
||||
import { standardErrorHandler } from '../../../utils/response';
|
||||
import DELETE from './DELETE';
|
||||
import GET from './GET';
|
||||
import POST from './POST';
|
||||
|
||||
const app = new Hono()
|
||||
.use('*', requireAuth)
|
||||
.route('/', GET)
|
||||
.route('/', POST)
|
||||
.route('/', DELETE)
|
||||
.onError(standardErrorHandler);
|
||||
|
||||
export default app;
|
|
@ -101,6 +101,7 @@ describe('metric-helpers', () => {
|
|||
workspaceSharingEnabledBy: null,
|
||||
deletedAt: null,
|
||||
screenshotBucketKey: null,
|
||||
savedToLibrary: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
ALTER TABLE "chats" ADD COLUMN "saved_to_library" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "dashboard_files" ADD COLUMN "saved_to_library" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "metric_files" ADD COLUMN "saved_to_library" boolean DEFAULT false NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "report_files" ADD COLUMN "saved_to_library" boolean DEFAULT false NOT NULL;
|
File diff suppressed because it is too large
Load Diff
|
@ -820,6 +820,13 @@
|
|||
"when": 1759357134333,
|
||||
"tag": "0117_careful_alex_power",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 118,
|
||||
"version": "7",
|
||||
"when": 1759428123492,
|
||||
"tag": "0118_violet_vin_gonzales",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
import { and, db, eq, isNull } from '../../connection';
|
||||
import { chats, dashboardFiles, metricFiles, reportFiles } from '../../schema';
|
||||
import type {
|
||||
BulkUpdateLibraryFieldInput,
|
||||
BulkUpdateLibraryFieldResponse,
|
||||
LibraryAssetType,
|
||||
} from '../../schema-types';
|
||||
|
||||
type LibraryAssetTable =
|
||||
| typeof chats
|
||||
| typeof dashboardFiles
|
||||
| typeof metricFiles
|
||||
| typeof reportFiles;
|
||||
|
||||
export const libraryAssetTableMap: Record<LibraryAssetType, LibraryAssetTable> = {
|
||||
chat: chats,
|
||||
dashboard_file: dashboardFiles,
|
||||
metric_file: metricFiles,
|
||||
report_file: reportFiles,
|
||||
};
|
||||
|
||||
export async function bulkUpdateLibraryField(
|
||||
input: BulkUpdateLibraryFieldInput,
|
||||
savedToLibrary: boolean
|
||||
): Promise<BulkUpdateLibraryFieldResponse> {
|
||||
const failedItems: BulkUpdateLibraryFieldResponse['failedItems'] = [];
|
||||
const successItems: BulkUpdateLibraryFieldResponse['successItems'] = [];
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (const asset of input) {
|
||||
promises.push(updateAssetLibraryField(asset.assetId, asset.assetType, savedToLibrary));
|
||||
}
|
||||
const results = await Promise.allSettled(promises);
|
||||
|
||||
for (let i = 0; i < results.length; i++) {
|
||||
const result = results[i];
|
||||
const asset = input[i];
|
||||
|
||||
if (asset && result) {
|
||||
if (result.status === 'fulfilled') {
|
||||
successItems.push(asset);
|
||||
} else {
|
||||
failedItems.push({
|
||||
...asset,
|
||||
error: result.reason?.message || 'Unknown error',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const success = failedItems.length === 0;
|
||||
|
||||
return {
|
||||
success,
|
||||
successItems,
|
||||
failedItems,
|
||||
};
|
||||
}
|
||||
|
||||
async function updateAssetLibraryField(
|
||||
assetId: string,
|
||||
assetType: LibraryAssetType,
|
||||
savedToLibrary: boolean
|
||||
): Promise<void> {
|
||||
const table = libraryAssetTableMap[assetType];
|
||||
|
||||
await db
|
||||
.update(table)
|
||||
.set({
|
||||
savedToLibrary,
|
||||
updatedAt: new Date().toISOString(),
|
||||
})
|
||||
.where(and(eq(table.id, assetId), isNull(table.deletedAt)));
|
||||
}
|
|
@ -53,3 +53,7 @@ export {
|
|||
GetAssetScreenshotBucketKeyInputSchema,
|
||||
type GetAssetScreenshotBucketKeyInput,
|
||||
} from './get-asset-screenshot-bucket-key';
|
||||
|
||||
export { bulkUpdateLibraryField } from './bulk-update-asset-library-field';
|
||||
|
||||
export { listPermissionedLibraryAssets } from './list-permissioned-library-assets';
|
||||
|
|
|
@ -0,0 +1,278 @@
|
|||
import {
|
||||
type SQL,
|
||||
and,
|
||||
count,
|
||||
desc,
|
||||
eq,
|
||||
exists,
|
||||
gte,
|
||||
inArray,
|
||||
isNull,
|
||||
lte,
|
||||
ne,
|
||||
not,
|
||||
or,
|
||||
sql,
|
||||
} from 'drizzle-orm';
|
||||
import { db } from '../../connection';
|
||||
import {
|
||||
assetPermissions,
|
||||
chats,
|
||||
dashboardFiles,
|
||||
metricFiles,
|
||||
reportFiles,
|
||||
users,
|
||||
} from '../../schema';
|
||||
import {
|
||||
type LibraryAssetListItem,
|
||||
type LibraryAssetType,
|
||||
type ListPermissionedLibraryAssetsInput,
|
||||
ListPermissionedLibraryAssetsInputSchema,
|
||||
type ListPermissionedLibraryAssetsResponse,
|
||||
createPaginatedResponse,
|
||||
} from '../../schema-types';
|
||||
|
||||
export async function listPermissionedLibraryAssets(
|
||||
input: ListPermissionedLibraryAssetsInput
|
||||
): Promise<ListPermissionedLibraryAssetsResponse> {
|
||||
const {
|
||||
organizationId,
|
||||
userId,
|
||||
assetTypes,
|
||||
createdById,
|
||||
startDate,
|
||||
endDate,
|
||||
includeCreatedBy,
|
||||
excludeCreatedBy,
|
||||
page,
|
||||
page_size,
|
||||
} = ListPermissionedLibraryAssetsInputSchema.parse(input);
|
||||
|
||||
const offset = (page - 1) * page_size;
|
||||
|
||||
const permissionedReportFiles = db
|
||||
.select({
|
||||
assetId: reportFiles.id,
|
||||
assetType: sql`'report_file'::asset_type_enum`.as('assetType'),
|
||||
name: reportFiles.name,
|
||||
createdAt: reportFiles.createdAt,
|
||||
updatedAt: reportFiles.updatedAt,
|
||||
createdBy: reportFiles.createdBy,
|
||||
organizationId: reportFiles.organizationId,
|
||||
})
|
||||
.from(reportFiles)
|
||||
.where(
|
||||
and(
|
||||
eq(reportFiles.organizationId, organizationId),
|
||||
eq(reportFiles.savedToLibrary, true),
|
||||
isNull(reportFiles.deletedAt),
|
||||
or(
|
||||
ne(reportFiles.workspaceSharing, 'none'),
|
||||
exists(
|
||||
db
|
||||
.select({ value: sql`1` })
|
||||
.from(assetPermissions)
|
||||
.where(
|
||||
and(
|
||||
eq(assetPermissions.assetId, reportFiles.id),
|
||||
eq(assetPermissions.assetType, 'report_file'),
|
||||
eq(assetPermissions.identityId, userId),
|
||||
eq(assetPermissions.identityType, 'user'),
|
||||
isNull(assetPermissions.deletedAt)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const permissionedMetricFiles = db
|
||||
.select({
|
||||
assetId: metricFiles.id,
|
||||
assetType: sql`'metric_file'::asset_type_enum`.as('assetType'),
|
||||
name: metricFiles.name,
|
||||
createdAt: metricFiles.createdAt,
|
||||
updatedAt: metricFiles.updatedAt,
|
||||
createdBy: metricFiles.createdBy,
|
||||
organizationId: metricFiles.organizationId,
|
||||
})
|
||||
.from(metricFiles)
|
||||
.where(
|
||||
and(
|
||||
eq(metricFiles.organizationId, organizationId),
|
||||
eq(metricFiles.savedToLibrary, true),
|
||||
isNull(metricFiles.deletedAt),
|
||||
or(
|
||||
ne(metricFiles.workspaceSharing, 'none'),
|
||||
exists(
|
||||
db
|
||||
.select({ value: sql`1` })
|
||||
.from(assetPermissions)
|
||||
.where(
|
||||
and(
|
||||
eq(assetPermissions.assetId, metricFiles.id),
|
||||
eq(assetPermissions.assetType, 'metric_file'),
|
||||
eq(assetPermissions.identityId, userId),
|
||||
eq(assetPermissions.identityType, 'user'),
|
||||
isNull(assetPermissions.deletedAt)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const permissionedDashboardFiles = db
|
||||
.select({
|
||||
assetId: dashboardFiles.id,
|
||||
assetType: sql`'dashboard_file'::asset_type_enum`.as('assetType'),
|
||||
name: dashboardFiles.name,
|
||||
createdAt: dashboardFiles.createdAt,
|
||||
updatedAt: dashboardFiles.updatedAt,
|
||||
createdBy: dashboardFiles.createdBy,
|
||||
organizationId: dashboardFiles.organizationId,
|
||||
})
|
||||
.from(dashboardFiles)
|
||||
.where(
|
||||
and(
|
||||
eq(dashboardFiles.organizationId, organizationId),
|
||||
eq(dashboardFiles.savedToLibrary, true),
|
||||
isNull(dashboardFiles.deletedAt),
|
||||
or(
|
||||
ne(dashboardFiles.workspaceSharing, 'none'),
|
||||
exists(
|
||||
db
|
||||
.select({ value: sql`1` })
|
||||
.from(assetPermissions)
|
||||
.where(
|
||||
and(
|
||||
eq(assetPermissions.assetId, dashboardFiles.id),
|
||||
eq(assetPermissions.assetType, 'dashboard_file'),
|
||||
eq(assetPermissions.identityId, userId),
|
||||
eq(assetPermissions.identityType, 'user'),
|
||||
isNull(assetPermissions.deletedAt)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const permissionedChats = db
|
||||
.select({
|
||||
assetId: chats.id,
|
||||
assetType: sql`'chat'::asset_type_enum`.as('assetType'),
|
||||
name: chats.title,
|
||||
createdAt: chats.createdAt,
|
||||
updatedAt: chats.updatedAt,
|
||||
createdBy: chats.createdBy,
|
||||
organizationId: chats.organizationId,
|
||||
})
|
||||
.from(chats)
|
||||
.where(
|
||||
and(
|
||||
eq(chats.organizationId, organizationId),
|
||||
eq(chats.savedToLibrary, true),
|
||||
isNull(chats.deletedAt),
|
||||
or(
|
||||
ne(chats.workspaceSharing, 'none'),
|
||||
exists(
|
||||
db
|
||||
.select({ value: sql`1` })
|
||||
.from(assetPermissions)
|
||||
.where(
|
||||
and(
|
||||
eq(assetPermissions.assetId, chats.id),
|
||||
eq(assetPermissions.assetType, 'chat'),
|
||||
eq(assetPermissions.identityId, userId),
|
||||
eq(assetPermissions.identityType, 'user'),
|
||||
isNull(assetPermissions.deletedAt)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
const permissionedAssets = permissionedReportFiles
|
||||
.union(permissionedMetricFiles)
|
||||
.union(permissionedDashboardFiles)
|
||||
.union(permissionedChats)
|
||||
.as('permissioned_assets');
|
||||
|
||||
const filters: SQL[] = [];
|
||||
|
||||
if (assetTypes && assetTypes.length > 0) {
|
||||
filters.push(inArray(permissionedAssets.assetType, assetTypes));
|
||||
}
|
||||
|
||||
if (createdById) {
|
||||
filters.push(eq(permissionedAssets.createdBy, createdById));
|
||||
}
|
||||
|
||||
if (startDate) {
|
||||
filters.push(gte(permissionedAssets.createdAt, startDate));
|
||||
}
|
||||
|
||||
if (endDate) {
|
||||
filters.push(lte(permissionedAssets.createdAt, endDate));
|
||||
}
|
||||
|
||||
if (includeCreatedBy && includeCreatedBy.length > 0) {
|
||||
filters.push(inArray(permissionedAssets.createdBy, includeCreatedBy));
|
||||
}
|
||||
|
||||
if (excludeCreatedBy && excludeCreatedBy.length > 0) {
|
||||
filters.push(not(inArray(permissionedAssets.createdBy, excludeCreatedBy)));
|
||||
}
|
||||
|
||||
const whereCondition =
|
||||
filters.length === 0 ? undefined : filters.length === 1 ? filters[0] : and(...filters);
|
||||
|
||||
const baseAssetQuery = db
|
||||
.select({
|
||||
assetId: permissionedAssets.assetId,
|
||||
assetType: permissionedAssets.assetType,
|
||||
name: permissionedAssets.name,
|
||||
updatedAt: permissionedAssets.updatedAt,
|
||||
createdAt: permissionedAssets.createdAt,
|
||||
createdBy: permissionedAssets.createdBy,
|
||||
createdByName: users.name,
|
||||
createdByEmail: users.email,
|
||||
createdByAvatarUrl: users.avatarUrl,
|
||||
})
|
||||
.from(permissionedAssets)
|
||||
.innerJoin(users, eq(permissionedAssets.createdBy, users.id));
|
||||
|
||||
const filteredAssetQuery = whereCondition ? baseAssetQuery.where(whereCondition) : baseAssetQuery;
|
||||
|
||||
const assetsResult = await filteredAssetQuery
|
||||
.orderBy(desc(permissionedAssets.createdAt))
|
||||
.limit(page_size)
|
||||
.offset(offset);
|
||||
|
||||
const baseCountQuery = db.select({ total: count() }).from(permissionedAssets);
|
||||
const countResult = await (whereCondition
|
||||
? baseCountQuery.where(whereCondition)
|
||||
: baseCountQuery);
|
||||
const totalValue = countResult[0]?.total ?? 0;
|
||||
|
||||
const libraryAssets: LibraryAssetListItem[] = assetsResult.map((asset) => ({
|
||||
asset_id: asset.assetId,
|
||||
asset_type: asset.assetType as LibraryAssetType,
|
||||
name: asset.name ?? '',
|
||||
created_at: asset.createdAt,
|
||||
updated_at: asset.updatedAt,
|
||||
created_by: asset.createdBy,
|
||||
created_by_name: asset.createdByName,
|
||||
created_by_email: asset.createdByEmail,
|
||||
created_by_avatar_url: asset.createdByAvatarUrl,
|
||||
}));
|
||||
|
||||
return createPaginatedResponse({
|
||||
data: libraryAssets,
|
||||
page,
|
||||
page_size,
|
||||
total: totalValue,
|
||||
});
|
||||
}
|
|
@ -21,7 +21,7 @@ type ScreenshotTable =
|
|||
| typeof metricFiles
|
||||
| typeof reportFiles;
|
||||
|
||||
const assetTableMap: Record<AssetType, ScreenshotTable> = {
|
||||
export const assetTableMap: Record<AssetType, ScreenshotTable> = {
|
||||
chat: chats,
|
||||
collection: collections,
|
||||
dashboard_file: dashboardFiles,
|
||||
|
|
|
@ -39,3 +39,5 @@ export * from './search';
|
|||
export * from './chat';
|
||||
|
||||
export * from './pagination';
|
||||
|
||||
export * from './library';
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
import z from 'zod';
|
||||
import { AssetTypeSchema } from './asset';
|
||||
import { type PaginatedResponse, PaginationInputSchema } from './pagination';
|
||||
|
||||
export const LibraryAssetTypeSchema = AssetTypeSchema.exclude(['collection']);
|
||||
export type LibraryAssetType = z.infer<typeof LibraryAssetTypeSchema>;
|
||||
|
||||
export const LibraryAssetIdentifierSchema = z.object({
|
||||
assetId: z.string().uuid(),
|
||||
assetType: LibraryAssetTypeSchema,
|
||||
});
|
||||
export type LibraryAssetIdentifier = z.infer<typeof LibraryAssetIdentifierSchema>;
|
||||
|
||||
export const BulkUpdateLibraryFieldInputSchema = z.array(LibraryAssetIdentifierSchema);
|
||||
export type BulkUpdateLibraryFieldInput = z.infer<typeof BulkUpdateLibraryFieldInputSchema>;
|
||||
|
||||
export const BulkUpdateLibraryFieldResponseSchema = z.object({
|
||||
success: z.boolean(),
|
||||
successItems: z.array(LibraryAssetIdentifierSchema),
|
||||
failedItems: z.array(
|
||||
LibraryAssetIdentifierSchema.extend({
|
||||
error: z.string(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
export type BulkUpdateLibraryFieldResponse = z.infer<typeof BulkUpdateLibraryFieldResponseSchema>;
|
||||
|
||||
export const LibraryAssetListItemSchema = z.object({
|
||||
asset_id: z.string().uuid(),
|
||||
asset_type: LibraryAssetTypeSchema,
|
||||
name: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
created_by: z.string().uuid(),
|
||||
created_by_name: z.string().nullable(),
|
||||
created_by_email: z.string(),
|
||||
created_by_avatar_url: z.string().nullable(),
|
||||
});
|
||||
|
||||
export type LibraryAssetListItem = z.infer<typeof LibraryAssetListItemSchema>;
|
||||
|
||||
export const ListPermissionedLibraryAssetsInputSchema = z
|
||||
.object({
|
||||
organizationId: z.string().uuid(),
|
||||
userId: z.string().uuid(),
|
||||
assetTypes: LibraryAssetTypeSchema.array().min(1).optional(),
|
||||
createdById: z.string().uuid().optional(),
|
||||
startDate: z.string().datetime().optional(),
|
||||
endDate: z.string().datetime().optional(),
|
||||
includeCreatedBy: z.string().uuid().array().optional(),
|
||||
excludeCreatedBy: z.string().uuid().array().optional(),
|
||||
})
|
||||
.merge(PaginationInputSchema);
|
||||
|
||||
export type ListPermissionedLibraryAssetsInput = z.infer<
|
||||
typeof ListPermissionedLibraryAssetsInputSchema
|
||||
>;
|
||||
|
||||
export type ListPermissionedLibraryAssetsResponse = PaginatedResponse<LibraryAssetListItem>;
|
|
@ -743,6 +743,7 @@ export const dashboardFiles = pgTable(
|
|||
mode: 'string',
|
||||
}),
|
||||
screenshotBucketKey: text('screenshot_bucket_key'),
|
||||
savedToLibrary: boolean('saved_to_library').default(false).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('dashboard_files_created_by_idx').using(
|
||||
|
@ -817,6 +818,7 @@ export const reportFiles = pgTable(
|
|||
mode: 'string',
|
||||
}),
|
||||
screenshotBucketKey: text('screenshot_bucket_key'),
|
||||
savedToLibrary: boolean('saved_to_library').default(false).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('report_files_created_by_idx').using(
|
||||
|
@ -889,6 +891,7 @@ export const chats = pgTable(
|
|||
mode: 'string',
|
||||
}),
|
||||
screenshotBucketKey: text('screenshot_bucket_key'),
|
||||
savedToLibrary: boolean('saved_to_library').default(false).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('chats_created_at_idx').using(
|
||||
|
@ -1040,6 +1043,7 @@ export const metricFiles = pgTable(
|
|||
mode: 'string',
|
||||
}),
|
||||
screenshotBucketKey: text('screenshot_bucket_key'),
|
||||
savedToLibrary: boolean('saved_to_library').default(false).notNull(),
|
||||
},
|
||||
(table) => [
|
||||
index('metric_files_created_by_idx').using(
|
||||
|
|
|
@ -107,6 +107,10 @@
|
|||
"./screenshots": {
|
||||
"types": "./dist/screenshots/index.d.ts",
|
||||
"default": "./dist/screenshots/index.js"
|
||||
},
|
||||
"./library": {
|
||||
"types": "./dist/library/index.d.ts",
|
||||
"default": "./dist/library/index.js"
|
||||
}
|
||||
},
|
||||
"module": "src/index.ts",
|
||||
|
|
|
@ -37,3 +37,4 @@ export * from './shortcuts';
|
|||
export * from './healthcheck';
|
||||
export * from './sql';
|
||||
export * from './logs-writeback';
|
||||
export * from './library';
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export * from './requests';
|
||||
export * from './responses';
|
|
@ -0,0 +1,48 @@
|
|||
import {
|
||||
type BulkUpdateLibraryFieldInput,
|
||||
BulkUpdateLibraryFieldInputSchema,
|
||||
LibraryAssetTypeSchema,
|
||||
} from '@buster/database/schema-types';
|
||||
import { z } from 'zod';
|
||||
import { PaginatedRequestSchema } from '../type-utilities';
|
||||
|
||||
export const GetLibraryAssetsRequestQuerySchema = z
|
||||
.object({
|
||||
assetTypes: z
|
||||
.preprocess((val) => {
|
||||
if (typeof val === 'string') {
|
||||
return [val];
|
||||
}
|
||||
return val;
|
||||
}, LibraryAssetTypeSchema.array().min(1))
|
||||
.optional(),
|
||||
startDate: z.string().datetime().optional(),
|
||||
endDate: z.string().datetime().optional(),
|
||||
includeCreatedBy: z
|
||||
.preprocess((val) => {
|
||||
if (typeof val === 'string') {
|
||||
return [val];
|
||||
}
|
||||
return val;
|
||||
}, z.string().uuid().array())
|
||||
.optional(),
|
||||
excludeCreatedBy: z
|
||||
.preprocess((val) => {
|
||||
if (typeof val === 'string') {
|
||||
return [val];
|
||||
}
|
||||
return val;
|
||||
}, z.string().uuid().array())
|
||||
.optional(),
|
||||
})
|
||||
.merge(PaginatedRequestSchema);
|
||||
|
||||
export type GetLibraryAssetsRequestQuery = z.infer<typeof GetLibraryAssetsRequestQuerySchema>;
|
||||
|
||||
export const LibraryPostRequestBodySchema = BulkUpdateLibraryFieldInputSchema;
|
||||
|
||||
export type LibraryPostRequestBody = BulkUpdateLibraryFieldInput;
|
||||
|
||||
export const LibraryDeleteRequestBodySchema = BulkUpdateLibraryFieldInputSchema;
|
||||
|
||||
export type LibraryDeleteRequestBody = BulkUpdateLibraryFieldInput;
|
|
@ -0,0 +1,15 @@
|
|||
import {
|
||||
type BulkUpdateLibraryFieldResponse,
|
||||
BulkUpdateLibraryFieldResponseSchema,
|
||||
type ListPermissionedLibraryAssetsResponse,
|
||||
} from '@buster/database/schema-types';
|
||||
|
||||
export type LibraryGetResponse = ListPermissionedLibraryAssetsResponse;
|
||||
|
||||
export const LibraryPostResponseSchema = BulkUpdateLibraryFieldResponseSchema;
|
||||
|
||||
export type LibraryPostResponse = BulkUpdateLibraryFieldResponse;
|
||||
|
||||
export const LibraryDeleteResponseSchema = BulkUpdateLibraryFieldResponseSchema;
|
||||
|
||||
export type LibraryDeleteResponse = BulkUpdateLibraryFieldResponse;
|
Loading…
Reference in New Issue