Compare commits

..

1 Commits

Author SHA1 Message Date
f291495d0c feat: add reminder source 2026-06-14 00:00:13 +01:00
10 changed files with 8 additions and 220 deletions

View File

@@ -1,83 +0,0 @@
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,7 +5,6 @@ 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) {
@@ -23,15 +22,6 @@ 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 = LocationSource.id
readonly sourceId = "freya.location"
async feedSourceForUser(
_userId: string,

View File

@@ -1,85 +0,0 @@
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

@@ -1,30 +0,0 @@
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 = WebSearchSource.id
readonly sourceId = "freya.web-search"
private readonly apiKey: string | undefined
private readonly client: WebSearchClient | undefined

View File

@@ -18,7 +18,7 @@ describe("LocationSource", () => {
describe("FeedSource interface", () => {
test("has correct id", () => {
const source = new LocationSource()
expect(source.id).toBe(LocationSource.id)
expect(source.id).toBe("freya.location")
})
test("fetchItems always returns empty array", async () => {

View File

@@ -5,6 +5,8 @@ import { type } from "arktype"
import { Location, type LocationSourceOptions } from "./types.ts"
export const LocationKey: ContextKey<Location> = contextKey("freya.location", "location")
/**
* A FeedSource that provides location context.
*
@@ -14,9 +16,7 @@ import { Location, type LocationSourceOptions } from "./types.ts"
* Does not produce feed items - always returns empty array from `fetchItems`.
*/
export class LocationSource implements FeedSource {
static readonly id = "freya.location"
readonly id = LocationSource.id
readonly id = "freya.location"
private readonly historySize: number
private locations: Location[] = []
@@ -97,5 +97,3 @@ export class LocationSource implements FeedSource {
return []
}
}
export const LocationKey: ContextKey<Location> = contextKey(LocationSource.id, "location")

View File

@@ -37,7 +37,7 @@ describe("WebSearchSource", () => {
test("has correct id", () => {
const source = new WebSearchSource({ client: new RecordingSearchClient() })
expect(source.id).toBe(WebSearchSource.id)
expect(source.id).toBe("freya.web-search")
})
test("does not provide context or feed items", async () => {

View File

@@ -41,9 +41,7 @@ const SearchInput = type({
* action and receive structured web results.
*/
export class WebSearchSource implements FeedSource {
static readonly id = "freya.web-search"
readonly id = WebSearchSource.id
readonly id = "freya.web-search"
private readonly client: WebSearchClient