refactor: unify member query helpers in core

Extract duplicated member list/pagination logic from electron/main/worker/query/basic.ts
and packages/server/src/http/routes/web.ts into shared core functions.

- Add getMembersPaginated with search, sort, pagination to core
- Extend MemberWithAliases type with avatar field
- Extend getMembersWithAliases with avatar column detection
- Electron basic.ts: getMemberNameHistory, getMembers, getMembersPaginated delegate to core
- Server web.ts: /_web/members and /_web/members/paginated both delegate to core
- Retain Electron-only schema migrations and member DML operations
- Add 14 unit tests covering aliases/avatar presence, pagination, search, sort, system filter

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
diguaandCursor committed 2026-05-17 13:24:54 +08:00
1 parent f4d5010e77
commit 7c11aefacd
6 files changed
+435 -236

No files matched your search

+18 -170
View File
@@ -5,7 +5,7 @@
import Database from 'better-sqlite3'
import * as fs from 'fs'
import { openDatabase, closeDatabase, getDbPath, getCacheDir, openDatabaseAdapter, type TimeFilter } from '../core'
import { closeDatabase, getDbPath, getCacheDir, openDatabaseAdapter, type TimeFilter } from '../core'
import { getCache, CACHE_KEY_OVERVIEW, type OverviewCache } from '../../database/sessionCache'
import {
getAvailableYears as coreGetAvailableYears,
@@ -18,7 +18,11 @@ import {
getMessageTypeStats as coreGetMessageTypeStats,
getMessageLengthDistribution as coreGetMessageLengthDistribution,
getTimeRange as coreGetTimeRange,
getMemberNameHistory as coreGetMemberNameHistory,
getMembersWithAliases as coreGetMembersWithAliases,
getMembersPaginated as coreGetMembersPaginated,
} from '@openchatlab/core'
import type { MembersPaginationParams, MembersPaginatedResult, MemberWithAliases } from '@openchatlab/core'
// ==================== 基础查询(委托给 core) ====================
@@ -94,41 +98,16 @@ export function getTimeRange(sessionId: string): { start: number; end: number }
}
/**
* 获取成员的历史昵称记录
* 获取成员的历史昵称记录 (delegates to core)
*/
export function getMemberNameHistory(sessionId: string, memberId: number): any[] {
const db = openDatabase(sessionId)
const db = openDatabaseAdapter(sessionId)
if (!db) return []
const rows = db
.prepare(
`
SELECT name_type as nameType, name, start_ts as startTs, end_ts as endTs
FROM member_name_history
WHERE member_id = ?
ORDER BY start_ts DESC
`
)
.all(memberId) as Array<{ nameType: string; name: string; startTs: number; endTs: number | null }>
return rows
return coreGetMemberNameHistory(db, memberId)
}
// ==================== 成员管理 ====================
/**
* 成员信息(含统计数据)
*/
interface MemberWithStats {
id: number
platformId: string
accountName: string | null
groupNickname: string | null
aliases: string[]
messageCount: number
avatar: string | null
}
// 用于标记已检查过 aliases 字段的会话
const aliasesCheckedSessions = new Set<string>()
// 用于标记已检查过 avatar 字段的会话
@@ -208,165 +187,34 @@ export function ensureAvatarColumn(sessionId: string): void {
/**
* 获取所有成员列表(含消息数、别名和头像)
* Delegates to core getMembersWithAliases after ensuring schema columns exist.
*/
export function getMembers(sessionId: string): MemberWithStats[] {
// 先确保数据库有 aliases 和 avatar 字段(兼容旧数据库)
export function getMembers(sessionId: string): MemberWithAliases[] {
ensureAliasesColumn(sessionId)
ensureAvatarColumn(sessionId)
const db = openDatabase(sessionId)
const db = openDatabaseAdapter(sessionId)
if (!db) return []
const rows = db
.prepare(
`
SELECT
m.id,
m.platform_id as platformId,
m.account_name as accountName,
m.group_nickname as groupNickname,
m.aliases,
m.avatar,
COUNT(msg.id) as messageCount
FROM member m
LEFT JOIN message msg ON m.id = msg.sender_id
WHERE COALESCE(m.group_nickname, m.account_name, m.platform_id) != '系统消息'
GROUP BY m.id
ORDER BY messageCount DESC
`
)
.all() as Array<{
id: number
platformId: string
accountName: string | null
groupNickname: string | null
aliases: string | null
avatar: string | null
messageCount: number
}>
return rows.map((row) => ({
id: row.id,
platformId: row.platformId,
accountName: row.accountName,
groupNickname: row.groupNickname,
aliases: row.aliases ? JSON.parse(row.aliases) : [],
messageCount: row.messageCount,
avatar: row.avatar,
}))
return coreGetMembersWithAliases(db)
}
/**
* 分页参数类型
*/
export interface MembersPaginationParams {
page: number
pageSize: number
search?: string
sortOrder?: 'asc' | 'desc'
}
/**
* 分页结果类型
*/
export interface MembersPaginatedResult {
members: MemberWithStats[]
total: number
page: number
pageSize: number
totalPages: number
}
export type { MembersPaginationParams, MembersPaginatedResult }
/**
* 获取成员列表(分页版本,支持搜索和排序)
* Delegates to core getMembersPaginated after ensuring schema columns exist.
*/
export function getMembersPaginated(sessionId: string, params: MembersPaginationParams): MembersPaginatedResult {
const { page = 1, pageSize = 20, search = '', sortOrder = 'desc' } = params
// 先确保数据库有 aliases 和 avatar 字段(兼容旧数据库)
ensureAliasesColumn(sessionId)
ensureAvatarColumn(sessionId)
const db = openDatabase(sessionId)
const db = openDatabaseAdapter(sessionId)
if (!db) {
const page = params.page ?? 1
const pageSize = params.pageSize ?? 20
return { members: [], total: 0, page, pageSize, totalPages: 0 }
}
// 构建搜索条件
const searchCondition = search
? `AND (
m.group_nickname LIKE '%' || @search || '%' COLLATE NOCASE
OR m.account_name LIKE '%' || @search || '%' COLLATE NOCASE
OR m.platform_id LIKE '%' || @search || '%' COLLATE NOCASE
OR m.aliases LIKE '%' || @search || '%' COLLATE NOCASE
)`
: ''
// 排序方向
const orderDirection = sortOrder === 'asc' ? 'ASC' : 'DESC'
// 计算总数
const countResult = db
.prepare(
`
SELECT COUNT(*) as total FROM (
SELECT m.id
FROM member m
LEFT JOIN message msg ON m.id = msg.sender_id
WHERE COALESCE(m.group_nickname, m.account_name, m.platform_id) != '系统消息'
${searchCondition}
GROUP BY m.id
)
`
)
.get({ search }) as { total: number }
const total = countResult?.total || 0
const totalPages = Math.ceil(total / pageSize)
const offset = (page - 1) * pageSize
// 查询分页数据
const rows = db
.prepare(
`
SELECT
m.id,
m.platform_id as platformId,
m.account_name as accountName,
m.group_nickname as groupNickname,
m.aliases,
m.avatar,
COUNT(msg.id) as messageCount
FROM member m
LEFT JOIN message msg ON m.id = msg.sender_id
WHERE COALESCE(m.group_nickname, m.account_name, m.platform_id) != '系统消息'
${searchCondition}
GROUP BY m.id
ORDER BY messageCount ${orderDirection}
LIMIT @pageSize OFFSET @offset
`
)
.all({ search, pageSize, offset }) as Array<{
id: number
platformId: string
accountName: string | null
groupNickname: string | null
aliases: string | null
avatar: string | null
messageCount: number
}>
const members = rows.map((row) => ({
id: row.id,
platformId: row.platformId,
accountName: row.accountName,
groupNickname: row.groupNickname,
aliases: row.aliases ? JSON.parse(row.aliases) : [],
messageCount: row.messageCount,
avatar: row.avatar,
}))
return { members, total, page, pageSize, totalPages }
return coreGetMembersPaginated(db, params)
}
/**
+3
View File
@@ -54,6 +54,7 @@ export {
getConversationBetween,
getMemberNameHistory,
getMembersWithAliases,
getMembersPaginated,
executeParameterizedSql,
getCatchphraseAnalysis,
getMentionAnalysis,
@@ -107,6 +108,8 @@ export type {
ConversationData,
MemberNameHistoryEntry,
MemberWithAliases,
MembersPaginationParams,
MembersPaginatedResult,
CatchphraseAnalysis,
MemberCatchphrase,
CatchphraseItem,
@@ -0,0 +1,290 @@
/**
* Tests for member query functions: getMembersWithAliases, getMembersPaginated.
*
* Covers:
* - aliases/avatar column existence vs. absence
* - pagination, search, asc/desc sort
* - system message member filtering
*
* Run: npx tsx --test packages/core/src/query/__tests__/member-queries.test.ts
*/
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import { getMembersWithAliases, getMembersPaginated } from '../message-queries'
import type { DatabaseAdapter, PreparedStatement } from '../../interfaces'
// ==================== Mock helpers ====================
interface MockMember {
id: number
platformId: string
accountName: string | null
groupNickname: string | null
aliases: string | null
avatar: string | null
messageCount: number
}
const SAMPLE_MEMBERS: MockMember[] = [
{
id: 1,
platformId: 'u1',
accountName: 'Alice',
groupNickname: 'A',
aliases: '["小A"]',
avatar: 'data:img1',
messageCount: 100,
},
{ id: 2, platformId: 'u2', accountName: 'Bob', groupNickname: null, aliases: '[]', avatar: null, messageCount: 50 },
{
id: 3,
platformId: 'u3',
accountName: 'Carol',
groupNickname: 'C',
aliases: null,
avatar: 'data:img3',
messageCount: 30,
},
{
id: 4,
platformId: 'sys',
accountName: '系统消息',
groupNickname: '系统消息',
aliases: null,
avatar: null,
messageCount: 999,
},
]
function createMockDb(opts: { hasAliases?: boolean; hasAvatar?: boolean } = {}): DatabaseAdapter {
const { hasAliases = true, hasAvatar = true } = opts
const nonSystemMembers = SAMPLE_MEMBERS.filter((m) => {
const displayName = m.groupNickname || m.accountName || m.platformId
return displayName !== '系统消息'
})
return {
prepare(sql: string): PreparedStatement {
return {
get(...params: unknown[]) {
if (sql.includes('PRAGMA table_info')) {
return undefined
}
if (sql.includes('COUNT(*)')) {
const searchParam = params.find((p) => typeof p === 'string' && (p as string).includes('%'))
let filtered = nonSystemMembers
if (searchParam) {
const term = (searchParam as string).replace(/%/g, '').toLowerCase()
filtered = filtered.filter(
(m) =>
m.accountName?.toLowerCase().includes(term) ||
m.groupNickname?.toLowerCase().includes(term) ||
m.platformId.toLowerCase().includes(term) ||
(hasAliases && m.aliases?.toLowerCase().includes(term))
)
}
return { total: filtered.length }
}
return undefined
},
all(...params: unknown[]) {
if (sql.includes('PRAGMA table_info')) {
const cols = [{ name: 'id' }, { name: 'platform_id' }, { name: 'account_name' }, { name: 'group_nickname' }]
if (sql.includes('member')) {
if (hasAliases) cols.push({ name: 'aliases' })
if (hasAvatar) cols.push({ name: 'avatar' })
}
return cols
}
let filtered = nonSystemMembers
const searchParam = params.find((p) => typeof p === 'string' && (p as string).includes('%'))
if (searchParam) {
const term = (searchParam as string).replace(/%/g, '').toLowerCase()
filtered = filtered.filter(
(m) =>
m.accountName?.toLowerCase().includes(term) ||
m.groupNickname?.toLowerCase().includes(term) ||
m.platformId.toLowerCase().includes(term) ||
(hasAliases && m.aliases?.toLowerCase().includes(term))
)
}
if (sql.includes('ORDER BY messageCount ASC')) {
filtered = [...filtered].sort((a, b) => a.messageCount - b.messageCount)
} else {
filtered = [...filtered].sort((a, b) => b.messageCount - a.messageCount)
}
const limitIdx = params.findIndex((_, i) => {
const remaining = params.slice(i)
return remaining.length >= 2 && typeof remaining[0] === 'number' && typeof remaining[1] === 'number'
})
if (limitIdx >= 0) {
const limit = params[limitIdx] as number
const offset = params[limitIdx + 1] as number
filtered = filtered.slice(offset, offset + limit)
}
return filtered.map((m) => ({
id: m.id,
platformId: m.platformId,
accountName: m.accountName,
groupNickname: m.groupNickname,
aliases: hasAliases ? m.aliases : null,
avatar: hasAvatar ? m.avatar : null,
messageCount: m.messageCount,
}))
},
run() {
return { changes: 0, lastInsertRowid: 0 }
},
}
},
// eslint-disable-next-line @typescript-eslint/no-empty-function
exec() {},
transaction<T>(fn: () => T) {
return fn()
},
pragma() {
return undefined
},
// eslint-disable-next-line @typescript-eslint/no-empty-function
close() {},
}
}
// ==================== getMembersWithAliases ====================
describe('getMembersWithAliases', () => {
it('returns members with parsed aliases and avatar', () => {
const db = createMockDb({ hasAliases: true, hasAvatar: true })
const result = getMembersWithAliases(db)
assert.equal(result.length, 3, 'should exclude system message member')
assert.deepEqual(result[0].aliases, ['小A'])
assert.equal(result[0].avatar, 'data:img1')
assert.equal(result[0].accountName, 'Alice')
assert.equal(result[0].messageCount, 100)
})
it('returns empty aliases and null avatar when columns do not exist', () => {
const db = createMockDb({ hasAliases: false, hasAvatar: false })
const result = getMembersWithAliases(db)
assert.equal(result.length, 3)
for (const m of result) {
assert.deepEqual(m.aliases, [], 'aliases should default to empty array')
assert.equal(m.avatar, null, 'avatar should default to null')
}
})
it('excludes system message members', () => {
const db = createMockDb()
const result = getMembersWithAliases(db)
const hasSys = result.some((m) => m.accountName === '系统消息')
assert.equal(hasSys, false)
})
it('sorts by messageCount descending', () => {
const db = createMockDb()
const result = getMembersWithAliases(db)
for (let i = 1; i < result.length; i++) {
assert.ok(result[i - 1].messageCount >= result[i].messageCount)
}
})
})
// ==================== getMembersPaginated ====================
describe('getMembersPaginated', () => {
it('returns first page with correct pagination meta', () => {
const db = createMockDb()
const result = getMembersPaginated(db, { page: 1, pageSize: 2 })
assert.equal(result.page, 1)
assert.equal(result.pageSize, 2)
assert.equal(result.total, 3)
assert.equal(result.totalPages, 2)
assert.equal(result.members.length, 2)
})
it('returns second page', () => {
const db = createMockDb()
const result = getMembersPaginated(db, { page: 2, pageSize: 2 })
assert.equal(result.page, 2)
assert.equal(result.members.length, 1)
})
it('filters by search term', () => {
const db = createMockDb()
const result = getMembersPaginated(db, { search: 'alice' })
assert.equal(result.total, 1)
assert.equal(result.members[0].accountName, 'Alice')
})
it('sorts ascending', () => {
const db = createMockDb()
const result = getMembersPaginated(db, { sortOrder: 'asc' })
for (let i = 1; i < result.members.length; i++) {
assert.ok(result.members[i - 1].messageCount <= result.members[i].messageCount)
}
})
it('sorts descending by default', () => {
const db = createMockDb()
const result = getMembersPaginated(db, {})
for (let i = 1; i < result.members.length; i++) {
assert.ok(result.members[i - 1].messageCount >= result.members[i].messageCount)
}
})
it('works when aliases/avatar columns are missing', () => {
const db = createMockDb({ hasAliases: false, hasAvatar: false })
const result = getMembersPaginated(db, {})
assert.equal(result.total, 3)
for (const m of result.members) {
assert.deepEqual(m.aliases, [])
assert.equal(m.avatar, null)
}
})
it('uses default page and pageSize when omitted', () => {
const db = createMockDb()
const result = getMembersPaginated(db, {})
assert.equal(result.page, 1)
assert.equal(result.pageSize, 20)
})
it('clamps page to minimum 1', () => {
const db = createMockDb()
const result = getMembersPaginated(db, { page: -5 })
assert.equal(result.page, 1)
})
it('clamps pageSize to range [1, 100]', () => {
const db = createMockDb()
const r1 = getMembersPaginated(db, { pageSize: 0 })
assert.equal(r1.pageSize, 1)
const r2 = getMembersPaginated(db, { pageSize: 999 })
assert.equal(r2.pageSize, 100)
})
it('excludes system message members', () => {
const db = createMockDb()
const result = getMembersPaginated(db, { pageSize: 100 })
const hasSys = result.members.some((m) => m.accountName === '系统消息')
assert.equal(hasSys, false)
})
})
+3
View File
@@ -60,6 +60,7 @@ export {
getConversationBetween,
getMemberNameHistory,
getMembersWithAliases,
getMembersPaginated,
executeParameterizedSql,
} from './message-queries'
export type {
@@ -72,6 +73,8 @@ export type {
ConversationData,
MemberNameHistoryEntry,
MemberWithAliases,
MembersPaginationParams,
MembersPaginatedResult,
} from './message-queries'
// Shared full-message SQL, types, and mapper
+107 -4
View File
@@ -351,9 +351,25 @@ export interface MemberWithAliases {
accountName: string | null
groupNickname: string | null
aliases: string[]
avatar: string | null
messageCount: number
}
export interface MembersPaginationParams {
page?: number
pageSize?: number
search?: string
sortOrder?: 'asc' | 'desc'
}
export interface MembersPaginatedResult {
members: MemberWithAliases[]
total: number
page: number
pageSize: number
totalPages: number
}
/**
* Batch-load messages by IDs with full sender info (avatar, aliases, reply, etc.)
*/
@@ -533,18 +549,20 @@ export function getMemberNameHistory(db: DatabaseAdapter, memberId: number): Mem
}
/**
* Get member list with aliases and detailed info
* Get member list with aliases, avatar and detailed info
*/
export function getMembersWithAliases(db: DatabaseAdapter): MemberWithAliases[] {
const aliasesAvailable = hasColumn(db, 'member', 'aliases')
const avatarAvailable = hasColumn(db, 'member', 'avatar')
const aliasesSelect = aliasesAvailable ? 'm.aliases' : 'NULL as aliases'
const avatarSelect = avatarAvailable ? 'm.avatar' : 'NULL as avatar'
const rows = db
.prepare(
`SELECT
m.id, m.platform_id as platformId,
m.account_name as accountName, m.group_nickname as groupNickname,
${aliasesSelect},
${aliasesSelect}, ${avatarSelect},
COUNT(msg.id) as messageCount
FROM member m
LEFT JOIN message msg ON m.id = msg.sender_id
@@ -557,17 +575,102 @@ export function getMembersWithAliases(db: DatabaseAdapter): MemberWithAliases[]
accountName: string | null
groupNickname: string | null
aliases: string | null
avatar: string | null
messageCount: number
}>
return rows.map((row) => ({
return rows.map(mapMemberRow)
}
/**
* Paginated member list with search and sort.
*/
export function getMembersPaginated(db: DatabaseAdapter, params: MembersPaginationParams): MembersPaginatedResult {
const page = Math.max(1, params.page ?? 1)
const pageSize = Math.min(100, Math.max(1, params.pageSize ?? 20))
const search = params.search?.trim() || ''
const sortDirection = params.sortOrder === 'asc' ? 'ASC' : 'DESC'
const aliasesAvailable = hasColumn(db, 'member', 'aliases')
const avatarAvailable = hasColumn(db, 'member', 'avatar')
const aliasesSelect = aliasesAvailable ? 'm.aliases' : 'NULL as aliases'
const avatarSelect = avatarAvailable ? 'm.avatar' : 'NULL as avatar'
let searchClause = ''
const searchParams: unknown[] = []
if (search) {
const clauses = ['m.account_name LIKE ?', 'm.group_nickname LIKE ?', 'm.platform_id LIKE ?']
const like = `%${search}%`
searchParams.push(like, like, like)
if (aliasesAvailable) {
clauses.push('m.aliases LIKE ?')
searchParams.push(like)
}
searchClause = `AND (${clauses.join(' OR ')})`
}
const systemFilter = "COALESCE(m.group_nickname, m.account_name, m.platform_id) != '系统消息'"
const countRow = db
.prepare(
`SELECT COUNT(*) as total FROM (
SELECT m.id FROM member m
LEFT JOIN message msg ON m.id = msg.sender_id
WHERE ${systemFilter} ${searchClause}
GROUP BY m.id
)`
)
.get(...searchParams) as { total: number } | undefined
const total = countRow?.total ?? 0
const totalPages = Math.ceil(total / pageSize)
const offset = (page - 1) * pageSize
const rows = db
.prepare(
`SELECT
m.id, m.platform_id as platformId,
m.account_name as accountName, m.group_nickname as groupNickname,
${aliasesSelect}, ${avatarSelect},
COUNT(msg.id) as messageCount
FROM member m
LEFT JOIN message msg ON m.id = msg.sender_id
WHERE ${systemFilter} ${searchClause}
GROUP BY m.id
ORDER BY messageCount ${sortDirection}
LIMIT ? OFFSET ?`
)
.all(...searchParams, pageSize, offset) as Array<{
id: number
platformId: string
accountName: string | null
groupNickname: string | null
aliases: string | null
avatar: string | null
messageCount: number
}>
return { members: rows.map(mapMemberRow), total, page, pageSize, totalPages }
}
function mapMemberRow(row: {
id: number
platformId: string
accountName: string | null
groupNickname: string | null
aliases: string | null
avatar: string | null
messageCount: number
}): MemberWithAliases {
return {
id: row.id,
platformId: row.platformId,
accountName: row.accountName,
groupNickname: row.groupNickname,
aliases: row.aliases ? JSON.parse(row.aliases) : [],
avatar: row.avatar ?? null,
messageCount: row.messageCount,
}))
}
}
/**
+14 -62
View File
@@ -13,6 +13,8 @@ import type { DatabaseManager } from '@openchatlab/node-runtime'
import type { TimeFilter } from '@openchatlab/shared-types'
import {
getSessionInfo,
getMembersWithAliases,
getMembersPaginated,
getTimeRange,
getAvailableYears,
getMemberActivity,
@@ -201,23 +203,7 @@ export function registerWebRoutes(server: FastifyInstance, dbManager: DatabaseMa
server.get<{ Params: { id: string } }>('/_web/sessions/:id/members', async (request) => {
const db = ensureDb(dbManager, request.params.id)
const rows = db
.prepare(
`SELECT
m.id, m.platform_id as platformId,
m.account_name as accountName,
m.group_nickname as groupNickname,
m.aliases, m.avatar,
(SELECT COUNT(*) FROM message WHERE sender_id = m.id) as messageCount
FROM member m
WHERE COALESCE(m.account_name, '') != '系统消息'
ORDER BY messageCount DESC`
)
.all() as any[]
return rows.map((r: any) => ({
...r,
aliases: r.aliases ? (typeof r.aliases === 'string' ? JSON.parse(r.aliases) : r.aliases) : [],
}))
return getMembersWithAliases(db)
})
server.get<{
@@ -225,52 +211,18 @@ export function registerWebRoutes(server: FastifyInstance, dbManager: DatabaseMa
Querystring: { page?: string; pageSize?: string; search?: string; sortOrder?: string }
}>('/_web/sessions/:id/members/paginated', async (request) => {
const db = ensureDb(dbManager, request.params.id)
const page = Math.max(1, parseInt(request.query.page || '1', 10))
const pageSize = Math.min(100, Math.max(1, parseInt(request.query.pageSize || '20', 10)))
const search = request.query.search?.trim() || ''
const sortOrder = request.query.sortOrder === 'asc' ? 'ASC' : 'DESC'
let whereClause = ''
const params: unknown[] = []
if (search) {
whereClause = `WHERE (m.account_name LIKE ? OR m.group_nickname LIKE ? OR m.platform_id LIKE ? OR m.aliases LIKE ?)`
const searchParam = `%${search}%`
params.push(searchParam, searchParam, searchParam, searchParam)
}
const countRow = db.prepare(`SELECT COUNT(*) as total FROM member m ${whereClause}`).get(...params) as {
total: number
}
const total = countRow.total
const totalPages = Math.ceil(total / pageSize)
const offset = (page - 1) * pageSize
const rows = db
.prepare(
`SELECT
m.id, m.platform_id as platformId,
m.account_name as accountName,
m.group_nickname as groupNickname,
m.aliases, m.avatar,
COUNT(msg.id) as messageCount
FROM member m
LEFT JOIN message msg ON m.id = msg.sender_id
${whereClause}
GROUP BY m.id
ORDER BY messageCount ${sortOrder}
LIMIT ? OFFSET ?`
)
.all(...params, pageSize, offset) as any[]
const result = getMembersPaginated(db, {
page: parseInt(request.query.page || '1', 10),
pageSize: parseInt(request.query.pageSize || '20', 10),
search: request.query.search,
sortOrder: request.query.sortOrder === 'asc' ? 'asc' : 'desc',
})
return {
items: rows.map((r: any) => ({
...r,
aliases: r.aliases ? (typeof r.aliases === 'string' ? JSON.parse(r.aliases) : r.aliases) : [],
})),
total,
page,
pageSize,
totalPages,
items: result.members,
total: result.total,
page: result.page,
pageSize: result.pageSize,
totalPages: result.totalPages,
}
})