mirror of
https://github.com/flarum/framework.git
synced 2025-02-21 04:31:57 +08:00
feat: add support for PgSQL (#3985)
* feat: add support for `PgSQL` * chore: generate dump * feat: query exception errors db driver hint * feat: allow defining supported databases * chore: review comments * feat: setting for pgsql preferred search config
This commit is contained in:
parent
d04cda6ca3
commit
379298acb0
55
.github/workflows/REUSABLE_backend.yml
vendored
55
.github/workflows/REUSABLE_backend.yml
vendored
@ -44,7 +44,7 @@ on:
|
||||
description: Versions of databases to test with. Should be array of strings encoded as JSON array
|
||||
type: string
|
||||
required: false
|
||||
default: '["mysql:5.7", "mysql:8.0.30", "mysql:8.1.0", "mariadb", "sqlite:3"]'
|
||||
default: '["mysql:5.7", "mysql:8.0.30", "mysql:8.1.0", "mariadb", "sqlite:3", "postgres:10"]'
|
||||
|
||||
php_ini_values:
|
||||
description: PHP ini values
|
||||
@ -68,6 +68,9 @@ env:
|
||||
# `inputs.composer_directory` defaults to `inputs.backend_directory`
|
||||
FLARUM_TEST_TMP_DIR_LOCAL: tests/integration/tmp
|
||||
COMPOSER_AUTH: ${{ secrets.composer_auth }}
|
||||
DB_DATABASE: flarum_test
|
||||
DB_USERNAME: root
|
||||
DB_PASSWORD: root
|
||||
|
||||
jobs:
|
||||
test:
|
||||
@ -98,6 +101,9 @@ jobs:
|
||||
- service: 'sqlite:3'
|
||||
db: SQLite
|
||||
driver: sqlite
|
||||
- service: 'postgres:10'
|
||||
db: PostgreSQL 10
|
||||
driver: pgsql
|
||||
|
||||
# Include Database prefix tests with only one PHP version.
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
@ -106,30 +112,24 @@ jobs:
|
||||
driver: mysql
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'mysql:8.0.30'
|
||||
db: MySQL 8.0
|
||||
driver: mysql
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: mariadb
|
||||
db: MariaDB
|
||||
driver: mysql
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'mysql:8.1.0'
|
||||
db: MySQL 8.1
|
||||
driver: mysql
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'sqlite:3'
|
||||
db: SQLite
|
||||
driver: sqlite
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'postgres:10'
|
||||
db: PostgreSQL 10
|
||||
driver: pgsql
|
||||
prefix: flarum_
|
||||
prefixStr: (prefix)
|
||||
|
||||
# To reduce number of actions, we exclude some PHP versions from running with some DB versions.
|
||||
exclude:
|
||||
@ -147,12 +147,34 @@ jobs:
|
||||
service: 'sqlite:3'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[1] }}
|
||||
service: 'sqlite:3'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[0] }}
|
||||
service: 'postgres:10'
|
||||
- php: ${{ fromJSON(inputs.php_versions)[1] }}
|
||||
service: 'postgres:10'
|
||||
|
||||
services:
|
||||
mysql:
|
||||
image: ${{ matrix.service != 'sqlite:3' && matrix.service || '' }}
|
||||
image: ${{ matrix.driver == 'mysql' && matrix.service || '' }}
|
||||
env:
|
||||
MYSQL_DATABASE: ${{ env.DB_DATABASE }}
|
||||
MYSQL_USER: ${{ env.DB_USERNAME }}
|
||||
MYSQL_PASSWORD: ${{ env.DB_PASSWORD }}
|
||||
MYSQL_ROOT_PASSWORD: ${{ env.DB_PASSWORD }}
|
||||
ports:
|
||||
- 13306:3306
|
||||
postgres:
|
||||
image: ${{ matrix.driver == 'pgsql' && matrix.service || '' }}
|
||||
env:
|
||||
POSTGRES_DB: ${{ env.DB_DATABASE }}
|
||||
POSTGRES_USER: ${{ env.DB_USERNAME }}
|
||||
POSTGRES_PASSWORD: ${{ env.DB_PASSWORD }}
|
||||
ports:
|
||||
- 15432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
name: 'PHP ${{ matrix.php }} / ${{ matrix.db }} ${{ matrix.prefixStr }}'
|
||||
|
||||
@ -173,7 +195,7 @@ jobs:
|
||||
ini-values: ${{ matrix.php_ini_values }}
|
||||
|
||||
- name: Create MySQL Database
|
||||
if: ${{ matrix.service != 'sqlite:3' }}
|
||||
if: ${{ matrix.driver == 'mysql' }}
|
||||
run: |
|
||||
sudo systemctl start mysql
|
||||
mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
|
||||
@ -200,8 +222,7 @@ jobs:
|
||||
fi
|
||||
working-directory: ${{ inputs.backend_directory }}
|
||||
env:
|
||||
DB_PORT: 13306
|
||||
DB_PASSWORD: root
|
||||
DB_PORT: ${{ matrix.driver == 'mysql' && 13306 || 15432 }}
|
||||
DB_PREFIX: ${{ matrix.prefix }}
|
||||
DB_DRIVER: ${{ matrix.driver }}
|
||||
COMPOSER_PROCESS_TIMEOUT: 600
|
||||
|
@ -11,8 +11,12 @@ namespace Flarum\Approval\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Approval\Tests\integration\InteractsWithUnapprovedContent;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
|
||||
class ApprovePostsTest extends TestCase
|
||||
{
|
||||
@ -26,23 +30,23 @@ class ApprovePostsTest extends TestCase
|
||||
$this->extension('flarum-approval');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'luceos', 'email' => 'luceos@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 1, 'comment_count' => 1, 'is_approved' => 1],
|
||||
],
|
||||
'posts' => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 1, 'number' => 2],
|
||||
['id' => 3, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 0, 'number' => 3],
|
||||
Post::class => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => null, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => null, 'is_approved' => 1, 'number' => 2],
|
||||
['id' => 3, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => null, 'is_approved' => 0, 'number' => 3],
|
||||
['id' => 4, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => Carbon::now(), 'is_approved' => 1, 'number' => 4],
|
||||
['id' => 5, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => 0, 'is_approved' => 0, 'number' => 5],
|
||||
['id' => 5, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'hidden_at' => null, 'is_approved' => 0, 'number' => 5],
|
||||
],
|
||||
'groups' => [
|
||||
Group::class => [
|
||||
['id' => 4, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
['id' => 5, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
],
|
||||
|
@ -11,9 +11,12 @@ namespace Flarum\Approval\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Approval\Tests\integration\InteractsWithUnapprovedContent;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
|
||||
class CreatePostsTest extends TestCase
|
||||
{
|
||||
@ -27,18 +30,18 @@ class CreatePostsTest extends TestCase
|
||||
$this->extension('flarum-flags', 'flarum-approval');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'luceos', 'email' => 'luceos@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 1, 'comment_count' => 1, 'is_approved' => 1],
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 2, 'comment_count' => 1, 'is_approved' => 0],
|
||||
['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 4, 'first_post_id' => 3, 'comment_count' => 1, 'is_approved' => 0],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 2],
|
||||
['id' => 3, 'discussion_id' => 1, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 3],
|
||||
@ -49,7 +52,7 @@ class CreatePostsTest extends TestCase
|
||||
['id' => 8, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 1, 'number' => 2],
|
||||
['id' => 9, 'discussion_id' => 3, 'user_id' => 4, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'is_private' => 0, 'is_approved' => 0, 'number' => 3],
|
||||
],
|
||||
'groups' => [
|
||||
Group::class => [
|
||||
['id' => 4, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
['id' => 5, 'name_singular' => 'Acme', 'name_plural' => 'Acme', 'is_hidden' => 0],
|
||||
],
|
||||
@ -60,6 +63,7 @@ class CreatePostsTest extends TestCase
|
||||
'group_permission' => [
|
||||
['group_id' => 4, 'permission' => 'discussion.startWithoutApproval'],
|
||||
['group_id' => 5, 'permission' => 'discussion.replyWithoutApproval'],
|
||||
['group_id' => Group::MEMBER_ID, 'permission' => 'postWithoutThrottle'],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
@ -53,7 +53,10 @@ class FlagResource extends AbstractDatabaseResource
|
||||
public function query(Context $context): object
|
||||
{
|
||||
if ($context->listing(self::class)) {
|
||||
$query = Flag::query()->groupBy('post_id');
|
||||
$query = Flag::query()->whenPgSql(
|
||||
fn (Builder $query) => $query->distinct('post_id')->orderBy('post_id'),
|
||||
else: fn (Builder $query) => $query->groupBy('post_id')
|
||||
);
|
||||
|
||||
$this->scope($query, $context);
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
namespace Flarum\Flags\Tests\integration\api\flags;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Group\Group;
|
||||
@ -16,6 +17,7 @@ use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\PostgresConnection;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class ListTest extends TestCase
|
||||
@ -58,12 +60,12 @@ class ListTest extends TestCase
|
||||
['id' => 4, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>', 'is_private' => true],
|
||||
],
|
||||
Flag::class => [
|
||||
['id' => 1, 'post_id' => 1, 'user_id' => 1],
|
||||
['id' => 2, 'post_id' => 1, 'user_id' => 2],
|
||||
['id' => 3, 'post_id' => 1, 'user_id' => 3],
|
||||
['id' => 4, 'post_id' => 2, 'user_id' => 2],
|
||||
['id' => 5, 'post_id' => 3, 'user_id' => 1],
|
||||
['id' => 6, 'post_id' => 4, 'user_id' => 1],
|
||||
['id' => 1, 'post_id' => 1, 'user_id' => 1, 'created_at' => Carbon::now()->addMinutes(2)],
|
||||
['id' => 2, 'post_id' => 1, 'user_id' => 2, 'created_at' => Carbon::now()->addMinutes(3)],
|
||||
['id' => 3, 'post_id' => 1, 'user_id' => 3, 'created_at' => Carbon::now()->addMinutes(4)],
|
||||
['id' => 4, 'post_id' => 2, 'user_id' => 2, 'created_at' => Carbon::now()->addMinutes(5)],
|
||||
['id' => 5, 'post_id' => 3, 'user_id' => 1, 'created_at' => Carbon::now()->addMinutes(6)],
|
||||
['id' => 6, 'post_id' => 4, 'user_id' => 1, 'created_at' => Carbon::now()->addMinutes(7)],
|
||||
]
|
||||
]);
|
||||
}
|
||||
@ -79,12 +81,19 @@ class ListTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body = $response->getBody()->getContents());
|
||||
$body = $response->getBody()->getContents();
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$data = json_decode($body, true)['data'];
|
||||
|
||||
$ids = Arr::pluck($data, 'id');
|
||||
$this->assertEqualsCanonicalizing(['1', '4', '5'], $ids);
|
||||
|
||||
if ($this->database() instanceof PostgresConnection) {
|
||||
$this->assertEqualsCanonicalizing(['3', '4', '5'], $ids);
|
||||
} else {
|
||||
$this->assertEqualsCanonicalizing(['1', '4', '5'], $ids);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -122,7 +131,7 @@ class ListTest extends TestCase
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
|
||||
$ids = Arr::pluck($data, 'id');
|
||||
$this->assertEqualsCanonicalizing(['1', '4', '5'], $ids);
|
||||
$this->assertCount(3, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -18,6 +18,7 @@ use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
class ListWithTagsTest extends TestCase
|
||||
{
|
||||
@ -86,16 +87,16 @@ class ListWithTagsTest extends TestCase
|
||||
],
|
||||
Flag::class => [
|
||||
// From regular ListTest
|
||||
['id' => 1, 'post_id' => 1, 'user_id' => 1],
|
||||
['id' => 2, 'post_id' => 1, 'user_id' => 2],
|
||||
['id' => 3, 'post_id' => 1, 'user_id' => 3],
|
||||
['id' => 4, 'post_id' => 2, 'user_id' => 2],
|
||||
['id' => 5, 'post_id' => 3, 'user_id' => 1],
|
||||
['id' => 1, 'post_id' => 1, 'user_id' => 1, 'created_at' => Carbon::now()->addMinutes(2)],
|
||||
['id' => 2, 'post_id' => 1, 'user_id' => 2, 'created_at' => Carbon::now()->addMinutes(3)],
|
||||
['id' => 3, 'post_id' => 1, 'user_id' => 3, 'created_at' => Carbon::now()->addMinutes(4)],
|
||||
['id' => 4, 'post_id' => 2, 'user_id' => 2, 'created_at' => Carbon::now()->addMinutes(5)],
|
||||
['id' => 5, 'post_id' => 3, 'user_id' => 1, 'created_at' => Carbon::now()->addMinutes(6)],
|
||||
// In tags
|
||||
['id' => 6, 'post_id' => 4, 'user_id' => 1],
|
||||
['id' => 7, 'post_id' => 5, 'user_id' => 1],
|
||||
['id' => 8, 'post_id' => 6, 'user_id' => 1],
|
||||
['id' => 9, 'post_id' => 7, 'user_id' => 1],
|
||||
['id' => 6, 'post_id' => 4, 'user_id' => 1, 'created_at' => Carbon::now()->addMinutes(7)],
|
||||
['id' => 7, 'post_id' => 5, 'user_id' => 1, 'created_at' => Carbon::now()->addMinutes(8)],
|
||||
['id' => 8, 'post_id' => 6, 'user_id' => 1, 'created_at' => Carbon::now()->addMinutes(9)],
|
||||
['id' => 9, 'post_id' => 7, 'user_id' => 1, 'created_at' => Carbon::now()->addMinutes(10)],
|
||||
]
|
||||
]);
|
||||
}
|
||||
@ -111,12 +112,14 @@ class ListWithTagsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$body = $response->getBody()->getContents();
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$data = json_decode($body, true)['data'];
|
||||
|
||||
$ids = Arr::pluck($data, 'id');
|
||||
$this->assertEqualsCanonicalizing(['1', '4', '5', '6', '7', '8', '9'], $ids);
|
||||
$this->assertCount(7, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -154,7 +157,9 @@ class ListWithTagsTest extends TestCase
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
|
||||
$ids = Arr::pluck($data, 'id');
|
||||
$this->assertEqualsCanonicalizing(['1', '4', '5', '8', '9'], $ids);
|
||||
// 7 is included, even though mods can't view discussions.
|
||||
// This is because the UI doesnt allow discussions.viewFlags without viewDiscussions.
|
||||
$this->assertCount(5, $data);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -9,9 +9,14 @@
|
||||
|
||||
namespace Flarum\Flags\Tests\integration\api\posts;
|
||||
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Flags\Flag;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class IncludeFlagsVisibilityTest extends TestCase
|
||||
@ -28,7 +33,7 @@ class IncludeFlagsVisibilityTest extends TestCase
|
||||
$this->extension('flarum-tags', 'flarum-flags');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
$this->normalUser(),
|
||||
[
|
||||
'id' => 3,
|
||||
@ -56,7 +61,7 @@ class IncludeFlagsVisibilityTest extends TestCase
|
||||
['group_id' => 5, 'user_id' => 2],
|
||||
['group_id' => 6, 'user_id' => 3],
|
||||
],
|
||||
'groups' => [
|
||||
Group::class => [
|
||||
['id' => 5, 'name_singular' => 'group5', 'name_plural' => 'group5', 'color' => null, 'icon' => 'fas fa-crown', 'is_hidden' => false],
|
||||
['id' => 6, 'name_singular' => 'group1', 'name_plural' => 'group1', 'color' => null, 'icon' => 'fas fa-cog', 'is_hidden' => false],
|
||||
],
|
||||
@ -67,11 +72,11 @@ class IncludeFlagsVisibilityTest extends TestCase
|
||||
['group_id' => 6, 'permission' => 'tag1.discussion.viewFlags'],
|
||||
['group_id' => 6, 'permission' => 'tag1.viewForum'],
|
||||
],
|
||||
'tags' => [
|
||||
Tag::class => [
|
||||
['id' => 1, 'name' => 'Tag 1', 'slug' => 'tag-1', 'is_primary' => false, 'position' => null, 'parent_id' => null, 'is_restricted' => true],
|
||||
['id' => 2, 'name' => 'Tag 2', 'slug' => 'tag-2', 'is_primary' => true, 'position' => 2, 'parent_id' => null, 'is_restricted' => false],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => 'Test1', 'user_id' => 1, 'comment_count' => 1],
|
||||
['id' => 2, 'title' => 'Test2', 'user_id' => 1, 'comment_count' => 1],
|
||||
],
|
||||
@ -79,7 +84,7 @@ class IncludeFlagsVisibilityTest extends TestCase
|
||||
['discussion_id' => 1, 'tag_id' => 1],
|
||||
['discussion_id' => 2, 'tag_id' => 2],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
@ -87,7 +92,7 @@ class IncludeFlagsVisibilityTest extends TestCase
|
||||
['id' => 4, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
['id' => 5, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p></p></t>'],
|
||||
],
|
||||
'flags' => [
|
||||
Flag::class => [
|
||||
['id' => 1, 'post_id' => 1, 'user_id' => 1],
|
||||
['id' => 2, 'post_id' => 1, 'user_id' => 5],
|
||||
['id' => 3, 'post_id' => 1, 'user_id' => 3],
|
||||
|
@ -58,6 +58,7 @@ class PostResourceFields
|
||||
// So that we can tell if the current user has liked the post.
|
||||
$query
|
||||
->orderBy(new Expression($grammar->wrap('user_id').' = '.$actor->id), 'desc')
|
||||
->orderBy('created_at')
|
||||
->limit(static::$maxLikes);
|
||||
}),
|
||||
];
|
||||
|
@ -54,17 +54,17 @@ class ListPostsTest extends TestCase
|
||||
['id' => 112, 'username' => 'user112', 'email' => '112@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'post_likes' => [
|
||||
['user_id' => 102, 'post_id' => 101],
|
||||
['user_id' => 104, 'post_id' => 101],
|
||||
['user_id' => 105, 'post_id' => 101],
|
||||
['user_id' => 106, 'post_id' => 101],
|
||||
['user_id' => 107, 'post_id' => 101],
|
||||
['user_id' => 108, 'post_id' => 101],
|
||||
['user_id' => 109, 'post_id' => 101],
|
||||
['user_id' => 110, 'post_id' => 101],
|
||||
['user_id' => 2, 'post_id' => 101],
|
||||
['user_id' => 111, 'post_id' => 101],
|
||||
['user_id' => 112, 'post_id' => 101],
|
||||
['user_id' => 102, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(2)],
|
||||
['user_id' => 104, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(3)],
|
||||
['user_id' => 105, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(4)],
|
||||
['user_id' => 106, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(5)],
|
||||
['user_id' => 107, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(6)],
|
||||
['user_id' => 108, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(7)],
|
||||
['user_id' => 109, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(8)],
|
||||
['user_id' => 110, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(9)],
|
||||
['user_id' => 2, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(10)],
|
||||
['user_id' => 111, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(11)],
|
||||
['user_id' => 112, 'post_id' => 101, 'created_at' => Carbon::now()->addMinutes(12)],
|
||||
],
|
||||
'group_permission' => [
|
||||
['group_id' => Group::GUEST_ID, 'permission' => 'searchUsers'],
|
||||
|
@ -12,7 +12,7 @@ namespace Flarum\Lock\Filter;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
|
@ -249,9 +249,11 @@ class GroupMentionsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode());
|
||||
$body = $response->getBody()->getContents();
|
||||
|
||||
$response = json_decode($response->getBody(), true);
|
||||
$this->assertEquals(201, $response->getStatusCode(), $body);
|
||||
|
||||
$response = json_decode($body, true);
|
||||
|
||||
$this->assertStringNotContainsString('@Members', $response['data']['attributes']['contentHtml']);
|
||||
$this->assertStringNotContainsString('@Guests', $response['data']['attributes']['contentHtml']);
|
||||
|
@ -116,35 +116,35 @@ class ListPostsTest extends TestCase
|
||||
{
|
||||
$this->prepareDatabase([
|
||||
Discussion::class => [
|
||||
['id' => 100, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 101, 'comment_count' => 12],
|
||||
['id' => 100, 'title' => __CLASS__, 'created_at' => Carbon::parse('2024-05-04'), 'user_id' => 1, 'first_post_id' => 101, 'comment_count' => 12],
|
||||
],
|
||||
Post::class => [
|
||||
['id' => 101, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 102, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 103, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>', 'is_private' => 1],
|
||||
['id' => 104, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 105, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 106, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 107, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 108, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 109, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 110, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 111, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 112, 'discussion_id' => 100, 'created_at' => Carbon::now(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 101, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04'), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 102, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(2), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 103, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(3), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>', 'is_private' => 1],
|
||||
['id' => 104, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(4), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 105, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(5), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 106, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(6), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 107, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(7), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 108, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(8), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 109, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(9), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 110, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(10), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 111, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(11), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
['id' => 112, 'discussion_id' => 100, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(12), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>text</p></t>'],
|
||||
],
|
||||
'post_mentions_post' => [
|
||||
['post_id' => 102, 'mentions_post_id' => 101],
|
||||
['post_id' => 103, 'mentions_post_id' => 101],
|
||||
['post_id' => 104, 'mentions_post_id' => 101],
|
||||
['post_id' => 105, 'mentions_post_id' => 101],
|
||||
['post_id' => 106, 'mentions_post_id' => 101],
|
||||
['post_id' => 107, 'mentions_post_id' => 101],
|
||||
['post_id' => 108, 'mentions_post_id' => 101],
|
||||
['post_id' => 109, 'mentions_post_id' => 101],
|
||||
['post_id' => 110, 'mentions_post_id' => 101],
|
||||
['post_id' => 111, 'mentions_post_id' => 101],
|
||||
['post_id' => 112, 'mentions_post_id' => 101],
|
||||
['post_id' => 103, 'mentions_post_id' => 112],
|
||||
['post_id' => 102, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(2)],
|
||||
['post_id' => 103, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(3)],
|
||||
['post_id' => 104, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(4)],
|
||||
['post_id' => 105, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(5)],
|
||||
['post_id' => 106, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(6)],
|
||||
['post_id' => 107, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(7)],
|
||||
['post_id' => 108, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(8)],
|
||||
['post_id' => 109, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(9)],
|
||||
['post_id' => 110, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(10)],
|
||||
['post_id' => 111, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(11)],
|
||||
['post_id' => 112, 'mentions_post_id' => 101, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(12)],
|
||||
['post_id' => 103, 'mentions_post_id' => 112, 'created_at' => Carbon::parse('2024-05-04')->addMinutes(13)],
|
||||
],
|
||||
]);
|
||||
}
|
||||
@ -187,10 +187,11 @@ class ListPostsTest extends TestCase
|
||||
])->withQueryParams([
|
||||
'filter' => ['discussion' => 100],
|
||||
'include' => 'mentionedBy',
|
||||
'sort' => 'createdAt',
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($body = $response->getBody()->getContents(), true)['data'] ?? [];
|
||||
$data = json_decode($body = $response->getBody()->getContents(), true)['data'];
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
|
@ -51,7 +51,7 @@ class PostMentionsTest extends TestCase
|
||||
['id' => 8, 'number' => 6, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="i_am_a_deleted_user" id="2020" number="8" discussionid="2" username="i_am_a_deleted_user">@"i_am_a_deleted_user"#p2020</POSTMENTION></r>'],
|
||||
['id' => 9, 'number' => 10, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 5, 'type' => 'comment', 'content' => '<r><p>I am bad</p></r>'],
|
||||
['id' => 10, 'number' => 11, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="Bad "#p6 User" id="9" number="10" discussionid="2">@"Bad "#p6 User"#p9</POSTMENTION></r>'],
|
||||
['id' => 11, 'number' => 12, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 40, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="Bad "#p6 User" id="9" number="10" discussionid="2">@"Bad "#p6 User"#p9</POSTMENTION></r>'],
|
||||
['id' => 11, 'number' => 12, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => null, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="Bad "#p6 User" id="9" number="10" discussionid="2">@"Bad "#p6 User"#p9</POSTMENTION></r>'],
|
||||
['id' => 12, 'number' => 13, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="deleted_user" id="11" number="12" discussionid="2">@"acme"#p11</POSTMENTION></r>'],
|
||||
|
||||
// Restricted access
|
||||
@ -95,9 +95,11 @@ class PostMentionsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode());
|
||||
$body = $response->getBody()->getContents();
|
||||
|
||||
$response = json_decode($response->getBody(), true);
|
||||
$this->assertEquals(201, $response->getStatusCode(), $body);
|
||||
|
||||
$response = json_decode($body, true);
|
||||
|
||||
$this->assertStringNotContainsString('POTATO$', $response['data']['attributes']['contentHtml']);
|
||||
$this->assertEquals('@potato#4', $response['data']['attributes']['content']);
|
||||
@ -191,9 +193,11 @@ class PostMentionsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(201, $response->getStatusCode());
|
||||
$body = $response->getBody()->getContents();
|
||||
|
||||
$response = json_decode($response->getBody(), true);
|
||||
$this->assertEquals(201, $response->getStatusCode(), $body);
|
||||
|
||||
$response = json_decode($body, true);
|
||||
|
||||
$this->assertStringContainsString('POTATO$', $response['data']['attributes']['contentHtml']);
|
||||
$this->assertEquals('@"POTATO$"#p4', $response['data']['attributes']['content']);
|
||||
@ -514,9 +518,11 @@ class PostMentionsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$body = $response->getBody()->getContents();
|
||||
|
||||
$response = json_decode($response->getBody(), true);
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$response = json_decode($body, true);
|
||||
|
||||
$this->assertStringContainsString('Bad "#p6 User', $response['data']['attributes']['contentHtml']);
|
||||
$this->assertEquals('@"Bad _ User"#p9', $response['data']['attributes']['content']);
|
||||
|
@ -38,6 +38,7 @@ class UserMentionsTest extends TestCase
|
||||
['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 5, 'username' => 'bad_user', 'email' => 'bad_user@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 50]
|
||||
],
|
||||
Discussion::class => [
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
|
||||
@ -500,9 +501,11 @@ class UserMentionsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$body = $response->getBody()->getContents();
|
||||
|
||||
$response = json_decode($response->getBody(), true);
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$response = json_decode($body, true);
|
||||
|
||||
$this->assertStringContainsString('Bad "#p6 User', $response['data']['attributes']['contentHtml']);
|
||||
$this->assertEquals('@"Bad _ User"#5', $response['data']['attributes']['content']);
|
||||
|
@ -11,6 +11,7 @@ namespace Flarum\Statistics\Api\Controller;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use DateTime;
|
||||
use Exception;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Http\Exception\InvalidParameterException;
|
||||
use Flarum\Http\RequestUtil;
|
||||
@ -130,11 +131,19 @@ class ShowStatisticsData implements RequestHandlerInterface
|
||||
$endDate = new DateTime();
|
||||
}
|
||||
|
||||
$formats = match ($query->getConnection()->getDriverName()) {
|
||||
'pgsql' => ['YYYY-MM-DD HH24:00:00', 'YYYY-MM-DD'],
|
||||
default => ['%Y-%m-%d %H:00:00', '%Y-%m-%d'],
|
||||
};
|
||||
|
||||
// if within the last 24 hours, group by hour
|
||||
$format = 'CASE WHEN '.$column.' > ? THEN \'%Y-%m-%d %H:00:00\' ELSE \'%Y-%m-%d\' END';
|
||||
$format = "CASE WHEN $column > ? THEN '$formats[0]' ELSE '$formats[1]' END";
|
||||
|
||||
$dbFormattedDatetime = match ($query->getConnection()->getDriverName()) {
|
||||
'sqlite' => 'strftime('.$format.', '.$column.')',
|
||||
default => 'DATE_FORMAT('.$column.', '.$format.')',
|
||||
'sqlite' => "strftime($format, $column)",
|
||||
'pgsql' => "TO_CHAR($column, $format)",
|
||||
'mysql' => "DATE_FORMAT($column, $format)",
|
||||
default => throw new Exception('Unsupported database driver'),
|
||||
};
|
||||
|
||||
$results = $query
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
namespace Flarum\Sticky;
|
||||
|
||||
use DateTime;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\SearchCriteria;
|
||||
use Flarum\Tags\Search\Filter\TagFilter;
|
||||
@ -19,7 +20,7 @@ class PinStickiedDiscussionsToTop
|
||||
public function __invoke(DatabaseSearchState $state, SearchCriteria $criteria): void
|
||||
{
|
||||
if ($criteria->sortIsDefault && ! $state->isFulltextSearch()) {
|
||||
$query = $state->getQuery();
|
||||
$query = $state->getQuery()->getQuery();
|
||||
|
||||
// If we are viewing a specific tag, then pin all stickied
|
||||
// discussions to the top no matter what.
|
||||
@ -46,6 +47,8 @@ class PinStickiedDiscussionsToTop
|
||||
$sticky->where('is_sticky', true);
|
||||
unset($sticky->orders);
|
||||
|
||||
$epochTime = (new DateTime('@0'))->format('Y-m-d H:i:s');
|
||||
|
||||
/** @var Builder $q */
|
||||
foreach ([$sticky, $query] as $q) {
|
||||
$read = $q->newQuery()
|
||||
@ -58,7 +61,7 @@ class PinStickiedDiscussionsToTop
|
||||
// Add the bindings manually (rather than as the second
|
||||
// argument in orderByRaw) for now due to a bug in Laravel which
|
||||
// would add the bindings in the wrong order.
|
||||
$q->selectRaw('(is_sticky and not exists ('.$read->toSql().') and last_posted_at > ?) as is_unread_sticky', array_merge($read->getBindings(), [$state->getActor()->marked_all_as_read_at ?: 0]));
|
||||
$q->selectRaw('(is_sticky and not exists ('.$read->toSql().') and last_posted_at > ?) as is_unread_sticky', array_merge($read->getBindings(), [$state->getActor()->marked_all_as_read_at ?: $epochTime]));
|
||||
}
|
||||
|
||||
$query->union($sticky);
|
||||
|
@ -12,7 +12,7 @@ namespace Flarum\Sticky\Query;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
|
@ -62,11 +62,13 @@ class ListDiscussionsTest extends TestCase
|
||||
$this->request('GET', '/api/discussions')
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body = $response->getBody()->getContents());
|
||||
$body = $response->getBody()->getContents();
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$data = json_decode($body, true);
|
||||
|
||||
$this->assertEqualsCanonicalizing([3, 1, 2, 4], Arr::pluck($data['data'], 'id'));
|
||||
$this->assertEquals([3, 1, 2, 4], Arr::pluck($data['data'], 'id'));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
@ -114,10 +116,12 @@ class ListDiscussionsTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body = $response->getBody()->getContents());
|
||||
$body = $response->getBody()->getContents();
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$data = json_decode($body, true);
|
||||
|
||||
$this->assertEqualsCanonicalizing([3, 1, 2, 4], Arr::pluck($data['data'], 'id'));
|
||||
$this->assertEquals([3, 1, 2, 4], Arr::pluck($data['data'], 'id'));
|
||||
}
|
||||
}
|
||||
|
@ -10,8 +10,12 @@
|
||||
namespace Flarum\Sticky\Tests\integration\api;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Group\Group;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
|
||||
class StickyDiscussionsTest extends TestCase
|
||||
{
|
||||
@ -24,18 +28,24 @@ class StickyDiscussionsTest extends TestCase
|
||||
$this->extension('flarum-sticky');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
['id' => 1, 'username' => 'Muralf', 'email' => 'muralf@machine.local', 'is_email_confirmed' => 1],
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'Muralf_', 'email' => 'muralf_@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'is_sticky' => true, 'last_post_number' => 1],
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now()->addMinutes(2), 'last_posted_at' => Carbon::now()->addMinutes(5), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'is_sticky' => false, 'last_post_number' => 1],
|
||||
['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now()->addMinutes(3), 'last_posted_at' => Carbon::now()->addMinute(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'is_sticky' => true, 'last_post_number' => 1],
|
||||
['id' => 4, 'title' => __CLASS__, 'created_at' => Carbon::now()->addMinutes(4), 'last_posted_at' => Carbon::now()->addMinutes(2), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'is_sticky' => false, 'last_post_number' => 1],
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now()->addMinutes(2), 'last_posted_at' => Carbon::now()->addMinutes(5), 'user_id' => 1, 'first_post_id' => 2, 'comment_count' => 1, 'is_sticky' => false, 'last_post_number' => 1],
|
||||
['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now()->addMinutes(3), 'last_posted_at' => Carbon::now()->addMinute(), 'user_id' => 1, 'first_post_id' => 3, 'comment_count' => 1, 'is_sticky' => true, 'last_post_number' => 1],
|
||||
['id' => 4, 'title' => __CLASS__, 'created_at' => Carbon::now()->addMinutes(4), 'last_posted_at' => Carbon::now()->addMinutes(2), 'user_id' => 1, 'first_post_id' => 4, 'comment_count' => 1, 'is_sticky' => false, 'last_post_number' => 1],
|
||||
],
|
||||
'groups' => [
|
||||
Post::class => [
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'number' => 1],
|
||||
['id' => 3, 'discussion_id' => 3, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'number' => 1],
|
||||
['id' => 4, 'discussion_id' => 4, 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>Text</p></t>', 'number' => 1],
|
||||
],
|
||||
Group::class => [
|
||||
['id' => 5, 'name_singular' => 'Group', 'name_plural' => 'Groups', 'color' => 'blue'],
|
||||
],
|
||||
'group_user' => [
|
||||
|
@ -14,7 +14,7 @@ use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\Search\ValidateFilterTrait;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
|
@ -41,15 +41,15 @@ class ReplyNotificationTest extends TestCase
|
||||
['id' => 33, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 33, 'comment_count' => 6, 'last_post_number' => 6, 'last_post_id' => 38],
|
||||
],
|
||||
Post::class => [
|
||||
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 1],
|
||||
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(1)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(2)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 1],
|
||||
|
||||
['id' => 33, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 1],
|
||||
['id' => 34, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 2],
|
||||
['id' => 35, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 3],
|
||||
['id' => 36, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 4],
|
||||
['id' => 37, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 5],
|
||||
['id' => 38, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 6],
|
||||
['id' => 33, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(3)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 1],
|
||||
['id' => 34, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(4)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 2],
|
||||
['id' => 35, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(5)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 3],
|
||||
['id' => 36, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(6)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 4],
|
||||
['id' => 37, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(7)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 5],
|
||||
['id' => 38, 'discussion_id' => 33, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(8)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 6],
|
||||
],
|
||||
'discussion_user' => [
|
||||
['discussion_id' => 1, 'user_id' => 1, 'last_read_post_number' => 1, 'subscription' => 'follow'],
|
||||
|
@ -10,8 +10,11 @@
|
||||
namespace Flarum\Subscriptions\Tests\integration\api\discussions;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Post\Post;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
use Flarum\Testing\integration\TestCase;
|
||||
use Flarum\User\User;
|
||||
|
||||
class SubscribeTest extends TestCase
|
||||
{
|
||||
@ -24,18 +27,18 @@ class SubscribeTest extends TestCase
|
||||
$this->extension('flarum-subscriptions');
|
||||
|
||||
$this->prepareDatabase([
|
||||
'users' => [
|
||||
User::class => [
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1, 'preferences' => json_encode(['flarum-subscriptions.notify_for_all_posts' => true])],
|
||||
['id' => 4, 'username' => 'acme2', 'email' => 'acme2@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
'discussions' => [
|
||||
Discussion::class => [
|
||||
['id' => 1, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 1, 'comment_count' => 1, 'last_post_number' => 1, 'last_post_id' => 1],
|
||||
['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 2, 'comment_count' => 1, 'last_post_number' => 1, 'last_post_id' => 2],
|
||||
|
||||
['id' => 33, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 1, 'first_post_id' => 33, 'comment_count' => 6, 'last_post_number' => 6, 'last_post_id' => 38],
|
||||
],
|
||||
'posts' => [
|
||||
Post::class => [
|
||||
['id' => 1, 'discussion_id' => 1, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 1],
|
||||
['id' => 2, 'discussion_id' => 2, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 1],
|
||||
|
||||
|
@ -15,7 +15,7 @@ use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\User\Guest;
|
||||
use Flarum\User\UserRepository;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
|
@ -16,8 +16,9 @@ use Flarum\Search\SearchState;
|
||||
use Flarum\Search\ValidateFilterTrait;
|
||||
use Flarum\Tags\Tag;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
@ -53,7 +54,7 @@ class TagFilter implements FilterInterface
|
||||
$query->where(function (Builder $query) use ($slugs, $negate, $actor) {
|
||||
foreach ($slugs as $slug) {
|
||||
if ($slug === 'untagged') {
|
||||
$query->whereIn('discussions.id', function (Builder $query) {
|
||||
$query->whereIn('discussions.id', function (QueryBuilder $query) {
|
||||
$query->select('discussion_id')
|
||||
->from('discussion_tag');
|
||||
}, 'or', ! $negate);
|
||||
@ -65,7 +66,7 @@ class TagFilter implements FilterInterface
|
||||
$id = null;
|
||||
}
|
||||
|
||||
$query->whereIn('discussions.id', function (Builder $query) use ($id) {
|
||||
$query->whereIn('discussions.id', function (QueryBuilder $query) use ($id) {
|
||||
$query->select('discussion_id')
|
||||
->from('discussion_tag')
|
||||
->where('tag_id', $id);
|
||||
|
@ -31,6 +31,8 @@ class TagState extends AbstractModel
|
||||
|
||||
protected $casts = ['marked_as_read_at' => 'datetime'];
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
public function tag(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tag::class);
|
||||
|
@ -32,11 +32,18 @@ export type Extension = {
|
||||
extra: {
|
||||
'flarum-extension': {
|
||||
title: string;
|
||||
'database-support': undefined | string[];
|
||||
};
|
||||
};
|
||||
require?: Record<string, string>;
|
||||
};
|
||||
|
||||
export enum DatabaseDriver {
|
||||
MySQL = 'MySQL',
|
||||
PostgreSQL = 'PostgreSQL',
|
||||
SQLite = 'SQLite',
|
||||
}
|
||||
|
||||
export interface AdminApplicationData extends ApplicationData {
|
||||
extensions: Record<string, Extension>;
|
||||
settings: Record<string, string>;
|
||||
@ -48,6 +55,14 @@ export interface AdminApplicationData extends ApplicationData {
|
||||
maintenanceByConfig: boolean;
|
||||
safeModeExtensions?: string[] | null;
|
||||
safeModeExtensionsConfig?: string[] | null;
|
||||
|
||||
dbDriver: DatabaseDriver;
|
||||
dbVersion: string;
|
||||
dbOptions: Record<string, string>;
|
||||
phpVersion: string;
|
||||
queueDriver: string;
|
||||
schedulerStatus: string;
|
||||
sessionDriver: string;
|
||||
}
|
||||
|
||||
export default class AdminApplication extends Application {
|
||||
|
@ -11,6 +11,7 @@ import { MaintenanceMode } from '../../common/Application';
|
||||
import Button from '../../common/components/Button';
|
||||
import classList from '../../common/utils/classList';
|
||||
import ExtensionBisect from './ExtensionBisect';
|
||||
import { DatabaseDriver } from '../AdminApplication';
|
||||
|
||||
export default class AdvancedPage<CustomAttrs extends IPageAttrs = IPageAttrs> extends AdminPage<CustomAttrs> {
|
||||
searchDriverOptions: Record<string, Record<string, string>> = {};
|
||||
@ -68,6 +69,10 @@ export default class AdvancedPage<CustomAttrs extends IPageAttrs = IPageAttrs> e
|
||||
|
||||
items.add('maintenance', this.maintenance(), 90);
|
||||
|
||||
if (app.data.dbDriver === DatabaseDriver.PostgreSQL) {
|
||||
items.add(DatabaseDriver.PostgreSQL, this.pgsqlSettings(), 80);
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
@ -187,4 +192,19 @@ export default class AdvancedPage<CustomAttrs extends IPageAttrs = IPageAttrs> e
|
||||
</FormSection>
|
||||
);
|
||||
}
|
||||
|
||||
pgsqlSettings() {
|
||||
return (
|
||||
<FormSection label={DatabaseDriver.PostgreSQL}>
|
||||
<Form>
|
||||
{this.buildSettingComponent({
|
||||
type: 'select',
|
||||
setting: 'pgsql_search_configuration',
|
||||
options: app.data.dbOptions.search_configurations,
|
||||
label: app.translator.trans('core.admin.advanced.pgsql.search_configuration'),
|
||||
})}
|
||||
</Form>
|
||||
</FormSection>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import Form from '../../common/components/Form';
|
||||
import Icon from '../../common/components/Icon';
|
||||
import { MaintenanceMode } from '../../common/Application';
|
||||
import InfoTile from '../../common/components/InfoTile';
|
||||
import Alert from '../../common/components/Alert';
|
||||
|
||||
export interface ExtensionPageAttrs extends IPageAttrs {
|
||||
id: string;
|
||||
@ -79,8 +80,19 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
|
||||
}
|
||||
|
||||
body(vnode: Mithril.VnodeDOM<Attrs, this>) {
|
||||
const supportsDbDriver =
|
||||
!this.extension.extra['flarum-extension']['database-support'] ||
|
||||
this.extension.extra['flarum-extension']['database-support'].map((driver) => driver.toLowerCase()).includes(app.data.dbDriver.toLowerCase());
|
||||
|
||||
return this.isEnabled() ? (
|
||||
<div className="ExtensionPage-body">{this.sections(vnode).toArray()}</div>
|
||||
<div className="ExtensionPage-body">
|
||||
{!supportsDbDriver && (
|
||||
<Alert type="error" dismissible={false}>
|
||||
{app.translator.trans('core.admin.extension.database_driver_mismatch')}
|
||||
</Alert>
|
||||
)}
|
||||
{this.sections(vnode).toArray()}
|
||||
</div>
|
||||
) : (
|
||||
<div className="container">
|
||||
<h3 className="ExtensionPage-subHeader">{app.translator.trans('core.admin.extension.enable_to_see')}</h3>
|
||||
@ -187,7 +199,6 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
|
||||
}
|
||||
};
|
||||
|
||||
// TODO v2.0: rename `uninstall` to `purge`
|
||||
items.add(
|
||||
'uninstall',
|
||||
<Button icon="fas fa-trash-alt" className="Button Button--primary" onclick={purge.bind(this)}>
|
||||
@ -225,6 +236,27 @@ export default class ExtensionPage<Attrs extends ExtensionPageAttrs = ExtensionP
|
||||
}
|
||||
});
|
||||
|
||||
let supportedDatabases = this.extension.extra['flarum-extension']['database-support'] ?? null;
|
||||
if (supportedDatabases && supportedDatabases.length) {
|
||||
supportedDatabases = supportedDatabases.map((database: string) => {
|
||||
return (
|
||||
{
|
||||
mysql: 'MySQL',
|
||||
sqlite: 'SQLite',
|
||||
pgsql: 'PostgreSQL',
|
||||
}[database] || database
|
||||
);
|
||||
});
|
||||
|
||||
items.add(
|
||||
'database-support',
|
||||
<span className="LinkButton">
|
||||
<Icon name="fas fa-database" />
|
||||
{supportedDatabases.join(', ')}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const extension = this.extension;
|
||||
items.add(
|
||||
'readme',
|
||||
|
@ -559,7 +559,11 @@ export default class Application {
|
||||
break;
|
||||
|
||||
default:
|
||||
if (this.requestWasCrossOrigin(error)) {
|
||||
const code = error.response?.errors?.[0]?.code;
|
||||
|
||||
if (code === 'db_error' && app.session.user?.isAdmin()) {
|
||||
content = app.translator.trans('core.lib.error.db_error_message');
|
||||
} else if (this.requestWasCrossOrigin(error)) {
|
||||
content = app.translator.trans('core.lib.error.generic_cross_origin_message');
|
||||
} else {
|
||||
content = app.translator.trans('core.lib.error.generic_message');
|
||||
|
@ -45,6 +45,8 @@ core:
|
||||
safe_mode_extensions: Extensions allowed to boot during safe mode
|
||||
safe_mode_extensions_override_help: "This setting is overridden by the <code>safe_mode_extensions</code> key in your <code>config.php</code> file. (<b>{extensions}</b>)"
|
||||
section_label: Maintenance
|
||||
pgsql:
|
||||
search_configuration: Search configuration to use
|
||||
search:
|
||||
section_label: Search Drivers
|
||||
driver_heading: "Search Driver: {model}"
|
||||
@ -211,6 +213,7 @@ core:
|
||||
extension:
|
||||
configure_scopes: Configure Scopes
|
||||
confirm_purge: Purging will remove all database entries and assets related to the extension. It will not uninstall the extension; that must be done via Composer. Are you sure you want to continue?
|
||||
database_driver_mismatch: This extension does not support your configured database driver.
|
||||
disabled: Disabled
|
||||
enable_to_see: Enable the extension to view and change settings.
|
||||
enabled: Enabled
|
||||
@ -698,6 +701,7 @@ core:
|
||||
# These translations are displayed as error messages.
|
||||
error:
|
||||
circular_dependencies_message: "Circular dependencies detected: {extensions}. Aborting. Please disable one of the extensions and try again."
|
||||
db_error_message: "Database query failed. This may be caused by an incompatibility between an extension and your database driver."
|
||||
dependent_extensions_message: "Cannot disable {extension} until the following dependent extensions are disabled: {extensions}"
|
||||
extension_initialiation_failed_message: "{extension} failed to initialize, check the browser console for further information."
|
||||
generic_message: "Oops! Something went wrong. Please reload the page and try again."
|
||||
|
@ -32,11 +32,10 @@ return [
|
||||
$table->unique(['discussion_id', 'number']);
|
||||
});
|
||||
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
|
||||
if ($connection->getDriverName() !== 'sqlite') {
|
||||
$connection->statement('ALTER TABLE '.$prefix.'posts ADD FULLTEXT content (content)');
|
||||
if ($schema->getConnection()->getDriverName() !== 'sqlite') {
|
||||
$schema->table('posts', function (Blueprint $table) {
|
||||
$table->fullText('content');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -26,11 +26,22 @@ return [
|
||||
$table->integer('user_id')->unsigned()->change();
|
||||
});
|
||||
|
||||
// Use a separate schema instance because this column gets renamed
|
||||
// in the previous one.
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
$table->dateTime('last_activity_at')->change();
|
||||
});
|
||||
if ($schema->getConnection()->getDriverName() === 'pgsql') {
|
||||
$prefix = $schema->getConnection()->getTablePrefix();
|
||||
|
||||
// Changing an integer col to datetime is an unusual operation in PostgreSQL.
|
||||
$schema->getConnection()->statement(<<<SQL
|
||||
ALTER TABLE {$prefix}access_tokens
|
||||
ALTER COLUMN last_activity_at TYPE TIMESTAMP(0) WITHOUT TIME ZONE
|
||||
USING to_timestamp(last_activity_at)
|
||||
SQL);
|
||||
} else {
|
||||
// Use a separate schema instance because this column gets renamed
|
||||
// in the previous one.
|
||||
$schema->table('access_tokens', function (Blueprint $table) {
|
||||
$table->dateTime('last_activity_at')->change();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
|
@ -7,21 +7,23 @@
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
|
||||
if ($connection->getDriverName() !== 'sqlite') {
|
||||
$connection->statement('ALTER TABLE '.$prefix.'discussions ADD FULLTEXT title (title)');
|
||||
if ($schema->getConnection()->getDriverName() !== 'sqlite') {
|
||||
$schema->table('discussions', function (Blueprint $table) {
|
||||
$table->fullText('title');
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
$connection = $schema->getConnection();
|
||||
$prefix = $connection->getTablePrefix();
|
||||
$connection->statement('ALTER TABLE '.$prefix.'discussions DROP INDEX title');
|
||||
if ($schema->getConnection()->getDriverName() !== 'sqlite') {
|
||||
$schema->table('discussions', function (Blueprint $table) {
|
||||
$table->dropFullText('title');
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
||||
|
@ -28,6 +28,13 @@ return [
|
||||
|
||||
$db->table('groups')->insert(array_combine(['id', 'name_singular', 'name_plural', 'color', 'icon'], $group));
|
||||
}
|
||||
|
||||
// PgSQL doesn't auto-increment the sequence when inserting the IDs manually.
|
||||
if ($db->getDriverName() === 'pgsql') {
|
||||
$table = $db->getSchemaGrammar()->wrapTable('groups');
|
||||
$seq = $db->getSchemaGrammar()->wrapTable('groups_id_seq');
|
||||
$db->statement("SELECT setval('$seq', (SELECT MAX(id) FROM $table))");
|
||||
}
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
|
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
if ($schema->getConnection()->getDriverName() === 'pgsql') {
|
||||
$users = $schema->getConnection()->getSchemaGrammar()->wrapTable('users');
|
||||
$preferences = $schema->getConnection()->getSchemaGrammar()->wrap('preferences');
|
||||
$schema->getConnection()->statement("ALTER TABLE $users ALTER COLUMN $preferences TYPE JSON USING preferences::TEXT::JSON");
|
||||
} else {
|
||||
$schema->table('users', function (Blueprint $table) {
|
||||
$table->json('preferences')->nullable()->change();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
if ($schema->getConnection()->getDriverName() === 'pgsql') {
|
||||
$users = $schema->getConnection()->getSchemaGrammar()->wrapTable('users');
|
||||
$preferences = $schema->getConnection()->getSchemaGrammar()->wrap('preferences');
|
||||
$schema->getConnection()->statement("ALTER TABLE $users ALTER COLUMN $preferences TYPE BYTEA USING preferences::TEXT::BYTEA");
|
||||
} else {
|
||||
$schema->table('users', function (Blueprint $table) {
|
||||
$table->binary('preferences')->nullable()->change();
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Schema\Builder;
|
||||
|
||||
return [
|
||||
'up' => function (Builder $schema) {
|
||||
if ($schema->getConnection()->getDriverName() === 'pgsql') {
|
||||
$notifications = $schema->getConnection()->getSchemaGrammar()->wrapTable('notifications');
|
||||
$data = $schema->getConnection()->getSchemaGrammar()->wrap('data');
|
||||
$schema->getConnection()->statement("ALTER TABLE $notifications ALTER COLUMN $data TYPE JSON USING data::TEXT::JSON");
|
||||
} else {
|
||||
$schema->table('notifications', function (Blueprint $table) {
|
||||
$table->json('data')->nullable()->change();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
'down' => function (Builder $schema) {
|
||||
if ($schema->getConnection()->getDriverName() === 'pgsql') {
|
||||
$notifications = $schema->getConnection()->getSchemaGrammar()->wrapTable('notifications');
|
||||
$data = $schema->getConnection()->getSchemaGrammar()->wrap('data');
|
||||
$schema->getConnection()->statement("ALTER TABLE $notifications ALTER COLUMN $data TYPE BYTEA USING data::TEXT::BYTEA");
|
||||
} else {
|
||||
$schema->table('notifications', function (Blueprint $table) {
|
||||
$table->binary('data')->nullable()->change();
|
||||
});
|
||||
}
|
||||
}
|
||||
];
|
1247
framework/core/migrations/pgsql-install.dump
Normal file
1247
framework/core/migrations/pgsql-install.dump
Normal file
File diff suppressed because it is too large
Load Diff
@ -62,6 +62,7 @@ class AdminPayload
|
||||
$document->payload['phpVersion'] = $this->appInfo->identifyPHPVersion();
|
||||
$document->payload['dbDriver'] = $this->appInfo->identifyDatabaseDriver();
|
||||
$document->payload['dbVersion'] = $this->appInfo->identifyDatabaseVersion();
|
||||
$document->payload['dbOptions'] = $this->appInfo->identifyDatabaseOptions();
|
||||
$document->payload['debugEnabled'] = Arr::get($this->config, 'debug');
|
||||
|
||||
if ($this->appInfo->scheduledTasksRegistered()) {
|
||||
|
@ -217,7 +217,11 @@ class UserResource extends AbstractDatabaseResource
|
||||
|| $context->getActor()->can('editCredentials', $user);
|
||||
})
|
||||
->set(function (User $user, ?string $value) {
|
||||
$user->exists && $user->changePassword($value);
|
||||
if ($user->exists) {
|
||||
$user->changePassword($value);
|
||||
} else {
|
||||
$user->password = $value;
|
||||
}
|
||||
}),
|
||||
// Registration token.
|
||||
Schema\Str::make('token')
|
||||
|
@ -18,7 +18,9 @@ use Illuminate\Contracts\Container\Container;
|
||||
use Illuminate\Database\Capsule\Manager;
|
||||
use Illuminate\Database\ConnectionInterface;
|
||||
use Illuminate\Database\ConnectionResolverInterface;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DatabaseServiceProvider extends AbstractServiceProvider
|
||||
@ -28,6 +30,7 @@ class DatabaseServiceProvider extends AbstractServiceProvider
|
||||
public function register(): void
|
||||
{
|
||||
$this->registerEloquentFactory();
|
||||
$this->registerBuilderMacros();
|
||||
|
||||
$this->container->singleton(Manager::class, function (ContainerImplementation $container) {
|
||||
$manager = new Manager($container);
|
||||
@ -78,6 +81,41 @@ class DatabaseServiceProvider extends AbstractServiceProvider
|
||||
});
|
||||
}
|
||||
|
||||
protected function registerBuilderMacros(): void
|
||||
{
|
||||
$drivers = [
|
||||
'mysql' => 'MySql',
|
||||
'pgsql' => 'PgSql',
|
||||
'sqlite' => 'Sqlite',
|
||||
];
|
||||
|
||||
foreach ([QueryBuilder::class, EloquentBuilder::class] as $builder) {
|
||||
foreach ($drivers as $driver => $macro) {
|
||||
$builder::macro('when'.$macro, function ($callback, $else) use ($driver) {
|
||||
/** @var QueryBuilder|EloquentBuilder $this */
|
||||
if ($this->getConnection()->getDriverName() === $driver) {
|
||||
$callback($this);
|
||||
} else {
|
||||
$else($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
});
|
||||
|
||||
$builder::macro('unless'.$macro, function ($callback, $else) use ($driver) {
|
||||
/** @var QueryBuilder|EloquentBuilder $this */
|
||||
if ($this->getConnection()->getDriverName() !== $driver) {
|
||||
$callback($this);
|
||||
} else {
|
||||
$else($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function registerEloquentFactory(): void
|
||||
{
|
||||
$this->app->singleton(FakerGenerator::class, function ($app, $parameters) {
|
||||
|
@ -69,8 +69,38 @@ class Migrator
|
||||
// Once we have the array of migrations, we will spin through them and run the
|
||||
// migrations "up" so the changes are made to the databases. We'll then log
|
||||
// that the migration was run so we don't repeat it next time we execute.
|
||||
foreach ($migrations as $file) {
|
||||
$this->runUp($path, $file, $extension);
|
||||
$this->runUpMigrations($migrations, $path, $extension);
|
||||
}
|
||||
|
||||
protected function runUpMigrations(array $migrations, string $path, ?Extension $extension = null): void
|
||||
{
|
||||
$process = function () use ($migrations, $path, $extension) {
|
||||
foreach ($migrations as $migration) {
|
||||
$this->runUp($path, $migration, $extension);
|
||||
}
|
||||
};
|
||||
|
||||
// PgSQL allows DDL statements in transactions.
|
||||
if ($this->connection->getDriverName() === 'pgsql') {
|
||||
$this->connection->transaction($process);
|
||||
} else {
|
||||
$process();
|
||||
}
|
||||
}
|
||||
|
||||
protected function runDownMigrations(array $migrations, string $path, ?Extension $extension = null): void
|
||||
{
|
||||
$process = function () use ($migrations, $path, $extension) {
|
||||
foreach ($migrations as $migration) {
|
||||
$this->runDown($path, $migration, $extension);
|
||||
}
|
||||
};
|
||||
|
||||
// PgSQL allows DDL statements in transactions.
|
||||
if ($this->connection->getDriverName() === 'pgsql') {
|
||||
$this->connection->transaction($process);
|
||||
} else {
|
||||
$process();
|
||||
}
|
||||
}
|
||||
|
||||
@ -103,9 +133,7 @@ class Migrator
|
||||
if ($count === 0) {
|
||||
$this->note('<info>Nothing to rollback.</info>');
|
||||
} else {
|
||||
foreach ($migrations as $migration) {
|
||||
$this->runDown($path, $migration, $extension);
|
||||
}
|
||||
$this->runDownMigrations($migrations, $path, $extension);
|
||||
}
|
||||
|
||||
return $count;
|
||||
@ -221,9 +249,11 @@ class Migrator
|
||||
|
||||
$dump = file_get_contents($schemaPath);
|
||||
|
||||
$dumpWithoutComments = preg_replace('/^--.*$/m', '', $dump);
|
||||
|
||||
$this->connection->getSchemaBuilder()->disableForeignKeyConstraints();
|
||||
|
||||
foreach (explode(';', $dump) as $statement) {
|
||||
foreach (explode(';', $dumpWithoutComments) as $statement) {
|
||||
$statement = trim($statement);
|
||||
|
||||
if (empty($statement) || str_starts_with($statement, '/*')) {
|
||||
@ -238,6 +268,10 @@ class Migrator
|
||||
$this->connection->statement($statement);
|
||||
}
|
||||
|
||||
if ($driver === 'pgsql') {
|
||||
$this->connection->statement('SELECT pg_catalog.set_config(\'search_path\', \'public\', false)');
|
||||
}
|
||||
|
||||
$this->connection->getSchemaBuilder()->enableForeignKeyConstraints();
|
||||
|
||||
$runTime = number_format((microtime(true) - $startTime) * 1000, 2);
|
||||
|
@ -183,7 +183,7 @@ class Discussion extends AbstractModel
|
||||
|
||||
public function refreshLastPost(): static
|
||||
{
|
||||
if ($lastPost = $this->comments()->latest()->first()) {
|
||||
if ($lastPost = $this->comments()->latest()->latest('id')->first()) {
|
||||
/** @var Post $lastPost */
|
||||
$this->setLastPost($lastPost);
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\Search\ValidateFilterTrait;
|
||||
use Flarum\User\UserRepository;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
|
@ -13,7 +13,7 @@ use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\Search\ValidateFilterTrait;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
/**
|
||||
@ -40,7 +40,7 @@ class CreatedFilter implements FilterInterface
|
||||
$this->constrain($state->getQuery(), $from, $to, $negate);
|
||||
}
|
||||
|
||||
public function constrain(Builder $query, ?string $from, ?string $to, bool $negate): void
|
||||
protected function constrain(Builder $query, ?string $from, ?string $to, bool $negate): void
|
||||
{
|
||||
// If we've just been provided with a single YYYY-MM-DD date, then find
|
||||
// discussions that were started on that exact date. But if we've been
|
||||
|
@ -12,7 +12,7 @@ namespace Flarum\Discussion\Search\Filter;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
|
@ -14,7 +14,7 @@ use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
@ -41,11 +41,15 @@ class UnreadFilter implements FilterInterface
|
||||
if ($actor->exists) {
|
||||
$readIds = $this->discussions->getReadIdsQuery($actor);
|
||||
|
||||
$query->where(function ($query) use ($readIds, $negate, $actor) {
|
||||
$query->where(function (Builder $query) use ($readIds, $negate, $actor) {
|
||||
if (! $negate) {
|
||||
$query->whereNotIn('id', $readIds)->where('last_posted_at', '>', $actor->marked_all_as_read_at ?: 0);
|
||||
$query->whereNotIn('id', $readIds)->when($actor->marked_all_as_read_at, function (Builder $query) use ($actor) {
|
||||
$query->where('last_posted_at', '>', $actor->marked_all_as_read_at);
|
||||
});
|
||||
} else {
|
||||
$query->whereIn('id', $readIds)->orWhere('last_posted_at', '<=', $actor->marked_all_as_read_at ?: 0);
|
||||
$query->whereIn('id', $readIds)->when($actor->marked_all_as_read_at, function (Builder $query) use ($actor) {
|
||||
$query->orWhere('last_posted_at', '<=', $actor->marked_all_as_read_at);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -14,37 +14,57 @@ use Flarum\Post\Post;
|
||||
use Flarum\Search\AbstractFulltextFilter;
|
||||
use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\SearchState;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Flarum\Settings\SettingsRepositoryInterface;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
use Illuminate\Database\Query\Expression;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* @extends AbstractFulltextFilter<DatabaseSearchState>
|
||||
*/
|
||||
class FulltextFilter extends AbstractFulltextFilter
|
||||
{
|
||||
public function __construct(
|
||||
protected SettingsRepositoryInterface $settings
|
||||
) {
|
||||
}
|
||||
|
||||
public function search(SearchState $state, string $value): void
|
||||
{
|
||||
match ($state->getQuery()->getConnection()->getDriverName()) {
|
||||
'mysql' => $this->mysql($state, $value),
|
||||
'pgsql' => $this->pgsql($state, $value),
|
||||
'sqlite' => $this->sqlite($state, $value),
|
||||
default => throw new RuntimeException('Unsupported database driver: '.$state->getQuery()->getConnection()->getDriverName()),
|
||||
};
|
||||
}
|
||||
|
||||
protected function sqlite(DatabaseSearchState $state, string $value): void
|
||||
{
|
||||
/** @var Builder $query */
|
||||
$query = $state->getQuery();
|
||||
|
||||
if ($query->getConnection()->getDriverName() === 'sqlite') {
|
||||
$query->where(function (Builder $query) use ($state, $value) {
|
||||
$query->where('discussions.title', 'like', "%$value%")
|
||||
->orWhereExists(function (Builder $query) use ($state, $value) {
|
||||
$query->selectRaw('1')
|
||||
->from(
|
||||
Post::whereVisibleTo($state->getActor())
|
||||
->whereColumn('discussion_id', 'discussions.id')
|
||||
->where('type', 'comment')
|
||||
->where('content', 'like', "%$value%")
|
||||
->limit(1)
|
||||
->toBase()
|
||||
);
|
||||
});
|
||||
});
|
||||
$query->where(function (Builder $query) use ($state, $value) {
|
||||
$query->where('discussions.title', 'like', "%$value%")
|
||||
->orWhereExists(function (QueryBuilder $query) use ($state, $value) {
|
||||
$query->selectRaw('1')
|
||||
->from(
|
||||
Post::whereVisibleTo($state->getActor())
|
||||
->whereColumn('discussion_id', 'discussions.id')
|
||||
->where('type', 'comment')
|
||||
->where('content', 'like', "%$value%")
|
||||
->limit(1)
|
||||
->toBase()
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
protected function mysql(DatabaseSearchState $state, string $value): void
|
||||
{
|
||||
/** @var Builder $query */
|
||||
$query = $state->getQuery();
|
||||
|
||||
// Replace all non-word characters with spaces.
|
||||
// We do this to prevent MySQL fulltext search boolean mode from taking
|
||||
@ -53,10 +73,15 @@ class FulltextFilter extends AbstractFulltextFilter
|
||||
|
||||
$grammar = $query->getGrammar();
|
||||
|
||||
$match = 'MATCH('.$grammar->wrap('posts.content').') AGAINST (?)';
|
||||
$matchBooleanMode = 'MATCH('.$grammar->wrap('posts.content').') AGAINST (? IN BOOLEAN MODE)';
|
||||
$matchTitle = 'MATCH('.$grammar->wrap('discussions.title').') AGAINST (?)';
|
||||
$mostRelevantPostId = 'SUBSTRING_INDEX(GROUP_CONCAT('.$grammar->wrap('posts.id').' ORDER BY '.$match.' DESC, '.$grammar->wrap('posts.number').'), \',\', 1) as most_relevant_post_id';
|
||||
|
||||
$discussionSubquery = Discussion::select('id')
|
||||
->selectRaw('NULL as score')
|
||||
->selectRaw('first_post_id as most_relevant_post_id')
|
||||
->whereRaw('MATCH('.$grammar->wrap('discussions.title').') AGAINST (? IN BOOLEAN MODE)', [$value]);
|
||||
->whereRaw($matchTitle, [$value]);
|
||||
|
||||
// Construct a subquery to fetch discussions which contain relevant
|
||||
// posts. Retrieve the collective relevance of each discussion's posts,
|
||||
@ -64,10 +89,10 @@ class FulltextFilter extends AbstractFulltextFilter
|
||||
// the ID of the most relevant post.
|
||||
$subquery = Post::whereVisibleTo($state->getActor())
|
||||
->select('posts.discussion_id')
|
||||
->selectRaw('SUM(MATCH('.$grammar->wrap('posts.content').') AGAINST (?)) as score', [$value])
|
||||
->selectRaw('SUBSTRING_INDEX(GROUP_CONCAT('.$grammar->wrap('posts.id').' ORDER BY MATCH('.$grammar->wrap('posts.content').') AGAINST (?) DESC, '.$grammar->wrap('posts.number').'), \',\', 1) as most_relevant_post_id', [$value])
|
||||
->selectRaw("SUM($match) as score", [$value])
|
||||
->selectRaw($mostRelevantPostId, [$value])
|
||||
->where('posts.type', 'comment')
|
||||
->whereRaw('MATCH('.$grammar->wrap('posts.content').') AGAINST (? IN BOOLEAN MODE)', [$value])
|
||||
->whereRaw($matchBooleanMode, [$value])
|
||||
->groupBy('posts.discussion_id')
|
||||
->union($discussionSubquery);
|
||||
|
||||
@ -84,9 +109,71 @@ class FulltextFilter extends AbstractFulltextFilter
|
||||
->groupBy('discussions.id')
|
||||
->addBinding($subquery->getBindings(), 'join');
|
||||
|
||||
$state->setDefaultSort(function (Builder $query) use ($grammar, $value) {
|
||||
$query->orderByRaw('MATCH('.$grammar->wrap('discussions.title').') AGAINST (?) desc', [$value]);
|
||||
$state->setDefaultSort(function (Builder $query) use ($value, $matchTitle) {
|
||||
$query->orderByRaw("$matchTitle desc", [$value]);
|
||||
$query->orderBy('posts_ft.score', 'desc');
|
||||
});
|
||||
}
|
||||
|
||||
protected function pgsql(DatabaseSearchState $state, string $value): void
|
||||
{
|
||||
$searchConfig = $this->settings->get('pgsql_search_configuration');
|
||||
|
||||
/** @var Builder $query */
|
||||
$query = $state->getQuery();
|
||||
|
||||
$grammar = $query->getGrammar();
|
||||
|
||||
$matchCondition = "to_tsvector('$searchConfig', ".$grammar->wrap('posts.content').") @@ plainto_tsquery('$searchConfig', ?)";
|
||||
$matchScore = "ts_rank(to_tsvector('$searchConfig', ".$grammar->wrap('posts.content')."), plainto_tsquery('$searchConfig', ?))";
|
||||
$matchTitleCondition = "to_tsvector('$searchConfig', ".$grammar->wrap('discussions.title').") @@ plainto_tsquery('$searchConfig', ?)";
|
||||
$matchTitleScore = "ts_rank(to_tsvector('$searchConfig', ".$grammar->wrap('discussions.title')."), plainto_tsquery('$searchConfig', ?))";
|
||||
$mostRelevantPostId = 'CAST(SPLIT_PART(STRING_AGG(CAST('.$grammar->wrap('posts.id')." AS VARCHAR), ',' ORDER BY ".$matchScore.' DESC, '.$grammar->wrap('posts.number')."), ',', 1) AS INTEGER) as most_relevant_post_id";
|
||||
|
||||
$discussionSubquery = Discussion::select('id')
|
||||
->selectRaw('NULL as score')
|
||||
->selectRaw('first_post_id as most_relevant_post_id')
|
||||
->whereRaw($matchTitleCondition, [$value]);
|
||||
|
||||
// Construct a subquery to fetch discussions which contain relevant
|
||||
// posts. Retrieve the collective relevance of each discussion's posts,
|
||||
// which we will use later in the order by clause, and also retrieve
|
||||
// the ID of the most relevant post.
|
||||
$subquery = Post::whereVisibleTo($state->getActor())
|
||||
->select('posts.discussion_id')
|
||||
->selectRaw("SUM($matchScore) as score", [$value])
|
||||
->selectRaw($mostRelevantPostId, [$value])
|
||||
->where('posts.type', 'comment')
|
||||
->whereRaw($matchCondition, [$value])
|
||||
->groupBy('posts.discussion_id')
|
||||
->union($discussionSubquery);
|
||||
|
||||
// Join the subquery into the main search query and scope results to
|
||||
// discussions that have a relevant title or that contain relevant posts.
|
||||
$query
|
||||
->distinct('discussions.id')
|
||||
->addSelect('posts_ft.most_relevant_post_id')
|
||||
->addSelect('posts_ft.score')
|
||||
->join(
|
||||
new Expression('('.$subquery->toSql().') '.$grammar->wrapTable('posts_ft')),
|
||||
'posts_ft.discussion_id',
|
||||
'=',
|
||||
'discussions.id'
|
||||
)
|
||||
->addBinding($subquery->getBindings(), 'join')
|
||||
->orderBy('discussions.id');
|
||||
|
||||
$state->setQuery(
|
||||
$query
|
||||
->getModel()
|
||||
->newQuery()
|
||||
->select('*')
|
||||
->fromSub($query, 'discussions')
|
||||
);
|
||||
|
||||
$state->setDefaultSort(function (Builder $query) use ($value, $matchTitleScore) {
|
||||
$query->orderByRaw("$matchTitleScore desc", [$value]);
|
||||
$query->orderBy('discussions.score', 'desc');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -44,6 +44,8 @@ class UserState extends AbstractModel
|
||||
'last_read_at' => 'datetime'
|
||||
];
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*/
|
||||
|
@ -71,7 +71,7 @@ class ApplicationInfoProvider
|
||||
public function identifyDatabaseVersion(): string
|
||||
{
|
||||
return match ($this->config['database.driver']) {
|
||||
'mysql' => $this->db->selectOne('select version() as version')->version,
|
||||
'mysql', 'pgsql' => $this->db->selectOne('select version() as version')->version,
|
||||
'sqlite' => $this->db->selectOne('select sqlite_version() as version')->version,
|
||||
default => 'Unknown',
|
||||
};
|
||||
@ -81,11 +81,26 @@ class ApplicationInfoProvider
|
||||
{
|
||||
return match ($this->config['database.driver']) {
|
||||
'mysql' => 'MySQL',
|
||||
'pgsql' => 'PostgreSQL',
|
||||
'sqlite' => 'SQLite',
|
||||
default => $this->config['database.driver'],
|
||||
};
|
||||
}
|
||||
|
||||
public function identifyDatabaseOptions(): array
|
||||
{
|
||||
if ($this->config['database.driver'] === 'pgsql') {
|
||||
return [
|
||||
'search_configurations' => collect($this->db->select('SELECT * FROM pg_ts_config'))
|
||||
->pluck('cfgname')
|
||||
->mapWithKeys(fn (string $cfgname) => [$cfgname => $cfgname])
|
||||
->toArray(),
|
||||
];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reports on the session driver in use based on three scenarios:
|
||||
* 1. If the configured session driver is valid and in use, it will be returned.
|
||||
|
@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Flarum.
|
||||
*
|
||||
* For detailed copyright and license information, please view the
|
||||
* LICENSE file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace Flarum\Foundation\ErrorHandling\ExceptionHandler;
|
||||
|
||||
use Flarum\Foundation\ErrorHandling\HandledError;
|
||||
use Illuminate\Database\QueryException;
|
||||
|
||||
class QueryExceptionHandler
|
||||
{
|
||||
public function handle(QueryException $e): HandledError
|
||||
{
|
||||
return (new HandledError(
|
||||
$e,
|
||||
'db_error',
|
||||
500,
|
||||
true
|
||||
))->withDetails([]);
|
||||
}
|
||||
}
|
@ -30,7 +30,8 @@ class HandledError
|
||||
public function __construct(
|
||||
private readonly Throwable $error,
|
||||
private readonly string $type,
|
||||
private readonly int $statusCode
|
||||
private readonly int $statusCode,
|
||||
private bool $report = false
|
||||
) {
|
||||
}
|
||||
|
||||
@ -58,7 +59,7 @@ class HandledError
|
||||
|
||||
public function shouldBeReported(): bool
|
||||
{
|
||||
return $this->type === 'unknown';
|
||||
return $this->type === 'unknown' || $this->report;
|
||||
}
|
||||
|
||||
public function getDetails(): array
|
||||
|
@ -13,6 +13,7 @@ use Flarum\Extension\Exception as ExtensionException;
|
||||
use Flarum\Foundation\ErrorHandling as Handling;
|
||||
use Flarum\Http\Exception\InvalidParameterException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Validation\ValidationException as IlluminateValidationException;
|
||||
use Tobyz\JsonApiServer\Exception as TobyzJsonApiServerException;
|
||||
|
||||
@ -65,6 +66,7 @@ class ErrorServiceProvider extends AbstractServiceProvider
|
||||
ExtensionException\CircularDependenciesException::class => ExtensionException\CircularDependenciesExceptionHandler::class,
|
||||
ExtensionException\DependentExtensionsException::class => ExtensionException\DependentExtensionsExceptionHandler::class,
|
||||
ExtensionException\MissingDependenciesException::class => ExtensionException\MissingDependenciesExceptionHandler::class,
|
||||
QueryException::class => Handling\ExceptionHandler\QueryExceptionHandler::class,
|
||||
TobyzJsonApiServerException\ErrorProvider::class => Handling\ExceptionHandler\JsonApiExceptionHandler::class,
|
||||
];
|
||||
});
|
||||
|
@ -26,6 +26,8 @@ class Permission extends AbstractModel
|
||||
'created_at' => 'datetime'
|
||||
];
|
||||
|
||||
public $incrementing = false;
|
||||
|
||||
public function group(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Group::class);
|
||||
|
@ -42,20 +42,31 @@ class UserDataProvider implements DataProviderInterface
|
||||
|
||||
private function getDatabaseConfiguration(): DatabaseConfig
|
||||
{
|
||||
$host = $this->ask('Database host (required):');
|
||||
$port = 3306;
|
||||
$driver = $this->ask('Database driver (mysql, sqlite, pgsql) (Default: mysql):', 'mysql');
|
||||
$port = match ($driver) {
|
||||
'mysql' => 3306,
|
||||
'pgsql' => 5432,
|
||||
default => 0,
|
||||
};
|
||||
|
||||
if (Str::contains($host, ':')) {
|
||||
list($host, $port) = explode(':', $host, 2);
|
||||
if (in_array($driver, ['mysql', 'pgsql'])) {
|
||||
$host = $this->ask('Database host (required):');
|
||||
|
||||
if (Str::contains($host, ':')) {
|
||||
list($host, $port) = explode(':', $host, 2);
|
||||
}
|
||||
|
||||
$user = $this->ask('Database user (required):');
|
||||
$password = $this->secret('Database password:');
|
||||
}
|
||||
|
||||
return new DatabaseConfig(
|
||||
$this->ask('Database driver (mysql, sqlite) (Default: mysql):', 'mysql'),
|
||||
$host,
|
||||
$driver,
|
||||
$host ?? null,
|
||||
intval($port),
|
||||
$this->ask('Database name (required):'),
|
||||
$this->ask('Database user (required):'),
|
||||
$this->secret('Database password:'),
|
||||
$user ?? null,
|
||||
$password ?? null,
|
||||
$this->ask('Prefix:')
|
||||
);
|
||||
}
|
||||
|
@ -76,20 +76,25 @@ class InstallController implements RequestHandlerInterface
|
||||
|
||||
private function makeDatabaseConfig(array $input): DatabaseConfig
|
||||
{
|
||||
$host = Arr::get($input, 'mysqlHost');
|
||||
$port = 3306;
|
||||
$driver = Arr::get($input, 'dbDriver');
|
||||
$host = Arr::get($input, 'dbHost');
|
||||
$port = match ($driver) {
|
||||
'mysql' => 3306,
|
||||
'pgsql' => 5432,
|
||||
default => 0,
|
||||
};
|
||||
|
||||
if (Str::contains($host, ':')) {
|
||||
list($host, $port) = explode(':', $host, 2);
|
||||
}
|
||||
|
||||
return new DatabaseConfig(
|
||||
Arr::get($input, 'dbDriver'),
|
||||
$driver,
|
||||
$host,
|
||||
intval($port),
|
||||
Arr::get($input, 'dbName'),
|
||||
Arr::get($input, 'mysqlUsername'),
|
||||
Arr::get($input, 'mysqlPassword'),
|
||||
Arr::get($input, 'dbUsername'),
|
||||
Arr::get($input, 'dbPassword'),
|
||||
Arr::get($input, 'tablePrefix')
|
||||
);
|
||||
}
|
||||
|
@ -16,11 +16,11 @@ class DatabaseConfig implements Arrayable
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $driver,
|
||||
private readonly string $host,
|
||||
private readonly ?string $host,
|
||||
private readonly int $port,
|
||||
private string $database,
|
||||
private readonly string $username,
|
||||
private readonly string $password,
|
||||
private readonly ?string $username,
|
||||
private readonly ?string $password,
|
||||
private readonly string $prefix
|
||||
) {
|
||||
$this->validate();
|
||||
@ -42,15 +42,15 @@ class DatabaseConfig implements Arrayable
|
||||
throw new ValidationFailed('Please specify a database driver.');
|
||||
}
|
||||
|
||||
if (! in_array($this->driver, ['mysql', 'sqlite'])) {
|
||||
if (! in_array($this->driver, ['mysql', 'sqlite', 'pgsql'])) {
|
||||
throw new ValidationFailed('Currently, only MySQL/MariaDB and SQLite are supported.');
|
||||
}
|
||||
|
||||
if ($this->driver === 'mysql' && empty($this->host)) {
|
||||
if (in_array($this->driver, ['mysql', 'pgsql']) && empty($this->host)) {
|
||||
throw new ValidationFailed('Please specify the hostname of your database server.');
|
||||
}
|
||||
|
||||
if ($this->driver === 'mysql' && ($this->port < 1 || $this->port > 65535)) {
|
||||
if (in_array($this->driver, ['mysql', 'pgsql']) && ($this->port < 1 || $this->port > 65535)) {
|
||||
throw new ValidationFailed('Please provide a valid port number between 1 and 65535.');
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ class DatabaseConfig implements Arrayable
|
||||
throw new ValidationFailed('Please specify the database name.');
|
||||
}
|
||||
|
||||
if ($this->driver === 'mysql' && empty($this->username)) {
|
||||
if (in_array($this->driver, ['mysql', 'pgsql']) && empty($this->username)) {
|
||||
throw new ValidationFailed('Please specify the username for accessing the database.');
|
||||
}
|
||||
|
||||
@ -94,6 +94,15 @@ class DatabaseConfig implements Arrayable
|
||||
'engine' => 'InnoDB',
|
||||
'strict' => false,
|
||||
],
|
||||
'pgsql' => [
|
||||
'host' => $this->host,
|
||||
'port' => $this->port,
|
||||
'username' => $this->username,
|
||||
'password' => $this->password,
|
||||
'charset' => 'utf8',
|
||||
'search_path' => 'public',
|
||||
'sslmode' => 'prefer',
|
||||
],
|
||||
'sqlite' => [
|
||||
'foreign_key_constraints' => true,
|
||||
],
|
||||
|
@ -13,8 +13,10 @@ use Closure;
|
||||
use Flarum\Install\DatabaseConfig;
|
||||
use Flarum\Install\Step;
|
||||
use Illuminate\Database\Connectors\MySqlConnector;
|
||||
use Illuminate\Database\Connectors\PostgresConnector;
|
||||
use Illuminate\Database\Connectors\SQLiteConnector;
|
||||
use Illuminate\Database\MySqlConnection;
|
||||
use Illuminate\Database\PostgresConnection;
|
||||
use Illuminate\Database\SQLiteConnection;
|
||||
use Illuminate\Support\Str;
|
||||
use InvalidArgumentException;
|
||||
@ -40,6 +42,7 @@ class ConnectToDatabase implements Step
|
||||
|
||||
match ($config['driver']) {
|
||||
'mysql' => $this->mysql($config),
|
||||
'pgsql' => $this->pgsql($config),
|
||||
'sqlite' => $this->sqlite($config),
|
||||
default => throw new InvalidArgumentException('Unsupported database driver: '.$config['driver']),
|
||||
};
|
||||
@ -53,11 +56,11 @@ class ConnectToDatabase implements Step
|
||||
|
||||
if (Str::contains($version, 'MariaDB')) {
|
||||
if (version_compare($version, '10.10.0', '<')) {
|
||||
throw new RangeException('MariaDB version too low. You need at least MariaDB 10.0.5');
|
||||
throw new RangeException("MariaDB version ($version) too low. You need at least MariaDB 10.10");
|
||||
}
|
||||
} else {
|
||||
if (version_compare($version, '5.7.0', '<')) {
|
||||
throw new RangeException('MySQL version too low. You need at least MySQL 5.7');
|
||||
throw new RangeException("MySQL version ($version) too low. You need at least MySQL 5.7");
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,6 +74,27 @@ class ConnectToDatabase implements Step
|
||||
);
|
||||
}
|
||||
|
||||
private function pgsql(array $config): void
|
||||
{
|
||||
$pdo = (new PostgresConnector)->connect($config);
|
||||
|
||||
$version = $pdo->query('SHOW server_version')->fetchColumn();
|
||||
$version = Str::before($version, ' ');
|
||||
|
||||
if (version_compare($version, '9.5.0', '<')) {
|
||||
throw new RangeException("PostgreSQL version ($version) too low. You need at least PostgreSQL 9.5");
|
||||
}
|
||||
|
||||
($this->store)(
|
||||
new PostgresConnection(
|
||||
$pdo,
|
||||
$config['database'],
|
||||
$config['prefix'],
|
||||
$config
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private function sqlite(array $config): void
|
||||
{
|
||||
if (! file_exists($config['database'])) {
|
||||
@ -81,8 +105,8 @@ class ConnectToDatabase implements Step
|
||||
|
||||
$version = $pdo->query('SELECT sqlite_version()')->fetchColumn();
|
||||
|
||||
if (version_compare($version, '3.8.8', '<')) {
|
||||
throw new RangeException('SQLite version too low. You need at least SQLite 3.8.8');
|
||||
if (version_compare($version, '3.35.0', '<')) {
|
||||
throw new RangeException("SQLite version ($version) too low. You need at least SQLite 3.35.0");
|
||||
}
|
||||
|
||||
($this->store)(
|
||||
|
@ -24,7 +24,7 @@ use League\Flysystem\Local\LocalFilesystemAdapter;
|
||||
|
||||
class EnableBundledExtensions implements Step
|
||||
{
|
||||
public const EXTENSION_WHITELIST = [
|
||||
public const DEFAULT_ENABLED_EXTENSIONS = [
|
||||
'flarum-approval',
|
||||
'flarum-bbcode',
|
||||
'flarum-emoji',
|
||||
@ -54,7 +54,7 @@ class EnableBundledExtensions implements Step
|
||||
private readonly string $assetPath,
|
||||
?array $enabledExtensions = null
|
||||
) {
|
||||
$this->enabledExtensions = $enabledExtensions ?? self::EXTENSION_WHITELIST;
|
||||
$this->enabledExtensions = $enabledExtensions ?? self::DEFAULT_ENABLED_EXTENSIONS;
|
||||
}
|
||||
|
||||
public function getMessage(): string
|
||||
|
@ -159,7 +159,17 @@ class Notification extends AbstractModel
|
||||
*/
|
||||
public function scopeMatchingBlueprint(Builder $query, BlueprintInterface $blueprint): Builder
|
||||
{
|
||||
return $query->where(static::getBlueprintAttributes($blueprint));
|
||||
$attributes = static::getBlueprintAttributes($blueprint);
|
||||
|
||||
$data = $attributes['data'];
|
||||
unset($attributes['data']);
|
||||
|
||||
return $query->where($attributes)
|
||||
->whenPgSql(function ($query) use ($data) {
|
||||
return $query->whereRaw('data::text = ?', [$data]);
|
||||
}, function ($query) use ($data) {
|
||||
return $query->where('data', $data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -31,7 +31,7 @@ class NotificationRepository
|
||||
{
|
||||
$primaries = Notification::query()
|
||||
->selectRaw('MAX(id) AS id')
|
||||
->selectRaw('SUM(read_at IS NULL) AS unread_count')
|
||||
->selectRaw('COUNT(read_at IS NULL) AS unread_count')
|
||||
->where('user_id', $user->id)
|
||||
->whereIn('type', $user->getAlertableNotificationTypes())
|
||||
->where('is_deleted', false)
|
||||
|
@ -33,7 +33,7 @@ abstract class AbstractSearcher implements SearcherInterface
|
||||
$query = $this->getQuery($actor);
|
||||
|
||||
$search = new DatabaseSearchState($actor, $criteria->isFulltext());
|
||||
$search->setQuery($query->getQuery());
|
||||
$search->setQuery($query);
|
||||
|
||||
$this->filters->apply($search, $criteria->filters);
|
||||
|
||||
@ -45,6 +45,8 @@ abstract class AbstractSearcher implements SearcherInterface
|
||||
$mutator($search, $criteria);
|
||||
}
|
||||
|
||||
$query = $search->getQuery();
|
||||
|
||||
// Execute the search query and retrieve the results. We get one more
|
||||
// results than the user asked for, so that we can say if there are more
|
||||
// results. If there are, we will get rid of that extra result.
|
||||
|
@ -10,7 +10,7 @@
|
||||
namespace Flarum\Search\Database;
|
||||
|
||||
use Flarum\Search\SearchState;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class DatabaseSearchState extends SearchState
|
||||
{
|
||||
|
@ -30,6 +30,7 @@ class SettingsServiceProvider extends AbstractServiceProvider
|
||||
'search_driver_Flarum\Group\Group' => 'default',
|
||||
'search_driver_Flarum\Post\Post' => 'default',
|
||||
'search_driver_Flarum\Http\AccessToken' => 'default',
|
||||
'pgsql_search_configuration' => 'english',
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -13,7 +13,7 @@ use Flarum\Search\Database\DatabaseSearchState;
|
||||
use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\Search\ValidateFilterTrait;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
|
@ -15,7 +15,7 @@ use Flarum\Search\Filter\FilterInterface;
|
||||
use Flarum\Search\SearchState;
|
||||
use Flarum\Search\ValidateFilterTrait;
|
||||
use Flarum\User\User;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* @implements FilterInterface<DatabaseSearchState>
|
||||
@ -50,7 +50,7 @@ class GroupFilter implements FilterInterface
|
||||
|
||||
$groupQuery = Group::whereVisibleTo($actor)
|
||||
->join('group_user', 'groups.id', 'group_user.group_id')
|
||||
->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($ids, $names) {
|
||||
->where(function (Builder $query) use ($ids, $names) {
|
||||
$query->whereIn('groups.id', $ids)
|
||||
->orWhereIn($query->raw('lower(name_singular)'), $names)
|
||||
->orWhereIn($query->raw('lower(name_plural)'), $names);
|
||||
|
@ -367,7 +367,9 @@ class User extends AbstractModel
|
||||
public function getNewNotificationCount(): int
|
||||
{
|
||||
return $this->unreadNotifications()
|
||||
->where('created_at', '>', $this->read_notifications_at ?? 0)
|
||||
->when($this->read_notifications_at, function (Builder|HasMany $query) {
|
||||
$query->where('created_at', '>', $this->read_notifications_at);
|
||||
})
|
||||
->count();
|
||||
}
|
||||
|
||||
|
@ -85,7 +85,11 @@ class ListTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
$body = $response->getBody()->getContents();
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$data = json_decode($body, true)['data'];
|
||||
|
||||
// Order-independent comparison
|
||||
$this->assertEqualsCanonicalizing(['2', '3'], Arr::pluck($data, 'id'), 'IDs do not match');
|
||||
@ -123,7 +127,9 @@ class ListTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
$data = json_decode($body = $response->getBody()->getContents(), true)['data'] ?? null;
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
// Order-independent comparison
|
||||
$this->assertEquals(['3'], Arr::pluck($data, 'id'), 'IDs do not match');
|
||||
|
@ -85,7 +85,10 @@ class ListWithFulltextSearchTest extends TestCase
|
||||
])
|
||||
);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true);
|
||||
$data = json_decode($body = $response->getBody()->getContents(), true);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||
|
||||
$ids = array_map(function ($row) {
|
||||
return $row['id'];
|
||||
}, $data['data']);
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
namespace Flarum\Tests\integration\api\notifications;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Notification\Notification;
|
||||
use Flarum\Post\Post;
|
||||
@ -38,7 +39,7 @@ class UpdateTest extends TestCase
|
||||
['id' => 1, 'discussion_id' => 1, 'user_id' => 2, 'type' => 'comment', 'content' => 'Foo'],
|
||||
],
|
||||
Notification::class => [
|
||||
['id' => 1, 'user_id' => 2, 'from_user_id' => 1, 'type' => 'discussionRenamed', 'subject_id' => 1, 'read_at' => null],
|
||||
['id' => 1, 'user_id' => 2, 'from_user_id' => 1, 'type' => 'discussionRenamed', 'subject_id' => 1, 'read_at' => null, 'created_at' => Carbon::now()],
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
@ -30,19 +30,20 @@ class DeleteTest extends TestCase
|
||||
|
||||
$this->prepareDatabase([
|
||||
User::class => [
|
||||
$this->normalUser(),
|
||||
['id' => 3, 'username' => 'acme', 'email' => 'acme@machine.local', 'is_email_confirmed' => 1],
|
||||
['id' => 4, 'username' => 'acme2', 'email' => 'acme2@machine.local', 'is_email_confirmed' => 1],
|
||||
],
|
||||
Discussion::class => [
|
||||
['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 2, 'first_post_id' => 1, 'comment_count' => 5, 'last_post_number' => 5, 'last_post_id' => 10],
|
||||
['id' => 3, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 2, 'first_post_id' => 5, 'comment_count' => 5, 'last_post_number' => 5, 'last_post_id' => 10],
|
||||
],
|
||||
Post::class => [
|
||||
['id' => 5, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 1],
|
||||
['id' => 6, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 2],
|
||||
['id' => 7, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 3],
|
||||
['id' => 8, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 4],
|
||||
['id' => 9, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 5],
|
||||
['id' => 10, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 6],
|
||||
['id' => 5, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(2)->toDateTimeString(), 'user_id' => 2, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 1],
|
||||
['id' => 6, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(3)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 2],
|
||||
['id' => 7, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(4)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 3],
|
||||
['id' => 8, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(5)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 4],
|
||||
['id' => 9, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(6)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 5],
|
||||
['id' => 10, 'discussion_id' => 3, 'created_at' => Carbon::createFromDate(1975, 5, 21)->addMinutes(7)->toDateTimeString(), 'user_id' => 1, 'type' => 'comment', 'content' => '<t><p>foo bar</p></t>', 'number' => 6],
|
||||
],
|
||||
'discussion_user' => [
|
||||
['discussion_id' => 3, 'user_id' => 2, 'last_read_post_number' => 6],
|
||||
|
@ -46,7 +46,7 @@ class GroupSearchTest extends TestCase
|
||||
{
|
||||
$response = $this->createRequest(['admin'], 1);
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -89,7 +89,7 @@ class ListTest extends TestCase
|
||||
|
||||
$this->assertEquals(200, $response->getStatusCode());
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
$this->assertEquals(['1', '2'], Arr::pluck($data, 'id'));
|
||||
$this->assertEqualsCanonicalizing(['1', '2'], Arr::pluck($data, 'id'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
namespace Flarum\Tests\integration\extenders;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Flarum\Discussion\Discussion;
|
||||
use Flarum\Extend;
|
||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||
@ -38,6 +39,7 @@ class ModelPrivateTest extends TestCase
|
||||
$discussion = Discussion::create([
|
||||
'title' => 'Some Discussion',
|
||||
'user_id' => $user->id,
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
$this->assertNull($discussion->is_private);
|
||||
@ -62,10 +64,12 @@ class ModelPrivateTest extends TestCase
|
||||
$privateDiscussion = Discussion::create([
|
||||
'title' => 'Private Discussion',
|
||||
'user_id' => $user->id,
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
$publicDiscussion = Discussion::create([
|
||||
'title' => 'Public Discussion',
|
||||
'user_id' => $user->id,
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
$this->assertTrue($privateDiscussion->is_private);
|
||||
@ -89,10 +93,12 @@ class ModelPrivateTest extends TestCase
|
||||
$privateDiscussion = Discussion::create([
|
||||
'title' => 'Private Discussion',
|
||||
'user_id' => $user->id,
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
$publicDiscussion = Discussion::create([
|
||||
'title' => 'Public Discussion',
|
||||
'user_id' => $user->id,
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
$this->assertTrue($privateDiscussion->is_private);
|
||||
@ -122,10 +128,12 @@ class ModelPrivateTest extends TestCase
|
||||
$privateDiscussion = Discussion::create([
|
||||
'title' => 'Private Discussion',
|
||||
'user_id' => $user->id,
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
$publicDiscussion = Discussion::create([
|
||||
'title' => 'Public Discussion',
|
||||
'user_id' => $user->id,
|
||||
'created_at' => Carbon::now(),
|
||||
]);
|
||||
|
||||
$this->assertTrue($privateDiscussion->is_private);
|
||||
|
@ -13,9 +13,9 @@
|
||||
</div>
|
||||
|
||||
<div class="FormGroup">
|
||||
<div data-group="sqlite" style="display:none">
|
||||
<div data-group="sqlite,pgsql" style="display:none">
|
||||
<div class="Alert Alert--warning">
|
||||
<strong>Warning:</strong> Please keep in mind that while Flarum supports SQLite, not all ecosystem extensions do. If you're planning to install extensions, you should expect some of them to not work properly or at all.
|
||||
<strong>Warning:</strong> Please keep in mind that while Flarum supports SQLite and PostgreSQL, not all ecosystem extensions do. If you're planning to install extensions, you should expect some of them to not work properly or at all.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -25,6 +25,7 @@
|
||||
<label>Database Driver</label>
|
||||
<select class="FormControl" name="dbDriver">
|
||||
<option value="mysql">MySQL</option>
|
||||
<option value="pgsql">PostgreSQL</option>
|
||||
<option value="sqlite">SQLite</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -34,20 +35,20 @@
|
||||
<input class="FormControl" name="dbName" value="flarum">
|
||||
</div>
|
||||
|
||||
<div data-group="mysql">
|
||||
<div data-group="mysql,pgsql">
|
||||
<div class="FormField">
|
||||
<label>MySQL Host</label>
|
||||
<input class="FormControl" name="mysqlHost" value="localhost">
|
||||
<label>Host</label>
|
||||
<input class="FormControl" name="dbHost" value="localhost">
|
||||
</div>
|
||||
|
||||
<div class="FormField">
|
||||
<label>MySQL Username</label>
|
||||
<input class="FormControl" name="mysqlUsername">
|
||||
<label>Username</label>
|
||||
<input class="FormControl" name="dbUsername">
|
||||
</div>
|
||||
|
||||
<div class="FormField">
|
||||
<label>MySQL Password</label>
|
||||
<input class="FormControl" type="password" name="mysqlPassword">
|
||||
<label>Password</label>
|
||||
<input class="FormControl" type="password" name="dbPassword">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -93,7 +94,7 @@
|
||||
group.style.display = 'none';
|
||||
});
|
||||
|
||||
const groups = document.querySelectorAll('[data-group="' + this.value + '"]');
|
||||
const groups = document.querySelectorAll('[data-group*="' + this.value + '"]');
|
||||
|
||||
groups.forEach(function(group) {
|
||||
group.style.display = 'block';
|
||||
|
@ -40,7 +40,11 @@ class SetupScript
|
||||
{
|
||||
$this->driver = getenv('DB_DRIVER') ?: 'mysql';
|
||||
$this->host = getenv('DB_HOST') ?: 'localhost';
|
||||
$this->port = intval(getenv('DB_PORT') ?: 3306);
|
||||
$this->port = intval(getenv('DB_PORT') ?: match ($this->driver) {
|
||||
'mysql' => 3306,
|
||||
'pgsql' => 5432,
|
||||
default => 0,
|
||||
});
|
||||
$this->name = getenv('DB_DATABASE') ?: 'flarum_test';
|
||||
$this->user = getenv('DB_USERNAME') ?: 'root';
|
||||
$this->pass = getenv('DB_PASSWORD') ?? 'root';
|
||||
|
@ -201,6 +201,10 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
|
||||
*/
|
||||
$this->database()->getSchemaBuilder()->disableForeignKeyConstraints();
|
||||
|
||||
if ($this->database()->getDriverName() === 'pgsql') {
|
||||
$this->database()->statement("SET session_replication_role = 'replica'");
|
||||
}
|
||||
|
||||
$databaseContent = [];
|
||||
|
||||
foreach ($this->databaseContent as $tableOrModelClass => $_rows) {
|
||||
@ -224,6 +228,8 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
|
||||
}
|
||||
}
|
||||
|
||||
$tables = [];
|
||||
|
||||
// Then, insert all rows required for this test case.
|
||||
foreach ($databaseContent as $table => $data) {
|
||||
foreach ($data['rows'] as $row) {
|
||||
@ -238,9 +244,24 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase
|
||||
}
|
||||
|
||||
$this->database()->table($table)->updateOrInsert($unique, $row);
|
||||
|
||||
if (isset($row['id'])) {
|
||||
$tables[$table] = 'id';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->database()->getDriverName() === 'pgsql') {
|
||||
// PgSQL doesn't auto-increment the sequence when inserting the IDs manually.
|
||||
foreach ($tables as $table => $id) {
|
||||
$wrappedTable = $this->database()->getSchemaGrammar()->wrapTable($table);
|
||||
$seq = $this->database()->getSchemaGrammar()->wrapTable($table.'_'.$id.'_seq');
|
||||
$this->database()->statement("SELECT setval('$seq', (SELECT MAX($id) FROM $wrappedTable))");
|
||||
}
|
||||
|
||||
$this->database()->statement("SET session_replication_role = 'origin'");
|
||||
}
|
||||
|
||||
// And finally, turn on foreign key checks again.
|
||||
$this->database()->getSchemaBuilder()->enableForeignKeyConstraints();
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user