@meith/db 0.28.1 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -414,6 +414,13 @@
|
|
|
414
414
|
"when": 1787771682008,
|
|
415
415
|
"tag": "0058_mass_mail_opt_in",
|
|
416
416
|
"breakpoints": true
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
"idx": 59,
|
|
420
|
+
"version": "7",
|
|
421
|
+
"when": 1788876000000,
|
|
422
|
+
"tag": "0059_member_directory_navigation",
|
|
423
|
+
"breakpoints": true
|
|
417
424
|
}
|
|
418
425
|
]
|
|
419
426
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meith/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -22,35 +22,35 @@
|
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"drizzle-orm": "^0.45.2",
|
|
24
24
|
"postgres": "^3.4.7",
|
|
25
|
-
"@meith/accounts": "0.
|
|
26
|
-
"@meith/admin": "0.
|
|
27
|
-
"@meith/antispam": "0.
|
|
28
|
-
"@meith/api": "0.
|
|
29
|
-
"@meith/attachments": "0.
|
|
30
|
-
"@meith/authorization": "0.
|
|
31
|
-
"@meith/avatars": "0.
|
|
32
|
-
"@meith/core": "0.
|
|
33
|
-
"@meith/drafts": "0.
|
|
34
|
-
"@meith/events": "0.
|
|
35
|
-
"@meith/
|
|
36
|
-
"@meith/
|
|
37
|
-
"@meith/i18n": "0.
|
|
38
|
-
"@meith/markdown": "0.
|
|
39
|
-
"@meith/marketplace": "0.
|
|
40
|
-
"@meith/messages": "0.
|
|
41
|
-
"@meith/moderation": "0.
|
|
42
|
-
"@meith/notifications": "0.
|
|
43
|
-
"@meith/plugin-kit": "0.
|
|
44
|
-
"@meith/polls": "0.
|
|
45
|
-
"@meith/profile-fields": "0.
|
|
46
|
-
"@meith/relations": "0.
|
|
47
|
-
"@meith/
|
|
48
|
-
"@meith/
|
|
49
|
-
"@meith/
|
|
50
|
-
"@meith/
|
|
51
|
-
"@meith/
|
|
52
|
-
"@meith/
|
|
53
|
-
"@meith/
|
|
25
|
+
"@meith/accounts": "0.29.0",
|
|
26
|
+
"@meith/admin": "0.29.0",
|
|
27
|
+
"@meith/antispam": "0.29.0",
|
|
28
|
+
"@meith/api": "0.29.0",
|
|
29
|
+
"@meith/attachments": "0.29.0",
|
|
30
|
+
"@meith/authorization": "0.29.0",
|
|
31
|
+
"@meith/avatars": "0.29.0",
|
|
32
|
+
"@meith/core": "0.29.0",
|
|
33
|
+
"@meith/drafts": "0.29.0",
|
|
34
|
+
"@meith/events": "0.29.0",
|
|
35
|
+
"@meith/forums": "0.29.0",
|
|
36
|
+
"@meith/groups": "0.29.0",
|
|
37
|
+
"@meith/i18n": "0.29.0",
|
|
38
|
+
"@meith/markdown": "0.29.0",
|
|
39
|
+
"@meith/marketplace": "0.29.0",
|
|
40
|
+
"@meith/messages": "0.29.0",
|
|
41
|
+
"@meith/moderation": "0.29.0",
|
|
42
|
+
"@meith/notifications": "0.29.0",
|
|
43
|
+
"@meith/plugin-kit": "0.29.0",
|
|
44
|
+
"@meith/polls": "0.29.0",
|
|
45
|
+
"@meith/profile-fields": "0.29.0",
|
|
46
|
+
"@meith/relations": "0.29.0",
|
|
47
|
+
"@meith/search": "0.29.0",
|
|
48
|
+
"@meith/settings": "0.29.0",
|
|
49
|
+
"@meith/signatures": "0.29.0",
|
|
50
|
+
"@meith/subscriptions": "0.29.0",
|
|
51
|
+
"@meith/tasks": "0.29.0",
|
|
52
|
+
"@meith/threads": "0.29.0",
|
|
53
|
+
"@meith/reputation": "0.29.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"drizzle-kit": "^0.31.6",
|
|
@@ -15,6 +15,18 @@ export interface GroupIdentity {
|
|
|
15
15
|
|
|
16
16
|
export interface MemberStanding extends GroupIdentity {
|
|
17
17
|
readonly reputation: number
|
|
18
|
+
readonly groups: readonly GroupIdentity[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function identityOf(row: Record<string, unknown>): GroupIdentity {
|
|
22
|
+
return {
|
|
23
|
+
groupId: Number(row.group_id),
|
|
24
|
+
title: String(row.title),
|
|
25
|
+
nameColorLight: row.name_color_light === null ? null : String(row.name_color_light),
|
|
26
|
+
nameColorDark: row.name_color_dark === null ? null : String(row.name_color_dark),
|
|
27
|
+
badgeImageLight: row.badge_image_light === null ? null : String(row.badge_image_light),
|
|
28
|
+
badgeImageDark: row.badge_image_dark === null ? null : String(row.badge_image_dark),
|
|
29
|
+
}
|
|
18
30
|
}
|
|
19
31
|
|
|
20
32
|
export class PostgresGroupIdentityRepository {
|
|
@@ -32,33 +44,47 @@ export class PostgresGroupIdentityRepository {
|
|
|
32
44
|
g.name_color_light,
|
|
33
45
|
g.name_color_dark,
|
|
34
46
|
g.badge_image_light,
|
|
35
|
-
g.badge_image_dark
|
|
47
|
+
g.badge_image_dark,
|
|
48
|
+
(g.id = ${displayGroupIdSql('u', 'p')}) as is_display
|
|
36
49
|
from users u
|
|
37
50
|
join usergroups p
|
|
38
51
|
on p.id = u.primary_group_id
|
|
39
52
|
join usergroups g
|
|
40
53
|
on g.id = ${displayGroupIdSql('u', 'p')}
|
|
54
|
+
or g.id = u.primary_group_id
|
|
55
|
+
or g.id in (
|
|
56
|
+
select m.group_id
|
|
57
|
+
from user_group_memberships m
|
|
58
|
+
where m.user_id = u.id
|
|
59
|
+
and (m.expires_at is null or m.expires_at > now())
|
|
60
|
+
)
|
|
41
61
|
where u.id in ${sql`(${sql.join(
|
|
42
62
|
userIds.map((id) => sql`${id}`),
|
|
43
63
|
sql`, `,
|
|
44
64
|
)})`}
|
|
65
|
+
order by u.id, is_display desc, g.display_order, g.title, g.id
|
|
45
66
|
`),
|
|
46
67
|
) as Array<Record<string, unknown>>
|
|
47
68
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
)
|
|
69
|
+
const byUser = new Map<number, { reputation: number; groups: GroupIdentity[] }>()
|
|
70
|
+
|
|
71
|
+
for (const row of rows) {
|
|
72
|
+
const userId = Number(row.user_id)
|
|
73
|
+
let entry = byUser.get(userId)
|
|
74
|
+
if (entry === undefined) {
|
|
75
|
+
entry = { reputation: Number(row.reputation ?? 0), groups: [] }
|
|
76
|
+
byUser.set(userId, entry)
|
|
77
|
+
}
|
|
78
|
+
entry.groups.push(identityOf(row))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const standings = new Map<number, MemberStanding>()
|
|
82
|
+
for (const [userId, entry] of byUser) {
|
|
83
|
+
const display = entry.groups[0]
|
|
84
|
+
if (display === undefined) continue
|
|
85
|
+
standings.set(userId, { ...display, reputation: entry.reputation, groups: entry.groups })
|
|
86
|
+
}
|
|
87
|
+
return standings
|
|
62
88
|
}
|
|
63
89
|
|
|
64
90
|
async styled(): Promise<readonly GroupIdentity[]> {
|
|
@@ -72,13 +98,6 @@ export class PostgresGroupIdentityRepository {
|
|
|
72
98
|
`),
|
|
73
99
|
) as Array<Record<string, unknown>>
|
|
74
100
|
|
|
75
|
-
return rows.map(
|
|
76
|
-
groupId: Number(row.group_id),
|
|
77
|
-
title: String(row.title),
|
|
78
|
-
nameColorLight: row.name_color_light === null ? null : String(row.name_color_light),
|
|
79
|
-
nameColorDark: row.name_color_dark === null ? null : String(row.name_color_dark),
|
|
80
|
-
badgeImageLight: row.badge_image_light === null ? null : String(row.badge_image_light),
|
|
81
|
-
badgeImageDark: row.badge_image_dark === null ? null : String(row.badge_image_dark),
|
|
82
|
-
}))
|
|
101
|
+
return rows.map(identityOf)
|
|
83
102
|
}
|
|
84
103
|
}
|
package/src/index.ts
CHANGED
|
@@ -157,6 +157,13 @@ export {
|
|
|
157
157
|
} from './latest-repo'
|
|
158
158
|
export { PostgresMaintenanceRepository } from './maintenance-repo'
|
|
159
159
|
export { PostgresMarketplaceCacheRepository } from './marketplace-repo'
|
|
160
|
+
export {
|
|
161
|
+
type MemberDirectoryPage,
|
|
162
|
+
type MemberDirectoryRow,
|
|
163
|
+
type MemberDirectorySort,
|
|
164
|
+
PostgresMemberDirectoryRepository,
|
|
165
|
+
type StaffGroupRow,
|
|
166
|
+
} from './member-directory-repo'
|
|
160
167
|
export { PostgresMemberProfileRepository } from './member-profile-repo'
|
|
161
168
|
export { PostgresMemberSettingsRepository } from './member-settings-repo'
|
|
162
169
|
export { PostgresMessageRepository } from './message-repo'
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { sql } from 'drizzle-orm'
|
|
2
|
+
|
|
3
|
+
import type { Database } from './client'
|
|
4
|
+
import { resultRows } from './result-rows'
|
|
5
|
+
|
|
6
|
+
export type MemberDirectorySort = 'name' | 'posts' | 'joined'
|
|
7
|
+
|
|
8
|
+
export interface MemberDirectoryRow {
|
|
9
|
+
readonly id: number
|
|
10
|
+
readonly username: string
|
|
11
|
+
readonly postCount: number
|
|
12
|
+
readonly createdAt: Date
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface MemberDirectoryPage {
|
|
16
|
+
readonly rows: readonly MemberDirectoryRow[]
|
|
17
|
+
readonly total: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface StaffGroupRow {
|
|
21
|
+
readonly groupId: number
|
|
22
|
+
readonly title: string
|
|
23
|
+
readonly description: string | null
|
|
24
|
+
readonly members: readonly { readonly id: number; readonly username: string }[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const SORT_SQL: Readonly<Record<MemberDirectorySort, ReturnType<typeof sql.raw>>> = {
|
|
28
|
+
name: sql.raw('u.username_lower asc, u.id asc'),
|
|
29
|
+
posts: sql.raw('u.post_count desc, u.username_lower asc, u.id asc'),
|
|
30
|
+
joined: sql.raw('u.created_at desc, u.id desc'),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class PostgresMemberDirectoryRepository {
|
|
34
|
+
constructor(private readonly db: Database) {}
|
|
35
|
+
|
|
36
|
+
async page(input: {
|
|
37
|
+
readonly offset: number
|
|
38
|
+
readonly limit: number
|
|
39
|
+
readonly sort: MemberDirectorySort
|
|
40
|
+
readonly nameContains?: string
|
|
41
|
+
}): Promise<MemberDirectoryPage> {
|
|
42
|
+
const name = input.nameContains?.trim().toLowerCase() ?? ''
|
|
43
|
+
const nameFilter =
|
|
44
|
+
name === ''
|
|
45
|
+
? sql``
|
|
46
|
+
: sql` and u.username_lower like ${`%${name.replaceAll('\\', '\\\\').replaceAll('%', '\\%').replaceAll('_', '\\_')}%`}`
|
|
47
|
+
|
|
48
|
+
const counted = resultRows(
|
|
49
|
+
await this.db.execute(sql`
|
|
50
|
+
select count(*) as total
|
|
51
|
+
from users u
|
|
52
|
+
where u.state <> 'deleted'${nameFilter}
|
|
53
|
+
`),
|
|
54
|
+
) as Array<Record<string, unknown>>
|
|
55
|
+
|
|
56
|
+
const rows = resultRows(
|
|
57
|
+
await this.db.execute(sql`
|
|
58
|
+
select u.id, u.username, u.post_count, u.created_at
|
|
59
|
+
from users u
|
|
60
|
+
where u.state <> 'deleted'${nameFilter}
|
|
61
|
+
order by ${SORT_SQL[input.sort]}
|
|
62
|
+
limit ${input.limit} offset ${input.offset}
|
|
63
|
+
`),
|
|
64
|
+
) as Array<Record<string, unknown>>
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
total: Number(counted[0]?.total ?? 0),
|
|
68
|
+
rows: rows.map((row) => ({
|
|
69
|
+
id: Number(row.id),
|
|
70
|
+
username: String(row.username),
|
|
71
|
+
postCount: Number(row.post_count ?? 0),
|
|
72
|
+
createdAt: new Date(row.created_at as string),
|
|
73
|
+
})),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async staff(): Promise<readonly StaffGroupRow[]> {
|
|
78
|
+
const rows = resultRows(
|
|
79
|
+
await this.db.execute(sql`
|
|
80
|
+
select g.id as group_id, g.title, g.description, u.id as user_id, u.username
|
|
81
|
+
from usergroups g
|
|
82
|
+
join users u
|
|
83
|
+
on (u.primary_group_id = g.id
|
|
84
|
+
or exists (
|
|
85
|
+
select 1
|
|
86
|
+
from user_group_memberships m
|
|
87
|
+
where m.user_id = u.id
|
|
88
|
+
and m.group_id = g.id
|
|
89
|
+
and (m.expires_at is null or m.expires_at > now())
|
|
90
|
+
))
|
|
91
|
+
and u.state <> 'deleted'
|
|
92
|
+
where g.is_staff_group = true
|
|
93
|
+
order by g.display_order, g.id, u.username_lower, u.id
|
|
94
|
+
`),
|
|
95
|
+
) as Array<Record<string, unknown>>
|
|
96
|
+
|
|
97
|
+
const groups = new Map<
|
|
98
|
+
number,
|
|
99
|
+
{ title: string; description: string | null; members: { id: number; username: string }[] }
|
|
100
|
+
>()
|
|
101
|
+
|
|
102
|
+
for (const row of rows) {
|
|
103
|
+
const groupId = Number(row.group_id)
|
|
104
|
+
let entry = groups.get(groupId)
|
|
105
|
+
if (entry === undefined) {
|
|
106
|
+
entry = {
|
|
107
|
+
title: String(row.title),
|
|
108
|
+
description: row.description === null ? null : String(row.description),
|
|
109
|
+
members: [],
|
|
110
|
+
}
|
|
111
|
+
groups.set(groupId, entry)
|
|
112
|
+
}
|
|
113
|
+
entry.members.push({ id: Number(row.user_id), username: String(row.username) })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return [...groups].map(([groupId, entry]) => ({ groupId, ...entry }))
|
|
117
|
+
}
|
|
118
|
+
}
|