mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
generate raw sql because postgres does not support prepared statements in DO blocks
This commit is contained in:
parent
1079a19cd9
commit
55518139fb
3 changed files with 129 additions and 42 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import * as httpContext from 'express-http-context2'
|
||||
import { EntityManager, EntityTarget, Repository } from 'typeorm'
|
||||
import { EntityManager, EntityTarget, QueryBuilder, Repository } from 'typeorm'
|
||||
import { appDataSource } from '../data_source'
|
||||
import { Claims } from '../resolvers/types'
|
||||
import { SetClaimsRole } from '../utils/dictionary'
|
||||
|
|
@ -45,3 +45,59 @@ export const authTrx = async <T>(
|
|||
export const getRepository = <T>(entity: EntityTarget<T>) => {
|
||||
return appDataSource.getRepository(entity)
|
||||
}
|
||||
|
||||
export const queryBuilderToRawSql = <T>(q: QueryBuilder<T>): string => {
|
||||
const queryAndParams = q.getQueryAndParameters()
|
||||
let sql = queryAndParams[0]
|
||||
const params = queryAndParams[1]
|
||||
|
||||
params.forEach((value, index) => {
|
||||
if (typeof value === 'string') {
|
||||
sql = sql.replace(`$${index + 1}`, `'${value}'`)
|
||||
} else if (typeof value === 'object') {
|
||||
if (Array.isArray(value)) {
|
||||
sql = sql.replace(
|
||||
`$${index + 1}`,
|
||||
value
|
||||
.map((element) => {
|
||||
if (typeof element === 'string') {
|
||||
return `'${element}'`
|
||||
}
|
||||
|
||||
if (typeof element === 'number' || typeof element === 'boolean') {
|
||||
return element.toString()
|
||||
}
|
||||
})
|
||||
.join(',')
|
||||
)
|
||||
} else if (value instanceof Date) {
|
||||
sql = sql.replace(`$${index + 1}`, `'${value.toISOString()}'`)
|
||||
}
|
||||
} else if (typeof value === 'number' || typeof value === 'boolean') {
|
||||
sql = sql.replace(`$${index + 1}`, value.toString())
|
||||
}
|
||||
})
|
||||
|
||||
return sql
|
||||
}
|
||||
|
||||
export const valuesToRawSql = (
|
||||
values: Record<string, string | number | boolean>
|
||||
): string => {
|
||||
let sql = ''
|
||||
|
||||
Object.keys(values).forEach((key, index) => {
|
||||
const value = values[key]
|
||||
if (typeof value === 'string') {
|
||||
sql += `${key} = '${value}'`
|
||||
} else {
|
||||
sql += `${key} = ${value.toString()}`
|
||||
}
|
||||
|
||||
if (index < Object.keys(values).length - 1) {
|
||||
sql += ', '
|
||||
}
|
||||
})
|
||||
|
||||
return sql
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,12 @@ import { Label } from '../entity/label'
|
|||
import { LibraryItem, LibraryItemState } from '../entity/library_item'
|
||||
import { BulkActionType, InputMaybe, SortParams } from '../generated/graphql'
|
||||
import { createPubSubClient, EntityType } from '../pubsub'
|
||||
import { authTrx, getColumns } from '../repository'
|
||||
import {
|
||||
authTrx,
|
||||
getColumns,
|
||||
queryBuilderToRawSql,
|
||||
valuesToRawSql,
|
||||
} from '../repository'
|
||||
import { libraryItemRepository } from '../repository/library_item'
|
||||
import { wordsCount } from '../utils/helpers'
|
||||
import { parseSearchQuery } from '../utils/search'
|
||||
|
|
@ -901,20 +906,21 @@ export const batchUpdateLibraryItems = async (
|
|||
return 'folder' in args
|
||||
}
|
||||
|
||||
const now = new Date().toISOString()
|
||||
// build the script
|
||||
let values: Record<string, any> = {}
|
||||
let values: Record<string, string | number> = {}
|
||||
let addLabels = false
|
||||
switch (action) {
|
||||
case BulkActionType.Archive:
|
||||
values = {
|
||||
archivedAt: new Date(),
|
||||
archived_at: now,
|
||||
state: LibraryItemState.Archived,
|
||||
}
|
||||
break
|
||||
case BulkActionType.Delete:
|
||||
values = {
|
||||
state: LibraryItemState.Deleted,
|
||||
deletedAt: new Date(),
|
||||
deleted_at: now,
|
||||
}
|
||||
break
|
||||
case BulkActionType.AddLabels:
|
||||
|
|
@ -922,9 +928,9 @@ export const batchUpdateLibraryItems = async (
|
|||
break
|
||||
case BulkActionType.MarkAsRead:
|
||||
values = {
|
||||
readAt: new Date(),
|
||||
readingProgressTopPercent: 100,
|
||||
readingProgressBottomPercent: 100,
|
||||
read_at: now,
|
||||
reading_progress_top_percent: 100,
|
||||
reading_progress_bottom_percent: 100,
|
||||
}
|
||||
break
|
||||
case BulkActionType.MoveToFolder:
|
||||
|
|
@ -934,7 +940,7 @@ export const batchUpdateLibraryItems = async (
|
|||
|
||||
values = {
|
||||
folder: args.folder,
|
||||
savedAt: new Date(),
|
||||
saved_at: now,
|
||||
}
|
||||
|
||||
break
|
||||
|
|
@ -984,19 +990,20 @@ export const batchUpdateLibraryItems = async (
|
|||
return tx.getRepository(EntityLabel).save(labelsToAdd)
|
||||
}
|
||||
|
||||
const countSql = queryBuilder.select('COUNT(1) INTO total_rows').getSql()
|
||||
const [subQuery, params] = queryBuilder.select('id').getQueryAndParameters()
|
||||
const valuesSql = Object.keys(values)
|
||||
// eslint-disable-next-line @typescript-eslint/restrict-template-expressions
|
||||
.map((key) => `${key} = ${values[key]}`)
|
||||
.join(', ')
|
||||
// generate raw sql because postgres doesn't support prepared statements in DO blocks
|
||||
const countSql = queryBuilderToRawSql(
|
||||
queryBuilder.select('COUNT(1) INTO total_rows')
|
||||
)
|
||||
const subQuery = queryBuilderToRawSql(queryBuilder.select('id'))
|
||||
const valuesSql = valuesToRawSql(values)
|
||||
|
||||
const batchSize = 100
|
||||
const sql = `
|
||||
-- Set batch size
|
||||
DO $$
|
||||
DECLARE
|
||||
batch_size INT := 100;
|
||||
total_rows INT := 1000;
|
||||
batch_size INT := ${batchSize};
|
||||
total_rows INT;
|
||||
num_batches INT;
|
||||
current_offset INT;
|
||||
BEGIN
|
||||
|
|
@ -1025,7 +1032,7 @@ export const batchUpdateLibraryItems = async (
|
|||
END $$
|
||||
`
|
||||
|
||||
return tx.query(sql, params)
|
||||
return tx.query(sql)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2107,31 +2107,31 @@ describe('Article API', () => {
|
|||
}
|
||||
`
|
||||
|
||||
before(async () => {
|
||||
// Create some test items
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await createLibraryItem(
|
||||
{
|
||||
user,
|
||||
itemType: i == 0 ? PageType.Article : PageType.File,
|
||||
title: 'test item',
|
||||
readableContent: '<p>test</p>',
|
||||
slug: '',
|
||||
state:
|
||||
i == 0 ? LibraryItemState.Failed : LibraryItemState.Succeeded,
|
||||
originalUrl: `https://blog.omnivore.app/p/bulk-action-${i}`,
|
||||
},
|
||||
user.id
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// Delete all items
|
||||
await deleteLibraryItemsByUserId(user.id)
|
||||
})
|
||||
|
||||
context('when action is MarkAsRead and query is in:unread', () => {
|
||||
before(async () => {
|
||||
// Create some test items
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await createLibraryItem(
|
||||
{
|
||||
user,
|
||||
itemType: i == 0 ? PageType.Article : PageType.File,
|
||||
title: 'test item',
|
||||
readableContent: '<p>test</p>',
|
||||
slug: '',
|
||||
state:
|
||||
i == 0 ? LibraryItemState.Failed : LibraryItemState.Succeeded,
|
||||
originalUrl: `https://blog.omnivore.app/p/bulk-action-${i}`,
|
||||
},
|
||||
user.id
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// Delete all items
|
||||
await deleteLibraryItemsByUserId(user.id)
|
||||
})
|
||||
|
||||
it('marks unread items as read', async () => {
|
||||
const res = await graphqlRequest(
|
||||
bulkActionQuery(BulkActionType.MarkAsRead, 'is:unread'),
|
||||
|
|
@ -2199,6 +2199,30 @@ describe('Article API', () => {
|
|||
)
|
||||
|
||||
context('when action is Delete', () => {
|
||||
before(async () => {
|
||||
// Create some test items
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await createLibraryItem(
|
||||
{
|
||||
user,
|
||||
itemType: i == 0 ? PageType.Article : PageType.File,
|
||||
title: 'test item',
|
||||
readableContent: '<p>test</p>',
|
||||
slug: '',
|
||||
state:
|
||||
i == 0 ? LibraryItemState.Failed : LibraryItemState.Succeeded,
|
||||
originalUrl: `https://blog.omnivore.app/p/bulk-action-${i}`,
|
||||
},
|
||||
user.id
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
after(async () => {
|
||||
// Delete all items
|
||||
await deleteLibraryItemsByUserId(user.id)
|
||||
})
|
||||
|
||||
it('deletes all items', async () => {
|
||||
const res = await graphqlRequest(
|
||||
bulkActionQuery(BulkActionType.Delete),
|
||||
|
|
|
|||
Loading…
Reference in a new issue