feat: seed default user sources

This commit is contained in:
2026-06-14 14:24:17 +01:00
parent efd7537008
commit 8532d6dbac
10 changed files with 220 additions and 8 deletions

View File

@@ -0,0 +1,83 @@
import { afterEach, describe, expect, test } from "bun:test"
import type { Database } from "../db/index.ts"
import { DEFAULT_ENABLED_SOURCE_IDS } from "../sources/default-sources.ts"
import { createAuth } from "./index.ts"
interface UserSourceInsertRow {
sourceId: string
}
interface RecordingDb {
db: Database
rows: () => UserSourceInsertRow[] | undefined
}
const originalBetterAuthSecret = process.env.BETTER_AUTH_SECRET
function createRecordingDb(): RecordingDb {
let insertedRows: UserSourceInsertRow[] | undefined
const db = {
insert() {
return {
values(rows: UserSourceInsertRow[]) {
insertedRows = rows
return {
async onConflictDoNothing() {},
}
},
}
},
} as unknown as Database
return {
db,
rows: () => insertedRows,
}
}
afterEach(() => {
if (originalBetterAuthSecret === undefined) {
delete process.env.BETTER_AUTH_SECRET
return
}
process.env.BETTER_AUTH_SECRET = originalBetterAuthSecret
})
describe("createAuth", () => {
test("inserts default sources after Better Auth creates a user", async () => {
process.env.BETTER_AUTH_SECRET = "test-secret"
const recording = createRecordingDb()
const auth = createAuth(recording.db)
const afterCreateUser = auth.options.databaseHooks?.user?.create?.after
if (!afterCreateUser) {
throw new Error("Expected a user create after hook")
}
const now = new Date()
await afterCreateUser(
{
id: "user-1",
name: "Test User",
email: "test@example.com",
emailVerified: false,
image: null,
createdAt: now,
updatedAt: now,
},
null,
)
const rows = recording.rows()
if (!rows) {
throw new Error("Expected the auth hook to insert default sources")
}
expect(rows.map((row) => row.sourceId)).toEqual([...DEFAULT_ENABLED_SOURCE_IDS])
})
})

View File

@@ -5,6 +5,7 @@ import { admin } from "better-auth/plugins"
import type { Database } from "../db/index.ts"
import * as schema from "../db/schema.ts"
import { insertDefaultUserSources } from "../sources/default-sources.ts"
export function createAuth(db: Database) {
if (!process.env.BETTER_AUTH_SECRET) {
@@ -22,6 +23,15 @@ export function createAuth(db: Database) {
emailAndPassword: {
enabled: true,
},
databaseHooks: {
user: {
create: {
async after(user, _context) {
await insertDefaultUserSources(db, user.id)
},
},
},
},
plugins: [admin()],
})
}

View File

@@ -3,7 +3,7 @@ import { LocationSource } from "@freya/source-location"
import type { FeedSourceProvider } from "../session/feed-source-provider.ts"
export class LocationSourceProvider implements FeedSourceProvider {
readonly sourceId = "freya.location"
readonly sourceId = LocationSource.id
async feedSourceForUser(
_userId: string,

View File

@@ -0,0 +1,85 @@
import { LocationSource } from "@freya/source-location"
import { WebSearchSource } from "@freya/source-web-search"
import { describe, expect, test } from "bun:test"
import type { Database } from "../db/index.ts"
import { userSources } from "../db/schema.ts"
import { DEFAULT_ENABLED_SOURCE_IDS, insertDefaultUserSources } from "./default-sources.ts"
interface UserSourceInsertRow {
userId: string
sourceId: string
enabled: boolean
config: unknown
createdAt: Date
updatedAt: Date
}
interface RecordingDb {
db: Database
table: () => unknown
rows: () => UserSourceInsertRow[] | undefined
conflictTarget: () => readonly unknown[] | undefined
}
function createRecordingDb(): RecordingDb {
let insertedTable: unknown
let insertedRows: UserSourceInsertRow[] | undefined
let target: readonly unknown[] | undefined
const db = {
insert(table: unknown) {
insertedTable = table
return {
values(rows: UserSourceInsertRow[]) {
insertedRows = rows
return {
async onConflictDoNothing(options: { target: readonly unknown[] }) {
target = options.target
},
}
},
}
},
} as unknown as Database
return {
db,
table: () => insertedTable,
rows: () => insertedRows,
conflictTarget: () => target,
}
}
describe("default user sources", () => {
test("defines location and web search as default enabled sources", () => {
expect(DEFAULT_ENABLED_SOURCE_IDS).toEqual([LocationSource.id, WebSearchSource.id])
})
test("inserts default enabled source rows for a user", async () => {
const recording = createRecordingDb()
await insertDefaultUserSources(recording.db, "user-1")
const rows = recording.rows()
if (!rows) {
throw new Error("Expected default source rows to be inserted")
}
expect(recording.table()).toBe(userSources)
expect(rows).toHaveLength(2)
expect(rows.map((row) => row.sourceId)).toEqual([...DEFAULT_ENABLED_SOURCE_IDS])
expect(recording.conflictTarget()).toEqual([userSources.userId, userSources.sourceId])
for (const row of rows) {
expect(row.userId).toBe("user-1")
expect(row.enabled).toBe(true)
expect(row.config).toEqual({})
expect(row.createdAt).toBeInstanceOf(Date)
expect(row.updatedAt).toBe(row.createdAt)
}
})
})

View File

@@ -0,0 +1,30 @@
import { LocationSource } from "@freya/source-location"
import { WebSearchSource } from "@freya/source-web-search"
import type { Database } from "../db/index.ts"
import { userSources } from "../db/schema.ts"
export const DEFAULT_ENABLED_SOURCE_IDS = [LocationSource.id, WebSearchSource.id] as const
export type DefaultEnabledSourceId = (typeof DEFAULT_ENABLED_SOURCE_IDS)[number]
export async function insertDefaultUserSources(db: Database, userId: string): Promise<void> {
const now = new Date()
await db
.insert(userSources)
.values(
DEFAULT_ENABLED_SOURCE_IDS.map((sourceId) => ({
userId,
sourceId,
enabled: true,
config: {},
createdAt: now,
updatedAt: now,
})),
)
.onConflictDoNothing({
target: [userSources.userId, userSources.sourceId],
})
}

View File

@@ -7,7 +7,7 @@ export type WebSearchSourceProviderOptions =
| { apiKey?: never; client: WebSearchClient }
export class WebSearchSourceProvider implements FeedSourceProvider {
readonly sourceId = "freya.web-search"
readonly sourceId = WebSearchSource.id
private readonly apiKey: string | undefined
private readonly client: WebSearchClient | undefined