See the best posts from any Bluesky account
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Rename users table to tracked_profiles ahead of adding accounts table

Renames the SQLite `users` table to `tracked_profiles` and the `User`
model to `TrackedProfile` to avoid confusion when we add a separate
`accounts` table for authenticated Bluesky users. Includes a migration
to rename the table and its index, and updates all imports, raw queries,
and comments across the codebase.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+106 -86
+6 -6
app/controllers/profile_controller.ts
··· 6 6 import type { Facet, FacetLink, FacetMention } from '#lib/atproto/index' 7 7 import { ClickHouseStore } from '#lib/clickhouse/index' 8 8 import { runBackfillStream, type SseWriter } from '#lib/backfill_stream' 9 - import User from '#models/user' 9 + import TrackedProfile from '#models/tracked_profile' 10 10 import BackfillJobRow from '#models/backfill_job' 11 11 import BackfillJob from '#jobs/backfill_job' 12 12 import env from '#start/env' ··· 110 110 } 111 111 112 112 // 2. Look up the user and the backfill job row. Both must exist. 113 - const user = await User.findBy('handle', canonicalHandle) 113 + const user = await TrackedProfile.findBy('handle', canonicalHandle) 114 114 if (!user) { 115 115 return response.status(404).json({ error: 'not_found' }) 116 116 } ··· 223 223 } 224 224 225 225 // 3. Look up user in SQLite 226 - const user = await User.findBy('handle', canonicalHandle) 226 + const user = await TrackedProfile.findBy('handle', canonicalHandle) 227 227 228 228 // 4. Branch on user state 229 229 if (!user || user.backfilledAt === null) { ··· 336 336 * Ensure the backfill is started for a handle that hasn't been backfilled yet. 337 337 * 338 338 * 1. Resolves the handle to a DID (network call). 339 - * 2. Upserts the User row (idempotent). 339 + * 2. Upserts the TrackedProfile row (idempotent). 340 340 * 3. Attempts to insert a BackfillJobRow. If we win the insert race, dispatches 341 341 * the BackfillJob queue job. If another request already inserted the row 342 342 * (duplicate primary key), we skip the dispatch — the dedup gate. ··· 355 355 // writes so network failures leave the schema untouched. 356 356 const { postsCount } = await this.atprotoClient.getProfile(did) 357 357 358 - // 3. Upsert User row (idempotent — firstOrCreate won't duplicate) 359 - await User.firstOrCreate( 358 + // 3. Upsert TrackedProfile row (idempotent — firstOrCreate won't duplicate) 359 + await TrackedProfile.firstOrCreate( 360 360 { did }, 361 361 { 362 362 did,
+8 -8
app/jobs/backfill_job.ts
··· 3 3 import { Job } from '@adonisjs/queue' 4 4 import { AtprotoClient, parseGetAuthorFeedResponse } from '#lib/atproto/index' 5 5 import { ClickHouseStore } from '#lib/clickhouse/index' 6 - import User from '#models/user' 6 + import TrackedProfile from '#models/tracked_profile' 7 7 import BackfillJobRow from '#models/backfill_job' 8 8 9 9 // --------------------------------------------------------------------------- ··· 32 32 * other users (feed items where post.author.did !== targetDid). 33 33 * 3. Inserts snapshots into ClickHouse and updates backfill_jobs.fetched_posts 34 34 * after every page so the loading page can display progress. 35 - * 4. On completion, sets users.backfilled_at = now() and 35 + * 4. On completion, sets tracked_profiles.backfilled_at = now() and 36 36 * backfill_jobs.state = 'done'. 37 37 * 5. On permanent failure (after all retries), sets 38 38 * backfill_jobs.state = 'failed' and backfill_jobs.error = <message>. ··· 61 61 // Read BACKFILL_MAX_POSTS from env (default 10,000) 62 62 const maxPosts = Number(process.env['BACKFILL_MAX_POSTS'] ?? 10000) 63 63 64 - // Fetch the User row — dispatcher must insert it before dispatching 65 - const user = await User.find(did) 66 - if (!user) { 64 + // Fetch the TrackedProfile row — dispatcher must insert it before dispatching 65 + const profile = await TrackedProfile.find(did) 66 + if (!profile) { 67 67 throw new Error( 68 - `BackfillJob: User row missing for DID ${did} — dispatcher should insert it first` 68 + `BackfillJob: TrackedProfile row missing for DID ${did} — dispatcher should insert it first` 69 69 ) 70 70 } 71 71 ··· 136 136 137 137 const now = Date.now() 138 138 139 - user.backfilledAt = now 140 - await user.save() 139 + profile.backfilledAt = now 140 + await profile.save() 141 141 142 142 jobRow.state = 'done' 143 143 jobRow.finishedAt = now
+5 -5
app/lib/bluecrawler/seeder.ts
··· 1 - import User from '#models/user' 1 + import TrackedProfile from '#models/tracked_profile' 2 2 import BackfillJobRow from '#models/backfill_job' 3 3 import type { BluecrawlerAccount } from './parser.js' 4 4 ··· 8 8 } 9 9 10 10 /** 11 - * Create User + BackfillJob rows for a list of accounts and dispatch 11 + * Create TrackedProfile + BackfillJob rows for a list of accounts and dispatch 12 12 * backfill jobs for each new account. 13 13 * 14 - * Skips accounts that already have a User row in the database. 14 + * Skips accounts that already have a TrackedProfile row in the database. 15 15 * 16 16 * @param accounts - Parsed bluecrawler accounts to seed 17 17 * @param dispatchFn - Called with the DID for each new account to dispatch a backfill job ··· 29 29 const account = accounts[i] 30 30 31 31 // Skip if user already exists 32 - const existing = await User.find(account.did) 32 + const existing = await TrackedProfile.find(account.did) 33 33 if (existing) { 34 34 skipped++ 35 35 onProgress?.(i + 1, accounts.length) 36 36 continue 37 37 } 38 38 39 - await User.create({ 39 + await TrackedProfile.create({ 40 40 did: account.did, 41 41 handle: account.handle, 42 42 displayName: account.displayName || null,
+2 -1
app/models/user.ts app/models/tracked_profile.ts
··· 1 1 import { BaseModel, column } from '@adonisjs/lucid/orm' 2 2 3 - export default class User extends BaseModel { 3 + export default class TrackedProfile extends BaseModel { 4 + static table = 'tracked_profiles' 4 5 static primaryKey = 'did' 5 6 static selfAssignPrimaryKey = true 6 7
+3 -3
app/services/jetstream_consumer.ts
··· 38 38 /** 39 39 * Returns the current map of tracked DIDs → handles from the database. 40 40 * Used both for filtering events and for looking up reply parent handles. 41 - * In production: reads from SQLite users table. 41 + * In production: reads from SQLite tracked_profiles table. 42 42 * In tests: returns a controlled map. 43 43 */ 44 44 readTrackedDids: () => Promise<Map<string, string>> ··· 71 71 72 72 /** 73 73 * Marks a user as deleted/taken down in the metadata store. 74 - * In production: UPDATE users SET deleted_at = now() WHERE did = ? 74 + * In production: UPDATE tracked_profiles SET deleted_at = now() WHERE did = ? 75 75 * In tests: records the call. 76 76 */ 77 77 markUserDeleted: (did: string) => Promise<void> 78 78 79 79 /** 80 80 * Updates the handle for a tracked user in the metadata store. 81 - * In production: UPDATE users SET handle = newHandle WHERE did = ? 81 + * In production: UPDATE tracked_profiles SET handle = newHandle WHERE did = ? 82 82 * In tests: records the call. 83 83 */ 84 84 updateUserHandle: (did: string, handle: string) => Promise<void>
+5 -5
commands/jetstream_consume.ts
··· 3 3 import { ClickHouseStore } from '#lib/clickhouse/index' 4 4 import { JetstreamConsumer } from '#services/jetstream_consumer' 5 5 import { readCursor, writeCursor } from '#services/jetstream_cursor_io' 6 - import User from '#models/user' 6 + import TrackedProfile from '#models/tracked_profile' 7 7 8 8 /** 9 9 * Ace command: node ace jetstream:consume ··· 51 51 return new WebSocket(url) 52 52 }, 53 53 54 - // Read tracked DIDs → handles from SQLite users table (exclude soft-deleted users) 54 + // Read tracked DIDs → handles from SQLite tracked_profiles table (exclude soft-deleted profiles) 55 55 readTrackedDids: async () => { 56 - const rows = await db.from('users').whereNull('deleted_at').select('did', 'handle') 56 + const rows = await db.from('tracked_profiles').whereNull('deleted_at').select('did', 'handle') 57 57 return new Map<string, string>( 58 58 rows.map((r: { did: string; handle: string }) => [r.did, r.handle]) 59 59 ) ··· 66 66 67 67 // Mark a user as deleted/takendown in SQLite 68 68 markUserDeleted: async (did: string) => { 69 - await User.query().where('did', did).update({ deletedAt: Date.now() }) 69 + await TrackedProfile.query().where('did', did).update({ deletedAt: Date.now() }) 70 70 }, 71 71 72 72 // Update a user's handle in SQLite 73 73 updateUserHandle: async (did: string, handle: string) => { 74 - await User.query().where('did', did).update({ handle }) 74 + await TrackedProfile.query().where('did', did).update({ handle }) 75 75 }, 76 76 }) 77 77
+19
database/migrations/1775991741241_rename_users_to_tracked_profiles.ts
··· 1 + import { BaseSchema } from '@adonisjs/lucid/schema' 2 + 3 + export default class extends BaseSchema { 4 + async up() { 5 + this.schema.renameTable('users', 'tracked_profiles') 6 + this.schema.alterTable('tracked_profiles', (table) => { 7 + table.dropIndex([], 'users_handle') 8 + table.index(['handle'], 'tracked_profiles_handle') 9 + }) 10 + } 11 + 12 + async down() { 13 + this.schema.alterTable('tracked_profiles', (table) => { 14 + table.dropIndex([], 'tracked_profiles_handle') 15 + table.index(['handle'], 'users_handle') 16 + }) 17 + this.schema.renameTable('tracked_profiles', 'users') 18 + } 19 + }
+2 -2
tests/browser/dark_mode.spec.ts
··· 8 8 import testUtils from '@adonisjs/core/services/test_utils' 9 9 import { ClickHouseStore } from '#lib/clickhouse/index' 10 10 import { AtprotoClient } from '#lib/atproto/index' 11 - import User from '#models/user' 11 + import TrackedProfile from '#models/tracked_profile' 12 12 13 13 // --------------------------------------------------------------------------- 14 14 // ClickHouse helpers (same pattern as profile_controller.spec.ts) ··· 174 174 swap(AtprotoClient, makeFakeAtprotoClient()) 175 175 176 176 // Seed data 177 - await User.create({ 177 + await TrackedProfile.create({ 178 178 did: TEST_DID, 179 179 handle: TEST_HANDLE, 180 180 displayName: 'Test User',
+5 -5
tests/functional/backfill_stream.spec.ts
··· 13 13 */ 14 14 import { test } from '@japa/runner' 15 15 import testUtils from '@adonisjs/core/services/test_utils' 16 - import User from '#models/user' 16 + import TrackedProfile from '#models/tracked_profile' 17 17 import BackfillJobRow from '#models/backfill_job' 18 18 19 19 test.group('GET /profile/:handle/backfill/stream', (group) => { ··· 25 25 }) 26 26 27 27 test('404 when user exists but no backfill job row', async ({ client }) => { 28 - await User.create({ 28 + await TrackedProfile.create({ 29 29 did: 'did:plc:streamtest001', 30 30 handle: 'nojob.bsky.social', 31 31 firstSeenAt: Date.now(), ··· 46 46 client, 47 47 assert, 48 48 }) => { 49 - await User.create({ 49 + await TrackedProfile.create({ 50 50 did: 'did:plc:streamdone001', 51 51 handle: 'donedone.bsky.social', 52 52 firstSeenAt: Date.now(), ··· 72 72 }) 73 73 74 74 test('failed state emits failed event with error payload', async ({ client, assert }) => { 75 - await User.create({ 75 + await TrackedProfile.create({ 76 76 did: 'did:plc:streamfail001', 77 77 handle: 'failfail.bsky.social', 78 78 firstSeenAt: Date.now(), ··· 97 97 }) 98 98 99 99 test('failed state with null error falls back to "unknown"', async ({ client, assert }) => { 100 - await User.create({ 100 + await TrackedProfile.create({ 101 101 did: 'did:plc:streamfail002', 102 102 handle: 'unknownfail.bsky.social', 103 103 firstSeenAt: Date.now(),
+7 -7
tests/functional/profile_controller.spec.ts
··· 14 14 import { ClickHouseStore } from '#lib/clickhouse/index' 15 15 import { AtprotoClient } from '#lib/atproto/index' 16 16 import { HandleResolver } from '#services/handle_resolver' 17 - import User from '#models/user' 17 + import TrackedProfile from '#models/tracked_profile' 18 18 19 19 // --------------------------------------------------------------------------- 20 20 // ClickHouse helpers (mirror clickhouse_store.spec.ts) ··· 213 213 client, 214 214 assert, 215 215 }) => { 216 - await User.create({ 216 + await TrackedProfile.create({ 217 217 did: 'did:plc:deleted001', 218 218 handle: 'dril.bsky.social', 219 219 firstSeenAt: Date.now(), ··· 290 290 }, 291 291 } as unknown as AtprotoClient) 292 292 293 - await User.create({ 293 + await TrackedProfile.create({ 294 294 did: 'did:plc:test001', 295 295 handle: 'dril.bsky.social', 296 296 firstSeenAt: Date.now(), ··· 336 336 }, 337 337 } as unknown as AtprotoClient) 338 338 339 - await User.create({ 339 + await TrackedProfile.create({ 340 340 did: 'did:plc:test002', 341 341 handle: 'dril.bsky.social', 342 342 firstSeenAt: Date.now(), ··· 392 392 }, 393 393 } as unknown as AtprotoClient) 394 394 395 - await User.create({ 395 + await TrackedProfile.create({ 396 396 did: 'did:plc:canonical001', 397 397 handle: 'dril.bsky.social', 398 398 firstSeenAt: Date.now(), ··· 415 415 }, 416 416 } as unknown as AtprotoClient) 417 417 418 - await User.create({ 418 + await TrackedProfile.create({ 419 419 did: 'did:plc:canonical002', 420 420 handle: 'dril.bsky.social', 421 421 firstSeenAt: Date.now(), ··· 447 447 }, 448 448 } as unknown as AtprotoClient) 449 449 450 - await User.create({ 450 + await TrackedProfile.create({ 451 451 did: 'did:plc:test003', 452 452 handle: 'dril.bsky.social', 453 453 firstSeenAt: Date.now(),
+12 -12
tests/functional/profile_controller_dispatch.spec.ts
··· 14 14 import { test } from '@japa/runner' 15 15 import testUtils from '@adonisjs/core/services/test_utils' 16 16 import queue from '@adonisjs/queue/services/main' 17 - import User from '#models/user' 17 + import TrackedProfile from '#models/tracked_profile' 18 18 import BackfillJobRow from '#models/backfill_job' 19 19 import BackfillJob from '#jobs/backfill_job' 20 20 import { HandleResolver, HandleNotFoundError } from '#services/handle_resolver' ··· 108 108 assert.include(response.text(), 'total: 1234') 109 109 assert.include(response.text(), 'role="progressbar"') 110 110 111 - // User row created 112 - const user = await User.findBy('handle', TEST_HANDLE) 111 + // TrackedProfile row created 112 + const user = await TrackedProfile.findBy('handle', TEST_HANDLE) 113 113 assert.isNotNull(user) 114 114 assert.equal(user!.did, TEST_DID) 115 115 assert.isNull(user!.backfilledAt) ··· 147 147 r1.assertStatus(200) 148 148 r2.assertStatus(200) 149 149 150 - // Only ONE User row 151 - const users = await User.query().where('handle', TEST_HANDLE) 150 + // Only ONE TrackedProfile row 151 + const users = await TrackedProfile.query().where('handle', TEST_HANDLE) 152 152 assert.equal(users.length, 1) 153 153 154 154 // Only ONE BackfillJobRow with totalPosts set ··· 169 169 swap, 170 170 }) => { 171 171 // Pre-seed the user (no backfilledAt) and an in-progress BackfillJobRow 172 - await User.create({ 172 + await TrackedProfile.create({ 173 173 did: TEST_DID, 174 174 handle: TEST_HANDLE, 175 175 firstSeenAt: Date.now(), ··· 231 231 assert.include(response.text(), "can't find") 232 232 233 233 // No rows created 234 - const user = await User.findBy('handle', 'nope.bsky.social') 234 + const user = await TrackedProfile.findBy('handle', 'nope.bsky.social') 235 235 assert.isNull(user) 236 236 const jobs = await BackfillJobRow.all() 237 237 assert.equal(jobs.length, 0) ··· 262 262 assert.include(response.text(), 'rate-limited') 263 263 264 264 // No rows created 265 - const user = await User.findBy('handle', 'throttled.bsky.social') 265 + const user = await TrackedProfile.findBy('handle', 'throttled.bsky.social') 266 266 assert.isNull(user) 267 267 268 268 // getProfile was NOT called — resolve threw first ··· 314 314 }) 315 315 316 316 // Test D9: getProfile throws BlueskyRateLimitedError → 503 and no rows created. 317 - // Controller must call getProfile before upserting User, so both User and 317 + // Controller must call getProfile before upserting TrackedProfile, so both TrackedProfile and 318 318 // BackfillJobRow stay absent on network failure. 319 319 test('getProfile BlueskyRateLimitedError renders 503 and creates no rows', async ({ 320 320 client, ··· 332 332 response.assertStatus(503) 333 333 assert.include(response.text(), 'rate-limited') 334 334 335 - const user = await User.findBy('handle', TEST_HANDLE) 335 + const user = await TrackedProfile.findBy('handle', TEST_HANDLE) 336 336 assert.isNull(user) 337 337 const jobs = await BackfillJobRow.all() 338 338 assert.equal(jobs.length, 0) ··· 357 357 response.assertStatus(404) 358 358 assert.include(response.text(), "can't find") 359 359 360 - const user = await User.findBy('handle', TEST_HANDLE) 360 + const user = await TrackedProfile.findBy('handle', TEST_HANDLE) 361 361 assert.isNull(user) 362 362 const jobs = await BackfillJobRow.all() 363 363 assert.equal(jobs.length, 0) ··· 380 380 // Test D8: Already-backfilled user skips dispatch 381 381 test('already-backfilled user does not trigger dispatch', async ({ client, assert, swap }) => { 382 382 // Pre-seed a fully backfilled user 383 - await User.create({ 383 + await TrackedProfile.create({ 384 384 did: TEST_DID, 385 385 handle: TEST_HANDLE, 386 386 firstSeenAt: Date.now(),
+15 -15
tests/unit/jobs/backfill_job.spec.ts
··· 3 3 import type { PostSnapshot } from '#lib/atproto/index' 4 4 import type { AtprotoClient } from '#lib/atproto/index' 5 5 import type { ClickHouseStore } from '#lib/clickhouse/index' 6 - import User from '#models/user' 6 + import TrackedProfile from '#models/tracked_profile' 7 7 import BackfillJobRow from '#models/backfill_job' 8 8 import BackfillJob from '#jobs/backfill_job' 9 9 ··· 142 142 143 143 test('inserts snapshots, sets backfilledAt, marks job done', async ({ assert }) => { 144 144 // Seed SQLite rows 145 - await User.create({ did: TEST_DID, handle: 'test.bsky.social', firstSeenAt: Date.now() }) 145 + await TrackedProfile.create({ did: TEST_DID, handle: 'test.bsky.social', firstSeenAt: Date.now() }) 146 146 await BackfillJobRow.create({ 147 147 did: TEST_DID, 148 148 startedAt: Date.now(), ··· 173 173 assert.equal(clickhouse._calls[0].length, 5) 174 174 175 175 // users.backfilledAt is now set 176 - const user = await User.findOrFail(TEST_DID) 176 + const user = await TrackedProfile.findOrFail(TEST_DID) 177 177 assert.isNotNull(user.backfilledAt) 178 178 179 179 // backfill_jobs.state = 'done' ··· 190 190 test('calls getAuthorFeed twice, never calls getPosts, final fetchedPosts is 200', async ({ 191 191 assert, 192 192 }) => { 193 - await User.create({ did: TEST_DID, handle: 'test.bsky.social', firstSeenAt: Date.now() }) 193 + await TrackedProfile.create({ did: TEST_DID, handle: 'test.bsky.social', firstSeenAt: Date.now() }) 194 194 await BackfillJobRow.create({ 195 195 did: TEST_DID, 196 196 startedAt: Date.now(), ··· 250 250 group.each.setup(() => testUtils.db().withGlobalTransaction()) 251 251 252 252 test('stops after max posts, does not fetch more pages', async ({ assert }) => { 253 - await User.create({ did: TEST_DID, handle: 'test.bsky.social', firstSeenAt: Date.now() }) 253 + await TrackedProfile.create({ did: TEST_DID, handle: 'test.bsky.social', firstSeenAt: Date.now() }) 254 254 await BackfillJobRow.create({ 255 255 did: TEST_DID, 256 256 startedAt: Date.now(), ··· 310 310 group.each.setup(() => testUtils.db().withGlobalTransaction()) 311 311 312 312 test('no insertPostSnapshots call, backfilledAt still set, state done', async ({ assert }) => { 313 - await User.create({ did: TEST_DID, handle: 'test.bsky.social', firstSeenAt: Date.now() }) 313 + await TrackedProfile.create({ did: TEST_DID, handle: 'test.bsky.social', firstSeenAt: Date.now() }) 314 314 await BackfillJobRow.create({ 315 315 did: TEST_DID, 316 316 startedAt: Date.now(), ··· 330 330 // No snapshots inserted (empty user has no posts) 331 331 assert.equal(clickhouse._calls.length, 0) 332 332 333 - // User still gets marked as backfilled 334 - const user = await User.findOrFail(TEST_DID) 333 + // TrackedProfile still gets marked as backfilled 334 + const user = await TrackedProfile.findOrFail(TEST_DID) 335 335 assert.isNotNull(user.backfilledAt) 336 336 337 337 // Job row marked done ··· 347 347 test('drops reposts of other users; fetched_posts counts only author-owned snapshots', async ({ 348 348 assert, 349 349 }) => { 350 - await User.create({ did: TEST_DID, handle: 'test.bsky.social', firstSeenAt: Date.now() }) 350 + await TrackedProfile.create({ did: TEST_DID, handle: 'test.bsky.social', firstSeenAt: Date.now() }) 351 351 await BackfillJobRow.create({ 352 352 did: TEST_DID, 353 353 startedAt: Date.now(), ··· 390 390 group.each.setup(() => testUtils.db().withGlobalTransaction()) 391 391 392 392 test('failed() sets state=failed and saves error message', async ({ assert }) => { 393 - await User.create({ did: TEST_DID, handle: 'test.bsky.social', firstSeenAt: Date.now() }) 393 + await TrackedProfile.create({ did: TEST_DID, handle: 'test.bsky.social', firstSeenAt: Date.now() }) 394 394 await BackfillJobRow.create({ 395 395 did: TEST_DID, 396 396 startedAt: Date.now(), ··· 416 416 group.each.setup(() => testUtils.db().withGlobalTransaction()) 417 417 418 418 test('second execution bails out when lock is already held', async ({ assert }) => { 419 - await User.create({ did: TEST_DID, handle: 'test.bsky.social', firstSeenAt: Date.now() }) 419 + await TrackedProfile.create({ did: TEST_DID, handle: 'test.bsky.social', firstSeenAt: Date.now() }) 420 420 await BackfillJobRow.create({ 421 421 did: TEST_DID, 422 422 startedAt: Date.now(), ··· 488 488 test.group('BackfillJob — error cases', (group) => { 489 489 group.each.setup(() => testUtils.db().withGlobalTransaction()) 490 490 491 - test('throws if User row is missing', async ({ assert }) => { 492 - // No User.create — just the backfill job row 491 + test('throws if TrackedProfile row is missing', async ({ assert }) => { 492 + // No TrackedProfile.create — just the backfill job row 493 493 await BackfillJobRow.create({ 494 494 did: TEST_DID, 495 495 startedAt: Date.now(), ··· 501 501 const clickhouse = makeMockClickHouseStore() 502 502 const job = makeJob(atproto, clickhouse) 503 503 504 - await assert.rejects(() => job.execute(), /User row missing/) 504 + await assert.rejects(() => job.execute(), /TrackedProfile row missing/) 505 505 }) 506 506 507 507 test('throws if BackfillJob row is missing', async ({ assert }) => { 508 508 // No BackfillJobRow.create — just the user 509 - await User.create({ did: TEST_DID, handle: 'test.bsky.social', firstSeenAt: Date.now() }) 509 + await TrackedProfile.create({ did: TEST_DID, handle: 'test.bsky.social', firstSeenAt: Date.now() }) 510 510 511 511 const atproto = makeMockAtprotoClient({ pages: [] }) 512 512 const clickhouse = makeMockClickHouseStore()
+10 -10
tests/unit/lucid_schemas.spec.ts
··· 1 1 import { test } from '@japa/runner' 2 2 import testUtils from '@adonisjs/core/services/test_utils' 3 3 import db from '@adonisjs/lucid/services/db' 4 - import User from '#models/user' 4 + import TrackedProfile from '#models/tracked_profile' 5 5 import JetstreamCursor from '#models/jetstream_cursor' 6 6 import BackfillJob from '#models/backfill_job' 7 7 ··· 11 11 * schema is wired correctly. 12 12 */ 13 13 14 - test.group('Lucid schemas — User', (group) => { 14 + test.group('Lucid schemas — TrackedProfile', (group) => { 15 15 group.each.setup(() => testUtils.db().withGlobalTransaction()) 16 16 17 17 test('can create a user row with DID as primary key', async ({ assert }) => { 18 - await User.create({ 18 + await TrackedProfile.create({ 19 19 did: 'did:plc:testuser001', 20 20 handle: 'testuser.bsky.social', 21 21 firstSeenAt: Date.now(), 22 22 }) 23 23 24 24 // Re-fetch to get DB-defaulted values 25 - const user = await User.findOrFail('did:plc:testuser001') 25 + const user = await TrackedProfile.findOrFail('did:plc:testuser001') 26 26 27 27 assert.equal(user.did, 'did:plc:testuser001') 28 28 assert.equal(user.handle, 'testuser.bsky.social') ··· 34 34 }) 35 35 36 36 test('can fetch user by DID', async ({ assert }) => { 37 - await User.create({ 37 + await TrackedProfile.create({ 38 38 did: 'did:plc:fetchtest', 39 39 handle: 'fetchtest.bsky.social', 40 40 firstSeenAt: Date.now(), 41 41 }) 42 42 43 - const found = await User.find('did:plc:fetchtest') 43 + const found = await TrackedProfile.find('did:plc:fetchtest') 44 44 assert.isNotNull(found) 45 45 assert.equal(found!.handle, 'fetchtest.bsky.social') 46 46 }) ··· 73 73 }) 74 74 }) 75 75 76 - test.group('Lucid schemas — User whereNull deleted_at filter', (group) => { 76 + test.group('Lucid schemas — TrackedProfile whereNull deleted_at filter', (group) => { 77 77 group.each.setup(() => testUtils.db().withGlobalTransaction()) 78 78 79 79 test('whereNull deleted_at excludes users with deleted_at set', async ({ assert }) => { 80 80 // Insert an active user (no deleted_at) 81 - await User.create({ 81 + await TrackedProfile.create({ 82 82 did: 'did:plc:activeuser001', 83 83 handle: 'activeuser.bsky.social', 84 84 firstSeenAt: Date.now(), 85 85 }) 86 86 87 87 // Insert a deleted user (deleted_at is set) 88 - await User.create({ 88 + await TrackedProfile.create({ 89 89 did: 'did:plc:deleteduser001', 90 90 handle: 'deleteduser.bsky.social', 91 91 firstSeenAt: Date.now(), ··· 93 93 }) 94 94 95 95 // Run the same query used in jetstream_consume.ts (with the fix applied) 96 - const rows = await db.from('users').whereNull('deleted_at').select('did') 96 + const rows = await db.from('tracked_profiles').whereNull('deleted_at').select('did') 97 97 const dids = rows.map((r: { did: string }) => r.did) 98 98 99 99 assert.include(dids, 'did:plc:activeuser001', 'active user should be included')
+7 -7
tests/unit/seed_top_accounts.spec.ts
··· 1 1 import { test } from '@japa/runner' 2 2 import testUtils from '@adonisjs/core/services/test_utils' 3 - import User from '#models/user' 3 + import TrackedProfile from '#models/tracked_profile' 4 4 import BackfillJobRow from '#models/backfill_job' 5 5 import type { BluecrawlerAccount } from '#lib/bluecrawler/parser' 6 6 import { seedTopAccounts } from '#lib/bluecrawler/seeder' ··· 25 25 test.group('seedTopAccounts', (group) => { 26 26 group.each.setup(() => testUtils.db().withGlobalTransaction()) 27 27 28 - test('creates User and BackfillJob rows for each account', async ({ assert }) => { 28 + test('creates TrackedProfile and BackfillJob rows for each account', async ({ assert }) => { 29 29 const accounts = makeAccounts(3) 30 30 const dispatched: string[] = [] 31 31 ··· 37 37 assert.equal(result.skipped, 0) 38 38 39 39 for (const account of accounts) { 40 - const user = await User.find(account.did) 41 - assert.isNotNull(user, `User ${account.did} should exist`) 40 + const user = await TrackedProfile.find(account.did) 41 + assert.isNotNull(user, `TrackedProfile ${account.did} should exist`) 42 42 assert.equal(user!.handle, account.handle) 43 43 assert.equal(user!.displayName, account.displayName) 44 44 ··· 50 50 assert.deepEqual(dispatched, accounts.map((a) => a.did)) 51 51 }) 52 52 53 - test('skips accounts that already have a User row', async ({ assert }) => { 53 + test('skips accounts that already have a TrackedProfile row', async ({ assert }) => { 54 54 const accounts = makeAccounts(2) 55 55 56 56 // Pre-create one user 57 - await User.create({ 57 + await TrackedProfile.create({ 58 58 did: accounts[0].did, 59 59 handle: accounts[0].handle, 60 60 firstSeenAt: Date.now(), ··· 74 74 const accounts = makeAccounts(1) 75 75 76 76 // Pre-create user + backfill job 77 - await User.create({ 77 + await TrackedProfile.create({ 78 78 did: accounts[0].did, 79 79 handle: accounts[0].handle, 80 80 firstSeenAt: Date.now(),