introduce simple SQL query builder

pull/353/head
Sven Sauleau 2023-02-28 16:51:38 +00:00
rodzic 42a56c560a
commit 217ff34353
7 zmienionych plików z 47 dodań i 14 usunięć

Wyświetl plik

@ -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<Env, 'DATABASE'>): 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<Env, 'DATABASE'>): Database {
const db = DATABASE as any
db.qb = qb
return db as Database
}

Wyświetl plik

@ -13,6 +13,7 @@ export interface Database {
dump(): Promise<ArrayBuffer>
batch<T = unknown>(statements: PreparedStatement[]): Promise<Result<T>[]>
exec<T = unknown>(query: string): Promise<Result<T>>
qb: QueryBuilder
}
export interface PreparedStatement {
@ -23,6 +24,13 @@ export interface PreparedStatement {
raw<T = unknown>(): Promise<T[]>
}
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<Env, 'DATABASE'>): Promise<Database> {
return d1(env)
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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,

Wyświetl plik

@ -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=?
`

Wyświetl plik

@ -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<Database> {
db.exec(content)
}
return db2 as unknown as Database
const env = { DATABASE: db2 } as any
return d1(env)
}
export function assertCORS(response: Response) {

Wyświetl plik

@ -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