Skip to content

Commit

Permalink
Improve admin invite code views (bluesky-social#764)
Browse files Browse the repository at this point in the history
allow filter on search & add tests
  • Loading branch information
dholms authored Apr 5, 2023
1 parent 83828c5 commit 5ae26d9
Show file tree
Hide file tree
Showing 7 changed files with 53 additions and 27 deletions.
6 changes: 1 addition & 5 deletions lexicons/com/atproto/admin/defs.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,7 @@
"relatedRecords": {"type": "array", "items": {"type": "unknown"}},
"indexedAt": {"type": "string", "format": "datetime"},
"moderation": {"type": "ref", "ref": "#moderation"},
"invitedBy": {"type": "ref", "ref": "com.atproto.server.defs#inviteCode"},
"invites": {
"type": "array",
"items": {"type": "ref", "ref": "com.atproto.server.defs#inviteCode"}
}
"invitedBy": {"type": "ref", "ref": "com.atproto.server.defs#inviteCode"}
}
},
"repoViewDetail": {
Expand Down
4 changes: 2 additions & 2 deletions packages/pds/src/api/com/atproto/admin/searchRepos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ export default function (server: Server, ctx: AppContext) {
handler: async ({ params }) => {
const { db, services } = ctx
const moderationService = services.moderation(db)
const { term = '', limit = 50, cursor } = params
const { term = '', limit = 50, cursor, invitedBy } = params

if (!term) {
const results = await services
.account(db)
.list({ limit, cursor, includeSoftDeleted: true })
.list({ limit, cursor, includeSoftDeleted: true, invitedBy })
const keyset = new ListKeyset(sql``, sql``)

return {
Expand Down
35 changes: 20 additions & 15 deletions packages/pds/src/services/account/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,11 +212,12 @@ export class AccountService {
limit: number
cursor?: string
includeSoftDeleted?: boolean
invitedBy?: string
}): Promise<(RepoRoot & DidHandle)[]> {
const { limit, cursor, includeSoftDeleted } = opts
const { limit, cursor, includeSoftDeleted, invitedBy } = opts
const { ref } = this.db.db.dynamic

const builder = this.db.db
let builder = this.db.db
.selectFrom('repo_root')
.innerJoin('did_handle', 'did_handle.did', 'repo_root.did')
.if(!includeSoftDeleted, (qb) =>
Expand All @@ -225,6 +226,17 @@ export class AccountService {
.selectAll('did_handle')
.selectAll('repo_root')

if (invitedBy) {
builder = builder
.innerJoin(
'invite_code_use as code_use',
'code_use.usedBy',
'did_handle.did',
)
.innerJoin('invite_code', 'invite_code.code', 'code_use.code')
.where('invite_code.forUser', '=', invitedBy)
}

const keyset = new ListKeyset(ref('indexedAt'), ref('handle'))

return await paginate(builder, {
Expand Down Expand Up @@ -304,11 +316,12 @@ export class AccountService {
}))
}

async getInviteCodesForAccounts(dids: string[]): Promise<InviteCodesByDid> {
async getInvitedByForAccounts(
dids: string[],
): Promise<Record<string, CodeDetail>> {
if (dids.length < 1) return {}
const codeDetailsRes = await this.selectInviteCodesQb()
.where('forAccount', 'in', dids)
.orWhere('code', 'in', (qb) =>
.where('code', 'in', (qb) =>
qb
.selectFrom('invite_code_use')
.where('usedBy', 'in', dids)
Expand All @@ -325,22 +338,14 @@ export class AccountService {
disabled: row.disabled === 1,
}))
return codeDetails.reduce((acc, cur) => {
acc[cur.forAccount] ??= { invitedBy: undefined, invites: [] }
acc[cur.forAccount].invites.push(cur)
for (const use of cur.uses) {
acc[use.usedBy] ??= { invitedBy: undefined, invites: [] }
acc[use.usedBy].invitedBy = cur
acc[use.usedBy] = cur
}
return acc
}, {} as InviteCodesByDid)
}, {} as Record<string, CodeDetail>)
}
}

type InviteCodesByDid = Record<
string,
{ invitedBy?: CodeDetail; invites: CodeDetail[] }
>

type CodeDetail = {
code: string
available: number
Expand Down
11 changes: 6 additions & 5 deletions packages/pds/src/services/moderation/views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class ModerationViews {
const results = Array.isArray(result) ? result : [result]
if (results.length === 0) return []

const [info, actionResults, inviteCodes] = await Promise.all([
const [info, actionResults, invitedBy] = await Promise.all([
await this.db.db
.selectFrom('did_handle')
.leftJoin('user_account', 'user_account.did', 'did_handle.did')
Expand Down Expand Up @@ -71,7 +71,7 @@ export class ModerationViews {
.execute(),
this.services
.account(this.db)
.getInviteCodesForAccounts(results.map((r) => r.did)),
.getInvitedByForAccounts(results.map((r) => r.did)),
])

const infoByDid = info.reduce(
Expand Down Expand Up @@ -101,8 +101,7 @@ export class ModerationViews {
? { id: action.id, action: action.action }
: undefined,
},
invitedBy: inviteCodes[r.did]?.invitedBy,
invites: inviteCodes[r.did]?.invites,
invitedBy: invitedBy[r.did],
}
})

Expand All @@ -111,7 +110,7 @@ export class ModerationViews {

async repoDetail(result: RepoResult): Promise<RepoViewDetail> {
const repo = await this.repo(result)
const [reportResults, actionResults] = await Promise.all([
const [reportResults, actionResults, inviteCodes] = await Promise.all([
this.db.db
.selectFrom('moderation_report')
.where('subjectType', '=', 'com.atproto.admin.defs#repoRef')
Expand All @@ -126,6 +125,7 @@ export class ModerationViews {
.orderBy('id', 'desc')
.selectAll()
.execute(),
this.services.account(this.db).getAccountInviteCodes(repo.did),
])
const [reports, actions] = await Promise.all([
this.report(reportResults),
Expand All @@ -138,6 +138,7 @@ export class ModerationViews {
reports,
actions,
},
invites: inviteCodes,
}
}

Expand Down
1 change: 1 addition & 0 deletions packages/pds/src/services/util/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const getUserSearchQueryPg = (
limit: number
cursor?: string
includeSoftDeleted?: boolean
invitedBy?: string
},
) => {
const { ref } = db.db.dynamic
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Object {
"did": "user(0)",
"handle": "alice.test",
"indexedAt": "1970-01-01T00:00:00.000Z",
"invites": Array [],
"moderation": Object {
"actions": Array [
Object {
Expand Down
22 changes: 22 additions & 0 deletions packages/pds/tests/views/admin/invites.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,26 @@ describe('pds admin invite views', () => {
const combined = [...first.data.codes, ...second.data.codes]
expect(combined).toEqual(full.data.codes)
})

it('filters admin.searchRepos by invitedBy', async () => {
const searchView = await agent.api.com.atproto.admin.searchRepos(
{ invitedBy: alice },
{ headers: { authorization: adminAuth() } },
)
expect(searchView.data.repos.length).toBe(2)
expect(searchView.data.repos[0].invitedBy?.available).toBe(1)
expect(searchView.data.repos[0].invitedBy?.uses.length).toBe(1)
expect(searchView.data.repos[1].invitedBy?.available).toBe(1)
expect(searchView.data.repos[1].invitedBy?.uses.length).toBe(1)
})

it('hydrates invites into admin.getRepo', async () => {
const aliceView = await agent.api.com.atproto.admin.getRepo(
{ did: alice },
{ headers: { authorization: adminAuth() } },
)
expect(aliceView.data.invitedBy?.available).toBe(10)
expect(aliceView.data.invitedBy?.uses.length).toBe(2)
expect(aliceView.data.invites?.length).toBe(6)
})
})

0 comments on commit 5ae26d9

Please sign in to comment.