Skip to content

Commit

Permalink
Implement searchStarterPacks endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelbsky committed Nov 13, 2024
1 parent d522841 commit 45e0fc3
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 0 deletions.
116 changes: 116 additions & 0 deletions packages/bsky/src/api/app/bsky/graph/searchStarterPacks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import AppContext from '../../../../context'
import { Server } from '../../../../lexicon'
import { mapDefined } from '@atproto/common'
import { AtpAgent, AtUri } from '@atproto/api'
import { QueryParams } from '../../../../lexicon/types/app/bsky/graph/searchStarterPacks'
import {
HydrationFnInput,
PresentationFnInput,
RulesFnInput,
SkeletonFnInput,
createPipeline,
} from '../../../../pipeline'
import { HydrateCtx, Hydrator } from '../../../../hydration/hydrator'
import { Views } from '../../../../views'
import { DataPlaneClient } from '../../../../data-plane'
import { parseString } from '../../../../hydration/util'
import { resHeaders } from '../../../util'

export default function (server: Server, ctx: AppContext) {
const searchStarterPacks = createPipeline(
skeleton,
hydration,
noBlocks,
presentation,
)
server.app.bsky.graph.searchStarterPacks({
auth: ctx.authVerifier.standardOptional,
handler: async ({ auth, params, req }) => {
const { viewer, includeTakedowns } = ctx.authVerifier.parseCreds(auth)
const labelers = ctx.reqLabelers(req)
const hydrateCtx = await ctx.hydrator.createContext({
viewer,
labelers,
includeTakedowns,
})
const results = await searchStarterPacks({ ...params, hydrateCtx }, ctx)
return {
encoding: 'application/json',
body: results,
headers: resHeaders({ labelers: hydrateCtx.labelers }),
}
},
})
}

const skeleton = async (inputs: SkeletonFnInput<Context, Params>) => {
const { ctx, params } = inputs
const { q } = params

if (ctx.searchAgent) {
// @NOTE cursors won't change on appview swap
const { data: res } =
await ctx.searchAgent.app.bsky.unspecced.searchStarterPacksSkeleton({
q,
cursor: params.cursor,
limit: params.limit,
viewer: params.hydrateCtx.viewer ?? undefined,
})
return {
uris: res.starterPacks.map(({ uri }) => uri),
cursor: parseString(res.cursor),
}
}

const res = await ctx.dataplane.searchStarterPacks({
term: q,
limit: params.limit,
cursor: params.cursor,
})
return {
uris: res.uris,
cursor: parseString(res.cursor),
}
}

const hydration = async (
inputs: HydrationFnInput<Context, Params, Skeleton>,
) => {
const { ctx, params, skeleton } = inputs
return ctx.hydrator.hydrateStarterPacks(skeleton.uris, params.hydrateCtx)
}

const noBlocks = (inputs: RulesFnInput<Context, Params, Skeleton>) => {
const { ctx, skeleton, hydration } = inputs
skeleton.uris = skeleton.uris.filter(
(uri) => !ctx.views.viewerBlockExists(new AtUri(uri).hostname, hydration),
)
return skeleton
}

const presentation = (
inputs: PresentationFnInput<Context, Params, Skeleton>,
) => {
const { ctx, skeleton, hydration } = inputs
const starterPacks = mapDefined(skeleton.uris, (uri) =>
ctx.views.starterPackBasic(uri, hydration),
)
return {
starterPacks: starterPacks,
cursor: skeleton.cursor,
}
}

type Context = {
dataplane: DataPlaneClient
hydrator: Hydrator
views: Views
searchAgent?: AtpAgent
}

type Params = QueryParams & { hydrateCtx: HydrateCtx }

type Skeleton = {
uris: string[]
cursor?: string
}
2 changes: 2 additions & 0 deletions packages/bsky/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import getMutes from './app/bsky/graph/getMutes'
import getRelationships from './app/bsky/graph/getRelationships'
import getStarterPack from './app/bsky/graph/getStarterPack'
import getStarterPacks from './app/bsky/graph/getStarterPacks'
import searchStarterPacks from './app/bsky/graph/searchStarterPacks'
import muteActor from './app/bsky/graph/muteActor'
import unmuteActor from './app/bsky/graph/unmuteActor'
import muteActorList from './app/bsky/graph/muteActorList'
Expand Down Expand Up @@ -95,6 +96,7 @@ export default function (server: Server, ctx: AppContext) {
getRelationships(server, ctx)
getStarterPack(server, ctx)
getStarterPacks(server, ctx)
searchStarterPacks(server, ctx)
muteActor(server, ctx)
unmuteActor(server, ctx)
muteActorList(server, ctx)
Expand Down
28 changes: 28 additions & 0 deletions packages/bsky/src/data-plane/server/routes/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,34 @@ export default (db: Database): Partial<ServiceImpl<typeof Service>> => ({
cursor: keyset.packFromResult(res),
}
},

async searchStarterPacks(req) {
// @NOTE we don't store the term in the db, so we can't search by it.
const { term: _term, limit, cursor } = req
const { ref } = db.db.dynamic
let builder = db.db.selectFrom('starter_pack').selectAll()

const keyset = new TimeCidKeyset(
ref('starter_pack.sortAt'),
ref('starter_pack.cid'),
)

builder = paginate(builder, {
limit,
cursor,
keyset,
tryIndex: true,
})

const res = await builder.execute()

const cur = keyset.packFromResult(res)

return {
uris: res.map((row) => row.uri),
cursor: cur,
}
},
})

// Remove leading @ in case a handle is input that way
Expand Down
47 changes: 47 additions & 0 deletions packages/bsky/tests/views/starter-packs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('starter packs', () => {
let sp1: RecordRef
let sp2: RecordRef
let sp3: RecordRef
let sp4: RecordRef

beforeAll(async () => {
network = await TestNetwork.create({
Expand Down Expand Up @@ -68,6 +69,13 @@ describe('starter packs', () => {
)
await sc.block(sc.dids.frankie, sc.dids.alice)

sp4 = await sc.createStarterPack(
sc.dids.bob,
"bob's starter pack in case you block alice",
[sc.dids.alice, sc.dids.frankie],
[],
)

await network.processAll()
})

Expand Down Expand Up @@ -198,4 +206,43 @@ describe('starter packs', () => {
expect(view.data.starterPack.listItemsSample?.length).toBe(3)
expect(forSnapshot(view.data.starterPack.listItemsSample)).toMatchSnapshot()
})

describe('searchStarterPacks', () => {
it('searches starter packs and returns paginated', async () => {
const { data: page0 } = await agent.app.bsky.graph.searchStarterPacks({
q: 'starter',
limit: 3,
})

expect(page0.starterPacks).toHaveLength(3)

const { data: page1 } = await agent.api.app.bsky.graph.searchStarterPacks(
{
q: 'starter',
limit: 3,
cursor: page0.cursor,
},
)

// There are 4 starter packs, so 3 for page0, 1 for page1
expect(page1.starterPacks).toHaveLength(1)
})

it('does not include starter packs with creator block relationship for non-creator viewers', async () => {
const { data } = await agent.app.bsky.graph.searchStarterPacks(
{ q: 'starter' },
{
headers: await network.serviceHeaders(
sc.dids.frankie,
ids.AppBskyGraphSearchStarterPacks,
),
},
)

expect(data.starterPacks.length).toBe(1)
expect(
data.starterPacks.filter((sp) => sp.creator.did === sc.dids.alice),
).toHaveLength(0)
})
})
})

0 comments on commit 45e0fc3

Please sign in to comment.