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' import type { Env } from 'wildebeest/backend/src/types/env'
export default function make({ DATABASE }: Pick<Env, 'DATABASE'>): Database { const qb: QueryBuilder = {
return DATABASE 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> dump(): Promise<ArrayBuffer>
batch<T = unknown>(statements: PreparedStatement[]): Promise<Result<T>[]> batch<T = unknown>(statements: PreparedStatement[]): Promise<Result<T>[]>
exec<T = unknown>(query: string): Promise<Result<T>> exec<T = unknown>(query: string): Promise<Result<T>>
qb: QueryBuilder
} }
export interface PreparedStatement { export interface PreparedStatement {
@ -23,6 +24,13 @@ export interface PreparedStatement {
raw<T = unknown>(): Promise<T[]> 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> { export async function getDatabase(env: Pick<Env, 'DATABASE'>): Promise<Database> {
return d1(env) return d1(env)
} }

Wyświetl plik

@ -16,7 +16,7 @@ export async function getHomeTimeline(domain: string, db: Database, actor: Actor
` `
SELECT SELECT
actor_following.target_actor_id as id, 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 FROM actor_following
INNER JOIN actors ON actors.id = actor_following.target_actor_id INNER JOIN actors ON actors.id = actor_following.target_actor_id
WHERE actor_id=? AND state='accepted' 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 INNER JOIN actors ON actors.id = outbox_objects.actor_id
WHERE WHERE
objects.type = 'Note' objects.type = 'Note'
AND outbox_objects.actor_id IN (SELECT value FROM json_each(?2)) AND outbox_objects.actor_id IN ${db.qb.set('?2')}
AND json_extract(objects.properties, '$.inReplyTo') IS NULL AND ${db.qb.jsonExtractIsNull('objects.properties', 'inReplyTo')}
AND (outbox_objects.target = '${PUBLIC_GROUP}' OR outbox_objects.target IN (SELECT value FROM json_each(?3))) AND (outbox_objects.target = '${PUBLIC_GROUP}' OR outbox_objects.target IN ${db.qb.set('?3')})
GROUP BY objects.id GROUP BY objects.id
ORDER by outbox_objects.published_date DESC ORDER by outbox_objects.published_date DESC
LIMIT ?4 LIMIT ?4
@ -101,7 +101,7 @@ export enum LocalPreference {
function localPreferenceQuery(preference: LocalPreference): string { function localPreferenceQuery(preference: LocalPreference): string {
switch (preference) { switch (preference) {
case LocalPreference.NotSet: case LocalPreference.NotSet:
return '1' return 'true'
case LocalPreference.OnlyLocal: case LocalPreference.OnlyLocal:
return 'objects.local = 1' return 'objects.local = 1'
case LocalPreference.OnlyRemote: 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 LEFT JOIN note_hashtags ON objects.id=note_hashtags.object_id
WHERE objects.type='Note' WHERE objects.type='Note'
AND ${localPreferenceQuery(localPreference)} AND ${localPreferenceQuery(localPreference)}
AND json_extract(objects.properties, '$.inReplyTo') IS NULL AND ${db.qb.jsonExtractIsNull('objects.properties', 'inReplyTo')}
AND outbox_objects.target = '${PUBLIC_GROUP}' AND outbox_objects.target = '${PUBLIC_GROUP}'
${hashtagFilter} ${hashtagFilter}
GROUP BY objects.id GROUP BY objects.id

Wyświetl plik

@ -76,12 +76,14 @@ describe('Mastodon APIs', () => {
}) })
test('GET /apps is bad request', async () => { test('GET /apps is bad request', async () => {
const db = await makeDB()
const vapidKeys = await generateVAPIDKeys() const vapidKeys = await generateVAPIDKeys()
const request = new Request('https://example.com') const request = new Request('https://example.com')
const ctx: any = { const ctx: any = {
next: () => new Response(), next: () => new Response(),
data: null, data: null,
env: { env: {
DATABASE: db,
VAPID_JWK: JSON.stringify(vapidKeys), VAPID_JWK: JSON.stringify(vapidKeys),
}, },
request, request,

Wyświetl plik

@ -81,7 +81,7 @@ describe('Mastodon APIs', () => {
.prepare( .prepare(
` `
SELECT SELECT
json_extract(properties, '$.content') as content, ${db.qb.jsonExtract('properties', 'content')} as content,
original_actor_id, original_actor_id,
original_object_id original_object_id
FROM objects FROM objects
@ -758,7 +758,7 @@ describe('Mastodon APIs', () => {
const row = await db const row = await db
.prepare( .prepare(
` `
SELECT json_extract(properties, '$.inReplyTo') as inReplyTo SELECT ${db.qb.jsonExtract('properties', 'inReplyTo')} as inReplyTo
FROM objects FROM objects
WHERE mastodon_id=? WHERE mastodon_id=?
` `

Wyświetl plik

@ -9,6 +9,7 @@ import * as path from 'path'
import { BetaDatabase } from '@miniflare/d1' import { BetaDatabase } from '@miniflare/d1'
import * as SQLiteDatabase from 'better-sqlite3' import * as SQLiteDatabase from 'better-sqlite3'
import { type Database } from 'wildebeest/backend/src/database' import { type Database } from 'wildebeest/backend/src/database'
import d1 from 'wildebeest/backend/src/database/d1'
export function isUrlValid(s: string) { export function isUrlValid(s: string) {
let url let url
@ -32,7 +33,8 @@ export async function makeDB(): Promise<Database> {
db.exec(content) db.exec(content)
} }
return db2 as unknown as Database const env = { DATABASE: db2 } as any
return d1(env)
} }
export function assertCORS(response: Response) { 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 objects ON objects.id=outbox_objects.object_id
INNER JOIN actors ON actors.id=outbox_objects.actor_id INNER JOIN actors ON actors.id=outbox_objects.actor_id
WHERE objects.type='Note' 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.target = '${PUBLIC_GROUP}'
AND outbox_objects.actor_id = ?1 AND outbox_objects.actor_id = ?1
AND outbox_objects.cdate > ?2 AND outbox_objects.cdate > ?2
@ -161,7 +161,7 @@ LIMIT ?3 OFFSET ?4
return new Response(JSON.stringify(out), { headers }) 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')) { if (url.searchParams.has('max_id')) {
// Client asked to retrieve statuses after the 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 // As opposed to Mastodon we don't use incremental ID but UUID, we need