kopia lustrzana https://github.com/cloudflare/wildebeest
introduce simple SQL query builder
rodzic
42a56c560a
commit
217ff34353
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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=?
|
||||||
`
|
`
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
Ładowanie…
Reference in New Issue