feat: improve workspace index
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "blobs_workspace_id_status_deleted_at_idx" ON "blobs"("workspace_id", "status", "deleted_at");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "workspace_pages_workspace_id_public_idx" ON "workspace_pages"("workspace_id", "public");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "workspace_user_permissions_workspace_id_type_status_idx" ON "workspace_user_permissions"("workspace_id", "type", "status");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "workspaces_created_at_idx" ON "workspaces"("created_at");
|
||||||
@@ -135,6 +135,7 @@ model Workspace {
|
|||||||
commentAttachments CommentAttachment[]
|
commentAttachments CommentAttachment[]
|
||||||
|
|
||||||
@@index([lastCheckEmbeddings])
|
@@index([lastCheckEmbeddings])
|
||||||
|
@@index([createdAt])
|
||||||
@@map("workspaces")
|
@@map("workspaces")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,6 +160,7 @@ model WorkspaceDoc {
|
|||||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@id([workspaceId, docId])
|
@@id([workspaceId, docId])
|
||||||
|
@@index([workspaceId, public])
|
||||||
@@map("workspace_pages")
|
@@map("workspace_pages")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,6 +208,7 @@ model WorkspaceUserRole {
|
|||||||
|
|
||||||
@@unique([workspaceId, userId])
|
@@unique([workspaceId, userId])
|
||||||
// optimize for querying user's workspace permissions
|
// optimize for querying user's workspace permissions
|
||||||
|
@@index([workspaceId, type, status])
|
||||||
@@index(userId)
|
@@index(userId)
|
||||||
@@map("workspace_user_permissions")
|
@@map("workspace_user_permissions")
|
||||||
}
|
}
|
||||||
@@ -832,6 +835,7 @@ model Blob {
|
|||||||
AiWorkspaceBlobEmbedding AiWorkspaceBlobEmbedding[]
|
AiWorkspaceBlobEmbedding AiWorkspaceBlobEmbedding[]
|
||||||
|
|
||||||
@@id([workspaceId, key])
|
@@id([workspaceId, key])
|
||||||
|
@@index([workspaceId, status, deletedAt])
|
||||||
@@map("blobs")
|
@@map("blobs")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ export class AdminWorkspaceResolver {
|
|||||||
keyword: filter.keyword,
|
keyword: filter.keyword,
|
||||||
features: filter.features,
|
features: filter.features,
|
||||||
order: this.mapSort(filter.orderBy),
|
order: this.mapSort(filter.orderBy),
|
||||||
|
includeTotal: false,
|
||||||
});
|
});
|
||||||
return rows;
|
return rows;
|
||||||
}
|
}
|
||||||
@@ -174,11 +175,9 @@ export class AdminWorkspaceResolver {
|
|||||||
@Args('filter', { type: () => ListWorkspaceInput })
|
@Args('filter', { type: () => ListWorkspaceInput })
|
||||||
filter: ListWorkspaceInput
|
filter: ListWorkspaceInput
|
||||||
) {
|
) {
|
||||||
const { total } = await this.models.workspace.adminListWorkspaces({
|
const total = await this.models.workspace.adminCountWorkspaces({
|
||||||
...filter,
|
keyword: filter.keyword,
|
||||||
first: 1,
|
features: filter.features,
|
||||||
skip: 0,
|
|
||||||
order: this.mapSort(filter.orderBy),
|
|
||||||
});
|
});
|
||||||
return total;
|
return total;
|
||||||
}
|
}
|
||||||
@@ -193,6 +192,7 @@ export class AdminWorkspaceResolver {
|
|||||||
skip: 0,
|
skip: 0,
|
||||||
keyword: id,
|
keyword: id,
|
||||||
order: 'createdAt',
|
order: 'createdAt',
|
||||||
|
includeTotal: false,
|
||||||
});
|
});
|
||||||
const row = rows.find(r => r.id === id);
|
const row = rows.find(r => r.id === id);
|
||||||
if (!row) {
|
if (!row) {
|
||||||
@@ -281,6 +281,7 @@ export class AdminWorkspaceResolver {
|
|||||||
skip: 0,
|
skip: 0,
|
||||||
keyword: id,
|
keyword: id,
|
||||||
order: 'createdAt',
|
order: 'createdAt',
|
||||||
|
includeTotal: false,
|
||||||
});
|
});
|
||||||
const row = rows.find(r => r.id === id);
|
const row = rows.find(r => r.id === id);
|
||||||
if (!row) {
|
if (!row) {
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ type RawWorkspaceSummary = {
|
|||||||
ownerName: string | null;
|
ownerName: string | null;
|
||||||
ownerEmail: string | null;
|
ownerEmail: string | null;
|
||||||
ownerAvatarUrl: string | null;
|
ownerAvatarUrl: string | null;
|
||||||
total: bigint | number;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type AdminWorkspaceSummary = {
|
export type AdminWorkspaceSummary = {
|
||||||
@@ -187,12 +186,21 @@ export class WorkspaceModel extends BaseModel {
|
|||||||
keyword?: string | null;
|
keyword?: string | null;
|
||||||
features?: WorkspaceFeatureName[] | null;
|
features?: WorkspaceFeatureName[] | null;
|
||||||
order?: 'createdAt' | 'snapshotSize' | 'blobCount' | 'blobSize';
|
order?: 'createdAt' | 'snapshotSize' | 'blobCount' | 'blobSize';
|
||||||
|
includeTotal?: boolean;
|
||||||
}): Promise<{ rows: AdminWorkspaceSummary[]; total: number }> {
|
}): Promise<{ rows: AdminWorkspaceSummary[]; total: number }> {
|
||||||
const keyword = options.keyword?.trim();
|
const keyword = options.keyword?.trim();
|
||||||
const features = options.features ?? [];
|
const features = options.features ?? [];
|
||||||
const order = this.buildAdminOrder(options.order);
|
const includeTotal = options.includeTotal ?? true;
|
||||||
|
const total = includeTotal
|
||||||
|
? await this.adminCountWorkspaces({ keyword, features })
|
||||||
|
: 0;
|
||||||
|
if (includeTotal && total === 0) {
|
||||||
|
return { rows: [], total: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
const rows = await this.db.$queryRaw<RawWorkspaceSummary[]>`
|
const rows =
|
||||||
|
options.order === 'createdAt' || !options.order
|
||||||
|
? await this.db.$queryRaw<RawWorkspaceSummary[]>`
|
||||||
WITH feature_set AS (
|
WITH feature_set AS (
|
||||||
SELECT workspace_id, array_agg(DISTINCT name) FILTER (WHERE activated) AS features
|
SELECT workspace_id, array_agg(DISTINCT name) FILTER (WHERE activated) AS features
|
||||||
FROM workspace_features
|
FROM workspace_features
|
||||||
@@ -209,31 +217,7 @@ export class WorkspaceModel extends BaseModel {
|
|||||||
WHERE wur.type = ${WorkspaceRole.Owner}
|
WHERE wur.type = ${WorkspaceRole.Owner}
|
||||||
AND wur.status = ${Prisma.sql`${WorkspaceMemberStatus.Accepted}::"WorkspaceMemberStatus"`}
|
AND wur.status = ${Prisma.sql`${WorkspaceMemberStatus.Accepted}::"WorkspaceMemberStatus"`}
|
||||||
),
|
),
|
||||||
snapshot_stats AS (
|
filtered AS (
|
||||||
SELECT workspace_id,
|
|
||||||
SUM(COALESCE(size, octet_length(blob))) AS snapshot_size,
|
|
||||||
COUNT(*) AS snapshot_count
|
|
||||||
FROM snapshots
|
|
||||||
GROUP BY workspace_id
|
|
||||||
),
|
|
||||||
blob_stats AS (
|
|
||||||
SELECT workspace_id,
|
|
||||||
SUM(size) FILTER (WHERE deleted_at IS NULL AND status = 'completed') AS blob_size,
|
|
||||||
COUNT(*) FILTER (WHERE deleted_at IS NULL AND status = 'completed') AS blob_count
|
|
||||||
FROM blobs
|
|
||||||
GROUP BY workspace_id
|
|
||||||
),
|
|
||||||
member_stats AS (
|
|
||||||
SELECT workspace_id, COUNT(*) AS member_count
|
|
||||||
FROM workspace_user_permissions
|
|
||||||
GROUP BY workspace_id
|
|
||||||
),
|
|
||||||
public_pages AS (
|
|
||||||
SELECT workspace_id, COUNT(*) AS public_page_count
|
|
||||||
FROM workspace_pages
|
|
||||||
WHERE public = true
|
|
||||||
GROUP BY workspace_id
|
|
||||||
)
|
|
||||||
SELECT w.id,
|
SELECT w.id,
|
||||||
w.public,
|
w.public,
|
||||||
w.created_at AS "createdAt",
|
w.created_at AS "createdAt",
|
||||||
@@ -242,25 +226,14 @@ export class WorkspaceModel extends BaseModel {
|
|||||||
w.enable_ai AS "enableAi",
|
w.enable_ai AS "enableAi",
|
||||||
w.enable_url_preview AS "enableUrlPreview",
|
w.enable_url_preview AS "enableUrlPreview",
|
||||||
w.enable_doc_embedding AS "enableDocEmbedding",
|
w.enable_doc_embedding AS "enableDocEmbedding",
|
||||||
COALESCE(ms.member_count, 0) AS "memberCount",
|
|
||||||
COALESCE(pp.public_page_count, 0) AS "publicPageCount",
|
|
||||||
COALESCE(ss.snapshot_count, 0) AS "snapshotCount",
|
|
||||||
COALESCE(ss.snapshot_size, 0) AS "snapshotSize",
|
|
||||||
COALESCE(bs.blob_count, 0) AS "blobCount",
|
|
||||||
COALESCE(bs.blob_size, 0) AS "blobSize",
|
|
||||||
COALESCE(fs.features, ARRAY[]::text[]) AS features,
|
COALESCE(fs.features, ARRAY[]::text[]) AS features,
|
||||||
o.owner_id AS "ownerId",
|
o.owner_id AS "ownerId",
|
||||||
o.owner_name AS "ownerName",
|
o.owner_name AS "ownerName",
|
||||||
o.owner_email AS "ownerEmail",
|
o.owner_email AS "ownerEmail",
|
||||||
o.owner_avatar_url AS "ownerAvatarUrl",
|
o.owner_avatar_url AS "ownerAvatarUrl"
|
||||||
COUNT(*) OVER() AS total
|
|
||||||
FROM workspaces w
|
FROM workspaces w
|
||||||
LEFT JOIN feature_set fs ON fs.workspace_id = w.id
|
LEFT JOIN feature_set fs ON fs.workspace_id = w.id
|
||||||
LEFT JOIN owner o ON o.workspace_id = w.id
|
LEFT JOIN owner o ON o.workspace_id = w.id
|
||||||
LEFT JOIN snapshot_stats ss ON ss.workspace_id = w.id
|
|
||||||
LEFT JOIN blob_stats bs ON bs.workspace_id = w.id
|
|
||||||
LEFT JOIN member_stats ms ON ms.workspace_id = w.id
|
|
||||||
LEFT JOIN public_pages pp ON pp.workspace_id = w.id
|
|
||||||
WHERE ${
|
WHERE ${
|
||||||
keyword
|
keyword
|
||||||
? Prisma.sql`
|
? Prisma.sql`
|
||||||
@@ -277,13 +250,138 @@ export class WorkspaceModel extends BaseModel {
|
|||||||
? Prisma.sql`COALESCE(fs.features, ARRAY[]::text[]) @> ${features}`
|
? Prisma.sql`COALESCE(fs.features, ARRAY[]::text[]) @> ${features}`
|
||||||
: Prisma.sql`TRUE`
|
: Prisma.sql`TRUE`
|
||||||
}
|
}
|
||||||
ORDER BY ${Prisma.raw(order)}
|
ORDER BY w.created_at DESC
|
||||||
|
LIMIT ${options.first}
|
||||||
|
OFFSET ${options.skip}
|
||||||
|
)
|
||||||
|
SELECT f.*,
|
||||||
|
COALESCE(ms.member_count, 0) AS "memberCount",
|
||||||
|
COALESCE(pp.public_page_count, 0) AS "publicPageCount",
|
||||||
|
COALESCE(ss.snapshot_count, 0) AS "snapshotCount",
|
||||||
|
COALESCE(ss.snapshot_size, 0) AS "snapshotSize",
|
||||||
|
COALESCE(bs.blob_count, 0) AS "blobCount",
|
||||||
|
COALESCE(bs.blob_size, 0) AS "blobSize"
|
||||||
|
FROM filtered f
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT COUNT(*) AS member_count
|
||||||
|
FROM workspace_user_permissions
|
||||||
|
WHERE workspace_id = f.id
|
||||||
|
) ms ON TRUE
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT COUNT(*) AS public_page_count
|
||||||
|
FROM workspace_pages
|
||||||
|
WHERE workspace_id = f.id AND public = true
|
||||||
|
) pp ON TRUE
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT COUNT(*) AS snapshot_count,
|
||||||
|
SUM(size) AS snapshot_size
|
||||||
|
FROM snapshots
|
||||||
|
WHERE workspace_id = f.id
|
||||||
|
) ss ON TRUE
|
||||||
|
LEFT JOIN LATERAL (
|
||||||
|
SELECT COUNT(*) FILTER (WHERE deleted_at IS NULL AND status = 'completed') AS blob_count,
|
||||||
|
SUM(size) FILTER (WHERE deleted_at IS NULL AND status = 'completed') AS blob_size
|
||||||
|
FROM blobs
|
||||||
|
WHERE workspace_id = f.id
|
||||||
|
) bs ON TRUE
|
||||||
|
ORDER BY f."createdAt" DESC
|
||||||
|
`
|
||||||
|
: await this.db.$queryRaw<RawWorkspaceSummary[]>`
|
||||||
|
WITH feature_set AS (
|
||||||
|
SELECT workspace_id, array_agg(DISTINCT name) FILTER (WHERE activated) AS features
|
||||||
|
FROM workspace_features
|
||||||
|
GROUP BY workspace_id
|
||||||
|
),
|
||||||
|
owner AS (
|
||||||
|
SELECT wur.workspace_id,
|
||||||
|
u.id AS owner_id,
|
||||||
|
u.name AS owner_name,
|
||||||
|
u.email AS owner_email,
|
||||||
|
u.avatar_url AS owner_avatar_url
|
||||||
|
FROM workspace_user_permissions AS wur
|
||||||
|
JOIN users u ON wur.user_id = u.id
|
||||||
|
WHERE wur.type = ${WorkspaceRole.Owner}
|
||||||
|
AND wur.status = ${Prisma.sql`${WorkspaceMemberStatus.Accepted}::"WorkspaceMemberStatus"`}
|
||||||
|
),
|
||||||
|
filtered AS (
|
||||||
|
SELECT w.id,
|
||||||
|
w.public,
|
||||||
|
w.created_at AS "createdAt",
|
||||||
|
w.name,
|
||||||
|
w.avatar_key AS "avatarKey",
|
||||||
|
w.enable_ai AS "enableAi",
|
||||||
|
w.enable_url_preview AS "enableUrlPreview",
|
||||||
|
w.enable_doc_embedding AS "enableDocEmbedding",
|
||||||
|
COALESCE(fs.features, ARRAY[]::text[]) AS features,
|
||||||
|
o.owner_id AS "ownerId",
|
||||||
|
o.owner_name AS "ownerName",
|
||||||
|
o.owner_email AS "ownerEmail",
|
||||||
|
o.owner_avatar_url AS "ownerAvatarUrl"
|
||||||
|
FROM workspaces w
|
||||||
|
LEFT JOIN feature_set fs ON fs.workspace_id = w.id
|
||||||
|
LEFT JOIN owner o ON o.workspace_id = w.id
|
||||||
|
WHERE ${
|
||||||
|
keyword
|
||||||
|
? Prisma.sql`
|
||||||
|
(
|
||||||
|
w.id ILIKE ${'%' + keyword + '%'}
|
||||||
|
OR o.owner_id ILIKE ${'%' + keyword + '%'}
|
||||||
|
OR o.owner_email ILIKE ${'%' + keyword + '%'}
|
||||||
|
)
|
||||||
|
`
|
||||||
|
: Prisma.sql`TRUE`
|
||||||
|
}
|
||||||
|
AND ${
|
||||||
|
features.length
|
||||||
|
? Prisma.sql`COALESCE(fs.features, ARRAY[]::text[]) @> ${features}`
|
||||||
|
: Prisma.sql`TRUE`
|
||||||
|
}
|
||||||
|
),
|
||||||
|
snapshot_stats AS (
|
||||||
|
SELECT workspace_id,
|
||||||
|
SUM(size) AS snapshot_size,
|
||||||
|
COUNT(*) AS snapshot_count
|
||||||
|
FROM snapshots
|
||||||
|
WHERE workspace_id IN (SELECT id FROM filtered)
|
||||||
|
GROUP BY workspace_id
|
||||||
|
),
|
||||||
|
blob_stats AS (
|
||||||
|
SELECT workspace_id,
|
||||||
|
SUM(size) FILTER (WHERE deleted_at IS NULL AND status = 'completed') AS blob_size,
|
||||||
|
COUNT(*) FILTER (WHERE deleted_at IS NULL AND status = 'completed') AS blob_count
|
||||||
|
FROM blobs
|
||||||
|
WHERE workspace_id IN (SELECT id FROM filtered)
|
||||||
|
GROUP BY workspace_id
|
||||||
|
),
|
||||||
|
member_stats AS (
|
||||||
|
SELECT workspace_id, COUNT(*) AS member_count
|
||||||
|
FROM workspace_user_permissions
|
||||||
|
WHERE workspace_id IN (SELECT id FROM filtered)
|
||||||
|
GROUP BY workspace_id
|
||||||
|
),
|
||||||
|
public_pages AS (
|
||||||
|
SELECT workspace_id, COUNT(*) AS public_page_count
|
||||||
|
FROM workspace_pages
|
||||||
|
WHERE public = true AND workspace_id IN (SELECT id FROM filtered)
|
||||||
|
GROUP BY workspace_id
|
||||||
|
)
|
||||||
|
SELECT f.*,
|
||||||
|
COALESCE(ms.member_count, 0) AS "memberCount",
|
||||||
|
COALESCE(pp.public_page_count, 0) AS "publicPageCount",
|
||||||
|
COALESCE(ss.snapshot_count, 0) AS "snapshotCount",
|
||||||
|
COALESCE(ss.snapshot_size, 0) AS "snapshotSize",
|
||||||
|
COALESCE(bs.blob_count, 0) AS "blobCount",
|
||||||
|
COALESCE(bs.blob_size, 0) AS "blobSize"
|
||||||
|
FROM filtered f
|
||||||
|
LEFT JOIN snapshot_stats ss ON ss.workspace_id = f.id
|
||||||
|
LEFT JOIN blob_stats bs ON bs.workspace_id = f.id
|
||||||
|
LEFT JOIN member_stats ms ON ms.workspace_id = f.id
|
||||||
|
LEFT JOIN public_pages pp ON pp.workspace_id = f.id
|
||||||
|
ORDER BY ${Prisma.raw(this.buildAdminOrder(options.order))}
|
||||||
LIMIT ${options.first}
|
LIMIT ${options.first}
|
||||||
OFFSET ${options.skip}
|
OFFSET ${options.skip}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const total = rows.at(0)?.total ? Number(rows[0].total) : 0;
|
|
||||||
|
|
||||||
const mapped = rows.map(row => ({
|
const mapped = rows.map(row => ({
|
||||||
id: row.id,
|
id: row.id,
|
||||||
public: row.public,
|
public: row.public,
|
||||||
@@ -313,6 +411,53 @@ export class WorkspaceModel extends BaseModel {
|
|||||||
return { rows: mapped, total };
|
return { rows: mapped, total };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async adminCountWorkspaces(options: {
|
||||||
|
keyword?: string | null;
|
||||||
|
features?: WorkspaceFeatureName[] | null;
|
||||||
|
}) {
|
||||||
|
const keyword = options.keyword?.trim();
|
||||||
|
const features = options.features ?? [];
|
||||||
|
|
||||||
|
const [row] = await this.db.$queryRaw<{ total: bigint | number }[]>`
|
||||||
|
WITH feature_set AS (
|
||||||
|
SELECT workspace_id, array_agg(DISTINCT name) FILTER (WHERE activated) AS features
|
||||||
|
FROM workspace_features
|
||||||
|
GROUP BY workspace_id
|
||||||
|
),
|
||||||
|
owner AS (
|
||||||
|
SELECT wur.workspace_id,
|
||||||
|
u.id AS owner_id,
|
||||||
|
u.email AS owner_email
|
||||||
|
FROM workspace_user_permissions AS wur
|
||||||
|
JOIN users u ON wur.user_id = u.id
|
||||||
|
WHERE wur.type = ${WorkspaceRole.Owner}
|
||||||
|
AND wur.status = ${Prisma.sql`${WorkspaceMemberStatus.Accepted}::"WorkspaceMemberStatus"`}
|
||||||
|
)
|
||||||
|
SELECT COUNT(*) AS total
|
||||||
|
FROM workspaces w
|
||||||
|
LEFT JOIN feature_set fs ON fs.workspace_id = w.id
|
||||||
|
LEFT JOIN owner o ON o.workspace_id = w.id
|
||||||
|
WHERE ${
|
||||||
|
keyword
|
||||||
|
? Prisma.sql`
|
||||||
|
(
|
||||||
|
w.id ILIKE ${'%' + keyword + '%'}
|
||||||
|
OR o.owner_id ILIKE ${'%' + keyword + '%'}
|
||||||
|
OR o.owner_email ILIKE ${'%' + keyword + '%'}
|
||||||
|
)
|
||||||
|
`
|
||||||
|
: Prisma.sql`TRUE`
|
||||||
|
}
|
||||||
|
AND ${
|
||||||
|
features.length
|
||||||
|
? Prisma.sql`COALESCE(fs.features, ARRAY[]::text[]) @> ${features}`
|
||||||
|
: Prisma.sql`TRUE`
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return row?.total ? Number(row.total) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
private buildAdminOrder(
|
private buildAdminOrder(
|
||||||
order?: 'createdAt' | 'snapshotSize' | 'blobCount' | 'blobSize'
|
order?: 'createdAt' | 'snapshotSize' | 'blobCount' | 'blobSize'
|
||||||
) {
|
) {
|
||||||
|
|||||||
Reference in New Issue
Block a user