diff --git a/backend/src/database/d1.ts b/backend/src/database/d1.ts index 99359e8..41c7fe4 100644 --- a/backend/src/database/d1.ts +++ b/backend/src/database/d1.ts @@ -1,6 +1,27 @@ -import { type Database } from 'wildebeest/backend/src/database' +import { type Database, QueryBuilder } from 'wildebeest/backend/src/database' import type { Env } from 'wildebeest/backend/src/types/env' -export default function make({ DATABASE }: Pick): Database { - return DATABASE +const qb: QueryBuilder = { + jsonExtract(obj: string, prop: string): string { + return `json_extract(${obj}, '$.${prop}')` + }, + + jsonExtractIsNull(obj: string, prop: string): string { + return `${qb.jsonExtract(obj, prop)} IS NULL` + }, + + set(array: string): string { + return `(SELECT value FROM json_each(${array}))` + }, + + epoch(): string { + return '00-00-00 00:00:00' + }, +} + +export default function make({ DATABASE }: Pick): Database { + const db = DATABASE as any + db.qb = qb + + return db as Database } diff --git a/backend/src/database/index.ts b/backend/src/database/index.ts index 01ab295..4d92787 100644 --- a/backend/src/database/index.ts +++ b/backend/src/database/index.ts @@ -13,6 +13,7 @@ export interface Database { dump(): Promise batch(statements: PreparedStatement[]): Promise[]> exec(query: string): Promise> + qb: QueryBuilder } export interface PreparedStatement { @@ -23,6 +24,13 @@ export interface PreparedStatement { raw(): Promise } +export interface QueryBuilder { + jsonExtract(obj: string, prop: string): string + jsonExtractIsNull(obj: string, prop: string): string + set(array: string): string + epoch(): string +} + export async function getDatabase(env: Pick): Promise { return d1(env) } diff --git a/backend/src/mastodon/timeline.ts b/backend/src/mastodon/timeline.ts index 57c945b..b57a283 100644 --- a/backend/src/mastodon/timeline.ts +++ b/backend/src/mastodon/timeline.ts @@ -16,7 +16,7 @@ export async function getHomeTimeline(domain: string, db: Database, actor: Actor ` SELECT actor_following.target_actor_id as id, - json_extract(actors.properties, '$.followers') as actorFollowersURL + ${db.qb.jsonExtract('actors.properties', 'followers')} as actorFollowersURL FROM actor_following INNER JOIN actors ON actors.id = actor_following.target_actor_id WHERE actor_id=? AND state='accepted' @@ -60,9 +60,9 @@ INNER JOIN objects ON objects.id = outbox_objects.object_id INNER JOIN actors ON actors.id = outbox_objects.actor_id WHERE objects.type = 'Note' - AND outbox_objects.actor_id IN (SELECT value FROM json_each(?2)) - AND json_extract(objects.properties, '$.inReplyTo') IS NULL - AND (outbox_objects.target = '${PUBLIC_GROUP}' OR outbox_objects.target IN (SELECT value FROM json_each(?3))) + AND outbox_objects.actor_id IN ${db.qb.set('?2')} + AND ${db.qb.jsonExtractIsNull('objects.properties', 'inReplyTo')} + AND (outbox_objects.target = '${PUBLIC_GROUP}' OR outbox_objects.target IN ${db.qb.set('?3')}) GROUP BY objects.id ORDER by outbox_objects.published_date DESC LIMIT ?4 @@ -101,7 +101,7 @@ export enum LocalPreference { function localPreferenceQuery(preference: LocalPreference): string { switch (preference) { case LocalPreference.NotSet: - return '1' + return 'true' case LocalPreference.OnlyLocal: return 'objects.local = 1' case LocalPreference.OnlyRemote: @@ -136,7 +136,7 @@ INNER JOIN actors ON actors.id=outbox_objects.actor_id LEFT JOIN note_hashtags ON objects.id=note_hashtags.object_id WHERE objects.type='Note' AND ${localPreferenceQuery(localPreference)} - AND json_extract(objects.properties, '$.inReplyTo') IS NULL + AND ${db.qb.jsonExtractIsNull('objects.properties', 'inReplyTo')} AND outbox_objects.target = '${PUBLIC_GROUP}' ${hashtagFilter} GROUP BY objects.id diff --git a/backend/test/mastodon/apps.spec.ts b/backend/test/mastodon/apps.spec.ts index b80edc7..998a2fa 100644 --- a/backend/test/mastodon/apps.spec.ts +++ b/backend/test/mastodon/apps.spec.ts @@ -76,12 +76,14 @@ describe('Mastodon APIs', () => { }) test('GET /apps is bad request', async () => { + const db = await makeDB() const vapidKeys = await generateVAPIDKeys() const request = new Request('https://example.com') const ctx: any = { next: () => new Response(), data: null, env: { + DATABASE: db, VAPID_JWK: JSON.stringify(vapidKeys), }, request, diff --git a/backend/test/mastodon/statuses.spec.ts b/backend/test/mastodon/statuses.spec.ts index d81c05f..3e834dd 100644 --- a/backend/test/mastodon/statuses.spec.ts +++ b/backend/test/mastodon/statuses.spec.ts @@ -81,7 +81,7 @@ describe('Mastodon APIs', () => { .prepare( ` SELECT - json_extract(properties, '$.content') as content, + ${db.qb.jsonExtract('properties', 'content')} as content, original_actor_id, original_object_id FROM objects @@ -758,7 +758,7 @@ describe('Mastodon APIs', () => { const row = await db .prepare( ` - SELECT json_extract(properties, '$.inReplyTo') as inReplyTo + SELECT ${db.qb.jsonExtract('properties', 'inReplyTo')} as inReplyTo FROM objects WHERE mastodon_id=? ` diff --git a/backend/test/utils.ts b/backend/test/utils.ts index f0b1471..7220ca7 100644 --- a/backend/test/utils.ts +++ b/backend/test/utils.ts @@ -9,6 +9,7 @@ import * as path from 'path' import { BetaDatabase } from '@miniflare/d1' import * as SQLiteDatabase from 'better-sqlite3' import { type Database } from 'wildebeest/backend/src/database' +import d1 from 'wildebeest/backend/src/database/d1' export function isUrlValid(s: string) { let url @@ -32,7 +33,8 @@ export async function makeDB(): Promise { db.exec(content) } - return db2 as unknown as Database + const env = { DATABASE: db2 } as any + return d1(env) } export function assertCORS(response: Response) { diff --git a/functions/api/v1/accounts/[id]/statuses.ts b/functions/api/v1/accounts/[id]/statuses.ts index ae44cfe..2ef1de2 100644 --- a/functions/api/v1/accounts/[id]/statuses.ts +++ b/functions/api/v1/accounts/[id]/statuses.ts @@ -140,7 +140,7 @@ FROM outbox_objects INNER JOIN objects ON objects.id=outbox_objects.object_id INNER JOIN actors ON actors.id=outbox_objects.actor_id WHERE objects.type='Note' - ${withReplies ? '' : "AND json_extract(objects.properties, '$.inReplyTo') IS NULL"} + ${withReplies ? '' : 'AND ' + db.qb.jsonExtractIsNull('objects.properties', 'inReplyTo')} AND outbox_objects.target = '${PUBLIC_GROUP}' AND outbox_objects.actor_id = ?1 AND outbox_objects.cdate > ?2 @@ -161,7 +161,7 @@ LIMIT ?3 OFFSET ?4 return new Response(JSON.stringify(out), { headers }) } - let afterCdate = '00-00-00 00:00:00' + let afterCdate = db.qb.epoch() if (url.searchParams.has('max_id')) { // Client asked to retrieve statuses after the max_id // As opposed to Mastodon we don't use incremental ID but UUID, we need