Sifa professional network API (Fastify, AT Protocol, Jetstream)
sifa.id/
1import { Agent } from '@atproto/api';
2import type { OAuthSession } from '@atproto/oauth-client';
3import { eq } from 'drizzle-orm';
4import { buildApplyWritesOp, writeToUserPds } from './pds-writer.js';
5import type { ApplyWritesOp } from './pds-writer.js';
6import type { Database } from '../db/index.js';
7import { profiles, externalAccountVerifications } from '../db/schema/index.js';
8
9const SIFA_COLLECTIONS = [
10 'id.sifa.profile.self',
11 'id.sifa.profile.position',
12 'id.sifa.profile.education',
13 'id.sifa.profile.skill',
14 'id.sifa.profile.certification',
15 'id.sifa.profile.project',
16 'id.sifa.profile.volunteering',
17 'id.sifa.profile.publication',
18 'id.sifa.profile.course',
19 'id.sifa.profile.honor',
20 'id.sifa.profile.language',
21 'id.sifa.profile.externalAccount',
22] as const;
23
24export { SIFA_COLLECTIONS };
25
26export async function buildPdsDeleteOps(
27 agent: Agent,
28 did: string,
29 collections: readonly string[],
30): Promise<ApplyWritesOp[]> {
31 const ops: ApplyWritesOp[] = [];
32 for (const collection of collections) {
33 let cursor: string | undefined;
34 do {
35 const existing = await agent.com.atproto.repo.listRecords({
36 repo: did,
37 collection,
38 limit: 100,
39 cursor,
40 });
41 for (const rec of existing.data.records) {
42 const rkey = rec.uri.split('/').pop() ?? '';
43 if (rkey) ops.push(buildApplyWritesOp('delete', collection, rkey));
44 }
45 cursor = existing.data.cursor;
46 } while (cursor);
47 }
48 return ops;
49}
50
51export async function wipeSifaData(
52 session: OAuthSession,
53 did: string,
54 db: Database,
55): Promise<void> {
56 const agent = new Agent(session);
57 const ops = await buildPdsDeleteOps(agent, did, SIFA_COLLECTIONS);
58
59 // applyWrites has a 200-op limit per call
60 const BATCH_SIZE = 200;
61 for (let i = 0; i < ops.length; i += BATCH_SIZE) {
62 const batch = ops.slice(i, i + BATCH_SIZE);
63 await writeToUserPds(session, did, batch);
64 }
65
66 // Update instead of delete: preserve createdAt (join date), langs, headlineOverride,
67 // aboutOverride, handle, displayName, avatarUrl, and pdsHost -- only null profile content.
68 await db
69 .update(profiles)
70 .set({
71 headline: null,
72 about: null,
73 industry: null,
74 locationCountry: null,
75 locationRegion: null,
76 locationCity: null,
77 countryCode: null,
78 openTo: null,
79 preferredWorkplace: null,
80 updatedAt: new Date(),
81 })
82 .where(eq(profiles.did, did));
83 await db.delete(externalAccountVerifications).where(eq(externalAccountVerifications.did, did));
84}